1 /++
2 This module defines the base types for unit and quantity handling.
3 
4 Copyright: Copyright 2013-2015, Nicolas Sicard
5 Authors: Nicolas Sicard
6 License: $(LINK www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
7 Source: $(LINK https://github.com/biozic/quantities)
8 +/
9 module quantities.base;
10 
11 import quantities.internal.dimensions;
12 import std.exception;
13 import std.format;
14 import std.string;
15 import std.traits;
16 
17 version (unittest) 
18 {
19     import std.math : approxEqual;
20     // import std.conv : text;
21 }
22 
23 /++
24 A quantity  that can  be represented as  the product  of a number  and a  set of
25 dimensions. The  number is  stored internally as  a member of  type N,  which is
26 enforced to  be a built-in  numeric type  (isNumeric!N is true).  The dimensions
27 are stored as a private struct that  is equivalent to an associative array where
28 keys are dimension symbols (e.g. "L" for  length, "T" for time, etc.) and values
29 are dimension exponents.
30 
31 Units are  just instances  of a  Quantity struct where  the value  is 1  and the
32 dimensions only  contain one symbol, with  the power 1. For  instance, the meter
33 unit can be defined using the template `unit` as:
34 ---
35 enum meter = unit!(double, "L");
36 ---
37 The main  quantities compliant with the  international system of units  (SI) are
38 actually predefined  in  the module  quantities.si.
39 
40 Any quantity can be expressed as the product  of a number ($(I n)) and a unit of
41 the right dimensions ($(I U)). For instance:
42 ---
43 auto size = 9.5 * meter;
44 auto time = 120 * milli(second);
45 ---
46 The unit  $(I U)  is not  actually stored along  with the  number in  a Quantity
47 struct,  only the  dimensions are.  This  is because  the same  quantity can  be
48 expressed in an  infinity of different units.  The value of $(I n)  is stored as
49 if the quantity was  expressed in the base units of the  same dimemsions. In the
50 example above,  $(I n) = 9.5  for the variable size  and $(I n) =  0.120 for the
51 variable time.
52 
53 The method `value` can  be used  to  extract the  number  $(I n)  as  if it  was
54 expressed in  any possible  unit. The user  must pass this  unit to  the method.
55 This way, the user makes it clear in which unit the value was expressed.
56 ---
57 auto size = 9.5 * meter;
58 auto valueMeter      = size.value(meter);        // valueMeter == 9.5
59 auto valueCentimeter = size.value(centi(meter)); // valueCentimeter == 950
60 ---
61 Arithmetic operators (+ - * /),  as well as assignment and comparison operators,
62 are  defined when  the  operations are  dimensionally  consistent, otherwise  an
63 error occurs at compile-time:
64 ---
65 auto time = 2 * hour + 17 * minute;
66 auto frequency = time / second;
67 time = time + 2 * meter; // Compilation error
68 ---
69 Any kind  of quantities  and units  can be  defined with  this module,  not just
70 those  from the  SI. When  a quantity  that is  not predefined  has to  be used,
71 instead of instantiating the Quantity template  first, it is preferable to start
72 defining a new base unit (with only  one dimension) using the unit template, and
73 then the quantity type with the typeof operator:
74 ---
75 enum euro = unit!"C"; // C for currency
76 alias Currency = typeof(euro);
77 ---
78 This means that all currencies will be defined with respect to euro.
79 
80 Params:
81     N = The numeric type of the quantity used to store the value internally (e.g. `double`).
82     dims = The dimensions of the quantity.
83 +/
84 struct Quantity(N, Dimensions dims)
85 {
86     static assert(isNumeric!N, "Incompatible type: " ~ N.stringof);
87 
88 private:
89     static void checkDim(Dimensions dim)()
90     {
91         static assert(dim == dimensions, "Dimension error: %s is not compatible with %s"
92             .format(dim.toString, dimensions.toString));
93     }
94     
95     static void checkValueType(T)()
96     {
97         static assert(is(T : valueType), "%s is not implicitly convertible to %s"
98             .format(T.stringof, valueType.stringof));
99     }
100 
101 package:
102     N _value;
103     enum dimensions = dims;
104     
105     // Should be a constructor
106     // Workaround for @@BUG 5770@@
107     // (https://d.puremagic.com/issues/show_bug.cgi?id=5770)
108     // "Template constructor bypass access check"
109     package static Quantity make(T)(T value)
110         if (isNumeric!T)
111     {
112         checkValueType!T;
113         Quantity ret;
114         ret._value = value;
115         return ret;
116     }
117     
118     // Gets the internal number of this quantity.
119     package N rawValue() const
120     {
121         return _value;
122     }
123 
124 public:
125     /// The type of the underlying numeric value.
126     alias valueType = N;
127 
128     // Implicitly convert a dimensionless value to the value type
129     static if (dimensions.empty)
130     {
131         // Gets the internal number of this quantity.
132         N get() const
133         {
134             return _value;
135         }
136         alias get this;
137     }
138 
139     /// Gets the base unit of this quantity.
140     static Quantity baseUnit()
141     {
142         N one = 1;
143         return Quantity.make(one);
144     }
145 
146     // Creates a new quantity from another one with the same dimensions
147     this(Q)(Q other)
148         if (isQuantity!Q)
149     {
150         checkDim!(other.dimensions);
151         checkValueType!(Q.valueType);
152         _value = other._value;
153     }
154 
155     // Creates a new dimensionless quantity from a number
156     this(T)(T value)
157         if (isNumeric!T && dimensions.empty)
158     {
159         checkValueType!T;
160         _value = value;
161     }
162 
163     /++
164     Gets the _value of this quantity expressed in the given target unit.
165     +/
166     N value(Q)(Q target) const
167         if (isQuantity!Q)
168     {
169         checkDim!(target.dimensions);
170         checkValueType!(Q.valueType);
171         return _value / target._value;
172     }
173     ///
174     pure nothrow @nogc @safe unittest
175     {
176         import quantities.si : minute, hour;
177 
178         auto time = 120 * minute;
179         assert(time.value(hour) == 2);
180         assert(time.value(minute) == 120);
181     }
182 
183     /++
184     Tests wheter this quantity has the same dimensions as another one.
185     +/
186     bool isConsistentWith(Q)(Q other) const
187         if (isQuantity!Q)
188     {
189         enum ret = dimensions == other.dimensions;
190         return ret;
191     }
192     ///
193     pure nothrow @nogc @safe unittest
194     {
195         import quantities.si : minute, second, meter;
196 
197         assert(minute.isConsistentWith(second));
198         assert(!meter.isConsistentWith(second));
199     }
200 
201     /++
202     Convert a quantity to another one with the same dimensions.
203     +/
204     Q convert(Q)(Q target) const
205         if (isQuantity!Q)
206     {
207         return Q.make(_value / target._value);
208     }
209     ///
210     unittest
211     {
212         import quantities.si : minute, second;
213         import std.math : approxEqual;
214 
215         auto min = 2 * minute;
216         auto sec = min.convert(second);
217         assert(sec.value(second).approxEqual(120.0));
218     }
219 
220     /// Overloaded operators.
221     /// Only dimensionally correct operations will compile.
222 
223     // Cast a quantity to another quantity type with the same dimensions
224     Q opCast(Q)() const
225         if (isQuantity!Q)
226     {
227         checkDim!(Q.dimensions);
228         checkValueType!(Q.valueType);
229         return Q.make(_value);
230     }
231 
232     // Cast a dimensionless quantity to a numeric type
233     T opCast(T)() const
234         if (isNumeric!T)
235     {
236         import std.conv;
237         checkDim!(Dimensions.init);
238         checkValueType!T;
239         return _value.to!T;
240     }
241 
242     // Assign from another quantity
243     void opAssign(Q)(Q other)
244         if (isQuantity!Q)
245     {
246         checkDim!(other.dimensions);
247         checkValueType!(Q.valueType);
248         _value = other._value;
249     }
250 
251     // Assign from a numeric value if this quantity is dimensionless
252     /// ditto
253     void opAssign(T)(T other)
254         if (isNumeric!T)
255     {
256         checkDim!(Dimensions.init);
257         checkValueType!T;
258         _value = other;
259     }
260 
261     // Unary + and -
262     /// ditto
263     auto opUnary(string op)() const
264         if (op == "+" || op == "-")
265     {
266         return Quantity.make(mixin(op ~ "_value"));
267     }
268     
269     // Unary ++ and --
270     /// ditto
271     auto opUnary(string op)()
272         if (op == "++" || op == "--")
273     {
274         mixin(op ~ "_value;");
275         return this;
276     }
277 
278     // Add (or substract) two quantities if they share the same dimensions
279     /// ditto
280     auto opBinary(string op, Q)(Q other) const
281         if (isQuantity!Q && (op == "+" || op == "-"))
282     {
283         checkDim!(other.dimensions);
284         checkValueType!(Q.valueType);
285         return Quantity.make(mixin("_value" ~ op ~ "other._value"));
286     }
287 
288     // Add (or substract) a dimensionless quantity and a number
289     /// ditto
290     auto opBinary(string op, T)(T other) const
291         if (isNumeric!T && (op == "+" || op == "-"))
292     {
293         checkDim!(Dimensions.init);
294         checkValueType!T;
295         return Quantity.make(mixin("_value" ~ op ~ "other"));
296     }
297 
298     /// ditto
299     auto opBinaryRight(string op, T)(T other) const
300         if (isNumeric!T && (op == "+" || op == "-"))
301     {
302         return opBinary!op(other);
303     }
304 
305     // Multiply or divide two quantities
306     /// ditto
307     auto opBinary(string op, Q)(Q other) const
308         if (isQuantity!Q && (op == "*" || op == "/" || op == "%"))
309     {
310         checkValueType!(Q.valueType);
311         return Quantity!(N, dimensions.binop!op(other.dimensions))
312             .make(mixin("(_value" ~ op ~ "other._value)"));
313     }
314 
315     // Multiply or divide a quantity by a number
316     /// ditto
317     auto opBinary(string op, T)(T other) const
318         if (isNumeric!T && (op == "*" || op == "/" || op == "%"))
319     {
320         checkValueType!T;
321         return Quantity.make(mixin("_value" ~ op ~ "other"));
322     }
323 
324     /// ditto
325     auto opBinaryRight(string op, T)(T other) const
326         if (isNumeric!T && op == "*")
327     {
328         checkValueType!T;
329         return this * other;
330     }
331 
332     /// ditto
333     auto opBinaryRight(string op, T)(T other) const
334         if (isNumeric!T && (op == "/" || op == "%"))
335     {
336         checkValueType!T;
337         return Quantity!(N, dimensions.invert()).make(mixin("other" ~ op ~ "_value"));
338     }
339 
340     auto opBinary(string op, T)(T power) const
341         if (op == "^^")
342     {
343         static assert(false, "Unsupporter operator: ^^");
344     }
345 
346     // Add/sub assign with a quantity that shares the same dimensions
347     /// ditto
348     void opOpAssign(string op, Q)(Q other)
349         if (isQuantity!Q && (op == "+" || op == "-"))
350     {
351         checkDim!(other.dimensions);
352         checkValueType!(Q.valueType);
353         mixin("_value " ~ op ~ "= other._value;");
354     }
355 
356     // Add/sub assign a number to a dimensionless quantity
357     /// ditto
358     void opOpAssign(string op, T)(T other)
359         if (isNumeric!T && (op == "+" || op == "-"))
360     {
361         checkDim!(Dimensions.init);
362         checkValueType!T;
363         mixin("_value " ~ op ~ "= other;");
364     }
365 
366     // Mul/div assign with a dimensionless quantity
367     /// ditto
368     void opOpAssign(string op, Q)(Q other)
369         if (isQuantity!Q && (op == "*" || op == "/" || op == "%"))
370     {
371         Q.checkDim!(Dimensions.init);
372         checkValueType!(Q.valueType);
373         mixin("_value" ~ op ~ "= other._value;");
374     }
375 
376     // Mul/div assign with a number
377     /// ditto
378     void opOpAssign(string op, T)(T other)
379         if (isNumeric!T && (op == "*" || op == "/" || op == "%"))
380     {
381         checkValueType!T;
382         mixin("_value" ~ op ~ "= other;");
383     }
384 
385     // Exact equality between quantities
386     /// ditto
387     bool opEquals(Q)(Q other) const
388         if (isQuantity!Q)
389     {
390         checkDim!(other.dimensions);
391         return _value == other._value;
392     }
393 
394     // Exact equality between a dimensionless quantity and a number
395     /// ditto
396     bool opEquals(T)(T other) const
397         if (isNumeric!T)
398     {
399         checkValueType!T;
400         checkDim!(Dimensions.init);
401         return _value == other;
402     }
403 
404     // Comparison between two quantities
405     /// ditto
406     int opCmp(Q)(Q other) const
407         if (isQuantity!Q)
408     {
409         checkDim!(other.dimensions);
410         if (_value == other._value)
411             return 0;
412         if (_value < other._value)
413             return -1;
414         return 1;
415     }
416 
417     // Comparison between a dimensionless quantity and a number
418     /// ditto
419     int opCmp(T)(T other) const
420         if (isNumeric!T)
421     {
422         checkValueType!T;
423         checkDim!(Dimensions.init);
424         if (_value < other)
425             return -1;
426         if (_value > other)
427             return 1;
428         return 0;
429     }
430 
431     // Default string formatting function
432     void toString(scope void delegate(const(char)[]) sink, FormatSpec!char fmt) const
433     {
434         sink.formatValue(_value, fmt);
435         sink(" ");
436         sink(dimensions.toString);
437     }
438 }
439 
440 pure nothrow @nogc @safe unittest // Quantity.baseUnit
441 {
442     import quantities.si : minute, second;
443 
444     assert(minute.baseUnit == second);
445 }
446 
447 pure nothrow @nogc @safe unittest // Quantity constructor
448 {
449     import quantities.si : minute, second, radian;
450     import std.math : approxEqual;
451 
452     auto time = typeof(second)(1 * minute);
453     assert(time.value(second) == 60);
454 
455 
456     auto angle = typeof(radian)(3.14);
457     assert(angle.value(radian).approxEqual(3.14));
458 }
459 
460 pure nothrow @nogc @safe unittest // QVariant.alias this
461 {
462     import quantities.si : radian;
463 
464     static double foo(double d) nothrow @nogc { return d; }
465     assert(foo(2 * radian) == 2);
466 }
467 
468 pure nothrow @nogc @safe unittest // Quantity.opCast
469 {
470     import quantities.si : second, radian;
471 
472     auto fsec = unit!(float, "T");
473     assert((cast(typeof(second)) fsec).value(second) == 1);
474     auto angle = 12 * radian;
475     assert(cast(double) angle == 12);
476 }
477 
478 pure nothrow @nogc @safe unittest // Quantity.opAssign Q = Q
479 {
480     import quantities.si : meter, radian;
481 
482     auto length = meter;
483     length = 100 * meter;
484     assert(length.value(meter) == 100);
485     auto angle = radian;
486     angle = 2;
487     assert(angle.value(radian) == 2);
488 }
489 
490 pure nothrow @nogc @safe unittest // Quantity.opUnary +Q -Q ++Q --Q
491 {
492     import quantities.si : meter;
493 
494     auto length = + meter;
495     assert(length == 1 * meter);
496     auto length2 = - meter;
497     assert(length2 == -1 * meter);
498     
499     auto len = ++meter;
500     assert(len.value(meter).approxEqual(2));
501     len = --meter;
502     assert(len.value(meter).approxEqual(0));
503     len++;
504     assert(len.value(meter).approxEqual(1));    
505 }
506 
507 pure nothrow @nogc @safe unittest // Quantity.opBinary Q*N Q/N
508 {
509     import quantities.si : second;
510     import std.math : approxEqual;
511 
512     auto time = second * 60;
513     assert(time.value(second) == 60);
514     auto time2 = second / 2;
515     assert(time2.value(second).approxEqual(0.5));
516 }
517 
518 pure nothrow @nogc @safe unittest // Quantity.opBinary Q*Q Q/Q
519 {
520     import quantities.si : meter, minute, second;
521 
522     auto hertz = 1 / second;
523 
524     auto length = meter * 5;
525     auto surface = length * length;
526     assert(surface.value(meter * meter) == 5*5);
527     auto length2 = surface / length;
528     assert(length2.value(meter) == 5);
529 
530     auto x = minute / second;
531     assert(x.rawValue == 60);
532 
533     auto y = minute * hertz;
534     assert(y.rawValue == 60);
535 }
536 
537 pure nothrow @nogc @safe unittest // Quantity.opBinaryRight N*Q
538 {
539     import quantities.si : meter;
540 
541     auto length = 100 * meter;
542     assert(length == meter * 100);
543 }
544 
545 pure nothrow @nogc @safe  unittest // Quantity.opBinaryRight N/Q
546 {
547     import quantities.si : meter;
548     import std.math : approxEqual;
549 
550     auto x = 1 / (2 * meter);
551     assert(x.value(1 / meter).approxEqual(0.5));
552 }
553 
554 pure nothrow @nogc @safe unittest // Quantity.opBinary Q%Q Q%N N%Q
555 {
556     import quantities.si : meter;
557 
558     auto x = 258.1 * meter;
559     auto y1 = x % (50 * meter);
560     assert((cast(double) y1).approxEqual(8.1));
561     auto y2 = x % 50;
562     assert(y2.value(meter).approxEqual(8.1));
563 }
564 
565 pure nothrow @nogc @safe unittest // Quantity.opBinary Q+Q Q-Q
566 {
567     import quantities.si : meter;
568     
569     auto length = meter + meter;
570     assert(length.value(meter) == 2);
571     auto length2 = length - meter;
572     assert(length2.value(meter) == 1);
573 }
574 
575 pure nothrow @nogc @safe unittest // Quantity.opBinary Q+N Q-N
576 {
577     import quantities.si : radian;
578     
579     auto angle = radian + 1;
580     assert(angle.value(radian) == 2);
581     angle = angle - 1;
582     assert(angle.value(radian) == 1);
583     angle = 1 + angle;
584     assert(angle.value(radian) == 2);
585 }
586 
587 pure nothrow @nogc @safe unittest // Quantity.opOpAssign Q+=Q Q-=Q
588 {
589     import quantities.si : second;
590 
591     auto time = 10 * second;
592     time += 50 * second;
593     assert(time.value(second).approxEqual(60));
594     time -= 40 * second;
595     assert(time.value(second).approxEqual(20));
596 }
597 
598 pure nothrow @nogc @safe unittest // Quantity.opBinary Q+N Q-N
599 {
600     import quantities.si : radian;
601     
602     auto angle = 1 * radian;
603     angle += 1;
604     assert(angle.value(radian) == 2);
605     angle -= 1;
606     assert(angle.value(radian) == 1);
607 }
608 
609 pure nothrow @nogc @safe unittest // Quantity.opOpAssign Q*=N Q/=N Q%=N
610 {
611     import quantities.si : second;
612 
613     auto time = 20 * second;
614     time *= 2;
615     assert(time.value(second).approxEqual(40));
616     time /= 4;
617     assert(time.value(second).approxEqual(10));
618     time %= 3;
619     assert(time.value(second).approxEqual(1));
620 }
621 
622 pure nothrow @nogc @safe unittest // Quantity.opOpAssign Q*=N Q/=N Q%=N
623 {
624     import quantities.si : meter, second;
625     
626     auto time = 20 * second;
627     time *= (2 * meter) / meter;
628     assert(time.value(second).approxEqual(40));
629     time /= (4 * meter) / meter;
630     assert(time.value(second).approxEqual(10));
631     time %= (3 * meter) / meter;
632     assert(time.value(second).approxEqual(1));
633 }
634 
635 pure nothrow @nogc @safe unittest // Quantity.opEquals
636 {
637     import quantities.si : radian, minute, second;
638 
639     assert(1 * minute == 60 * second);
640     assert(1 * radian == 1);
641 }
642 
643 pure nothrow @nogc @safe unittest // Quantity.opCmp
644 {
645     import quantities.si : minute, second;
646 
647     auto hour = 60 * minute;
648     assert(second < minute);
649     assert(minute <= minute);
650     assert(hour > minute);
651     assert(hour >= hour);
652 }
653 
654 pure nothrow @nogc @safe unittest // Quantity.opCmp
655 {
656     import quantities.si : radian;
657     
658     auto angle = 2 * radian;
659     assert(angle < 4);
660     assert(angle <= 2);
661     assert(angle > 1);
662     assert(angle >= 2);
663 }
664 
665 unittest // Quantity.toString
666 {
667     import quantities.si : meter;
668     import std.conv : text;
669 
670     auto length = 12 * meter;
671     assert(length.text == "12 [L]", length.text);
672 }
673 
674 pure nothrow @nogc @safe unittest // Compilation errors for incompatible dimensions
675 {
676     import quantities.si : meter, second;
677 
678     auto m = meter;
679     static assert(!__traits(compiles, m.value(second)));
680     static assert(!__traits(compiles, m = second));
681     static assert(!__traits(compiles, m + second));
682     static assert(!__traits(compiles, m - second));
683     static assert(!__traits(compiles, m + 1));
684     static assert(!__traits(compiles, m - 1));
685     static assert(!__traits(compiles, 1 + m));
686     static assert(!__traits(compiles, 1 - m));
687     static assert(!__traits(compiles, m += second));
688     static assert(!__traits(compiles, m -= second));
689     static assert(!__traits(compiles, m *= second));
690     static assert(!__traits(compiles, m /= second));
691     static assert(!__traits(compiles, m *= meter));
692     static assert(!__traits(compiles, m /= meter));
693     static assert(!__traits(compiles, m += 1));
694     static assert(!__traits(compiles, m -= 1));
695     static assert(!__traits(compiles, m == 1));
696     static assert(!__traits(compiles, m == second));
697     static assert(!__traits(compiles, m < second));
698     static assert(!__traits(compiles, m < 1));
699 }
700 
701 pure nothrow @nogc @safe unittest // immutable Quantity
702 {
703     import quantities.si : meter, minute, second;
704 
705     immutable length = 3e8 * meter;
706     immutable time = 1 * second;
707     immutable speedOfLight = length / time;
708     assert(speedOfLight == 3e8 * meter / second);
709     assert(speedOfLight > 1 * meter / minute);
710 }
711 
712 /// Tests whether T is a quantity type
713 template isQuantity(T)
714 {
715     static if (is(Unqual!T == Quantity!X, X...))
716         enum isQuantity = true;
717     else
718         enum isQuantity = false;
719 }
720 
721 /// Creates a new monodimensional unit.
722 template unit(N, string symbol)
723 {
724     enum dim = Dimensions.mono(symbol);
725     enum unit = {return Quantity!(N, dim).make(1); }();
726 }
727 ///
728 pure nothrow @nogc @safe unittest
729 {
730     auto euro = unit!(double, "C"); // C for Currency
731     assert(isQuantity!(typeof(euro)));
732     auto dollar = euro / 1.35;
733     assert((1.35 * dollar).value(euro).approxEqual(1));
734 }
735 
736 /// Check that two quantity types are dimensionally consistent.
737 template AreConsistent(Q1, Q2)
738     if (isQuantity!Q1 && isQuantity!Q2)
739 {
740     enum AreConsistent = Q1.dimensions == Q2.dimensions;
741 }
742 ///
743 pure nothrow @nogc @safe unittest
744 {
745     import quantities.si : meter, second;
746 
747     alias Speed = typeof(meter/second);
748     alias Velocity = typeof((1/second * meter));
749     static assert(AreConsistent!(Speed, Velocity));
750 }
751 
752 /++
753 Creates a new prefix function that mutlpy a Quantity by _factor factor.
754 +/
755 template prefix(alias factor)
756 {
757     alias N = typeof(factor);
758     static assert(isNumeric!N, "Incompatible type: " ~ N.stringof);
759 
760     auto prefix(Q)(Q base)
761         if (isQuantity!Q)
762     {
763         return base * factor;
764     }
765 }
766 ///
767 pure nothrow @nogc @safe unittest
768 {
769     import quantities.si : meter;
770 
771     alias milli = prefix!1e-3;
772     assert(milli(meter).value(meter).approxEqual(1e-3));
773 }