1 /++
2 This module defines quantities that are statically checked for dimensional
3 consistency at compile-time.
4 
5 The dimensions are part of their types, so that the compilation fails if an
6 operation or a function call is not dimensionally consistent.
7 
8 Copyright: Copyright 2013-2018, Nicolas Sicard
9 Authors: Nicolas Sicard
10 License: $(LINK www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
11 Source: $(LINK https://github.com/biozic/quantities)
12 +/
13 module quantities.compiletime;
14 
15 ///
16 unittest
17 {
18     import quantities.compiletime;
19     import quantities.si;
20     import std.format : format;
21     import std.math : approxEqual;
22 
23     // Introductory example
24     {
25         // Use the predefined quantity types (in module quantities.si)
26         Volume volume;
27         Concentration concentration;
28         Mass mass;
29 
30         // Define a new quantity type
31         alias MolarMass = typeof(kilogram / mole);
32 
33         // I have to make a new solution at the concentration of 5 mmol/L
34         concentration = 5.0 * milli(mole) / liter;
35 
36         // The final volume is 100 ml.
37         volume = 100.0 * milli(liter);
38 
39         // The molar mass of my compound is 118.9 g/mol
40         MolarMass mm = 118.9 * gram / mole;
41 
42         // What mass should I weigh?
43         mass = concentration * volume * mm;
44         assert(format("%s", mass) == "5.945e-05 [M]");
45         // Wait! That's not really useful!
46         assert(siFormat!"%.1f mg"(mass) == "59.5 mg");
47     }
48 
49     // Working with predefined units
50     {
51         auto distance = 384_400 * kilo(meter); // From Earth to Moon
52         auto speed = 299_792_458 * meter / second; // Speed of light
53         auto time = distance / speed;
54         assert(time.siFormat!"%.3f s" == "1.282 s");
55     }
56 
57     // Dimensional correctness is check at compile-time
58     {
59         Mass mass;
60         assert(!__traits(compiles, mass = 15 * meter));
61         assert(!__traits(compiles, mass = 1.2));
62     }
63 
64     // Calculations can be done at compile-time
65     {
66         enum distance = 384_400 * kilo(meter); // From Earth to Moon
67         enum speed = 299_792_458 * meter / second; // Speed of light
68         enum time = distance / speed;
69         /* static */
70         assert(time.siFormat!"%.3f s" == "1.282 s");
71         // NB. Phobos can't format floating point values at run-time.
72     }
73 
74     // Create a new unit from the predefined ones
75     {
76         auto inch = 2.54 * centi(meter);
77         auto mile = 1609 * meter;
78         assert(mile.value(inch).approxEqual(63_346)); // inches in a mile
79         // NB. Cannot use siFormatter, because inches are not SI units
80     }
81 
82     // Create a new unit with new dimensions
83     {
84         // Create a new base unit of currency
85         auto euro = unit!(double, "C"); // C is the chosen dimension symol (for currency...)
86 
87         auto dollar = euro / 1.35;
88         auto price = 2000 * dollar;
89         assert(price.value(euro).approxEqual(1481)); // Price in euros
90     }
91 
92     // Compile-time parsing
93     {
94         enum distance = si!"384_400 km";
95         enum speed = si!"299_792_458 m/s";
96         assert(is(typeof(distance) == Length));
97         assert(is(typeof(speed) == Speed));
98     }
99 
100     // Run-time parsing of statically typed Quantities
101     {
102         auto data = ["distance-to-the-moon" : "384_400 km", "speed-of-light" : "299_792_458 m/s"];
103         auto distance = parseSI!Length(data["distance-to-the-moon"]);
104         auto speed = parseSI!Speed(data["speed-of-light"]);
105     }
106 }
107 
108 import quantities.internal.dimensions;
109 import quantities.common;
110 import quantities.runtime;
111 import std.format;
112 import std.math;
113 import std.traits : isNumeric, isIntegral;
114 
115 /++
116 A quantity checked at compile-time for dimensional consistency.
117 
118 Params:
119     N = the numeric type of the quantity.
120 
121 See_Also:
122     QVariant has the same public members and overloaded operators as Quantity.
123 +/
124 struct Quantity(N, alias dims)
125 {
126     static assert(isNumeric!N);
127     static assert(is(typeof(dims) : Dimensions));
128     static assert(Quantity.sizeof == N.sizeof);
129 
130 private:
131     N _value;
132 
133     // Creates a new quantity with non-empty dimensions
134     static Quantity make(T)(T scalar)
135             if (isNumeric!T)
136     {
137         Quantity result;
138         result._value = scalar;
139         return result;
140     }
141 
142     void ensureSameDim(const Dimensions d)() const
143     {
144         static assert(dimensions == d,
145                 "Dimension error: %s is not consistent with %s".format(dimensions, d));
146     }
147 
148     void ensureEmpty(const Dimensions d)() const
149     {
150         static assert(d.empty, "Dimension error: %s instead of no dimensions".format(d));
151     }
152 
153 package(quantities):
154     alias valueType = N;
155 
156     N rawValue() const
157     {
158         return _value;
159     }
160 
161 public:
162     /++
163     Creates a new quantity from another one with the same dimensions.
164 
165     If Q is a QVariant, throws a DimensionException if the parsed quantity
166     doesn't have the same dimensions as Q. If Q is a Quantity, inconsistent
167     dimensions produce a compilation error.
168     +/
169     this(Q)(auto ref const Q qty)
170             if (isQuantity!Q)
171     {
172         ensureSameDim!(Q.dimensions);
173         _value = qty._value;
174     }
175 
176     /// ditto
177     this(Q)(auto ref const Q qty)
178             if (isQVariant!Q)
179     {
180         import std.exception;
181 
182         enforce(dimensions == qty.dimensions,
183                 new DimensionException("Incompatible dimensions", dimensions, qty.dimensions));
184         _value = qty.rawValue;
185     }
186 
187     /// Creates a new dimensionless quantity from a number
188     this(T)(T scalar)
189             if (isNumeric!T && isDimensionless)
190     {
191         _value = scalar;
192     }
193 
194     /// The dimensions of the quantity
195     enum dimensions = dims;
196 
197     /++
198     Implicitly convert a dimensionless value to the value type.
199     +/
200     static if (isDimensionless)
201     {
202         N get() const
203         {
204             return _value;
205         }
206 
207         alias get this;
208     }
209 
210     /++
211     Gets the _value of this quantity when expressed in the given target unit.
212 
213     If Q is a QVariant, throws a DimensionException if the parsed quantity
214     doesn't have the same dimensions as Q. If Q is a Quantity, inconsistent
215     dimensions produce a compilation error.
216     +/
217     N value(Q)(auto ref const Q target) const 
218             if (isQuantity!Q)
219     {
220         mixin ensureSameDim!(Q.dimensions);
221         return _value / target._value;
222     }
223 
224     /// ditto
225     N value(Q)(auto ref const Q target) const 
226             if (isQVariant!Q)
227     {
228         import std.exception;
229 
230         enforce(dimensions == target.dimensions,
231                 new DimensionException("Incompatible dimensions", dimensions, target.dimensions));
232         return _value / target.rawValue;
233     }
234 
235     /++
236     Test whether this quantity is dimensionless
237     +/
238     enum bool isDimensionless = dimensions.length == 0;
239 
240     /++
241     Tests wheter this quantity has the same dimensions as another one.
242     +/
243     bool isConsistentWith(Q)(auto ref const Q qty) const 
244             if (isQVariantOrQuantity!Q)
245     {
246         return dimensions == qty.dimensions;
247     }
248 
249     /++
250     Returns the base unit of this quantity.
251     +/
252     Quantity baseUnit() @property const
253     {
254         return Quantity.make(1);
255     }
256 
257     /++
258     Cast a dimensionless quantity to a numeric type.
259 
260     The cast operation will throw DimensionException if the quantity is not
261     dimensionless.
262     +/
263     static if (isDimensionless)
264     {
265         T opCast(T)() const 
266                 if (isNumeric!T)
267         {
268             return _value;
269         }
270     }
271 
272     // Assign from another quantity
273     /// Operator overloading
274     ref Quantity opAssign(Q)(auto ref const Q qty)
275             if (isQuantity!Q)
276     {
277         ensureSameDim!(Q.dimensions);
278         _value = qty._value;
279         return this;
280     }
281 
282     /// ditto
283     ref Quantity opAssign(Q)(auto ref const Q qty)
284             if (isQVariant!Q)
285     {
286         import std.exception;
287 
288         enforce(dimensions == qty.dimensions,
289                 new DimensionException("Incompatible dimensions", dimensions, qty.dimensions));
290         _value = qty.rawValue;
291         return this;
292     }
293 
294     // Assign from a numeric value if this quantity is dimensionless
295     /// ditto
296     ref Quantity opAssign(T)(T scalar)
297             if (isNumeric!T)
298     {
299         ensureEmpty!dimensions;
300         _value = scalar;
301         return this;
302     }
303 
304     // Unary + and -
305     /// ditto
306     Quantity opUnary(string op)() const 
307             if (op == "+" || op == "-")
308     {
309         return Quantity.make(mixin(op ~ "_value"));
310     }
311 
312     // Unary ++ and --
313     /// ditto
314     Quantity opUnary(string op)()
315             if (op == "++" || op == "--")
316     {
317         mixin(op ~ "_value;");
318         return this;
319     }
320 
321     // Add (or substract) two quantities if they share the same dimensions
322     /// ditto
323     Quantity opBinary(string op, Q)(auto ref const Q qty) const 
324             if (isQuantity!Q && (op == "+" || op == "-"))
325     {
326         ensureSameDim!(Q.dimensions);
327         return Quantity.make(mixin("_value" ~ op ~ "qty._value"));
328     }
329 
330     // Add (or substract) a dimensionless quantity and a number
331     /// ditto
332     Quantity opBinary(string op, T)(T scalar) const 
333             if (isNumeric!T && (op == "+" || op == "-"))
334     {
335         ensureEmpty!dimensions;
336         return Quantity.make(mixin("_value" ~ op ~ "scalar"));
337     }
338 
339     /// ditto
340     Quantity opBinaryRight(string op, T)(T scalar) const 
341             if (isNumeric!T && (op == "+" || op == "-"))
342     {
343         ensureEmpty!dimensions;
344         return Quantity.make(mixin("scalar" ~ op ~ "_value"));
345     }
346 
347     // Multiply or divide a quantity by a number
348     /// ditto
349     Quantity opBinary(string op, T)(T scalar) const 
350             if (isNumeric!T && (op == "*" || op == "/" || op == "%"))
351     {
352         return Quantity.make(mixin("_value" ~ op ~ "scalar"));
353     }
354 
355     /// ditto
356     Quantity opBinaryRight(string op, T)(T scalar) const 
357             if (isNumeric!T && op == "*")
358     {
359         return Quantity.make(mixin("scalar" ~ op ~ "_value"));
360     }
361 
362     /// ditto
363     auto opBinaryRight(string op, T)(T scalar) const 
364             if (isNumeric!T && (op == "/" || op == "%"))
365     {
366         alias RQ = Quantity!(N, dimensions.inverted());
367         return RQ.make(mixin("scalar" ~ op ~ "_value"));
368     }
369 
370     // Multiply or divide two quantities
371     /// ditto
372     auto opBinary(string op, Q)(auto ref const Q qty) const 
373             if (isQuantity!Q && (op == "*" || op == "/"))
374     {
375         alias RQ = Quantity!(N, mixin("dimensions" ~ op ~ "Q.dimensions"));
376         return RQ.make(mixin("_value" ~ op ~ "qty._value"));
377     }
378 
379     /// ditto
380     Quantity opBinary(string op, Q)(auto ref const Q qty) const 
381             if (isQuantity!Q && (op == "%"))
382     {
383         ensureSameDim!(Q.dimensions);
384         return Quantity.make(mixin("_value" ~ op ~ "qty._value"));
385     }
386 
387     // Add/sub assign with a quantity that shares the same dimensions
388     /// ditto
389     void opOpAssign(string op, Q)(auto ref const Q qty)
390             if (isQuantity!Q && (op == "+" || op == "-"))
391     {
392         ensureSameDim!(Q.dimensions);
393         mixin("_value " ~ op ~ "= qty._value;");
394     }
395 
396     // Add/sub assign a number to a dimensionless quantity
397     /// ditto
398     void opOpAssign(string op, T)(T scalar)
399             if (isNumeric!T && (op == "+" || op == "-"))
400     {
401         ensureEmpty!dimensions;
402         mixin("_value " ~ op ~ "= scalar;");
403     }
404 
405     // Mul/div assign another dimensionless quantity to a dimensionsless quantity
406     /// ditto
407     void opOpAssign(string op, Q)(auto ref const Q qty)
408             if (isQuantity!Q && (op == "*" || op == "/" || op == "%"))
409     {
410         ensureEmpty!dimensions;
411         mixin("_value" ~ op ~ "= qty._value;");
412     }
413 
414     // Mul/div assign a number to a quantity
415     /// ditto
416     void opOpAssign(string op, T)(T scalar)
417             if (isNumeric!T && (op == "*" || op == "/"))
418     {
419         mixin("_value" ~ op ~ "= scalar;");
420     }
421 
422     /// ditto
423     void opOpAssign(string op, T)(T scalar)
424             if (isNumeric!T && op == "%")
425     {
426         ensureEmpty!dimensions;
427         mixin("_value" ~ op ~ "= scalar;");
428     }
429 
430     // Exact equality between quantities
431     /// ditto
432     bool opEquals(Q)(auto ref const Q qty) const 
433             if (isQuantity!Q)
434     {
435         ensureSameDim!(Q.dimensions);
436         return _value == qty._value;
437     }
438 
439     // Exact equality between a dimensionless quantity and a number
440     /// ditto
441     bool opEquals(T)(T scalar) const 
442             if (isNumeric!T)
443     {
444         ensureEmpty!dimensions;
445         return _value == scalar;
446     }
447 
448     // Comparison between two quantities
449     /// ditto
450     int opCmp(Q)(auto ref const Q qty) const 
451             if (isQuantity!Q)
452     {
453         ensureSameDim!(Q.dimensions);
454         if (_value == qty._value)
455             return 0;
456         if (_value < qty._value)
457             return -1;
458         return 1;
459     }
460 
461     // Comparison between a dimensionless quantity and a number
462     /// ditto
463     int opCmp(T)(T scalar) const 
464             if (isNumeric!T)
465     {
466         ensureEmpty!dimensions;
467         if (_value < scalar)
468             return -1;
469         if (_value > scalar)
470             return 1;
471         return 0;
472     }
473 
474     void toString(scope void delegate(const(char)[]) sink, FormatSpec!char fmt) const
475     {
476         sink.formatValue(_value, fmt);
477         sink(" ");
478         sink.formattedWrite!"%s"(dimensions);
479     }
480 }
481 
482 /++
483 Creates a new monodimensional unit as a Quantity.
484 
485 Params:
486     N = The numeric type of the value part of the quantity.
487 
488     dimSymbol = The symbol of the dimension of this quantity.
489 
490     rank = The rank of the dimensions of this quantity in the dimension vector,
491            when combining this quantity with other oned.
492 +/
493 auto unit(N, string dimSymbol, size_t rank = size_t.max)()
494 {
495     enum dims = Dimensions.mono(dimSymbol, rank);
496     return Quantity!(N, dims).make(1);
497 }
498 ///
499 unittest
500 {
501     enum meter = unit!(double, "L", 1);
502     enum kilogram = unit!(double, "M", 2);
503     // Dimensions will be in this order: L M
504 }
505 
506 /// Tests whether T is a quantity type.
507 template isQuantity(T)
508 {
509     import std.traits : Unqual;
510 
511     alias U = Unqual!T;
512     static if (is(U == Quantity!X, X...))
513         enum isQuantity = true;
514     else
515         enum isQuantity = false;
516 }
517 
518 /// Basic math functions that work with Quantity.
519 auto square(Q)(auto ref const Q quantity)
520         if (isQuantity!Q)
521 {
522     return Quantity!(Q.valueType, Q.dimensions.pow(2)).make(quantity._value ^^ 2);
523 }
524 
525 /// ditto
526 auto sqrt(Q)(auto ref const Q quantity)
527         if (isQuantity!Q)
528 {
529     return Quantity!(Q.valueType, Q.dimensions.powinverse(2)).make(std.math.sqrt(quantity._value));
530 }
531 
532 /// ditto
533 auto cubic(Q)(auto ref const Q quantity)
534         if (isQuantity!Q)
535 {
536     return Quantity!(Q.valueType, Q.dimensions.pow(3)).make(quantity._value ^^ 3);
537 }
538 
539 /// ditto
540 auto cbrt(Q)(auto ref const Q quantity)
541         if (isQuantity!Q)
542 {
543     return Quantity!(Q.valueType, Q.dimensions.powinverse(3)).make(std.math.cbrt(quantity._value));
544 }
545 
546 /// ditto
547 auto pow(int n, Q)(auto ref const Q quantity)
548         if (isQuantity!Q)
549 {
550     return Quantity!(Q.valueType, Q.dimensions.pow(n)).make(std.math.pow(quantity._value, n));
551 }
552 
553 /// ditto
554 auto nthRoot(int n, Q)(auto ref const Q quantity)
555         if (isQuantity!Q)
556 {
557     return Quantity!(Q.valueType, Q.dimensions.powinverse(n)).make(
558             std.math.pow(quantity._value, 1.0 / n));
559 }
560 
561 /// ditto
562 Q abs(Q)(auto ref const Q quantity)
563         if (isQuantity!Q)
564 {
565     return Q.make(std.math.fabs(quantity._value));
566 }