convert-units-0: src/Data/Units/Base/Convert.hs
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DefaultSignatures #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
--------------------------------------------------------------------------------
-- |
--
-- Module : Data.Units.Base.Convert
-- Description : Conversion between units
-- Copyright : (c) Alice Rixte 2025
-- License : BSD 3
-- Maintainer : alice.rixte@u-bordeaux.fr
-- Stability : unstable
-- Portability : non-portable (GHC extensions)
--
-- Conversion between units. Use @'from'@, @'to'@, or @'fromTo'@ to convert
-- between two units of the same dimension.
--
-- = Implementing conversions for custom units
--
-- Depending on how the custom unit is converted to its standard unit, there are
-- three ways to implement its conversion summarized in the following table and
-- described with further details afterwards:
--
-- +-----------------------+---------------------------+----------------------+
-- | | Which instances | Note |
-- | | to declare | |
-- +=======================+===========================+======================+
-- | Conversion factor | @'ConversionFactor'@ and | |
-- | | @'ConvertibleUnit'@ using | |
-- | | the default | @'fromTo' == @ |
-- | | implementations | @'fromTo''@ |
-- | | for 'fromBaseUnit' and | |
-- | | 'toBaseUnit' | |
-- +-----------------------+---------------------------+----------------------+
-- | Affine conversion | @'ConversionFactor'@ and | |
-- | | @'ConvertibleUnit'@ | |
-- | | | @'fromTo' /= @ |
-- | | | @'fromTo''@ |
-- +-----------------------+---------------------------+----------------------+
-- | Non linear conversion | | @'from''@, @'to''@ |
-- | | @'ConvertibleUnit'@ | and @'fromTo''@ |
-- | | | cannot be used |
-- +-----------------------+---------------------------+----------------------+
--
--
-- === Multiplication by a conversion factor
--
-- For units that can be converted to and from their corresponding standard
-- units by multiplication of a converion factor, you only need to declare an
-- instance of @'ConversionFactor'@, like
--
-- @
-- instance Fractional a => ConversionFactor Hour a where
-- factor = 3600
--
-- instance Fractional a => ConvertibleUnit Hour a
-- -- uses default implementations for 'fromBaseUnit' and 'toBaseUnit'
-- @
--
-- >>> fromTo @Hour @Second 1
-- Second 3600.0
-- >>> fromTo' @Hour @Second 1
-- Second 3600.0
--
-- === Affine conversion (with an offset)
--
-- Some units cannot be conversed by a simple multiplication. For instance, the
-- conversion between Celsius degrees and Kelvin degrees involves addition
-- @x °C = x + 273.15 K@.
--
-- However, when considered as /differences/ of temperatures, Celsius degrees
-- are converted to Kelvin degrees by a multiplication of @1@.
--
-- This can be expressed by the following instances:
--
-- @
-- instance Num a => ConversionFactor Celsius a where
-- factor = 1
--
-- instance Fractional a => ConvertibleUnit Celsius a where
-- toBaseUnit (Celsius x) = Kelvin (x - 273.15)
-- fromBaseUnit (Kelvin x) = Celsius (x + 273.15)
-- @
--
-- >>> fromTo @Celsius @Kelvin 0
-- Kelvin 273.15
-- >>> fromTo' @Celsius @Kelvin 0
-- Kelvin 0.0
--
-- === Other conversions
--
-- Any other conversion can be implemented, like for instance logarithmic units.
-- In this case, you should only give an instance for @'ConvertibleUnit'@, and
-- no instance for @'ConversionFactor'@. See for instance linear picth
-- @'Data.Unit.NonStd.Frequency.Tet'@.
--
--------------------------------------------------------------------------------
module Data.Units.Base.Convert
( DimEq
-- * Generic conversion between units
, ConvertibleUnit (..)
, FromTo
, fromTo
, from
, to
, ($~)
, (~&)
-- * Conversion using conversion factors
, ConversionFactor (..)
, toBaseUnit'
, fromBaseUnit'
, FromTo'
, fromTo'
, from'
, to'
)
where
import Data.Proxy
import Data.Kind
import Data.Type.Bool
import Data.Type.Equality
import GHC.TypeError
import Data.Type.Int
import Data.Units.Base.System
-- | A constraint to test whether two units have
type family DimEq (u :: Unit) (v :: Unit) :: Constraint where
DimEq u v = DimEqStd u v (DimOf u) (DimOf v)
type family DimEqStd (u :: Unit) (v :: Unit) (du :: Dim) (dv :: Dim)
:: Constraint where
DimEqStd u v du dv =
( IsUnit u
, IsUnit v
, du ~ dv
, If (du == dv) (() :: Constraint)
(TypeError (
Text "Cannot convert unit ‘"
:<>: ShowUnitType u
:<>: Text "’ to unit ‘"
:<>: ShowUnitType v
:<>: Text "’ because their dimensions do not match."
:$$: Text "Dimension of ‘"
:<>: ShowUnitType u
:<>: Text "’ is: "
:<>: ShowDimType du
:$$: Text "Dimension of ‘"
:<>: ShowUnitType v
:<>: Text "’ is: "
:<>: ShowDimType dv
)))
-- | A unit whose quantities are convertible from that unit to its corresponding
-- base unit.
--
-- Instances must satisfy the following law :
--
-- * @'toBaseUnit' . 'fromBaseUnit' == 'id'@
--
class (IsUnit u, IsUnit (BaseUnitOf u)) => ConvertibleUnit u a where
-- | Convert a quantity to its base unit.
--
-- >>> import Data.Units.NonStd.Time
-- >>> toBaseUnit @Hour 1
-- Second 3600.0
-- >>> toBaseUnit (Hour 1)
-- Second 3600.0
-- >>> toBaseUnit @(Kilo Meter ./. Hour) 36
-- quantity @(Meter .*. Second .^- 1) 10.0
-- >>> toBaseUnit (Celsius 0)
-- Kelvin 273.15
toBaseUnit :: u a -> BaseUnitOf u a
default toBaseUnit :: ConversionFactor u a => u a -> BaseUnitOf u a
toBaseUnit = toBaseUnit'
{-# INLINE toBaseUnit #-}
-- | Convert a quantity from its base unit to another unit.
--
-- >>> fromBaseUnit @Hour 1800
-- Hour 0.5
-- >>> fromBaseUnit 1800 :: Hour Double
-- Hour 0.5
-- >>> fromBaseUnit @(Kilo Meter ./. Hour) 10
-- quantity @(Kilo Meter .*. Hour .^- 1) 36.0
-- >>> fromBaseUnit @Celsius 0
-- Celsius (-273.15)
--
fromBaseUnit :: BaseUnitOf u a -> u a
default fromBaseUnit :: ConversionFactor u a => BaseUnitOf u a -> u a
fromBaseUnit = fromBaseUnit'
{-# INLINE fromBaseUnit #-}
-- | A constraint that is satisfied when both units have the same dimension and
-- are such that @u@ can be converted to @v@.
--
type FromTo u v a = (DimEq u v, ConvertibleUnit u a, ConvertibleUnit v a)
-- | Conversion between two quantities with the same dimension.
--
-- >>> fromTo @Celsius @Kelvin 0
-- Kelvin 273.15
-- >>> fromTo @(Milli Second) @Hour 1
-- Hour 2.7777777777777776e-7
-- >>> fromTo (Milli (Second 1)) :: Hour Double
-- Hour 2.7777777777777776e-7
-- >>> fromTo @Turn @Degree (1/4) -- angle conversion
-- Degree 90.0
-- >>> fromTo @(Kilo Meter ./. Hour) @(Milli Meter ./. Milli Second) 36
-- quantity @(Milli Meter .*. Milli Second .^- 1) 10.0
--
fromTo :: FromTo u v a => u a -> v a
fromTo = fromBaseUnit . toBaseUnit
{-# INLINE fromTo #-}
-- | A mere synonym of @'fromTo'@ where it is more intuitive to use only one
-- type application.
--
-- >>> from @Celsius 0 :: Kelvin Double
-- Kelvin 273.15
--
from :: FromTo u v a => u a -> v a
from = fromTo
{-# INLINE from #-}
-- | Same as @'fromTo'@ but the type applications are reversed
--
-- >>> to @Kelvin (Celsius 0)
-- Kelvin 273.15
--
to :: forall v u a. FromTo u v a => u a -> v a
to = fromTo
{-# INLINE to #-}
-- | A convenient operator for converting a unit before feeding it to a
-- function.
--
-- >>> import Linear
-- >>> rotation (Radian th) = V2 (V2 (cos th) (- sin th)) (V2 (sin th) (cos th))
-- >>> rotation $~ Degree 90
-- V2 (V2 6.123031769111886e-17 (-1.0)) (V2 1.0 6.123031769111886e-17)
--
($~) :: FromTo u v a => (v a -> b) -> u a -> b
f $~ x = f (fromTo x)
{-# INLINE ($~) #-}
infixr 0 $~
-- | Same as @'($~)'@ but with arguments flipped.
--
(~&) :: FromTo u v a => u a -> (v a -> b) -> b
(~&) = flip ($~)
{-# INLINE (~&) #-}
infixl 1 ~&
--------------------------------------------------------------------------------
-- | Unit that can be converted to their corresponding standard unit by
-- multiplication with a conversion factor.
--
-- Instances must satisfy the following laws:
--
-- * @'toBaseUnit' @u == 'quantity' ('unQuantity' q * 'factor' @u)@
-- * @'fromBaseUnit' @u == 'quantity' (''unQuantity' q / 'factor' @u)@
--
class (ConvertibleUnit u a, Fractional a) => ConversionFactor u a where
{-# MINIMAL factor #-}
-- | Multiplying a quantity of type @u a@ with @'factor'@ will convert it
-- to its corresponding base unit @BaseUnitOf u a@
--
-- >>> factor @Hour :: Double
-- 3600.0
-- >>> factor @Celsius :: Double
-- 1.0
-- >>> factor @(Kilo Meter ./. Hour) :: Double
-- 0.2777777777777778
factor :: a
instance Fractional a => ConvertibleUnit NoUnit a
instance Fractional a => ConversionFactor NoUnit a where
factor = 1
{-# INLINE factor #-}
instance (Num a, ConversionFactor u a, ConversionFactor v a, IsUnit (BaseUnitOf (u .*. v)))
=> ConvertibleUnit (u .*. v) a
instance (Num a, ConversionFactor u a, ConversionFactor v a, IsUnit (BaseUnitOf (u .*. v)))
=> ConversionFactor (u .*. v) a where
factor = factor @u * factor @v
{-# INLINE factor #-}
instance (ConversionFactor u a, IsUnit (BaseUnitOf (u .^. n)), KnownInt n)
=> ConvertibleUnit (u .^. n) a
instance (ConversionFactor u a, IsUnit (BaseUnitOf (u .^. n)), KnownInt n)
=> ConversionFactor (u .^. n) a where
factor = factor @u ^^ intVal (Proxy :: Proxy n)
{-# INLINE factor #-}
-- | Convert a quantity to its corresponding base unit by multiplying it
-- by @'factor'@.
--
-- >>> toBaseUnit' @Hour 1
-- Second 3600.0
-- >>> toBaseUnit' (Hour 1)
-- Second 3600.0
-- >>> toBaseUnit' @(Kilo Meter ./. Hour) 36
-- quantity @(Meter .*. Second .^- 1) 10.0
-- >>> toBaseUnit' (Celsius 0)
-- Kelvin 0.0
--
toBaseUnit' :: forall u a. ConversionFactor u a
=> u a -> BaseUnitOf u a
toBaseUnit' q = quantity (unQuantity q * factor @u)
{-# INLINE toBaseUnit' #-}
-- | Convert a standard quantity to a unit @u@ by dividing it by
-- by @'factor'@.
--
-- >>> fromBaseUnit' @Hour 1800
-- Hour 0.5
-- >>> fromBaseUnit' 1800 :: Hour Double
-- Hour 0.5
-- >>> fromBaseUnit' @(Kilo Meter ./. Hour) 10
-- quantity @(Kilo Meter .*. Hour .^- 1) 36.0
-- >>> fromBaseUnit' @Celsius 0
-- Celsius 0.0
--
fromBaseUnit' :: forall u a. ConversionFactor u a
=> BaseUnitOf u a -> u a
fromBaseUnit' q = quantity (unQuantity q / factor @u)
{-# INLINE fromBaseUnit' #-}
-- | A constraint that is satisfied when both units have the same dimension and
-- are such that @u@ can be converted to @v@ by using a conversion factor.
--
type FromTo' u v a = (DimEq u v, ConversionFactor u a, ConversionFactor v a)
-- | Conversion, using conversion factors, between two quantities with the same
-- dimension
--
-- >>> fromTo' @Celsius @Kelvin 0
-- Kelvin 0.0
-- >>> fromTo' @(Milli Second) @Hour 1
-- Hour 2.7777777777777776e-7
-- >>> fromTo' (Milli (Second 1)) :: Hour Double
-- Hour 2.7777777777777776e-7
-- >>> fromTo' @Turn @Degree (1/4) -- angle conversion
-- Degree 90.0
-- >>> fromTo' @(Kilo Meter ./. Hour) @(Milli Meter ./. Milli Second) 36
-- quantity @(Milli Meter .*. Milli Second .^- 1) 10.0
--
fromTo' :: forall u v a.
FromTo' u v a
=> u a -> v a
fromTo' q = quantity (unQuantity q * (factor @u / factor @v))
{-# INLINE fromTo' #-}
-- | A mere synonym of @'fromTo''@ where it is more intuitive to use only one
-- type application.
--
-- >>> from' @Celsius 0 :: Kelvin Double
-- Kelvin 0.0
--
from' :: FromTo' u v a => u a -> v a
from' = fromTo'
{-# INLINE from' #-}
-- | Same as @'fromTo''@ but the type applications are reversed
--
-- >>> to' @Kelvin (Celsius 0)
-- Kelvin 0.0
--
to' :: forall v u a. FromTo' u v a => u a -> v a
to' = fromTo'
{-# INLINE to' #-}