diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/calculator/calculator.go | 81 | ||||
| -rw-r--r-- | internal/calculator/calculator_test.go | 267 | ||||
| -rw-r--r-- | internal/version.go | 3 |
3 files changed, 351 insertions, 0 deletions
diff --git a/internal/calculator/calculator.go b/internal/calculator/calculator.go new file mode 100644 index 0000000..ee1ff6f --- /dev/null +++ b/internal/calculator/calculator.go @@ -0,0 +1,81 @@ +package calculator + +import ( + "fmt" + "regexp" + "strconv" + "strings" +) + +func Parse(input string) (string, error) { + input = strings.ToLower(strings.TrimSpace(input)) + input = strings.ReplaceAll(input, "what is ", "") + input = strings.TrimSpace(input) + + if result, ok := parseXPercentOfY(input); ok { + return result, nil + } + + if result, ok := parseXIsWhatPercentOfY(input); ok { + return result, nil + } + + if result, ok := parseXIsYPercentOfWhat(input); ok { + return result, nil + } + + return "", fmt.Errorf("unable to parse input. See usage for examples") +} + +func parseXPercentOfY(input string) (string, bool) { + re := regexp.MustCompile(`^(\d+(?:\.\d+)?)\s*%\s*(?:of\s+)?(\d+(?:\.\d+)?)$`) + matches := re.FindStringSubmatch(input) + + if matches == nil { + return "", false + } + + percent, _ := strconv.ParseFloat(matches[1], 64) + base, _ := strconv.ParseFloat(matches[2], 64) + + result := (percent / 100.0) * base + return fmt.Sprintf("%.2f%% of %.2f = %.2f", percent, base, result), true +} + +func parseXIsWhatPercentOfY(input string) (string, bool) { + re := regexp.MustCompile(`^(\d+(?:\.\d+)?)\s+is\s+what\s*%\s*(?:of\s+)?(\d+(?:\.\d+)?)$`) + matches := re.FindStringSubmatch(input) + + if matches == nil { + return "", false + } + + part, _ := strconv.ParseFloat(matches[1], 64) + whole, _ := strconv.ParseFloat(matches[2], 64) + + if whole == 0 { + return "", false + } + + percent := (part / whole) * 100.0 + return fmt.Sprintf("%.2f is %.2f%% of %.2f", part, percent, whole), true +} + +func parseXIsYPercentOfWhat(input string) (string, bool) { + re := regexp.MustCompile(`^(\d+(?:\.\d+)?)\s+is\s+(\d+(?:\.\d+)?)\s*%\s*(?:of\s+)?what$`) + matches := re.FindStringSubmatch(input) + + if matches == nil { + return "", false + } + + part, _ := strconv.ParseFloat(matches[1], 64) + percent, _ := strconv.ParseFloat(matches[2], 64) + + if percent == 0 { + return "", false + } + + whole := (part / percent) * 100.0 + return fmt.Sprintf("%.2f is %.2f%% of %.2f", part, percent, whole), true +} diff --git a/internal/calculator/calculator_test.go b/internal/calculator/calculator_test.go new file mode 100644 index 0000000..defbd13 --- /dev/null +++ b/internal/calculator/calculator_test.go @@ -0,0 +1,267 @@ +package calculator + +import ( + "strings" + "testing" +) + +func TestParseXPercentOfY(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "20% of 150", + input: "20% of 150", + expected: "20.00% of 150.00 = 30.00", + }, + { + name: "what is 20% of 150", + input: "what is 20% of 150", + expected: "20.00% of 150.00 = 30.00", + }, + { + name: "50% of 200", + input: "50% of 200", + expected: "50.00% of 200.00 = 100.00", + }, + { + name: "decimal percent", + input: "12.5% of 80", + expected: "12.50% of 80.00 = 10.00", + }, + { + name: "decimal base", + input: "20% of 75.5", + expected: "20.00% of 75.50 = 15.10", + }, + { + name: "without 'of'", + input: "25% 400", + expected: "25.00% of 400.00 = 100.00", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := Parse(tt.input) + if err != nil { + t.Fatalf("Parse(%q) returned error: %v", tt.input, err) + } + if result != tt.expected { + t.Errorf("Parse(%q) = %q, expected %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestParseXIsWhatPercentOfY(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "30 is what % of 150", + input: "30 is what % of 150", + expected: "30.00 is 20.00% of 150.00", + }, + { + name: "50 is what % of 200", + input: "50 is what % of 200", + expected: "50.00 is 25.00% of 200.00", + }, + { + name: "decimal values", + input: "12.5 is what % of 50", + expected: "12.50 is 25.00% of 50.00", + }, + { + name: "without spaces around %", + input: "75 is what% of 300", + expected: "75.00 is 25.00% of 300.00", + }, + { + name: "without 'of'", + input: "100 is what % 400", + expected: "100.00 is 25.00% of 400.00", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := Parse(tt.input) + if err != nil { + t.Fatalf("Parse(%q) returned error: %v", tt.input, err) + } + if result != tt.expected { + t.Errorf("Parse(%q) = %q, expected %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestParseXIsYPercentOfWhat(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "30 is 20% of what", + input: "30 is 20% of what", + expected: "30.00 is 20.00% of 150.00", + }, + { + name: "50 is 25% of what", + input: "50 is 25% of what", + expected: "50.00 is 25.00% of 200.00", + }, + { + name: "decimal values", + input: "15 is 30% of what", + expected: "15.00 is 30.00% of 50.00", + }, + { + name: "without spaces around %", + input: "75 is 25% of what", + expected: "75.00 is 25.00% of 300.00", + }, + { + name: "without 'of'", + input: "40 is 20% what", + expected: "40.00 is 20.00% of 200.00", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := Parse(tt.input) + if err != nil { + t.Fatalf("Parse(%q) returned error: %v", tt.input, err) + } + if result != tt.expected { + t.Errorf("Parse(%q) = %q, expected %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestParseErrors(t *testing.T) { + tests := []struct { + name string + input string + }{ + { + name: "invalid input", + input: "hello world", + }, + { + name: "incomplete input", + input: "20%", + }, + { + name: "missing numbers", + input: "% of", + }, + { + name: "random text", + input: "calculate percentage", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := Parse(tt.input) + if err == nil { + t.Errorf("Parse(%q) expected error, got nil", tt.input) + } + }) + } +} + +func TestParseCaseInsensitive(t *testing.T) { + tests := []struct { + name string + input string + }{ + { + name: "uppercase WHAT IS", + input: "WHAT IS 20% OF 150", + }, + { + name: "mixed case What Is", + input: "What Is 20% Of 150", + }, + { + name: "uppercase IS WHAT", + input: "30 IS WHAT % OF 150", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := Parse(tt.input) + if err != nil { + t.Errorf("Parse(%q) should be case-insensitive, got error: %v", tt.input, err) + } + }) + } +} + +func TestParseDivisionByZero(t *testing.T) { + tests := []struct { + name string + input string + }{ + { + name: "X is what % of 0", + input: "30 is what % of 0", + }, + { + name: "X is 0% of what", + input: "30 is 0% of what", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := Parse(tt.input) + if err == nil { + t.Errorf("Parse(%q) should handle division by zero, expected error", tt.input) + } + }) + } +} + +func TestParseWhitespace(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "extra spaces", + input: " 20% of 150 ", + expected: "20.00% of 150.00 = 30.00", + }, + { + name: "tabs and spaces", + input: "30 is what % of 150", + expected: "30.00 is 20.00% of 150.00", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := Parse(tt.input) + if err != nil { + t.Fatalf("Parse(%q) returned error: %v", tt.input, err) + } + if !strings.Contains(result, "of") { + t.Errorf("Parse(%q) should handle whitespace properly, got %q", tt.input, result) + } + }) + } +} diff --git a/internal/version.go b/internal/version.go new file mode 100644 index 0000000..3fd5dc6 --- /dev/null +++ b/internal/version.go @@ -0,0 +1,3 @@ +package internal + +const Version = "v0.0.0" |
