{-|
    This module is about fractions.

    A fraction can be seen as a real number from the closed interval [0,1]. It can also be seen as a
    percentage. A typical example of a fraction is the extend of a progress bar.
-}
module Data.Fraction (

    -- * Fraction type
    Fraction,

    -- * Conversion
    fromFactor,
    fromPercentage,
    fromNumber,
    toFactor,
    toPercentage,
    toNumber

) where

    -- Data
    import Data.Semigroup as Semigroup
    import Data.Monoid    as Monoid

    -- * Fraction type
    -- |A fraction.
    newtype Fraction = Fraction Double

    instance Semigroup Fraction where

        (<>) = mappend

    instance Monoid Fraction where

        mempty = Fraction 1

        Fraction factor1 `mappend` Fraction factor2 = Fraction (factor1 * factor2)

    -- * Conversion
    {-|
        Converts a factor into its corresponding fraction.

        If the factor is not from the interval [0,1], a runtime error occurs.
    -}
    fromFactor :: (Real real) => real -> Fraction
    fromFactor = fromNumber (0,1)

    {-|
        Converts a percentage into its corresponding fraction.

        If the percentage is not from the interval [0,100], a runtime error occurs.
    -}
    fromPercentage :: (Real real) => real -> Fraction
    fromPercentage = fromNumber (0,100)

    {-|
        Converts a number into its corresponding fraction regarding a certain interval.

        If the lower bound of the interval is equal to or greater than the upper bound or the value
        is not from the interval, a runtime error occurs.
    -}
    fromNumber :: (Real real) => (real,real) -> real -> Fraction
    fromNumber (lower,upper) real = checkInterval (lower,upper) `seq`
                                    if lower <= real && real <= upper
                                        then Fraction $
                                             (realToFrac real - fracMin) / (fracMax - fracMin)
                                        else error "fraction: real out of bounds" where

        fracMin = realToFrac lower

        fracMax = realToFrac upper

    -- |Converts a fraction into its corresponding factor.
    toFactor :: Fraction -> Double
    toFactor (Fraction factor) = factor

    -- |Converts a fraction into its corresponding percentage.
    toPercentage :: Fraction -> Double
    toPercentage (Fraction factor) = factor * 100

    {-|
        Converts a fraction into its corresponding number regarding a certain interval.

        If the lower bound of the interval is equal to or greater than the upper bound, a runtime
        error occurs.
    -}
    toNumber :: (Double,Double) -> Fraction -> Double
    toNumber (lower,upper) (Fraction factor) = checkInterval (lower,upper) `seq`
                                               factor * (upper - lower) + lower

    checkInterval :: (Real real) => (real,real) -> ()
    checkInterval (lower,upper) | lower < upper = ()
                                | otherwise     = error "fraction: no proper interval"