{- |

A numeric type to manage engineering units.

Provides automatic unit conversion:

> > print $ value (1 * h + 1 * min') s        -- Time in seconds of 1 hour + 1 minute.
> 3660.0

Automatic unit reduction:

> > print $ value (20 * gpm * 10 * min') gal  -- Note the minutes cancel each other out.
> 200.0

And consistency checking:

> > print $ value (22 * mph + 3 gal) mph      -- Note that speed (m/s) is inconsistent with volume (m^3).
> *** Exception: Incompatible units: [M]/[S] /= [M,M,M]/[]

And defining new units is easy:

> -- | Millimeters, a measure of distance, is 1/1000 of a meter.
> mm :: Value
> mm = 0.001 * m

> -- | Joules, a measure of energy, is one newton meter.
> j :: Value
> j = n * m

-}
module Data.EngineeringUnits
  ( Value
  , value
  -- * Distance
  , m    
  , cm   
  , mm
  , km
  , in'  
  , ft   
  , mi   
  -- * Area
  , cm2  
  , in2  
  -- * Volume
  , cm3  
  , ml
  , l
  , in3  
  , gal  
  -- * Mass
  , kg   
  , g
  , mg
  -- * Force
  , n    
  , lbs  
  -- * Rotation
  , rev  
  -- * Speed
  , mph  
  , kph
  -- * Rotational Rate
  , rpm  
  -- * Time
  , s    
  , ns
  , us
  , ms
  , min' 
  , h
  -- * Energy
  , j
  , btu
  -- * Power
  , hp   
  , w    
  , kw   
  -- * Pressure
  , psi  
  , bar
  -- * Flow
  , gpm  
  , lpm
  -- * Misc
  , s2   
  , radsPerRev
  ) where

import Data.List

-- | The base units for distance, time, mass, and revolutions.
data Unit = M | S | Kg | Rev deriving (Eq, Ord, Show)

-- | A value is a number and its associated units.
data Value = Value Double [Unit] [Unit] deriving (Show, Eq, Ord)

instance Num Value where
  a@(Value aV _ _) + b@(Value bV _ _) = same a b $ aV + bV
  a@(Value aV _ _) - b@(Value bV _ _) = same a b $ aV - bV
  Value aV aN aD * Value bV bN bD = normalize $ Value (aV * bV) (aN ++ bN) (aD ++ bD)
  fromInteger a = Value (fromIntegral a) [] []
  negate (Value v n d) = Value (negate v) n d
  abs    (Value v n d) = Value (abs    v) n d
  signum (Value v n d) = Value (signum v) n d

instance Fractional Value where
  Value aV aN aD / Value bV bN bD = normalize $ Value (aV / bV) (aN ++ bD) (aD ++ bN)
  recip (Value v n d) = Value (recip v) d n
  fromRational a = Value (fromRational a) [] []

instance Floating Value where
  pi    = Value pi [] []
  (Value a n d) ** b = case b of
    Value 2 [] [] -> normalize $ Value (a ** 2) (n ++ n) (d ++ d)
    _ -> error "Not supported (**) where power is not a unitless value of 2."
  sqrt v@(Value a n d) = Value (sqrt a) (sqrt' n) (sqrt' d)
    where
    sqrt' a = case a of
      []  -> []
      [_] ->  error $ "Sqrt failed on unit reduction: " ++ show v
      a : b : c
        | a == b -> a : sqrt' c
        | otherwise -> error $ "Sqrt failed on unit reduction: " ++ show v
  exp   = error "Not supported yet for Value: exp  "
  log   = error "Not supported yet for Value: log  "
  sin   = unitless "sin" sin
  cos   = unitless "cos" cos
  tan   = unitless "tan" tan
  asin  = unitless "asin" asin
  acos  = unitless "acos" acos
  atan  = unitless "atan" atan
  sinh  = error "Not supported yet for Value: sinh "
  cosh  = error "Not supported yet for Value: cosh "
  asinh = error "Not supported yet for Value: asinh"
  acosh = error "Not supported yet for Value: acosh"
  atanh = error "Not supported yet for Value: atanh"

unitless :: String -> (Double -> Double) -> Value -> Value
unitless msg f (Value a n d)
  | null n && null d = Value (f a) [] []
  | otherwise        = error $ msg ++ " requires unitless value."

-- | Normalize a value, i.e. simplify and sort units.
normalize :: Value -> Value
normalize a@(Value _ n d) = order $ reduce (n ++ d) a
  where
  reduce :: [Unit] -> Value -> Value
  reduce [] a = a
  reduce (a : rest) (Value v n d)
    | elem a n && elem a d = reduce rest $ Value v (delete a n) (delete a d)
    | otherwise            = reduce rest $ Value v n d
  order :: Value -> Value
  order (Value v n d) = Value v (sort n) (sort d)

-- | Create a value if two units are compatible.
same :: Value -> Value -> Double -> Value
same (Value _ aN aD) (Value _ bN bD) v
  | aN == bN && aD == bD = Value v aN aD
  | otherwise = error $ "Incompatible units: " ++ show aN ++ "/" ++ show aD ++ " /= " ++ show bN ++ "/" ++ show bD

-- | Extract a value in the given units.
--
--   > value val units
--   > value (2.54 * cm) in'
value :: Value -> Value -> Double
value val@(Value v _ _) units@(Value k _ _) = result
  where
  Value result _ _ = same val units $ v / k

-- | Meters.
m    = Value 1 [M]   []
-- | Seconds.
s    = Value 1 [S]   []
-- | Kilograms.
kg   = Value 1 [Kg]  []
-- | Revolutions.
rev  = Value 1 [Rev] []
-- | Seconds ^ 2.
s2   = s * s
-- | Centimeters.
cm   = 0.01 * m
-- | Centimeters ^ 2.
cm2  = cm * cm
-- | Centimeters ^ 3.
cm3  = cm * cm * cm
-- | Millimeters.
mm   = 0.1 * cm
-- | Kilometers.
km   = 1000 * m
-- | Milliliters.
ml   = cm3
-- | Liters.
l    = 1000 * ml
-- | Grams.
g    = 0.001 * kg
-- | Milligrams.
mg   = 0.001 * g
-- | Inches.
in'  = 2.54 * cm
-- | Inches ^ 2.
in2  = in' * in'
-- | Inches ^ 3.
in3  = in' * in' * in'
-- | Feet.
ft   = 12 * in'
-- | Nanoseconds.
ns = 0.000000001 *s
-- | Microseconds.
us = 0.000001 *s
-- | Milliseconds.
ms = 0.001 * s
-- | Minutes.
min' = 60 * s
-- | Hours.
h    = 60 * min'
-- | Newtons.
n    = kg * m / s2
-- | Pounds.
lbs  = 4.4482216152605 * n
-- | Miles.
mi   = 5280 * ft
-- | Gallons.
gal  = 231 * in3
-- | Horsepower.
hp   = 33000 * ft * lbs / min'
-- | Kilowatts.
kw   = 1.3410220888 * hp
-- | Watts.
w    = 0.001 * kw
-- | Pounds per inch ^ 2.
psi  = lbs / in2
-- | Bar.
bar  = 14.5037738 * psi
-- | Miles per hour.
mph  = mi / h
-- | Kilometers per hour.
kph  = km / h
-- | Revolutions per minute.
rpm  = rev / min'
-- | Gallons per minute.
gpm  = gal / min'
-- | Liters per minute.
lpm  = l / min'
-- | Radians per revolution: 2 * pi / rev
radsPerRev = 2 * pi / rev
-- | Joules.
j    = n * m
-- | BTUs.
btu  = 1055.05585 * j