1 /++
2 This module defines dimensionally variant quantities, mainly for use at run-time.
3 
4 The dimensions are stored in a field, along with the numerical value of the
5 quantity. Operations and function calls fail if they are not dimensionally
6 consistent, by throwing a `DimensionException`.
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.runtime;
14 
15 ///
16 unittest
17 {
18     import quantities.runtime;
19     import quantities.si;
20     import std.format : format;
21     import std.math : approxEqual;
22 
23     // Note: the types of the predefined SI units (gram, mole, liter...)
24     // are Quantity instances, not QVariant instance.
25 
26     // Introductory example
27     {
28         // I have to make a new solution at the concentration of 5 mmol/L
29         QVariant!double concentration = 5.0 * milli(mole) / liter;
30 
31         // The final volume is 100 ml.
32         QVariant!double volume = 100.0 * milli(liter);
33 
34         // The molar mass of my compound is 118.9 g/mol
35         QVariant!double molarMass = 118.9 * gram / mole;
36 
37         // What mass should I weigh?
38         QVariant!double mass = concentration * volume * molarMass;
39         assert(format("%s", mass) == "5.945e-05 [M]");
40         // Wait! That's not really useful!
41         assert(siFormat!"%.1f mg"(mass) == "59.5 mg");
42     }
43 
44     // Working with predefined units
45     {
46         QVariant!double distance = 384_400 * kilo(meter); // From Earth to Moon
47         QVariant!double speed = 299_792_458 * meter / second; // Speed of light
48         QVariant!double time = distance / speed;
49         assert(time.siFormat!"%.3f s" == "1.282 s");
50     }
51 
52     // Dimensional correctness
53     {
54         import std.exception : assertThrown;
55 
56         QVariant!double mass = 4 * kilogram;
57         assertThrown!DimensionException(mass + meter);
58         assertThrown!DimensionException(mass == 1.2);
59     }
60 
61     // Create a new unit from the predefined ones
62     {
63         QVariant!double inch = 2.54 * centi(meter);
64         QVariant!double mile = 1609 * meter;
65         assert(mile.value(inch).approxEqual(63_346)); // inches in a mile
66         // NB. Cannot use siFormatter, because inches are not SI units
67     }
68 
69     // Create a new unit with new dimensions
70     {
71         // Create a new base unit of currency
72         QVariant!double euro = unit!double("C"); // C is the chosen dimension symol (for currency...)
73 
74         QVariant!double dollar = euro / 1.35;
75         QVariant!double price = 2000 * dollar;
76         assert(price.value(euro).approxEqual(1481)); // Price in euros
77     }
78 
79     // Run-time parsing
80     {
81         auto data = ["distance-to-the-moon" : "384_400 km", "speed-of-light" : "299_792_458 m/s"];
82         QVariant!double distance = parseSI(data["distance-to-the-moon"]);
83         QVariant!double speed = parseSI(data["speed-of-light"]);
84         QVariant!double time = distance / speed;
85     }
86 }
87 
88 import quantities.internal.dimensions;
89 import quantities.common;
90 import quantities.compiletime;
91 
92 import std.conv;
93 import std.exception;
94 import std.format;
95 import std.math;
96 import std.string;
97 import std.traits;
98 
99 /++
100 Exception thrown when operating on two units that are not interconvertible.
101 +/
102 class DimensionException : Exception
103 {
104     /// Holds the dimensions of the quantity currently operated on
105     Dimensions thisDim;
106     /// Holds the dimensions of the eventual other operand
107     Dimensions otherDim;
108 
109     mixin basicExceptionCtors;
110 
111     this(string msg, Dimensions thisDim, Dimensions otherDim,
112             string file = __FILE__, size_t line = __LINE__, Throwable next = null) @safe pure nothrow
113     {
114         super(msg, file, line, next);
115         this.thisDim = thisDim;
116         this.otherDim = otherDim;
117     }
118 }
119 ///
120 unittest
121 {
122     import std.exception : assertThrown;
123 
124     enum meter = unit!double("L");
125     enum second = unit!double("T");
126     assertThrown!DimensionException(meter + second);
127 }
128 
129 /++
130 A dimensionnaly variant quantity.
131 
132 Params:
133     N = the numeric type of the quantity.
134 
135 See_Also:
136     QVariant has the same public members and overloaded operators as Quantity.
137 +/
138 struct QVariant(N)
139 {
140     static assert(isNumeric!N, "Incompatible type: " ~ N.stringof);
141 
142 private:
143     N _value;
144     Dimensions _dimensions;
145 
146     void checkDim(Dimensions dim) @safe pure const
147     {
148         enforce(_dimensions == dim,
149                 new DimensionException("Incompatible dimensions", _dimensions, dim));
150     }
151 
152     void checkDimensionless() @safe pure const
153     {
154         enforce(_dimensions.empty, new DimensionException("Not dimensionless",
155                 _dimensions, Dimensions.init));
156     }
157 
158 package(quantities):
159     alias valueType = N;
160 
161     N rawValue() const
162     {
163         return _value;
164     }
165 
166 public:
167     // Creates a new quantity with non-empty dimensions
168     this(T)(T scalar, const Dimensions dim)
169             if (isNumeric!T)
170     {
171         _value = scalar;
172         _dimensions = dim;
173     }
174 
175     /// Creates a new quantity from another one with the same dimensions
176     this(Q)(auto ref const Q qty)
177             if (isQVariant!Q)
178     {
179         _value = qty._value;
180         _dimensions = qty._dimensions;
181     }
182 
183     /// ditto
184     this(Q)(auto ref const Q qty)
185             if (isQuantity!Q)
186     {
187         import quantities.compiletime : qVariant;
188 
189         this = qty.qVariant;
190     }
191 
192     /// Creates a new dimensionless quantity from a number
193     this(T)(T scalar)
194             if (isNumeric!T)
195     {
196         _dimensions = Dimensions.init;
197         _value = scalar;
198     }
199 
200     /// Returns the dimensions of the quantity
201     Dimensions dimensions() @property const
202     {
203         return _dimensions;
204     }
205 
206     /++
207     Implicitly convert a dimensionless value to the value type.
208 
209     Calling get will throw DimensionException if the quantity is not
210     dimensionless.
211     +/
212     N get() const
213     {
214         checkDimensionless;
215         return _value;
216     }
217 
218     alias get this;
219 
220     /++
221     Gets the _value of this quantity when expressed in the given target unit.
222     +/
223     N value(Q)(auto ref const Q target) const 
224             if (isQVariantOrQuantity!Q)
225     {
226         checkDim(target.dimensions);
227         return _value / target.rawValue;
228     }
229     ///
230     @safe pure unittest
231     {
232         auto minute = unit!int("T");
233         auto hour = 60 * minute;
234 
235         QVariant!int time = 120 * minute;
236         assert(time.value(hour) == 2);
237         assert(time.value(minute) == 120);
238     }
239 
240     /++
241     Test whether this quantity is dimensionless
242     +/
243     bool isDimensionless() @property const
244     {
245         return _dimensions.empty;
246     }
247 
248     /++
249     Tests wheter this quantity has the same dimensions as another one.
250     +/
251     bool isConsistentWith(Q)(auto ref const Q qty) const 
252             if (isQVariantOrQuantity!Q)
253     {
254         return _dimensions == qty.dimensions;
255     }
256     ///
257     @safe pure unittest
258     {
259         auto second = unit!double("T");
260         auto minute = 60 * second;
261         auto meter = unit!double("L");
262 
263         assert(minute.isConsistentWith(second));
264         assert(!meter.isConsistentWith(second));
265     }
266 
267     /++
268     Returns the base unit of this quantity.
269     +/
270     QVariant baseUnit() @property const
271     {
272         return QVariant(1, _dimensions);
273     }
274 
275     /++
276     Cast a dimensionless quantity to a numeric type.
277 
278     The cast operation will throw DimensionException if the quantity is not
279     dimensionless.
280     +/
281     T opCast(T)() const 
282             if (isNumeric!T)
283     {
284         checkDimensionless;
285         return _value;
286     }
287 
288     // Assign from another quantity
289     /// Operator overloading
290     ref QVariant opAssign(Q)(auto ref const Q qty)
291             if (isQVariantOrQuantity!Q)
292     {
293         _dimensions = qty.dimensions;
294         _value = qty.rawValue;
295         return this;
296     }
297 
298     // Assign from a numeric value if this quantity is dimensionless
299     /// ditto
300     ref QVariant opAssign(T)(T scalar)
301             if (isNumeric!T)
302     {
303         _dimensions = Dimensions.init;
304         _value = scalar;
305         return this;
306     }
307 
308     // Unary + and -
309     /// ditto
310     QVariant!N opUnary(string op)() const 
311             if (op == "+" || op == "-")
312     {
313         return QVariant(mixin(op ~ "_value"), _dimensions);
314     }
315 
316     // Unary ++ and --
317     /// ditto
318     QVariant!N opUnary(string op)()
319             if (op == "++" || op == "--")
320     {
321         mixin(op ~ "_value;");
322         return this;
323     }
324 
325     // Add (or substract) two quantities if they share the same dimensions
326     /// ditto
327     QVariant!N opBinary(string op, Q)(auto ref const Q qty) const 
328             if (isQVariantOrQuantity!Q && (op == "+" || op == "-"))
329     {
330         checkDim(qty.dimensions);
331         return QVariant(mixin("_value" ~ op ~ "qty.rawValue"), _dimensions);
332     }
333 
334     /// ditto
335     QVariant!N opBinaryRight(string op, Q)(auto ref const Q qty) const 
336             if (isQVariantOrQuantity!Q && (op == "+" || op == "-"))
337     {
338         checkDim(qty.dimensions);
339         return QVariant(mixin("qty.rawValue" ~ op ~ "_value"), _dimensions);
340     }
341 
342     // Add (or substract) a dimensionless quantity and a number
343     /// ditto
344     QVariant!N opBinary(string op, T)(T scalar) const 
345             if (isNumeric!T && (op == "+" || op == "-"))
346     {
347         checkDimensionless;
348         return QVariant(mixin("_value" ~ op ~ "scalar"), _dimensions);
349     }
350 
351     /// ditto
352     QVariant!N opBinaryRight(string op, T)(T scalar) const 
353             if (isNumeric!T && (op == "+" || op == "-"))
354     {
355         checkDimensionless;
356         return QVariant(mixin("scalar" ~ op ~ "_value"), _dimensions);
357     }
358 
359     // Multiply or divide a quantity by a number
360     /// ditto
361     QVariant!N opBinary(string op, T)(T scalar) const 
362             if (isNumeric!T && (op == "*" || op == "/" || op == "%"))
363     {
364         return QVariant(mixin("_value" ~ op ~ "scalar"), _dimensions);
365     }
366 
367     /// ditto
368     QVariant!N opBinaryRight(string op, T)(T scalar) const 
369             if (isNumeric!T && op == "*")
370     {
371         return QVariant(mixin("scalar" ~ op ~ "_value"), _dimensions);
372     }
373 
374     /// ditto
375     QVariant!N opBinaryRight(string op, T)(T scalar) const 
376             if (isNumeric!T && (op == "/" || op == "%"))
377     {
378         return QVariant(mixin("scalar" ~ op ~ "_value"), ~_dimensions);
379     }
380 
381     // Multiply or divide two quantities
382     /// ditto
383     QVariant!N opBinary(string op, Q)(auto ref const Q qty) const 
384             if (isQVariantOrQuantity!Q && (op == "*" || op == "/"))
385     {
386         return QVariant(mixin("_value" ~ op ~ "qty.rawValue"),
387                 mixin("_dimensions" ~ op ~ "qty.dimensions"));
388     }
389 
390     /// ditto
391     QVariant!N opBinaryRight(string op, Q)(auto ref const Q qty) const 
392             if (isQVariantOrQuantity!Q && (op == "*" || op == "/"))
393     {
394         return QVariant(mixin("qty.rawValue" ~ op ~ "_value"),
395                 mixin("qty.dimensions" ~ op ~ "_dimensions"));
396     }
397 
398     /// ditto
399     QVariant!N opBinary(string op, Q)(auto ref const Q qty) const 
400             if (isQVariantOrQuantity!Q && (op == "%"))
401     {
402         checkDim(qty.dimensions);
403         return QVariant(_value % qty.rawValue, _dimensions);
404     }
405 
406     /// ditto
407     QVariant!N opBinaryRight(string op, Q)(auto ref const Q qty) const 
408             if (isQVariantOrQuantity!Q && (op == "%"))
409     {
410         checkDim(qty.dimensions);
411         return QVariant(qty.rawValue % _value, _dimensions);
412     }
413 
414     /// ditto
415     QVariant!N opBinary(string op, T)(T power) const 
416             if (isIntegral!T && op == "^^")
417     {
418         return QVariant(_value ^^ power, _dimensions.pow(Rational(power)));
419     }
420 
421     /// ditto
422     QVariant!N opBinary(string op)(Rational power) const 
423             if (op == "^^")
424     {
425         static if (isIntegral!N)
426             auto newValue = std.math.pow(_value, cast(real) power).roundTo!N;
427         else static if (isFloatingPoint!N)
428             auto newValue = std.math.pow(_value, cast(real) power);
429         else
430             static assert(false, "Operation not defined for " ~ QVariant!N.stringof);
431         return QVariant(newValue, _dimensions.pow(power));
432     }
433 
434     // Add/sub assign with a quantity that shares the same dimensions
435     /// ditto
436     void opOpAssign(string op, Q)(auto ref const Q qty)
437             if (isQVariantOrQuantity!Q && (op == "+" || op == "-"))
438     {
439         checkDim(qty.dimensions);
440         mixin("_value " ~ op ~ "= qty.rawValue;");
441     }
442 
443     // Add/sub assign a number to a dimensionless quantity
444     /// ditto
445     void opOpAssign(string op, T)(T scalar)
446             if (isNumeric!T && (op == "+" || op == "-"))
447     {
448         checkDimensionless;
449         mixin("_value " ~ op ~ "= scalar;");
450     }
451 
452     // Mul/div assign another quantity to a quantity
453     /// ditto
454     void opOpAssign(string op, Q)(auto ref const Q qty)
455             if (isQVariantOrQuantity!Q && (op == "*" || op == "/" || op == "%"))
456     {
457         mixin("_value" ~ op ~ "= qty.rawValue;");
458         static if (op == "*")
459             _dimensions = _dimensions * qty.dimensions;
460         else
461             _dimensions = _dimensions / qty.dimensions;
462     }
463 
464     // Mul/div assign a number to a quantity
465     /// ditto
466     void opOpAssign(string op, T)(T scalar)
467             if (isNumeric!T && (op == "*" || op == "/"))
468     {
469         mixin("_value" ~ op ~ "= scalar;");
470     }
471 
472     /// ditto
473     void opOpAssign(string op, T)(T scalar)
474             if (isNumeric!T && op == "%")
475     {
476         checkDimensionless;
477         mixin("_value" ~ op ~ "= scalar;");
478     }
479 
480     // Exact equality between quantities
481     /// ditto
482     bool opEquals(Q)(auto ref const Q qty) const 
483             if (isQVariantOrQuantity!Q)
484     {
485         checkDim(qty.dimensions);
486         return _value == qty.rawValue;
487     }
488 
489     // Exact equality between a dimensionless quantity and a number
490     /// ditto
491     bool opEquals(T)(T scalar) const 
492             if (isNumeric!T)
493     {
494         checkDimensionless;
495         return _value == scalar;
496     }
497 
498     // Comparison between two quantities
499     /// ditto
500     int opCmp(Q)(auto ref const Q qty) const 
501             if (isQVariantOrQuantity!Q)
502     {
503         checkDim(qty.dimensions);
504         if (_value == qty.rawValue)
505             return 0;
506         if (_value < qty.rawValue)
507             return -1;
508         return 1;
509     }
510 
511     // Comparison between a dimensionless quantity and a number
512     /// ditto
513     int opCmp(T)(T scalar) const 
514             if (isNumeric!T)
515     {
516         checkDimensionless;
517         if (_value < scalar)
518             return -1;
519         if (_value > scalar)
520             return 1;
521         return 0;
522     }
523 
524     void toString(scope void delegate(const(char)[]) sink, FormatSpec!char fmt) const
525     {
526         sink.formatValue(_value, fmt);
527         sink(" ");
528         sink.formattedWrite!"%s"(_dimensions);
529     }
530 }
531 
532 /++
533 Creates a new monodimensional unit as a QVariant.
534 
535 Params:
536     N = The numeric type of the value part of the quantity.
537 
538     dimSymbol = The symbol of the dimension of this quantity.
539 
540     rank = The rank of the dimensions of this quantity in the dimension vector,
541            when combining this quantity with other oned.
542 +/
543 QVariant!N unit(N)(string dimSymbol, size_t rank = size_t.max)
544 {
545     return QVariant!N(N(1), Dimensions.mono(dimSymbol, rank));
546 }
547 ///
548 unittest
549 {
550     enum meter = unit!double("L", 1);
551     enum kilogram = unit!double("M", 2);
552     // Dimensions will be in this order: L M
553 }
554 
555 // Tests whether T is a quantity type
556 template isQVariant(T)
557 {
558     alias U = Unqual!T;
559     static if (is(U == QVariant!X, X...))
560         enum isQVariant = true;
561     else
562         enum isQVariant = false;
563 }
564 
565 enum isQVariantOrQuantity(T) = isQVariant!T || isQuantity!T;
566 
567 /// Turns a Quantity into a QVariant
568 auto qVariant(Q)(auto ref const Q qty)
569         if (isQuantity!Q)
570 {
571     return QVariant!(Q.valueType)(qty.rawValue, qty.dimensions);
572 }
573 
574 /// Turns a scalar into a dimensionless QVariant
575 auto qVariant(N)(N scalar)
576         if (isNumeric!N)
577 {
578     return QVariant!N(scalar, Dimensions.init);
579 }
580 
581 /// Basic math functions that work with QVariant.
582 auto square(Q)(auto ref const Q quantity)
583         if (isQVariant!Q)
584 {
585     return Q(quantity._value ^^ 2, quantity._dimensions.pow(2));
586 }
587 
588 /// ditto
589 auto sqrt(Q)(auto ref const Q quantity)
590         if (isQVariant!Q)
591 {
592     return Q(std.math.sqrt(quantity._value), quantity._dimensions.powinverse(2));
593 }
594 
595 /// ditto
596 auto cubic(Q)(auto ref const Q quantity)
597         if (isQVariant!Q)
598 {
599     return Q(quantity._value ^^ 3, quantity._dimensions.pow(3));
600 }
601 
602 /// ditto
603 auto cbrt(Q)(auto ref const Q quantity)
604         if (isQVariant!Q)
605 {
606     return Q(std.math.cbrt(quantity._value), quantity._dimensions.powinverse(3));
607 }
608 
609 /// ditto
610 auto pow(Q)(auto ref const Q quantity, Rational r)
611         if (isQVariant!Q)
612 {
613     return quantity ^^ r;
614 }
615 
616 auto pow(Q, I)(auto ref const Q quantity, I n)
617         if (isQVariant!Q && isIntegral!I)
618 {
619     return quantity ^^ Rational(n);
620 }
621 
622 /// ditto
623 auto nthRoot(Q)(auto ref const Q quantity, Rational r)
624         if (isQVariant!Q)
625 {
626     return quantity ^^ r.inverted;
627 }
628 
629 auto nthRoot(Q, I)(auto ref const Q quantity, I n)
630         if (isQVariant!Q && isIntegral!I)
631 {
632     return nthRoot(quantity, Rational(n));
633 }
634 
635 /// ditto
636 Q abs(Q)(auto ref const Q quantity)
637         if (isQVariant!Q)
638 {
639     return Q(std.math.fabs(quantity._value), quantity._dimensions);
640 }