1 /++
2 $(H1 @nogc and nothrow Parsing Utilities)
3
4 License: $(HTTP www.apache.org/licenses/LICENSE-2.0, Apache-2.0)
5 Authors: Ilya Yaroshenko
6 Copyright: 2020 Ilya Yaroshenko, Kaleidic Associates Advisory Limited, Symmetry Investments
7 +/
8 module mir.parse;
9
10 /// `mir.conv: to` extension.
11 version(mir_test)
12 @safe pure @nogc
13 unittest
14 {
15 import mir.conv: to;
16
17 assert("123.0".to!double == 123);
18 assert("123".to!int == 123);
19 assert("123".to!byte == 123);
20
21 import mir.small_string;
22 alias S = SmallString!32;
23 assert(S("123.0").to!double == 123);
24 assert(S("123.").to!double == 123.);
25 assert(S(".123").to!double == .123);
26 assert(S("123").to!(immutable int) == 123);
27 }
28
29 import mir.primitives;
30 import std.traits: isMutable, isFloatingPoint, isSomeChar;
31
32 /++
33 Performs `nothrow` and `@nogc` string to native type conversion.
34
35 Returns:
36 parsed value
37 Throws:
38 `nogc` Exception in case of parse error or non-empty remaining input.
39
40 Floating_point:
41 Mir parsing supports up-to quadruple precision.
42 The conversion error is 0 ULP for normal numbers.
43 Subnormal numbers with an exponent greater than or equal to -512 have upper error bound equal to 1 ULP.+/
44 T fromString(T, C)(scope const(C)[] str)
45 if (isMutable!T)
46 {
47 import mir.utility: _expect;
48 static immutable excfp = new Exception("fromString failed to parse " ~ T.stringof);
49
50 static if (isFloatingPoint!T)
51 {
52 T value;
53 if (_expect(fromString(str, value), true))
54 return value;
55 version (D_Exceptions)
56 throw excfp;
57 else
58 assert(0);
59 }
60 else
61 {
62 static immutable excne = new Exception("fromString: remaining input is not empty after parsing " ~ T.stringof);
63
64 T value;
65 if (_expect(parse!T(str, value), true))
66 {
67 if (_expect(str.empty, true))
68 return value;
69 version (D_Exceptions)
70 throw excne;
71 else
72 assert(0);
73 }
74 else
75 {
76 version (D_Exceptions)
77 throw excfp;
78 else
79 assert(0);
80 }
81 }
82 }
83
84 ///
85 version(mir_bignum_test)
86 @safe pure @nogc unittest
87 {
88 assert("123".fromString!int == 123);
89 static assert("-123".fromString!int == -123);
90
91 assert(".5".fromString!float == .5);
92 assert("12.3".fromString!double == 12.3);
93 assert("12.3".fromString!float == 12.3f);
94 assert("12.3".fromString!real == 12.3L);
95 assert("-12.3e-30".fromString!double == -12.3e-30);
96 assert("2.9802322387695312E-8".fromString!double == 2.9802322387695312E-8);
97
98 // default support of underscores
99 assert("123_456.789_012".fromString!double == 123_456.789_012);
100 assert("12_34_56_78_90_12e-6".fromString!double == 123_456.789_012);
101
102 // default support of leading zeros
103 assert("010".fromString!double == 10.0);
104 assert("000010".fromString!double == 10.0);
105 assert("0000.10".fromString!double == 0.1);
106 assert("0000e10".fromString!double == 0);
107
108 /// Test CTFE support
109 static assert("-12.3e-30".fromString!double == -0x1.f2f280b2414d5p-97);
110 static assert("+12.3e+30".fromString!double == 0x1.367ee3119d2bap+103);
111
112 static assert("1.448997445238699".fromString!double == 0x1.72f17f1f49aadp0);
113 static if (real.mant_dig >= 64)
114 static assert("1.448997445238699".fromString!real == 1.448997445238699L);
115
116 static assert("3.518437208883201171875".fromString!float == 0x1.c25c26p+1);
117 static assert("3.518437208883201171875".fromString!double == 0x1.c25c268497684p+1);
118 static if (real.mant_dig >= 64)
119 static assert("3.518437208883201171875".fromString!real == 0xe.12e13424bb4232fp-2L);
120
121 // Related DMD Issues:
122 // https://issues.dlang.org/show_bug.cgi?id=20951
123 // https://issues.dlang.org/show_bug.cgi?id=20952
124 // https://issues.dlang.org/show_bug.cgi?id=20953
125 // https://issues.dlang.org/show_bug.cgi?id=20967
126 }
127
128 version(mir_bignum_test)
129 @safe pure unittest
130 {
131 import std.exception: assertThrown;
132 assertThrown("1_".fromString!float);
133 assertThrown("1__2".fromString!float);
134 assertThrown("_1".fromString!float);
135 assertThrown("123_.456".fromString!float);
136 assertThrown("123_e0".fromString!float);
137 assertThrown("123._456".fromString!float);
138 assertThrown("12__34.56".fromString!float);
139 assertThrown("123.456_".fromString!float);
140 assertThrown("-_123.456".fromString!float);
141 assertThrown("_123.456".fromString!float);
142 }
143
144 /++
145 Performs `nothrow` and `@nogc` string to native type conversion.
146
147 Returns: true if success and false otherwise.
148 +/
149 bool fromString(T, C)(scope const(C)[] str, ref T value)
150 if (isSomeChar!C)
151 {
152 static if (isFloatingPoint!T)
153 {
154 import mir.bignum.decimal: Decimal, DecimalExponentKey;
155 import mir.utility: _expect;
156
157 Decimal!256 decimal = void;
158 DecimalExponentKey key;
159 auto ret = decimal.fromStringImpl(str, key);
160 if (_expect(ret, true))
161 {
162 switch(key) with(DecimalExponentKey)
163 {
164 case nan: value = decimal.coefficient.sign ? -T.nan : T.nan; break;
165 case infinity: value = decimal.coefficient.sign ? -T.infinity : T.infinity; break;
166 default: value = cast(T) decimal; break;
167 }
168 }
169 return ret;
170 }
171 else
172 {
173 return parse!T(str, value) && str.empty;
174 }
175 }
176
177 ///
178 version(mir_test)
179 @safe pure nothrow @nogc unittest
180 {
181 int value;
182 assert("123".fromString(value) && value == 123);
183 }
184
185 /++
186 Single character parsing utilities.
187
188 Returns: true if success and false otherwise.
189 +/
190 bool parse(T, Range)(scope ref Range r, scope ref T value)
191 if (isInputRange!Range && isSomeChar!T)
192 {
193 if (r.empty)
194 return false;
195 value = r.front;
196 r.popFront;
197 return true;
198 }
199
200 ///
201 version(mir_test) @safe pure nothrow @nogc
202 unittest
203 {
204 auto s = "str";
205 char c;
206 assert(parse(s, c));
207 assert(c == 's');
208 assert(s == "tr");
209 }
210
211 /++
212 Integer parsing utilities.
213
214 Returns: true if success and false otherwise.
215 +/
216 bool parse(T, Range)(scope ref Range r, scope ref T value)
217 if ((is(T == byte) || is(T == short)) && isInputRange!Range && !__traits(isUnsigned, T))
218 {
219 int lvalue;
220 auto ret = parse!(int, Range)(r, lvalue);
221 value = cast(T) lvalue;
222 return ret && value == lvalue;
223 }
224
225 /// ditto
226 bool parse(T, Range)(scope ref Range r, scope ref T value)
227 if (is(T == int) && isInputRange!Range && !__traits(isUnsigned, T))
228 {
229 version(LDC) pragma(inline, true);
230 return parseSignedImpl!(int, Range)(r, value);
231 }
232
233 /// ditto
234 bool parse(T, Range)(scope ref Range r, scope ref T value)
235 if (is(T == long) && isInputRange!Range && !__traits(isUnsigned, T))
236 {
237 version(LDC) pragma(inline, true);
238 return parseSignedImpl!(long, Range)(r, value);
239 }
240
241 /// ditto
242 bool parse(T, Range)(scope ref Range r, scope ref T value)
243 if ((is(T == ubyte) || is(T == ushort)) && isInputRange!Range && __traits(isUnsigned, T))
244 {
245 uint lvalue;
246 auto ret = parse!(uint, Range)(r, lvalue);
247 value = cast(T) lvalue;
248 return ret && value == lvalue;
249 }
250
251 /// ditto
252 bool parse(T, Range)(scope ref Range r, scope ref T value)
253 if (is(T == uint) && isInputRange!Range && __traits(isUnsigned, T))
254 {
255 version(LDC) pragma(inline, true);
256 return parseUnsignedImpl!(uint, Range)(r, value);
257 }
258
259 /// ditto
260 bool parse(T, Range)(scope ref Range r, scope ref T value)
261 if (is(T == ulong) && isInputRange!Range && __traits(isUnsigned, T))
262 {
263 version(LDC) pragma(inline, true);
264 return parseUnsignedImpl!(ulong, Range)(r, value);
265 }
266
267
268 ///
269 version (mir_test) unittest
270 {
271 import std.meta: AliasSeq;
272 foreach (T; AliasSeq!(byte, ubyte, short, ushort, int, uint, long, ulong))
273 {
274 auto str = "123";
275 T val;
276 assert(parse(str, val));
277 assert(val == 123);
278 str = "0";
279 assert(parse(str, val));
280 assert(val == 0);
281 str = "9";
282 assert(parse(str, val));
283 assert(val == 9);
284 str = "";
285 assert(!parse(str, val));
286 assert(val == 0);
287 str = "text";
288 assert(!parse(str, val));
289 assert(val == 0);
290 }
291 }
292
293 ///
294 version (mir_test) unittest
295 {
296 import std.meta: AliasSeq;
297 foreach (T; AliasSeq!(byte, short, int, long))
298 {
299 auto str = "-123";
300 T val;
301 assert(parse(str, val));
302 assert(val == -123);
303 str = "-0";
304 assert(parse(str, val));
305 assert(val == 0);
306 str = "-9text";
307 assert(parse(str, val));
308 assert(val == -9);
309 assert(str == "text");
310 enum m = T.min + 0;
311 str = m.stringof;
312 assert(parse(str, val));
313 assert(val == T.min);
314 }
315 }
316
317 private bool parseUnsignedImpl(T, Range)(scope ref Range r, scope ref T value)
318 if(__traits(isUnsigned, T))
319 {
320 version(LDC) pragma(inline, true);
321 import mir.checkedint: addu, mulu;
322
323 bool sign;
324 B:
325 if (!r.empty)
326 {
327 auto f = r.front + 0u;
328 if (!sign && f == '+')
329 {
330 r.popFront;
331 sign = true;
332 goto B;
333 }
334 uint c = f - '0';
335 if (c >= 10)
336 goto F;
337 T x = c;
338 for(;;)
339 {
340 r.popFront;
341 if (r.empty)
342 break;
343 c = r.front - '0';
344 if (c >= 10)
345 break;
346 bool overflow;
347 T y = mulu(x, cast(uint)10, overflow);
348 if (overflow)
349 goto R;
350 x = y;
351 T z = addu(x, cast(uint)c, overflow);
352 if (overflow)
353 goto R;
354 x = z;
355 }
356 value = x;
357 return true;
358 }
359 F: value = 0;
360 R: return false;
361 }
362
363 private bool parseSignedImpl(T, Range)(scope ref Range r, scope ref T value)
364 if(!__traits(isUnsigned, T))
365 {
366 version(LDC) pragma(inline, true);
367 import core.checkedint: negs;
368 import std.traits: Unsigned;
369
370 bool sign;
371 B:
372 if (!r.empty)
373 {
374 auto f = r.front + 0u;
375 if (!sign && f == '-')
376 {
377 r.popFront;
378 sign = true;
379 goto B;
380 }
381 auto retu = (()@trusted=>parse(r, *cast(Unsigned!T*) &value))();
382 // auto retu = false;
383 if (!retu)
384 goto R;
385 if (!sign)
386 {
387 if (value < 0)
388 goto R;
389 }
390 else
391 {
392 if (value < 0 && value != T.min)
393 goto R;
394 value = -value;
395 }
396 return true;
397 }
398 F: value = 0;
399 R: return false;
400 }