convert-units (empty) → 0
raw patch · 43 files changed
+4927/−0 lines, 43 filesdep +QuickCheckdep +basedep +convert-unitssetup-changed
Dependencies added: QuickCheck, base, convert-units, criterion, hspec, linear, template-haskell
Files
- CHANGELOG.md +11/−0
- LICENSE +28/−0
- README.md +174/−0
- Setup.hs +2/−0
- benchmark/Main.hs +42/−0
- convert-units.cabal +158/−0
- src/Data/Type/Int.hs +142/−0
- src/Data/Type/Int/Proxy.hs +42/−0
- src/Data/Units.hs +9/−0
- src/Data/Units/AngleSI.hs +37/−0
- src/Data/Units/AngleSI/Derived.hs +63/−0
- src/Data/Units/AngleSI/NonStd.hs +10/−0
- src/Data/Units/AngleSI/NonStd/Angle.hs +65/−0
- src/Data/Units/AngleSI/System.hs +50/−0
- src/Data/Units/Base.hs +27/−0
- src/Data/Units/Base/Arithmetic.hs +393/−0
- src/Data/Units/Base/Convert.hs +393/−0
- src/Data/Units/Base/Prefix.hs +166/−0
- src/Data/Units/Base/System.hs +916/−0
- src/Data/Units/Base/TH.hs +441/−0
- src/Data/Units/NonStd.hs +10/−0
- src/Data/Units/NonStd/Frequency.hs +69/−0
- src/Data/Units/NonStd/Temperature.hs +25/−0
- src/Data/Units/NonStd/Time.hs +36/−0
- src/Data/Units/SI.hs +25/−0
- src/Data/Units/SI/Derived.hs +24/−0
- src/Data/Units/SI/Derived/Angle.hs +84/−0
- src/Data/Units/SI/Derived/NonAngle.hs +255/−0
- src/Data/Units/SI/NonStd.hs +24/−0
- src/Data/Units/SI/NonStd/Angle.hs +54/−0
- src/Data/Units/SI/Prefixes.hs +145/−0
- src/Data/Units/SI/System.hs +174/−0
- test/Data/Epsilon.hs +40/−0
- test/Data/Units/Base/ArithmeticProp.hs +454/−0
- test/Data/Units/Base/ConvertProp.hs +109/−0
- test/Data/Units/BaseProp.hs +45/−0
- test/Data/Units/NonStd/AngleSpec.hs +30/−0
- test/Data/Units/NonStd/FrequencySpec.hs +17/−0
- test/Data/Units/NonStd/TemperatureSpec.hs +18/−0
- test/Data/Units/NonStd/TimeSpec.hs +13/−0
- test/Data/Units/SI/PrefixSpec.hs +85/−0
- test/Data/Units/SI/SystemSpec.hs +21/−0
- test/Spec.hs +1/−0
+ CHANGELOG.md view
@@ -0,0 +1,11 @@+# Changelog for `tiles` + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to the +[Haskell Package Versioning Policy](https://pvp.haskell.org/). + +## Unreleased + +## 0.1.0.0 - YYYY-MM-DD
+ LICENSE view
@@ -0,0 +1,28 @@+BSD 3-Clause License + +Copyright (c) 2025, Alice Rixte + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,174 @@+# convert-units + +[](https://haskell.org) [](https://github.com/AliceRixte/convert-units/LICENSE) [](https://hackage.haskell.org/package/convert-units) [](https://www.stackage.org/nightly/package/convert-units) [](https://www.stackage.org/lts/package/convert-units) + +A Haskell library to convert between units, that will statically check the dimensions of the units being converted. + +Conversions are usually as fast as manual multiplication by a conversion factor, thanks to heavy use of inlining. + +## Usage + +You will need the `TypeApplications` extension: + +``` haskell +>>> :set -XTypeApplications +``` + +### Convert between units + +You can use `to` or `fromTo` for conversions: +``` haskell +>>> t = Hour 8 +>>> to @Minute t +Minute 480.0 + +>>> fromTo @Hour @Minute 8 +Minute 480.0 +``` + +User-friendly static errors when trying to convert between incompatible dimensions: + +```haskell +>>> fromTo @Minute @Meter 1 +• Cannot convert unit ‘min’ to unit ‘m’ because their dimensions do not match. + Dimension of ‘min’ is: T + Dimension of ‘m’ is: L +``` + +There are two sorts of unit conversions: + +1. The regular ones +``` haskell +>>> fromTo @Celsius @Kelvin 0 +Kelvin 273.15 +``` + +2. Conversion that only takes the conversion factor into account (and not potential offsets): +``` haskell +>>> fromTo' @Celsius @Kelvin 0 +Kelvin 0.0 +``` + +### Pretty printing + +``` haskell +>>> putQuantity (Celsius 25) +25 °C +>>> putQuantity (quantity @(Kilo Meter ./. Hour) 130) +130 km⋅hr⁻¹ +``` + +Get info about some unit: + +``` haskell +>>> putInfoU @Newton +Unit: Newton + abbr: N +Dimension: Mass .*. Length .*. Time.^-2 + abbr: M⋅L⋅T⁻² +Normalized: Kilo Gram .*. Meter .*. Second.^-2 + abbr: kg⋅m⋅s⁻² +``` + +### Unit arithmetics + +Multiplication by a scalar: + +``` haskell +>>> 2 * Meter 4 +Meter 8 +``` + +You can multiply or divide two units: + +``` haskell +>>> putQuantity $ Newton 1 .*. Meter 2 +2 N⋅m +``` + +``` haskell +>>> v = Kilo (Meter 10) ./. Hour 2 +>>> putQuantity v +5.0 km⋅hr⁻¹ +>>> putQuantity $ to @(Meter ./. Second) v +1.3888888888888888 m⋅s⁻¹ +``` + +Automatically simplify units by converting the right unit : +``` haskell +>>> putQuantity $ Kilo (Meter 2) .*~ Meter 3 +6.0e-3 km² +``` + +or the left unit: + +``` haskell +>>> putQuantity $ Kilo (Meter 2) ~*. Meter 3 +6000.0 m² +``` + +### Convert to and from SI base units + +``` haskell +>>> v = toBaseUnit (quantity @(Kilo Meter ./. Hour) 36) +>>> putQuantity v +10.0 m⋅s⁻¹ +``` + +### Make your own units, prefixes and dimensions + +Make a new dimension with its associated base unit: + +``` haskell +$(mkDim "Angle" "A" 1000) +$(mkBaseUnit "Radian" "rad" ''Angle) +``` + +Make a new unit convertible by multiplying with some factor: +``` haskell +$(mkUnit "Minute" "min" ''Time 60) +``` + +Make a new prefix: + +``` haskell +$(mkPrefix "Micro" "µ" 1e-6) +``` + +Make a new unit with special conversion: + +``` haskell +$(mkUnitNoFactor "Fahrenheit" "°F" ''Temperature) + +instance Fractional a => ConversionFactor Fahrenheit a where + factor = 5 / 9 + {-# INLINE factor #-} + +instance Fractional a => ConvertibleUnit Fahrenheit a where + toBaseUnit (Fahrenheit x) = Kelvin ((x + 459.67) * 5 / 9) + {-# INLINE toBaseUnit #-} + + fromBaseUnit (Kelvin x) = Fahrenheit (x * 9 / 5 - 459.67) + {-# INLINE fromBaseUnit #-} +``` + +## Comparison with other Haskell unit libraries + +There are other excellent units libraries out there, the two most used being: +- [dimensional](https://hackage.haskell.org/package/dimensional) +- [units](https://hackage.haskell.org/package/units) + +Compared to these two libraries, `convert-units` offers + +* Greater flexibility for conversions that do not use conversion factors, for instance for logarithmic units (see logarithmic pitch units in `Data.Unit.NonStd.Frequency` for instance) +* The possibility to add dimensions, such as `Angle`, `Information` (not yet implemented, see this [wikipedia article](https://en.wikipedia.org/wiki/Quantities_of_information)), and so on ... + +| Feature | convert-units | dimensional | units | +|---------------------------------------|:------------:|:-----------:|:-----:| +| Static dimension checking | ✅ | ✅ | ✅ | +| Custom unit | ✅ | ✅ | ✅ | +| Custom prefixes | ✅ | ✅ | ✅ | +| Custom dimensions | ✅ | ❌ | ❌ | +| Pretty-printing units | ✅ | ✅ | ✅ | +| Offset-aware conversions (e.g. °C/K) | ✅ | ✅ | ❌ | +| Any conversion | ✅ | ❌ | ❌ |
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple +main = defaultMain
+ benchmark/Main.hs view
@@ -0,0 +1,42 @@+{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE OverloadedLabels #-} +{-# LANGUAGE DataKinds#-} +{-# LANGUAGE TypeOperators#-} + +{-# OPTIONS_GHC -ddump-to-file #-} + +module Main (main) where + +import Criterion.Main + +import Data.Units + +mkBench f n = bench (show n) $ nf f n + +iterations :: [Int] +iterations = [10, 100, 1000] + + +kmphTomps :: Int -> Double +kmphTomps n = iterate + (unQuantity . fromTo @(Kilo Meter ./. Hour) @(Meter ./. Second) . quantity) + 5 !! n + +multConv :: Int -> Double +multConv n = iterate (*3.6) (5 :: Double) !! n + +-- | Check ddump to see it inlines correctly +u :: (.^-) Second 1 Double +u = fromTo (1415 :: Hertz Double) + +-- main :: IO() +-- main = print u + +main :: IO() +main = do + defaultMain [ + bgroup "~>" [ + bgroup "kmph ~> mps" (fmap (mkBench kmphTomps) iterations) + , bgroup "mult" (fmap (mkBench multConv) iterations) + ] + ]
+ convert-units.cabal view
@@ -0,0 +1,158 @@+cabal-version: 2.2 ++-- This file has been generated from package.yaml by hpack version 0.37.0.+--+-- see: https://github.com/sol/hpack++name: convert-units+version: 0+synopsis: Arithmetic and type checked conversions between units.+description: Please see the README on GitHub at <https://github.com/AliceRixte/convert-units/blob/main/README.md>+category: Physics, Data, Numeric, Math+homepage: https://github.com/AliceRixte/convert-units#readme+bug-reports: https://github.com/AliceRixte/convert-units/issues+author: Alice Rixte+maintainer: alice.rixte@u-bordeaux.fr+copyright: BSD 3+license: BSD-3-Clause+license-file: LICENSE+build-type: Simple+extra-source-files:+ README.md+extra-doc-files:+ CHANGELOG.md++source-repository head+ type: git+ location: https://github.com/AliceRixte/convert-units++library+ exposed-modules:+ Data.Type.Int+ Data.Type.Int.Proxy+ Data.Units+ Data.Units.AngleSI+ Data.Units.AngleSI.Derived+ Data.Units.AngleSI.NonStd+ Data.Units.AngleSI.NonStd.Angle+ Data.Units.AngleSI.System+ Data.Units.Base+ Data.Units.Base.Arithmetic+ Data.Units.Base.Convert+ Data.Units.Base.Prefix+ Data.Units.Base.System+ Data.Units.Base.TH+ Data.Units.NonStd+ Data.Units.NonStd.Frequency+ Data.Units.NonStd.Temperature+ Data.Units.NonStd.Time+ Data.Units.SI+ Data.Units.SI.Derived+ Data.Units.SI.Derived.Angle+ Data.Units.SI.Derived.NonAngle+ Data.Units.SI.NonStd+ Data.Units.SI.NonStd.Angle+ Data.Units.SI.Prefixes+ Data.Units.SI.System+ other-modules:+ Paths_convert_units+ autogen-modules:+ Paths_convert_units+ hs-source-dirs:+ src+ default-extensions:+ TypeApplications+ StandaloneDeriving+ DerivingVia+ GeneralizedNewtypeDeriving+ TypeOperators+ PolyKinds+ ScopedTypeVariables+ DataKinds+ TypeFamilies+ FlexibleInstances+ UndecidableInstances+ MultiParamTypeClasses+ FlexibleContexts+ TemplateHaskell+ ghc-options: -Wall -threaded+ build-depends:+ base >=4.18 && <5+ , template-haskell >=2.21.0+ default-language: Haskell2010++test-suite convert-units-test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ Data.Epsilon+ Data.Units.Base.ArithmeticProp+ Data.Units.Base.ConvertProp+ Data.Units.BaseProp+ Data.Units.NonStd.AngleSpec+ Data.Units.NonStd.FrequencySpec+ Data.Units.NonStd.TemperatureSpec+ Data.Units.NonStd.TimeSpec+ Data.Units.SI.PrefixSpec+ Data.Units.SI.SystemSpec+ Paths_convert_units+ autogen-modules:+ Paths_convert_units+ hs-source-dirs:+ test+ default-extensions:+ TypeApplications+ StandaloneDeriving+ DerivingVia+ GeneralizedNewtypeDeriving+ TypeOperators+ PolyKinds+ ScopedTypeVariables+ DataKinds+ TypeFamilies+ FlexibleInstances+ UndecidableInstances+ MultiParamTypeClasses+ FlexibleContexts+ TemplateHaskell+ ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ QuickCheck >=2.14+ , base >=4.18 && <5+ , convert-units+ , hspec >=2.11+ , linear+ , template-haskell >=2.21.0+ default-language: Haskell2010++benchmark convert-units-bench+ type: exitcode-stdio-1.0+ main-is: Main.hs+ other-modules:+ Paths_convert_units+ autogen-modules:+ Paths_convert_units+ hs-source-dirs:+ benchmark+ default-extensions:+ TypeApplications+ StandaloneDeriving+ DerivingVia+ GeneralizedNewtypeDeriving+ TypeOperators+ PolyKinds+ ScopedTypeVariables+ DataKinds+ TypeFamilies+ FlexibleInstances+ UndecidableInstances+ MultiParamTypeClasses+ FlexibleContexts+ TemplateHaskell+ ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ base >=4.18 && <5+ , convert-units+ , criterion >=1.6+ , template-haskell >=2.21.0+ default-language: Haskell2010
+ src/Data/Type/Int.hs view
@@ -0,0 +1,142 @@+{-# LANGUAGE NoStarIsType #-} +{-# LANGUAGE ExistentialQuantification #-} + +-------------------------------------------------------------------------------- +-- | +-- +-- Module : Data.Type.Int +-- Description : Type-level integers +-- Copyright : (c) Alice Rixte 2025 +-- License : BSD 3 +-- Maintainer : alice.rixte@u-bordeaux.fr +-- Stability : unstable +-- Portability : non-portable (GHC extensions) +-- +-- Type level integers. +-- +-------------------------------------------------------------------------------- + + +module Data.Type.Int + ( module Data.Type.Int + ) where + +import GHC.TypeLits +import Data.Type.Ord +import Data.Type.Equality +import Data.Type.Bool + +import Data.Proxy + +-- | Add a sign to any type +-- +data Signed a = Pos a | Neg a | Zero + +-- | Type integers +-- +-- ZZ represents the mathematical font for the set of integers +type ZZ = Signed Nat + + + +type instance Compare (a :: Signed k) (b :: Signed k) = CmpSigned a b + +type family IsPos (a :: ZZ) :: Bool where + IsPos (Pos a) = 'True + IsPos b = 'False + +-- | Compare Signed kinds when those kinds are comparable. +type family CmpSigned a b where + CmpSigned (Neg a) (Neg b) = FlipOrdering (Compare a b) + CmpSigned (Neg a) b = 'LT + CmpSigned Zero (Neg a) = 'GT + CmpSigned Zero Zero = 'EQ + CmpSigned Zero (Pos a) = 'GT + CmpSigned (Pos a) (Pos b) = Compare a b + CmpSigned (Pos a) b = 'GT + +-- | Always use @Zero@ instead of @Pos 0@ or @Neg 0@. +type family NormalizeInt (a :: ZZ) :: ZZ where + NormalizeInt (Pos 0) = Zero + NormalizeInt (Neg 0) = Zero + NormalizeInt n = n + + +-- | Reverse the order of an Ordering +-- +-- This should be declared in to Data.Type.Ord in base +type family FlipOrdering (o :: Ordering) :: Ordering where + FlipOrdering 'LT = 'GT + FlipOrdering 'EQ = 'EQ + FlipOrdering 'GT = 'LT + + +-- | Absolute value +-- +type family Abs (a :: ZZ) :: Nat where + Abs (Pos a) = a + Abs (Neg a) = a + Abs Zero = 0 + +-- | Unary negation +-- +type family Negate (a :: ZZ) :: ZZ where + Negate (Pos a) = Neg a + Negate (Neg a) = Pos a + Negate Zero = Zero + + +-- | Utility family for Add +-- +type family AddCmp (cmp :: Ordering) (a :: ZZ) (b :: ZZ) where + AddCmp _ a Zero = a + AddCmp _ Zero b = b + AddCmp _ (Pos a) (Pos b) = Pos (a + b) + AddCmp _ (Neg a) (Neg b) = Neg (a + b) + AddCmp EQ _ _ = Zero + AddCmp LT (Pos a) (Neg b) = Neg (b - a) + AddCmp GT (Pos a) (Neg b) = Pos (a - b) + AddCmp LT (Neg a) (Pos b) = Pos (b - a) + AddCmp GT (Neg a) (Pos b) = Neg (a - b) + +-- | Addition +type family Add (a :: ZZ) (b :: ZZ) :: ZZ where + Add a b = AddCmp (Compare (Abs a) (Abs b)) a b + +-- | Subtraction +type family Sub (a :: ZZ) (b :: ZZ) :: ZZ where + Sub a b = Add a (Negate b) + +-- | Multiplication +type family Mul (a :: ZZ) (b :: ZZ) :: ZZ where + Mul a Zero = a + Mul Zero b = b + Mul (Pos a) (Pos b) = Pos (a * b) + Mul (Pos a) (Neg b) = Neg (a * b) + Mul (Neg a) (Pos b) = Neg (a * b) + Mul (Neg a) (Neg b) = Pos (a * b) + + +-- | Exponentiation +type family Pow (a :: ZZ) (n :: Nat) :: ZZ where + Pow Zero 0 = Pos 1 -- Following Nat from Base : 0^0 :: Natural = 1 + Pow Zero n = Zero + Pow (Pos a) n = Pos (a ^ n) + Pow (Neg a) n = If (Mod n 2 == 0) (Pos (a^n)) (Neg (a^n)) + + +-- | Gives the integer associated to a type-level integer. +class KnownInt (r :: ZZ) where + -- | Reify a type integer to an integer. + intVal :: proxy r -> Integer + +instance KnownInt Zero where + intVal _ = 0 + +instance KnownNat n => KnownInt (Pos n) where + intVal _ = natVal (Proxy :: Proxy n) + +instance KnownNat n => KnownInt (Neg n) where + intVal _ = -natVal (Proxy :: Proxy n) + +
+ src/Data/Type/Int/Proxy.hs view
@@ -0,0 +1,42 @@+module Data.Type.Int.Proxy + ( zero + , pos1 + , neg1 + , pos2 + , neg2 + , pos3 + , neg3 + , pos4 + , neg4 + ) + where + +import Data.Proxy +import Data.Type.Int + +zero :: Proxy Zero +zero = Proxy + +pos1 :: Proxy (Pos 1) +pos1 = Proxy + +neg1 :: Proxy (Neg 1) +neg1 = Proxy + +pos2 :: Proxy (Pos 2) +pos2 = Proxy + +neg2 :: Proxy (Neg 2) +neg2 = Proxy + +pos3 :: Proxy (Pos 3) +pos3 = Proxy + +neg3 :: Proxy (Neg 3) +neg3 = Proxy + +pos4 :: Proxy (Pos 4) +pos4 = Proxy + +neg4 :: Proxy (Neg 4) +neg4 = Proxy
+ src/Data/Units.hs view
@@ -0,0 +1,9 @@+module Data.Units + ( module Data.Units.Base + , module Data.Units.SI + , module Data.Units.SI.NonStd + ) where + +import Data.Units.Base +import Data.Units.SI +import Data.Units.SI.NonStd
+ src/Data/Units/AngleSI.hs view
@@ -0,0 +1,37 @@+ +-------------------------------------------------------------------------------- +-- | +-- +-- Module : Data.Units.AngleSI.Angle +-- Description : SI with an angle dimension @A@ +-- Copyright : (c) Alice Rixte 2025 +-- License : BSD 3 +-- Maintainer : alice.rixte@u-bordeaux.fr +-- Stability : unstable +-- Portability : non-portable (GHC extensions) +-- +-- There is an ongoing debate about including Angle as a dimension of its own, +-- see for instance +-- +-- * [On the dimension of angles and their +-- units](https://iopscience.iop.org/article/10.1088/1681-7575/ac7bc2/pdf) , +-- Peter J Mohr /et all/, in Metrologia +-- * [Angles are inherently neither length ratios nor +-- dimensionless](https://iopscience.iop.org/article/10.1088/1681-7575/ab27d7/pdf), +-- Paul Quincey /et all/, in Metrologia +-- +-- This module adds an Angle dimension to the SI system. To use dimensionless +-- angles, see "Data.Units.SI". +-- +-------------------------------------------------------------------------------- + +module Data.Units.AngleSI + ( module Data.Units.AngleSI.System + , module Data.Units.AngleSI.Derived + , module Data.Units.SI.Prefixes + + ) where + +import Data.Units.AngleSI.System +import Data.Units.AngleSI.Derived +import Data.Units.SI.Prefixes
+ src/Data/Units/AngleSI/Derived.hs view
@@ -0,0 +1,63 @@+-------------------------------------------------------------------------------- +-- | +-- +-- Module : Data.Units.AngleSI.Derived +-- Description : Dimensionless angles +-- Copyright : (c) Alice Rixte 2025 +-- License : BSD 3 +-- Maintainer : alice.rixte@u-bordeaux.fr +-- Stability : unstable +-- Portability : non-portable (GHC extensions) +-- +-- This module defines SI derived units that use angles, where angles have a +-- dimension @`A`@. +-- +-- See "Data.Units.SI.Derived.Angle" for an equivalent of this where angles are +-- dimensionless. +-- +-------------------------------------------------------------------------------- + +module Data.Units.AngleSI.Derived + ( module Data.Units.SI.Derived.NonAngle + , SolidAngle + , Steradian (..) + , LuminousFlux + , Lumen (..) + , Illuminance + , Lux (..) + ) + where + +import Data.Coerce + +import Data.Units.Base +import Data.Units.AngleSI.System +import Data.Units.SI.Derived.NonAngle + + +-- | Solid angle quantity. +type SolidAngle = Angle .^+ 2 + +-- | A solid angle in steradians. +-- +$(mkUnit "Steradian" "sr" ''SolidAngle 1) + +-- | Luminous flux quantity. Equal to +-- +-- @ 'Angle'.^+2 .*. 'LuminousIntensity'@ +-- +type LuminousFlux = NormalizeDim (LuminousIntensity .*. SolidAngle) + +-- | Luminous flux in lumens +-- +$(mkUnit "Lumen" "lm" ''LuminousFlux 1) + +-- | Illuminance quantity. Equal to +-- +-- @ 'Angle'.^+2 .*. 'Length'.^-2 .*. 'LuminousIntensity' @ +-- +type Illuminance = NormalizeDim (LuminousFlux ./. Area) + +-- | Illuminance in lux +-- +$(mkUnit "Lux" "lx" ''Illuminance 1)
+ src/Data/Units/AngleSI/NonStd.hs view
@@ -0,0 +1,10 @@+module Data.Units.AngleSI.NonStd + ( module Data.Units.NonStd + , module Data.Units.AngleSI.Derived + , module Data.Units.AngleSI.NonStd.Angle + ) +where + +import Data.Units.NonStd +import Data.Units.AngleSI.Derived +import Data.Units.AngleSI.NonStd.Angle
+ src/Data/Units/AngleSI/NonStd/Angle.hs view
@@ -0,0 +1,65 @@+ +-------------------------------------------------------------------------------- +-- | +-- +-- Module : Data.Units.AngleSI.NonStd.Angle +-- Description : Non standard angle units with a dimension A +-- Copyright : (c) Alice Rixte 2025 +-- License : BSD 3 +-- Maintainer : alice.rixte@u-bordeaux.fr +-- Stability : unstable +-- Portability : non-portable (GHC extensions) +-- +-- Non standard angle units with a dimension A. +-- +-------------------------------------------------------------------------------- + + +module Data.Units.AngleSI.NonStd.Angle + ( Degree (..) + , Turn (..) + , Gradian (..) + ) +where + +import Data.Units.Base + +import Data.Units.AngleSI.System + + + +-- | Angle in degrees. +-- +$(mkUnitNoFactor "Degree" "°" ''Angle) + +-- | Angle in complete turns (also called cycles or revolutions) +-- +-- See https://en.wikipedia.org/wiki/Turn_(angle) +-- +$(mkUnitNoFactor "Turn" "tr" ''Angle) + +-- | Angle in gradians +-- +-- See https://en.wikipedia.org/wiki/Gradian +-- +$(mkUnitNoFactor "Gradian" "grad" ''Angle) + + +instance Floating a => ConvertibleUnit Degree a + +instance Floating a => ConversionFactor Degree a where + factor = pi / 180 + {-# INLINE factor #-} + +instance Floating a => ConvertibleUnit Turn a + +instance Floating a => ConversionFactor Turn a where + factor = 2 * pi + {-# INLINE factor #-} + +instance Floating a => ConvertibleUnit Gradian a + +instance Floating a => ConversionFactor Gradian a where + factor = pi / 200 + {-# INLINE factor #-} +
+ src/Data/Units/AngleSI/System.hs view
@@ -0,0 +1,50 @@+ +-------------------------------------------------------------------------------- +-- | +-- +-- Module : Data.Units.AngleSI.System +-- Description : SI unit system with dimensional angles of dimension A +-- Copyright : (c) Alice Rixte 2025 +-- License : BSD 3 +-- Maintainer : alice.rixte@u-bordeaux.fr +-- Stability : unstable +-- Portability : non-portable (GHC extensions) +-- +-- This module defines radians as the standard unit for the angle (`@A@`) +-- dimension. +-- +-- See "Data.Units.SI.Angle" for dimensionless radians and steradians. +-- +-------------------------------------------------------------------------------- + + +module Data.Units.AngleSI.System + ( module Data.Units.SI.System + , Angle (..) + , Radian (..) + , normalizeRadians + ) + where + +import Data.Fixed +import Data.Coerce + +import Data.Units.Base +import Data.Units.SI.System + +-- | The angle dimension, denotated @A@. +-- +$(mkDim "Angle" "A" 1000) + + +-- | An angle in radians. +-- +$(mkBaseUnit "Radian" "rad" ''Angle) + +-- | Normalize an angle to the range ]-pi, pi] +normalizeRadians :: (RealFrac a, Floating a) => Radian a -> Radian a +normalizeRadians x = if xmod > pi then xmod - twoPi else xmod + where + twoPi = 2 * pi + xmod = x `mod'` twoPi +
+ src/Data/Units/Base.hs view
@@ -0,0 +1,27 @@+module Data.Units.Base + ( -- ** Core modules + module Data.Units.Base.System + , module Data.Units.Base.Convert + , module Data.Units.Base.Arithmetic + , module Data.Units.Base.Prefix + , module Data.Units.Base.TH + -- ** Re-exported for Template Haskell to work out of the box + -- on @import Data.Units.Base@ + , coerce + -- ** Type level integers + , module Data.Type.Int + , module Data.Type.Int.Proxy + -- ** Re-exported from GHC.TypeError + , ErrorMessage (..) + ) where + +import GHC.TypeError (ErrorMessage(..)) +import Data.Coerce (coerce) + +import Data.Units.Base.System +import Data.Units.Base.Convert +import Data.Units.Base.Arithmetic +import Data.Units.Base.Prefix +import Data.Units.Base.TH +import Data.Type.Int +import Data.Type.Int.Proxy
+ src/Data/Units/Base/Arithmetic.hs view
@@ -0,0 +1,393 @@+-------------------------------------------------------------------------------- +-- | +-- +-- 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 ~^~ + +
+ src/Data/Units/Base/Convert.hs view
@@ -0,0 +1,393 @@+{-# 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' #-} + +
+ src/Data/Units/Base/Prefix.hs view
@@ -0,0 +1,166 @@+{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE QuantifiedConstraints #-} +{-# LANGUAGE ConstraintKinds #-} +{-# LANGUAGE InstanceSigs #-} + +-------------------------------------------------------------------------------- +-- | +-- +-- Module : Data.Units.Base.Prefix +-- Description : Unit prefix for a system of units +-- Copyright : (c) Alice Rixte 2025 +-- License : BSD 3 +-- Maintainer : alice.rixte@u-bordeaux.fr +-- Stability : unstable +-- Portability : non-portable (GHC extensions) +-- +-- Provides a way to define prefixes for any system of units. +-- +-------------------------------------------------------------------------------- + + +module Data.Units.Base.Prefix where + +import GHC.TypeError + +import Data.Units.Base.System +import Data.Units.Base.Convert + +-- | A unit prefix, like Kilo, Milli, etc. +type Prefix = Unit -> Unit + +-- | The application of a prefix to a unit must always be a unit. +class (forall (u :: Unit). IsUnit u => IsUnit (p u)) + => IsPrefix (p :: Prefix) + +instance (forall (u :: Unit). IsUnit u => IsUnit (p u)) + => IsPrefix (p :: Prefix) + +-- | A prefix that has a conversion factor. +-- +class (Fractional a, IsPrefix p) => PrefixFactor (p :: Prefix) a where + -- | Prefix conversion factor from the prefixed unit to the corresponding + -- standard unit + -- + -- >>> prefixFactor @Kilo + -- 1000.0 + -- + prefixFactor :: a + +-- | Prefixes that can be shown as a string, or as a type error message. +class IsPrefix p => ShowPrefix (p :: Prefix) where + {-# MINIMAL showPrefix | showsPrefixPrec #-} + + -- | Allows to print units in conversion error messages + -- + -- >>> type ShowPrefix Kilo = "k" + -- + type ShowPrefixType p :: ErrorMessage + + -- | Convert a prefix to a readable string + -- + -- @'showsPrefixPrec'@ should satisfy the law : + -- + -- @showsPrefixPrec d x r ++ s == showsPrec d x (r ++ s)@ + -- + showsPrefixPrec :: Int -> ShowS + showsPrefixPrec _ = (showPrefix @p ++) + + -- | Convert a prefix to a string representing its type. + -- + -- >>> showPrefix @Kilo + -- "Kilo" + -- + showPrefix :: String + showPrefix = showsPrefix @p "" + + -- | Same as @'showsPrefixPrec'@ but for pretty printing. + -- + -- @'prettysPrefixPrec'@ should satisfy the law : + -- + -- @prettysPrefixPrec d x r ++ s == prettysPrec d x (r ++ s)@ + -- + prettysPrefixPrec :: Int -> ShowS + prettysPrefixPrec _ = (prettyPrefix @p ++) + + -- | Same as @'showPrefix'@ but for pretty printing + -- + -- >>> prettyPrefix @Kilo + -- "k" + prettyPrefix :: String + prettyPrefix = prettysPrefix @p "" + +-- | Equivalent to 'showsPrefixPrec' with a precedence of 0. +showsPrefix :: forall p. ShowPrefix p => ShowS +showsPrefix = showsPrefixPrec @p 0 + +-- | Equivalent to 'prettysPrefixPrec' with a precedence of 0. +prettysPrefix :: forall p. ShowPrefix p => ShowS +prettysPrefix = prettysPrefixPrec @p 0 + + +-- | A prefix that can represent any prefix. +-- +-- This can be used with the `deriving via` mechanism to derive some of the +-- prefix instances. +-- +newtype MetaPrefix (p :: Prefix) (u :: Unit) a = MetaPrefix (p u a) + deriving Show via (MetaUnit (p u) a) + +instance PrefixFactor p a => PrefixFactor (MetaPrefix p) a where + prefixFactor = prefixFactor @p + {-# INLINE prefixFactor #-} + +instance + (PrefixFactor p a, ConversionFactor u a, BaseUnitOf (p u) ~ BaseUnitOf u) + => ConversionFactor (MetaPrefix p u) a where + factor = prefixFactor @p * factor @u + {-# INLINE factor #-} + +instance + (PrefixFactor p a, ConvertibleUnit u a, BaseUnitOf (p u) ~ BaseUnitOf u) + => ConvertibleUnit (MetaPrefix p u) a where + toBaseUnit (MetaPrefix a) = prefixToBaseUnit @p @u a + {-# INLINE toBaseUnit #-} + fromBaseUnit a = MetaPrefix $ prefixFromBaseUnit @p @u a + {-# INLINE fromBaseUnit #-} + +-- | Convert a prefixed unit to the corresponding standard unit. +-- +prefixToBaseUnit :: forall (p :: Prefix) (u :: Unit) a. + (PrefixFactor p a, ConvertibleUnit u a, BaseUnitOf (p u) ~ BaseUnitOf u) + => p u a -> BaseUnitOf u a +prefixToBaseUnit u = + toBaseUnit @u $ quantity @u (prefixFactor @p * unQuantity u) +{-# INLINE prefixToBaseUnit #-} + +-- | Convert a standard unit to the corresponding prefixed unit. +-- +prefixFromBaseUnit :: forall (p :: Prefix) (u :: Unit) a. + (PrefixFactor p a, ConvertibleUnit u a + , BaseUnitOf (p u) a ~ BaseUnitOf u a) + => BaseUnitOf (p u) a -> p u a +prefixFromBaseUnit a = quantity $ unQuantity (fromBaseUnit @u a) / prefixFactor @p +{-# INLINE prefixFromBaseUnit #-} + + +instance ShowPrefix p => ShowPrefix (MetaPrefix p) where + type ShowPrefixType (MetaPrefix p) = ShowPrefixType p + showsPrefixPrec = showsPrefixPrec @p + showPrefix = showPrefix @p + prettysPrefixPrec = prettysPrefixPrec @p + prettyPrefix = prettyPrefix @p + +instance (IsPrefix p, IsUnit u) + => IsUnit (MetaPrefix p u) where + type DimOf (MetaPrefix p u) = DimOf u + +instance (ShowPrefix p, ShowUnit u) + => ShowUnit (MetaPrefix p u) where + type ShowUnitType (MetaPrefix p u) = ShowPrefixType p :<>: ShowUnitType u + showsUnitPrec d = showParen (d > 10) $ + showsPrefix @p . showString " " . showsUnitPrec @u 11 + prettysUnitPrec d = showParen (d > 10) $ + prettysPrefix @p . prettysUnit @u +
+ src/Data/Units/Base/System.hs view
@@ -0,0 +1,916 @@+{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE DeriveFunctor #-} +{-# LANGUAGE QuantifiedConstraints #-} + +-------------------------------------------------------------------------------- +-- | +-- +-- Module : Data.Units.Base.System +-- Description : System of units +-- Copyright : (c) Alice Rixte 2025 +-- License : BSD 3 +-- Maintainer : alice.rixte@u-bordeaux.fr +-- Stability : unstable +-- Portability : non-portable (GHC extensions) +-- +-- Describe a system of units and their dimensions. +-- +-------------------------------------------------------------------------------- + +module Data.Units.Base.System + ( + -- * Dimensions + Dim + , DimId + , IsDim (..) + , ShowDim (..) + , prettysDim + , showsDim + , showDimOf + , prettyDimOf + , putDimOf + , NormalizeDim + + -- * Units + , Unit + , ShowUnit (..) + , prettysUnit + , showsUnit + , prettyUnitInfo + , putInfoU + , IsUnit (..) + + -- * Quantity + , quantity + , unQuantity + , showQuantity + , prettyQuantity + , putQuantity + , putInfoQ + + -- * Unit and dimension constructors + , NoDim (..) + , NoUnit (..) + , MetaUnit (..) + , type (.*.) (..) + , type (.^.) (..) + , type (.^+) + , type (.^-) + + + -- * Unit normalization + , BaseUnitOf + , NormalizeUnitL + , NormalizeUnitR + -- ** Normalization operators + -- *** Multiplication + , type (.*~) + , type (~*.) + , type (~*~) + -- *** Division + , type (./.) + , type (./~) + , type (~/.) + , type (~/~) + -- *** Exponentiation + , type (~^.) + , type (.^~) + , type (~^~) + ) + where + +import Data.Coerce +import Data.Kind +import Data.Proxy +import Data.Type.Ord +import Data.Type.Bool +import Data.Type.Equality +import GHC.TypeError +import GHC.TypeLits + +import Data.Type.Int + + +---------------------------------- Dimension ----------------------------------- + +-- | A unit dimension. +-- +-- Modeled as a newtype constructor, just like @'Unit'@. +-- +-- >>> type Speed = Length -/- Time +-- +type Dim = Type -> Type + +class (IsUnit (DimToUnit d), forall a. Coercible (d a) a) + => IsDim (d :: Dim) where + type DimToUnit d :: Unit + +-- | A dimension identifier. +-- +-- This identifiers allow to sort the units when computing the standard unit. +-- +-- >>> type instance DimId Length = 300 +-- +-- >>> :kind! BaseUnitOf (Second .^- 1 .*. Meter) +-- Meter .*. (Second .^. Neg 1) +-- +-- +-- Two different dimensions must have different identifiers. To make sure this +-- remains true, we maintain here an /exhaustive/ list of dimensions declared +-- in this package /and/ any package that depends on it. Please raise an issue +-- if you added a new dimension. +-- +-- [This package:] +-- +-- +----------------------------------------------------+----------------+ +-- | Dimension | Id | +-- +====================================================+================+ +-- | Reserved | 0 | +-- +----------------------------------------------------+----------------+ +-- | @'NoDim'@ | 1 | +-- +----------------------------------------------------+----------------+ +-- | @'Data.Units.AngleSI.System.Angle'@ | 1000 | +-- +----------------------------------------------------+----------------+ +-- | @'Data.Units.SI.System.Mass'@ | 2000 | +-- +----------------------------------------------------+----------------+ +-- | @'Data.Units.SI.System.Length'@ | 3000 | +-- +----------------------------------------------------+----------------+ +-- | @'Data.Units.SI.System.Time'@ | 4000 | +-- +----------------------------------------------------+----------------+ +-- | @'Data.Units.SI.System.ElectricCurrent'@ | 5000 | +-- +----------------------------------------------------+----------------+ +-- | @'Data.Units.SI.System.Temperature'@ | 6000 | +-- +----------------------------------------------------+----------------+ +-- | @'Data.Units.SI.System.AmountOfSubstance'@ | 7000 | +-- +----------------------------------------------------+----------------+ +-- | @'Data.Units.SI.System.LuminousIntensity'@ | 8000 | +-- +----------------------------------------------------+----------------+ +-- +type family DimId (d:: Dim) :: ZZ + +-- | Dimensions that can be shown as a string, or as a type error message. +-- +class ShowDim (d :: Dim) where + {-# MINIMAL showDim | showsDimPrec #-} + + -- | Allows to print dimensions in conversion error messages + -- + -- >>> type ShowDimType Length = "L" + -- + type ShowDimType d :: ErrorMessage + + -- | Convert a dimension to a readable string + -- + -- @'showsDimPrec'@ should satisfy the law : + -- + -- @showsDimPrec d x r ++ s == showsPrec d x (r ++ s)@ + showsDimPrec :: Int -> ShowS + showsDimPrec _ = (showDim @d ++) + + -- | Convert a dimension to a string representing its type. + -- + -- >>> showDim @(Length ./. Time) + -- "Length .*. Time.^-1" + showDim :: String + showDim = showsDim @d "" + + -- | Same as @'showsDimPrec'@ but for pretty printing. + -- + -- @'prettysDimPrec'@ should satisfy the law : + -- + -- @prettysDimPrec d x r ++ s == prettysPrec d x (r ++ s)@ + -- + prettysDimPrec :: Int -> ShowS + prettysDimPrec _ = (prettyDim @d ++) + + -- | Same as @'showDim'@ but for pretty printing + -- + -- >>> putStrLn $ prettyDim @(Kilo Meter ./. Second) + -- km.s⁻¹ + -- + prettyDim :: String + prettyDim = prettysDim @d "" + +-- | Equivalent to 'showsDimPrec' with a precedence of 0. +-- +showsDim :: forall d. ShowDim d => ShowS +showsDim = showsDimPrec @d 0 + +-- | Equivalent to 'prettysDimPrec' with a precedence of 0. +-- +prettysDim :: forall d. ShowDim d => ShowS +prettysDim = prettysDimPrec @d 0 + +-- | Show the dimension of a quantity. +-- +-- >>> showDimOf (quantity @(Kilo Meter ./. Second) 1) +-- "Length .*. Time.^-1" +-- +showDimOf :: forall u a. (IsUnit u, ShowDim (DimOf u)) => u a -> String +showDimOf _ = showDim @(DimOf u) + +-- | Same as 'showDimOf' but for pretty printing. +-- >>> putStrLn $ prettyDimOf (quantity @(Kilo Meter ./. Second) 1) +-- L.T⁻¹ +prettyDimOf :: forall u a. (IsUnit u, ShowDim (DimOf u)) => u a -> String +prettyDimOf _ = prettyDim @(DimOf u) + +-- | Print the dimension of a quantity. +-- +-- >>> putDimOf (quantity @(Kilo Meter ./. Second) 1) +-- L.T⁻¹ +-- +putDimOf :: forall u a. (IsUnit u, ShowDim (DimOf u)) => u a -> IO () +putDimOf = putStrLn . prettyDimOf + + + +-- | The dimension of non dimensional quantities +-- +newtype NoDim a = NoDim a + deriving ( Show, Eq, Ord, Num, Fractional, Floating, Real + , RealFrac, RealFloat, Bounded, Enum, Semigroup, Monoid, Functor) + + +type instance DimId NoDim = Pos 1 + +instance ShowDim NoDim where + type ShowDimType NoDim = Text "NoDim" + showDim = "NoDim" + prettyDim = "NoDim" + + +type family CmpDim (d :: Dim) (e :: Dim) :: Ordering where + CmpDim (d .*. d') (e .*. e') = + If (CmpDim d e == 'EQ) (CmpDim d' e') (CmpDim d e) + CmpDim (d .*. d') e ='LT + CmpDim d (e .*. e') = 'GT + CmpDim (d .^. dn) (e .^. en) = + If (CmpDim d e == 'EQ) (CmpSigned dn en) (CmpDim d e) + CmpDim (d .^. dn) e = + If (CmpDim d e == 'EQ) (CmpSigned dn (Pos 1)) (CmpDim d e) + CmpDim d (e .^. en) = + If (CmpDim d e == 'EQ) (CmpSigned (Pos 1) en) (CmpDim d e) + CmpDim d e = CmpSigned (DimId d) (DimId e) + + +-------------------------- Dimension normalization --------------------------- + + +-- | Helper type family for defining DimOf for .*. and .^. +-- +type family DimOf' (u :: Unit) :: Dim where + DimOf' u = NormalizeDim (UnitToDim u) + +type family UnitToDim (u :: Unit) :: Dim where + UnitToDim (u .*. v) = UnitToDim u .*. UnitToDim v + UnitToDim (u .^. n) = UnitToDim u .^. n + UnitToDim u = DimOf u + +type NormalizeDim d = NormalizeFlatDim (Flatten d) + +type family Flatten u where + Flatten (u .*. v) = Flatten u .*. Flatten v + Flatten ((u .*. v) .^. n) = Flatten (u .^. n) .*. Flatten (v .^. n) + Flatten ((u .^. n) .^. m) = Flatten (u .^. Mul n m) + Flatten (u .^. n) = u .^. n + Flatten u = u + +type family NormalizeFlatDim d where + NormalizeFlatDim (d .*. NoDim) = NormalizeFlatDim d + NormalizeFlatDim (NoDim .*. e) = NormalizeFlatDim e + NormalizeFlatDim ((d .*. e) .*. f) = NormalizeFlatDim (d .*. (e .*. f)) + NormalizeFlatDim (d .*. e) = + InsertDim (NormalizeFlatDim d) (NormalizeFlatDim e) + NormalizeFlatDim (NoDim .^. n) = NoDim + NormalizeFlatDim (d .^. n) = NormalizeExpDim (d .^. n) + NormalizeFlatDim d = d + +type family InsertDim d e where + InsertDim NoDim e = e + InsertDim d NoDim = d + InsertDim d (e .*. f) = + InsertCmpDim (Compare (DimId d) (DimId e)) d (e .*. f) + InsertDim d e = + InsertCmpDim (Compare (DimId d) (DimId e)) d e + +type family InsertCmpDim cmp d v where + InsertCmpDim 'LT d (e .*. f) = d .*. e .*. f + InsertCmpDim 'GT d (e .*. f) = e .*. InsertDim d f + InsertCmpDim 'EQ d (e .*. f) = MulNoDim (MulPowDim d e) f + InsertCmpDim 'LT d e = d .*. e + InsertCmpDim 'GT d e = e .*. d + InsertCmpDim 'EQ d e = MulPowDim d e + +type family MulNoDim d e where + MulNoDim NoDim e = e + MulNoDim d NoDim = d + MulNoDim d e = d .*. e + +type family MulPowDim d e where + MulPowDim (d .^. n) (d .^. m) = NormalizeExpDim (d .^. Add n m) + MulPowDim d (d .^. m) = NormalizeExpDim (d .^. Add (Pos 1) m) + MulPowDim (d .^. n) d = NormalizeExpDim (d .^. Add n (Pos 1)) + MulPowDim d d = d .^. Pos 2 + MulPowDim d e = TypeError ( + Text "Failed to multiply two different units ‘" + :<>: ShowUnitType d + :<>: Text "’ and ‘" + :<>: ShowUnitType e + :<>: Text "’ with the same dimension ‘" + :<>: ShowDimType (DimOf d) + :<>: Text "’." + :$$: Text "Hint : Did you try to multiply via (.*.) two quantities with" + :$$: Text " the same dimension but different units ?" + :$$: Text "If so, you might want to use (~*-), (-*~) or (~*~) instead. " + ) + +type family NormalizeExpDim u where + NormalizeExpDim (u .^. Pos 1) = u + NormalizeExpDim (u .^. Zero) = NoDim + NormalizeExpDim u = u + + +------------------------------------ Units ------------------------------------- + +-- | A unit is represented by a newtype constructor. A quantity of some unit +-- @u@ is of type @u a@. +-- +type Unit = Type -> Type + +-- | Any unit must have a dimension. Additionally, a unit is a newtype +-- constructor : a quantity @u a@ can always be coerced to its magnitude @a@. +-- +class (forall a. Coercible (u a) a) => IsUnit (u :: Unit) where + type DimOf u :: Dim + +-- | Make a quantity out of any numerical value (called the /magnitude/ of that +-- quantity) +-- +-- >>> quantity @(Meter ./. Second) 1 +-- quantity @(Meter .*. Second .^- 1) 1 +quantity :: forall u a. IsUnit u => a -> u a +quantity = coerce +{-# INLINE quantity #-} + +-- | Get the magnitude of a quantity. +-- +-- @unQuantity (quantity @u a) === a @ +-- +unQuantity :: IsUnit u => u a -> a +unQuantity = coerce +{-# INLINE unQuantity #-} + +-- | Units that can be shown as a string, or as a type error message. +-- +class IsUnit u => ShowUnit (u :: Unit) where + {-# MINIMAL showUnit | showsUnitPrec #-} + + -- | Allows to print units in conversion error messages + -- + -- >>> type ShowUnit Meter = "m" + -- + type ShowUnitType u :: ErrorMessage + + -- | Convert a unit to a readable string + -- + -- @'showsUnitPrec'@ should satisfy the law : + -- + -- @showsUnitPrec d x r ++ s == showsPrec d x (r ++ s)@ + -- + showsUnitPrec :: Int -> ShowS + showsUnitPrec _ = (showUnit @u ++) + + -- | Convert a unit to a string representing its type. + -- + -- >>> showUnit @(Kilo Meter ./. Second) + -- "Kilo Meter .*. Second.^-1" + showUnit :: String + showUnit = showsUnit @u "" + + -- | Same as @'showsUnitPrec'@ but for pretty printing. + -- + prettysUnitPrec :: Int -> ShowS + prettysUnitPrec _ = (prettyUnit @u ++) + + -- | Same as @'showUnit'@ but for pretty printing + -- + -- >>> putStrLn $ prettyUnit @(Kilo Meter ./. Second) + -- km.s⁻¹ + -- + prettyUnit :: String + prettyUnit = prettysUnit @u "" + + +-- | Equivalent to 'showsUnitPrec' with a precedence of 0. +-- +showsUnit :: forall u. ShowUnit u => ShowS +showsUnit = showsUnitPrec @u 0 + +-- | Equivalent to 'prettysUnitPrec' with a precedence of 0. +-- +prettysUnit :: forall u. ShowUnit u => ShowS +prettysUnit = prettysUnitPrec @u 0 + +-- | Pretty print information about a unit, its dimension and its normalized +-- form. +-- +prettyUnitInfo :: forall u du nu. + ( du ~ DimOf u + , nu ~ BaseUnitOf u + , ShowUnit u + , ShowDim du + , ShowUnit nu + ) => String +prettyUnitInfo = + "Unit: " ++ showUnit @u ++ "\n" ++ + " abbr: " ++ prettyUnit @u ++ "\n" ++ + "Dimension: " ++ showDim @du ++ "\n" ++ + " abbr: " ++ prettyDim @du ++ "\n" ++ + "Normalized: " ++ showUnit @nu ++ "\n" ++ + " abbr: " ++ prettyUnit @nu ++ "\n" + +-- | Print information about a unit, its dimension and its normalized form. +-- +-- >>> putInfoU @Newton +-- Unit: Newton +-- abbr: N +-- Dimension: Mass .*. Length .*. Time.^-2 +-- abbr: M⋅L⋅T⁻² +-- Normalized: Kilo Gram .*. Meter .*. Second.^-2 +-- abbr: kg⋅m⋅s⁻² +-- +putInfoU :: forall u du nu. + ( du ~ DimOf u + , nu ~ BaseUnitOf u + , ShowUnit u + , ShowDim du + , ShowUnit nu + ) => IO () +putInfoU = putStr $ prettyUnitInfo @u + +-- | Same as 'prettyUnitInfo' but for quantities. +-- +prettyQuantityInfo :: forall u a. + ( ShowUnit u + , ShowDim (DimOf u) + , ShowUnit (BaseUnitOf u) + , Show a + ) => u a -> String +prettyQuantityInfo u = prettyUnitInfo @u ++ + "Magnitude: " ++ show (unQuantity u) ++ "\n" + +-- | Same as 'putInfoU' but for quantities. +-- +-- >>> putInfoQ (Newton 4) +-- Unit: Newton +-- abbr: N +-- Dimension: Mass .*. Length .*. Time.^-2 +-- abbr: M⋅L⋅T⁻² +-- Normalized: Kilo Gram .*. Meter .*. Second.^-2 +-- abbr: kg⋅m⋅s⁻² +-- Magnitude: 4 +putInfoQ :: forall u a. + ( ShowUnit u + , ShowDim (DimOf u) + , ShowUnit (BaseUnitOf u) + , Show a + ) => u a -> IO () +putInfoQ u = putStr $ prettyQuantityInfo @u u + + +-- | Same as 'showsUnitPrec' but for quantities. +-- +showsQuantityPrec :: forall u a. (ShowUnit u, Show a) => Int -> u a -> ShowS +showsQuantityPrec d u = showParen (d > 10) $ + showString "quantity @" . showsUnitPrec @u 11 . showString " " . + showsPrec 11 (unQuantity u) + +-- | Equivalent to 'showsQuantityPrec' with a precedence of 0. +-- +showsQuantity :: (ShowUnit u, Show a) => u a -> ShowS +showsQuantity = showsQuantityPrec 0 + +-- | Same as 'showUnit' but for quantities +-- +-- >>> showQuantity (quantity @(Kilo Meter ./. Second) 1) +-- "quantity @(Kilo Meter .*. Second.^-1) 1.0" +-- +showQuantity :: (ShowUnit u, Show a) => u a -> String +showQuantity u = showsQuantity u "" + +-- | Same as 'prettyUnit' but for quantities +-- +-- >>> putStrLn $ prettyQuantity (quantity @(Kilo Meter ./. Second) 1) +-- 1 km.s⁻¹ +-- +prettyQuantity :: forall u a. (ShowUnit u, Show a) => u a -> String +prettyQuantity u = show (unQuantity u) ++ " " ++ prettyUnit @u + +-- | Pretty print a quantity. +-- +-- >>> putQuantity (quantity @(Kilo Meter ./. Second) 1) +-- 1 km.s⁻¹ +putQuantity :: (Show a, ShowUnit u) => u a -> IO () +putQuantity = putStrLn . prettyQuantity + + +-------------------------------------------------------------------------------- + + +-- | A unit that can represent any unit. +-- +-- This can be used with the `deriving via` mechanism to derive some of the +-- unit instances. +-- +newtype MetaUnit (u :: Unit) a = MetaUnit a + deriving ( Eq, Ord, Num, Fractional, Floating, Real + , RealFrac, RealFloat, Bounded, Enum, Semigroup, Monoid, Functor) + +instance ShowUnit u => ShowUnit (MetaUnit u) where + type ShowUnitType (MetaUnit u) = ShowUnitType u + prettysUnitPrec = prettysUnitPrec @u + showsUnitPrec = showsUnitPrec @u + +instance (Show a, ShowUnit u) => Show (MetaUnit u a) where + showsPrec = showsQuantityPrec + +instance IsUnit u => IsUnit (MetaUnit u) where + type DimOf (MetaUnit u) = DimOf u + +-------------------------------------------------------------------------------- + +-- | A unit that has no dimension. +-- +-- @ +-- type MyHertz = NoUnit ./. Second +-- @ +-- +newtype NoUnit a = NoUnit a + deriving ( Show, Eq, Ord, Num, Fractional, Floating, Real + , RealFrac, RealFloat, Functor) + +instance IsUnit NoUnit where + type DimOf NoUnit = NoDim + +instance IsDim NoDim where + type DimToUnit NoDim = NoUnit + + +-- | Multiplication of two units. +-- +-- @ +-- type MyForceMoment = Newton .*. Meter +-- @ +-- +newtype ((u :: Unit) .*. (v :: Unit)) a = MulUnit a + deriving ( Eq, Ord, Num, Fractional, Floating, Real + , RealFrac, RealFloat, Functor) + deriving Show via MetaUnit (u .*. v) a + +infixr 7 .*. + +instance (ShowUnit u, ShowUnit v) => ShowUnit (u .*. v) where + type ShowUnitType (u .*. v) = + ShowUnitType u + :<>: Text "⋅" :<>: ShowUnitType v + prettysUnitPrec d = showParen (d > 7) $ + prettysUnitPrec @u 7 . showString "⋅" . prettysUnitPrec @v 7 + showsUnitPrec d = showParen (d > 7) $ + showsUnitPrec @u 7 . showString " .*. " . showsUnitPrec @v 7 + +instance (ShowDim u, ShowDim v) => ShowDim (u .*. v) where + type ShowDimType (u .*. v) = + ShowDimType u + :<>: Text "⋅" :<>: ShowDimType v + prettysDimPrec d = showParen (d > 7) $ + prettysDimPrec @u 7 . showString "⋅" . prettysDimPrec @v 7 + showsDimPrec d = showParen (d > 7) $ + showsDimPrec @u 7 . showString " .*. " . showsDimPrec @v 7 + + +instance (IsUnit u, IsUnit v) => IsUnit (u .*. v) where + type DimOf (u .*. v) = DimOf' (u .*. v) + +instance (IsDim d, IsDim e) => IsDim (d .*. e) where + type DimToUnit (d .*. e) = DimToUnit d .*. DimToUnit e + + + +-------------------------------- Unit division --------------------------------- + +type family InverseUnit u where + InverseUnit (u .*. v) = InverseUnit u .*. InverseUnit v + InverseUnit (u .^. n) = NormalizeExp (u .^. Negate n) + InverseUnit NoUnit = NoUnit + InverseUnit u = u .^. Neg 1 + +-- | Division of two units. +-- +-- @ +-- type MySpeed a = (Meter ./. Second) a +-- type MyMolarEntropy a = (Joule ./. Mole .*. Kelvin) a +-- @ +-- +-- Notice that multiplication has priority over division. +-- +type family (u :: Unit) ./. (v :: Unit) :: Unit where + u ./. v = u .*. InverseUnit v + +infix 6 ./. + + +----------------------------- Unit exponentiation ------------------------------ + + +-- | Exponentiation of a unit +-- +-- @ +-- type Acceleration = Meter .*. Second .^. Neg 2 +-- @ +-- +newtype ((u :: Unit) .^. (n :: ZZ)) a = PowUnit a + deriving ( Eq, Ord, Num, Fractional, Floating, Real + , RealFrac, RealFloat, Functor) + deriving Show via MetaUnit (u .^. n) a +infix 8 .^. + +-- | Positive exponentiation of a unit +-- +-- @ +-- type Area = Meter .^+ 2 +-- @ +-- +type a .^+ b = a .^. Pos b +infix 8 .^+ + +-- | Negative exponentiation of a unit +-- +-- @ +-- type Hertz = Second .^- 1 +-- @ +-- +type a .^- b = a .^. Neg b +infix 8 .^- + +type instance DimId (d .^. n) = DimId d + +type family ShowIntExponent (n :: ZZ) :: ErrorMessage where + ShowIntExponent (Pos n) = + If (n <=? 9) (ShowDigitExponent n) (Text "^" :<>: ShowType n) + ShowIntExponent Zero = Text "⁰" + ShowIntExponent (Neg n) = + If (n <=? 9) (Text "⁻" :<>: ShowDigitExponent n) + (Text "^-" :<>: ShowType n) + +type family ShowDigitExponent (n :: Nat) :: ErrorMessage where + ShowDigitExponent 0 = Text "⁰" + ShowDigitExponent 1 = Text "¹" + ShowDigitExponent 2 = Text "²" + ShowDigitExponent 3 = Text "³" + ShowDigitExponent 4 = Text "⁴" + ShowDigitExponent 5 = Text "⁵" + ShowDigitExponent 6 = Text "⁶" + ShowDigitExponent 7 = Text "⁷" + ShowDigitExponent 8 = Text "⁸" + ShowDigitExponent 9 = Text "⁹" + +instance IsUnit u => IsUnit (u .^. n) where + type DimOf (u .^. n) = DimOf' (u .^. n) + +instance IsDim d => IsDim (d .^. n) where + type DimToUnit (d .^. n) = DimToUnit d .^. n + + +instance (ShowUnit u, KnownInt n) => ShowUnit (u .^. n) where + type ShowUnitType (u .^. n) = + ShowUnitType u :<>: ShowIntExponent n + prettysUnitPrec d = showParen (d >= 8) $ + prettysUnitPrec @u 8 . showString (toSuperscript <$> show (intVal (Proxy :: Proxy n))) + showsUnitPrec d = showParen (d >= 8) $ + if n >= 0 then + showsUnitPrec @u 8 . showString ".^+" . shows n + else + showsUnitPrec @u 8 . showString ".^-" . shows (-n) + where + n = intVal (Proxy :: Proxy n) + +instance (ShowDim u, KnownInt n) => ShowDim (u .^. n) where + type ShowDimType (u .^. n) = + ShowDimType u :<>: ShowIntExponent n + prettysDimPrec d = showParen (d >= 8) $ + prettysDimPrec @u 8 . showString (toSuperscript <$> show (intVal (Proxy :: Proxy n))) + showsDimPrec d = showParen (d >= 8) $ + if n >= 0 then + showsDimPrec @u 8 . showString ".^+" . shows n + else + showsDimPrec @u 8 . showString ".^-" . shows (-n) + where + n = intVal (Proxy :: Proxy n) + + +toSuperscript :: Char -> Char +toSuperscript '0' = '⁰' +toSuperscript '1' = '¹' +toSuperscript '2' = '²' +toSuperscript '3' = '³' +toSuperscript '4' = '⁴' +toSuperscript '5' = '⁵' +toSuperscript '6' = '⁶' +toSuperscript '7' = '⁷' +toSuperscript '8' = '⁸' +toSuperscript '9' = '⁹' +toSuperscript '+' = '⁺' +toSuperscript '-' = '⁻' +toSuperscript ')' = '⁾' +toSuperscript '(' = '⁽' +toSuperscript '=' = '⁼' +toSuperscript a = a + +------------------------------ Unit normalization ------------------------------ + +-- | Normalizes a unit by converting it to a product of exponentiations of base +-- units. +type BaseUnitOf u = DimToUnit (DimOf u) + +-- | Multiplies two units and normalizes the result. +-- +type (u :: Unit) ~*~ (v :: Unit) = BaseUnitOf (u .*. v) + +infixr 7 ~*~ + +-- | Divides two units and normalizes the result. +-- +type (u :: Unit) ~/~ (v :: Unit) = BaseUnitOf (u ./. v) + +infixr 6 ~/~ + +-- | Exponentiates a unit and normalizes the result. +-- +type (u :: Unit) ~^~ (n :: ZZ) = BaseUnitOf (u .^. n) + +infixr 8 ~^~ + + +type family MulNoUnit d e where + MulNoUnit NoUnit e = e + MulNoUnit d NoUnit = d + MulNoUnit d e = d .*. e + +type family NormalizeExp u where + NormalizeExp (u .^. Pos 1) = u + NormalizeExp (u .^. Zero) = NoUnit + NormalizeExp u = u + + +--------------------- Unit normalization left priority ---------------------- + +-- | Tries to normalize a unit without converting to base units. +-- +-- >>> :kind! NormalizeUnitR (Minute .*. Second) +-- Minute .^. Pos 2 +-- +type NormalizeUnitL u = NormalizeFlatUnitL (Flatten u) + +-- | Multiplies two units and use left weak normalization. +type (u :: Unit) .*~ (v :: Unit) = NormalizeUnitL (u .*. v) + +infixr 7 .*~ + +-- | Divides two units and use left weak normalization. +type (u :: Unit) ./~ (v :: Unit) = NormalizeUnitL (u ./. v) + +infixr 6 ./~ + +-- | Exponentiates a unit and use left weak normalization. +type (u :: Unit) .^~ (n :: ZZ) = NormalizeUnitL (u .^. n) + +infixr 8 .^~ + + +type family NormalizeFlatUnitL u where + NormalizeFlatUnitL (u .*. NoUnit) = NormalizeFlatUnitL u + NormalizeFlatUnitL (NoUnit .*. v) = NormalizeFlatUnitL v + NormalizeFlatUnitL ((u .*. v) .*. w) = NormalizeFlatUnitL (u .*. (v .*. w)) + NormalizeFlatUnitL (u .*. v) = + InsertUnitL (NormalizeFlatUnitL u) (NormalizeFlatUnitL v) + NormalizeFlatUnitL (NoUnit .^. n) = NoUnit + NormalizeFlatUnitL (u .^. n) = NormalizeExp (u .^. n) + NormalizeFlatUnitL u = u + +type family InsertUnitL u v where + InsertUnitL NoUnit v = v + InsertUnitL u NoUnit = u + InsertUnitL (u .*. v) w = + TypeError (Text + "Insert unit : Removing left association failed in NormalizeFlatUnitL") + InsertUnitL (u .^. n) (v .^. m .*. w) = + InsertCmpL (CmpDim (DimOf u) (DimOf v)) (u .^. n) (v .^. m .*. w) + InsertUnitL (u .^. n) (v .*. w) = + InsertCmpL (CmpDim (DimOf u) (DimOf v)) (u .^. n) (v .*. w) + InsertUnitL (u .^. n) (v .^. m) = + InsertCmpL (CmpDim (DimOf u) (DimOf v)) (u .^. n) (v .^. m) + InsertUnitL (u .^. n) v = + InsertCmpL (CmpDim (DimOf u) (DimOf v)) (u .^. n) v + InsertUnitL u (v .^. m .*. w) = + InsertCmpL (CmpDim (DimOf u) (DimOf v)) u (v .^. m .*. w) + InsertUnitL u (v .*. w) = + InsertCmpL (CmpDim (DimOf u) (DimOf v)) u (v .*. w) + InsertUnitL u (v .^. m) = + InsertCmpL (CmpDim (DimOf u) (DimOf v)) u (v .^. m) + InsertUnitL u v = + InsertCmpL (CmpDim (DimOf u) (DimOf v)) u v + +type family InsertCmpL cmp u v where + InsertCmpL 'LT u (v .*. w) = u .*. v .*. w + InsertCmpL 'GT u (v .*. w) = v .*. InsertUnitL u w + InsertCmpL 'EQ u (v .*. w) = MulNoUnit (MulSameDimL u v) w + InsertCmpL 'LT u v = u .*. v + InsertCmpL 'GT u v = v .*. u + InsertCmpL 'EQ u v = MulSameDimL u v + +type family MulSameDimL u v where + MulSameDimL (u .^. n) (v .^. m) = NormalizeExp (u .^. Add n m) + MulSameDimL u (v .^. m) = NormalizeExp (u .^. Add (Pos 1) m) + MulSameDimL (u .^. n) v = NormalizeExp (u .^. Add n (Pos 1)) + MulSameDimL u v = u .^. Pos 2 + + + +---------------------- Unit normalization right priotiy ----------------------- + +-- The only difference with right is MulSameDim + + +-- | Tries to normalize a unit without converting to base units. When two units +-- have the same dimension, they will be collapsed to an exponentiation right +-- most unit. +-- +-- >>> :kind! NormalizeUnitR (Minute .*. Second) +-- Second .^. Pos 2 +-- +type NormalizeUnitR u = NormalizeFlatUnitR (Flatten u) + +-- | Same as @'(~*.)'@ but with priority to right most units +type (u :: Unit) ~*. (v :: Unit) = NormalizeUnitR (u .*. v) + +infixr 7 ~*. + +-- | Same as @'(~/.)'@ but with priority to right most units +type (u :: Unit) ~/. (v :: Unit) = NormalizeUnitR (u ./. v) + +infixr 7 ~/. + +-- | Same as @'(.^~)'@ but with priority to right most units +type (u :: Unit) ~^. (n :: ZZ) = NormalizeUnitR (u .^. n) + +infix 8 ~^. + + +type family NormalizeFlatUnitR u where + NormalizeFlatUnitR (u .*. NoUnit) = NormalizeFlatUnitR u + NormalizeFlatUnitR (NoUnit .*. v) = NormalizeFlatUnitR v + NormalizeFlatUnitR ((u .*. v) .*. w) = NormalizeFlatUnitR (u .*. (v .*. w)) + NormalizeFlatUnitR (u .*. v) = + InsertUnitR (NormalizeFlatUnitR u) (NormalizeFlatUnitR v) + NormalizeFlatUnitR (NoUnit .^. n) = NoUnit + NormalizeFlatUnitR (u .^. n) = NormalizeExp (u .^. n) + NormalizeFlatUnitR u = u + +type family InsertUnitR u v where + InsertUnitR NoUnit v = v + InsertUnitR u NoUnit = u + InsertUnitR (u .*. v) w = + TypeError (Text + "Insert unit : Removing left association failed in NormalizeFlatUnitR") + InsertUnitR (u .^. n) (v .^. m .*. w) = + InsertCmpR (CmpDim (DimOf u) (DimOf v)) (u .^. n) (v .^. m .*. w) + InsertUnitR (u .^. n) (v .*. w) = + InsertCmpR (CmpDim (DimOf u) (DimOf v)) (u .^. n) (v .*. w) + InsertUnitR (u .^. n) (v .^. m) = + InsertCmpR (CmpDim (DimOf u) (DimOf v)) (u .^. n) (v .^. m) + InsertUnitR (u .^. n) v = + InsertCmpR (CmpDim (DimOf u) (DimOf v)) (u .^. n) v + InsertUnitR u (v .^. m .*. w) = + InsertCmpR (CmpDim (DimOf u) (DimOf v)) u (v .^. m .*. w) + InsertUnitR u (v .*. w) = + InsertCmpR (CmpDim (DimOf u) (DimOf v)) u (v .*. w) + InsertUnitR u (v .^. m) = + InsertCmpR (CmpDim (DimOf u) (DimOf v)) u (v .^. m) + InsertUnitR u v = + InsertCmpR (CmpDim (DimOf u) (DimOf v)) u v + +type family InsertCmpR cmp u v where + InsertCmpR 'LT u (v .*. w) = u .*. v .*. w + InsertCmpR 'GT u (v .*. w) = v .*. InsertUnitR u w + InsertCmpR 'EQ u (v .*. w) = MulNoUnit (MulSameDimR u v) w + InsertCmpR 'LT u v = u .*. v + InsertCmpR 'GT u v = v .*. u + InsertCmpR 'EQ u v = MulSameDimR u v + +type family MulSameDimR u v where + MulSameDimR (u .^. n) (v .^. m) = NormalizeExp (v .^. Add n m) + MulSameDimR u (v .^. m) = NormalizeExp (v .^. Add (Pos 1) m) + MulSameDimR (u .^. n) v = NormalizeExp (v .^. Add n (Pos 1)) + MulSameDimR u v = v .^. Pos 2
+ src/Data/Units/Base/TH.hs view
@@ -0,0 +1,441 @@+ +{-# LANGUAGE TemplateHaskell #-} + + +-------------------------------------------------------------------------------- +-- | +-- +-- Module : Data.Units.Base.TH +-- Description : Template Haskell quasi quoter for unit declaration +-- Copyright : (c) Alice Rixte 2025 +-- License : BSD 3 +-- Maintainer : alice.rixte@u-bordeaux.fr +-- Stability : unstable +-- Portability : non-portable (GHC extensions) +-- +-------------------------------------------------------------------------------- + + +module Data.Units.Base.TH + ( -- * Units + mkUnit + , mkUnitNoFactor + , mkBaseUnit + -- * Dimensions + , mkDim + -- * Prefixes + , mkPrefix + ) + where + +import GHC.TypeError + +import Language.Haskell.TH + +import Data.Type.Int +import Data.Units.Base.System +import Data.Units.Base.Convert +import Data.Units.Base.Prefix + +------------------------------------ Units ------------------------------------- + +-- | List of derived class for a unit. +-- +deriveList :: [Name] +deriveList = + [''Eq + , ''Ord + , ''Num + , ''Fractional + , ''Floating + , ''Real + , ''RealFrac + , ''RealFloat + ] + +-- | List of derived classes for a unit. +-- +deriveListUnit :: [Name] +deriveListUnit = ''Show : deriveList + +-- | Make a newtype of the form +-- +-- @ +-- newtype Minute a = Minute a +-- deriving ( Show, Eq, Ord, Num, Fractional, Floating, Real +-- , RealFrac, RealFloat) +-- @ +-- +mkUnitNewtype :: Quote m + => [Name] -> Name -> m Dec +mkUnitNewtype l unitName = + let a = varT (mkName "a") in + newtypeD + (cxt []) + unitName + [PlainTV (mkName "a") BndrReq] + Nothing + (normalC unitName + [bangType (bang noSourceUnpackedness noSourceStrictness) a]) + [ derivClause Nothing (conT <$> l)] + +-- | Make instance of the form +-- +-- @ +-- instance IsUnit Hour where +-- type DimOf Hour = Time +-- @ +-- +mkIsUnitInstance :: Quote m => Name -> Name -> m [Dec] +mkIsUnitInstance unitName dimName = [d| + instance IsUnit $(conT unitName) where + type DimOf $(conT unitName) = $(conT dimName) + |] + +-- | Make an instance of the form +-- +-- @ +-- instance ShowUnit Minute where +-- type ShowUnitType Minute = Text "min" +-- showUnit = "Minute" +-- prettyUnit = "min" +-- @ +-- +mkShowUnitInstance :: Quote m => Name -> String -> String -> m [Dec] +mkShowUnitInstance unitName unitStr prettyStr = [d| + instance ShowUnit $(conT unitName) where + type ShowUnitType $(conT unitName) = Text $(pure (LitT (StrTyLit prettyStr))) + showUnit = $(litE (StringL unitStr)) + prettyUnit = $(litE (StringL prettyStr)) + |] + + +-- | Make an instance of the form +-- +-- @ +-- instance Fractional a => ConversionFactor Minute a where +-- factor = 60 +-- @ +-- +mkConvFactorInstance :: Quote m => Name -> Rational -> m [Dec] +mkConvFactorInstance unitName fctr = [d| + instance Fractional a => ConversionFactor $(conT unitName) a where + factor = $(litE (RationalL fctr)) + |] + + +-- | Make an instance of the form +-- +-- @ +-- instance Fractional a => ConvertibleUnit Minute a +-- @ +-- +mkDefaultSigConvertibleInstance :: Quote m => Name -> m [Dec] +mkDefaultSigConvertibleInstance unitName = [d| + instance Fractional a => ConvertibleUnit $(conT unitName) a + |] + +-- | Make an instance of the form +-- +-- @ +-- instance Fractional a => ConvertibleUnit Meter a where +-- toBaseUnit = coerce +-- {-# INLINE toBaseUnit #-} +-- fromBaseUnit = coerce +-- {-# INLINE fromBaseUnit #-} +-- @ +-- +mkNormalConvertibleInstance :: Quote m => Name -> m [Dec] +mkNormalConvertibleInstance unitName = [d| + instance Fractional a => ConvertibleUnit $(conT unitName) a where + toBaseUnit = coerce + {-# INLINE toBaseUnit #-} + fromBaseUnit = coerce + {-# INLINE fromBaseUnit #-} + |] + + +-- | Make a unit that can be converted via a factor +-- +-- [Usage:] +-- +-- @ +-- \$(mkUnit "Minute" "min" ''Time 60) +-- @ +-- +mkUnit :: String -> String -> Name -> Rational -> Q [Dec] +mkUnit unitStr prettyStr dimName fctr = do + let unitName = mkName unitStr + newtypeDec <- mkUnitNewtype deriveListUnit unitName + isUnitDec <- mkIsUnitInstance unitName dimName + showUnitDec <- mkShowUnitInstance unitName unitStr prettyStr + convFactorDec <- mkConvFactorInstance unitName fctr + convUnitDec <- + if fctr == 1 then + mkNormalConvertibleInstance unitName + else + mkDefaultSigConvertibleInstance unitName + return $ + [newtypeDec] ++ isUnitDec ++ showUnitDec ++ convFactorDec ++ convUnitDec + +-- | Make a unit without declaring any conversion instances. +-- +-- Conversion instances must be added by hand. +-- +-- [Usage:] +-- +-- @ +-- \$(mkUnit \"Bel\" "B" ''NoUnit ) +-- @ +-- +mkUnitNoFactor :: String -> String -> Name -> Q [Dec] +mkUnitNoFactor unitStr prettyStr dimName = do + let unitName = mkName unitStr + newtypeDec <- mkUnitNewtype deriveListUnit unitName + isUnitDec <- mkIsUnitInstance unitName dimName + showUnitDec <- mkShowUnitInstance unitName unitStr prettyStr + return $ [newtypeDec] ++ isUnitDec ++ showUnitDec + + +-- | Make a base unit. +-- +-- In addition to calling 'mkUnit' with factor 1, this also makes an +-- instance of 'IsDim', which cannot be done in 'mkDim' since the base unit is +-- not yet declared. +-- +-- [Usage:] +-- +-- @ \$(mkBaseUnit "Second" "s" ''Time ) @ +-- +mkBaseUnit :: String -> String -> Name -> Q [Dec] +mkBaseUnit unitStr prettyStr dimName = do + let unitName = mkName unitStr + unitDec <- mkUnit unitStr prettyStr dimName 1 + isDimDec <- mkIsDimInstance dimName unitName + return $ unitDec ++ isDimDec + +---------------------------------- Dimensions ---------------------------------- + +-- | List of derived classes for a dimension. +-- +deriveListDim :: [Name] +deriveListDim = deriveListUnit + + +mkDimNewtype :: Quote m + => [Name] -> Name -> m Dec +mkDimNewtype = mkUnitNewtype + +-- | Make an instance of the form +-- +-- @ +-- instance IsDim Time where +-- type DimToUnit Time = Second +-- @ +-- +mkIsDimInstance :: Quote m => Name -> Name -> m [Dec] +mkIsDimInstance dimName unitName = [d| + instance IsDim $(conT dimName) where + type DimToUnit $(conT dimName) = $(conT unitName) + |] + +-- | Make a type instance of the form +-- +-- @ +-- type instance DimId Time = 400 +-- @ +-- +mkDimIdTypeInstance :: Quote m => Name -> Integer -> m [Dec] +mkDimIdTypeInstance dimName n = [d| + type instance DimId $(conT dimName) = Pos $(litT (numTyLit n)) + |] + +-- | Make a type instance of the form +-- +-- @ +-- instance ShowDim Minute where +-- type ShowDimType Minute = Text "min" +-- showDim = "Minute" +-- prettyDim = "min" +-- @ +-- +mkShowDimInstance :: Quote m => Name -> String -> String -> m [Dec] +mkShowDimInstance dimName dimStr prettyStr = [d| + instance ShowDim $(conT dimName) where + type ShowDimType $(conT dimName) = Text $(pure (LitT (StrTyLit prettyStr))) + showDim = $(litE (StringL dimStr)) + prettyDim = $(litE (StringL prettyStr)) + |] + +-- | Make a dimension. +-- +-- This will not declare an instance for 'IsDim', which is instead declared +-- using 'mkBaseUnit'. + +-- [Usage:] +-- +-- @ \$(mkDim "Time" "T" 400) @ +-- +mkDim :: String -> String -> Integer -> Q [Dec] +mkDim dimStr prettyStr n = do + let dimName = mkName dimStr + newtypeDec <- mkDimNewtype deriveListDim dimName + -- isDimDec <- mkIsDimInstance dimName unitName + dimIdDec <- mkDimIdTypeInstance dimName n + showDimDec <- mkShowDimInstance dimName dimStr prettyStr + return $ [newtypeDec] ++ dimIdDec ++ showDimDec + + + +----------------------------------- Prefixes ----------------------------------- + + + +-- | Make a newtype of the form: +-- +-- @ +-- newtype Milli (u :: Unit) a = Milli (u a) +-- deriving (Eq, Ord, Num, Fractional, Floating, Real +-- , RealFrac, RealFloat, Functor) +-- deriving Show via MetaPrefix Milli u a +-- deriving ShowUnit via MetaPrefix Milli u +-- @ +-- +mkPrefixNewtype :: Monad m => [Name] -> Name -> m Dec +mkPrefixNewtype l prefixName = pure $ + NewtypeD + [] + prefixName + [ KindedTV (mkName "u") BndrReq (ConT ''Unit) + , PlainTV (mkName "a") BndrReq + ] + Nothing + (NormalC prefixName + [ ( Bang NoSourceUnpackedness NoSourceStrictness + , AppT (VarT (mkName "u")) (VarT (mkName "a")) + ) + ]) + [ DerivClause + (Just NewtypeStrategy) + (map ConT l) + , DerivClause + (Just (ViaStrategy viaType)) + [ConT ''Show] + , DerivClause + (Just (ViaStrategy viaTypeNoA)) + [ConT ''ShowUnit] + ] + where + viaType = + foldl AppT (ConT ''MetaPrefix) + [ ConT prefixName + , VarT (mkName "u") + , VarT (mkName "a") + ] + + viaTypeNoA = + foldl AppT (ConT ''MetaPrefix) + [ ConT prefixName + , VarT (mkName "u") + ] + +-- | Make a deriving instance of the form +-- +-- @ +-- deriving via MetaPrefix Milli u instance IsUnit u => IsUnit (Milli u ) +-- @ +-- +mkPrefixIsUnitInstance :: Name -> Q [Dec] +mkPrefixIsUnitInstance prefixName = [d| + deriving via MetaPrefix $(conT prefixName) u instance IsUnit u => IsUnit ($(conT prefixName) u) + |] + +-- | Make an instance of the form +-- +-- @ +-- instance ShowPrefix Milli where +-- type ShowPrefixType Milli = Text "m" +-- showPrefix = "Milli" +-- prettyPrefix = "m" +-- @ +-- +mkShowPrefixInstance :: Quote m => Name -> String -> String -> m [Dec] +mkShowPrefixInstance prefixName prefixStr prettyStr = [d| + instance ShowPrefix $(conT prefixName) where + type ShowPrefixType $(conT prefixName) = Text $(pure (LitT (StrTyLit prettyStr))) + showPrefix = $(litE (StringL prefixStr)) + prettyPrefix = $(litE (StringL prettyStr)) + |] + +-- | Make an instance of the form +-- +-- @ +-- instance Fractional a => PrefixFactor Kilo a where +-- prefixFactor = 1000 +-- {-# INLINE prefixFactor #-} +-- +mkPrefixFactorInstance :: Name -> Rational -> Q [Dec] +mkPrefixFactorInstance prefixName fctr = [d| + instance Fractional a => PrefixFactor $(conT prefixName) a where + prefixFactor = $(litE (RationalL fctr)) + {-# INLINE prefixFactor #-} + |] + +-- | Make an instance of the form +-- +-- @ +-- instance ConversionFactor u a => ConversionFactor (Milli u) a where +-- factor = factor @(MetaPrefix Milli u) +-- {-# INLINE factor #-} +-- @ +-- +mkPrefixConvFactorInstance :: Name -> Q [Dec] +mkPrefixConvFactorInstance prefixName = [d| + instance ConversionFactor u a => ConversionFactor ($(conT prefixName) u) a where + factor = factor @(MetaPrefix $(conT prefixName) u) + {-# INLINE factor #-} + |] + +-- | Make an instance of the form +-- +-- @ +-- instance (ConvertibleUnit u a, Fractional a) +-- => ConvertibleUnit (Milli u) a where +-- toBaseUnit = prefixToBaseUnit +-- {-# INLINE toBaseUnit #-} +-- fromBaseUnit = prefixFromBaseUnit +-- {-# INLINE fromBaseUnit #-} +-- @ +-- +mkPrefixConvUnitInstance :: Name -> Q [Dec] +mkPrefixConvUnitInstance prefixName = [d| + instance (ConvertibleUnit u a, Fractional a) + => ConvertibleUnit ($(conT prefixName) u) a where + toBaseUnit = prefixToBaseUnit + {-# INLINE toBaseUnit #-} + fromBaseUnit = prefixFromBaseUnit + {-# INLINE fromBaseUnit #-} + |] + +-- | Make a unit prefix. +-- +-- [Usage:] +-- +-- @ +-- \$(mkPrefix "Kilo" "k" 1000) +-- @ +-- +mkPrefix :: String -> String -> Rational -> Q [Dec] +mkPrefix prefixStr prettyStr fctr = do + let prefixName = mkName prefixStr + newtypeDec <- mkPrefixNewtype deriveList prefixName + isUnitDec <- mkPrefixIsUnitInstance prefixName + showPrefixDec <- mkShowPrefixInstance prefixName prefixStr prettyStr + factorDec <- mkPrefixFactorInstance prefixName fctr + convFactorDec <- mkPrefixConvFactorInstance prefixName + convUnitDec <- mkPrefixConvUnitInstance prefixName + return $ [newtypeDec] ++ isUnitDec ++ showPrefixDec + ++ factorDec ++ convFactorDec ++ convUnitDec + + +
+ src/Data/Units/NonStd.hs view
@@ -0,0 +1,10 @@+module Data.Units.NonStd + ( module Data.Units.NonStd.Temperature + , module Data.Units.NonStd.Frequency + , module Data.Units.NonStd.Time + ) + where + +import Data.Units.NonStd.Temperature +import Data.Units.NonStd.Frequency +import Data.Units.NonStd.Time
+ src/Data/Units/NonStd/Frequency.hs view
@@ -0,0 +1,69 @@+module Data.Units.NonStd.Frequency where + +import Control.Exception +import Data.Word +import Data.Fixed +import GHC.TypeLits +import Data.Proxy + +import Data.Units.Base +import Data.Units.SI.System + +-- | Frequency in Tone Equal Temperament +-- +newtype Tet (b :: Nat) (offs :: ZZ) a = Tet a + deriving ( Show, Eq, Ord, Num, Fractional, Floating, Real + , RealFrac, RealFloat) + +instance (Floating a, KnownNat b, KnownInt offs) + => ConvertibleUnit (Tet b offs) a where + toBaseUnit (Tet a) = quantity $ 440 * 2 ** ((a + offs) / b) + where + b = fromIntegral $ natVal (Proxy :: Proxy b) + offs = fromIntegral (intVal (Proxy :: Proxy offs)) / 100 + fromBaseUnit a = Tet $ b * logBase 2 (unQuantity a/ 440) - offs + where + b = fromIntegral $ natVal (Proxy :: Proxy b) + offs = fromIntegral (intVal (Proxy :: Proxy offs)) / 100 + +instance IsUnit (Tet b offs) where + type DimOf (Tet b offs) = Time .^- 1 + +instance (KnownNat b, KnownInt offs) => ShowUnit (Tet b offs) where + type ShowUnitType (Tet b offs) = + Text "tet{b=" :<>: ShowType b + :<>: Text ",offs=" :<>: ShowType offs :<>: Text "}" + showsUnitPrec d = showParen (d > 10) $ + showString "Tet " . shows (natVal (Proxy :: Proxy b)) + . showString " " + . shows (intVal (Proxy :: Proxy offs)) + prettysUnitPrec d = showParen (d > 10) $ + showString "tet " . shows (natVal (Proxy :: Proxy b)) + . showString " " + . shows (intVal (Proxy :: Proxy offs)) + + +type MidiPitch = Tet 12 (Neg 6900) + +data PitchException = OutOfMidiRange + +instance Exception PitchException + +instance Show PitchException where + show OutOfMidiRange = "A linear pitch is either negative or higher than 127,\ + \ and therefore cannot be converted to MIDI" + +safeDecomposePitchCents :: Real a => Tet b offs a -> Maybe (Word8, a) +safeDecomposePitchCents n = + if n >= 128 || n < 0 then + Nothing + else + Just (divMod' (unQuantity n) 1) + +decomposePitchCents :: Real a => Tet b offs a-> (Word8, a) +decomposePitchCents n = + if n >= 128 || n < 0 then + throw OutOfMidiRange + else + divMod' (unQuantity n) 1 +
+ src/Data/Units/NonStd/Temperature.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE TemplateHaskell #-} +module Data.Units.NonStd.Temperature + ( Data.Units.SI.Derived.Celsius (..) + , Fahrenheit(..) + ) where + +import Data.Units.Base +import Data.Units.SI +import Data.Units.SI.Derived + + +-- | Thermodynamic temperature in Fahrenheit degrees +-- +$(mkUnitNoFactor "Fahrenheit" "°F" ''Temperature) + +instance Fractional a => ConversionFactor Fahrenheit a where + factor = 5 / 9 + {-# INLINE factor #-} + +instance Fractional a => ConvertibleUnit Fahrenheit a where + toBaseUnit (Fahrenheit x) = Kelvin ((x + 459.67) * 5 / 9) + {-# INLINE toBaseUnit #-} + + fromBaseUnit (Kelvin x) = Fahrenheit (x * 9 / 5 - 459.67) + {-# INLINE fromBaseUnit #-}
+ src/Data/Units/NonStd/Time.hs view
@@ -0,0 +1,36 @@+{-# LANGUAGE TemplateHaskell #-} + + + +-------------------------------------------------------------------------------- +-- | +-- +-- Module : Data.Units.NonStd.Time +-- Description : Non standard time units +-- Copyright : (c) Alice Rixte 2025 +-- License : BSD 3 +-- Maintainer : alice.rixte@u-bordeaux.fr +-- Stability : unstable +-- Portability : non-portable (GHC extensions) +-- +-- Non standard time units. +-- +-------------------------------------------------------------------------------- + + +module Data.Units.NonStd.Time + ( module Data.Units.NonStd.Time + ) where + +import Data.Units.Base.TH +import Data.Units.SI + +-- | Time quantity in minutes +-- +$(mkUnit "Minute" "min" ''Time 60) + +-- | Time quantity in hours +-- +$(mkUnit "Hour" "hr" ''Time 3600) + +
+ src/Data/Units/SI.hs view
@@ -0,0 +1,25 @@+-------------------------------------------------------------------------------- +-- | +-- +-- Module : Data.Units.AngleSI.Angle +-- Description : International System of Units +-- Copyright : (c) Alice Rixte 2025 +-- License : BSD 3 +-- Maintainer : alice.rixte@u-bordeaux.fr +-- Stability : unstable +-- Portability : non-portable (GHC extensions) +-- +-- Provides standard units for the International System of Units (abbreviated +-- SI). +-- +-------------------------------------------------------------------------------- + +module Data.Units.SI + ( module Data.Units.SI.System + , module Data.Units.SI.Prefixes + , module Data.Units.SI.Derived + ) where + +import Data.Units.SI.System +import Data.Units.SI.Prefixes +import Data.Units.SI.Derived
+ src/Data/Units/SI/Derived.hs view
@@ -0,0 +1,24 @@+-------------------------------------------------------------------------------- +-- | +-- +-- Module : Data.Units.SI.Derived +-- Description : Derived units and dimensions for the International System of Units +-- Copyright : (c) Alice Rixte 2025 +-- License : BSD 3 +-- Maintainer : alice.rixte@u-bordeaux.fr +-- Stability : stable +-- Portability : non-portable (GHC extensions) +-- +-- Derived units and dimensions for the International System of Units. +-- +-------------------------------------------------------------------------------- + + +module Data.Units.SI.Derived + ( module Data.Units.SI.Derived.Angle + , module Data.Units.SI.Derived.NonAngle + ) + where + +import Data.Units.SI.Derived.Angle +import Data.Units.SI.Derived.NonAngle
+ src/Data/Units/SI/Derived/Angle.hs view
@@ -0,0 +1,84 @@+-------------------------------------------------------------------------------- +-- | +-- +-- Module : Data.Units.SI.Derived.Angle +-- Description : Dimensionless angles +-- Copyright : (c) Alice Rixte 2025 +-- License : BSD 3 +-- Maintainer : alice.rixte@u-bordeaux.fr +-- Stability : unstable +-- Portability : non-portable (GHC extensions) +-- +-- This module defines radians and steradians as derived dimensionless units. +-- +-- See "Data.Units.AngleSI" for radians and steradians in an angle +-- dimension `@A@`. +-- +-------------------------------------------------------------------------------- + +module Data.Units.SI.Derived.Angle + ( Angle + , Radian (..) + , normalizeRadians + , SolidAngle + , Steradian (..) + , LuminousFlux + , Lumen (..) + , Illuminance + , Lux (..) + ) + where + +import Data.Fixed +import Data.Coerce + +import Data.Units.Base +import Data.Units.SI.System +import Data.Units.SI.Derived.NonAngle + +-- | The angle derived dimension in SI. Equal to +-- +-- @'NoDim'@ +-- +type Angle = NormalizeDim (Length ./. Length) + +-- | An angle in radians. +-- +$(mkUnit "Radian" "rad" ''Angle 1) + +-- | The solid angle derived dimension in SI. Equal to +-- +-- @'NoDim'@ +-- +type SolidAngle = NormalizeDim (Angle .^+ 2) + +-- | A solid angle in steradians. +-- +$(mkUnit "Steradian" "sr" ''SolidAngle 1) + +-- | Normalize an angle to the range ]-pi, pi] +normalizeRadians :: (RealFrac a, Floating a) => Radian a -> Radian a +normalizeRadians x = if xmod > pi then xmod - twoPi else xmod + where + twoPi = 2 * pi + xmod = x `mod'` twoPi + +-- | Luminous flux quantity. Equal to +-- +-- @ 'LuminousIntensity'@ +-- +type LuminousFlux = NormalizeDim (LuminousIntensity .*. SolidAngle) + +-- | Luminous flux in lumens +-- +$(mkUnit "Lumen" "lm" ''LuminousFlux 1) + +-- | Illuminance quantity. Equal to +-- +-- @ 'Length'.^-2 .*. 'LuminousIntensity' @ +-- +type Illuminance = NormalizeDim (LuminousFlux ./. Area) + +-- | Illuminance in lux +-- +$(mkUnit "Lux" "lx" ''Illuminance 1)
+ src/Data/Units/SI/Derived/NonAngle.hs view
@@ -0,0 +1,255 @@+-------------------------------------------------------------------------------- +-- | +-- +-- Module : Data.Units.SI.System +-- Description : SI derived units not containing angles. +-- Copyright : (c) Alice Rixte 2025 +-- License : BSD 3 +-- Maintainer : alice.rixte@u-bordeaux.fr +-- Stability : stable +-- Portability : non-portable (GHC extensions) +-- +-- Derived units and dimensions for the International System of Units that do +-- not contain angles. +-- +-- This follows the wikipedia page https://en.wikipedia.org/wiki/SI_derived_unit. +-- +-------------------------------------------------------------------------------- + + +module Data.Units.SI.Derived.NonAngle + ( -- * Official derived units from SI + Celsius (..) + , Area + , Volume + , Frequency + , Hertz (..) + , Radioactivity + , Becquerel (..) + , Speed + , Acceleration + , Force + , Newton (..) + , Pressure + , Pascal (..) + , Stress + , Energy + , Joule (..) + , Work + , Heat + , Power + , RadiantFlux + , Watt (..) + , ElectricCharge + , Coulomb (..) + , QuantityOfElectricity + , Voltage + , Volt (..) + , ElectricPotential + , ElectromotiveForce + , Capacitance + , Farad (..) + , Resistance + , Ohm (..) + , Impedance + , Reactance + , Conductance + , Siemens (..) + , MagneticFlux + , Weber (..) + , MagneticInduction + , MagneticFluxDensity + , Tesla (..) + , Inductance + , Henry (..) + , EquivalentDose + , AbsorbedDose + , Gray (..) + , Sievert (..) + , CatalyticActivity + , Katal (..) + ) + where + +import Data.Units.Base +import Data.Units.SI.System + +type Area = Length .^+ 2 +type Volume = Length .^+ 3 + +type Frequency = Time .^- 1 + +-- | Frequency in hertz +-- +$(mkUnit "Hertz" "Hz" ''Frequency 1) + +type Radioactivity = Time .^- 1 + +-- | Radioactivity in becquerels +-- +$(mkUnit "Becquerel" "Bq" ''Radioactivity 1) + +-- | Speed quantity. Equal to +-- +-- @'Length' .*. 'Time'.^-1@ +-- +type Speed = NormalizeDim (Length ./. Time) + +type Acceleration = NormalizeDim (Length ./. Time .^+ 2) + +-- | Acceleration quantity. Equal to +-- +-- @'Length' .*. 'Time'.^-2@ +-- +type Force = NormalizeDim (Mass .*. Acceleration) + +-- | Force in newtons +-- +$(mkUnit "Newton" "N" ''Force 1) + +-- | Pressure quantity. Equal to +-- +-- @'Mass' .*. 'Length'.^-1 .*. 'Time'.^-2@ +-- +type Pressure = NormalizeDim (Force ./. (Length .^+ 2)) +type Stress = Pressure + +-- | Pressure in pascals +-- +$(mkUnit "Pascal" "Pa" ''Pressure 1) + +-- | Energy quantity. Equal to +-- +-- @ 'Mass' .*. 'Length'.^+2 .*. 'Time'.^-2@ +-- +type Energy = NormalizeDim (Length .*. Force) +type Work = Energy +type Heat = Energy + +-- | Energy in joules +-- +$(mkUnit "Joule" "J" ''Energy 1) + +-- | Power quantity. Equal to +-- +-- @ 'Mass' .*. 'Length'.^+2 .*. 'Time'.^-3 @ +-- + + +type Power = NormalizeDim (Energy ./. Time) +type RadiantFlux = Power + +-- | Power in watts +-- +$(mkUnit "Watt" "W" ''Power 1) + +-- | Electric charge quantity. Equal to +-- +-- @ 'Time' .*. 'Current'@ +-- +type ElectricCharge = NormalizeDim (Time .*. Current) +type QuantityOfElectricity = ElectricCharge + + +$(mkUnit "Coulomb" "C" ''ElectricCharge 1) + +-- | Electric voltage quantity. Equal to +-- +-- @ 'Mass' .*. 'Length'.^+2 .*. 'Time'.^-3 .*. 'Current'.^-1@ +-- +type Voltage = NormalizeDim (Power ./. Current) +type ElectricPotential = Voltage +type ElectromotiveForce = Voltage + +$(mkUnit "Volt" "V" ''Voltage 1) + +-- | Electric capacitance quantity. Equal to +-- +-- @'Mass'.^-1 .*. 'Length'.^-2 .*. 'Time'.^+4 .*. 'Current'.^+2@. +-- +type Capacitance = NormalizeDim (ElectricCharge ./. Voltage) + +$(mkUnit "Farad" "F" ''Capacitance 1) + +-- | Electric resistance quantity. Equal to +-- +-- @ 'Mass' .*. 'Length'.^+2 .*. 'Time'.^-3 .*. 'Current'.^-2@ +-- +type Resistance = NormalizeDim (Voltage ./. Current) +type Impedance = Resistance +type Reactance = Resistance + +$(mkUnit "Ohm" "Ω" ''Resistance 1) + +-- | Electric conductance quantity. Equal to +-- +-- @ Mass.^-1 .*. Length.^-2 .*. Time.^+3 .*. Current.^+2 @ +-- +type Conductance = NormalizeDim (Current ./. Voltage) + +$(mkUnit "Siemens" "S" ''Conductance 1) + +-- | Magnetic flux quantity. Equal to +-- +-- @ 'Mass' .*. 'Length'.^+2 .*. 'Time'.^-2 .*. 'Current'.^-1@ +-- +type MagneticFlux = NormalizeDim (Voltage .*. Time) + +$(mkUnit "Weber" "Wb" ''MagneticFlux 1) + +-- | Magnetic induction quantity. Equal to +-- +-- @ 'Mass' .*. 'Time'.^-2 .*. 'Current'.^-1@ +-- +type MagneticInduction = NormalizeDim (MagneticFlux ./. (Length .^+ 2)) +type MagneticFluxDensity = MagneticInduction + +$(mkUnit "Tesla" "T" ''MagneticInduction 1) + +-- | Inductance quantity. Equal to +-- +-- @ 'Mass' .*. 'Length'.^+2 .*. 'Time'.^-2 .*. 'Current'.^-2@ + +type Inductance = NormalizeDim (MagneticFlux ./. Current) + +$(mkUnit "Henry" "H" ''Inductance 1) + +-- | Thermodynamic temperature in Celsius degrees +-- +$(mkUnitNoFactor "Celsius" "°C" ''Temperature) + +instance Fractional a => ConversionFactor Celsius a where + factor = 1 + {-# INLINE factor #-} + +instance Fractional a => ConvertibleUnit Celsius a where + toBaseUnit (Celsius x) = Kelvin (x + 273.15) + {-# INLINE toBaseUnit #-} + + fromBaseUnit (Kelvin x) = Celsius (x - 273.15) + {-# INLINE fromBaseUnit #-} + +-- | Absorbed dose quantity. Equal to +-- +-- @ 'Length'.^+2 .*. 'Time'.^-2 @ +-- +type AbsorbedDose = NormalizeDim (Energy ./. Mass) +type EquivalentDose = AbsorbedDose + +-- | Absorbed dose in grays +-- +$(mkUnit "Gray" "Gy" ''AbsorbedDose 1) + +-- | Dose equivalent in sieverts +-- +$(mkUnit "Sievert" "Sv" ''EquivalentDose 1) + +-- | Catalytic activity quantity. Equal to +-- +-- @ 'Time'.^-1 .*. 'AmountOfSubstance' @ +-- +type CatalyticActivity = NormalizeDim (AmountOfSubstance ./. Time) + +-- | Catalytic activity in katal +-- +$(mkUnit "Katal" "kat" ''CatalyticActivity 1)
+ src/Data/Units/SI/NonStd.hs view
@@ -0,0 +1,24 @@+ + +-------------------------------------------------------------------------------- +-- | +-- +-- Module : Data.Units.SI.NonStd +-- Description : Non standard units in the SI dimensions +-- Copyright : (c) Alice Rixte 2025 +-- License : BSD 3 +-- Maintainer : alice.rixte@u-bordeaux.fr +-- Stability : stable +-- Portability : non-portable (GHC extensions) +-- +-------------------------------------------------------------------------------- + +module Data.Units.SI.NonStd + ( module Data.Units.NonStd + , module Data.Units.SI.NonStd.Angle + ) +where + +import Data.Units.NonStd +import Data.Units.SI.NonStd.Angle +
+ src/Data/Units/SI/NonStd/Angle.hs view
@@ -0,0 +1,54 @@+-------------------------------------------------------------------------------- +-- | +-- +-- Module : Data.Units.SI.NonStd.Angle +-- Description : Dimensionless non standard angle units +-- Copyright : (c) Alice Rixte 2025 +-- License : BSD 3 +-- Maintainer : alice.rixte@u-bordeaux.fr +-- Stability : unstable +-- Portability : non-portable (GHC extensions) +-- +-- Dimensionless non standard angle units. +-- +-------------------------------------------------------------------------------- + + +module Data.Units.SI.NonStd.Angle where + +import Data.Units.Base + +-- | Angle in degrees. +-- +$(mkUnitNoFactor "Degree" "°" ''NoDim) + +-- | Angle in complete turns (also called cycles or revolutions) +-- +-- See https://en.wikipedia.org/wiki/Turn_(angle) +-- +$(mkUnitNoFactor "Turn" "tr" ''NoDim) + +-- | Angle in gradians +-- +-- See https://en.wikipedia.org/wiki/Gradian +-- +$(mkUnitNoFactor "Gradian" "grad" ''NoDim) + +instance Floating a => ConvertibleUnit Degree a + +instance Floating a => ConversionFactor Degree a where + factor = pi / 180 + {-# INLINE factor #-} + +instance Floating a => ConvertibleUnit Turn a + +instance Floating a => ConversionFactor Turn a where + factor = 2 * pi + {-# INLINE factor #-} + +instance Floating a => ConvertibleUnit Gradian a + +instance Floating a => ConversionFactor Gradian a where + factor = pi / 200 + {-# INLINE factor #-} +
+ src/Data/Units/SI/Prefixes.hs view
@@ -0,0 +1,145 @@+{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE QuantifiedConstraints #-} +{-# LANGUAGE InstanceSigs #-} +{-# LANGUAGE TemplateHaskell #-} + +-------------------------------------------------------------------------------- +-- | +-- +-- Module : Data.Units.SI.Prefixes +-- Description : Prefixes of the International System of Units +-- Copyright : (c) Alice Rixte 2025 +-- License : BSD 3 +-- Maintainer : alice.rixte@u-bordeaux.fr +-- Stability : stable +-- Portability : non-portable (GHC extensions) +-- +-- Prefixes for the International System of Units (abbreviated SI). +-- +-------------------------------------------------------------------------------- + +module Data.Units.SI.Prefixes + ( Quecto (..) + , Ronto (..) + , Yocto (..) + , Zepto (..) + , Atto (..) + , Femto (..) + , Pico (..) + , Nano (..) + , Micro (..) + , Milli (..) + , Centi (..) + , Deci (..) + , Deca (..) + , Hecto (..) + , Kilo (..) + , Mega (..) + , Giga (..) + , Tera (..) + , Peta (..) + , Exa (..) + , Zetta (..) + , Yotta (..) + , Ronna (..) + , Quecca (..) + ) where + +import Data.Units.Base + +-- | SI prefix for 10⁻³⁰ +-- +$(mkPrefix "Quecto" "q" 1e-30) + +-- | SI prefix for 10⁻²⁷ +-- +$(mkPrefix "Ronto" "r" 1e-27) + +-- | SI prefix for 10⁻²⁴ +-- +$(mkPrefix "Yocto" "y" 1e-24) + +-- | SI prefix for 10⁻²¹ +-- +$(mkPrefix "Zepto" "z" 1e-21) + +-- | SI prefix for 10⁻¹⁸ +-- +$(mkPrefix "Atto" "a" 1e-18) + +-- | SI prefix for 10⁻¹⁵ +-- +$(mkPrefix "Femto" "f" 1e-15) + +-- | SI prefix for 10⁻¹² +-- +$(mkPrefix "Pico" "p" 1e-12) + +-- | SI prefix for 10⁻⁹ +-- +$(mkPrefix "Nano" "n" 1e-9) + +-- | SI prefix for 10⁻⁶ +-- +$(mkPrefix "Micro" "µ" 1e-6) + +-- | SI prefix for 10⁻³ +-- +$(mkPrefix "Milli" "m" 1e-3) + +-- | SI prefix for 10⁻² +-- +$(mkPrefix "Centi" "c" 1e-2) + +-- | SI prefix for 10⁻¹ +-- +$(mkPrefix "Deci" "d" 1e-1) + +-- | SI prefix for 10¹ +-- +$(mkPrefix "Deca" "da" 1e1) + +-- | SI prefix for 10² +-- +$(mkPrefix "Hecto" "h" 1e2) + +-- | SI prefix for 10³ +-- +$(mkPrefix "Kilo" "k" 1e3) + +-- | SI prefix for 10⁶ +-- +$(mkPrefix "Mega" "M" 1e6) + +-- | SI prefix for 10⁹ +-- +$(mkPrefix "Giga" "G" 1e9) + +-- | SI prefix for 10¹² +-- +$(mkPrefix "Tera" "T" 1e12) + +-- | SI prefix for 10¹⁵ +-- +$(mkPrefix "Peta" "P" 1e15) + +-- | SI prefix for 10¹⁸ +-- +$(mkPrefix "Exa" "E" 1e18) + +-- | SI prefix for 10²¹ +-- +$(mkPrefix "Zetta" "Z" 1e21) + +-- | SI prefix for 10²⁴ +-- +$(mkPrefix "Yotta" "Y" 1e24) + +-- | SI prefix for 10²⁷ +-- +$(mkPrefix "Ronna" "R" 1e27) + +-- | SI prefix for 10³⁰ +-- +$(mkPrefix "Quecca" "Q" 1e30)
+ src/Data/Units/SI/System.hs view
@@ -0,0 +1,174 @@+-------------------------------------------------------------------------------- +-- | +-- +-- Module : Data.Units.SI.System +-- Description : Base units and dimensions of the International System of Units +-- Copyright : (c) Alice Rixte 2025 +-- License : BSD 3 +-- Maintainer : alice.rixte@u-bordeaux.fr +-- Stability : stable +-- Portability : non-portable (GHC extensions) +-- +-- Base units and dimensions of the International System of Units (abbreviated +-- SI). +-- +-------------------------------------------------------------------------------- + +module Data.Units.SI.System + ( Mass (..) + , Gram (..) + , Length (..) + , Meter (..) + , Time (..) + , Second (..) + , Temperature (..) + , Kelvin (..) + , AmountOfSubstance (..) + , Mole (..) + , Current (..) + , Ampere (..) + , LuminousIntensity (..) + , Candela (..) + ) where + +import Data.Coerce +import Data.Units.Base +import Data.Units.SI.Prefixes + +-- +--------------------------------------+-----------------+ +-- | Dimension | Id | +-- +======================================+=================+ +-- | Reserved | 0 | +-- +======================================+=================+ +-- | @'NoDim'@ | 1 | +-- +--------------------------------------+-----------------+ +-- | @'Data.Units.AngleSI.Angle.Angle'@ | 1000 | +-- +--------------------------------------+-----------------+ +-- | @'Data.Units.SI.Mass'@ | 2000 | +-- +--------------------------------------+-----------------+ +-- | @'Data.Units.SI.Length'@ | 3000 | +-- +--------------------------------------+-----------------+ +-- | @'Data.Units.SI.Time'@ | 4000 | +-- +--------------------------------------+-----------------+ +-- | @'Data.Units.SI.ElectricCurrent'@ | 5000 | +-- +--------------------------------------+-----------------+ +-- | @'Data.Units.SI.Temperature'@ | 6000 | +-- +--------------------------------------+-----------------+ +-- | @'Data.Units.SI.AmountOfSubstance'@ | 7000 | +-- +--------------------------------------+-----------------+ +-- | @'Data.Units.SI.LuminousIntensity'@ | 8000 | +-- +--------------------------------------+-----------------+ + +-- | The mass dimension, denotated @M@ in SI. +-- +-- This may contain a mass quantity with unspecified unit. +-- +$(mkDim "Mass" "M" 2000) + + +-- | A quantity in grams, denotated @g@ in SI. +-- +-- Notice that the base unit for the mass dimension is @'Kilo' 'Gram'@, not +-- @'Gram'@. +-- +-- This is correctly taken into account by this library: +-- +-- >>> from (Gram 8) +-- quantity @(Kilo Gram) 8.0e-3 +-- >>> :kind! BaseUnitOf Gram +-- BaseUnitOf Gram :: * -> * +-- = Kilo Gram +-- +$(mkUnitNoFactor "Gram" "g" ''Mass) + +instance Fractional a => ConvertibleUnit Gram a + +instance Fractional a => ConversionFactor Gram a where + factor = 1e-3 + {-# INLINE factor #-} + +instance IsDim Mass where + type DimToUnit Mass = Kilo Gram + +-- | The length dimension, denotated @L@ in SI. +-- +-- This may contain a length quantity with unspecified unit. +-- +$(mkDim "Length" "L" 3000) + +-- | A quantity in meters, denotated @m@ in SI. +-- +-- This is the base unit of the length dimension in the SI system. +-- +$(mkBaseUnit "Meter" "m" ''Length) + + +-- | The time dimension, denotated @T@ in SI. +-- +-- This may contain a length quantity with unspecified unit. +-- +$(mkDim "Time" "T" 4000) + + +-- | A quantity in seconds, denotated @s@ in SI. +-- +-- This is the base unit of the time dimension in the SI system. +-- +$(mkBaseUnit "Second" "s" ''Time) + +-- | The electric current dimension, denotated @I@ in SI. +-- +-- This may contain an electric current quantity with unspecified unit. +-- +$(mkDim "Current" "I" 5000) + + +-- | A quantity in amperes, denotated @A@ in SI. +-- +-- This is the base unit of the electric current dimension in the SI +-- system. +-- +$(mkBaseUnit "Ampere" "A" ''Current) + +-- | The thermodynamic temperature dimension, denotated @Θ@ in SI. +-- +-- This may contain a temperature quantity with unspecified unit. +-- +$(mkDim "Temperature" "Θ" 6000) + + +-- | A quantity in Kelvin, denotated @K@ in SI. +-- +-- This is the base unit of the thermodynamic temperature dimension in the SI +-- system. +-- +$(mkBaseUnit "Kelvin" "K" ''Temperature) + +-- | The amount of substance dimension, denotated @N@ in SI. +-- +-- This may contain an amount of substance quantity with unspecified unit. +-- +$(mkDim "AmountOfSubstance" "N" 7000) + + +-- | A quantity in moles, denotated @mol@ in SI. +-- +-- This is the base unit of the amount of substance dimension in the SI +-- system. +-- +$(mkBaseUnit "Mole" "mol" ''AmountOfSubstance) + + +-- | The luminous intensity dimension, denotated @J@ in SI. +-- +-- This may contain a luminous intensity quantity with unspecified unit. +-- +$(mkDim "LuminousIntensity" "J" 8000) + +-- | A quantity in candelas, denotated @cd@ in SI. +-- +-- This is the base unit of the luminous intensity dimension in the SI +-- system. +-- +$(mkBaseUnit "Candela" "cd" ''LuminousIntensity) +
+ test/Data/Epsilon.hs view
@@ -0,0 +1,40 @@+module Data.Epsilon + ( MkEpsilon (..) + , aboutEqual + , isApproxId + , module Linear.Epsilon + ) where + +import Foreign.C.Types (CFloat, CDouble) +import Linear.Epsilon + +-- | Provides a near-zero quantity +class MkEpsilon a where + -- | A near-zero quantity + epsilon :: a + +-- | ε = 1e-6 +instance MkEpsilon Float where + epsilon = 1e-6 + +-- | ε = 1e-12 +instance MkEpsilon Double where + epsilon = 1e-12 + +-- | ε = 1e-6 +instance MkEpsilon CFloat where + epsilon = 1e-6 + +-- | ε = 1e-12 +instance MkEpsilon CDouble where + epsilon = 1e-12 + + +-- | Equality up to epsilon +-- +-- @'aboutEqual' a b@ is true iff @|a - b| < ε@ +aboutEqual :: Epsilon a => a -> a -> Bool +aboutEqual a b = nearZero (a - b) + +isApproxId :: Epsilon a => (a -> a) -> a -> Bool +isApproxId f a = aboutEqual (f a) a
+ test/Data/Units/Base/ArithmeticProp.hs view
@@ -0,0 +1,454 @@+{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE TypeApplications #-} + +module Data.Units.Base.ArithmeticProp where + +import Data.Coerce + +import Test.QuickCheck +import Test.Hspec + +import Data.Epsilon + +import Data.Units + +import Data.Units.Base.ConvertProp + + +------------------------- Num and fractional instances ------------------------- + +addNumProp :: forall u a. + ( IsUnit u, Floating (u a) + , Show a, Epsilon a, Floating a, Eq a, Arbitrary a) => Property +addNumProp = property (\ (a :: a) (b :: a) -> + a + b + pi == + coerce (coerce a + coerce b + pi :: u a) + ) + +divNumProp :: forall u a. + ( IsUnit u, Fractional (u a) + , Show a, Epsilon a, Fractional a, Eq a, Arbitrary a) + => Property +divNumProp = property (\ (a :: a) (b :: a) -> b == 0 || + a / b == + coerce (coerce a / coerce b :: u a)) + +floatingSpec :: forall u a. + ( IsUnit u, Floating (u a), ShowUnit u + , Show a, Epsilon a, Floating a, Eq a, Arbitrary a + ) + => Spec +floatingSpec = it ("Floating instance for " ++ showUnit @u ) $ + addNumProp @u @a .&&. divNumProp @u @a + + +-------------------------- Arithmetic same dimension --------------------------- + + + +addRight :: forall u v a. + ( FromTo' v u a + ) + => a -> a -> a +addRight u v = coerce $ (coerce u :: u a) .+~ (coerce v :: v a) + +addRightProp :: forall u v a. + ( FromTo' v u a + , DimEq u v + , Show a, Epsilon a, Arbitrary a + ) + => Property +addRightProp = property (\(a :: a) (b :: a) -> + aboutEqual (coerce (fromTo' (coerce a :: u a) :: v a) + b :: a) + (addRight @u @v a b)) + +addRightSpec :: forall u v a. + ( FromTo' v u a + , DimEq u v + , ShowUnit u, ShowUnit v + , Show a, Epsilon a, Arbitrary a + ) => Spec +addRightSpec = it (showUnit @u ++ " .+~ " ++ showUnit @v) + $ addRightProp @u @v @a + +addLeft :: forall u v a. + ( FromTo' u v a + ) + => a -> a -> a +addLeft u v = coerce $ (coerce u :: u a) ~+. (coerce v :: v a) + +addLeftProp :: forall u v a. + ( FromTo' u v a + , DimEq u v + , Show a, Epsilon a, Arbitrary a + ) + => Property +addLeftProp = property (\(a :: a) (b :: a) -> + aboutEqual (coerce (fromTo' (coerce a :: u a) :: v a) + b :: a) + (addLeft @u @v a b)) + +addLeftSpec :: forall u v a. + ( FromTo' u v a + , DimEq u v + , ShowUnit u, ShowUnit v + , Show a, Epsilon a, Arbitrary a + ) => Spec +addLeftSpec = it (showUnit @u ++ " ~+. " ++ showUnit @v) + $ addLeftProp @u @v @a + + +addSame :: forall u v a. + FromTo' u v a + => a -> a -> a +addSame u v = coerce $ (coerce u :: u a) ~+~ (coerce v :: v a) + +addSameProp :: forall u v a. + ( FromTo' u v a + , DimEq u v + , Show a, Epsilon a, Arbitrary a + ) + => Property +addSameProp = property (\(a :: a) (b :: a) -> + aboutEqual (coerce (fromTo' (coerce a :: u a) :: v a) + b :: a) + (addSame @u @v a b)) + +addSameSpec :: forall u v a. + ( FromTo' u v a + , DimEq u v + , ShowUnit u, ShowUnit v + , Show a, Epsilon a, Arbitrary a + ) => Spec +addSameSpec = it (showUnit @u ++ " ~+~ " ++ showUnit @v) + $ addSameProp @u @v @a + +--------------------------------- Subtraction ---------------------------------- + +subRight :: forall u v a. + ( FromTo' v u a + ) + => a -> a -> a +subRight u v = coerce $ (coerce u :: u a) .-~ (coerce v :: v a) + +subRightProp :: forall u v a. + ( FromTo' v u a + , DimEq u v + , Show a, Epsilon a, Arbitrary a + ) + => Property +subRightProp = property (\(a :: a) (b :: a) -> + aboutEqual (coerce (fromTo' (coerce a :: u a) :: v a) - b :: a) + (subRight @u @v a b)) + +subRightSpec :: forall u v a. + ( FromTo' v u a + , DimEq u v + , ShowUnit u, ShowUnit v + , Show a, Epsilon a, Arbitrary a + ) => Spec +subRightSpec = it (showUnit @u ++ " .-~ " ++ showUnit @v) + $ subRightProp @u @v @a + +subLeft :: forall u v a. + ( FromTo' u v a + ) + => a -> a -> a +subLeft u v = coerce $ (coerce u :: u a) ~-. (coerce v :: v a) + +subLeftProp :: forall u v a. + ( FromTo' u v a + , DimEq u v + , Show a, Epsilon a, Arbitrary a + ) + => Property +subLeftProp = property (\(a :: a) (b :: a) -> + aboutEqual (coerce (fromTo' (coerce a :: u a) :: v a) - b :: a) + (subLeft @u @v a b)) + +subLeftSpec :: forall u v a. + ( FromTo' u v a + , DimEq u v + , ShowUnit u, ShowUnit v + , Show a, Epsilon a, Arbitrary a + ) => Spec +subLeftSpec = it (showUnit @u ++ " ~-. " ++ showUnit @v) + $ subLeftProp @u @v @a + + +subSame :: forall u v a. + FromTo' u v a + => a -> a -> a +subSame u v = coerce $ (coerce u :: u a) ~-~ (coerce v :: v a) + +subSameProp :: forall u v a. + ( FromTo' u v a + , DimEq u v + , Show a, Epsilon a, Arbitrary a + ) + => Property +subSameProp = property (\(a :: a) (b :: a) -> + aboutEqual (coerce (fromTo' (coerce a :: u a) :: v a) - b :: a) + (subSame @u @v a b)) + +subSameSpec :: forall u v a. + ( FromTo' u v a + , DimEq u v + , ShowUnit u, ShowUnit v + , Show a, Epsilon a, Arbitrary a + ) => Spec +subSameSpec = it (showUnit @u ++ " ~-~ " ++ showUnit @v) + $ subSameProp @u @v @a + + +-------------------------------- Multiplication -------------------------------- + +mulSame :: forall u v a u2 . + ( u2 ~ BaseUnitOf u .^+ 2, IsUnit u2 + , DimEq u v + , ConversionFactor u a, ConversionFactor v a + ) + => a -> a -> a +mulSame u v = coerce $ (coerce u :: u a) ~*~ (coerce v :: v a) + +mulSameProp :: forall u v a u2 . + ( u2 ~ BaseUnitOf u .^+ 2, IsUnit u2 + , DimEq u v + , ConversionFactor u a, ConversionFactor v a + , Show a, Epsilon a, Arbitrary a + ) + => Property +mulSameProp = property (\(a :: a) (b :: a) -> + aboutEqual (coerce (fromTo' (coerce a :: u a) :: v a) * b :: a) + (mulSame @u @v a b)) + +mulSameSpec :: forall u v a u2 . + ( u2 ~ BaseUnitOf u .^+ 2, IsUnit u2 + , DimEq u v + , ConversionFactor u a, ConversionFactor v a + , ShowUnit u, ShowUnit v + , Show a, Epsilon a, Arbitrary a + ) + => Spec +mulSameSpec = it (showUnit @u ++ " ~*~ " ++ showUnit @v) + $ mulSameProp @u @v @a + +divSame :: forall u v a u2 . + ( u2 ~ BaseUnitOf u .^+ 2, IsUnit u2 + , DimEq u v + , ConversionFactor u a, ConversionFactor v a + ) + => a -> a -> a +divSame u v = coerce $ (coerce u :: u a) ~/~ (coerce v :: v a) + +divSameProp :: forall u v a u2 . + ( u2 ~ BaseUnitOf u .^+ 2, IsUnit u2 + , DimEq u v + , ConversionFactor u a, ConversionFactor v a + , Show a, Epsilon a, Eq a, Arbitrary a + ) + => Property +divSameProp = property (\(a :: a) (b :: a) -> b == 0 || + aboutEqual (coerce (fromTo' (coerce a :: u a) :: v a) / b :: a) + (divSame @u @v a b)) + +divSameSpec :: forall u v a u2 . + ( u2 ~ BaseUnitOf u .^+ 2, IsUnit u2 + , DimEq u v + , ConversionFactor u a, ConversionFactor v a + , ShowUnit u, ShowUnit v + , Show a, Epsilon a, Eq a, Arbitrary a + ) + => Spec +divSameSpec = it (showUnit @u ++ " ~/~ " ++ showUnit @v) + $ divSameProp @u @v @a + +------------------ Arithmetic with two different dimensions ------------------- + +-- mulRight :: forall u v a u2. +-- ( u2 ~ BaseUnitOf' (u .^+ 2) , IsUnit u2 +-- , DimEq u v +-- , FromTo' v u a +-- ) +-- => a -> a -> a +-- mulRight u v = coerce $ (coerce u :: u a) .*~ (coerce v :: v a) + +-- mulRightProp :: forall u v a u2. +-- ( u2 ~ BaseUnitOf' (u .^+ 2) , IsUnit u2 +-- , DimEq u v +-- , FromTo' v u a +-- , Show a, Epsilon a, Arbitrary a +-- ) +-- => Property +-- mulRightProp = property (\(a :: a) (b :: a) -> +-- aboutEqual (coerce (fromTo' (coerce a :: u a) :: v a) * b :: a) +-- (mulRight @u @v a b)) + +-- mulRightSpec :: forall u v a u2. +-- ( u2 ~ BaseUnitOf' (u .^+ 2) , IsUnit u2 +-- , DimEq u v +-- , FromTo' v u a +-- , ShowUnit u, ShowUnit v +-- , Show a, Epsilon a, Arbitrary a +-- ) => Spec +-- mulRightSpec = it (showUnit @u ++ " .*~ " ++ showUnit @v) +-- $ mulRightProp @u @v @a + +-- mulLeft :: forall u v a v2. +-- ( v2 ~ BaseUnitOf' (v .^+ 2), IsUnit v2 +-- , DimEq v u +-- , FromTo' u v a +-- ) +-- => a -> a -> a +-- mulLeft u v = coerce $ (coerce u :: u a) ~*. (coerce v :: v a) + +-- mulLeftProp :: forall u v a v2. +-- ( v2 ~ BaseUnitOf' (v .^+ 2), IsUnit v2 +-- , DimEq v u +-- , FromTo' u v a +-- , Show a, Epsilon a, Arbitrary a +-- ) +-- => Property +-- mulLeftProp = property (\(a :: a) (b :: a) -> +-- aboutEqual (coerce (fromTo' (coerce a :: u a) :: v a) * b :: a) +-- (mulLeft @u @v a b)) + +-- mulLeftSpec :: forall u v a v2. +-- ( v2 ~ BaseUnitOf' (v .^+ 2), IsUnit v2 +-- , DimEq v u +-- , FromTo' u v a +-- , ShowUnit u, ShowUnit v +-- , Show a, Epsilon a, Arbitrary a +-- ) => Spec +-- mulLeftSpec = it (showUnit @u ++ " ~*. " ++ showUnit @v) +-- $ mulLeftProp @u @v @a + + +mulDiffDim :: forall u v a. + ( ConversionFactor u a, ConversionFactor v a) + => a -> a -> a +mulDiffDim u v = coerce $ (coerce u :: u a) .*. (coerce v :: v a) + +mulDiffDimProp :: forall u v a. + ( ConversionFactor u a, ConversionFactor v a + , ConversionFactor (u .*. v) a + , Arbitrary a, Show a, Epsilon a + ) + => Property +mulDiffDimProp = + property (\a b -> aboutEqual (a * b) (mulDiffDim @u @v @a a b) ) + .&&. toFromProp @(u .*. v) @a + +mulDiffDimSpec :: forall u v a. + ( ConversionFactor u a, ConversionFactor v a + , ConversionFactor (u .*. v) a + , Arbitrary a, Show a, Epsilon a + , ShowUnit u, ShowUnit v + ) + => Spec +mulDiffDimSpec = it (showUnit @u ++ " .*. " ++ showUnit @v) + $ mulDiffDimProp @u @v @a + +divDiffDim :: forall u v a. + ( IsUnit u, IsUnit v, IsUnit (u ./. v) + , Fractional a + ) + => a -> a -> a +divDiffDim u v = coerce $ (coerce u :: u a) ./. (coerce v :: v a) + +divDiffDimProp :: forall u v a. + ( IsUnit u, IsUnit v, IsUnit (u ./. v) + , ConversionFactor (u ./. v) a + , IsUnit (u ./. v) + , Arbitrary a, Show a, Epsilon a, Eq a, Fractional a + ) + => Property +divDiffDimProp = + property (\a b -> b == 0 || aboutEqual (a / b) (divDiffDim @u @v @a a b) ) + .&&. toFromProp @(u ./. v) @a + +divDiffDimSpec :: forall u v a. + (IsUnit u, IsUnit v, IsUnit (u ./. v) + , ConversionFactor (u ./. v) a + , IsUnit (u ./. v) + , Arbitrary a, Show a, Epsilon a, Eq a, Fractional a + , ShowUnit u, ShowUnit v + ) + => Spec +divDiffDimSpec = it (showUnit @u ++ " ./. " ++ showUnit @v) + $ divDiffDimProp @u @v @a + +-------------------------------- Exponentiation -------------------------------- + +exp2 :: forall u a. + ConversionFactor u a + => a -> a +exp2 u = coerce $ (coerce u :: u a) .^. pos2 + +expm1 :: forall u a. + ConversionFactor u a + => a -> a +expm1 u = coerce $ (coerce u :: u a) .^. neg1 + +exp2Prop :: forall u a. + ( ConversionFactor u a + , Arbitrary a, Show a, Epsilon a + ) + => Property +exp2Prop = + property (\a -> aboutEqual (a * a) (exp2 @u @a a)) + +expm1Prop :: forall u a. + ( ConversionFactor u a + , Arbitrary a, Show a, Eq a, Epsilon a + ) + => Property +expm1Prop = + property (\a -> a == 0 || aboutEqual (1 / a) (expm1 @u @a a)) + +expSpec :: forall u a. + ( ConversionFactor u a + , ShowUnit u + , Arbitrary a, Show a, Eq a, Epsilon a + ) + => Spec +expSpec = it (showUnit @u ++ " .^+ 2 ," ++ showUnit @u ++ " .^- 1") $ + exp2Prop @u @a .&&. expm1Prop @u @a + +exp2Conv :: forall u a. + ConversionFactor u a + => a -> a +exp2Conv u = coerce $ (coerce u :: u a) ~^~ pos2 + +expm1Conv :: forall u a. + ConversionFactor u a + => a -> a +expm1Conv u = coerce $ (coerce u :: u a) ~^~ neg1 + +exp2ConvProp :: forall u a. + ( ConversionFactor u a + , Arbitrary a, Show a, Epsilon a + ) + => Property +exp2ConvProp = + property (\a -> aboutEqual + (coerce (toBaseUnit' (coerce a :: u a)) ^^ (2 :: Int)) (exp2Conv @u @a a)) + +expm1ConvProp :: forall u a. + ( ConversionFactor u a + , Arbitrary a, Show a, Eq a, Epsilon a + ) + => Property +expm1ConvProp = + property (\a -> a == 0 || + aboutEqual (coerce (toBaseUnit' (coerce a :: u a)) ^^ (-1 :: Int)) (expm1Conv @u @a a)) + + +expConvSpec :: forall u a. + ( ConversionFactor u a + , ShowUnit u + , Arbitrary a, Show a, Eq a, Epsilon a + ) + => Spec +expConvSpec = it (showUnit @u ++ "~^~ pos2 ," ++ showUnit @u ++ "~^~ neg1") $ + exp2ConvProp @u @a .&&. expm1ConvProp @u @a +
+ test/Data/Units/Base/ConvertProp.hs view
@@ -0,0 +1,109 @@+{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE AllowAmbiguousTypes #-} +-- {-# LANGUAGE MonoLocalBinds #-} +{-# LANGUAGE TypeApplications #-} + +module Data.Units.Base.ConvertProp where + + +import Data.Coerce + +import Data.Epsilon + +import Test.QuickCheck +import Test.Hspec + +import Data.Units + +------------------------------------ toFrom ------------------------------------ + +toFrom :: forall u a. + (ConvertibleUnit u a) + => a -> a +toFrom a = coerce + (toBaseUnit (fromBaseUnit @u (coerce a :: BaseUnitOf u a) :: u a) + :: BaseUnitOf u a) + +toFromProp :: forall u a. + ( ConvertibleUnit u a + , Arbitrary a, Show a, Epsilon a + ) + => Property +toFromProp = property (isApproxId (toFrom @u @a)) + +toFromSpec :: forall u a. + ( ShowUnit u + , ConvertibleUnit u a + , Arbitrary a, Show a, Epsilon a + ) + => Spec +toFromSpec = + it ("toFrom " ++ showUnit @u) (toFromProp @u @a) + + +------------------------------------ fromTo ------------------------------------ + +fromToRoundtrip :: forall u v a. + ( IsUnit u, IsUnit v + , FromTo u v a, FromTo v u a + ) + => a -> a +fromToRoundtrip a = coerce (fromTo (fromTo (coerce a :: u a) :: v a) :: u a) + +fromToProp :: forall u v a. + ( ShowUnit u, ShowUnit v + , FromTo u v a, FromTo v u a + , Arbitrary a, Show a, Epsilon a + ) + => Property +fromToProp = property $ isApproxId (fromToRoundtrip @u @v @a) + +fromToSpec :: forall u v a. + ( ShowUnit u, ShowUnit v + , FromTo u v a, FromTo v u a + , Arbitrary a, Show a, Epsilon a + ) + => Spec +fromToSpec = + it ("From `" ++ showUnit @u ++ "` to `" ++ showUnit @v ++ "`") + $ fromToProp @u @v @a + +fromToAssert :: forall a u v. + ( Show (u a), Show (v a) + , FromTo u v a + , FromTo v u a + , Arbitrary a, ShowUnit u, ShowUnit v, Show a, Epsilon a + ) + => u a -> v a -> Spec +fromToAssert u v = + it ("`" ++ prettyQuantity u ++ "` should be `" ++ prettyQuantity v ++ "`") $ + aboutEqual (coerce (fromTo u :: v a) :: a) (coerce v :: a) + && aboutEqual (coerce (fromTo v :: u a) :: a) (coerce u :: a) + `shouldBe` True + + +----------------------------------- fromTo' ------------------------------------ + +fromToRoundtrip' :: forall u v a. + ( FromTo' u v a, FromTo' v u a) + => a -> a +fromToRoundtrip' a = coerce (fromTo' (fromTo' (coerce a :: u a) :: v a) :: u a) + +fromToProp' :: forall u v a. + ( ShowUnit u, ShowUnit v + , FromTo' u v a, FromTo' v u a + , Arbitrary a, Show a, Epsilon a + ) + => Property +fromToProp' = property $ isApproxId (fromToRoundtrip' @u @v @a) + + +fromToSpec' :: forall u v a. + ( ShowUnit u, ShowUnit v + , FromTo' u v a, FromTo' v u a + , Arbitrary a, Show a, Epsilon a + ) + => Spec +fromToSpec' = + it ("From' `" ++ showUnit @u ++ "` to' `" ++ showUnit @v ++ "`") + $ fromToProp' @u @v @a
+ test/Data/Units/BaseProp.hs view
@@ -0,0 +1,45 @@+{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE TypeApplications #-} + +module Data.Units.BaseProp + ( module Data.Units.Base.ArithmeticProp + , module Data.Units.Base.ConvertProp + , module Data.Units.BaseProp + ) + where + +import Data.Units.Base.ArithmeticProp +import Data.Units.Base.ConvertProp + +import Test.QuickCheck +import Test.Hspec + +import Data.Epsilon + +import Data.Units + +sameDimSpec :: forall u a. + (IsUnit u, Floating (u a), ShowUnit u + , Show a, Epsilon a, Floating a, Eq a, Arbitrary a + , FromTo' u u a + , DimEq u u + , IsUnit (NormalizeUnitL (u .^+ 2)) + , IsUnit (BaseUnitOf u) + ) + => Spec +sameDimSpec = describe ("Unit " ++ showUnit @u) $ do + floatingSpec @u @a + addLeftSpec @u @u @a + addRightSpec @u @u @a + addSameSpec @u @u @a + subLeftSpec @u @u @a + subRightSpec @u @u @a + subSameSpec @u @u @a + mulSameSpec @u @u @a + -- mulLeftSpec @u @u @a + -- mulRightSpec @u @u @a + divSameSpec @u @u @a + expSpec @u @a + expConvSpec @u @a +
+ test/Data/Units/NonStd/AngleSpec.hs view
@@ -0,0 +1,30 @@+module Data.Units.NonStd.AngleSpec where + +import Test.Hspec + +import qualified Data.Units.SI.Derived.Angle as SI +import qualified Data.Units.SI.NonStd.Angle as SI +import qualified Data.Units.AngleSI.System as A +import qualified Data.Units.AngleSI.NonStd.Angle as A + +import Data.Units.Base.ConvertProp + +spec :: Spec +spec = do + describe "Angle" $ do + toFromSpec @SI.Radian @Double + toFromSpec @SI.Degree @Double + toFromSpec @SI.Turn @Double + toFromSpec @SI.Gradian @Double + toFromSpec @A.Radian @Double + toFromSpec @A.Degree @Double + toFromSpec @A.Turn @Double + toFromSpec @A.Gradian @Double + fromToAssert @Double (SI.Radian pi) (SI.Degree 180) + fromToAssert @Double (SI.Degree 90) (SI.Gradian 100) + fromToAssert @Double (SI.Turn (1 / 4)) (SI.Degree 90) + fromToAssert @Double (SI.Radian (pi/4)) (SI.Turn (1 / 8)) + fromToAssert @Double (A.Radian pi) (A.Degree 180) + fromToAssert @Double (A.Degree 90) (A.Gradian 100) + fromToAssert @Double (A.Turn (1 / 4)) (A.Degree 90) + fromToAssert @Double (A.Radian (pi/4)) (A.Turn (1 / 8))
+ test/Data/Units/NonStd/FrequencySpec.hs view
@@ -0,0 +1,17 @@+module Data.Units.NonStd.FrequencySpec where + +import Test.Hspec + +import Data.Units.SI +import Data.Units.NonStd.Frequency + +import Data.Units.Base.ConvertProp + +spec :: Spec +spec = do + describe "Frequency" $ do + fromToAssert @Double (69 :: MidiPitch Double) (Hertz 440) + it "decomposePitchCents, detune < 1" $ do + + decomposePitchCents (123.5 :: MidiPitch Double) `shouldBe` (123, 0.5) +
+ test/Data/Units/NonStd/TemperatureSpec.hs view
@@ -0,0 +1,18 @@+module Data.Units.NonStd.TemperatureSpec where + +import Test.Hspec + +import Data.Units.NonStd.Temperature +import Data.Units.SI + +import Data.Units.Base.ConvertProp + +spec :: Spec +spec = do + describe "Temperature" $ do + toFromSpec @Celsius @Double + toFromSpec @Fahrenheit @Double + toFromSpec @Kelvin @Double + fromToAssert @Double (Celsius 0) (Kelvin 273.15) + fromToAssert @Double (Fahrenheit 32) (Celsius 0) + fromToAssert @Double (Fahrenheit 212) (Kelvin 373.15)
+ test/Data/Units/NonStd/TimeSpec.hs view
@@ -0,0 +1,13 @@+module Data.Units.NonStd.TimeSpec where + +import Test.Hspec + +import Data.Units.NonStd.Time +import Data.Units.Base.ConvertProp + +spec :: Spec +spec = do + describe "Angle" $ do + toFromSpec @Minute @Double + toFromSpec @Hour @Double + fromToAssert @Double (Hour 1) (Minute 60)
+ test/Data/Units/SI/PrefixSpec.hs view
@@ -0,0 +1,85 @@+module Data.Units.SI.PrefixSpec (spec) where + +import Test.Hspec + +import Data.Units + +import Data.Units.BaseProp + +spec :: Spec +spec = do + describe "Prefix" $ do + describe "Quecto" $ do + toFromSpec @(Quecto Meter) @Double + toFromSpec @(Quecto Meter .^- 1) @Double + describe "Ronto" $ do + toFromSpec @(Ronto Meter) @Double + toFromSpec @(Ronto Meter .^- 1) @Double + describe "Yocto" $ do + toFromSpec @(Yocto Meter) @Double + toFromSpec @(Yocto Meter .^- 1) @Double + describe "Zepto" $ do + toFromSpec @(Zepto Meter) @Double + toFromSpec @(Zepto Meter .^- 1) @Double + describe "Atto" $ do + toFromSpec @(Atto Meter) @Double + toFromSpec @(Atto Meter .^- 1) @Double + describe "Femto" $ do + toFromSpec @(Femto Meter) @Double + toFromSpec @(Femto Meter .^- 1) @Double + describe "Pico" $ do + toFromSpec @(Pico Meter) @Double + toFromSpec @(Pico Meter .^- 1) @Double + describe "Nano" $ do + toFromSpec @(Nano Meter) @Double + toFromSpec @(Nano Meter .^- 1) @Double + describe "Micro" $ do + toFromSpec @(Micro Meter) @Double + toFromSpec @(Micro Meter .^- 1) @Double + describe "Milli" $ do + toFromSpec @(Milli Meter) @Double + toFromSpec @(Milli Meter .^- 1) @Double + describe "Centi" $ do + toFromSpec @(Centi Meter) @Double + toFromSpec @(Centi Meter .^- 1) @Double + describe "Deci" $ do + toFromSpec @(Deci Meter) @Double + toFromSpec @(Deci Meter .^- 1) @Double + describe "Deca" $ do + toFromSpec @(Deca Meter) @Double + toFromSpec @(Deca Meter .^- 1) @Double + describe "Hecto" $ do + toFromSpec @(Hecto Meter) @Double + toFromSpec @(Hecto Meter .^- 1) @Double + describe "Kilo" $ do + fromToSpec @(Kilo Meter) @(Milli Meter) @Double + fromToSpec' @(Kilo Meter) @(Milli Meter) @Double + toFromSpec @(Kilo Meter) @Double + toFromSpec @(Kilo Meter .^- 1) @Double + describe "Mega" $ do + toFromSpec @(Mega Meter) @Double + toFromSpec @(Mega Meter .^- 1) @Double + describe "Giga" $ do + toFromSpec @(Giga Meter) @Double + toFromSpec @(Giga Meter .^- 1) @Double + describe "Tera" $ do + toFromSpec @(Tera Meter) @Double + toFromSpec @(Tera Meter .^- 1) @Double + describe "Peta" $ do + toFromSpec @(Peta Meter) @Double + toFromSpec @(Peta Meter .^- 1) @Double + describe "Exa" $ do + toFromSpec @(Exa Meter) @Double + toFromSpec @(Exa Meter .^- 1) @Double + describe "Zetta" $ do + toFromSpec @(Zetta Meter) @Double + toFromSpec @(Zetta Meter .^- 1) @Double + describe "Yotta" $ do + toFromSpec @(Yotta Meter) @Double + toFromSpec @(Yotta Meter .^- 1) @Double + describe "Ronna" $ do + toFromSpec @(Ronna Meter) @Double + toFromSpec @(Ronna Meter .^- 1) @Double + describe "Quecca" $ do + toFromSpec @(Quecca Meter) @Double + toFromSpec @(Quecca Meter .^- 1) @Double
+ test/Data/Units/SI/SystemSpec.hs view
@@ -0,0 +1,21 @@+module Data.Units.SI.SystemSpec (spec) where + +import Test.Hspec + +import Data.Units + +import Data.Units.BaseProp + +spec :: Spec +spec = do + describe "Units" $ do + sameDimSpec @Meter @Double + toFromSpec @Gram @Double + toFromSpec @Meter @Double + toFromSpec @Second @Double + toFromSpec @Ampere @Double + toFromSpec @Kelvin @Double + toFromSpec @Mole @Double + toFromSpec @Candela @Double + +
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}