summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDimitri Sokolyuk <demon@dim13.org>2017-05-21 15:02:35 +0200
committerDimitri Sokolyuk <demon@dim13.org>2017-05-21 15:02:35 +0200
commit64a507e2f19fc1ed9dab7f8123b334067d107533 (patch)
tree28d554b5e5539f76632699bcffe559445eae17d0
initial import
-rw-r--r--equals.go8
-rw-r--r--equals_test.go41
-rw-r--r--format.go76
-rw-r--r--format_test.go122
-rw-r--r--round.go18
-rw-r--r--round_test.go38
6 files changed, 303 insertions, 0 deletions
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)
+ }
+}