packages feed

convert-units-0: src/Data/Units/Base/Arithmetic.hs

--------------------------------------------------------------------------------
-- |
--
-- Module      :  Data.Units.Base.Arithmetic
-- Description :  Addition, multiplication and exponentiation of quantities
-- Copyright   :  (c) Alice Rixte 2025
-- License     :  BSD 3
-- Maintainer  :  alice.rixte@u-bordeaux.fr
-- Stability   :  unstable
-- Portability :  non-portable (GHC extensions)
--
-- Addition, multiplication and exponentiation of quantities. Dimension analysis is done statically via the type system.
--
-- == Addition and multiplication of a quantity by a scalar
--
-- To add, multiply, divide, and so on, a quantity with a scalar , use its @'Fractional'@ instance :
--
-- >>> a = Milli (Second 5)
-- >>> 3 * a
-- quantity @(Milli Second) 15
--
-- [Warning] These instances are provided because they are convenient, but be careful !  This means that you can write:
--
-- >>> Second 2 * Second 3
-- Second 6
--
-- which does not respect dimension analysis: the multiplication of two time
-- quantities should be of dimension @T²@ and here it has dimension @T@.
--
-- Some of the operators proposed here solve this problem:
--
-- >>> Second 2 .*~ Second 3
-- quantity @(Second .^+ 2) 6
--
--------------------------------------------------------------------------------

module Data.Units.Base.Arithmetic
  (
  -- ** Addition
    (.+~)
  , (~+.)
  , (~+~)
  -- ** Subtraction
  , (.-~)
  , (~-.)
  , (~-~)
  -- ** Multiplication
  , (.*.)
  , (.*~)
  , (~*.)
  , (~*~)
  -- ** Division
  , (./.)
  , (./~)
  , (~/.)
  , (~/~)
  -- ** Exponentiation
  , (.^.)
  , (~^.)
  , (.^~)
  , (~^~)
  ) where

import Data.Type.Int

import Data.Units.Base.System
import Data.Units.Base.Convert

----------------------------------- Addition -----------------------------------

