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