summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-11-25 22:32:54 +0200
committerPaul Buetow <paul@buetow.org>2025-11-25 22:32:54 +0200
commitb97e4b19ec9c415cd5f3d204e23e5fde5180db26 (patch)
treeaf324415c9e0e71724d4a920a62cc4295877dbc0 /internal
Initial commit: perc v0.0.0 - percentage calculatorv0.0.0
Amp-Thread-ID: https://ampcode.com/threads/T-e4f4a959-8cc6-4ac0-b6fb-2779867e8b2a Co-authored-by: Amp <amp@ampcode.com>
Diffstat (limited to 'internal')
-rw-r--r--internal/calculator/calculator.go81
-rw-r--r--internal/calculator/calculator_test.go267
-rw-r--r--internal/version.go3
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"