packages feed

technique-0.2.5: lib/Technique/Quantity.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StrictData #-}

module Technique.Quantity
  ( Quantity (..),
    Decimal (..),
    Magnitude,
    decimalToRope,
    isZeroDecimal,
    negateDecimal,
    Symbol,
    Unit (..),
    Group (..),
    Prefix (..),
    units,
    prefixes,
  )
where

import Core.Data.Structures
import Core.Text.Rope
import Core.Text.Utilities
import Data.Int (Int64, Int8)

data Quantity
  = Number Int64
  | Quantity Decimal Decimal Magnitude Symbol
  deriving (Show, Eq, Ord)

type Symbol = Rope

type Magnitude = Int8

-- |
-- A decimal number with a fixed point resolution. The resolution (number
-- of decimal places) is arbitrary within the available range. This isn't
-- really for numerical analysis. It is for carrying information.
--
-- /Implementation note/
--
-- Internally this is a floating point where the mantissa is 19 characters
-- wide (the width of a 64-bit int in base 10). Thus the biggest number
-- representable is 9223372036854775807 and the smallest is
-- 0.0000000000000000001. We could change this to Integer and be arbitrary
-- precision but meh.
data Decimal = Decimal Int64 Int8
  deriving (Eq, Ord)

instance Show Decimal where
  show = show . decimalToRope

decimalToRope :: Decimal -> Rope
decimalToRope (Decimal number resolution)
  | resolution < 0 = error "resolution can't be negative"
  | resolution == 0 = intoRope (show number)
  | otherwise =
    let digits = intoRope (show (abs number))
        len = widthRope digits
        res = fromIntegral resolution
        pos = len - res
        result =
          if (pos <= 0)
            then "0." <> leftPadWith '0' res digits
            else let (whole, fraction) = splitRope pos digits in whole <> "." <> fraction
     in if number >= 0
          then result
          else "-" <> result

isZeroDecimal :: Decimal -> Bool
isZeroDecimal (Decimal number _) = if number == 0 then True else False

negateDecimal :: Decimal -> Decimal
negateDecimal (Decimal number resolution) = Decimal (negate number) resolution

units :: Map Symbol Unit
units =
  foldr f emptyMap knownUnits
  where
    f unit m = insertKeyValue (unitSymbol unit) unit m

-- |
-- Whether Système International metric prefixes can be used, or (as is the
-- case of time units) quantities should not be aggregated to other scales.
data Group
  = Metric -- has prefixes
  | Time
  | Normal
  | Scientific -- probable collision with type from **base**
  | Engineering
  deriving (Show, Eq)

knownUnits :: [Unit]
knownUnits =
  [ Unit "metre" "metres" "m" Metric,
    Unit "gram" "grams" "g" Metric,
    Unit "litre" "litres" "L" Metric,
    Unit "second" "seconds" "sec" Time,
    Unit "minute" "minutes" "min" Time,
    Unit "hour" "hours" "hr" Time,
    Unit "day" "days" "d" Time,
    Unit "degree celsius" "degrees celsius" "°C" Normal,
    Unit "degree kelvin" "degrees kelvin" "K" Metric
  ]

prefixes :: Map Symbol Prefix
prefixes =
  foldr g emptyMap knownPrefixes
  where
    g prefix m = insertKeyValue (prefixSymbol prefix) prefix m

knownPrefixes :: [Prefix]
knownPrefixes =
  [ Prefix "peta" "P" 15,
    Prefix "tera" "T" 12,
    Prefix "giga" "G" 9,
    Prefix "mega" "M" 6,
    Prefix "kilo" "k" 3,
    Prefix "" "" 0,
    Prefix "milli" "m" (-3),
    Prefix "micro" "μ" (-6),
    Prefix "nano" "n" (-9),
    Prefix "pico" "p" (-12)
  ]

data Prefix = Prefix
  { prefixName :: Rope,
    prefixSymbol :: Symbol,
    prefixScale :: Int -- FIXME change this to a hard coded numerical constant?
  }
  deriving (Show, Eq)

data Unit = Unit
  { unitName :: Rope,
    unitPlural :: Rope,
    unitSymbol :: Rope,
    unitGroup :: Group
  }
  deriving (Show, Eq)