From 64a507e2f19fc1ed9dab7f8123b334067d107533 Mon Sep 17 00:00:00 2001 From: Dimitri Sokolyuk Date: Sun, 21 May 2017 15:02:35 +0200 Subject: initial import --- equals.go | 8 ++++ equals_test.go | 41 +++++++++++++++++++ format.go | 76 +++++++++++++++++++++++++++++++++++ format_test.go | 122 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ round.go | 18 +++++++++ round_test.go | 38 ++++++++++++++++++ 6 files changed, 303 insertions(+) create mode 100644 equals.go create mode 100644 equals_test.go create mode 100644 format.go create mode 100644 format_test.go create mode 100644 round.go create mode 100644 round_test.go diff --git a/equals.go b/equals.go new file mode 100644 index 0000000..824077c --- /dev/null +++ b/equals.go @@ -0,0 +1,8 @@ +package float + +const epsilon = 1e-6 + +// Equals compares two float values +func Equals(a, b float64) bool { + return (a-b) < epsilon && (b-a) < epsilon +} diff --git a/equals_test.go b/equals_test.go new file mode 100644 index 0000000..93eabb7 --- /dev/null +++ b/equals_test.go @@ -0,0 +1,41 @@ +package float + +import ( + "fmt" + "testing" +) + +func TestEquals(t *testing.T) { + testCases := []struct { + a, b float64 + result bool + }{ + {0.0, 0.0, true}, + {0.0000001, 0.0000001, true}, + {0.00000011, -0.00000012, true}, + {-0.00000011, 0.00000012, true}, + {-0.00000011, -0.00000012, true}, + {0.0000001, 0.0000002, true}, + {0.0000002, 0.0000001, true}, + {0.000002, 0.000001, false}, + {0.000001, 0.000002, false}, + {0.00001, 0.00002, false}, + {0.00002, 0.00001, false}, + {0.0000101, 0.0000201, false}, + {0.0000101, 0.0000102, true}, + } + for _, tc := range testCases { + name := fmt.Sprintf("%v==%v", tc.a, tc.b) + t.Run(name, func(t *testing.T) { + if Equals(tc.a, tc.b) != tc.result { + t.Fail() + } + }) + } +} + +func BenchmarkEquals(b *testing.B) { + for i := 0; i < b.N; i++ { + Equals(1.0, 1.0) + } +} diff --git a/format.go b/format.go new file mode 100644 index 0000000..77f5457 --- /dev/null +++ b/format.go @@ -0,0 +1,76 @@ +package float + +import ( + "strconv" + "strings" +) + +func split(s string, n int) (ret []string) { + var pos int + if rest := len(s) % n; rest > 0 { + ret = append(ret, s[:rest]) + pos = rest + } + for pos < len(s) { + ret = append(ret, s[pos:pos+n]) + pos += n + } + return ret +} + +func fill(s string, n int) string { + if len(s) >= n { + return s[:n] + } + return s + strings.Repeat("0", n-len(s)) +} + +type Country struct { + Thousend, Decimal rune + Block, Fraction int +} + +const ( + Space = '\u0020' + Apostrophe = '\u0027' + Comma = '\u002c' + FullStop = '\u002e' + MiddleDot = '\u00b7' + ThinSpace = '\u2009' + NoBreakSpace = '\u202f' + DotAbove = '\u02d9' +) + +var ( + EN = Country{Space, FullStop, 3, 2} // SI EN + FR = Country{Space, Comma, 3, 2} // SI FR + US = Country{Comma, FullStop, 3, 2} + DE = Country{FullStop, Comma, 3, 2} + IR = Country{Space, MiddleDot, 3, 2} + CH = Country{Apostrophe, FullStop, 3, 2} + IT = Country{DotAbove, Comma, 3, 2} + CN = Country{Comma, FullStop, 4, 2} +) + +func (c Country) Format(v float64) string { + s := strconv.FormatFloat(v, 'f', -1, 64) + neg := strings.HasPrefix(s, "-") + if neg { + s = s[1:] + } + parts := strings.Split(s, ".") + if len(parts) == 1 { + parts = append(parts, "") + } + lhs := split(parts[0], c.Block) + rhs := fill(parts[1], c.Fraction) + s = strings.Join(lhs, string(c.Thousend)) + if neg { + s = "-" + s + } + return s + string(c.Decimal) + rhs +} + +func Format(v float64) string { + return FR.Format(v) +} diff --git a/format_test.go b/format_test.go new file mode 100644 index 0000000..bce622d --- /dev/null +++ b/format_test.go @@ -0,0 +1,122 @@ +package float + +import "testing" + +func cmp(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := 0; i < len(a); i++ { + if a[i] != b[i] { + return false + } + } + return true +} + +func TestSplit(t *testing.T) { + testCases := []struct { + s string + v []string + }{ + {"", []string{}}, + {"1", []string{"1"}}, + {"12", []string{"12"}}, + {"123", []string{"123"}}, + {"1234", []string{"1", "234"}}, + {"12345", []string{"12", "345"}}, + {"123456", []string{"123", "456"}}, + {"1234567", []string{"1", "234", "567"}}, + } + + for _, tc := range testCases { + t.Run(tc.s, func(t *testing.T) { + v := split(tc.s, 3) + if !cmp(v, tc.v) { + t.Errorf("got %q, want %q", v, tc.v) + } + }) + } +} + +func TestFill(t *testing.T) { + testCases := []struct { + s string + v string + }{ + {"", "00"}, + {"1", "10"}, + {"01", "01"}, + {"123", "12"}, + } + + for _, tc := range testCases { + t.Run(tc.s, func(t *testing.T) { + v := fill(tc.s, 2) + if v != tc.v { + t.Errorf("got %q, want %q", v, tc.v) + } + }) + } +} + +func TestFormat(t *testing.T) { + testCases := []struct { + f float64 + s string + }{ + {0.0, "0,00"}, + {1.0, "1,00"}, + {1000.0, "1 000,00"}, + {10000.0, "10 000,00"}, + {10000.10, "10 000,10"}, + {1234567.89, "1 234 567,89"}, + {-1234567.89, "-1 234 567,89"}, + {-1000.0, "-1 000,00"}, + {-100.0, "-100,00"}, + {-10.0, "-10,00"}, + {1.0 / 3, "0,33"}, + {0.555, "0,55"}, + } + + for _, tc := range testCases { + t.Run(tc.s, func(t *testing.T) { + s := Format(tc.f) + if s != tc.s { + t.Errorf("got %q, want %q", s, tc.s) + } + }) + } +} + +func BenchmarkFormat(b *testing.B) { + for i := 0; i < b.N; i++ { + Format(1234567.89) + } +} + +func TestCountry(t *testing.T) { + f := 1234567.89 + testCases := []struct { + f Country + v string + }{ + {EN, "1 234 567.89"}, + {FR, "1 234 567,89"}, + {US, "1,234,567.89"}, + {DE, "1.234.567,89"}, + {IR, "1 234 567·89"}, + {CH, "1'234'567.89"}, + {IT, "1˙234˙567,89"}, + {CN, "123,4567.89"}, + } + + for _, tc := range testCases { + t.Run(tc.v, func(t *testing.T) { + v := tc.f.Format(f) + if v != tc.v { + t.Errorf("got %q, want %q", v, tc.v) + } + }) + } +} diff --git a/round.go b/round.go new file mode 100644 index 0000000..89725f2 --- /dev/null +++ b/round.go @@ -0,0 +1,18 @@ +package float + +import "math" + +// ISO 80000-1:2012 +// q = \sgn(y) \left\lfloor \left| y \right| + 0.5 \right\rfloor +// = -\sgn(y) \left\lceil -\left| y \right| - 0.5 \right\rceil + +func round(v float64, places int) float64 { + scale := math.Pow(10, float64(places)) + abs := math.Abs(v*scale) + 0.5 + return math.Copysign(math.Floor(abs)/scale, v) +} + +// Round a float value to 2 decimal places +func Round(v float64) float64 { + return round(v, 2) +} diff --git a/round_test.go b/round_test.go new file mode 100644 index 0000000..adabd96 --- /dev/null +++ b/round_test.go @@ -0,0 +1,38 @@ +package float + +import ( + "fmt" + "testing" +) + +func TestRound(t *testing.T) { + testCases := []struct { + in, out float64 + }{ + {0.0, 0.0}, + {0.3, 0.3}, + {0.333, 0.33}, + {0.334, 0.33}, + {0.335, 0.34}, + {-0.333, -0.33}, + {-0.334, -0.33}, + {-0.335, -0.34}, + {495.17999999999995, 495.18}, + {-495.17999999999995, -495.18}, + {0.115, 0.12}, + {-0.115, -0.12}, + } + for _, tc := range testCases { + t.Run(fmt.Sprint(tc.in), func(t *testing.T) { + if r := Round(tc.in); r != tc.out { + t.Errorf("got %v, want %v", r, tc.out) + } + }) + } +} + +func BenchmarkRound(b *testing.B) { + for i := 0; i < b.N; i++ { + Round(0.333) + } +} -- cgit v1.2.3