Skip to content
5 changes: 5 additions & 0 deletions src/Data/Formatter/Number.js
Original file line number Diff line number Diff line change
@@ -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(); }
43 changes: 28 additions & 15 deletions src/Data/Formatter/Number.purs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
54 changes: 54 additions & 0 deletions test/src/Number.purs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down