The OpenD Programming Language

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 }