1 /++
2     This module contains functions that in one way or another converts its
3     arguments into something else.
4 
5     Credit for [Enum] goes to Stephan Koch (https://github.com/UplinkCoder).
6 
7     Example:
8     ---
9     enum SomeEnum { one, two, three };
10 
11     SomeEnum foo = Enum!SomeEnum.fromString("one");
12     SomeEnum bar = Enum!SomeEnum.fromString("three");
13 
14     assert(foo == SomeEnum.one);
15     assert(bar == SomeEnum.three);
16     ---
17  +/
18 module lu.conv;
19 
20 private:
21 
22 import std.range.primitives : isOutputRange;
23 import std.traits : isIntegral;
24 import std.typecons : Flag, No, Yes;
25 
26 public:
27 
28 @safe:
29 
30 
31 // Enum
32 /++
33     Template housing optimised functions to get the string name of an enum
34     member, or the enum member of a name string.
35 
36     [std.conv.to] is typically the go-to for this job; however it quickly bloats
37     the binary and is not performant on larger enums.
38 
39     Params:
40         E = enum to base this template on.
41  +/
42 template Enum(E)
43 if (is(E == enum))
44 {
45     // fromString
46     /++
47         Takes the member of an enum by string and returns that enum member.
48 
49         It lowers to a big switch of the enum member strings. It is faster than
50         [std.conv.to] and generates less template bloat. However, it does not work
51         with enums where multiple members share the same values, as the big switch
52         ends up getting duplicate cases.
53 
54         Taken from: https://forum.dlang.org/post/bfnwstkafhfgihavtzsz@forum.dlang.org
55         written by Stephan Koch (https://github.com/UplinkCoder).
56         Used with permission.
57 
58         Example:
59         ---
60         enum SomeEnum { one, two, three };
61 
62         SomeEnum foo = Enum!SomeEnum.fromString("one");
63         SomeEnum bar = Enum!SomeEnum.fromString("three");
64 
65         assert(foo == SomeEnum.one);
66         assert(bar == SomeEnum.three);
67         ---
68 
69         Params:
70             enumstring = the string name of an enum member.
71 
72         Returns:
73             The enum member whose name matches the enumstring string (not whose
74             *value* matches the string).
75 
76         Throws: [std.conv.ConvException|ConvException] if no matching enum member with the
77             passed name could be found.
78 
79         Bugs:
80             Does not work with enums that have members with duplicate values.
81      +/
82     E fromString(const string enumstring) pure
83     {
84         enum enumSwitch = ()
85         {
86             string enumSwitch = "import std.conv : ConvException;\n" ~
87                 "with (E) switch (enumstring)\n{\n";
88 
89             foreach (immutable memberstring; __traits(allMembers, E))
90             {
91                 enumSwitch ~= `case "` ~ memberstring ~ `": return ` ~ memberstring ~ ";\n";
92             }
93 
94             enumSwitch ~= "default:\n" ~
95                 "    //import std.traits : fullyQualifiedName;\n" ~
96                 `    throw new ConvException("No such " ~ E.stringof ~ ": " ~ enumstring);` ~ "\n}";
97 
98             return enumSwitch;
99         }();
100 
101         mixin(enumSwitch);
102     }
103 
104 
105     // toString
106     /++
107         The inverse of [fromString], this function takes an enum member value
108         and returns its string identifier.
109 
110         It lowers to a big switch of the enum members. It is faster than
111         [std.conv.to] and generates less template bloat.
112 
113         Taken from: https://forum.dlang.org/post/bfnwstkafhfgihavtzsz@forum.dlang.org
114         written by Stephan Koch (https://github.com/UplinkCoder).
115         Used with permission.
116 
117         Example:
118         ---
119         enum SomeEnum { one, two, three };
120 
121         string foo = Enum!SomeEnum.toString(one);
122         assert(foo == "one");
123         ---
124 
125         Params:
126             value = Enum member whose string name we want.
127 
128         Returns:
129             The string name of the passed enum member.
130      +/
131     string toString(E value) pure nothrow
132     {
133         switch (value)
134         {
135 
136         foreach (immutable m; __traits(allMembers, E))
137         {
138             case mixin("E." ~ m) : return m;
139         }
140 
141         default:
142             string result = "cast(" ~ E.stringof ~ ")";
143             uint val = value;
144             enum headLength = E.stringof.length + "cast()".length;
145 
146             immutable log10Val =
147                 (val < 10) ? 0 :
148                 (val < 100) ? 1 :
149                 (val < 1_000) ? 2 :
150                 (val < 10_000) ? 3 :
151                 (val < 100_000) ? 4 :
152                 (val < 1_000_000) ? 5 :
153                 (val < 10_000_000) ? 6 :
154                 (val < 100_000_000) ? 7 :
155                 (val < 1_000_000_000) ? 8 : 9;
156 
157             result.length += log10Val + 1;
158 
159             for (uint i; i != log10Val + 1; ++i)
160             {
161                 cast(char)result[headLength + log10Val - i] = cast(char)('0' + (val % 10));
162                 val /= 10;
163             }
164 
165             return result;
166         }
167     }
168 }
169 
170 ///
171 @system
172 unittest
173 {
174     import std.conv : ConvException;
175     import std.exception  : assertThrown;
176 
177     enum T
178     {
179         UNSET,
180         QUERY,
181         PRIVMSG,
182         RPL_ENDOFMOTD
183     }
184 
185     with (T)
186     {
187         static assert(Enum!T.fromString("QUERY") == QUERY);
188         static assert(Enum!T.fromString("PRIVMSG") == PRIVMSG);
189         static assert(Enum!T.fromString("RPL_ENDOFMOTD") == RPL_ENDOFMOTD);
190         static assert(Enum!T.fromString("UNSET") == UNSET);
191         assertThrown!ConvException(Enum!T.fromString("DOESNTEXIST"));  // needs @system
192     }
193 
194     with (T)
195     {
196         static assert(Enum!T.toString(QUERY) == "QUERY");
197         static assert(Enum!T.toString(PRIVMSG) == "PRIVMSG");
198         static assert(Enum!T.toString(RPL_ENDOFMOTD) == "RPL_ENDOFMOTD");
199     }
200 }
201 
202 
203 // numFromHex
204 /++
205     Returns the decimal value of a hex number in string form.
206 
207     Example:
208     ---
209     int fifteen = numFromHex("F");
210     int twofiftyfive = numFromHex("FF");
211     ---
212 
213     Params:
214         hex = Hexadecimal number in string form.
215         acceptLowercase = Flag of whether or not to accept `rrggbb` in lowercase form.
216 
217     Returns:
218         An integer equalling the value of the passed hexadecimal string.
219 
220     Throws: [std.conv.ConvException|ConvException] if the hex string was malformed.
221  +/
222 auto numFromHex(
223     const string hex,
224     const Flag!"acceptLowercase" acceptLowercase = Yes.acceptLowercase) pure
225 out (total; (total < 16^^hex.length), "`numFromHex` output is too large")
226 {
227     import std.string : representation;
228 
229     int val = -1;
230     int total;
231 
232     foreach (immutable c; hex.representation)
233     {
234         switch (c)
235         {
236         case '0':
237         ..
238         case '9':
239             val = (c - 48);
240             goto case 'F';
241 
242         case 'a':
243         ..
244         case 'f':
245             if (acceptLowercase)
246             {
247                 val = (c - (55+32));
248                 goto case 'F';
249             }
250             else
251             {
252                 goto default;
253             }
254 
255         case 'A':
256         ..
257         case 'F':
258             if (val < 0) val = (c - 55);
259             total *= 16;
260             total += val;
261             val = -1;
262             break;
263 
264         default:
265             import std.conv : ConvException;
266             throw new ConvException("Invalid hex string: " ~ hex);
267         }
268     }
269 
270     return total;
271 }
272 
273 
274 // rgbFromHex
275 /++
276     Convenience wrapper that takes a hex string and populates a Voldemort
277     struct with its RR, GG and BB components.
278 
279     This is to be used when mapping a `#RRGGBB` colour to their decimal
280     red/green/blue equivalents.
281 
282     Params:
283         hexString = Hexadecimal number (colour) in string form.
284         acceptLowercase = Whether or not to accept the `rrggbb` string in
285             lowercase letters.
286 
287     Returns:
288         A Voldemort struct with `r`, `g` and `b` members,
289  +/
290 auto rgbFromHex(
291     const string hexString,
292     const Flag!"acceptLowercase" acceptLowercase = No.acceptLowercase)
293 {
294     struct RGB
295     {
296         int r;
297         int g;
298         int b;
299     }
300 
301     RGB rgb;
302     immutable hex = (hexString[0] == '#') ? hexString[1..$] : hexString;
303 
304     rgb.r = numFromHex(hex[0..2], acceptLowercase);
305     rgb.g = numFromHex(hex[2..4], acceptLowercase);
306     rgb.b = numFromHex(hex[4..$], acceptLowercase);
307 
308     return rgb;
309 }
310 
311 ///
312 unittest
313 {
314     import std.conv : text;
315     {
316         auto rgb = rgbFromHex("000102");
317 
318         assert((rgb.r == 0), rgb.r.text);
319         assert((rgb.g == 1), rgb.g.text);
320         assert((rgb.b == 2), rgb.b.text);
321     }
322     {
323         auto rgb = rgbFromHex("#FFFFFF");
324 
325         assert((rgb.r == 255), rgb.r.text);
326         assert((rgb.g == 255), rgb.g.text);
327         assert((rgb.b == 255), rgb.b.text);
328     }
329     {
330         auto rgb = rgbFromHex("#3C507D");
331 
332         assert((rgb.r == 60), rgb.r.text);
333         assert((rgb.g == 80), rgb.b.text);
334         assert((rgb.b == 125), rgb.b.text);
335     }
336     {
337         auto rgb = rgbFromHex("9a4B7c", Yes.acceptLowercase);
338 
339         assert((rgb.r == 154), rgb.r.text);
340         assert((rgb.g == 75), rgb.g.text);
341         assert((rgb.b == 124), rgb.b.text);
342     }
343 }
344 
345 
346 // toAlphaInto
347 /++
348     Translates an integer into an alphanumeric string. Assumes ASCII.
349     Overload that takes an output range sink.
350 
351     Example:
352     ---
353     Appender!(char[]) sink;
354     int num = 12345;
355     num.toAlphaInto(sink);
356     assert(sink.data == "12345");
357     assert(sink.data == num.to!string);
358     ---
359 
360     Params:
361         maxDigits = The maximum number of digits to expect input of.
362         leadingZeroes = The minimum amount of leading zeroes to include in the
363             output, mirroring the format specifier "`%0nd`".
364         num = Integer to translate into string.
365         sink = Output range sink.
366  +/
367 void toAlphaInto(size_t maxDigits = 19, uint leadingZeroes = 0, Num, Sink)
368     (const Num num, auto ref Sink sink)
369 if (isIntegral!Num && isOutputRange!(Sink, char[]))
370 {
371     static assert((maxDigits >= leadingZeroes), "Cannot pass more leading zeroes " ~
372         "than max digits to `toAlphaInto`");
373 
374     if (num == 0)
375     {
376         static if (leadingZeroes > 0)
377         {
378             foreach (immutable i; 0..leadingZeroes)
379             {
380                 sink.put('0');
381             }
382         }
383         else
384         {
385             sink.put('0');
386         }
387         return;
388     }
389     else if (num < 0)
390     {
391         sink.put('-');
392     }
393 
394     static if (leadingZeroes > 0)
395     {
396         // Need default-initialised fields to be zeroes
397         ubyte[maxDigits] digits;
398     }
399     else
400     {
401         ubyte[maxDigits] digits = void;
402     }
403 
404     size_t pos;
405 
406     for (Num window = num; window != 0; window /= 10)
407     {
408         import std.math : abs;
409         digits[pos++] = cast(ubyte)abs(window % 10);
410     }
411 
412     static if (leadingZeroes > 0)
413     {
414         import std.algorithm.comparison : max;
415         size_t startingPos = max(leadingZeroes, pos);
416     }
417     else
418     {
419         alias startingPos = pos;
420     }
421 
422     foreach_reverse (immutable digit; digits[0..startingPos])
423     {
424         sink.put(cast(char)(digit + '0'));
425     }
426 }
427 
428 ///
429 unittest
430 {
431     import std.array : Appender;
432 
433     Appender!(char[]) sink;
434 
435     {
436         enum num = 123_456;
437         num.toAlphaInto(sink);
438         assert((sink.data == "123456"), sink.data);
439         sink.clear();
440     }
441     {
442         enum num = 0;
443         num.toAlphaInto(sink);
444         assert((sink.data == "0"), sink.data);
445         sink.clear();
446     }
447     {
448         enum num = 999;
449         num.toAlphaInto(sink);
450         assert((sink.data == "999"), sink.data);
451         sink.clear();
452     }
453     {
454         enum num = -987;
455         num.toAlphaInto(sink);
456         assert((sink.data == "-987"), sink.data);
457         sink.clear();
458     }
459     {
460         enum num = 123;
461         num.toAlphaInto!(12, 6)(sink);
462         assert((sink.data == "000123"), sink.data);
463         sink.clear();
464     }
465     {
466         enum num = -1;
467         num.toAlphaInto!(3, 3)(sink);
468         assert((sink.data == "-001"), sink.data);
469         sink.clear();
470     }
471     {
472         enum num = -123_456_789_012_345L;
473         num.toAlphaInto!15(sink);
474         assert((sink.data == "-123456789012345"), sink.data);
475         sink.clear();
476     }
477     {
478         enum num = long.min;
479         num.toAlphaInto(sink);
480         assert((sink.data == "-9223372036854775808"), sink.data);
481         //sink.clear();
482     }
483 }
484 
485 
486 // toAlpha
487 /++
488     Translates an integer into an alphanumeric string. Assumes ASCII.
489     Overload that returns the string.
490 
491     Merely leverages [toAlphaInto].
492 
493     Example:
494     ---
495     int num = 12345;
496     string asString = num.toAlpha;
497     assert(asString == "12345");
498     assert(asString == num.to!string);
499     ---
500 
501     Params:
502         maxDigits = The maximum number of digits to expect input of.
503         leadingZeroes = The minimum amount of leading zeroes to include in the
504             output, mirroring the format specifier "`%0nd`".
505         num = Integer to translate into string.
506 
507     Returns:
508         The passed integer `num` in string form.
509  +/
510 string toAlpha(size_t maxDigits = 19, uint leadingZeroes = 0, Num)(const Num num) pure
511 {
512     import std.array : Appender;
513 
514     Appender!(char[]) sink;
515     sink.reserve((num >= 0) ? maxDigits : maxDigits+1);
516     num.toAlphaInto!(maxDigits, leadingZeroes, Num)(sink);
517     return sink.data;
518 }
519 
520 ///
521 unittest
522 {
523     {
524         enum num = 123_456;
525         immutable translated = num.toAlpha;
526         assert((translated == "123456"), translated);
527     }
528     {
529         enum num = 0;
530         immutable translated = num.toAlpha;
531         assert((translated == "0"), translated);
532     }
533     {
534         enum num = 999;
535         immutable translated = num.toAlpha;
536         assert((translated == "999"), translated);
537     }
538     {
539         enum num = -987;
540         immutable translated = num.toAlpha;
541         assert((translated == "-987"), translated);
542     }
543     {
544         enum num = 123;
545         immutable translated = num.toAlpha!(12, 6);
546         assert((translated == "000123"), translated);
547     }
548     {
549         enum num = -1;
550         immutable translated = num.toAlpha!(3, 3);
551         assert((translated == "-001"), translated);
552     }
553     {
554         enum num = -123_456_789_012_345L;
555         immutable translated = num.toAlpha!15;
556         assert((translated == "-123456789012345"), translated);
557     }
558     {
559         enum num = long.min;
560         immutable translated = num.toAlpha;
561         assert((translated == "-9223372036854775808"), translated);
562     }
563 }