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         this = qty.qVariant;
188     }
189 
190     /// Creates a new dimensionless quantity from a number
191     this(T)(T scalar)
192             if (isNumeric!T)
193     {
194         _dimensions = Dimensions.init;
195         _value = scalar;
196     }
197 
198     /// Returns the dimensions of the quantity
199     Dimensions dimensions() @property const
200     {
201         return _dimensions;
202     }
203 
204     /++
205     Implicitly convert a dimensionless value to the value type.
206 
207     Calling get will throw DimensionException if the quantity is not
208     dimensionless.
209     +/
210     N get() const
211     {
212         checkDimensionless;
213         return _value;
214     }
215 
216     alias get this;
217 
218     /++
219     Gets the _value of this quantity when expressed in the given target unit.
220     +/
221     N value(Q)(auto ref const Q target) const 
222             if (isQVariantOrQuantity!Q)
223     {
224         checkDim(target.dimensions);
225         return _value / target.rawValue;
226     }
227     ///
228     @safe pure unittest
229     {
230         auto minute = unit!int("T");
231         auto hour = 60 * minute;
232 
233         QVariant!int time = 120 * minute;
234         assert(time.value(hour) == 2);
235         assert(time.value(minute) == 120);
236     }
237 
238     /++
239     Test whether this quantity is dimensionless
240     +/
241     bool isDimensionless() @property const
242     {
243         return _dimensions.empty;
244     }
245 
246     /++
247     Tests wheter this quantity has the same dimensions as another one.
248     +/
249     bool isConsistentWith(Q)(auto ref const Q qty) const 
250             if (isQVariantOrQuantity!Q)
251     {
252         return _dimensions == qty.dimensions;
253     }
254     ///
255     @safe pure unittest
256     {
257         auto second = unit!double("T");
258         auto minute = 60 * second;
259         auto meter = unit!double("L");
260 
261         assert(minute.isConsistentWith(second));
262         assert(!meter.isConsistentWith(second));
263     }
264 
265     /++
266     Returns the base unit of this quantity.
267     +/
268     QVariant baseUnit() @property const
269     {
270         return QVariant(1, _dimensions);
271     }
272 
273     /++
274     Cast a dimensionless quantity to a numeric type.
275 
276     The cast operation will throw DimensionException if the quantity is not
277     dimensionless.
278     +/
279     T opCast(T)() const 
280             if (isNumeric!T)
281     {
282         checkDimensionless;
283         return _value;
284     }
285 
286     // Assign from another quantity
287     /// Operator overloading
288     ref QVariant opAssign(Q)(auto ref const Q qty)
289             if (isQVariantOrQuantity!Q)
290     {
291         _dimensions = qty.dimensions;
292         _value = qty.rawValue;
293         return this;
294     }
295 
296     // Assign from a numeric value if this quantity is dimensionless
297     /// ditto
298     ref QVariant opAssign(T)(T scalar)
299             if (isNumeric!T)
300     {
301         _dimensions = Dimensions.init;
302         _value = scalar;
303         return this;
304     }
305 
306     // Unary + and -
307     /// ditto
308     QVariant!N opUnary(string op)() const 
309             if (op == "+" || op == "-")
310     {
311         return QVariant(mixin(op ~ "_value"), _dimensions);
312     }
313 
314     // Unary ++ and --
315     /// ditto
316     QVariant!N opUnary(string op)()
317             if (op == "++" || op == "--")
318     {
319         mixin(op ~ "_value;");
320         return this;
321     }
322 
323     // Add (or substract) two quantities if they share the same dimensions
324     /// ditto
325     QVariant!N opBinary(string op, Q)(auto ref const Q qty) const 
326             if (isQVariantOrQuantity!Q && (op == "+" || op == "-"))
327     {
328         checkDim(qty.dimensions);
329         return QVariant(mixin("_value" ~ op ~ "qty.rawValue"), _dimensions);
330     }
331 
332     /// ditto
333     QVariant!N opBinaryRight(string op, Q)(auto ref const Q qty) const 
334             if (isQVariantOrQuantity!Q && (op == "+" || op == "-"))
335     {
336         checkDim(qty.dimensions);
337         return QVariant(mixin("qty.rawValue" ~ op ~ "_value"), _dimensions);
338     }
339 
340     // Add (or substract) a dimensionless quantity and a number
341     /// ditto
342     QVariant!N opBinary(string op, T)(T scalar) const 
343             if (isNumeric!T && (op == "+" || op == "-"))
344     {
345         checkDimensionless;
346         return QVariant(mixin("_value" ~ op ~ "scalar"), _dimensions);
347     }
348 
349     /// ditto
350     QVariant!N opBinaryRight(string op, T)(T scalar) const 
351             if (isNumeric!T && (op == "+" || op == "-"))
352     {
353         checkDimensionless;
354         return QVariant(mixin("scalar" ~ op ~ "_value"), _dimensions);
355     }
356 
357     // Multiply or divide a quantity by a number
358     /// ditto
359     QVariant!N opBinary(string op, T)(T scalar) const 
360             if (isNumeric!T && (op == "*" || op == "/" || op == "%"))
361     {
362         return QVariant(mixin("_value" ~ op ~ "scalar"), _dimensions);
363     }
364 
365     /// ditto
366     QVariant!N opBinaryRight(string op, T)(T scalar) const 
367             if (isNumeric!T && op == "*")
368     {
369         return QVariant(mixin("scalar" ~ op ~ "_value"), _dimensions);
370     }
371 
372     /// ditto
373     QVariant!N opBinaryRight(string op, T)(T scalar) const 
374             if (isNumeric!T && (op == "/" || op == "%"))
375     {
376         return QVariant(mixin("scalar" ~ op ~ "_value"), ~_dimensions);
377     }
378 
379     // Multiply or divide two quantities
380     /// ditto
381     QVariant!N opBinary(string op, Q)(auto ref const Q qty) const 
382             if (isQVariantOrQuantity!Q && (op == "*" || op == "/"))
383     {
384         return QVariant(mixin("_value" ~ op ~ "qty.rawValue"),
385                 mixin("_dimensions" ~ op ~ "qty.dimensions"));
386     }
387 
388     /// ditto
389     QVariant!N opBinaryRight(string op, Q)(auto ref const Q qty) const 
390             if (isQVariantOrQuantity!Q && (op == "*" || op == "/"))
391     {
392         return QVariant(mixin("qty.rawValue" ~ op ~ "_value"),
393                 mixin("qty.dimensions" ~ op ~ "_dimensions"));
394     }
395 
396     /// ditto
397     QVariant!N opBinary(string op, Q)(auto ref const Q qty) const 
398             if (isQVariantOrQuantity!Q && (op == "%"))
399     {
400         checkDim(qty.dimensions);
401         return QVariant(_value % qty.rawValue, _dimensions);
402     }
403 
404     /// ditto
405     QVariant!N opBinaryRight(string op, Q)(auto ref const Q qty) const 
406             if (isQVariantOrQuantity!Q && (op == "%"))
407     {
408         checkDim(qty.dimensions);
409         return QVariant(qty.rawValue % _value, _dimensions);
410     }
411 
412     /// ditto
413     QVariant!N opBinary(string op, T)(T power) const 
414             if (isIntegral!T && op == "^^")
415     {
416         return QVariant(_value ^^ power, _dimensions.pow(Rational(power)));
417     }
418 
419     /// ditto
420     QVariant!N opBinary(string op)(Rational power) const 
421             if (op == "^^")
422     {
423         static if (isIntegral!N)
424             auto newValue = std.math.pow(_value, cast(real) power).roundTo!N;
425         else static if (isFloatingPoint!N)
426             auto newValue = std.math.pow(_value, cast(real) power);
427         else
428             static assert(false, "Operation not defined for " ~ QVariant!N.stringof);
429         return QVariant(newValue, _dimensions.pow(power));
430     }
431 
432     // Add/sub assign with a quantity that shares the same dimensions
433     /// ditto
434     void opOpAssign(string op, Q)(auto ref const Q qty)
435             if (isQVariantOrQuantity!Q && (op == "+" || op == "-"))
436     {
437         checkDim(qty.dimensions);
438         mixin("_value " ~ op ~ "= qty.rawValue;");
439     }
440 
441     // Add/sub assign a number to a dimensionless quantity
442     /// ditto
443     void opOpAssign(string op, T)(T scalar)
444             if (isNumeric!T && (op == "+" || op == "-"))
445     {
446         checkDimensionless;
447         mixin("_value " ~ op ~ "= scalar;");
448     }
449 
450     // Mul/div assign another quantity to a quantity
451     /// ditto
452     void opOpAssign(string op, Q)(auto ref const Q qty)
453             if (isQVariantOrQuantity!Q && (op == "*" || op == "/" || op == "%"))
454     {
455         mixin("_value" ~ op ~ "= qty.rawValue;");
456         static if (op == "*")
457             _dimensions = _dimensions * qty.dimensions;
458         else
459             _dimensions = _dimensions / qty.dimensions;
460     }
461 
462     // Mul/div assign a number to a quantity
463     /// ditto
464     void opOpAssign(string op, T)(T scalar)
465             if (isNumeric!T && (op == "*" || op == "/"))
466     {
467         mixin("_value" ~ op ~ "= scalar;");
468     }
469 
470     /// ditto
471     void opOpAssign(string op, T)(T scalar)
472             if (isNumeric!T && op == "%")
473     {
474         checkDimensionless;
475         mixin("_value" ~ op ~ "= scalar;");
476     }
477 
478     // Exact equality between quantities
479     /// ditto
480     bool opEquals(Q)(auto ref const Q qty) const 
481             if (isQVariantOrQuantity!Q)
482     {
483         checkDim(qty.dimensions);
484         return _value == qty.rawValue;
485     }
486 
487     // Exact equality between a dimensionless quantity and a number
488     /// ditto
489     bool opEquals(T)(T scalar) const 
490             if (isNumeric!T)
491     {
492         checkDimensionless;
493         return _value == scalar;
494     }
495 
496     // Comparison between two quantities
497     /// ditto
498     int opCmp(Q)(auto ref const Q qty) const 
499             if (isQVariantOrQuantity!Q)
500     {
501         checkDim(qty.dimensions);
502         if (_value == qty.rawValue)
503             return 0;
504         if (_value < qty.rawValue)
505             return -1;
506         return 1;
507     }
508 
509     // Comparison between a dimensionless quantity and a number
510     /// ditto
511     int opCmp(T)(T scalar) const 
512             if (isNumeric!T)
513     {
514         checkDimensionless;
515         if (_value < scalar)
516             return -1;
517         if (_value > scalar)
518             return 1;
519         return 0;
520     }
521 
522     void toString(scope void delegate(const(char)[]) sink, FormatSpec!char fmt) const
523     {
524         sink.formatValue(_value, fmt);
525         sink(" ");
526         sink.formattedWrite!"%s"(_dimensions);
527     }
528 }
529 
530 /++
531 Creates a new monodimensional unit as a QVariant.
532 
533 Params:
534     N = The numeric type of the value part of the quantity.
535 
536     dimSymbol = The symbol of the dimension of this quantity.
537 
538     rank = The rank of the dimensions of this quantity in the dimension vector,
539            when combining this quantity with other oned.
540 +/
541 QVariant!N unit(N)(string dimSymbol, size_t rank = size_t.max)
542 {
543     return QVariant!N(N(1), Dimensions.mono(dimSymbol, rank));
544 }
545 ///
546 unittest
547 {
548     enum meter = unit!double("L", 1);
549     enum kilogram = unit!double("M", 2);
550     // Dimensions will be in this order: L M
551 }
552 
553 // Tests whether T is a quantity type
554 template isQVariant(T)
555 {
556     alias U = Unqual!T;
557     static if (is(U == QVariant!X, X...))
558         enum isQVariant = true;
559     else
560         enum isQVariant = false;
561 }
562 
563 enum isQVariantOrQuantity(T) = isQVariant!T || isQuantity!T;
564 
565 /// Turns a Quantity into a QVariant
566 auto qVariant(Q)(auto ref const Q qty)
567         if (isQuantity!Q)
568 {
569     return QVariant!(Q.valueType)(qty.rawValue, qty.dimensions);
570 }
571 
572 /// Turns a scalar into a dimensionless QVariant
573 auto qVariant(N)(N scalar)
574         if (isNumeric!N)
575 {
576     return QVariant!N(scalar, Dimensions.init);
577 }
578 
579 /// Basic math functions that work with QVariant.
580 auto square(Q)(auto ref const Q quantity)
581         if (isQVariant!Q)
582 {
583     return Q(quantity._value ^^ 2, quantity._dimensions.pow(2));
584 }
585 
586 /// ditto
587 auto sqrt(Q)(auto ref const Q quantity)
588         if (isQVariant!Q)
589 {
590     return Q(std.math.sqrt(quantity._value), quantity._dimensions.powinverse(2));
591 }
592 
593 /// ditto
594 auto cubic(Q)(auto ref const Q quantity)
595         if (isQVariant!Q)
596 {
597     return Q(quantity._value ^^ 3, quantity._dimensions.pow(3));
598 }
599 
600 /// ditto
601 auto cbrt(Q)(auto ref const Q quantity)
602         if (isQVariant!Q)
603 {
604     return Q(std.math.cbrt(quantity._value), quantity._dimensions.powinverse(3));
605 }
606 
607 /// ditto
608 auto pow(Q)(auto ref const Q quantity, Rational r)
609         if (isQVariant!Q)
610 {
611     return quantity ^^ r;
612 }
613 
614 auto pow(Q, I)(auto ref const Q quantity, I n)
615         if (isQVariant!Q && isIntegral!I)
616 {
617     return quantity ^^ Rational(n);
618 }
619 
620 /// ditto
621 auto nthRoot(Q)(auto ref const Q quantity, Rational r)
622         if (isQVariant!Q)
623 {
624     return quantity ^^ r.inverted;
625 }
626 
627 auto nthRoot(Q, I)(auto ref const Q quantity, I n)
628         if (isQVariant!Q && isIntegral!I)
629 {
630     return nthRoot(quantity, Rational(n));
631 }
632 
633 /// ditto
634 Q abs(Q)(auto ref const Q quantity)
635         if (isQVariant!Q)
636 {
637     return Q(std.math.fabs(quantity._value), quantity._dimensions);
638 }