/++ A simplified version of `std.conv` with better error messages and faster compiles for supported types. History: Added May 22, 2025 +/ module arsd.conv; static import arsd.core; // FIXME: thousands separator for int to string (and float to string) // FIXME: intToStringArgs // FIXME: floatToStringArgs /++ Converts a string into the other given type. Throws on failure. +/ T to(T)(scope const(char)[] str) { static if(is(T : long)) { // FIXME: unsigned? overflowing? radix? keep reading or stop on invalid char? StringToIntArgs args; args.unsigned = __traits(isUnsigned, T); long v = stringToInt(str, args); T ret = cast(T) v; if(ret != v) throw new StringToIntConvException("overflow", 0, str.idup, 0); return ret; } else static if(is(T : double)) { import core.stdc.stdlib; import core.stdc.errno; arsd.core.CharzBuffer z = str; char* end; errno = 0; double res = strtod(z.ptr, &end); if(end !is (z.ptr + z.length) || errno) { string msg = errno == ERANGE ? "Over/underflow" : "Invalid input"; throw new StringToIntConvException(msg, 10, str.idup, end - z.ptr); } return res; } else { static assert(0, "Unsupported type: " ~ T.stringof); } } /++ 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. +/ string to(T:string, From)(From value) { static if(is(From == enum)) return arsd.core.enumNameForValue(value); else return arsd.core.toStringInternal(value); } /+ T to(T, F)(F value) if(!is(F : const(char)[])) { // if the language allows implicit conversion, let it do its thing static if(is(T : F)) { return value; } else // integral type conversions do checked things static if(is(T : long) && is(F : long)) { return checkedConversion!T(value); } else // array to array conversion: try to convert the individual elements, allocating a new return value. static if(is(T : TE[], TE) && is(F : FE[], FE)) { F ret = new F(value.length); foreach(i, e; value) ret[i] = to!TE(e); return ret; } else static assert(0, "Unsupported conversion types"); } +/ unittest { assert(to!int("5") == 5); assert(to!int("35") == 35); assert(to!string(35) == "35"); assert(to!int("0xA35d") == 0xA35d); assert(to!int("0b11001001") == 0b11001001); assert(to!int("0o777") == 511 /*0o777*/); assert(to!ubyte("255") == 255); assert(to!ulong("18446744073709551615") == ulong.max); void expectedToThrow(T...)(lazy T items) { int count; string messages; static foreach(idx, item; items) { try { auto result = item; if(messages.length) messages ~= ","; messages ~= idx.stringof[0..$-2]; } catch(StringToIntConvException e) { // passed the test; it was supposed to throw. // arsd.core.writeln(e); count++; } } assert(count == T.length, "Arg(s) " ~ messages ~ " did not throw"); } expectedToThrow( to!uint("-44"), // negative number to unsigned reuslt to!int("add"), // invalid base 10 chars to!byte("129"), // wrapped to negative to!int("0p4a0"), // invalid radix prefix to!int("5000000000"), // doesn't fit in int to!ulong("6000000000000000000900"), // overflow when reading into the ulong buffer ); } /++ +/ class ValueOutOfRangeException : arsd.core.ArsdExceptionBase { this(string type, long userSuppliedValue, long minimumAcceptableValue, long maximumAcceptableValue, string file = __FILE__, size_t line = __LINE__) { this.type = type; this.userSuppliedValue = userSuppliedValue; this.minimumAcceptableValue = minimumAcceptableValue; this.maximumAcceptableValue = maximumAcceptableValue; super("Value was out of range", file, line); } string type; long userSuppliedValue; long minimumAcceptableValue; long maximumAcceptableValue; override void getAdditionalPrintableInformation(scope void delegate(string name, in char[] value) sink) const { sink("type", type); sink("userSuppliedValue", arsd.core.toStringInternal(userSuppliedValue)); sink("minimumAcceptableValue", arsd.core.toStringInternal(minimumAcceptableValue)); sink("maximumAcceptableValue", arsd.core.toStringInternal(maximumAcceptableValue)); } } /++ +/ class StringToIntConvException : arsd.core.ArsdExceptionBase /*InvalidDataException*/ { this(string msg, int radix, string userInput, size_t offset, string file = __FILE__, size_t line = __LINE__) { this.radix = radix; this.userInput = userInput; this.offset = offset; super(msg, file, line); } override void getAdditionalPrintableInformation(scope void delegate(string name, in char[] value) sink) const { sink("radix", arsd.core.toStringInternal(radix)); sink("userInput", arsd.core.toStringInternal(userInput)); if(offset < userInput.length) sink("offset", arsd.core.toStringInternal(offset) ~ " ('" ~ userInput[offset] ~ "')"); } /// int radix; /// string userInput; /// size_t offset; } /++ if radix is 0, guess from 0o, 0x, 0b prefixes. +/ long stringToInt(scope const(char)[] str, StringToIntArgs args = StringToIntArgs.init) { long accumulator; auto original = str; Exception exception(string msg, size_t loopOffset = 0, string file = __FILE__, size_t line = __LINE__) { return new StringToIntConvException(msg, args.radix, original.dup, loopOffset + str.ptr - original.ptr, file, line); } if(str.length == 0) throw exception("empty string"); bool isNegative; if(str[0] == '-') { if(args.unsigned) throw exception("negative number given, but unsigned result desired"); isNegative = true; str = str[1 .. $]; } if(str.length == 0) throw exception("just a dash"); if(str[0] == '0') { if(str.length > 1 && (str[1] == 'b' || str[1] == 'x' || str[1] == 'o')) { if(args.radix != 0) { throw exception("string had specified base, but the radix arg was already supplied"); } switch(str[1]) { case 'b': args.radix = 2; break; case 'o': args.radix = 8; break; case 'x': args.radix = 16; break; default: assert(0); } str = str[2 .. $]; if(str.length == 0) throw exception("just a prefix"); } } if(args.radix == 0) args.radix = 10; foreach(idx, char ch; str) { if(ch && ch == args.ignoredSeparator) continue; auto before = accumulator; accumulator *= args.radix; int value = -1; if(ch >= '0' && ch <= '9') { value = ch - '0'; } else { ch |= 32; if(ch >= 'a' && ch <= 'z') value = ch - 'a' + 10; } if(value < 0) throw exception("invalid char", idx); if(value >= args.radix) throw exception("invalid char for given radix", idx); accumulator += value; if(args.unsigned) { auto b = cast(ulong) before; auto a = cast(ulong) accumulator; if(a < b) throw exception("value too big to fit in unsigned buffer", idx); } else { if(accumulator < before && !args.unsigned) throw exception("value too big to fit in signed buffer", idx); } } if(isNegative) accumulator = -accumulator; return accumulator; } /// ditto struct StringToIntArgs { int radix; bool unsigned; char ignoredSeparator = 0; } /++ Converts two integer types, returning the min/max of the desired type if the given value is out of range for it. +/ T saturatingConversion(T)(long value) { static assert(is(T : long), "Only works on integer types"); static if(is(T == ulong)) // the special case to try to handle the full range there ulong mv = cast(ulong) value; else long mv = value; if(mv > T.max) return T.max; else if(value < T.min) return T.min; else return cast(T) value; } unittest { assert(saturatingConversion!ubyte(256) == 255); assert(saturatingConversion!byte(256) == 127); assert(saturatingConversion!byte(-256) == -128); assert(saturatingConversion!ulong(0) == 0); assert(saturatingConversion!long(-5) == -5); assert(saturatingConversion!uint(-5) == 0); // assert(saturatingConversion!ulong(-5) == 0); // it can't catch this since the -5 is indistinguishable from the large ulong value here } /++ Truncates off bits that won't fit; equivalent to a built-in cast operation (you can just use a cast instead if you want). +/ T truncatingConversion(T)(long value) { static assert(is(T : long), "Only works on integer types"); return cast(T) value; } /++ Converts two integer types, throwing an exception if the given value is out of range for it. +/ T checkedConversion(T)(long value, long minimumAcceptableValue = T.min, long maximumAcceptableValue = T.max) { static assert(is(T : long), "Only works on integer types"); if(value > maximumAcceptableValue) throw new ValueOutOfRangeException(T.stringof, value, minimumAcceptableValue, maximumAcceptableValue); else if(value < minimumAcceptableValue) throw new ValueOutOfRangeException(T.stringof, value, minimumAcceptableValue, maximumAcceptableValue); else return cast(T) value; } /// ditto T checkedConversion(T:ulong)(ulong value, ulong minimumAcceptableValue = T.min, ulong maximumAcceptableValue = T.max) { if(value > maximumAcceptableValue) throw new ValueOutOfRangeException(T.stringof, value, minimumAcceptableValue, maximumAcceptableValue); else if(value < minimumAcceptableValue) throw new ValueOutOfRangeException(T.stringof, value, minimumAcceptableValue, maximumAcceptableValue); else return cast(T) value; } unittest { try { assert(checkedConversion!byte(155)); assert(0); } catch(ValueOutOfRangeException e) { } }