git.iko.soy/rowtype-yoga/purescript-fixed-precision
on main help sign in
Merge pull request #10 from lumihq/add-string-conversions

Add safe conversions to/from String

commit1f9d65e82fe2e6531787d7307dc82f6f1187ed48 authorHarry Garrood <harry@garrood.me> dateThu 31 Jan 2019, 20:24 (8 years ago) parentda8ca3b4c27e34
3 files changed+292−35
bower.json+3−1 generated-docs/Data/Fixed.md+128−32 src/Data/Fixed.purs+161−2
bower.json+3−1
@@ -14,7 +14,9 @@
1414 "dependencies": {
1515 "purescript-integers": "^4.0.0",
1616 "purescript-maybe": "^4.0.0",
17− "purescript-bigints": "^4.0.0"
17+ "purescript-bigints": "^4.0.0",
18+ "purescript-strings": "^4.0.1",
19+ "purescript-math": "^2.1.1"
1820 },
1921 "devDependencies": {
2022 "purescript-psci-support": "^4.0.0",
generated-docs/Data/Fixed.md+128−32
@@ -46,11 +46,21 @@ fromNumber :: forall precision. KnownPrecision precision => Number -> Maybe (Fix
4646Approximate a `Number` as a `Fixed` value with the specified precision.
4747
4848```
49−> fromNumber 0.1234 :: Fixed P10000
50−fromNumber 0.1234 :: P100
49+> fromNumber 0.1234 :: Maybe (Fixed P10000)
50+(Just (fromNumber 0.1234 :: P10000))
5151
52−> fromNumber 0.1234 :: Fixed P100
53−fromNumber 0.12 :: P100
52+> fromNumber 0.1234 :: Maybe (Fixed P100)
53+(Just (fromNumber 0.12 :: P100))
54+```
55+
56+When given a finite `Number`, this function always succeeds: the number is
57+truncated (rounded towards zero) to the closest possible `Fixed` value.
58+This function only returns `Nothing` if it is given NaN, or positive or
59+negative infinity.
60+
61+```
62+> fromNumber (1.0 / 0.0) :: Maybe (Fixed P100)
63+Nothing
5464```
5565
5666#### `toNumber`
@@ -62,6 +72,73 @@ toNumber :: forall precision. KnownPrecision precision => Fixed precision -> Num
6272Convert a `Fixed` value to a `Number`.
6373
6474_Note_: Overflow is possible here if the numerator is sufficiently large.
75+Consider using `toString` instead.
76+
77+#### `fromString`
78+
79+``` purescript
80+fromString :: forall precision. KnownPrecision precision => String -> Maybe (Fixed precision)
81+```
82+
83+Parse a fixed-precision number from a string. Any decimal digits which are
84+not representable in the specified precision will be ignored.
85+
86+```
87+> fromString "123.456" :: Maybe (Fixed P1000)
88+(Just (fromString "123.456" :: P1000))
89+```
90+
91+Where possible, this function should be preferred over `fromNumber`, since
92+it is exact (whereas `fromNumber` can only provide an approximation for
93+larger inputs).
94+
95+```
96+> fromString "9007199254740992.5" :: Maybe (Fixed P10)
97+(Just (fromString "9007199254740992.5" :: P10))
98+
99+> fromNumber 9007199254740992.5 :: Maybe (Fixed P10)
100+(Just (fromString "9007199254740992.0" :: P10))
101+```
102+
103+
104+#### `toString`
105+
106+``` purescript
107+toString :: forall precision. KnownPrecision precision => Fixed precision -> String
108+```
109+
110+Represent a `Fixed` value as a string, using all of the decimal places it
111+can represent (based on its precision).
112+
113+```
114+> map toString (fromString "100.5" :: Maybe (Fixed P10))
115+(Just "100.5")
116+
117+> map toString (fromString "100.5" :: Maybe (Fixed P100))
118+(Just "100.50")
119+```
120+
121+#### `toStringWithPrecision`
122+
123+``` purescript
124+toStringWithPrecision :: forall precision. KnownPrecision precision => Int -> Fixed precision -> String
125+```
126+
127+Represent a `Fixed` value as a string, with the given number of decimal
128+places.
129+
130+```
131+> map (toStringWithPrecision 2) (fromString "1234.567" :: Maybe (Fixed P1000))
132+(Just "1234.56")
133+```
134+
135+If more decimal places are asked for than the type can provide, the extra
136+decimal places will be provided as zeroes.
137+
138+```
139+> map (toStringWithPrecision 3) (fromString "1234.5" :: Maybe (Fixed P10))
140+(Just "1234.500")
141+```
65142
66143#### `numerator`
67144
@@ -72,11 +149,11 @@ numerator :: forall precision. Fixed precision -> BigInt
72149Extract the numerator from the representation of the number as a fraction.
73150
74151```
75−> numerator (fromNumber 0.1234 :: Fixed P1000)
76−fromString "123"
152+> map numerator (fromNumber 0.1234 :: Fixed P1000)
153+(Just fromString "123")
77154
78−> numerator (fromNumber 0.1239 :: Fixed P1000)
79−fromString "123"
155+> map numerator (fromNumber 0.1239 :: Fixed P1000)
156+(Just fromString "123")
80157```
81158
82159#### `floor`
@@ -89,14 +166,14 @@ Calculate the largest whole number smaller than or equal to the provided
89166value.
90167
91168```
92−> floor $ fromNumber 0.1 :: Fixed P10
93−fromNumber 0.0 :: P10
169+> map floor $ fromNumber 0.1 :: Maybe (Fixed P10)
170+(Just (fromNumber 0.0 :: P10))
94171
95−> floor $ fromNumber 1.0 :: Fixed P10
96−fromNumber 1.0 :: P10
172+> map floor $ fromNumber 1.0 :: Maybe (Fixed P10)
173+(Just (fromNumber 1.0 :: P10))
97174
98−> floor $ fromNumber (-0.1) :: Fixed P10
99−fromNumber (-1.0) :: P10
175+> floor $ fromNumber (-0.1) :: Maybe (Fixed P10)
176+(Just (fromNumber (-1.0) :: P10))
100177```
101178
102179#### `ceil`
@@ -109,14 +186,14 @@ Calculate the smallest whole number greater than or equal to the provided
109186value.
110187
111188```
112−> ceil $ fromNumber 0.1 :: Fixed P10
113−fromNumber 1.0 :: P10
189+> map ceil $ fromNumber 0.1 :: Maybe (Fixed P10)
190+(Just (fromNumber 1.0 :: P10))
114191
115−> ceil $ fromNumber 1.0 :: Fixed P10
116−fromNumber 1.0 :: P10
192+> map ceil $ fromNumber 1.0 :: Maybe (Fixed P10)
193+(Just (fromNumber 1.0 :: P10))
117194
118−> ceil $ fromNumber (-0.1) :: Fixed P10
119−fromNumber 0.0 :: P10
195+> map ceil $ fromNumber (-0.1) :: Maybe (Fixed P10)
196+(Just (fromNumber 0.0 :: P10))
120197```
121198
122199#### `round`
@@ -128,17 +205,17 @@ round :: forall precision. KnownPrecision precision => Fixed precision -> Fixed
128205Round the specified value to the nearest whole number.
129206
130207```
131−> round $ fromNumber 0.1 :: Fixed P10
132−fromNumber 0.0 :: P10
208+> map round $ fromNumber 0.1 :: Maybe (Fixed P10)
209+(Just (fromNumber 0.0 :: P10))
133210
134−> round $ fromNumber 0.9 :: Fixed P10
135−fromNumber 1.0 :: P10
211+> map round $ fromNumber 0.9 :: Maybe (Fixed P10)
212+(Just (fromNumber 1.0 :: P10))
136213
137−> round $ fromNumber 0.5 :: Fixed P10
138−fromNumber 1.0 :: P10
214+> map round $ fromNumber 0.5 :: Maybe (Fixed P10)
215+(Just (fromNumber 1.0 :: P10))
139216
140−> round $ fromNumber (-0.1) :: Fixed P10
141−fromNumber 0.0 :: P10
217+> map round $ fromNumber (-0.1) :: Maybe (Fixed P10)
218+(Just (fromNumber 0.0 :: P10))
142219```
143220
144221#### `approxDiv`
@@ -150,16 +227,16 @@ approxDiv :: forall precision. KnownPrecision precision => Fixed precision -> Fi
150227Approximate division of fixed-precision numbers.
151228
152229```
153−> fromNumber 22.0 `approxDiv` fromNumber 7.0 :: Fixed P100
154−fromNumber 3.14 :: P100
230+> lift2 approxDiv (fromNumber 22.0) (fromNumber 7.0) :: Maybe (Fixed P100)
231+(Just (fromNumber 3.14 :: P100))
155232```
156233
157234_Note_: `Fixed` is not a `EuclideanRing` in general - it is not even
158235an integral domain, since it has non-zero zero-divisors:
159236
160237```
161−> fromNumber 0.1 * fromNumber 0.1 :: Fixed P10
162−fromNumber 0.0 :: P10
238+> lift2 (*) (fromNumber 0.1) (fromNumber 0.1) :: Maybe (Fixed P10)
239+(Just (fromNumber 0.0 :: P10))
163240```
164241
165242#### `Precision`
@@ -272,12 +349,31 @@ value at runtime, given a `PProxy`.
272349`reflectPrecision` returns a multiple of ten, corresponding
273350to the maximum number of decimal places which can be stored.
274351
352+```
353+> reflectPrecision (PProxy :: PProxy P1000)
354+1000
355+```
356+
275357##### Instances
276358``` purescript
277359KnownPrecision One
278360(KnownPrecision p) => KnownPrecision (TenTimes p)
279361```
280362
363+#### `reflectPrecisionDecimalPlaces`
364+
365+``` purescript
366+reflectPrecisionDecimalPlaces :: forall precision. KnownPrecision precision => PProxy precision -> Int
367+```
368+
369+Get the number of decimal places associated with a given `Precision` at
370+the value level.
371+
372+```
373+> reflectPrecisionDecimalPlaces (PProxy :: PProxy P1000)
374+3
375+```
376+
281377#### `reifyPrecision`
282378
283379``` purescript
src/Data/Fixed.purs+161−2
@@ -7,6 +7,9 @@ module Data.Fixed
77 , fromInt
88 , fromNumber
99 , toNumber
10+ , fromString
11+ , toString
12+ , toStringWithPrecision
1013 , numerator
1114 , floor
1215 , ceil
@@ -25,13 +28,20 @@ module Data.Fixed
2528 , PProxy(..)
2629 , class KnownPrecision
2730 , reflectPrecision
31+ , reflectPrecisionDecimalPlaces
2832 , reifyPrecision
2933 ) where
3034
3135import Prelude
36+
37+import Control.MonadZero (guard)
38+import Data.Array (replicate)
3239import Data.BigInt as BigInt
3340import Data.Int as Int
3441import Data.Maybe (Maybe(..))
42+import Data.Monoid as Monoid
43+import Data.String.CodeUnits as StringCU
44+import Math as Math
3545
3646-- | A kind for type-level precision information
3747foreign import kind Precision
@@ -70,6 +80,11 @@ data PProxy (precision :: Precision) = PProxy
7080-- |
7181-- | `reflectPrecision` returns a multiple of ten, corresponding
7282-- | to the maximum number of decimal places which can be stored.
83+-- |
84+-- | ```
85+-- | > reflectPrecision (PProxy :: PProxy P1000)
86+-- | 1000
87+-- | ```
7388class KnownPrecision (precision :: Precision) where
7489 reflectPrecision :: PProxy precision -> Int
7590
@@ -79,6 +94,25 @@ instance knownPrecisionOne :: KnownPrecision One where
7994instance knownPrecisionTenTimes :: KnownPrecision p => KnownPrecision (TenTimes p) where
8095 reflectPrecision _ = 10 * reflectPrecision (PProxy :: PProxy p)
8196
97+-- | Get the number of decimal places associated with a given `Precision` at
98+-- | the value level.
99+-- |
100+-- | ```
101+-- | > reflectPrecisionDecimalPlaces (PProxy :: PProxy P1000)
102+-- | 3
103+-- | ```
104+reflectPrecisionDecimalPlaces
105+ :: forall precision
106+ . KnownPrecision precision
107+ => PProxy precision
108+ -> Int
109+reflectPrecisionDecimalPlaces _ =
110+ let
111+ p = reflectPrecision (PProxy :: PProxy precision)
112+ in
113+ Int.round (Math.log (Int.toNumber p) / Math.ln10)
114+
115+
82116-- | Reify an non-negative integer (a power of ten) as a `Precision`.
83117-- |
84118-- | For example
@@ -162,6 +196,7 @@ fromNumber n = map Fixed (BigInt.fromNumber (n * Int.toNumber (reflectPrecision
162196-- | Convert a `Fixed` value to a `Number`.
163197-- |
164198-- | _Note_: Overflow is possible here if the numerator is sufficiently large.
199+-- | Consider using `toString` instead.
165200toNumber
166201 :: forall precision
167202 . KnownPrecision precision
@@ -271,10 +306,134 @@ approxDiv a b = Fixed (x * n / y)
271306 y = numerator b
272307 n = BigInt.fromInt (denominator a)
273308
309+-- | Parse a fixed-precision number from a string. Any decimal digits which are
310+-- | not representable in the specified precision will be ignored.
311+-- |
312+-- | ```
313+-- | > fromString "123.456" :: Maybe (Fixed P1000)
314+-- | (Just (fromString "123.456" :: P1000))
315+-- | ```
316+-- |
317+-- | Where possible, this function should be preferred over `fromNumber`, since
318+-- | it is exact (whereas `fromNumber` can only provide an approximation for
319+-- | larger inputs).
320+-- |
321+-- | ```
322+-- | > fromString "9007199254740992.5" :: Maybe (Fixed P10)
323+-- | (Just (fromString "9007199254740992.5" :: P10))
324+-- |
325+-- | > fromNumber 9007199254740992.5 :: Maybe (Fixed P10)
326+-- | (Just (fromString "9007199254740992.0" :: P10))
327+-- | ```
328+-- |
329+fromString
330+ :: forall precision
331+ . KnownPrecision precision
332+ => String
333+ -> Maybe (Fixed precision)
334+fromString str =
335+ let
336+ numDigits = reflectPrecisionDecimalPlaces (PProxy :: PProxy precision)
337+ denom = BigInt.fromInt (reflectPrecision (PProxy :: PProxy precision))
338+
339+ isDigit = between '0' '9'
340+ wholeDigits = StringCU.countPrefix isDigit str
341+ { before, after } = StringCU.splitAt wholeDigits str
342+
343+ in do
344+ wholePart <-
345+ BigInt.fromString before
346+ fractionPart <-
347+ case after of
348+ "" -> pure zero
349+ "." -> pure zero
350+ _ -> do
351+ guard (StringCU.charAt 0 after == Just '.')
352+ let raw = StringCU.drop 1 after
353+ BigInt.fromString (rightJustify numDigits '0' raw)
354+ pure (Fixed (wholePart * denom + fractionPart))
355+
356+-- | Represent a `Fixed` value as a string, with the given number of decimal
357+-- | places.
358+-- |
359+-- | ```
360+-- | > map (toStringWithPrecision 2) (fromString "1234.567" :: Maybe (Fixed P1000))
361+-- | (Just "1234.56")
362+-- | ```
363+-- |
364+-- | If more decimal places are asked for than the type can provide, the extra
365+-- | decimal places will be provided as zeroes.
366+-- |
367+-- | ```
368+-- | > map (toStringWithPrecision 3) (fromString "1234.5" :: Maybe (Fixed P10))
369+-- | (Just "1234.500")
370+-- | ```
371+toStringWithPrecision
372+ :: forall precision
373+ . KnownPrecision precision
374+ => Int
375+ -> Fixed precision
376+ -> String
377+toStringWithPrecision requestedDigits fixed@(Fixed n) =
378+ let
379+ denom = BigInt.fromInt (reflectPrecision (PProxy :: PProxy precision))
380+ denomDigits = reflectPrecisionDecimalPlaces (PProxy :: PProxy precision)
381+ wholePart = n / denom
382+ fractionalPart = n `mod` denom
383+ in
384+ BigInt.toString wholePart
385+ <> Monoid.guard (requestedDigits > 0)
386+ ("." <>
387+ rightJustify requestedDigits '0'
388+ (leftJustify denomDigits '0' (BigInt.toString fractionalPart)))
389+
390+-- | Represent a `Fixed` value as a string, using all of the decimal places it
391+-- | can represent (based on its precision).
392+-- |
393+-- | ```
394+-- | > map toString (fromString "100.5" :: Maybe (Fixed P10))
395+-- | (Just "100.5")
396+-- |
397+-- | > map toString (fromString "100.5" :: Maybe (Fixed P100))
398+-- | (Just "100.50")
399+-- | ```
400+toString
401+ :: forall precision
402+ . KnownPrecision precision
403+ => Fixed precision
404+ -> String
405+toString =
406+ toStringWithPrecision
407+ (reflectPrecisionDecimalPlaces (PProxy :: PProxy precision))
408+
409+-- | If a string has less than the given number of characters (measured in code
410+-- | units), extend it with the given character until it reaches that length.
411+-- | If it has more, drop them from the end.
412+rightJustify :: Int -> Char -> String -> String
413+rightJustify desiredLength padding str =
414+ let
415+ actualLength = StringCU.length str
416+ in
417+ if actualLength >= desiredLength
418+ then StringCU.take desiredLength str
419+ else str <> StringCU.fromCharArray (replicate (desiredLength - actualLength) padding)
420+
421+-- | If a string has less than the given number of characters (measured in code
422+-- | units), prepend it with the given character until it reaches that length.
423+-- | If it has more, drop them from the start.
424+leftJustify :: Int -> Char -> String -> String
425+leftJustify desiredLength padding str =
426+ let
427+ actualLength = StringCU.length str
428+ in
429+ if actualLength >= desiredLength
430+ then StringCU.takeRight desiredLength str
431+ else StringCU.fromCharArray (replicate (desiredLength - actualLength) padding) <> str
432+
274433instance showFixed :: KnownPrecision precision => Show (Fixed precision) where
275434 show n =
276− "(fromNumber "
277− <> show (toNumber n)
435+ "(fromString "
436+ <> show (toString n)
278437 <> " :: P"
279438 <> show (reflectPrecision (PProxy :: PProxy precision))
280439 <> ")"