Copyright | © 2018 Phil de Joux © 2018 Block Scope Limited |
---|---|

License | MPL-2.0 |

Maintainer | Phil de Joux <phil.dejoux@blockscope.com> |

Stability | experimental |

Safe Haskell | None |

Language | Haskell2010 |

For encoding and decoding newtype rationals as scientific with a fixed number of decimal places.

- newtype DecimalPlaces = DecimalPlaces Int
- data ViaSci n where
- ViaSci :: (DefaultDecimalPlaces n, Newtype n Rational) => n -> ViaSci n

- class DefaultDecimalPlaces a where
- dpDegree :: DecimalPlaces
- showSci :: DecimalPlaces -> Scientific -> String
- fromSci :: Scientific -> Rational
- toSci :: DecimalPlaces -> Rational -> Scientific
- deriveDecimalPlaces :: DecimalPlaces -> Name -> Q [Dec]
- deriveJsonViaSci :: Name -> Q [Dec]
- deriveCsvViaSci :: Name -> Q [Dec]

# Usage

Let's say we have a latitude that is a `newtype`

`Rational`

number but we
want it to be encoded to JSON with a fixed number of decimal places.

`>>>`

`newtype Lat = Lat Rational deriving (Eq, Ord, Show)`

Types going `ViaSci`

also need to be instances of `DefaultDecimalPlaces`

and
`Newtype`

.

`>>>`

instance DefaultDecimalPlaces Lat where defdp _ = DecimalPlaces 8 instance Newtype Lat Rational where pack = Lat unpack (Lat a) = a instance ToJSON Lat where toJSON x = toJSON $ ViaSci x instance FromJSON Lat where parseJSON o = do ViaSci x <- parseJSON o; return x :}`:{`

`>>>`

`let x = 1122334455667788 % 10000000000000000`

`>>>`

0.1122334455667788`fromRational x`

`>>>`

0.11223345`toSci (DecimalPlaces 8) x`

When having to check numbers by hand, a fixed decimal is more familiar than a ratio of possibly large integers.

`>>>`

"{\"numerator\":280583613916947,\"denominator\":2500000000000000}"`encode x`

`>>>`

"0.11223345"`encode (Lat x)`

With too few decimal places, the encoding will be lossy.

`>>>`

True`decode (encode x) == Just x`

`>>>`

False`decode (encode (Lat x)) == Just (Lat x)`

`>>>`

0.11223345`let Just (Lat y) = decode (encode (Lat x)) in fromRational y`

Similarly for CSV.

`>>>`

instance ToField Lat where toField = toField . ViaSci instance FromField Lat where parseField c = do ViaSci x <- parseField c; return x :}`:{`

`>>>`

"A,0.11223345\r\n"`Csv.encode [("A", Lat x)]`

`>>>`

False`Csv.decode Csv.NoHeader (Csv.encode [("B", Lat x)]) == Right (fromList [("B", Lat x)])`

`>>>`

True`Csv.decode Csv.NoHeader (Csv.encode [("C", Lat x)]) == Right (fromList [("C", Lat . fromSci . toSci (DecimalPlaces 8) $ x)])`

# Decimal Places

newtype DecimalPlaces Source #

A positive number of decimal places.

An intermediate type used during encoding to JSON with `aeson`

and during
encoding to CSV with `cassava`

. It's also used during decoding.

The original type, a newtype `Rational`

, goes to and fro **via**
**sci**entific so that the rational value can be encoded as a scientific
value with a fixed number of decimal places.

ViaSci :: (DefaultDecimalPlaces n, Newtype n Rational) => n -> ViaSci n |

Eq n => Eq (ViaSci n) Source # | |

Ord n => Ord (ViaSci n) Source # | |

Show n => Show (ViaSci n) Source # | |

(DefaultDecimalPlaces n, Newtype n Rational) => ToJSON (ViaSci n) Source # | |

(DefaultDecimalPlaces n, Newtype n Rational) => FromJSON (ViaSci n) Source # | |

(DefaultDecimalPlaces n, Newtype n Rational) => FromField (ViaSci n) Source # | |

(DefaultDecimalPlaces n, Newtype n Rational) => ToField (ViaSci n) Source # | |

class DefaultDecimalPlaces a where Source #

A default number of decimal places for a type.

defdp :: a -> DecimalPlaces Source #

dpDegree :: DecimalPlaces Source #

A choice of 8 decimal places for decimal degrees is just a bit more than a mm at the equator and less elsewhere.

- 1.1132 mm at the equator
- 1.0247 mm at 23° N/S
- 787.1 µm at 45° N/S
- 434.96 µm at 67° N/S

showSci :: DecimalPlaces -> Scientific -> String Source #

Shows a `Scientific`

value with a fixed number of decimal places.

`>>>`

`let x = 0.1122334455667788`

`>>>`

"0.1122334455667788"`showSci (DecimalPlaces 16) x`

`>>>`

"0.11223345"`showSci (DecimalPlaces 8) x`

`>>>`

"0.1122"`showSci (DecimalPlaces 4) x`

`>>>`

"0.1"`showSci (DecimalPlaces 1) x`

`>>>`

"0"`showSci (DecimalPlaces 0) x`

`>>>`

"0"`showSci (DecimalPlaces (-1)) x`

`>>>`

"0.11223344556677880000000000000000"`showSci (DecimalPlaces 32) x`

# Conversions

fromSci :: Scientific -> Rational Source #

From `Scientific`

exactly to `Rational`

.

`>>>`

`let x = 0.1122334455667788`

`>>>`

4043636029064415 % 36028797018963968`fromSci x`

`>>>`

True`x == fromRational (fromSci x)`

toSci :: DecimalPlaces -> Rational -> Scientific Source #

To `Scientific`

from `Rational`

as near as possible up to the given number
of `DecimalPlaces`

with rounding.

`>>>`

`let x = 1122334455667788 % 10000000000000000`

`>>>`

0.11223345`toSci (DecimalPlaces 8) x`

`>>>`

False`x == toRational (toSci (DecimalPlaces 8) x)`

`>>>`

True`x == toRational (toSci (DecimalPlaces 16) x)`

`>>>`

True`x == toRational (toSci (DecimalPlaces 32) x)`

# Deriving instances with Template Haskell

deriveDecimalPlaces :: DecimalPlaces -> Name -> Q [Dec] Source #

Taking a number of decimal places from the given `DecimalPlaces`

newtype,
derives an instance of `DefaultDecimalPlaces`

.

`>>>`

...`deriveDecimalPlaces (DecimalPlaces 8) ''Lat`

deriveCsvViaSci :: Name -> Q [Dec] Source #

Similar to `deriveJsonViaSci`

but for instances of `ToField`

and `FromField`

.

`>>>`

...`deriveCsvViaSci ''Lat`