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 }