-- | Add two quantities of same dimension. The unit of the right operand is
-- converted to the unit of the left operand
--
-- >>> Kilo (Meter 5) .+~ Meter 80
-- quantity @(Kilo Meter) 5.08
--
-- >>> Meter 2 .+~ Second 3
--  • Cannot convert unit ‘s’ of dimension ‘T’
--    to unit ‘m’ of dimension ‘L’.
--
(.+~) :: forall u v a. FromTo' v u a => u a -> v a -> u a
u .+~ v = quantity (unQuantity u + unQuantity (fromTo' v :: u a))
{-# INLINE (.+~) #-}

infixr 5 .+~

-- | Same as @'(.+~)'@ but it is the left operand that is converted.
--
-- >>> Kilo (Meter 5) ~+. Meter 80
-- quantity @(Kilo Meter) 5080.0
--
(~+.) :: FromTo' u v a => u a -> v a -> v a
(~+.) = flip (.+~)
{-# INLINE (~+.) #-}

infixr 5 ~+.

-- | Add two quantities of same dimension and convert to the standard unit.
--
-- >>> Kilo (Meter 1) ~+~ Milli (Meter 150)
-- Meter 1000.15
--
(~+~) ::
  ( DimEq u v
  , ConversionFactor u a, ConversionFactor v a
  )
 => u a -> v a -> (BaseUnitOf u) a
u ~+~ v = quantity (unQuantity (toBaseUnit' u) + unQuantity (toBaseUnit' v))
{-# INLINE (~+~) #-}

infixr 5 ~+~

--------------------------------- Subtraction ----------------------------------

-- | Subtract two quantities of same dimension. The unit of the right operand is converted to the unit of the left operand
--
-- >>> Kilo (Meter 5) .-~ Meter 80
-- quantity @(Kilo Meter) 4.92
--
(.-~) :: forall u v a. FromTo' v u a => u a -> v a -> u a
u .-~ v = quantity (unQuantity u - unQuantity (fromTo' v :: u a))
{-# INLINE (.-~) #-}

infixr 5 .-~

-- | Same as @'(.-~)'@ but it is the left operand that is converted.
--
-- >>> Kilo (Meter 5) ~-. Meter 80
-- Meter 4920.0
--
(~-.) :: forall u v a. FromTo' u v a => u a -> v a -> v a
u ~-. v = quantity (unQuantity (fromTo' u :: v a) - unQuantity v)
-- {-# INLINE (~-.) #-}

infixr 5 ~-.

-- | Subtract two quantities of same dimension and convert to the standard unit.
--
-- >>> Kilo (Meter 1) ~-~ Milli (Meter 150)
-- Meter 999.85
--
(~-~) ::
  ( DimEq u v
  , ConversionFactor v a, ConversionFactor u a
  )
 => u a -> v a -> (BaseUnitOf u) a
u ~-~ v = quantity $ unQuantity (toBaseUnit' u) - unQuantity (toBaseUnit' v)
{-# INLINE (~-~) #-}

infixr 5 ~-~

-------------------------------- Multiplication --------------------------------



-- | Multiply two quantities.
--
-- Usage is not recommended, as this will result non standard units.
--
-- For instance:
--
-- >>> Kilo (Meter 2) .*. Milli (Meter 4)
-- quantity @(Kilo Meter .*. Milli Meter) 8
--
(.*.) ::
  ( IsUnit u, IsUnit v
  , Num a
  )
 => u a -> v a -> (u .*. v) a
u .*. v = quantity $ unQuantity u * unQuantity v
{-# INLINE (.*.) #-}

infixr 7 .*.

-- | Multiply two quantities, and tries to normalize the resulting unit, without
-- converting to base units.
--
-- >>> Meter 2 .*~ Meter 3 .*~ Meter 4
-- quantity @(Meter.^+3) 24
--
-- When two multiplied units have the same dimension, the right most unit is
-- converted to left most unit:
--
-- >>> Milli (Meter 2) .*~ Micro (Meter 3)
-- quantity @(Milli Meter.^+2) 6.0e-3
--
-- Derived units are not unfolded:
--
-- >>> Kilo Watt 3 .*~ Hour 5
-- quantity @(Kilo Watt .*. Hour) 14.999999999999998
--
-- Units are ordered, so that the result unit do not depend on the order of the
-- computations.
--
-- >>> Meter 2 .*~ Newton 2 .*~ Kilo (Meter 2) .*~ Kilo (Gram 1)
-- quantity @(Newton .*. Kilo Gram .*. Meter.^+2) 8000.0
--
(.*~) :: forall u v a uv.
  ( uv ~ u .*~ v
  , FromTo' (u .*. v) uv a
  , IsUnit u, IsUnit v, IsUnit uv
  , Num a
  )
  => u a -> v a -> uv a
u .*~ v = to' @uv (u .*. v)
{-# INLINE (.*~) #-}

infixr 7 .*~

-- | Same as '(.*~)' but with right priority
--
-- >>> Meter 2 ~*. Meter 3 ~*. Meter 4
-- quantity @(Meter.^+3) 24
--
-- >>> Milli (Meter 2) ~*. Micro (Meter 3)
-- quantity @(Micro Meter.^+2) 6000.000000000001
--
(~*.) :: forall u v a uv.
  ( uv ~ u ~*. v
  , FromTo' (u .*. v) uv a
  , IsUnit u, IsUnit v, IsUnit uv
  , Num a
  )
  => u a -> v a -> uv a
u ~*. v = to' @uv (u .*. v)
{-# INLINE (~*.) #-}

infixr 7 ~*.

-- | Multiply two quantities of the same dimension and convert both of them to the corresponding standard unity.
--
-- >>> Milli (Meter 2) ~*~ Kilo (Meter 3)
-- quantity @(Meter .^+ 2) 6.0
--
-- >>> Meter 2 ~*~ Second 5
-- • Cannot convert unit ‘m’ to unit ‘s’ because their dimensions do not match.
--   Dimension of ‘m’ is: L
--   Dimension of ‘s’ is: T
--
(~*~) ::
  ( u2 ~ BaseUnitOf u .^+ 2, IsUnit u2
  , DimEq u v
  , ConversionFactor u a, ConversionFactor v a
  )
 => u a -> v a -> u2 a
u ~*~ v = quantity $ unQuantity (toBaseUnit' u) * unQuantity (toBaseUnit' v)
{-# INLINE (~*~) #-}

infix 7 ~*~


----------------------------------- Division -----------------------------------

-- | Multiply two quantities.
--
-- Usage is not recommended, as this will result non standard units.
--
-- For instance:
--
-- >>> Kilo (Meter 2) ./. Milli (Meter 4)
-- quantity @(Kilo Meter .*. Milli Meter.^-1) 0.5
--
(./.) ::
  ( IsUnit u, IsUnit v, IsUnit (u ./. v)
  , Fractional a
  )
  => u a -> v a -> (u ./. v) a
u ./. v = quantity (unQuantity u / unQuantity v)
{-# INLINE (./.) #-}

infix 7 ./.

-- | Same '(.*~)' but for division.
--
-- >>> Milli (Meter 3) ./~ quantity @(Meter .^+ 2) 2
-- quantity @(Milli Meter.^-1) 1.5e-6
--
(./~) :: forall u v a uv.
  ( uv ~ u ./~ v
  , FromTo' (u ./. v) uv a
  , IsUnit u, IsUnit v, IsUnit uv
  , Num a
  )
  => u a -> v a -> uv a
u ./~ v = to' @uv (u ./. v)
{-# INLINE (./~) #-}

infix 7 ./~

-- | Same '(~/.)' but with right priority
--
-- >>> Milli (Meter 3) ~/. quantity @(Meter .^+ 2) 2
-- quantity @(Meter.^-1) 1.5e-3
--
(~/.) :: forall u v a uv.
  ( uv ~ u ~/. v
  , FromTo' (u ./. v) uv a
  , IsUnit u, IsUnit v, IsUnit uv
  , Num a
  )
  => u a -> v a -> uv a
u ~/. v = to' @uv (u ./. v)
{-# INLINE (~/.) #-}

infix 7 ~/.

-- | Divide two quantities of same dimensions. The numerator will be converted
-- to the denominator
--
-- Units of the same dimension are authorized only when the units are equal.
--
-- >>> Meter 4 ~/~ Kilo (Meter 1)
-- NoUnit 4.0e-3
--
(~/~) ::
  ( DimEq u v
  , ConversionFactor u a, ConversionFactor v a
  )
  => u a -> v a -> NoUnit a
u ~/~ v = quantity $ unQuantity (toBaseUnit' u)  / unQuantity (toBaseUnit' v)
{-# INLINE (~/~) #-}

infix 6 ~/~

-------------------------------- Exponentiation --------------------------------

-- | Raise a quantity to a power.
--
-- This is meant to be used with @'Data.Type.Int.Proxy'@
--
-- >>> Meter 2 .^. pos2
-- quantity @(Meter.^+2) 4.
--
-- Usage is not recommended, as this will result non standard units.
--
-- For instance:
--
-- >>> (Meter 2 .*. Centi (Meter 30)) .^. pos2
-- quantity @((Meter .*. Centi Meter).^+2) 3600.0
--
(.^.) :: forall (n :: ZZ) proxy u a. (IsUnit u, KnownInt n, Fractional a)
  => u a -> proxy n -> (u .^. n) a
u .^. p = quantity $ unQuantity u ^^ intVal p
{-# INLINE (.^.) #-}

infix 8 .^.

-- | Raise a quantity to a power and tries to normalize the resulting unit,
-- without converting to base units.
--
-- >>> Meter 2 ~^. pos2
-- quantity @(Meter.^+2) 4.0
--
-- >>> (Meter 2 .*. Centi (Meter 30)) ~^. pos2
-- quantity @(Centi Meter.^+4) 3.6e7
(~^.) :: forall (n :: ZZ) proxy u a un.
  (un ~ u ~^. n, FromTo' (u .^. n) un a, IsUnit u, KnownInt n, Fractional a)
  => u a -> proxy n -> un a
u ~^. p = to' @un (u .^. p)
{-# INLINE (~^.) #-}

infix 8 ~^.

-- | Same as @'(.^~)'@ but with priority to rightmost units.
--
-- >>> Meter 2 .^~ pos2
-- quantity @(Meter.^+2) 4.0
--
-- >>> (Meter 2 .*. Centi (Meter 30)) ~^. pos2
-- quantity @(Meter.^+4) 0.36000000000000004
--
(.^~ ) :: forall (n :: ZZ) proxy u a un.
  (un ~ u .^~  n, FromTo' (u .^. n) un a, IsUnit u, KnownInt n, Fractional a)
  => u a -> proxy n -> un a
u .^~  p = to' @un (u .^. p)
{-# INLINE (.^~ ) #-}

infix 8 .^~

-- | Raise a quantity to a power and convert to the standard unit.
--
-- >>> Kilo (Meter 2) ~^~ neg1
-- quantity @(Meter .^- 1) 5.0e-4
--
(~^~) :: forall (n :: ZZ) proxy u un a.
  (KnownInt n, ConversionFactor u a, un ~ BaseUnitOf u .^. n )
  => u a -> proxy n -> un a
u ~^~ p = quantity @un $ unQuantity (toBaseUnit' u) ^^ intVal p
{-# INLINE (~^~) #-}

infix 8 ~^~