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