diff --git a/src/Data/Formatter/Number.js b/src/Data/Formatter/Number.js new file mode 100644 index 0000000..c70fe6b --- /dev/null +++ b/src/Data/Formatter/Number.js @@ -0,0 +1,5 @@ + +// converts a number to a string of the nearest integer _without_ appending ".0" (like `show` for `Number`) or +// clamping to +/- 2 billion (like when working with `Int`). This is important for performance compared to other +// means of showing an integer potentially larger than +/- 2 billion. +exports.showNumberAsInt = function (n) { return Math.round(n).toString(); } diff --git a/src/Data/Formatter/Number.purs b/src/Data/Formatter/Number.purs index b7bfca3..3187da1 100644 --- a/src/Data/Formatter/Number.purs +++ b/src/Data/Formatter/Number.purs @@ -82,7 +82,18 @@ formatParser = do , abbreviations: isJust abbreviations } - +-- converts a number to a string of the nearest integer _without_ appending ".0" (like `show` for `Number`) or +-- clamping to +/- 2 billion (like when working with `Int`). This is important for performance compared to other +-- means of showing an integer potentially larger than +/- 2 billion. +foreign import showNumberAsInt :: Number -> String + +-- | Formats a number according to the format object provided. +-- | Due to the nature of floating point numbers, may yield unpredictable results for extremely +-- | large or extremely small numbers, such as numbers whose absolute values are ≥ 1e21 or ≤ 1e-21, +-- | or when formatting with > 20 digits after the decimal place. +-- | See [purescript-decimals](https://pursuit.purescript.org/packages/purescript-decimals/4.0.0) +-- | for working with arbitrary precision decimals, which supports simple number +-- | formatting for numbers that go beyond the precision available with `Number`. format ∷ Formatter → Number → String format (Formatter f) num = let @@ -111,18 +122,20 @@ format (Formatter f) num = else let zeros = f.before - tens - one - integer = Int.floor absed - leftover = absed - Int.toNumber integer - rounded = Int.round $ leftover * (Math.pow 10.0 (Int.toNumber f.after)) - roundedWithZeros = - let roundedString = show rounded - roundedLength = Str.length roundedString - zeros' = repeat "0" (f.after - roundedLength) - in zeros' <> roundedString - shownNumber = + factor = Math.pow 10.0 (Int.toNumber (max 0 f.after)) + rounded = Math.round (absed * factor) / factor + integer = Math.floor rounded + leftoverDecimal = rounded - integer + leftover = Math.round $ leftoverDecimal * factor + leftoverWithZeros = + let leftoverString = showNumberAsInt leftover + leftoverLength = Str.length leftoverString + zeros' = repeat "0" (f.after - leftoverLength) + in zeros' <> leftoverString + shownInt = if f.comma - then addCommas [] zero (Arr.reverse (CU.toCharArray (repeat "0" zeros <> show integer))) - else repeat "0" zeros <> show integer + then addCommas [] zero (Arr.reverse (CU.toCharArray (repeat "0" zeros <> showNumberAsInt integer))) + else repeat "0" zeros <> showNumberAsInt integer addCommas ∷ Array Char → Int → Array Char → String addCommas acc counter input = case Arr.uncons input of @@ -133,13 +146,13 @@ format (Formatter f) num = addCommas (Arr.cons ',' acc) zero input in (if num < zero then "-" else if num > zero && f.sign then "+" else "") - <> shownNumber + <> shownInt <> (if f.after < 1 then "" else "." - <> (if rounded == 0 then repeat "0" f.after else "") - <> (if rounded > 0 then roundedWithZeros else "")) + <> (if leftover == 0.0 then repeat "0" f.after else "") + <> (if leftover > 0.0 then leftoverWithZeros else "")) unformat ∷ Formatter → String → Either String Number diff --git a/test/src/Number.purs b/test/src/Number.purs index 65e793c..1a0bac9 100644 --- a/test/src/Number.purs +++ b/test/src/Number.purs @@ -36,6 +36,24 @@ numberTest = describe "Data.Formatter.Number" do ["+02.12", "+13.12", "-02.12", "-13.12"] (\n → (format fmt3 <$> (unformat fmt3 n)) `shouldEqual` (Right n)) + forAll (\{fmt: (Formatter fmt), input} -> "rounds up " <> show input <> " (" <> show fmt.after <> " digits)") + "rounding" + [ {fmt: fmt4, input: 1.99999, expected: "02"} + , {fmt: fmt1, input: 1.99999, expected: "002.00"} + , {fmt: fmt5, input: 1.99999, expected: "2.0000"} + , {fmt: fmt1, input: 1.89999, expected: "001.90"} + , {fmt: fmt5, input: 1.67899, expected: "1.6790"} + , {fmt: fmt6, input: 12.9, expected: "13"} + , {fmt: fmt7, input: 1.123456789012345678901234, expected: "1.1234567890123457"} + , {fmt: fmt6, input: 12345678901234567.8901234, expected: "12,345,678,901,234,568"} + , {fmt: fmt5, input: 123456789012.345678901234, expected: "123,456,789,012.3457"} + ] + (\{fmt, input, expected} -> do + format fmt input `shouldEqual` expected + format fmt (negate input) `shouldEqual` ("-" <> expected) + ) + + fmt1 ∷ Formatter fmt1 = Formatter { comma: false @@ -63,6 +81,42 @@ fmt3 = Formatter , sign: true } +fmt4 ∷ Formatter +fmt4 = Formatter + { comma: false + , before: 2 + , after: 0 + , abbreviations: false + , sign: false + } + +fmt5 ∷ Formatter +fmt5 = Formatter + { comma: true + , before: 1 + , after: 4 + , abbreviations: false + , sign: false + } + +fmt6 ∷ Formatter +fmt6 = Formatter + { comma: true + , before: 1 + , after: -1 + , abbreviations: false + , sign: false + } + +fmt7 ∷ Formatter +fmt7 = Formatter + { comma: true + , before: 1 + , after: 16 + , abbreviations: false + , sign: false + } + numberformatts ∷ Array { fmt ∷ Formatter, str ∷ String } numberformatts = [ { str: "000.00"