detour-via-sci-1.0.0: JSON and CSV encoding for rationals as decimal point numbers.

Copyright© 2018 Phil de Joux
© 2018 Block Scope Limited
LicenseMPL-2.0
MaintainerPhil de Joux <phil.dejoux@blockscope.com>
Stabilityexperimental
Safe HaskellNone
LanguageHaskell2010

Data.Via.Scientific

Contents

Description

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

Synopsis

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
>>> fromRational x
0.1122334455667788
>>> toSci (DecimalPlaces 8) x
0.11223345

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

>>> encode x
"{\"numerator\":280583613916947,\"denominator\":2500000000000000}"
>>> encode (Lat x)
"0.11223345"

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

>>> decode (encode x) == Just x
True
>>> decode (encode (Lat x)) == Just (Lat x)
False
>>> let Just (Lat y) = decode (encode (Lat x)) in fromRational y
0.11223345

Similarly for CSV.

>>> :{
instance ToField Lat where
    toField = toField . ViaSci
instance FromField Lat where
    parseField c = do ViaSci x <- parseField c; return x
:}
>>> Csv.encode [("A", Lat x)]
"A,0.11223345\r\n"
>>> Csv.decode Csv.NoHeader (Csv.encode [("B", Lat x)]) == Right (fromList [("B", Lat x)])
False
>>> Csv.decode Csv.NoHeader (Csv.encode [("C", Lat x)]) == Right (fromList [("C", Lat . fromSci . toSci (DecimalPlaces 8) $ x)])
True

Decimal Places

newtype DecimalPlaces Source #

A positive number of decimal places.

Constructors

DecimalPlaces Int 

data ViaSci n where Source #

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 scientific so that the rational value can be encoded as a scientific value with a fixed number of decimal places.

Constructors

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

Instances

class DefaultDecimalPlaces a where Source #

A default number of decimal places for a type.

Methods

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
>>> showSci (DecimalPlaces 16) x
"0.1122334455667788"
>>> showSci (DecimalPlaces 8) x
"0.11223345"
>>> showSci (DecimalPlaces 4) x
"0.1122"
>>> showSci (DecimalPlaces 1) x
"0.1"
>>> showSci (DecimalPlaces 0) x
"0"
>>> showSci (DecimalPlaces (-1)) x
"0"
>>> showSci (DecimalPlaces 32) x
"0.11223344556677880000000000000000"

Conversions

fromSci :: Scientific -> Rational Source #

From Scientific exactly to Rational.

>>> let x = 0.1122334455667788
>>> fromSci x
4043636029064415 % 36028797018963968
>>> x == fromRational (fromSci x)
True

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
>>> toSci (DecimalPlaces 8) x
0.11223345
>>> x == toRational (toSci (DecimalPlaces 8) x)
False
>>> x == toRational (toSci (DecimalPlaces 16) x)
True
>>> x == toRational (toSci (DecimalPlaces 32) x)
True

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
...

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

Derives an instance of ToJSON wrapping the value with ViaSci before encoding. Similarly the value is decoded as ViaSci and then unwrapped in the derived instance of FromJSON.

>>> deriveJsonViaSci ''Lat
...

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

Similar to deriveJsonViaSci but for instances of ToField and FromField.

>>> deriveCsvViaSci ''Lat
...