1 /++
2 This module defines dimensionnaly variant quantities.
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.qvariant;
10 
11 import quantities.internal.dimensions;
12 import quantities.base;
13 import quantities.parsing : DimensionException;
14 import std.exception;
15 import std.format;
16 import std.string;
17 import std.traits;
18 
19 version (unittest) import std.math : approxEqual;
20 
21 /++
22 QVariant  is analog  to Quantity  except  that the  dimensions are  stored in  a
23 private field  instead of a  type parameter. This makes  QVariant "dimensionnaly
24 variant", so  that a variable of  type QVariant can hold  quantities of variable
25 dimensions.  Yet,  only operations  that  are  dimensionnaly consistent  can  be
26 performed on QVariant variables.
27 
28 Warning:  Contrary to  Quantity, where  all dimensional  operations are  done at
29 compile-time,  the dimensions  of  QVariant  ate computed  at  runtime for  each
30 operation.  This  has a  significant  performance  cost.  Only use  QVariant  in
31 situation where using  Quantity is not possible, that is  when the dimensions of
32 the quantities are only known at runtime.
33 
34 Params:
35     N = the numeric type of the quantity.
36 
37 See_Also:
38     QVariant has the same public members and overloaded operators as Quantity.
39 +/
40 struct QVariant(N)
41 {
42     static assert(isNumeric!N, "Incompatible type: " ~ N.stringof);
43 
44 private:
45     void checkDim(in Dimensions dim) const pure @safe
46     {
47         enforceEx!DimensionException(dim == dimensions,
48             "Dimension error: %s is not compatible with %s"
49             .format(dim.toString, dimensions.toString));
50     }
51     
52     static void checkValueType(T)()
53     {
54         static assert(is(T : valueType), "%s is not implicitly convertible to %s"
55             .format(T.stringof, valueType.stringof));
56     }
57     
58 package:
59     N _value;
60     Dimensions dimensions;
61     
62     // Should be a constructor
63     // Workaround for @@BUG 5770@@
64     // (https://d.puremagic.com/issues/show_bug.cgi?id=5770)
65     // "Template constructor bypass access check"
66     static QVariant make(T)(T value, in Dimensions dim) @trusted
67         if (isNumeric!T)
68     {
69         checkValueType!T;
70         QVariant result = void;
71         result._value = value;
72         // The cast if safe: dim is a unique duplicate
73         result.dimensions = cast(Dimensions) dim;
74         return result;
75     }
76     
77     // Gets the internal number of this quantity.
78     N rawValue() const pure nothrow @nogc @safe
79     {
80         return _value;
81     }
82 
83 public:
84     alias valueType = N;
85     
86     // Gets the base unit of this quantity.
87     QVariant baseUnit() pure @safe
88     {
89         N one = 1;
90         return QVariant.make(one, dimensions);
91     }
92 
93     // Creates a new quantity from another one with the same dimensions
94     this(Q)(Q other)
95         if (isQVariant!Q || isQuantity!Q)
96     {
97         checkValueType!(Q.valueType);
98         _value = other.rawValue;
99         dimensions = other.dimensions;
100     }
101 
102     // Creates a new dimensionless quantity from a number
103     this(T)(T value)
104         if (isNumeric!T)
105     {
106         checkValueType!T;
107         dimensions = Dimensions.init;
108         _value = value;
109     }
110 
111     // Implicitly convert a dimensionless value to the value type
112     N get() const pure @safe
113     {
114         checkDim(Dimensions.init);
115         return _value;
116     }
117     alias get this;
118 
119     /+
120     Gets the _value of this quantity expressed in the given target unit.
121     +/
122     N value(Q)(Q target) const
123         if (isQVariant!Q || isQuantity!Q)
124     {
125         checkDim(target.dimensions);
126         checkValueType!(Q.valueType);
127         return _value / target.rawValue;
128     }
129     //
130     pure @safe unittest
131     {
132         import quantities.si : minute, hour;
133 
134         QVariant!double time = 120 * minute;
135         assert(time.value(hour) == 2);
136         assert(time.value(minute) == 120);
137     }
138 
139     /+
140     Tests wheter this quantity has the same dimensions as another one.
141     +/
142     bool isConsistentWith(Q)(Q other) const
143         if (isQVariant!Q || isQuantity!Q)
144     {
145         return dimensions == other.dimensions;
146     }
147     //
148     pure @safe unittest
149     {
150         import quantities.si : minute, second, meter;
151 
152         assert(minute.qVariant.isConsistentWith(second));
153         assert(!meter.qVariant.isConsistentWith(second));
154     }
155 
156     // Cast a QVariant to an equivalent Quantity
157     Q opCast(Q)() const
158         if (isQuantity!Q)
159     {
160         checkDim(Q.dimensions);
161         checkValueType!(Q.valueType);
162         return Q.make(_value);
163     }
164 
165     // Cast a dimensionless quantity to a numeric type
166     T opCast(T)() const
167         if (isNumeric!T)
168     {
169         checkDim(Dimensions.init);
170         checkValueType!T;
171         return _value;
172     }
173 
174     // Overloaded operators.
175     // Only dimensionally correct operations will compile.
176 
177     // Assign from another quantity
178     void opAssign(Q)(Q other)
179         if (isQVariant!Q || isQuantity!Q)
180     {
181         checkValueType!(Q.valueType);
182         dimensions = other.dimensions;
183         _value = other.rawValue;
184     }
185 
186     // Assign from a numeric value if this quantity is dimensionless
187     // ditto
188     void opAssign(T)(T other)
189         if (isNumeric!T)
190     {
191         checkValueType!T;
192         dimensions = Dimensions.init;
193         _value = other;
194     }
195 
196     // Unary + and -
197     // ditto
198     auto opUnary(string op)() const
199         if (op == "+" || op == "-")
200     {
201         return QVariant.make(mixin(op ~ "_value"), dimensions);
202     }
203     
204     // Unary ++ and --
205     // ditto
206     auto opUnary(string op)()
207         if (op == "++" || op == "--")
208     {
209         mixin(op ~ "_value;");
210         return this;
211     }
212 
213     // Add (or substract) two quantities if they share the same dimensions
214     // ditto
215     auto opBinary(string op, Q)(Q other) const
216         if ((isQVariant!Q || isQuantity!Q) && (op == "+" || op == "-"))
217     {
218         checkDim(other.dimensions);
219         checkValueType!(Q.valueType);
220         return QVariant.make(mixin("_value" ~ op ~ "other.rawValue"), dimensions);
221     }
222 
223     // ditto
224     auto opBinaryRight(string op, Q)(Q other) const
225         if ((isQVariant!Q || isQuantity!Q) && (op == "+" || op == "-"))
226     {
227         return opBinary!op(other);
228     }
229 
230     // Add (or substract) a dimensionless quantity and a number
231     // ditto
232     auto opBinary(string op, T)(T other) const
233         if (isNumeric!T && (op == "+" || op == "-"))
234     {
235         checkDim(Dimensions.init);
236         checkValueType!T;
237         return QVariant.make(mixin("_value" ~ op ~ "other"), dimensions);
238     }
239 
240     // ditto
241     auto opBinaryRight(string op, T)(T other) const
242         if (isNumeric!T && (op == "+" || op == "-"))
243     {
244         return opBinary!op(other);
245     }
246 
247     // Multiply or divide two quantities
248     // ditto
249     auto opBinary(string op, Q)(Q other) const
250         if ((isQVariant!Q || isQuantity!Q) && (op == "*" || op == "/" || op == "%"))
251     {
252         checkValueType!(Q.valueType);
253         return QVariant.make(mixin("(_value" ~ op ~ "other.rawValue)"),
254             dimensions.binop!op(other.dimensions));
255     }
256 
257     // ditto
258     auto opBinaryRight(string op, Q)(Q other) const
259         if ((isQVariant!Q || isQuantity!Q) && (op == "*" || op == "/" || op == "%"))
260     {
261         return this * other;
262     }
263 
264     // Multiply or divide a quantity by a number
265     // ditto
266     auto opBinary(string op, T)(T other) const
267         if (isNumeric!T && (op == "*" || op == "/" || op == "%"))
268     {
269         checkValueType!T;
270         return QVariant.make(mixin("_value" ~ op ~ "other"), dimensions);
271     }
272 
273     // ditto
274     auto opBinaryRight(string op, T)(T other) const
275         if (isNumeric!T && op == "*")
276     {
277         checkValueType!T;
278         return this * other;
279     }
280 
281     // ditto
282     auto opBinaryRight(string op, T)(T other) const
283         if (isNumeric!T && (op == "/" || op == "%"))
284     {
285         checkValueType!T;
286         return QVariant.make(mixin("other" ~ op ~ "_value"), dimensions.invert());
287     }
288 
289     // ditto
290     auto opBinary(string op, T)(T power) const
291         if (op == "^^")
292     {
293         if (__ctfe)
294             assert(false, "QVariant operator ^^ is not supported at compile-time");
295 
296         checkValueType!T;
297         return QVariant.make(_value^^power, dimensions.pow(power));
298     }
299 
300     // Add/sub assign with a quantity that shares the same dimensions
301     // ditto
302     void opOpAssign(string op, Q)(Q other)
303         if ((isQVariant!Q || isQuantity!Q) && (op == "+" || op == "-"))
304     {
305         checkDim(other.dimensions);
306         checkValueType!(Q.valueType);
307         mixin("_value " ~ op ~ "= other.rawValue;");
308     }
309 
310     // Add/sub assign a number to a dimensionless quantity
311     // ditto
312     void opOpAssign(string op, T)(T other)
313         if (isNumeric!T && (op == "+" || op == "-"))
314     {
315         checkDim(Dimensions.init);
316         checkValueType!T;
317         mixin("_value " ~ op ~ "= other;");
318     }
319 
320     // Mul/div assign with a dimensionless quantity
321     // ditto
322     void opOpAssign(string op, Q)(Q other)
323         if ((isQVariant!Q || isQuantity!Q) && (op == "*" || op == "/" || op == "%"))
324     {
325         checkValueType!(Q.valueType);
326         mixin("_value" ~ op ~ "= other.rawValue;");
327         dimensions = dimensions.binop!op(other.dimensions);
328     }
329 
330     // Mul/div assign with a number
331     // ditto
332     void opOpAssign(string op, T)(T other)
333         if (isNumeric!T && (op == "*" || op == "/" || op == "%"))
334     {
335         checkValueType!T;
336         mixin("_value" ~ op ~ "= other;");
337     }
338 
339     // Exact equality between quantities
340     // ditto
341     bool opEquals(Q)(Q other) const
342         if (isQVariant!Q || isQuantity!Q)
343     {
344         checkDim(other.dimensions);
345         return _value == other.rawValue;
346     }
347 
348     // Exact equality between a dimensionless quantity and a number
349     // ditto
350     bool opEquals(T)(T other) const
351         if (isNumeric!T)
352     {
353         checkDim(Dimensions.init);
354         return _value == other;
355     }
356 
357     // Comparison between two quantities
358     // ditto
359     int opCmp(Q)(Q other) const
360         if (isQVariant!Q || isQuantity!Q)
361     {
362         checkDim(other.dimensions);
363         if (_value == other.rawValue)
364             return 0;
365         if (_value < other.rawValue)
366             return -1;
367         return 1;
368     }
369 
370     // Comparison between a dimensionless quantity and a number
371     // ditto
372     int opCmp(T)(T other) const
373         if (isNumeric!T)
374     {
375         checkDim(Dimensions.init);
376         if (_value < other)
377             return -1;
378         if (_value > other)
379             return 1;
380         return 0;
381     }
382 
383     // String formatting function
384     void toString(scope void delegate(const(char)[]) sink, FormatSpec!char fmt) const
385     {
386         sink.formatValue(_value, fmt);
387         sink(" ");
388         sink(dimensions.toString);
389     }
390 }
391 
392 /// Converts a Quantity to an equivalent QVariant
393 auto qVariant(Q)(Q quantity)
394 {
395     return QVariant!(Q.valueType).make(quantity.rawValue, quantity.dimensions);
396 }
397 ///
398 pure @safe unittest
399 {
400     import quantities.si : meter, second;
401 
402     auto speed = 42 * meter/second;
403     auto qspeed = speed.qVariant;
404     assert(qspeed.value(meter/second) == 42);
405 }
406 
407 // Tests whether T is a quantity type
408 template isQVariant(T)
409 {
410     alias U = Unqual!T;
411     static if (is(U == QVariant!X, X...))
412         enum isQVariant = true;
413     else
414         enum isQVariant = false;
415 }
416 
417 pure @safe unittest // QVariant constructor
418 {
419     import quantities.si : minute, second, radian;
420 
421     QVariant!double time = typeof(second)(1 * minute);
422     assert(time.value(second) == 60);
423     assert(time.baseUnit == qVariant(second));
424 
425     QVariant!double angle = 2;
426     assert(angle.value(radian) == 2);
427 }
428 
429 pure @safe unittest // QVariant.alias this
430 {
431     import quantities.si : radian;
432 
433     static double foo(double d) { return d; }
434     assert(foo(2 * qVariant(radian)) == 2);
435 }
436 
437 pure @safe unittest // QVariant.opCast
438 {
439     import quantities.si : meter, radian, Angle;
440    
441     auto angle = cast(Angle) (12 * radian.qVariant);
442     assert(angle.value(radian) == 12);
443 
444     QVariant!double angle2 = 12 * radian;
445     assert(cast(double) angle2 == 12);
446 }
447 
448 pure @safe unittest // QVariant.opAssign Q = Q
449 {
450     import quantities.si : meter, second, radian;
451 
452     QVariant!double var = meter;
453     var = 100 * meter;
454     assert(var.value(meter) == 100);
455 
456     var /= 5 * second;
457     assert(var.value(meter/second).approxEqual(20));
458 
459     var = 3.14 * radian;
460 }
461 
462 pure @safe unittest // QVariant.opAssign Q = N
463 {
464     import quantities.si : radian;
465     
466     QVariant!double angle = radian;
467     angle = 2;
468     assert(angle.value(radian) == 2);
469 }
470 
471 pure @safe unittest // QVariant.opUnary +Q -Q ++Q --Q
472 {
473     import quantities.si : meter;
474 
475     QVariant!double length = + meter.qVariant;
476     assert(length == 1 * meter);
477     QVariant!double length2 = - meter.qVariant;
478     assert(length2 == -1 * meter);
479     
480     QVariant!double len = ++meter;
481     assert(len.value(meter).approxEqual(2));
482     len = --meter;
483     assert(len.value(meter).approxEqual(0));
484     ++len;
485     assert(len.value(meter).approxEqual(1));
486     len++;
487     assert(len.value(meter).approxEqual(2));
488 }
489 
490 pure @safe unittest // QVariant.opBinary Q*N Q/N
491 {
492     import quantities.si : second;
493 
494     QVariant!double time = second * 60;
495     assert(time.value(second) == 60);
496     QVariant!double time2 = second / 2;
497     assert(time2.value(second) == 1.0/2);
498 }
499 
500 pure @safe unittest // QVariant.opBinary Q+Q Q-Q
501 {
502     import quantities.si : meter;
503 
504     QVariant!double length = meter + meter;
505     assert(length.value(meter) == 2);
506     QVariant!double length2 = length - meter;
507     assert(length2.value(meter) == 1);
508 }
509 
510 pure @safe unittest // QVariant.opBinary Q+N Q-N
511 {
512     import quantities.si : radian;
513     
514     QVariant!double angle = radian + 1;
515     assert(angle.value(radian) == 2);
516     QVariant!double angle2 = angle - 1;
517     assert(angle2.value(radian) == 1);
518 }
519 
520 pure @safe unittest // QVariant.opBinary Q*Q Q/Q
521 {
522     import quantities.si : meter, minute, second;
523 
524     QVariant!double hertz = 1 / second;
525 
526     QVariant!double length = meter * 5;
527     QVariant!double surface = length * length;
528     assert(surface.value(meter * meter) == 5*5);
529     QVariant!double length2 = surface / length;
530     assert(length2.value(meter) == 5);
531 
532     QVariant!double x = minute / second;
533     assert(x.rawValue == 60);
534 
535     QVariant!double y = minute * hertz;
536     assert(y.rawValue == 60);
537 }
538 
539 pure @safe unittest // QVariant.opBinaryRight N*Q
540 {
541     import quantities.si : meter;
542 
543     QVariant!double length = 100 * meter;
544     assert(length == meter * 100);
545 }
546 
547 pure @safe unittest // QVariant.opBinaryRight N/Q
548 {
549     import quantities.si : meter;
550 
551     QVariant!double x = 1 / (2 * meter);
552     assert(x.value(1/meter) == 1.0/2);
553 }
554 
555 pure @safe unittest // QVariant.opBinary Q%Q Q%N N%Q
556 {
557     import quantities.si : meter;
558 
559     QVariant!double x = 258.1 * meter;
560     QVariant!double y1 = x % (50 * meter);
561     assert((cast(double) y1).approxEqual(8.1));
562     QVariant!double y2 = x % 50;
563     assert(y2.value(meter).approxEqual(8.1));
564 }
565 
566 pure @safe unittest // QVariant.opBinary Q^^N
567 {
568     import quantities.si : meter;
569     import quantities.si : cubic;
570 
571     QVariant!double x = 2 * meter;
572     assert((x^^3).value(cubic(meter)).approxEqual(8));
573 }
574 
575 pure @safe unittest // QVariant.opOpAssign Q+=Q Q-=Q
576 {
577     import quantities.si : second;
578 
579     QVariant!double time = 10 * second;
580     time += 50 * second;
581     assert(time.value(second).approxEqual(60));
582     time -= 40 * second;
583     assert(time.value(second).approxEqual(20));
584 }
585 
586 pure @safe unittest // QVariant.opOpAssign Q*=N Q/=N Q%=N
587 {
588     import quantities.si : second;
589 
590     QVariant!double time = 20 * second;
591     time *= 2;
592     assert(time.value(second).approxEqual(40));
593     time /= 4;
594     assert(time.value(second).approxEqual(10));
595     time %= 3;
596     assert(time.value(second).approxEqual(1));
597 }
598 
599 pure @safe unittest // QVariant.opEquals
600 {
601     import quantities.si : meter, minute, second, radian;
602 
603     assert(qVariant(1 * minute) == qVariant(60 * second));
604     assert(qVariant((1 / second) * meter) == qVariant(meter / second));
605     assert(radian.qVariant == 1);
606 }
607 
608 pure @safe unittest // QVariant.opCmp
609 {
610     import quantities.si : minute, second;
611 
612     QVariant!double hour = 60 * minute;
613     assert(second.qVariant < minute.qVariant);
614     assert(minute.qVariant <= minute.qVariant);
615     assert(hour > minute);
616     assert(hour >= hour);
617 }
618 
619 pure @safe unittest // Quantity.opCmp
620 {
621     import quantities.si : radian;
622     
623     QVariant!double angle = 2 * radian;
624     assert(angle < 4);
625     assert(angle <= 2);
626     assert(angle > 1);
627     assert(angle >= 2);
628 }
629 
630 unittest // Quantity.toString
631 {
632     import quantities.si : meter;
633     import std.conv : text;
634 
635     QVariant!double length = 12 * meter;
636     assert(length.text == "12 [L]", length.text);
637 }
638 
639 pure @safe unittest // Exceptions for incompatible dimensions
640 {
641     import quantities.si : meter, second;
642     import std.exception;
643 
644     QVariant!double m = meter;
645     assertThrown!DimensionException(m.value(second));
646     assertThrown!DimensionException(m + second);
647     assertThrown!DimensionException(m - second);
648     assertThrown!DimensionException(m + 1);
649     assertThrown!DimensionException(m - 1);
650     assertThrown!DimensionException(1 + m);
651     assertThrown!DimensionException(1 - m);
652     assertThrown!DimensionException(m += second);
653     assertThrown!DimensionException(m -= second);
654     assertThrown!DimensionException(m += 1);
655     assertThrown!DimensionException(m -= 1);
656     assertThrown!DimensionException(m == 1);
657     assertThrown!DimensionException(m == second);
658     assertThrown!DimensionException(m < second);
659     assertThrown!DimensionException(m < 1);
660 }
661 
662 pure @safe unittest // Compile-time
663 {
664     import quantities.si : meter, second, radian, cubic;
665         
666     enum length = 100 * meter.qVariant;
667     enum time = 5 * second.qVariant;
668     enum speed = length / time;
669     enum val = speed.value(meter/second);
670     static assert(val.approxEqual(20));
671 
672     version (none)
673     {
674         enum volume = length^^3;
675         static assert(volume.value(cubic(meter)).approxEqual(1e6));
676     }
677 }