/
Quantity.purs
291 lines (240 loc) · 8.91 KB
/
Quantity.purs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
-- | This module defines data types and functions to handle physical
-- | quantities.
module Data.Quantity
( Quantity
, quantity
, (.*)
, quantity'
, prettyPrint'
, prettyPrint
, showResult
, derivedUnit
, toStandard
, fullSimplify
, approximatelyEqual
-- Conversion errors
, UnificationError(..)
, errorMessage
-- Create a dimensionless quantity
, scalar
, scalar'
-- Convert quantities
, convert
, convertTo
, asValueIn
, asValueIn'
, toScalar
, toScalar'
-- Numerical properties
, isFinite
-- Calculate with quantities
, qNegate
, qAdd
, (⊕)
, qSubtract
, (⊖)
, qMultiply
, (⊗)
, qDivide
, (⊘)
, pow
, abs
, sqrt
) where
import Prelude
import Data.Either (Either(..))
import Data.Foldable (product, foldMap)
import Data.Tuple (Tuple(..), fst, snd)
import Data.Number.Approximate (Fraction(..), eqRelative)
import Data.Units (DerivedUnit, toString, (.^), (./), unity, removePrefix)
import Data.Units.SI.Derived (radian) as SI
import Data.Units.SI.Accepted (degree) as SI
import Data.Units as U
import Data.Decimal (Decimal, fromNumber, toNumber)
import Data.Decimal as D
-- | Representation of a physical quantity as a (product of a) numerical value
-- | and a physical unit.
data Quantity = Quantity Decimal DerivedUnit
-- | Helper operator, used internally for pattern matching.
infix 3 Quantity as .*.
-- Note that we define `quantity` below because we do not want to export the
-- `Quantity` constructor. This would leak the internal representation and the
-- bare numerical values.
-- | Construct a physical quantity from a numerical value and the physical
-- | unit.
quantity ∷ Number → DerivedUnit → Quantity
quantity n du = Quantity (fromNumber n) du
infix 5 quantity as .*
-- | Construct a physical quantity from a numerical value and the physical
-- | unit.
quantity' ∷ Decimal → DerivedUnit → Quantity
quantity' n du = Quantity n du
infix 5 quantity' as ..*
instance eqQuantity ∷ Eq Quantity where
eq q1 q2 = value q1' == value q2' && derivedUnit q1' == derivedUnit q2'
where
q1' = toStandard q1
q2' = toStandard q2
instance showQuantity ∷ Show Quantity where
show (Quantity num unit) = show num <> " .* " <> show unit
prettyDecimal ∷ Decimal → String
prettyDecimal d =
if D.isInteger d && d < (D.fromNumber 1.0e18)
then D.toString d
else D.toString (D.toSignificantDigits 6 d)
-- | Show a physical quantity in a human-readable form, value and unit
-- | separately.
prettyPrint' ∷ Quantity → Tuple String String
prettyPrint' (val .*. du)
| du == unity = Tuple (prettyDecimal val) ""
| otherwise = Tuple (prettyDecimal val) (toString du)
-- | Show a physical quantity in a human-readable form.
prettyPrint ∷ Quantity → String
prettyPrint q =
case prettyPrint' q of
Tuple v u → v <> u
-- | Show the (possibly failed) result of a computation in human-readable form.
showResult ∷ Either UnificationError Quantity → String
showResult (Left error) = errorMessage error
showResult (Right q) = prettyPrint q
-- | The numerical value stored inside a `Quantity`. For internal use only
-- | (bare `Decimal`s without units should be handled with care).
value ∷ Quantity → Decimal
value (v .*. _) = v
-- | The unit of a physical quantity.
derivedUnit ∷ Quantity → DerivedUnit
derivedUnit (_ .*. u) = u
-- | Convert a quantity to its standard representation.
toStandard ∷ Quantity → Quantity
toStandard (num .*. du) =
case U.toStandardUnit du of
Tuple du' conversion → (conversion * num) ..* du'
-- | Attempt to simplify the unit of a quantity.
fullSimplify ∷ Quantity → Quantity
fullSimplify q@(num .*. du) =
case toScalar' q of
Right n →
if removePrefix du /= SI.degree && removePrefix du /= SI.radian
then n .*. unity
else num ..* du
Left _ →
let list = U.splitByDimension du
toTuple (Tuple target us) =
case convertTo (one .* us) target of
Right (f .*. target') → Tuple f target'
Left _ → Tuple one target
list' = toTuple <$> list
factor = product (fst <$> list')
du' = foldMap snd list'
in (num * factor) .*. du'
-- | Check whether two quantities have matching units (or can be converted
-- | to the same representation) and test if the numerical values are
-- | approximately equal.
approximatelyEqual ∷ Number → Quantity → Quantity → Boolean
approximatelyEqual tol q1' q2' =
derivedUnit q1 == derivedUnit q2 &&
eqRelative (Fraction tol) v1 v2
where
q1 = toStandard q1'
q2 = toStandard q2'
v1 = toNumber $ value q1
v2 = toNumber $ value q2
-- | A unit conversion error that appears if two given units cannot be
-- | converted into each other.
data UnificationError = UnificationError DerivedUnit DerivedUnit
derive instance eqUnificationError ∷ Eq UnificationError
instance showUnificationError ∷ Show UnificationError where
show (UnificationError u1 u2) = "UnificationError (" <> show u1 <> ")"
<> " (" <> show u2 <> ")"
-- | Textual representation of a unit conversion error.
errorMessage ∷ UnificationError → String
errorMessage (UnificationError u1 u2) =
if u1 == unity
then "Cannot convert quantity of unit '" <> toString u2 <> "' to a scalar"
else
if u2 == unity
then "Cannot convert quantity of unit '" <> toString u1 <> "' to a scalar"
else
"Cannot unify unit '" <> toString u1 <> "'" <> baseRep u1 <> "\n" <>
" with unit '" <> toString u2 <> "'" <> baseRep u2 <> ""
where
baseRep u =
let u' = fst (U.toStandardUnit u)
in
if u' == unity
then ""
else " (SI: '" <> toString u' <> "')"
-- | Create a scalar (i.e. dimensionless) quantity from a number.
scalar ∷ Number → Quantity
scalar factor = factor .* U.unity
-- | Create a scalar (i.e. dimensionless) quantity from a number.
scalar' ∷ Decimal → Quantity
scalar' factor = factor ..* U.unity
-- | Attempt to convert a physical quantity to a given target unit. Returns a
-- | `UnificationError` if the conversion fails.
convert ∷ DerivedUnit → Quantity → Either UnificationError Quantity
convert to q@(val .*. from)
| to == from = Right q
| otherwise =
case U.toStandardUnit to of
Tuple to' factor →
let q' = toStandard q
from' = derivedUnit q'
in
if from' == to'
then Right $ case q' of
(val' .*. _) → (val' / factor) .*. to
else Left $ UnificationError from to
-- | Flipped version of `convert`.
convertTo ∷ Quantity → DerivedUnit → Either UnificationError Quantity
convertTo = flip convert
-- | Get the numerical value of a physical quantity in a given unit. Returns a
-- | `UnificationError` if the conversion fails.
asValueIn' ∷ Quantity → DerivedUnit → Either UnificationError Decimal
asValueIn' u = convertTo u >=> value >>> pure
-- | Get the numerical value of a physical quantity in a given unit. Returns a
-- | `UnificationError` if the conversion fails.
asValueIn ∷ Quantity → DerivedUnit → Either UnificationError Number
asValueIn q u = toNumber <$> (asValueIn' q u)
-- | Try to convert a quantity to a scalar value
toScalar' ∷ Quantity → Either UnificationError Decimal
toScalar' q = q `asValueIn'` unity
-- | Try to convert a quantity to a scalar value
toScalar ∷ Quantity → Either UnificationError Number
toScalar q = q `asValueIn` unity
-- | Check if the numerical value of a quantity is finite.
isFinite ∷ Quantity → Boolean
isFinite (n .*. _) = D.isFinite n
-- | Negate the numerical value of a quantity.
qNegate ∷ Quantity → Quantity
qNegate (v .*. u) = (-v) .*. u
-- | Attempt to add two quantities. If the units can not be unified, an error
-- | is returned.
qAdd ∷ Quantity → Quantity → Either UnificationError Quantity
qAdd (v1 .*. u1) q2 = do
q2' ← q2 `convertTo` u1
case q2' of
(v2 .*. _) → pure $ (v1 + v2) ..* u1
infixl 3 qAdd as ⊕
-- | Attempt to subtract two quantities. If the units can not be unified, an
-- | error is returned.
qSubtract ∷ Quantity → Quantity → Either UnificationError Quantity
qSubtract q1 (v2 .*. u2) = q1 ⊕ ((-v2) .*. u2)
infixl 3 qSubtract as ⊖
-- | Multiply two quantities.
qMultiply ∷ Quantity → Quantity → Quantity
qMultiply (v1 .*. u1) (v2 .*. u2) = (v1 * v2) ..* (u1 <> u2)
infixl 4 qMultiply as ⊗
-- | Divide two quantities.
qDivide ∷ Quantity → Quantity → Quantity
qDivide (v1 .*. u1) (v2 .*. u2) = (v1 / v2) ..* (u1 ./ u2)
infixl 4 qDivide as ⊘
-- | Raise a quantity to a given power.
pow ∷ Quantity → Decimal → Quantity
pow (val .*. u) exp = (val `D.pow` exp) ..* (u .^ toNumber exp)
-- | The absolute value of a quantity.
abs ∷ Quantity → Quantity
abs (val .*. u) = D.abs val ..* u
-- | The square root of a quantity.
sqrt ∷ Quantity → Quantity
sqrt q = q `pow` (fromNumber 0.5)