1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 
5 /**
6  * This module provides functions for converting between different types.
7  *
8  * Copyright: Eugene Wissner 2017-2020.
9  * License: $(LINK2 https://www.mozilla.org/en-US/MPL/2.0/,
10  *                  Mozilla Public License, v. 2.0).
11  * Authors: $(LINK2 mailto:info@caraus.de, Eugene Wissner)
12  * Source: $(LINK2 https://github.com/caraus-ecms/tanya/blob/master/source/tanya/conv.d,
13  *                 tanya/conv.d)
14  */
15 module tanya.conv;
16 
17 import std.traits : Unsigned;
18 import tanya.container..string;
19 import tanya.memory.allocator;
20 import tanya.meta.trait;
21 import tanya.meta.transform;
22 import tanya.range;
23 
24 /**
25  * Thrown if a type conversion fails.
26  */
27 final class ConvException : Exception
28 {
29     /**
30      * Params:
31      *  msg  = The message for the exception.
32      *  file = The file where the exception occurred.
33      *  line = The line number where the exception occurred.
34      *  next = The previous exception in the chain of exceptions, if any.
35      */
36     this(string msg,
37          string file = __FILE__,
38          size_t line = __LINE__,
39          Throwable next = null) @nogc @safe pure nothrow
40     {
41         super(msg, file, line, next);
42     }
43 }
44 
45 /*
46  * Converts a string $(D_PARAM range) into an integral value of type
47  * $(D_PARAM T) in $(D_PARAM base).
48  *
49  * The convertion stops when $(D_PARAM range) is empty of if the next character
50  * cannot be converted because it is not a digit (with respect to the
51  * $(D_PARAM base)) or if the reading the next character would cause integer
52  * overflow. The function returns the value converted so far then. The front
53  * element of the $(D_PARAM range) points to the first character cannot be
54  * converted or $(D_PARAM range) is empty if the whole string could be
55  * converted.
56  *
57  * Base must be between 2 and 36 inclursive. Default base is 10.
58  *
59  * The function doesn't handle the sign (+ or -) or number prefixes (like 0x).
60  */
61 package T readIntegral(T, R)(ref R range, const ubyte base = 10)
62 if (isInputRange!R
63         && isSomeChar!(ElementType!R)
64         && isIntegral!T
65         && isUnsigned!T)
66 in
67 {
68     assert(base >= 2);
69     assert(base <= 36);
70 }
71 do
72 {
73     T boundary = cast(T) (T.max / base);
74     if (range.empty)
75     {
76         return T.init;
77     }
78 
79     T n;
80     int digit;
81     do
82     {
83         if (range.front >= 'a')
84         {
85             digit = range.front - 'W';
86         }
87         else if (range.front >= 'A' && range.front <= 'Z')
88         {
89             digit = range.front - '7';
90         }
91         else if (range.front >= '0' && range.front <= '9')
92         {
93             digit = range.front - '0';
94         }
95         else
96         {
97             return n;
98         }
99         if (digit >= base)
100         {
101             return n;
102         }
103         n = cast(T) (n * base + digit);
104         range.popFront();
105 
106         if (range.empty)
107         {
108             return n;
109         }
110     }
111     while (n < boundary);
112 
113     if (range.front >= 'a')
114     {
115         digit = range.front - 'W';
116     }
117     else if (range.front >= 'A')
118     {
119         digit = range.front - '7';
120     }
121     else if (range.front >= '0')
122     {
123         digit = range.front - '0';
124     }
125     else
126     {
127         return n;
128     }
129     if (n > cast(T) ((T.max - digit) / base))
130     {
131         return n;
132     }
133     n = cast(T) (n * base + digit);
134     range.popFront();
135 
136     return n;
137 }
138 
139 /**
140  * If the source type $(D_PARAM From) and the target type $(D_PARAM To) are
141  * equal, does nothing. If $(D_PARAM From) can be implicitly converted to
142  * $(D_PARAM To), just returns $(D_PARAM from).
143  *
144  * Params:
145  *  To = Target type.
146  *
147  * Returns: $(D_PARAM from).
148  */
149 template to(To)
150 {
151     /**
152      * Params:
153      *  From = Source type.
154      *  from = Source value.
155      */
156     ref To to(From)(ref From from)
157     if (is(To == From))
158     {
159         return from;
160     }
161 
162     /// ditto
163     To to(From)(From from)
164     if (is(Unqual!To == Unqual!From) || (isNumeric!From && isFloatingPoint!To))
165     {
166         return from;
167     }
168 }
169 
170 ///
171 @nogc nothrow pure @safe unittest
172 {
173     auto val = 5.to!int();
174     assert(val == 5);
175     static assert(is(typeof(val) == int));
176 }
177 
178 /**
179  * Performs checked conversion from an integral type $(D_PARAM From) to an
180  * integral type $(D_PARAM To).
181  *
182  * Params:
183  *  From = Source type.
184  *  To   = Target type.
185  *  from = Source value.
186  *
187  * Returns: $(D_PARAM from) converted to $(D_PARAM To).
188  *
189  * Throws: $(D_PSYMBOL ConvException) if $(D_PARAM from) is too small or too
190  *         large to be represented by $(D_PARAM To).
191  */
192 To to(To, From)(From from)
193 if (isIntegral!From
194  && isIntegral!To
195  && !is(Unqual!To == Unqual!From)
196  && !is(To == enum))
197 {
198     static if ((isUnsigned!From && isSigned!To && From.sizeof == To.sizeof)
199             || From.sizeof > To.sizeof)
200     {
201         if (from > To.max)
202         {
203             throw make!ConvException(defaultAllocator,
204                                      "Positive integer overflow");
205         }
206     }
207     static if (isSigned!From)
208     {
209         static if (isUnsigned!To)
210         {
211             if (from < 0)
212             {
213                 throw make!ConvException(defaultAllocator,
214                                          "Negative integer overflow");
215             }
216         }
217         else static if (From.sizeof > To.sizeof)
218         {
219             if (from < To.min)
220             {
221                 throw make!ConvException(defaultAllocator,
222                                          "Negative integer overflow");
223             }
224         }
225     }
226     static if (From.sizeof <= To.sizeof)
227     {
228         return from;
229     }
230     else static if (isSigned!To)
231     {
232         return cast(To) from;
233     }
234     else
235     {
236         return from & To.max;
237     }
238 }
239 
240 /**
241  * Converts a floating point number to an integral type.
242  *
243  * Params:
244  *  From = Source type.
245  *  To   = Target type.
246  *  from = Source value.
247  *
248  * Returns: Truncated $(D_PARAM from) (everything after the decimal point is
249  *          dropped).
250  *
251  * Throws: $(D_PSYMBOL ConvException) if
252  *         $(D_INLINECODE from < To.min || from > To.max).
253  */
254 To to(To, From)(From from)
255 if (isFloatingPoint!From
256  && isIntegral!To
257  && !is(Unqual!To == Unqual!From)
258  && !is(To == enum))
259 {
260     if (from > To.max)
261     {
262         throw make!ConvException(defaultAllocator,
263                                  "Positive number overflow");
264     }
265     else if (from < To.min)
266     {
267         throw make!ConvException(defaultAllocator,
268                                  "Negative number overflow");
269     }
270     return cast(To) from;
271 }
272 
273 ///
274 @nogc pure @safe unittest
275 {
276     assert(1.5.to!int == 1);
277     assert(2147483646.5.to!int == 2147483646);
278     assert((-2147483647.5).to!int == -2147483647);
279     assert(2147483646.5.to!uint == 2147483646);
280 }
281 
282 /**
283  * Performs checked conversion from an integral type $(D_PARAM From) to an
284  * $(D_KEYWORD enum).
285  *
286  * Params:
287  *  From = Source type.
288  *  To   = Target type.
289  *  from = Source value.
290  *
291  * Returns: $(D_KEYWORD enum) value.
292  *
293  * Throws: $(D_PSYMBOL ConvException) if $(D_PARAM from) is not a member of
294  *         $(D_PSYMBOL To).
295  */
296 To to(To, From)(From from)
297 if (isIntegral!From && is(To == enum))
298 {
299     foreach (m; EnumMembers!To)
300     {
301         if (from == m)
302         {
303             return m;
304         }
305     }
306     throw make!ConvException(defaultAllocator,
307                              "Value not found in enum '" ~ To.stringof ~ "'");
308 }
309 
310 ///
311 @nogc pure @safe unittest
312 {
313     enum Test : int
314     {
315         one,
316         two,
317     }
318     static assert(is(typeof(1.to!Test) == Test));
319     assert(0.to!Test == Test.one);
320     assert(1.to!Test == Test.two);
321 }
322 
323 /**
324  * Converts $(D_PARAM from) to a boolean.
325  *
326  * If $(D_PARAM From) is a numeric type, then `1` becomes $(D_KEYWORD true),
327  * `0` $(D_KEYWORD false). Otherwise $(D_PSYMBOL ConvException) is thrown.
328  *
329  * If $(D_PARAM To) is a string (built-in string or $(D_PSYMBOL String)),
330  * then `"true"` or `"false"` are converted to the appropriate boolean value.
331  * Otherwise $(D_PSYMBOL ConvException) is thrown.
332  *
333  * Params:
334  *  From = Source type.
335  *  To   = Target type.
336  *  from = Source value.
337  *
338  * Returns: $(D_KEYWORD from) converted to a boolean.
339  *
340  * Throws: $(D_PSYMBOL ConvException) if $(D_PARAM from) isn't convertible.
341  */
342 To to(To, From)(From from)
343 if (isNumeric!From && is(Unqual!To == bool) && !is(Unqual!To == Unqual!From))
344 {
345     if (from == 0)
346     {
347         return false;
348     }
349     else if (from < 0)
350     {
351         throw make!ConvException(defaultAllocator,
352                                  "Negative number overflow");
353     }
354     else if (from <= 1)
355     {
356         return true;
357     }
358     throw make!ConvException(defaultAllocator,
359                              "Positive number overflow");
360 }
361 
362 ///
363 @nogc pure @safe unittest
364 {
365     assert(!0.0.to!bool);
366     assert(0.2.to!bool);
367     assert(0.5.to!bool);
368     assert(1.0.to!bool);
369 
370     assert(!0.to!bool);
371     assert(1.to!bool);
372 }
373 
374 /// ditto
375 To to(To, From)(auto ref const From from)
376 if ((is(From == String) || isSomeString!From) && is(Unqual!To == bool))
377 {
378     if (from == "true")
379     {
380         return true;
381     }
382     else if (from == "false")
383     {
384         return false;
385     }
386     throw make!ConvException(defaultAllocator,
387                              "String doesn't contain a boolean value");
388 }
389 
390 ///
391 @nogc pure @safe unittest
392 {
393     assert("true".to!bool);
394     assert(!"false".to!bool);
395     assert(String("true").to!bool);
396     assert(!String("false").to!bool);
397 
398 }
399 
400 /**
401  * Converts a boolean to $(D_PARAM To).
402  *
403  * If $(D_PARAM To) is a numeric type, then $(D_KEYWORD true) becomes `1`,
404  * $(D_KEYWORD false) `0`.
405  *
406  * If $(D_PARAM To) is a $(D_PSYMBOL String), then `"true"` or `"false"`
407  * is returned.
408  *
409  * Params:
410  *  From = Source type.
411  *  To   = Target type.
412  *  from = Source value.
413  *
414  * Returns: $(D_PARAM from) converted to $(D_PARAM To).
415  */
416 To to(To, From)(From from)
417 if (is(Unqual!From == bool) && isNumeric!To && !is(Unqual!To == Unqual!From))
418 {
419     return from;
420 }
421 
422 ///
423 @nogc nothrow pure @safe unittest
424 {
425     assert(true.to!float == 1.0);
426     assert(true.to!double == 1.0);
427     assert(true.to!ubyte == 1);
428     assert(true.to!byte == 1);
429     assert(true.to!ushort == 1);
430     assert(true.to!short == 1);
431     assert(true.to!uint == 1);
432     assert(true.to!int == 1);
433 
434     assert(false.to!float == 0);
435     assert(false.to!double == 0);
436     assert(false.to!ubyte == 0);
437     assert(false.to!byte == 0);
438     assert(false.to!ushort == 0);
439     assert(false.to!short == 0);
440     assert(false.to!uint == 0);
441     assert(false.to!int == 0);
442 }
443 
444 /**
445  * Converts a stringish range to an integral value.
446  *
447  * Params:
448  *  From = Source type.
449  *  To   = Target type.
450  *  from = Source value.
451  *
452  * Returns: $(D_PARAM from) converted to $(D_PARAM To).
453  *
454  * Throws: $(D_PSYMBOL ConvException) if $(D_PARAM from) doesn't contain an
455  *         integral value.
456  */
457 To to(To, From)(auto ref From from)
458 if (isInputRange!From && isSomeChar!(ElementType!From) && isIntegral!To)
459 {
460     if (from.empty)
461     {
462         throw make!ConvException(defaultAllocator, "Input range is empty");
463     }
464 
465     static if (isSigned!To)
466     {
467         bool negative;
468     }
469     if (from.front == '-')
470     {
471         static if (isUnsigned!To)
472         {
473             throw make!ConvException(defaultAllocator,
474                                      "Negative integer overflow");
475         }
476         else
477         {
478             negative = true;
479             from.popFront();
480         }
481     }
482 
483     if (from.empty)
484     {
485         throw make!ConvException(defaultAllocator, "Input range is empty");
486     }
487 
488     ubyte base = 10;
489     if (from.front == '0')
490     {
491         from.popFront();
492         if (from.empty)
493         {
494             return To.init;
495         }
496         else if (from.front == 'x' || from.front == 'X')
497         {
498             base = 16;
499             from.popFront();
500         }
501         else if (from.front == 'b' || from.front == 'B')
502         {
503             base = 2;
504             from.popFront();
505         }
506         else
507         {
508             base = 8;
509         }
510     }
511 
512     auto unsigned = readIntegral!(Unsigned!To, From)(from, base);
513     if (!from.empty)
514     {
515         throw make!ConvException(defaultAllocator, "Integer overflow");
516     }
517 
518     static if (isSigned!To)
519     {
520         if (negative)
521         {
522             auto predecessor = cast(Unsigned!To) (unsigned - 1);
523             if (predecessor > cast(Unsigned!To) To.max)
524             {
525                 throw make!ConvException(defaultAllocator,
526                                          "Negative integer overflow");
527             }
528             return cast(To) (-(cast(Largest!(To, ptrdiff_t)) predecessor) - 1);
529         }
530         else if (unsigned > cast(Unsigned!To) To.max)
531         {
532             throw make!ConvException(defaultAllocator, "Integer overflow");
533         }
534         else
535         {
536             return unsigned;
537         }
538     }
539     else
540     {
541         return unsigned;
542     }
543 }
544 
545 ///
546 @nogc pure @safe unittest
547 {
548     assert("1234".to!uint() == 1234);
549     assert("1234".to!int() == 1234);
550     assert("1234".to!int() == 1234);
551 
552     assert("0".to!int() == 0);
553     assert("-0".to!int() == 0);
554 
555     assert("0x10".to!int() == 16);
556     assert("0X10".to!int() == 16);
557     assert("-0x10".to!int() == -16);
558 
559     assert("0b10".to!int() == 2);
560     assert("0B10".to!int() == 2);
561     assert("-0b10".to!int() == -2);
562 
563     assert("010".to!int() == 8);
564     assert("-010".to!int() == -8);
565 
566     assert("-128".to!byte == cast(byte) -128);
567 }