1 /// Module to replace std.conv 'to' function with similar but @nogc
2 /// Function in this module use TLS buffer to store string result, so returned strings are valid only to next usage of X->string conversion functions
3 module mutils.conv;
4 
5 import std.meta : NoDuplicates;
6 import std.traits;
7 
8 extern (C) int sscanf(scope const char* s, scope const char* format, ...) nothrow @nogc;
9 extern (C) int snprintf(scope char* s, size_t n, scope const char* format, ...) nothrow @nogc;
10 
11 auto min(A, B)(A a, B b) {
12 	return (a < b) ? a : b;
13 }
14 
15 static char[1024] gTmpStrBuff;
16 
17 /// Converts variable from to Type TO
18 /// strings are stored in default global buffer
19 TO to(TO, FROM)(auto ref const FROM from) {
20 	return to!(TO, FROM)(from, gTmpStrBuff);
21 }
22 
23 /// Converts variable from to Type TO
24 /// strings are stored in buff buffer
25 TO to(TO, FROM)(auto ref const FROM from, char[] buff) {
26 	static if (is(TO == FROM)) {
27 		return from;
28 	} else static if (is(TO == string)) {
29 		static if (is(FROM == bool)) {
30 			return bool2str(from, buff);
31 		} else static if (is(FROM == enum)) {
32 			return enum2str(from, buff);
33 		} else static if (isSIMDVector!FROM) {
34 			return slice2str(from.array, buff);
35 		} else static if (worksWithStr2Num!FROM) {
36 			return num2str(from, buff);
37 		} else static if (is(FROM == struct)) {
38 			return struct2str(from, buff);
39 		} else static if (isDynamicArray!(FROM)) {
40 			return slice2str(from, buff);
41 		} else static if (isStaticArray!(FROM)) {
42 			return slice2str(from[], buff);
43 		} else static if (isDelegate!(FROM) || isFunctionPointer!FROM) {
44 			return del2str(from, buff);
45 		} else static if (is(FROM == class)) {
46 			return num2str(cast(void*) from, buff);
47 		} else {
48 			static assert(0, "Type conversion not supported");
49 		}
50 	} else static if (is(FROM == string)) {
51 		FROM frm = from;
52 		static if (is(TO == enum)) {
53 			return str2enum!(TO)(frm);
54 		} else static if (worksWithStr2Num!TO) {
55 			return str2num!(TO)(frm);
56 		} else static if (is(TO == struct)) {
57 			return str2struct!(TO)(frm);
58 		} else {
59 			static assert(0, "Type conversion not supported");
60 		}
61 	}
62 
63 }
64 
65 /// Converts part of from string to TO variable
66 TO parse(TO)(auto ref string from) {
67 	static if (is(TO == enum)) {
68 		return str2enum!(TO)(from);
69 	} else static if (worksWithStr2Num!TO) {
70 		return str2num!(TO)(from);
71 	} else static if (is(TO == struct)) {
72 		return str2struct!(TO)(from);
73 	} else {
74 		static assert(0, "Type conversion not supported");
75 	}
76 
77 }
78 
79 nothrow @nogc unittest {
80 	// Convert to same value
81 	assert(10.to!int == 10);
82 	// Convert numbers
83 	assert("10".to!ubyte == 10);
84 	assert(10.to!string == "10");
85 
86 	// Convert enums
87 	assert((TestEnum.a).to!string == "a");
88 	assert("a".to!TestEnum == TestEnum.a);
89 
90 	// Convert structs
91 	TestStructA testStruct = "TestStructA(10, 10)".str2struct!TestStructA;
92 	assert(testStruct.a == 10);
93 	assert(testStruct.b == 10);
94 	assert(testStruct.to!string == "TestStructA(10, 10)");
95 }
96 
97 ///////////////////////  Convert numbers
98 
99 /// Converts number of type NUM to string and stores it in buff
100 /// Internally uses snprintf, string might be cut down to fit in buffer
101 /// To check for buffer overflow you might compare length of buff and returned string, if they are equal there might be not enought space in buffer
102 /// NULL char is always added at the end of the string
103 string num2str(FROM)(FROM from, char[] buff) {
104 	static assert(worksWithStr2Num!FROM, "num2str converts only numeric or pointer type to string");
105 	string sp = getSpecifier!(FROM);
106 	char[5] format;
107 	format[0] = '%';
108 	foreach (i, c; sp)
109 		format.ptr[1 + i] = c;
110 	format.ptr[1 + sp.length] = '\0';
111 	int takesCharsNum = snprintf(buff.ptr, buff.length, format.ptr, from);
112 	if (takesCharsNum < buff.length) {
113 		return cast(string) buff[0 .. takesCharsNum];
114 	} else {
115 		return cast(string) buff;
116 	}
117 }
118 
119 nothrow @nogc unittest {
120 	char[4] buff;
121 	assert(num2str(10, gTmpStrBuff[]) == "10");
122 	assert(num2str(-10, gTmpStrBuff[]) == "-10");
123 	assert(num2str(123456789, buff[]) == "123\0");
124 }
125 
126 /// Converts string to numeric type NUM
127 /// If string is malformed NUM.init is returned
128 NUM str2num(NUM)(auto ref string from) {
129 	static assert(worksWithStr2Num!NUM, "str2num converts string to numeric or pointer type");
130 	if (from.length == 0) {
131 		return NUM.init;
132 	}
133 	NUM ret = NUM.init;
134 	string sp = getSpecifier!(NUM);
135 	char[32] format;
136 	format[0] = '%';
137 	int takesCharsNum = snprintf(format.ptr + 1, format.length, "%d", cast(int) from.length);
138 	foreach (i, c; sp)
139 		format.ptr[1 + takesCharsNum + i] = c;
140 	format.ptr[1 + takesCharsNum + sp.length] = '%';
141 	format.ptr[2 + takesCharsNum + sp.length] = 'n';
142 	format.ptr[3 + takesCharsNum + sp.length] = '\0';
143 	int charsScanned = 0;
144 	sscanf(from.ptr, format.ptr, &ret, &charsScanned);
145 	from = from[charsScanned .. $];
146 	return ret;
147 }
148 
149 nothrow @nogc unittest {
150 	char[18] noEnd = "123456789123456789"; // Test without c string ending (\0)
151 	string empty;
152 	assert(empty.str2num!ubyte == 0);
153 	assert("".str2num!ubyte == 0);
154 	assert("asdaf".str2num!ubyte == 0);
155 	assert(str2num!int(cast(string) noEnd[0 .. 2]) == 12);
156 
157 	assert("10".str2num!ubyte == 10);
158 	assert("10".str2num!ushort == 10);
159 	assert("+10".str2num!uint == 10);
160 	assert("+10".str2num!ulong == 10);
161 
162 	assert("-10".str2num!byte == -10);
163 	assert("-10".str2num!short == -10);
164 	assert("-10".str2num!int == -10);
165 	assert("-10".str2num!long == -10);
166 }
167 
168 ///////////////////////  Convert bools
169 
170 /// Converts bool to string
171 string bool2str(T)(auto ref const T bbb, char[] buff) {
172 	static assert(is(T == bool), "T must be a boolean");
173 	enum string[2] strs = ["false", "true"];
174 	string name = strs[bbb];
175 	size_t toCopy = min(name.length, buff.length);
176 
177 	foreach (i, char c; name[0 .. toCopy]) {
178 		buff[i] = c;
179 	}
180 	return cast(string) buff[0 .. toCopy];
181 }
182 ///////////////////////  Convert enums
183 
184 /// Converts enum to string
185 /// If wrong enum value is specified "WrongEnum" string is returned
186 string enum2str(T)(auto ref const T en, char[] buff) {
187 	static assert(is(T == enum), "T must be an enum");
188 	switch (en) {
189 		foreach (i, e; NoDuplicates!(EnumMembers!T)) {
190 	case e:
191 			enum name = __traits(allMembers, T)[i];
192 			foreach (k, char c; name) {
193 				buff[k] = c;
194 			}
195 			return cast(string) buff[0 .. name.length];
196 		}
197 	default:
198 		return "WrongEnum";
199 
200 	}
201 }
202 
203 nothrow @nogc unittest {
204 	assert(enum2str(TestEnum.a, gTmpStrBuff) == "a");
205 	assert(enum2str(TestEnum.b, gTmpStrBuff) == "b");
206 	assert(enum2str(cast(TestEnum) 123, gTmpStrBuff) == "WrongEnum");
207 }
208 
209 /// Converts string to enum
210 /// If wrong string is specified max enum base type is returned ex. for: enum:ubyte E{} will return 255
211 T str2enum(T)(auto ref string str) {
212 	static assert(is(T == enum), "T must be an enum");
213 
214 	foreach (i, e; NoDuplicates!(EnumMembers!T)) {
215 		enum name = __traits(allMembers, T)[i];
216 		if (str.length >= name.length && str[0 .. name.length] == name) {
217 			str = str[name.length .. $];
218 			return e;
219 		}
220 	}
221 	return cast(T)(OriginalType!T).max; // Probably invalid enum
222 
223 }
224 
225 nothrow @nogc unittest {
226 	assert(str2enum!(TestEnum)("a") == TestEnum.a);
227 	assert(str2enum!(TestEnum)("b") == TestEnum.b);
228 	assert(str2enum!(TestEnum)("ttt") == byte.max);
229 }
230 
231 ///////////////////////  Convert slices
232 
233 /// Converts slice to string
234 /// Uses to!(string)(el, buff) to convert inner elements
235 /// If buff.length<=5 null is returned
236 /// If there is not enought space in the buffer, function converts as much as it coud with string "...]" at the end 
237 string slice2str(T)(auto ref const T slice, char[] buff) {
238 	alias EL = Unqual!(ForeachType!T);
239 
240 	if (buff.length <= 5) {
241 		return null;
242 	}
243 
244 	static if (is(EL == char)) {
245 		buff[$ - 4 .. $] = "...]";
246 		size_t lengthToCopy = min(slice.length, buff.length - 2);
247 		buff[0] = '"';
248 		buff[1 + lengthToCopy] = '"';
249 		buff[1 .. 1 + lengthToCopy] = slice[0 .. lengthToCopy];
250 		return cast(string) buff[0 .. lengthToCopy + 2];
251 	} else {
252 		buff[0] = '[';
253 
254 		char[] buffSlice = buff[1 .. $];
255 		foreach (ref el; slice) {
256 			string elStr = to!(string)(el, buffSlice);
257 			if (elStr.length + 2 >= buffSlice.length) {
258 				buff[$ - 4 .. $] = "...]";
259 				buffSlice = null;
260 				break;
261 			}
262 			buffSlice[elStr.length] = ',';
263 			buffSlice[elStr.length + 1] = ' ';
264 			buffSlice = buffSlice[elStr.length + 2 .. $];
265 		}
266 		if (buffSlice.length == 0) {
267 			return cast(string) buff;
268 		}
269 
270 		size_t size = buff.length - buffSlice.length;
271 		buff[size - 2] = ']';
272 		return cast(string) buff[0 .. size - 1];
273 	}
274 }
275 
276 nothrow @nogc unittest {
277 	char[10] bb;
278 	TestStructA[9] sl;
279 	int[9] ints = [1, 2, 3, 4, 5, 6, 7, 8, 9];
280 	assert(slice2str(ints[], gTmpStrBuff) == "[1, 2, 3, 4, 5, 6, 7, 8, 9]");
281 	assert(slice2str(sl[], bb[]) == "[TestS...]");
282 }
283 
284 ///////////////////////  Convert delegates and functions
285 ///
286 /// Converts delegate or function to string
287 string del2str(FROM)(FROM from, char[] buff) {
288 	static assert(isDelegate!FROM || isFunctionPointer!FROM,
289 			"del2str converts only delegates and functions to string");
290 	static if (isDelegate!FROM) {
291 		int takesCharsNum = snprintf(buff.ptr, buff.length,
292 				"delegate(obj: %p, func: %p)", from.ptr, from.funcptr);
293 	} else {
294 		int takesCharsNum = snprintf(buff.ptr, buff.length,
295 				"function(func: %p)", cast(void*) from);
296 	}
297 	if (takesCharsNum < buff.length) {
298 		return cast(string) buff[0 .. takesCharsNum];
299 	} else {
300 		return cast(string) buff;
301 	}
302 }
303 
304 nothrow @nogc unittest {
305 	static void func() {
306 	}
307 
308 	static void del() {
309 	}
310 
311 	assert(del2str(&func, gTmpStrBuff));
312 	assert(del2str(&del, gTmpStrBuff));
313 }
314 
315 ///////////////////////  Convert structs
316 
317 /// Converts structs to strings
318 /// Function is not using to!string so inner elements might be displayed differently ex. enums (they are displayeed as numbers)
319 /// Elements which cannot be converted are skipped
320 string struct2str(T)(auto ref const T s, char[] buff) {
321 	static assert(is(T == struct), "T must be a struct");
322 	if (buff.length < 3) {
323 		return null;
324 	}
325 	char[] sl = buff;
326 	enum name = T.stringof;
327 	size_t namePart = min(name.length, buff.length);
328 	sl[0 .. namePart] = name[0 .. namePart];
329 	sl = sl[namePart .. $];
330 	if (sl.length > 1) {
331 		sl[0] = '(';
332 		sl = sl[1 .. $];
333 	}
334 
335 	foreach (i, ref var; s.tupleof) {
336 		alias Type = typeof(var);
337 		static if (is(Type == struct) || worksWithStr2Num!Type) {
338 			if (sl.length <= 3) {
339 				break;
340 			}
341 			string str = to!string(var, sl);
342 			sl = sl[str.length .. $];
343 			if (sl.length < 2) {
344 				break;
345 			}
346 			if (i != s.tupleof.length - 1) {
347 				sl[0] = ',';
348 				sl[1] = ' ';
349 				sl = sl[2 .. $];
350 			}
351 		}
352 	}
353 	if (sl.length > 1) {
354 		sl[0] = ')';
355 		sl = sl[1 .. $];
356 	}
357 	return cast(string) buff[0 .. buff.length - sl.length];
358 }
359 
360 nothrow @nogc unittest {
361 	TestStructB test = TestStructB(2);
362 	assert(struct2str(test,
363 			gTmpStrBuff) == "TestStructB(TestStructA(1, 255), 2, 9223372036854775807, a)");
364 }
365 
366 /// Converts string to struct
367 /// string format is very strict, returns 0 initialized variable if string is bad
368 /// Works like struct2str but opposite
369 T str2struct(T)(auto ref string str) {
370 	static assert(is(T == struct), "T must be a struct");
371 
372 	union ZeroInit { // Init for @disable this() structs
373 		mixin("align(T.alignof) ubyte[T.sizeof] zeros;"); // Workaround for IDE parser error
374 		T s = void;
375 	}
376 
377 	ZeroInit var;
378 	if (str[$ - 1] != ')') {
379 		return var.s;
380 	}
381 
382 	string sl = str;
383 	enum name = T.stringof;
384 	assert(sl[0 .. name.length] == name);
385 	sl = sl[name.length .. $];
386 	assert(sl[0] == '(');
387 	sl = sl[1 .. $];
388 
389 	foreach (i, ref v; var.s.tupleof) {
390 		alias Type = typeof(v);
391 		static if (is(Type == struct) || worksWithStr2Num!Type) {
392 			v = parse!Type(sl);
393 			if (i != var.s.tupleof.length - 1) {
394 				sl = sl[2 .. $];
395 			}
396 		}
397 	}
398 	assert(sl[0] == ')');
399 	sl = sl[1 .. $];
400 	str = sl;
401 	return var.s;
402 }
403 
404 nothrow @nogc unittest {
405 	string loadFrom = "TestStructB(TestStructA(1, 255), 2, 9223372036854775807, b)";
406 	TestStructB test = str2struct!(TestStructB)(loadFrom);
407 	assert(test.a == 2);
408 	assert(test.b == 9223372036854775807);
409 	assert(test.en == TestEnum.b);
410 	assert(test.c.a == 1);
411 	assert(test.c.b == 255);
412 }
413 
414 private bool worksWithStr2Num(T)() {
415 	return !isSIMDVector!(T) && (isNumeric!T || isPointer!T || is(Unqual!T == char));
416 }
417 
418 unittest {
419 	version (x86_64) {
420 		import core.simd : ushort8;
421 
422 		static assert(worksWithStr2Num!(int));
423 		static assert(worksWithStr2Num!(void*));
424 		static assert(worksWithStr2Num!(double));
425 		static assert(!worksWithStr2Num!(ushort8));
426 	}
427 }
428 
429 string getSpecifier(TTT)() {
430 	static if (is(TTT == enum)) {
431 		alias T = OriginalType!TTT;
432 	} else {
433 		alias T = Unqual!TTT;
434 	}
435 	static if (is(T == float))
436 		return "g";
437 	else static if (is(T == double))
438 		return "lg";
439 	else static if (is(T == real))
440 		return "Lg";
441 	else static if (is(T == char))
442 		return "c";
443 	else static if (is(T == byte))
444 		return "hhd";
445 	else static if (is(T == ubyte))
446 		return "hhu";
447 	else static if (is(T == short))
448 		return "hd";
449 	else static if (is(T == ushort))
450 		return "hu";
451 	else static if (is(T == int))
452 		return "d";
453 	else static if (is(T == uint))
454 		return "u";
455 	else static if (is(T == long))
456 		return "lld";
457 	else static if (is(T == ulong))
458 		return "llu";
459 	else static if (isPointer!T)
460 		return "p";
461 	else
462 		static assert(0, "Type conversion not supported");
463 }
464 
465 // Used for tests
466 
467 private enum TestEnum : byte {
468 	a = 50,
469 	b = 100,
470 }
471 
472 private struct TestStructA {
473 	int a = 1;
474 	ubyte b = ubyte.max;
475 }
476 
477 private struct TestStructB {
478 nothrow @nogc pure:
479 	@disable this();
480 
481 	this(int a) {
482 		this.a = a;
483 	}
484 
485 	TestStructA c;
486 	int a = 3;
487 	long b = long.max;
488 	TestEnum en;
489 }