1 /++ 2 A simplified version of `std.conv` with better error messages and faster compiles for supported types. 3 4 History: 5 Added May 22, 2025 6 +/ 7 module arsd.conv; 8 9 static import arsd.core; 10 11 // FIXME: thousands separator for int to string (and float to string) 12 // FIXME: intToStringArgs 13 // FIXME: floatToStringArgs 14 15 /++ 16 Converts a string into the other given type. Throws on failure. 17 +/ 18 T to(T)(scope const(char)[] str) { 19 static if(is(T == enum)) { 20 switch(str) { 21 default: 22 throw new EnumConvException(T.stringof, str.idup); 23 foreach(memberName; __traits(allMembers, T)) 24 case memberName: 25 return __traits(getMember, T, memberName); 26 } 27 28 } 29 else 30 static if(is(T : long)) { 31 // FIXME: unsigned? overflowing? radix? keep reading or stop on invalid char? 32 StringToIntArgs args; 33 args.unsigned = __traits(isUnsigned, T); 34 long v = stringToInt(str, args); 35 T ret = cast(T) v; 36 if(ret != v) 37 throw new StringToIntConvException("overflow", 0, str.idup, 0); 38 return ret; 39 } 40 else 41 static if(is(T : double)) { 42 import core.stdc.stdlib; 43 import core.stdc.errno; 44 arsd.core.CharzBuffer z = str; 45 char* end; 46 errno = 0; 47 double res = strtod(z.ptr, &end); 48 if(end !is (z.ptr + z.length) || errno) { 49 string msg = errno == ERANGE ? "Over/underflow" : "Invalid input"; 50 throw new StringToIntConvException(msg, 10, str.idup, end - z.ptr); 51 } 52 53 return res; 54 } 55 else 56 { 57 static assert(0, "Unsupported type: " ~ T.stringof); 58 } 59 } 60 61 /++ 62 Converts any given value to a string. The format of the string is unspecified; it is meant for a human reader and might be overridden by types. 63 +/ 64 string to(T:string, From)(From value) { 65 static if(is(From == enum)) 66 return arsd.core.enumNameForValue(value); 67 else 68 return arsd.core.toStringInternal(value); 69 } 70 71 /++ 72 Converts ints to other types of ints or enums 73 +/ 74 T to(T)(long value) { 75 static if(is(T == enum)) 76 return cast(T) value; // FIXME check if the value is actually in range 77 else 78 return checkedConversion!T(value); 79 } 80 81 /+ 82 T to(T, F)(F value) if(!is(F : const(char)[])) { 83 // if the language allows implicit conversion, let it do its thing 84 static if(is(T : F)) { 85 return value; 86 } 87 else 88 // integral type conversions do checked things 89 static if(is(T : long) && is(F : long)) { 90 return checkedConversion!T(value); 91 } 92 else 93 // array to array conversion: try to convert the individual elements, allocating a new return value. 94 static if(is(T : TE[], TE) && is(F : FE[], FE)) { 95 F ret = new F(value.length); 96 foreach(i, e; value) 97 ret[i] = to!TE(e); 98 return ret; 99 } 100 else 101 static assert(0, "Unsupported conversion types"); 102 } 103 +/ 104 105 unittest { 106 assert(to!int("5") == 5); 107 assert(to!int("35") == 35); 108 assert(to!string(35) == "35"); 109 assert(to!int("0xA35d") == 0xA35d); 110 assert(to!int("0b11001001") == 0b11001001); 111 assert(to!int("0o777") == 511 /*0o777*/); 112 113 assert(to!ubyte("255") == 255); 114 assert(to!ulong("18446744073709551615") == ulong.max); 115 116 void expectedToThrow(T...)(lazy T items) { 117 int count; 118 string messages; 119 static foreach(idx, item; items) { 120 try { 121 auto result = item; 122 if(messages.length) 123 messages ~= ","; 124 messages ~= idx.stringof[0..$-2]; 125 } catch(StringToIntConvException e) { 126 // passed the test; it was supposed to throw. 127 // arsd.core.writeln(e); 128 count++; 129 } 130 } 131 132 assert(count == T.length, "Arg(s) " ~ messages ~ " did not throw"); 133 } 134 135 expectedToThrow( 136 to!uint("-44"), // negative number to unsigned reuslt 137 to!int("add"), // invalid base 10 chars 138 to!byte("129"), // wrapped to negative 139 to!int("0p4a0"), // invalid radix prefix 140 to!int("5000000000"), // doesn't fit in int 141 to!ulong("6000000000000000000900"), // overflow when reading into the ulong buffer 142 ); 143 } 144 145 /++ 146 147 +/ 148 class ConvException : arsd.core.ArsdExceptionBase { 149 this(string msg, string file, size_t line) { 150 super(msg, file, line); 151 } 152 } 153 154 /++ 155 156 +/ 157 class ValueOutOfRangeException : ConvException { 158 this(string type, long userSuppliedValue, long minimumAcceptableValue, long maximumAcceptableValue, string file = __FILE__, size_t line = __LINE__) { 159 this.type = type; 160 this.userSuppliedValue = userSuppliedValue; 161 this.minimumAcceptableValue = minimumAcceptableValue; 162 this.maximumAcceptableValue = maximumAcceptableValue; 163 super("Value was out of range", file, line); 164 } 165 166 string type; 167 long userSuppliedValue; 168 long minimumAcceptableValue; 169 long maximumAcceptableValue; 170 171 override void getAdditionalPrintableInformation(scope void delegate(string name, in char[] value) sink) const { 172 sink("type", type); 173 sink("userSuppliedValue", arsd.core.toStringInternal(userSuppliedValue)); 174 sink("minimumAcceptableValue", arsd.core.toStringInternal(minimumAcceptableValue)); 175 sink("maximumAcceptableValue", arsd.core.toStringInternal(maximumAcceptableValue)); 176 } 177 } 178 179 /++ 180 181 +/ 182 class EnumConvException : ConvException { 183 this(string type, string userSuppliedValue, string file = __FILE__, size_t line = __LINE__) { 184 this.type = type; 185 this.userSuppliedValue = userSuppliedValue; 186 187 super("No such enum value", file, line); 188 189 } 190 string type; 191 string userSuppliedValue; 192 193 override void getAdditionalPrintableInformation(scope void delegate(string name, in char[] value) sink) const { 194 sink("type", type); 195 sink("userSuppliedValue", userSuppliedValue); 196 } 197 } 198 199 unittest { 200 enum A { a, b, c } 201 // to!A("d"); 202 } 203 204 205 /++ 206 207 +/ 208 class StringToIntConvException : arsd.core.ArsdExceptionBase /*InvalidDataException*/ { 209 this(string msg, int radix, string userInput, size_t offset, string file = __FILE__, size_t line = __LINE__) { 210 this.radix = radix; 211 this.userInput = userInput; 212 this.offset = offset; 213 214 super(msg, file, line); 215 } 216 217 override void getAdditionalPrintableInformation(scope void delegate(string name, in char[] value) sink) const { 218 sink("radix", arsd.core.toStringInternal(radix)); 219 sink("userInput", arsd.core.toStringInternal(userInput)); 220 if(offset < userInput.length) 221 sink("offset", arsd.core.toStringInternal(offset) ~ " ('" ~ userInput[offset] ~ "')"); 222 223 } 224 225 /// 226 int radix; 227 /// 228 string userInput; 229 /// 230 size_t offset; 231 } 232 233 /++ 234 if radix is 0, guess from 0o, 0x, 0b prefixes. 235 +/ 236 long stringToInt(scope const(char)[] str, StringToIntArgs args = StringToIntArgs.init) { 237 long accumulator; 238 239 auto original = str; 240 241 Exception exception(string msg, size_t loopOffset = 0, string file = __FILE__, size_t line = __LINE__) { 242 return new StringToIntConvException(msg, args.radix, original.dup, loopOffset + str.ptr - original.ptr, file, line); 243 } 244 245 if(str.length == 0) 246 throw exception("empty string"); 247 248 bool isNegative; 249 if(str[0] == '-') { 250 if(args.unsigned) 251 throw exception("negative number given, but unsigned result desired"); 252 253 isNegative = true; 254 str = str[1 .. $]; 255 } 256 257 if(str.length == 0) 258 throw exception("just a dash"); 259 260 if(str[0] == '0') { 261 if(str.length > 1 && (str[1] == 'b' || str[1] == 'x' || str[1] == 'o')) { 262 if(args.radix != 0) { 263 throw exception("string had specified base, but the radix arg was already supplied"); 264 } 265 266 switch(str[1]) { 267 case 'b': 268 args.radix = 2; 269 break; 270 case 'o': 271 args.radix = 8; 272 break; 273 case 'x': 274 args.radix = 16; 275 break; 276 default: 277 assert(0); 278 } 279 280 str = str[2 .. $]; 281 282 if(str.length == 0) 283 throw exception("just a prefix"); 284 } 285 } 286 287 if(args.radix == 0) 288 args.radix = 10; 289 290 foreach(idx, char ch; str) { 291 292 if(ch && ch == args.ignoredSeparator) 293 continue; 294 295 auto before = accumulator; 296 297 accumulator *= args.radix; 298 299 int value = -1; 300 if(ch >= '0' && ch <= '9') { 301 value = ch - '0'; 302 } else { 303 ch |= 32; 304 if(ch >= 'a' && ch <= 'z') 305 value = ch - 'a' + 10; 306 } 307 308 if(value < 0) 309 throw exception("invalid char", idx); 310 if(value >= args.radix) 311 throw exception("invalid char for given radix", idx); 312 313 accumulator += value; 314 if(args.unsigned) { 315 auto b = cast(ulong) before; 316 auto a = cast(ulong) accumulator; 317 if(a < b) 318 throw exception("value too big to fit in unsigned buffer", idx); 319 } else { 320 if(accumulator < before && !args.unsigned) 321 throw exception("value too big to fit in signed buffer", idx); 322 } 323 } 324 325 if(isNegative) 326 accumulator = -accumulator; 327 328 return accumulator; 329 } 330 331 /// ditto 332 struct StringToIntArgs { 333 int radix; 334 bool unsigned; 335 char ignoredSeparator = 0; 336 } 337 338 /++ 339 Converts two integer types, returning the min/max of the desired type if the given value is out of range for it. 340 +/ 341 T saturatingConversion(T)(long value) { 342 static assert(is(T : long), "Only works on integer types"); 343 344 static if(is(T == ulong)) // the special case to try to handle the full range there 345 ulong mv = cast(ulong) value; 346 else 347 long mv = value; 348 349 if(mv > T.max) 350 return T.max; 351 else if(value < T.min) 352 return T.min; 353 else 354 return cast(T) value; 355 } 356 357 unittest { 358 assert(saturatingConversion!ubyte(256) == 255); 359 assert(saturatingConversion!byte(256) == 127); 360 assert(saturatingConversion!byte(-256) == -128); 361 362 assert(saturatingConversion!ulong(0) == 0); 363 assert(saturatingConversion!long(-5) == -5); 364 365 assert(saturatingConversion!uint(-5) == 0); 366 367 // assert(saturatingConversion!ulong(-5) == 0); // it can't catch this since the -5 is indistinguishable from the large ulong value here 368 } 369 370 /++ 371 Truncates off bits that won't fit; equivalent to a built-in cast operation (you can just use a cast instead if you want). 372 +/ 373 T truncatingConversion(T)(long value) { 374 static assert(is(T : long), "Only works on integer types"); 375 376 return cast(T) value; 377 378 } 379 380 /++ 381 Converts two integer types, throwing an exception if the given value is out of range for it. 382 +/ 383 T checkedConversion(T)(long value, long minimumAcceptableValue = T.min, long maximumAcceptableValue = T.max) { 384 static assert(is(T : long), "Only works on integer types"); 385 386 if(value > maximumAcceptableValue) 387 throw new ValueOutOfRangeException(T.stringof, value, minimumAcceptableValue, maximumAcceptableValue); 388 else if(value < minimumAcceptableValue) 389 throw new ValueOutOfRangeException(T.stringof, value, minimumAcceptableValue, maximumAcceptableValue); 390 else 391 return cast(T) value; 392 } 393 /// ditto 394 T checkedConversion(T:ulong)(ulong value, ulong minimumAcceptableValue = T.min, ulong maximumAcceptableValue = T.max) { 395 if(value > maximumAcceptableValue) 396 throw new ValueOutOfRangeException(T.stringof, value, minimumAcceptableValue, maximumAcceptableValue); 397 else if(value < minimumAcceptableValue) 398 throw new ValueOutOfRangeException(T.stringof, value, minimumAcceptableValue, maximumAcceptableValue); 399 else 400 return cast(T) value; 401 } 402 403 unittest { 404 try { 405 assert(checkedConversion!byte(155)); 406 assert(0); 407 } catch(ValueOutOfRangeException e) { 408 409 } 410 }