{-# Language DeriveDataTypeable, DeriveGeneric #-}
{-|
Module      : Config.Number
Description : Scientific-notation numbers with explicit radix
Copyright   : (c) Eric Mertens, 2019
License     : ISC
Maintainer  : emertens@gmail.com

This module provides a representation of numbers in scientific
notation.
-}
module Config.Number
  ( Number(..)
  , Radix(..)
  , radixToInt
  , numberToRational
  , numberToInteger
  , integerToNumber
  , rationalToNumber
  ) where

import Data.Ratio (numerator, denominator)
import Data.Data (Data)
import GHC.Generics (Generic)

-- | Numbers are represented as base, coefficient, and exponent.
--
-- The most convenient way to get numbers into and out of this form
-- is to use one of: 'numberToRational', 'numberToInteger',
-- 'rationalToNumber', or 'integerToNumber'.
--
-- This representation is explicit about the radix and exponent
-- used to facilitate better pretty-printing. By using explicit
-- exponents extremely large numbers can be represented compactly.
-- Consider that it is easy to write `1e100000000` which would use
-- a significant amount of memory if realized as an 'Integer'. This
-- representation allows concerned programs to check bounds before
-- converting to a representation like 'Integer'.
data Number = MkNumber
  { numberRadix       :: !Radix
  , numberCoefficient :: !Rational
  }
  deriving (Eq, Ord, Read, Show, Data, Generic)

-- | Radix used for a number. Some radix modes support an
-- exponent.
data Radix
  = Radix2           -- ^ binary, base 2
  | Radix8           -- ^ octal, base 8
  | Radix10 !Integer -- ^ decimal, base 10, exponent base 10
  | Radix16 !Integer -- ^ hexdecimal, base 16, exponent base 2
  deriving (Eq, Ord, Read, Show, Data, Generic)

-- | Returns the radix as an integer ignoring any exponent.
radixToInt :: Radix -> Int
radixToInt r =
  case r of
    Radix2 {} ->  2
    Radix8 {} ->  8
    Radix10{} -> 10
    Radix16{} -> 16

-- | Convert a number to a 'Rational'. Warning: This can use a
-- lot of member in the case of very large exponent parts.
numberToRational :: Number -> Rational
numberToRational (MkNumber r c) =
  case r of
    Radix2    -> c
    Radix8    -> c
    Radix10 e -> c * 10 ^^ e
    Radix16 e -> c * 2  ^^ e

-- | Convert a number to a 'Integer'. Warning: This can use a
-- lot of member in the case of very large exponent parts.
numberToInteger :: Number -> Maybe Integer
numberToInteger n
  | denominator r == 1 = Just $! numerator r
  | otherwise          = Nothing
  where
    r = numberToRational n

-- | 'Integer' to a radix 10 'Number' with no exponent
integerToNumber :: Integer -> Number
integerToNumber = rationalToNumber . fromInteger

-- | 'Rational' to a radix 10 'Number' with no exponent
rationalToNumber :: Rational -> Number
rationalToNumber r = (MkNumber (Radix10 0) r)