{-| Stability: experimental -}
module Data.SciRatio
    (
      -- * The 'SciRatio' type
      SciRatio
    , SciRational
    , (.^)
    , fracSignificand
    , base10Exponent

      -- * Specialized functions
    , (^!)
    , (^^!)
    , fromSciRatio

      -- * Miscellaneous utilities
    , factorizeBase
    , ilogBase

      -- * Deprecated
    , intLog
    ) where
import Data.Ratio ((%), denominator, numerator)
import Data.Hashable (Hashable(hashWithSalt))
infixr 8 ^!, ^^!
infixl 7 :^, .^
infixl 1 ~~

-- Implementation note: a SciRatio must *always* remain in canonical form, so
-- don't use :^ directly unless you're absolutely sure it will stay canonical
-- (use .^ instead if you're unsure).

-- | Represents a fractional number stored in scientific notation: a product
--   of a fractional significand and an integral power of 10.
--
--   - The significand has type @a@ and should be both @'Fractional'@ and
--     @'Real'@.  Although this could be a floating-point type, it is not
--     recommended as floating-points use inexact arithmetic and strange bugs
--     will occur as a result.
--
--   - The exponent has type @b@ and should be @'Integral'@.
--
--   @'SciRatio'@ behaves in the same way as an ordinary @'Data.Ratio.Ratio'@
--   and supports the same operations.  The main property is that it is more
--   efficient than @'Data.Ratio.Ratio'@ when the exponent is large:
--
--   >>> 5 .^ 99999999 :: SciRational   -- works fine
--   >>> 5e99999999    :: Rational      -- takes forever
--
--   Specialized functions are provided in cases where they can be implemented
--   more efficiently than the default implementation.
--
--   The number is always stored in a unique, canonical form: the significand
--   shall never contain factors of 2 and 5 simultaneously, and their
--   multiplicities shall always be nonnegative.  (The significand is treated
--   as a rational number factorized into a product of prime numbers with
--   integral exponents that are not necessarily positive.)
--
--   __Note__: If inputs differ greatly in magnitude, @('+')@ and @('-')@ can
--             be quite slow: complexity is linear with the absolute
--             difference of the exponents.  Furthermore, the complexity of
--             @'toRational'@ is linear with the magnitude of the exponent.
--             These also apply to any functions that indirectly use these
--             operations (including all operations in @'RealFrac'@ and
--             @'Enum'@).
--
data SciRatio a b = !a :^ !b deriving Eq

-- | A specialization of 'SciRatio'.
type SciRational = SciRatio Rational Integer

instance (Fractional a, Real a, Integral b, Read a, Read b) =>
         Read (SciRatio a b) where
  {-# SPECIALIZE instance Read SciRational #-}
  readsPrec p = readParen (p > prec) $ \ r -> do
    (x,    s) <- readsPrec (succ prec) r
    (".^", t) <- lex s
    (y,    u) <- readsPrec (succ prec) t
    return (x .^ y, u)

instance (Show a, Show b) => Show (SciRatio a b) where
  {-# SPECIALIZE instance Show SciRational #-}
  showsPrec p (x :^ a) = showParen (p > prec) $
                         showsPrec (succ prec) x .
                         showString " .^ " .
                         showsPrec (succ prec) a

instance (Real a, Integral b, Ord a) => Ord (SciRatio a b) where
  {-# SPECIALIZE instance Ord SciRational #-}
  compare (x :^ a) (y :^ b) = case compare sgnX sgnY of
    EQ -> case sgnX of
      EQ -> EQ
      LT -> invert order
      GT -> order
    k  -> k
    where x'        = toRational x
          y'        = toRational y
          sgnX      = compare x 0
          sgnY      = compare y 0
          absM      = abs (numerator x' * denominator y')
          absN      = abs (numerator y' * denominator x')
          invert GT = LT
          invert EQ = EQ
          invert LT = GT
          order     = case (_ilogBase 10 absM, _ilogBase 10 absN) of
            -- initial comparison via floored logarithms to handle easy cases
            -- where exponents are wildly different
            (logM, logN) -> case compare (a + logM) (b + logN) of
              -- compare directly if the exponents are too close
              EQ | a >= b    -> compare (absM * 10 ^ (a - b)) absN
                 | otherwise -> compare absM (absN * 10 ^ (b - a))
              k              -> k

instance (Hashable a, Hashable b) => Hashable (SciRatio a b) where
  {-# SPECIALIZE instance Hashable SciRational #-}
  hashWithSalt s (r :^ e) = hashWithSalt s (magic, r, e)
    where magic = 0xaaa80d6b :: Int

instance (Fractional a, Real a, Integral b) => Num (SciRatio a b) where
  {-# SPECIALIZE instance Num SciRational #-}
  p + q             =  (x + y) .^ c where (x, y, c) = p ~~ q
  p - q             =  (x - y) .^ c where (x, y, c) = p ~~ q
  x :^ a * (y :^ b) =    x * y .^ (a + b)
  abs      (y :^ b) =    abs y :^ b
  negate   (y :^ b) = negate y :^ b
  signum            = (:^ 0) . signum . fracSignificand
  fromInteger       = (.^ 0) . fromInteger

instance (Fractional a, Real a, Integral b) => Fractional (SciRatio a b) where
  {-# SPECIALIZE instance Fractional SciRational #-}
  x :^ a / (y :^ b) =   x / y .^ (a - b)
  recip    (y :^ b) = recip y .^ (-b)
  fromRational      = (.^ 0) . fromRational

instance (Fractional a, Real a, Integral b) => Real (SciRatio a b) where
  {-# SPECIALIZE instance Real SciRational #-}
  toRational (y :^ b) = toRational y * 10 ^^ b

instance (Fractional a, Real a, Integral b) => RealFrac (SciRatio a b) where
  {-# SPECIALIZE instance RealFrac SciRational #-}
  properFraction q = (\ (n, p) -> (n, fromRational p))
                     . properFraction $ toRational q

instance (Fractional a, Real a, Integral b) => Enum (SciRatio a b) where
  {-# SPECIALIZE instance Enum SciRational #-}
  succ               = fromRational . succ . toRational
  pred               = fromRational . pred . toRational
  toEnum             = fromRational . toEnum
  fromEnum           = fromEnum . toRational
  enumFrom           = fmap fromRational . enumFrom . toRational
  enumFromThen   x   = fmap fromRational
                       . enumFromThen (toRational x)
                       . toRational
  enumFromTo     x   = fmap fromRational
                       . enumFromTo (toRational x)
                       . toRational
  enumFromThenTo x y = fmap fromRational
                       . enumFromThenTo (toRational x) (toRational y)
                       . toRational

-- | Precedence of the @('.^')@ operator.
prec :: Int
prec = 7

-- | Construct a number such that
--   @significand .^ exponent == significand * 10 ^^ exponent@.
{-# SPECIALISE (.^) :: Rational -> Integer -> SciRational #-}
(.^) :: (Fractional a, Real a, Integral b) =>
        a                               -- ^ significand
     -> b                               -- ^ exponent
     -> SciRatio a b
x .^ y = canonicalize $ x :^ y

-- | Extract the fractional significand.
fracSignificand :: SciRatio a b -> a
fracSignificand (x :^ _) = x

-- | Extract the base-10 exponent.
base10Exponent :: SciRatio a b -> b
base10Exponent (_ :^ x) = x

-- | Convert into a 'Fractional' number.
--
--   This is similar to 'realToFrac' but much more efficient for large
--   exponents.
{-# RULES "realToFrac/fromSciRational"
            forall (x :: SciRational) . realToFrac x = fromSciRatio x;
          "realToFrac/SciRational"
            realToFrac = id :: SciRational -> SciRational;
          "fromSciRatio/SciRational"
            fromSciRatio = id #-}
{-# NOINLINE [1] fromSciRatio #-}
fromSciRatio :: (Real a, Integral b, Fractional c) => SciRatio a b -> c
fromSciRatio (x :^ y) = realToFrac x * 10 ^^ y

-- | Specialized, more efficient version of @('^^')@.
{-# RULES "(^^)/SciRational" forall (x :: SciRational) . (^^) x = (^^!) x #-}
(^^!) :: (Fractional a, Real a, Integral b, Integral c) =>
         SciRatio a b -> c -> SciRatio a b
(x :^ a) ^^! b = x ^^ b :^ (a * fromIntegral b)

-- | Specialized, more efficient version of @('^')@.
{-# RULES "(^)/SciRational" forall (x :: SciRational) . (^) x = (^!) x #-}
(^!) :: (Real a, Integral b, Integral c) =>
        SciRatio a b -> c -> SciRatio a b
(x :^ a) ^! b = x ^ b :^ (a * fromIntegral b)

-- | Matches the exponents.
(~~) :: (Fractional a, Integral b) => SciRatio a b -> SciRatio a b -> (a, a, b)
x :^ a ~~ y :^ b = (x * 10 ^^ (a - c), y * 10 ^^ (b - c), c)
  where c = if abs a <= abs b then a else b

-- | Factorize a nonzero integer into a significand and a power of the base
--   such that the exponent is maximized:
--
--   > inputInteger = significand * base ^ exponent
--
--   That is, the significand shall not divisible by the base.  The base must
--   be greater than one.
--
--   /Since: 0.2.1/
factorizeBase :: (Integral a, Integral b) =>
                 a                      -- ^ base
              -> a                      -- ^ input integer
              -> (a, b)                 -- ^ significand and exponent
factorizeBase _ 0             = error ("Data.SciRatio.factorizeBase: " ++
                                       "input integer must be nonzero")
factorizeBase b n | b  <  2   = error ("Data.SciRatio.factorizeBase: " ++
                                       "base must be greater than one")
                  | otherwise = _factorizeBase b n

-- | Same as @'factorizeBase'@ but doesn't validate the arguments, so it might
--   loop forever or return something nonsensical.
_factorizeBase :: (Integral a, Integral b) => a -> a -> (a, b)
_factorizeBase b n | r  /= 0   = (n,    0)
                   | r' /= 0   = (n'',  e2 + 1)
                   | otherwise = (n''', e2 + 2)
  where (n',   r)  = n   `quotRem` b
        (n'',  e)  = _factorizeBase (b * b) n'
        (n''', r') = n'' `quotRem` b
        e2         = e * 2

-- | Alias of @'factorizeBase'@.
--
--   Note: Despite what the name suggests, the function does /not/ compute the
--         floored logarithm.
{-# DEPRECATED intLog "use @'factorizeBase'@ instead." #-}
intLog :: (Integral a, Integral b) => a -> a -> (a, b)
intLog = factorizeBase

-- | Calculate the floored logarithm of a positive integer.  The base must be
--   greater than one.
--
--   /Since: 0.2.1/
ilogBase :: (Integral a, Integral b) =>
            a                           -- ^ base
         -> a                           -- ^ input integer
         -> b                           -- ^ floor of the logarithm
ilogBase b | b < 2     = error ("Data.SciRatio.ilogBase: " ++
                                "base must be greater than one")
           | otherwise = _ilogBase b

-- | Same as @'ilogBase'@ but doesn't validate the arguments, so it might loop
--   forever or return something nonsensical.
_ilogBase :: (Integral a, Integral b) => a -> a -> b
_ilogBase = (fst .) . ilogr
  -- use the trick from section 14.4 of The Haskell 98 Language Report
  where ilogr b n | n  < b    =      (0, n)
                  | n' < b    =     (l2, n')
                  | otherwise = (1 + l2, n' `div` b)
          where (l, n') = ilogr (b * b) n
                l2      = l * 2

-- | Convert into canonical form by removing all factors of 2 and 5 from the
--   denominator and factoring out as many powers of the base as possible.
canonicalize :: (Fractional a, Real a, Integral b) =>
                SciRatio a b -> SciRatio a b
canonicalize (0 :^ _) = 0 :^ 0
canonicalize (r :^ e) =
  let r' = toRational r in
  case (_factorizeBase 10 (numerator r'), _factorizeBase 2 (denominator r')) of
    ((n, ne), (d, e2)) -> case _factorizeBase 5 d of
      (d', e5) -> case compare e2 e5 of
        EQ -> fromRational (n % d') :^ (e + ne - e5)
        LT -> fromRational ((n * 2 ^ (e5 - e2)) % d') :^ (e + ne - e5)
        GT -> fromRational ((n * 5 ^ (e2 - e5)) % d') :^ (e + ne - e2)
