{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-----------------------------------------------------------------------------
-- Copyright 2019, Ideas project team. This file is distributed under the
-- terms of the Apache License 2.0. For more information, see the files
-- "LICENSE.txt" and "NOTICE.txt", which are included in the distribution.
-----------------------------------------------------------------------------
-- |
-- Maintainer  :  bastiaan.heeren@ou.nl
-- Stability   :  provisional
-- Portability :  portable (depends on ghc)
--
-----------------------------------------------------------------------------

module Domain.Math.Data.DecimalFraction
   ( DecimalFraction(..), fromDouble, validDivisor, digits
   ) where

import Control.Monad
import Data.Maybe
import Data.Ratio
import Domain.Math.Safe

import Test.QuickCheck

-- |Data type for decimal fractions
newtype DecimalFraction = DF Rational -- Invariant: denominator is valid
   deriving (Eq, Ord, Num, Real, Arbitrary)

instance Show DecimalFraction where
   show d@(DF r) = show x ++ "." ++ replicate extra '0' ++ show y
    where
      digs   = digits d
      base   = 10^digs
      n      = numerator (r * fromInteger base)
      (x, y) = n `divMod` base
      extra  = digs - length (show y)

instance Fractional DecimalFraction where
   a/b = fromMaybe (error "invalid divisor") (safeDiv a b)
   fromRational r = fromInteger (numerator r) / fromInteger (denominator r)

instance SafeDiv DecimalFraction where
   safeDiv (DF a) (DF b) = do
      guard (validDivisor (DF b))
      fmap DF (a `safeDiv` b)

instance SafePower DecimalFraction where
   safePower x (DF r)
      | denominator r /= 1 = Nothing
      | y >= 0             = Just a
      | otherwise          = safeDiv 1 a
    where
      y = numerator r
      a = x Prelude.^ abs y
   safeRoot x y = safeRecip y >>= safePower x

-- | Approximation of a double, with a precision of 8 digits
fromDouble :: Double -> DecimalFraction
fromDouble d = DF (fromInteger base / 10^digs)
 where
   digs = 8 :: Int -- maximum number of digits
   base = round (d * 10^digs) :: Integer

-- |Tests whether it is safe to divide by this fraction: it is safe to divide
-- if its numerator(!) is a product of two's and five's.
validDivisor :: DecimalFraction -> Bool
validDivisor (DF a) = validDenominator (abs (numerator a))

-- |number of decimal digits
digits :: DecimalFraction -> Int
digits (DF r) = head $ filter p [0..]
 where
   p i = 10^i `mod` denominator r == 0

-- local helper
validDenominator :: Integer -> Bool
validDenominator n
   | n == 0         = False
   | even n         = validDenominator (n `div` 2)
   | n `mod` 5 == 0 = validDenominator (n `div` 5)
   | otherwise      = n == 1