1 /**
2 String build code, plus no-locale float parsing functions.
3 
4 Copyright: Guillaume Piolat, 2022.
5 License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
6 */
7 
8 module dplug.core..string;
9 
10 import core.stdc.stdlib;
11 import core.stdc.string;
12 import core.stdc.stdarg;
13 import dplug.core.vec;
14 
15 
16 nothrow @nogc:
17 
18 /// Create a `String` from a D `string`.
19 String makeString(const(char)[] s)
20 {
21     return String(s);
22 }
23 
24 /// For now, just a string builder that owns its memory.
25 /// Dplug `String`, designed to ease the usage of all the C string function,
26 /// allow appending, etc.
27 /// `String` always owns its memory, and can return as a D slice.
28 /// FUTURE: use realloc to be able to size down.
29 ///         Capacity to be a slice into existing memory and not own.
30 ///         Capacity to disown memory (implies: stop using Vec)
31 /// QUESTION: should String just be a managed slice!T instead? Like Go slices.
32 struct String
33 {
34 public:
35 nothrow @nogc:
36 
37     this(char ch)
38     {
39         this ~= ch;
40     }
41 
42     this(const(char)[] s)
43     {
44         this ~= s;
45     }
46 
47     ~this()
48     {
49     }
50 
51     @disable this(this);
52 
53     /// Sets as empty/null string.
54     void makeEmpty()
55     {
56         _chars.clearContents();
57     }
58 
59     /// Pointer to first character in the string, or `null`.
60     inout(char)* ptr() inout return
61     {
62         return _chars.ptr;
63     }
64 
65     /// Length in bytes of the string.
66     size_t length() const
67     {
68         return _chars.length;
69     }
70 
71     /// Converts to a D string, sliced into the `String` memory.
72     inout(char)[] asSlice() inout return
73     {
74         size_t len = length();
75         if (len == 0)
76             return null;
77         return _chars[0..len];
78     }
79 
80     /// Returns: Whole content of the sring in one slice.
81     inout(char)[] opSlice() inout return
82     {
83         return asSlice();
84     }
85 
86     /// Returns: A slice of the array.
87     inout(char)[] opSlice(size_t i1, size_t i2) inout
88     {
89         return _chars[i1 .. i2];
90     }
91 
92     void opAssign(T : char)(T x)
93     {
94         makeEmpty();
95         this ~= x;
96     }
97 
98     void opAssign(T : const(char)[])(T x)
99     {
100         makeEmpty();
101         this ~= x;
102     }
103 
104     void opAssign(T : String)(T x)
105     {
106         makeEmpty();
107         this ~= x;
108     }
109 
110     // <Appending>
111 
112     /// Append a character to the string. This invalidates pointers to characters
113     /// returned before.
114     void opOpAssign(string op)(char x) if (op == "~")
115     {
116         _chars.pushBack(x);
117     }
118 
119     /// Append a characters to the string.
120     void opOpAssign(string op)(const(char)[] str) if (op == "~")
121     {
122         size_t len = str.length;
123         for (size_t n = 0; n < len; ++n)
124             _chars.pushBack(str[n]);
125     }
126 
127     /// Append a characters to the string.
128     void opOpAssign(string op)(ref const(String) str) if (op == "~")
129     {
130         this ~= str.asSlice();
131     }
132 
133     /// Append a zero-terminated character to the string.
134     /// Name is explicit, because it should be rare and overload conflict.
135     void appendZeroTerminatedString(const(char)* str)
136     {
137         while(*str != '\0')
138             _chars.pushBack(*str++);
139     }
140 
141     bool opEquals(const(char)[] s)
142     {
143         size_t lenS = s.length;
144         size_t lenT = this.length;
145         if (lenS != lenT)
146             return false;
147         for (size_t n = 0; n < lenS; ++n)
148         {
149             if (s[n] != _chars[n])
150                 return false;
151         }        
152         return true;
153     }
154 
155     bool opEquals(ref const(String) str)
156     {
157         return this.asSlice() == str.asSlice();
158     }
159 
160     // </Appending>
161 
162 private:
163 
164     // FUTURE
165 
166     /*alias Flags = int;
167     enum : Flags
168     {
169         owned          = 1, /// String data is currently owned (C's malloc/free), not borrowed.
170         zeroTerminated = 2, /// String data is currently zero-terminated.
171     }
172 
173     Flags _flags = 0;
174     */
175 
176     Vec!char _chars;
177 
178     void clearContents()
179     {
180         _chars.clearContents();
181     }
182 }
183 
184 // Null and .ptr
185 unittest
186 {
187     string z;
188     string a = "";
189     string b = null;
190 
191     assert(a == z);
192     assert(b == z);
193     assert(a == b);
194     assert(a !is b);
195     assert(a.length == 0);
196     assert(b.length == 0);
197     assert(a.ptr !is null);
198 
199     // Must preserve semantics from D strings.
200     String Z = z;
201     String A = a;
202     String B = b;
203     assert(A == Z);
204     assert(B == Z);
205     assert(A == B);
206 }
207 
208 // Basic appending.
209 unittest
210 {
211     String s = "Hello,";
212     s ~= " world!";
213     assert(s == "Hello, world!");
214     s.makeEmpty();
215     assert(s == null);
216     assert(s.length == 0);
217 }
218 
219 /// strtod replacement, but without locale
220 ///     s Must be a zero-terminated string.
221 /// Note that this code is duplicated in wren-port, to avoid a dependency on dplug:core there.
222 public double strtod_nolocale(const(char)* s, const(char)** p)
223 {
224     bool strtod_err = false;
225     const(char)* pend;
226     double r = stb__clex_parse_number_literal(s, &pend, &strtod_err, true);
227     if (p) 
228         *p = pend;
229     if (strtod_err)
230         r = 0.0;
231     return r;
232 }
233 unittest
234 {
235     string[18] sPartial = 
236     [
237         "0x123lol", "+0x1.921fb54442d18p+0001()", "0,", "-0.0,,,,", 
238         "0.65,stuff", "1.64587okokok", "-1.0e+9HELLO", "1.1454e-25f#STUFF",
239         "+iNfu", "-infEXCESS", "infuh", "-infinity", 
240         "+infinity", "+nan", "-nan", "nan",
241         "INFINITY", "-NAN"
242     ]; 
243 
244     for (int n = 0; n < sPartial.length; ++n)
245     {
246         const(char)* p1, p2;
247         double r1 = strtod(sPartial[n].ptr, &p1); // in unittest, no program tampering the C locale
248         double r2 = strtod_nolocale(sPartial[n].ptr, &p2);
249         //import core.stdc.stdio;
250         //debug printf("parsing \"%s\" %lg %lg %p %p\n", sPartial[n].ptr, r1, r2, p1, p2);
251         assert(p1 == p2);
252     }
253 }
254 
255 /// C-locale independent string to integer parsing.
256 /// Params:
257 ///     s Must be a zero-terminated string.
258 ///     mustConsumeEntireInput if true, check that s is entirely consumed by parsing the number.
259 ///     err: optional bool
260 /// Note: unlike with `convertStringToDouble`, the string "4.7" will parse to just 4. Replaces %d in scanf-like functions.
261 /// Only parse correctly from -2147483648 to 2147483647.
262 /// Larger values are clamped to this -2147483648 to 2147483647 range.
263 public int convertStringToInteger(const(char)* s, 
264                                   bool mustConsumeEntireInput,
265                                   bool* err) pure nothrow @nogc
266 {
267     if (s is null)
268     {
269         if (err) *err = true;
270         return 0;
271     }
272 
273     const(char)* end;
274     bool strtod_err = false;
275     bool allowFloat = false;
276     double r = stb__clex_parse_number_literal(s, &end, &strtod_err, allowFloat);
277 
278     if (strtod_err)
279     {
280         if (err) *err = true;
281         return 0;
282     }
283 
284     if (mustConsumeEntireInput)
285     {
286         size_t len = strlen(s);
287         if (end != s + len)
288         {
289             if (err) *err = true; // did not consume whole string
290             return 0;
291         }
292     }
293 
294     if (err) *err = false; // no error
295 
296     double r2 = cast(int)r;
297     assert(r2 == r); // should have returned an integer that fits in a double, like the whole int.min to int.max range.
298     return cast(int)r;
299 }
300 unittest
301 {
302     bool err;
303     assert(4 == convertStringToInteger(" 4.7\n", false, &err));
304     assert(!err);
305 
306     assert(-2147483648 == convertStringToInteger("-2147483649", false, &err));
307     assert( 1 == convertStringToInteger("1e30", false, &err));
308     assert( 0 == convertStringToInteger("-0", false, &err));
309     assert( 2147483647 == convertStringToInteger("10000000000", false, &err));
310 }
311 
312 
313 /// C-locale independent string to float parsing.
314 /// Params:
315 ///     s Must be a zero-terminated string.
316 ///     mustConsumeEntireInput if true, check that s is entirely consumed by parsing the number.
317 ///     err: optional bool
318 public double convertStringToDouble(const(char)* s, 
319                                     bool mustConsumeEntireInput,
320                                     bool* err) pure nothrow @nogc
321 {
322     if (s is null)
323     {
324         if (err) *err = true;
325         return 0.0;
326     }
327 
328     const(char)* end;
329     bool strtod_err = false;
330     double r = stb__clex_parse_number_literal(s, &end, &strtod_err, true);
331 
332     if (strtod_err)
333     {
334         if (err) *err = true;
335         return 0.0;
336     }
337     
338     if (mustConsumeEntireInput)
339     {
340         size_t len = strlen(s);
341         if (end != s + len)
342         {
343             if (err) *err = true; // did not consume whole string
344             return 0.0;
345         }
346     }
347 
348     if (err) *err = false; // no error
349     return r;
350 }
351  
352 unittest
353 {
354     //import core.stdc.stdio;
355     import std.math.operations;
356 
357     string[9] s = ["14", "0x123", "+0x1.921fb54442d18p+0001", "0", "-0.0", "   \n\t\n\f\r 0.65", "1.64587", "-1.0e+9", "1.1454e-25"]; 
358     double[9] correct = [14, 0x123, +0x1.921fb54442d18p+0001, 0.0, -0.0, 0.65L, 1.64587, -1e9, 1.1454e-25f];
359 
360     string[9] sPartial = ["14top", "0x123lol", "+0x1.921fb54442d18p+0001()", "0,", "-0.0,,,,", "   \n\t\n\f\r 0.65,stuff", "1.64587okokok", "-1.0e+9HELLO", "1.1454e-25f#STUFF"]; 
361     for (int n = 0; n < s.length; ++n)
362     {
363         /*
364         // Check vs scanf
365         double sa;
366         if (sscanf(s[n].ptr, "%lf", &sa) == 1)
367         {
368             debug printf("scanf finds %lg\n", sa);
369         }
370         else
371             debug printf("scanf no parse\n");
372         */
373 
374         bool err;
375         double a = convertStringToDouble(s[n].ptr, true, &err);
376         //import std.stdio;
377         //debug writeln(a, " correct is ", correct[n]);
378         assert(!err);
379         assert( isClose(a, correct[n], 0.0001) );
380 
381         bool err2;
382         double b = convertStringToDouble(s[n].ptr, false, &err2);
383         assert(!err2);
384         assert(b == a); // same parse
385 
386         //debug printf("%lf\n", a);
387 
388         convertStringToDouble(s[n].ptr, true, null); // should run without error pointer
389     }
390 }
391 
392 private double stb__clex_parse_number_literal(const(char)* p, 
393                                               const(char)**q, 
394                                               bool* err,
395                                               bool allowFloat) pure nothrow @nogc
396 {
397     const(char)* s = p;
398     double value=0;
399     int base=10;
400     int exponent=0;
401     int signMantissa = 1;
402 
403     // Skip leading whitespace, like scanf and strtod do
404     while (true)
405     {
406         char ch = *p;
407         if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == '\f' || ch == '\r')
408         {
409             p += 1;
410         }
411         else
412             break;
413     }
414 
415 
416     if (*p == '-') 
417     {
418         signMantissa = -1;
419         p += 1;
420     } 
421     else if (*p == '+') 
422     {
423         p += 1;
424     }
425 
426     // Issue #865, "-inf" was parsed as 0
427     // libc can produce "infinity" as well as "inf"
428     // %f specifier can produce "infinity", "inf", "nan"
429     // %F specifier can produce "INFINITY", "INF", "NAN"
430     // In practice, C libraries parse combination of uppercase and lowercase
431     if (allowFloat)
432     {
433         if (  (p[0] == 'i' || p[0] == 'I')
434            && (p[1] == 'n' || p[1] == 'N')
435            && (p[2] == 'f' || p[2] == 'F') )
436         {
437             value = double.infinity;
438             p += 3;
439 
440             if (  (p[0] == 'i' || p[0] == 'I')
441                && (p[1] == 'n' || p[1] == 'N')
442                && (p[2] == 'i' || p[2] == 'I')
443                && (p[3] == 't' || p[3] == 'T')
444                && (p[4] == 'y' || p[4] == 'Y') )            
445                 p += 5;
446 
447             goto found_value;
448         }
449 
450          if (  (p[0] == 'n' || p[0] == 'N')
451             && (p[1] == 'a' || p[1] == 'A')
452             && (p[2] == 'n' || p[2] == 'N') )
453         {
454             value = double.nan;
455             p += 3;
456             goto found_value;
457         }
458     }
459 
460     if (*p == '0') 
461     {
462         if (p[1] == 'x' || p[1] == 'X') 
463         {
464             base=16;
465             p += 2;
466         }
467     }
468 
469     for (;;) 
470     {
471         if (*p >= '0' && *p <= '9')
472             value = value*base + (*p++ - '0');
473         else if (base == 16 && *p >= 'a' && *p <= 'f')
474             value = value*base + 10 + (*p++ - 'a');
475         else if (base == 16 && *p >= 'A' && *p <= 'F')
476             value = value*base + 10 + (*p++ - 'A');
477         else
478             break;
479     }
480 
481     if (allowFloat)
482     {
483         if (*p == '.') 
484         {
485             double pow, addend = 0;
486             ++p;
487             for (pow=1; ; pow*=base) 
488             {
489                 if (*p >= '0' && *p <= '9')
490                     addend = addend*base + (*p++ - '0');
491                 else if (base == 16 && *p >= 'a' && *p <= 'f')
492                     addend = addend*base + 10 + (*p++ - 'a');
493                 else if (base == 16 && *p >= 'A' && *p <= 'F')
494                     addend = addend*base + 10 + (*p++ - 'A');
495                 else
496                     break;
497             }
498             value += addend / pow;
499         }
500         if (base == 16) {
501             // exponent required for hex float literal, else it's an integer literal like 0x123
502             exponent = (*p == 'p' || *p == 'P');
503         } else
504             exponent = (*p == 'e' || *p == 'E');
505 
506         if (exponent) 
507         {
508             int sign = p[1] == '-';
509             uint exponent2 = 0;
510             double power=1;
511             ++p;
512             if (*p == '-' || *p == '+')
513                 ++p;
514             while (*p >= '0' && *p <= '9')
515                 exponent2 = exponent2*10 + (*p++ - '0');
516 
517             if (base == 16)
518                 power = stb__clex_pow(2, exponent2);
519             else
520                 power = stb__clex_pow(10, exponent2);
521             if (sign)
522                 value /= power;
523             else
524                 value *= power;
525         }
526     }
527 
528     found_value:
529     
530     if (q) *q = p;
531     if (err) *err = false; // seen no error
532 
533     if (signMantissa < 0)
534         value = -value;
535 
536     if (!allowFloat)
537     {
538         // clamp and round to nearest integer
539         if (value > int.max) value = int.max;
540         if (value < int.min) value = int.min;
541     }    
542     return value;
543 }
544 
545 private double stb__clex_pow(double base, uint exponent) pure nothrow @nogc
546 {
547     double value=1;
548     for ( ; exponent; exponent >>= 1) {
549         if (exponent & 1)
550             value *= base;
551         base *= base;
552     }
553     return value;
554 }