Skip to content

Commit 94a4a5c

Browse files
authored
Improve rounding in the number formatter (#56)
1 parent c39ae1f commit 94a4a5c

File tree

3 files changed

+87
-15
lines changed

3 files changed

+87
-15
lines changed

src/Data/Formatter/Number.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
// converts a number to a string of the nearest integer _without_ appending ".0" (like `show` for `Number`) or
3+
// clamping to +/- 2 billion (like when working with `Int`). This is important for performance compared to other
4+
// means of showing an integer potentially larger than +/- 2 billion.
5+
exports.showNumberAsInt = function (n) { return Math.round(n).toString(); }

src/Data/Formatter/Number.purs

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,18 @@ formatParser = do
8282
, abbreviations: isJust abbreviations
8383
}
8484

85-
85+
-- converts a number to a string of the nearest integer _without_ appending ".0" (like `show` for `Number`) or
86+
-- clamping to +/- 2 billion (like when working with `Int`). This is important for performance compared to other
87+
-- means of showing an integer potentially larger than +/- 2 billion.
88+
foreign import showNumberAsInt :: Number -> String
89+
90+
-- | Formats a number according to the format object provided.
91+
-- | Due to the nature of floating point numbers, may yield unpredictable results for extremely
92+
-- | large or extremely small numbers, such as numbers whose absolute values are ≥ 1e21 or ≤ 1e-21,
93+
-- | or when formatting with > 20 digits after the decimal place.
94+
-- | See [purescript-decimals](https://pursuit.purescript.org/packages/purescript-decimals/4.0.0)
95+
-- | for working with arbitrary precision decimals, which supports simple number
96+
-- | formatting for numbers that go beyond the precision available with `Number`.
8697
format Formatter Number String
8798
format (Formatter f) num =
8899
let
@@ -111,18 +122,20 @@ format (Formatter f) num =
111122
else
112123
let
113124
zeros = f.before - tens - one
114-
integer = Int.floor absed
115-
leftover = absed - Int.toNumber integer
116-
rounded = Int.round $ leftover * (Math.pow 10.0 (Int.toNumber f.after))
117-
roundedWithZeros =
118-
let roundedString = show rounded
119-
roundedLength = Str.length roundedString
120-
zeros' = repeat "0" (f.after - roundedLength)
121-
in zeros' <> roundedString
122-
shownNumber =
125+
factor = Math.pow 10.0 (Int.toNumber (max 0 f.after))
126+
rounded = Math.round (absed * factor) / factor
127+
integer = Math.floor rounded
128+
leftoverDecimal = rounded - integer
129+
leftover = Math.round $ leftoverDecimal * factor
130+
leftoverWithZeros =
131+
let leftoverString = showNumberAsInt leftover
132+
leftoverLength = Str.length leftoverString
133+
zeros' = repeat "0" (f.after - leftoverLength)
134+
in zeros' <> leftoverString
135+
shownInt =
123136
if f.comma
124-
then addCommas [] zero (Arr.reverse (CU.toCharArray (repeat "0" zeros <> show integer)))
125-
else repeat "0" zeros <> show integer
137+
then addCommas [] zero (Arr.reverse (CU.toCharArray (repeat "0" zeros <> showNumberAsInt integer)))
138+
else repeat "0" zeros <> showNumberAsInt integer
126139

127140
addCommas Array Char Int Array Char String
128141
addCommas acc counter input = case Arr.uncons input of
@@ -133,13 +146,13 @@ format (Formatter f) num =
133146
addCommas (Arr.cons ',' acc) zero input
134147
in
135148
(if num < zero then "-" else if num > zero && f.sign then "+" else "")
136-
<> shownNumber
149+
<> shownInt
137150
<> (if f.after < 1
138151
then ""
139152
else
140153
"."
141-
<> (if rounded == 0 then repeat "0" f.after else "")
142-
<> (if rounded > 0 then roundedWithZeros else ""))
154+
<> (if leftover == 0.0 then repeat "0" f.after else "")
155+
<> (if leftover > 0.0 then leftoverWithZeros else ""))
143156

144157

145158
unformat Formatter String Either String Number

test/src/Number.purs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,24 @@ numberTest = describe "Data.Formatter.Number" do
3636
["+02.12", "+13.12", "-02.12", "-13.12"]
3737
(\n → (format fmt3 <$> (unformat fmt3 n)) `shouldEqual` (Right n))
3838

39+
forAll (\{fmt: (Formatter fmt), input} -> "rounds up " <> show input <> " (" <> show fmt.after <> " digits)")
40+
"rounding"
41+
[ {fmt: fmt4, input: 1.99999, expected: "02"}
42+
, {fmt: fmt1, input: 1.99999, expected: "002.00"}
43+
, {fmt: fmt5, input: 1.99999, expected: "2.0000"}
44+
, {fmt: fmt1, input: 1.89999, expected: "001.90"}
45+
, {fmt: fmt5, input: 1.67899, expected: "1.6790"}
46+
, {fmt: fmt6, input: 12.9, expected: "13"}
47+
, {fmt: fmt7, input: 1.123456789012345678901234, expected: "1.1234567890123457"}
48+
, {fmt: fmt6, input: 12345678901234567.8901234, expected: "12,345,678,901,234,568"}
49+
, {fmt: fmt5, input: 123456789012.345678901234, expected: "123,456,789,012.3457"}
50+
]
51+
(\{fmt, input, expected} -> do
52+
format fmt input `shouldEqual` expected
53+
format fmt (negate input) `shouldEqual` ("-" <> expected)
54+
)
55+
56+
3957
fmt1 Formatter
4058
fmt1 = Formatter
4159
{ comma: false
@@ -63,6 +81,42 @@ fmt3 = Formatter
6381
, sign: true
6482
}
6583

84+
fmt4 Formatter
85+
fmt4 = Formatter
86+
{ comma: false
87+
, before: 2
88+
, after: 0
89+
, abbreviations: false
90+
, sign: false
91+
}
92+
93+
fmt5 Formatter
94+
fmt5 = Formatter
95+
{ comma: true
96+
, before: 1
97+
, after: 4
98+
, abbreviations: false
99+
, sign: false
100+
}
101+
102+
fmt6 Formatter
103+
fmt6 = Formatter
104+
{ comma: true
105+
, before: 1
106+
, after: -1
107+
, abbreviations: false
108+
, sign: false
109+
}
110+
111+
fmt7 Formatter
112+
fmt7 = Formatter
113+
{ comma: true
114+
, before: 1
115+
, after: 16
116+
, abbreviations: false
117+
, sign: false
118+
}
119+
66120
numberformatts Array { fmt Formatter, str String }
67121
numberformatts =
68122
[ { str: "000.00"

0 commit comments

Comments
 (0)