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