```{-|

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"
```