Skip to content

round up 1's digit in the number formatter #56

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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