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!double))
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!(double, "C");
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!(BigInt, "bit");
230     alias BinarySize = typeof(bit);
231 
232     SymbolList!BigInt symbolList;
233     symbolList.addUnit("bit", bit);
234     symbolList.addPrefix("hob", 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 hobbit"));
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         value = parseFun(str);
338     catch
339         value = 1;
340 
341     if (str.empty)
342         return RTQuantity!N(value, null);
343 
344     auto input = str.to!string;
345     auto tokens = lex(input);
346     auto parser = QuantityParser!N(symbolList);
347     
348     RTQuantity!N result = parser.parseCompoundUnit(tokens);
349     result.value *= value;
350     return result;
351 }
352 
353 unittest // Test parsing
354 {
355     enum meter = unit!(double, "L");
356     enum kilogram = unit!(double, "M");
357     enum second = unit!(double, "T");
358     enum one = meter / meter;
359 
360     enum siSL = makeSymbolList!double(
361         withUnit("m", meter),
362         withUnit("kg", kilogram),
363         withUnit("s", second),
364         withPrefix("c", 0.01L),
365         withPrefix("m", 0.001L)
366     );
367 
368     static bool checkParse(Q)(string input, Q quantity)
369     {
370         return parseRTQuantity!(double, std.conv.parse!(double, string))(input, siSL)
371             == quantity.toRT;
372     }
373 
374     assert(checkParse("1    m    ", meter));
375     assert(checkParse("1 mm", 0.001 * meter));
376     assert(checkParse("1 m^-1", 1 / meter));
377     assert(checkParse("1 m²", meter * meter));
378     assert(checkParse("1 m⁺²", meter * meter));
379     assert(checkParse("1 m⁻¹", 1 / meter));
380     assert(checkParse("1 (m)", meter));
381     assert(checkParse("1 (m^-1)", 1 / meter));
382     assert(checkParse("1 ((m)^-1)^-1", meter));
383     assert(checkParse("1 m*m", meter * meter));
384     assert(checkParse("1 m m", meter * meter));
385     assert(checkParse("1 m.m", meter * meter));
386     assert(checkParse("1 m⋅m", meter * meter));
387     assert(checkParse("1 m×m", meter * meter));
388     assert(checkParse("1 m/m", meter / meter));
389     assert(checkParse("1 m÷m", meter / meter));
390     assert(checkParse("1 m.s", second * meter));
391     assert(checkParse("1 m s", second * meter));
392     assert(checkParse("1 m*m/m", meter));
393     assert(checkParse("0.8", 0.8 * one));
394 
395     assertThrown!ParsingException(checkParse("1 c m", meter * meter));
396     assertThrown!ParsingException(checkParse("1 c", 0.01 * meter));
397     assertThrown!ParsingException(checkParse("1 Qm", meter));
398     assertThrown!ParsingException(checkParse("1 m/", meter));
399     assertThrown!ParsingException(checkParse("1 m^", meter));
400     assertThrown!ParsingException(checkParse("1 m ) m", meter * meter));
401     assertThrown!ParsingException(checkParse("1 m * m) m", meter * meter * meter));
402     assertThrown!ParsingException(checkParse("1 m^²", meter * meter));
403     assertThrown!ParsingException(checkParse("1-⁺⁵", one));
404 }
405 
406 // Holds a value and a dimensions for parsing
407 struct RTQuantity(N)
408 {
409     // The payload
410     N value;
411 
412     // The dimensions of the quantity
413     int[string] dimensions;
414 }
415 
416 // A parser that can parse a text for a unit or a quantity
417 struct QuantityParser(N)
418 {
419     alias RTQ = RTQuantity!N;
420 
421     private SymbolList!N symbolList;
422 
423     RTQ parseCompoundUnit(T)(auto ref T[] tokens, bool inParens = false)
424         if (is(T : Token))
425     {
426         RTQ ret = parseExponentUnit(tokens);
427         if (tokens.empty || (inParens && tokens.front.type == Tok.rparen))
428             return ret;
429 
430         do {
431             tokens.check();
432             auto cur = tokens.front;
433 
434             bool multiply = true;
435             if (cur.type == Tok.div)
436                 multiply = false;
437 
438             if (cur.type == Tok.mul || cur.type == Tok.div)
439             {
440                 tokens.advance();
441                 tokens.check();
442                 cur = tokens.front;
443             }
444 
445             RTQ rhs = parseExponentUnit(tokens);
446             if (multiply)
447             {
448                 ret.dimensions = ret.dimensions.binop!"*"(rhs.dimensions);
449                 ret.value = ret.value * rhs.value;
450             }
451             else
452             {
453                 ret.dimensions = ret.dimensions.binop!"/"(rhs.dimensions);
454                 ret.value = ret.value / rhs.value;
455             }
456 
457             if (tokens.empty || (inParens && tokens.front.type == Tok.rparen))
458                 break;
459 
460             cur = tokens.front;
461         }
462         while (!tokens.empty);
463 
464         return ret;
465     }
466 
467     RTQ parseExponentUnit(T)(auto ref T[] tokens)
468         if (is(T : Token))
469     {
470         RTQ ret = parseUnit(tokens);
471 
472         if (tokens.empty)
473             return ret;
474 
475         auto next = tokens.front;
476         if (next.type != Tok.exp && next.type != Tok.supinteger)
477             return ret;
478 
479         if (next.type == Tok.exp)
480             tokens.advance(Tok.integer);
481 
482         int n = parseInteger(tokens);
483 
484         static if (__traits(compiles, std.math.pow(ret.value, n)))
485             ret.value = std.math.pow(ret.value, n);
486         else
487             foreach (i; 1 .. n)
488                 ret.value *= ret.value;
489         ret.dimensions = ret.dimensions.exp(n);
490         return ret;
491     }
492 
493     int parseInteger(T)(auto ref T[] tokens)
494         if (is(T : Token))
495     {
496         tokens.check(Tok.integer, Tok.supinteger);
497         int n = tokens.front.integer;
498         if (tokens.length)
499             tokens.advance();
500         return n;
501     }
502 
503     RTQ parseUnit(T)(auto ref T[] tokens)
504         if (is(T : Token))
505     {
506         RTQ ret;
507 
508         if (tokens.front.type == Tok.lparen)
509         {
510             tokens.advance();
511             ret = parseCompoundUnit(tokens, true);
512             tokens.check(Tok.rparen);
513             tokens.advance();
514         }
515         else
516             ret = parsePrefixUnit(tokens);
517 
518         return ret;
519     }
520 
521     RTQ parsePrefixUnit(T)(auto ref T[] tokens)
522         if (is(T : Token))
523     {
524         tokens.check(Tok.symbol);
525         auto str = tokens.front.slice;
526         if (tokens.length)
527             tokens.advance();
528 
529         // Try a standalone unit symbol (no prefix)
530         auto uptr = str in symbolList.units;
531         if (uptr)
532             return *uptr;
533 
534         // Try with prefixes, the longest prefix first
535         N* factor;
536         for (size_t i = symbolList.maxPrefixLength; i > 0; i--)
537         {
538             if (str.length >= i)
539             {
540                 string prefix = str[0 .. i].to!string;
541                 factor = prefix in symbolList.prefixes;
542                 if (factor)
543                 {
544                     string unit = str[i .. $].to!string;
545                     enforceEx!ParsingException(unit.length, "Expecting a unit after the prefix " ~ prefix);
546                     uptr = unit in symbolList.units;
547                     if (uptr)
548                         return RTQ(*factor * uptr.value, uptr.dimensions);
549                 }
550             }
551         }
552 
553         throw new ParsingException("Unknown unit symbol: '%s'".format(str));
554     }
555 }
556 
557 // Convert a compile-time quantity to its runtime equivalent.
558 auto toRT(Q)(Q quantity)
559     if (isQuantity!Q)
560 {
561     return RTQuantity!(Q.valueType)(quantity.rawValue, toAA!(Q.dimensions));
562 }
563 
564 enum Tok
565 {
566     none,
567     symbol,
568     mul,
569     div,
570     exp,
571     integer,
572     supinteger,
573     rparen,
574     lparen
575 }
576 
577 struct Token
578 {
579     Tok type;
580     string slice;
581     int integer = int.max;
582 }
583 
584 enum ctSupIntegerMap = [
585     '⁰':'0',
586     '¹':'1',
587     '²':'2',
588     '³':'3',
589     '⁴':'4',
590     '⁵':'5',
591     '⁶':'6',
592     '⁷':'7',
593     '⁸':'8',
594     '⁹':'9',
595     '⁺':'+',
596     '⁻':'-'
597 ];
598 static dchar[dchar] supIntegerMap;
599 static this()
600 {
601     supIntegerMap = ctSupIntegerMap;
602 }
603 
604 Token[] lex(string input) @safe
605 {
606     enum State
607     {
608         none,
609         symbol,
610         integer,
611         supinteger
612     }
613 
614     Token[] tokens;
615     auto tokapp = appender(tokens); // Only for runtime
616 
617     void appendToken(Token token)
618     {
619         if (!__ctfe)
620             tokapp.put(token);
621         else
622             tokens ~= token;
623     }
624 
625     auto original = input;
626     size_t i, j;
627     State state = State.none;
628 
629     void pushToken(Tok type)
630     {
631         appendToken(Token(type, original[i .. j]));
632         i = j;
633         state = State.none;
634     }
635 
636     void pushInteger(Tok type)
637     {
638         auto slice = original[i .. j];
639 
640         if (type == Tok.supinteger)
641         {
642             if (__ctfe)
643                 slice = translate(slice, ctSupIntegerMap);
644             else
645                 slice = translate(slice, supIntegerMap);
646         }
647 
648         int n;
649         try
650             n = std.conv.parse!int(slice);
651         catch (Exception)
652             throw new ParsingException("Unexpected integer format: " ~ original[i .. j]);
653 
654         enforceEx!ParsingException(slice.empty, "Unexpected integer format: " ~ slice);
655 
656         appendToken(Token(type, original[i .. j], n));
657         i = j;
658         state = State.none;
659     }
660 
661     void push()
662     {
663         if (state == State.symbol)
664             pushToken(Tok.symbol);
665         else if (state == State.integer)
666             pushInteger(Tok.integer);
667         else if (state == State.supinteger)
668             pushInteger(Tok.supinteger);
669     }
670 
671     while (!input.empty)
672     {
673         auto cur = input.front;
674         auto len = cur.codeLength!char;
675         switch (cur)
676         {
677             // Whitespace
678             case ' ':
679             case '\t':
680             case '\u00A0':
681             case '\u2000': .. case '\u200A':
682             case '\u202F':
683             case '\u205F':
684                 push();
685                 j += len;
686                 i = j;
687                 break;
688 
689             case '(':
690                 push();
691                 j += len;
692                 pushToken(Tok.lparen);
693                 break;
694 
695             case ')':
696                 push();
697                 j += len;
698                 pushToken(Tok.rparen);
699                 break;
700 
701             case '*':
702             case '.':
703             case '⋅':
704             case '×':
705                 push();
706                 j += len;
707                 pushToken(Tok.mul);
708                 break;
709 
710             case '/':
711             case '÷':
712                 push();
713                 j += len;
714                 pushToken(Tok.div);
715                 break;
716 
717             case '^':
718                 push();
719                 j += len;
720                 pushToken(Tok.exp);
721                 break;
722 
723             case '0': .. case '9':
724             case '-':
725             case '+':
726                 if (state != State.integer)
727                     push();
728                 state = State.integer;
729                 j += len;
730                 break;
731 
732             case '⁰':
733             case '¹':
734             case '²':
735             case '³':
736             case '⁴':
737             case '⁵':
738             case '⁶':
739             case '⁷':
740             case '⁸':
741             case '⁹':
742             case '⁻':
743             case '⁺':
744                 if (state != State.supinteger)
745                     push();
746                 state = State.supinteger;
747                 j += len;
748                 break;
749 
750             default:
751                 if (state == State.integer || state == State.supinteger)
752                     push();
753                 state = State.symbol;
754                 j += len;
755                 break;
756         }
757         input.popFront();
758     }
759     push();
760 
761     if (!__ctfe)
762         return tokapp.data;
763     else
764         return tokens;
765 }
766 
767 void advance(Types...)(ref Token[] tokens, Types types)
768 {
769     enforceEx!ParsingException(!tokens.empty, "Unexpected end of input");
770     tokens.popFront();
771 
772     static if (Types.length)
773         check(tokens, types);
774 }
775 
776 void check(Types...)(Token[] tokens, Types types)
777 {
778     enforceEx!ParsingException(!tokens.empty, "Unexpected end of input");
779     auto token = tokens.front;
780 
781     static if (Types.length)
782     {
783         bool ok = false;
784         Tok[] valid = [types];
785         foreach (type; types)
786         {
787             if (token.type == type)
788             {
789                 ok = true;
790                 break;
791             }
792         }
793         import std.string : format;
794         enforceEx!ParsingException(ok, valid.length > 1
795                                    ? format("Found '%s' while expecting one of [%(%s, %)]", token.slice, valid)
796                                    : format("Found '%s' while expecting %s", token.slice, valid.front)
797                                    );
798     }
799 }
800 
801 // Mul or div two dimension arrays
802 int[string] binop(string op)(int[string] dim1, int[string] dim2)
803 {
804     static assert(op == "*" || op == "/", "Unsupported dimension operator: " ~ op);
805 
806     int[string] result;
807 
808     // Clone these dimensions in the result
809     if (__ctfe)
810     {
811         foreach (key; dim1.keys)
812             result[key] = dim1[key];
813     }
814     else
815         result = dim1.dup;
816 
817     // Merge the other dimensions
818     foreach (sym, pow; dim2)
819     {
820         enum powop = op == "*" ? "+" : "-";
821 
822         if (sym in dim1)
823         {
824             // A dimension is common between this one and the other:
825             // add or sub them
826             auto p = mixin("dim1[sym]" ~ powop ~ "pow");
827 
828             // If the power becomes 0, remove the dimension from the list
829             // otherwise, set the new power
830             if (p == 0)
831                 result.remove(sym);
832             else
833                 result[sym] = p;
834         }
835         else
836         {
837             // Add this new dimensions to the result
838             // (with a negative power if op == "/")
839             result[sym] = mixin(powop ~ "pow");
840         }
841     }
842 
843     return result;
844 }
845 
846 // Raise a dimension array to a integer power (value)
847 int[string] exp(int[string] dim, int value) @safe pure
848 {
849     if (value == 0)
850         return null;
851 
852     int[string] result;
853     foreach (sym, pow; dim)
854         result[sym] = pow * value;
855     return result;
856 }
857 
858 // Raise a dimension array to a rational power (1/value)
859 int[string] expInv(int[string] dim, int value) @safe pure
860 {
861     assert(value > 0, "Bug: using Dimensions.expInv with a value <= 0");
862 
863     int[string] result;
864     foreach (sym, pow; dim)
865     {
866         enforce(pow % value == 0, "Operation results in a non-integral dimension");
867         result[sym] = pow / value;
868     }
869     return result;
870 }