1 // Written in the D programming language
2 /++
3 This module defines functions to parse units and quantities. The text
4 input is parsed according to the following grammar. For example:
5 $(DL
6 $(DT Prefixes and unit symbols must be joined:)
7     $(DD "1 mm" = 1 millimeter)
8     $(DD "1 m m" = 1 square meter)
9 $(BR)
10 $(DT Standalone units are preferred over prefixed ones:)
11     $(DD "1 cd" = 1 candela, not 1 centiday)
12 $(BR)
13 $(DT Powers of units:)
14     $(DD "1 m^2")
15     $(DD "1 m²" $(I (superscript integer)))
16 $(BR)
17 $(DT Multiplication of to units:)
18     $(DD "1 N m" $(I (whitespace)))
19     $(DD "1 N . m")
20     $(DD "1 N ⋅ m" $(I (centered dot)))
21     $(DD "1 N * m")
22     $(DD "1 N × m" $(I (times sign)))
23 $(BR)
24 $(DT Division of to units:)
25     $(DD "1 mol / s")
26     $(DD "1 mol ÷ s")
27 $(BR)
28 $(DT Grouping of units with parentheses:)
29     $(DD "1 kg/(m.s^2)" = 1 kg m⁻¹ s⁻²)
30 )
31 
32 Grammar: (whitespace not significant)
33 $(DL
34 $(DT Quantity:)
35     $(DD Units)
36     $(DD Number Units)
37 $(BR)
38 $(DT Number:)
39     $(DD $(I Numeric value parsed by std.conv.parse!real))
40 $(BR)
41 $(DT Units:)
42     $(DD Unit)
43     $(DD Unit Units)
44     $(DD Unit Operator Units)
45 $(BR)
46 $(DT Operator:)
47     $(DD $(B *))
48     $(DD $(B .))
49     $(DD $(B ⋅))
50     $(DD $(B ×))
51     $(DD $(B /))
52     $(DD $(B ÷))
53 $(BR)
54 $(DT Unit:)
55     $(DD Base)
56     $(DD Base $(B ^) Integer)
57     $(DD Base SupInteger)
58 $(BR)
59 $(DT Base:)
60     $(DD Symbol)
61     $(DD Prefix Symbol)
62     $(DD $(B $(LPAREN)) Units $(B $(RPAREN)))
63 $(BR)
64 $(DT Symbol:)
65     $(DD $(I The symbol of a valid unit))
66 $(BR)
67 $(DT Prefix:)
68     $(DD $(I The symbol of a valid prefix))
69 $(BR)
70 $(DT Integer:)
71     $(DD $(I Integer value parsed by std.conv.parse!int))
72 $(BR)
73 $(DT SupInteger:)
74     $(DD $(I Superscript version of Integer))
75 )
76 
77 Copyright: Copyright 2013-2014, Nicolas Sicard
78 Authors: Nicolas Sicard
79 License: $(LINK www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
80 Source: $(LINK https://github.com/biozic/quantities)
81 +/
82 module quantities.parsing;
83 
84 import quantities.base;
85 import std.array;
86 import std.algorithm;
87 import std.conv;
88 import std.exception;
89 import std.math;
90 import std.range;
91 import std.string;
92 import std.traits;
93 import std.utf;
94 
95 /++
96 Contains the symbols of the units and the prefixes that a parser can handle.
97 +/
98 struct SymbolList(N)
99 {
100     static assert(isNumberLike!N, "Incompatible type: " ~ N.stringof);
101 
102     package
103     {
104         RTQuantity!N[string] units;
105         N[string] prefixes;
106         size_t maxPrefixLength;
107     }
108 
109     /// Adds (or replaces) a unit in the list
110     void addUnit(Q)(string symbol, Q unit)
111         if (isQuantity!Q)
112     {
113         units[symbol] = unit.toRT;
114     }
115 
116     /// Adds (or replaces) a prefix in the list
117     void addPrefix(N)(string symbol, N factor)
118         if (isNumberLike!N)
119     {
120         prefixes[symbol] = factor;
121         if (symbol.length > maxPrefixLength)
122             maxPrefixLength = symbol.length;
123     }
124 }
125 
126 /++
127 Helps build a SymbolList at compile-time.
128 
129 Use with the global addUnit and addPrefix functions.
130 +/
131 SymbolList!N makeSymbolList(N, Sym...)(Sym list)
132 {
133     SymbolList!N ret;
134     foreach (sym; list)
135     {
136         static if (is(typeof(sym) == WithUnit!Q, Q))
137         {
138             static assert(is(Q.valueType : N), "Incompatible value types: %s and %s" 
139                           .format(Q.valueType.stringof, N.stringof));
140             ret.units[sym.symbol] = sym.unit;
141         }
142         else static if (is(typeof(sym) == WithPrefix!T, T))
143         {
144             static assert(is(T : N), "Incompatible value types: %s and %s" 
145                           .format(T.stringof, N.stringof));
146             ret.prefixes[sym.symbol] = sym.factor;
147             if (sym.symbol.length > ret.maxPrefixLength)
148                 ret.maxPrefixLength = sym.symbol.length;
149         }
150         else
151             static assert(false, "Unexpected symbol: " ~ sym.stringof);
152     }
153     return ret;
154 }
155 ///
156 unittest
157 {
158     enum euro = unit!("C", double);
159     alias Currency = typeof(euro);
160     enum dollar = 1.35 * euro;
161 
162     enum symbolList = makeSymbolList!double(
163         withUnit("€", euro),
164         withUnit("$", dollar),
165         withPrefix("doz", 12)
166     );
167 }
168 
169 package struct WithUnit(Q)
170 {
171     string symbol;
172     RTQuantity!(Q.valueType) unit;
173 }
174 
175 /// Creates a unit that can be added to a SymbolList via the SymbolList constuctor.
176 auto withUnit(Q)(string symbol, Q unit)
177     if (isQuantity!Q)
178 {
179     return WithUnit!Q(symbol, unit.toRT);
180 }
181 
182 package struct WithPrefix(N)
183 {
184     string symbol;
185     N factor;
186 }
187 
188 /// Creates a prefix that can be added to a SymbolList via the SymbolList constuctor.
189 auto withPrefix(N)(string symbol, N factor)
190     if (isNumberLike!N)
191 {
192     return WithPrefix!N(symbol, factor);
193 }
194 
195 /++
196 Creates a runtime parser capable of working on user-defined units and prefixes.
197 
198 Params:
199     N = The type of the value type stored in the Quantity struct.
200     symbolList = A prefilled SymbolList struct that contains all units and prefixes.
201     parseFun = A function that can parse the beginning of a string to return a numeric value of type N.
202         After this function returns, it must have consumed the numeric part and leave only the unit part.
203     one = The value of type N that is equivalent to 1.
204 +/
205 template rtQuantityParser(
206     N, 
207     alias symbolList, 
208     alias parseFun = (ref string s) => parse!N(s)
209 )
210 {
211     auto rtQuantityParser(Q, S)(S str)
212         if (isQuantity!Q)
213     {
214         static assert(is(N : Q.valueType), "Incompatible value type: " ~ Q.valueType.stringof);
215 
216         auto rtQuant = parseRTQuantity!(Q.valueType, parseFun)(str, symbolList);
217         enforceEx!DimensionException(
218             toAA!(Q.dimensions) == rtQuant.dimensions,
219             "Dimension error: [%s] is not compatible with [%s]"
220             .format(quantities.base.dimstr!(Q.dimensions), dimstr(rtQuant.dimensions)));
221         return Q.make(rtQuant.value);
222     }    
223 }
224 ///
225 unittest
226 {
227     import std.bigint;
228     
229     enum bit = unit!("bit", BigInt);
230     alias BinarySize = typeof(bit);
231 
232     SymbolList!BigInt symbolList;
233     symbolList.addUnit("bit", bit);
234     symbolList.addPrefix("or", BigInt("1234567890987654321"));
235     
236     static BigInt parseFun(ref string input)
237     {
238         import std.exception, std.regex;
239         enum rgx = ctRegex!`^(\d*)\s*(.*)$`;
240         auto m = enforce(match(input, rgx));
241         input = m.captures[2];
242         return BigInt(m.captures[1]);
243     }
244     
245     alias parse = rtQuantityParser!(BigInt, symbolList, parseFun);
246 
247     auto foo = BigInt("1234567890987654300") * bit;
248     foo += BigInt(21) * bit;
249     assert(foo == parse!BinarySize("1 orbit"));
250 }
251 
252 /++
253 Creates a compile-time parser capable of working on user-defined units and prefixes.
254 
255 Contrary to a runtime parser, a compile-time parser infers the type of the parsed quantity
256 automatically from the dimensions of its components.
257 
258 Params:
259     N = The type of the value type stored in the Quantity struct.
260     symbolList = A prefilled SymbolList struct that contains all units and prefixes.
261     parseFun = A function that can parse the beginning of a string to return a numeric value of type N.
262         After this function returns, it must have consumed the numeric part and leave only the unit part.
263     one = The value of type N that is equivalent to 1.
264 +/
265 template ctQuantityParser(
266     N, 
267     alias symbolList, 
268     alias parseFun = (ref string s) => parse!N(s)
269 )
270 {
271     template ctQuantityParser(string str)
272     {
273         static string dimTup(int[string] dims)
274         {
275             return dims.keys.map!(x => `"%s", %s`.format(x, dims[x])).join(", ");
276         }
277         
278         // This is for a nice compile-time error message
279         enum msg = { return collectExceptionMsg(parseRTQuantity!(N, parseFun)(str, symbolList)); }();
280         static if (msg)
281         {
282             static assert(false, msg);
283         }
284         else
285         {
286             enum q = parseRTQuantity!(N, parseFun)(str, symbolList);
287             enum dimStr = dimTup(q.dimensions);
288             mixin("alias dims = TypeTuple!(%s);".format(dimStr));
289             enum ctQuantityParser = Quantity!(N, Sort!dims).make(q.value);
290         }
291     }
292 }
293 ///
294 version (D_Ddoc) // DMD BUG? (Differents symbolLists but same template instantiation)
295 unittest
296 {
297     enum bit = unit!("bit", ulong);
298     alias BinarySize = typeof(bit);
299     enum byte_ = 8 * bit;
300     
301     enum symbolList = makeSymbolList!ulong(
302         withUnit("bit", bit),
303         withUnit("B", byte_),
304         withPrefix("hob", 7)
305     );
306     
307     alias sz = ctQuantityParser!(ulong, symbolList);
308     
309     assert(sz!"1 hobbit".value(bit) == 7);
310 }
311 
312 /// Exception thrown when parsing encounters an unexpected token.
313 class ParsingException : Exception
314 {
315     @safe pure nothrow
316     this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null)
317     {
318         super(msg, file, line, next);
319     }
320 
321     @safe pure nothrow
322     this(string msg, Throwable next, string file = __FILE__, size_t line = __LINE__)
323     {
324         super(msg, file, line, next);
325     }
326 }
327 
328 package:
329 
330 RTQuantity!N parseRTQuantity(N, alias parseFun, S, SL)(S str, auto ref SL symbolList)
331 {
332     static assert(isForwardRange!S && isSomeChar!(ElementType!S),
333                   "input must be a forward range of a character type");
334 
335     N value;
336     try
337     {
338         value = parseFun(str);
339     }
340     catch
341     {
342         static if (isNumeric!N)
343             value = 1;
344         else static if (__traits(compiles, N(1)))
345             value = N(1);
346         else
347             static assert(false, "BUG");
348     }
349 
350     if (str.empty)
351         return RTQuantity!N(value, null);
352 
353     auto input = str.to!string;
354     auto tokens = lex(input);
355     auto parser = QuantityParser!N(symbolList);
356     
357     RTQuantity!N result = parser.parseCompoundUnit(tokens);
358     result.value *= value;
359     return result;
360 }
361 
362 unittest // Test parsing
363 {
364     enum meter = unit!"L";
365     enum kilogram = unit!"M";
366     enum second = unit!"T";
367     enum one = meter / meter;
368 
369     enum siSL = makeSymbolList!real(
370         withUnit("m", meter),
371         withUnit("kg", kilogram),
372         withUnit("s", second),
373         withPrefix("c", 0.01L),
374         withPrefix("m", 0.001L)
375     );
376 
377     static bool checkParse(Q)(string input, Q quantity)
378     {
379         return parseRTQuantity!(real, std.conv.parse!(real, string))(input, siSL)
380             == quantity.toRT;
381     }
382 
383     assert(checkParse("1    m    ", meter));
384     assert(checkParse("1 mm", 0.001L * meter));
385     assert(checkParse("1 m^-1", 1 / meter));
386     assert(checkParse("1 m²", meter * meter));
387     assert(checkParse("1 m⁺²", meter * meter));
388     assert(checkParse("1 m⁻¹", 1 / meter));
389     assert(checkParse("1 (m)", meter));
390     assert(checkParse("1 (m^-1)", 1 / meter));
391     assert(checkParse("1 ((m)^-1)^-1", meter));
392     assert(checkParse("1 m*m", meter * meter));
393     assert(checkParse("1 m m", meter * meter));
394     assert(checkParse("1 m.m", meter * meter));
395     assert(checkParse("1 m⋅m", meter * meter));
396     assert(checkParse("1 m×m", meter * meter));
397     assert(checkParse("1 m/m", meter / meter));
398     assert(checkParse("1 m÷m", meter / meter));
399     assert(checkParse("1 m.s", second * meter));
400     assert(checkParse("1 m s", second * meter));
401     assert(checkParse("1 m*m/m", meter));
402     assert(checkParse("0.8", 0.8L * one));
403 
404     assertThrown!ParsingException(checkParse("1 c m", meter * meter));
405     assertThrown!ParsingException(checkParse("1 c", 0.01L * meter));
406     assertThrown!ParsingException(checkParse("1 Qm", meter));
407     assertThrown!ParsingException(checkParse("1 m/", meter));
408     assertThrown!ParsingException(checkParse("1 m^", meter));
409     assertThrown!ParsingException(checkParse("1 m ) m", meter * meter));
410     assertThrown!ParsingException(checkParse("1 m * m) m", meter * meter * meter));
411     assertThrown!ParsingException(checkParse("1 m^²", meter * meter));
412     assertThrown!ParsingException(checkParse("1-⁺⁵", one));
413 }
414 
415 // Holds a value and a dimensions for parsing
416 struct RTQuantity(N)
417 {
418     // The payload
419     N value;
420 
421     // The dimensions of the quantity
422     int[string] dimensions;
423 }
424 
425 // A parser that can parse a text for a unit or a quantity
426 struct QuantityParser(N)
427 {
428     alias RTQ = RTQuantity!N;
429 
430     private SymbolList!N symbolList;
431 
432     RTQ parseCompoundUnit(T)(auto ref T[] tokens, bool inParens = false)
433         if (is(T : Token))
434     {
435         RTQ ret = parseExponentUnit(tokens);
436         if (tokens.empty || (inParens && tokens.front.type == Tok.rparen))
437             return ret;
438 
439         do {
440             tokens.check();
441             auto cur = tokens.front;
442 
443             bool multiply = true;
444             if (cur.type == Tok.div)
445                 multiply = false;
446 
447             if (cur.type == Tok.mul || cur.type == Tok.div)
448             {
449                 tokens.advance();
450                 tokens.check();
451                 cur = tokens.front;
452             }
453 
454             RTQ rhs = parseExponentUnit(tokens);
455             if (multiply)
456             {
457                 ret.dimensions = ret.dimensions.binop!"*"(rhs.dimensions);
458                 ret.value = ret.value * rhs.value;
459             }
460             else
461             {
462                 ret.dimensions = ret.dimensions.binop!"/"(rhs.dimensions);
463                 ret.value = ret.value / rhs.value;
464             }
465 
466             if (tokens.empty || (inParens && tokens.front.type == Tok.rparen))
467                 break;
468 
469             cur = tokens.front;
470         }
471         while (!tokens.empty);
472 
473         return ret;
474     }
475 
476     RTQ parseExponentUnit(T)(auto ref T[] tokens)
477         if (is(T : Token))
478     {
479         RTQ ret = parseUnit(tokens);
480 
481         if (tokens.empty)
482             return ret;
483 
484         auto next = tokens.front;
485         if (next.type != Tok.exp && next.type != Tok.supinteger)
486             return ret;
487 
488         if (next.type == Tok.exp)
489             tokens.advance(Tok.integer);
490 
491         int n = parseInteger(tokens);
492 
493         static if (__traits(compiles, std.math.pow(ret.value, n)))
494             ret.value = std.math.pow(ret.value, n);
495         else
496             foreach (i; 1 .. n)
497                 ret.value *= ret.value;
498         ret.dimensions = ret.dimensions.exp(n);
499         return ret;
500     }
501 
502     int parseInteger(T)(auto ref T[] tokens)
503         if (is(T : Token))
504     {
505         tokens.check(Tok.integer, Tok.supinteger);
506         int n = tokens.front.integer;
507         if (tokens.length)
508             tokens.advance();
509         return n;
510     }
511 
512     RTQ parseUnit(T)(auto ref T[] tokens)
513         if (is(T : Token))
514     {
515         RTQ ret;
516 
517         if (tokens.front.type == Tok.lparen)
518         {
519             tokens.advance();
520             ret = parseCompoundUnit(tokens, true);
521             tokens.check(Tok.rparen);
522             tokens.advance();
523         }
524         else
525             ret = parsePrefixUnit(tokens);
526 
527         return ret;
528     }
529 
530     RTQ parsePrefixUnit(T)(auto ref T[] tokens)
531         if (is(T : Token))
532     {
533         tokens.check(Tok.symbol);
534         auto str = tokens.front.slice;
535         if (tokens.length)
536             tokens.advance();
537 
538         // Try a standalone unit symbol (no prefix)
539         auto uptr = str in symbolList.units;
540         if (uptr)
541             return *uptr;
542 
543         // Try with prefixes, the longest prefix first
544         N* factor;
545         for (size_t i = symbolList.maxPrefixLength; i > 0; i--)
546         {
547             if (str.length >= i)
548             {
549                 string prefix = str[0 .. i].to!string;
550                 factor = prefix in symbolList.prefixes;
551                 if (factor)
552                 {
553                     string unit = str[i .. $].to!string;
554                     enforceEx!ParsingException(unit.length, "Expecting a unit after the prefix " ~ prefix);
555                     uptr = unit in symbolList.units;
556                     if (uptr)
557                         return RTQ(*factor * uptr.value, uptr.dimensions);
558                 }
559             }
560         }
561 
562         throw new ParsingException("Unknown unit symbol: '%s'".format(str));
563     }
564 }
565 
566 // Convert a compile-time quantity to its runtime equivalent.
567 auto toRT(Q)(Q quantity)
568     if (isQuantity!Q)
569 {
570     return RTQuantity!(Q.valueType)(quantity.rawValue, toAA!(Q.dimensions));
571 }
572 
573 enum Tok
574 {
575     none,
576     symbol,
577     mul,
578     div,
579     exp,
580     integer,
581     supinteger,
582     rparen,
583     lparen
584 }
585 
586 struct Token
587 {
588     Tok type;
589     string slice;
590     int integer = int.max;
591 }
592 
593 enum ctSupIntegerMap = [
594     '⁰':'0',
595     '¹':'1',
596     '²':'2',
597     '³':'3',
598     '⁴':'4',
599     '⁵':'5',
600     '⁶':'6',
601     '⁷':'7',
602     '⁸':'8',
603     '⁹':'9',
604     '⁺':'+',
605     '⁻':'-'
606 ];
607 static __gshared dchar[dchar] supIntegerMap;
608 shared static this()
609 {
610     supIntegerMap = ctSupIntegerMap;
611 }
612 
613 Token[] lex(string input)
614 {
615     enum State
616     {
617         none,
618         symbol,
619         integer,
620         supinteger
621     }
622 
623     Token[] tokens;
624     if (!__ctfe)
625         tokens.reserve(input.length);
626 
627     auto original = input;
628     size_t i, j;
629     State state = State.none;
630 
631     void pushToken(Tok type)
632     {
633         tokens ~= Token(type, original[i .. j]);
634         i = j;
635         state = State.none;
636     }
637 
638     void pushInteger(Tok type)
639     {
640         auto slice = original[i .. j];
641 
642         if (type == Tok.supinteger)
643         {
644             if (__ctfe)
645                 slice = translate(slice, ctSupIntegerMap);
646             else
647                 slice = translate(slice, supIntegerMap);
648         }
649 
650         int n;
651         try
652             n = std.conv.parse!int(slice);
653         catch (Exception)
654             throw new ParsingException("Unexpected integer format: " ~ original[i .. j]);
655 
656         enforceEx!ParsingException(slice.empty, "Unexpected integer format: " ~ slice);
657 
658         tokens ~= Token(type, original[i .. j], n);
659         i = j;
660         state = State.none;
661     }
662 
663     void push()
664     {
665         if (state == State.symbol)
666             pushToken(Tok.symbol);
667         else if (state == State.integer)
668             pushInteger(Tok.integer);
669         else if (state == State.supinteger)
670             pushInteger(Tok.supinteger);
671     }
672 
673     while (!input.empty)
674     {
675         auto cur = input.front;
676         auto len = cur.codeLength!char;
677         switch (cur)
678         {
679             // Whitespace
680             case ' ':
681             case '\t':
682             case '\u00A0':
683             case '\u2000': .. case '\u200A':
684             case '\u202F':
685             case '\u205F':
686                 push();
687                 j += len;
688                 i = j;
689                 break;
690 
691             case '(':
692                 push();
693                 j += len;
694                 pushToken(Tok.lparen);
695                 break;
696 
697             case ')':
698                 push();
699                 j += len;
700                 pushToken(Tok.rparen);
701                 break;
702 
703             case '*':
704             case '.':
705             case '⋅':
706             case '×':
707                 push();
708                 j += len;
709                 pushToken(Tok.mul);
710                 break;
711 
712             case '/':
713             case '÷':
714                 push();
715                 j += len;
716                 pushToken(Tok.div);
717                 break;
718 
719             case '^':
720                 push();
721                 j += len;
722                 pushToken(Tok.exp);
723                 break;
724 
725             case '0': .. case '9':
726             case '-':
727             case '+':
728                 if (state != State.integer)
729                     push();
730                 state = State.integer;
731                 j += len;
732                 break;
733 
734             case '⁰':
735             case '¹':
736             case '²':
737             case '³':
738             case '⁴':
739             case '⁵':
740             case '⁶':
741             case '⁷':
742             case '⁸':
743             case '⁹':
744             case '⁻':
745             case '⁺':
746                 if (state != State.supinteger)
747                     push();
748                 state = State.supinteger;
749                 j += len;
750                 break;
751 
752             default:
753                 if (state == State.integer || state == State.supinteger)
754                     push();
755                 state = State.symbol;
756                 j += len;
757                 break;
758         }
759         input.popFront();
760     }
761     push();
762     return tokens;
763 }
764 
765 void advance(Types...)(ref Token[] tokens, Types types)
766 {
767     enforceEx!ParsingException(!tokens.empty, "Unexpected end of input");
768     tokens.popFront();
769 
770     static if (Types.length)
771         check(tokens, types);
772 }
773 
774 void check(Types...)(Token[] tokens, Types types)
775 {
776     enforceEx!ParsingException(!tokens.empty, "Unexpected end of input");
777     auto token = tokens.front;
778 
779     static if (Types.length)
780     {
781         bool ok = false;
782         Tok[] valid = [types];
783         foreach (type; types)
784         {
785             if (token.type == type)
786             {
787                 ok = true;
788                 break;
789             }
790         }
791         import std.string : format;
792         enforceEx!ParsingException(ok, valid.length > 1
793                                    ? format("Found '%s' while expecting one of [%(%s, %)]", token.slice, valid)
794                                    : format("Found '%s' while expecting %s", token.slice, valid.front)
795                                    );
796     }
797 }
798 
799 // Mul or div two dimension arrays
800 int[string] binop(string op)(int[string] dim1, int[string] dim2)
801 {
802     static assert(op == "*" || op == "/", "Unsupported dimension operator: " ~ op);
803 
804     int[string] result;
805 
806     // Clone these dimensions in the result
807     if (__ctfe)
808     {
809         foreach (key; dim1.keys)
810             result[key] = dim1[key];
811     }
812     else
813         result = dim1.dup;
814 
815     // Merge the other dimensions
816     foreach (sym, pow; dim2)
817     {
818         enum powop = op == "*" ? "+" : "-";
819 
820         if (sym in dim1)
821         {
822             // A dimension is common between this one and the other:
823             // add or sub them
824             auto p = mixin("dim1[sym]" ~ powop ~ "pow");
825 
826             // If the power becomes 0, remove the dimension from the list
827             // otherwise, set the new power
828             if (p == 0)
829                 result.remove(sym);
830             else
831                 result[sym] = p;
832         }
833         else
834         {
835             // Add this new dimensions to the result
836             // (with a negative power if op == "/")
837             result[sym] = mixin(powop ~ "pow");
838         }
839     }
840 
841     return result;
842 }
843 
844 // Raise a dimension array to a integer power (value)
845 int[string] exp(int[string] dim, int value)
846 {
847     if (value == 0)
848         return null;
849 
850     int[string] result;
851     foreach (sym, pow; dim)
852         result[sym] = pow * value;
853     return result;
854 }
855 
856 // Raise a dimension array to a rational power (1/value)
857 int[string] expInv(int[string] dim, int value)
858 {
859     assert(value > 0, "Bug: using Dimensions.expInv with a value <= 0");
860 
861     int[string] result;
862     foreach (sym, pow; dim)
863     {
864         enforce(pow % value == 0, "Operation results in a non-integral dimension");
865         result[sym] = pow / value;
866     }
867     return result;
868 }
869 
870 // Returns the string representation of a dimension array
871 string dimstr(int[string] dim)
872 {
873     import std.algorithm : filter;
874     import std.array : join;
875     import std.conv : to;
876     
877     static string stringize(string base, int power)
878     {
879         if (power == 0)
880             return null;
881         if (power == 1)
882             return base;
883         return base ~ "^" ~ to!string(power);
884     }
885     
886     string[] dimstrs;
887     foreach (sym, pow; dim)
888         dimstrs ~= stringize(sym, pow);
889 
890     return "%-(%s %)".format(dimstrs.filter!"a !is null");
891 }
892 
893