|
6 | 6 | package cast |
7 | 7 |
|
8 | 8 | import ( |
| 9 | + "regexp" |
9 | 10 | "strconv" |
| 11 | + "strings" |
10 | 12 | "testing" |
11 | 13 |
|
12 | 14 | qt "github.com/frankban/quicktest" |
@@ -45,42 +47,136 @@ func TestTrimZeroDecimal(t *testing.T) { |
45 | 47 | } |
46 | 48 |
|
47 | 49 | func TestTrimDecimal(t *testing.T) { |
48 | | - c := qt.New(t) |
| 50 | + testCases := []struct { |
| 51 | + input string |
| 52 | + expected string |
| 53 | + }{ |
| 54 | + {"10.0", "10"}, |
| 55 | + {"10.010", "10"}, |
| 56 | + {"00000.00001", "00000"}, |
| 57 | + {"-0001.0", "-0001"}, |
| 58 | + {".5", "0"}, |
| 59 | + {"+12.", "+12"}, |
| 60 | + {"+.25", "+0"}, |
| 61 | + {"-.25", "-0"}, |
| 62 | + {"0.0000000000", "0"}, |
| 63 | + {"0.0000000001", "0"}, |
| 64 | + {"10.0000000000", "10"}, |
| 65 | + {"10.0000000001", "10"}, |
| 66 | + {"10000000000000.0000000000", "10000000000000"}, |
| 67 | + |
| 68 | + {"10...17", "10...17"}, |
| 69 | + {"10.foobar", "10.foobar"}, |
| 70 | + {"10.0i", "10.0i"}, |
| 71 | + {"10.0E9", "10.0E9"}, |
| 72 | + } |
49 | 73 |
|
50 | | - c.Assert(trimDecimal("10.0"), qt.Equals, "10") |
51 | | - c.Assert(trimDecimal("10.00"), qt.Equals, "10") |
52 | | - c.Assert(trimDecimal("10.010"), qt.Equals, "10") |
53 | | - c.Assert(trimDecimal("0.0000000000"), qt.Equals, "0") |
54 | | - c.Assert(trimDecimal("0.00000000001"), qt.Equals, "0") |
| 74 | + for _, testCase := range testCases { |
| 75 | + // TODO: remove after minimum Go version is >=1.22 |
| 76 | + testCase := testCase |
| 77 | + |
| 78 | + t.Run(testCase.input, func(t *testing.T) { |
| 79 | + c := qt.New(t) |
| 80 | + |
| 81 | + c.Assert(trimDecimal(testCase.input), qt.Equals, testCase.expected) |
| 82 | + }) |
| 83 | + } |
55 | 84 | } |
56 | 85 |
|
| 86 | +// Analysis (in the order of performance): |
| 87 | +// |
| 88 | +// - Trimming decimals based on decimal point yields a lot of incorrectly parsed values. |
| 89 | +// - Parsing to float might be better, but we still need to cast the number, it might overflow, problematic. |
| 90 | +// - Regex parsing is an order of magnitude slower, but it yields correct results. |
57 | 91 | func BenchmarkDecimal(b *testing.B) { |
58 | | - testCases := []string{"10.0", "10.00", "10.010", "0.0000000000", "0.0000000001", "10.0000000000", "10.0000000001", "10000000000000.0000000000"} |
| 92 | + testCases := []struct { |
| 93 | + input string |
| 94 | + expectError bool |
| 95 | + }{ |
| 96 | + {"10.0", false}, |
| 97 | + {"10.00", false}, |
| 98 | + {"10.010", false}, |
| 99 | + {"0.0000000000", false}, |
| 100 | + {"0.0000000001", false}, |
| 101 | + {"10.0000000000", false}, |
| 102 | + {"10.0000000001", false}, |
| 103 | + {"10000000000000.0000000000", false}, |
| 104 | + |
| 105 | + // {"10...17", true}, |
| 106 | + // {"10.foobar", true}, |
| 107 | + // {"10.0i", true}, |
| 108 | + // {"10.0E9", true}, |
| 109 | + } |
| 110 | + |
| 111 | + trimDecimalString := func(s string) string { |
| 112 | + // trim the decimal part (if any) |
| 113 | + if i := strings.Index(s, "."); i >= 0 { |
| 114 | + s = s[:i] |
| 115 | + } |
| 116 | + |
| 117 | + return s |
| 118 | + } |
| 119 | + |
| 120 | + re := regexp.MustCompile(`^([-+]?\d*)(\.\d*)?$`) |
| 121 | + |
| 122 | + trimDecimalRegex := func(s string) string { |
| 123 | + matches := re.FindStringSubmatch(s) |
| 124 | + if matches != nil { |
| 125 | + // matches[1] is the captured integer part with sign |
| 126 | + return matches[1] |
| 127 | + } |
| 128 | + |
| 129 | + return s |
| 130 | + } |
59 | 131 |
|
60 | 132 | for _, testCase := range testCases { |
61 | 133 | // TODO: remove after minimum Go version is >=1.22 |
62 | 134 | testCase := testCase |
63 | 135 |
|
64 | | - b.Run(testCase, func(b *testing.B) { |
| 136 | + b.Run(testCase.input, func(b *testing.B) { |
65 | 137 | b.Run("ParseFloat", func(b *testing.B) { |
66 | 138 | // TODO: use b.Loop() once updated to Go 1.24 |
67 | 139 | for i := 0; i < b.N; i++ { |
68 | | - v, err := strconv.ParseFloat(testCase, 64) |
69 | | - if err != nil { |
70 | | - b.Fatal(err) |
| 140 | + v, err := strconv.ParseFloat(testCase.input, 64) |
| 141 | + if (err != nil) != testCase.expectError { |
| 142 | + if err != nil { |
| 143 | + b.Fatal(err) |
| 144 | + } |
| 145 | + |
| 146 | + b.Fatal("expected error, but got none") |
71 | 147 | } |
72 | 148 |
|
73 | 149 | n := int64(v) |
74 | 150 | _ = n |
75 | 151 | } |
76 | 152 | }) |
77 | 153 |
|
78 | | - b.Run("TrimDecimal", func(b *testing.B) { |
| 154 | + b.Run("TrimDecimalString", func(b *testing.B) { |
| 155 | + // TODO: use b.Loop() once updated to Go 1.24 |
| 156 | + for i := 0; i < b.N; i++ { |
| 157 | + v, err := strconv.ParseInt(trimDecimalString(testCase.input), 0, 0) |
| 158 | + if (err != nil) != testCase.expectError { |
| 159 | + if err != nil { |
| 160 | + b.Fatal(err) |
| 161 | + } |
| 162 | + |
| 163 | + b.Fatal("expected error, but got none") |
| 164 | + } |
| 165 | + |
| 166 | + _ = v |
| 167 | + } |
| 168 | + }) |
| 169 | + |
| 170 | + b.Run("TrimDecimalRegex", func(b *testing.B) { |
79 | 171 | // TODO: use b.Loop() once updated to Go 1.24 |
80 | 172 | for i := 0; i < b.N; i++ { |
81 | | - v, err := strconv.ParseInt(trimDecimal(testCase), 0, 0) |
82 | | - if err != nil { |
83 | | - b.Fatal(err) |
| 173 | + v, err := strconv.ParseInt(trimDecimalRegex(testCase.input), 0, 0) |
| 174 | + if (err != nil) != testCase.expectError { |
| 175 | + if err != nil { |
| 176 | + b.Fatal(err) |
| 177 | + } |
| 178 | + |
| 179 | + b.Fatal("expected error, but got none") |
84 | 180 | } |
85 | 181 |
|
86 | 182 | _ = v |
|
0 commit comments