The OpenD Programming Language

1 /++
2 $(H1 @nogc Formatting Utilities)
3 
4 License: $(HTTP www.apache.org/licenses/LICENSE-2.0, Apache-2.0)
5 Authors: Ilia Ki
6 +/
7 module mir.format;
8 
9 import std.traits;
10 import mir.primitives: isOutputRange;
11 
12 ///Scalar styles.
13 enum StringStyle
14 {
15     /// Uninitialized style
16     none,
17     /// Literal block style, `|`
18     longMultiLine,
19     /// Folded block style, `>`
20     longSingleLine,
21     /// Plain scalar
22     plain,
23     /// Single quoted scalar
24     asSingleQuoted,
25     /// Double quoted scalar
26     asEscapedString,
27 }
28 
29 /// Collection styles
30 enum CollectionStyle
31 {
32     /// Uninitialized style
33     none,
34     /// Block style
35     block,
36     /// Flow style
37     flow,
38 }
39 
40 /// `mir.conv: to` extension.
41 version(mir_test)
42 @safe pure @nogc
43 unittest
44 {
45     import mir.conv: to;
46     import mir.small_string;
47     alias S = SmallString!32;
48 
49     // Floating-point numbers are formatted to
50     // the shortest precise exponential notation.
51     assert(123.0.to!S == "123.0");
52     assert(123.to!(immutable S) == "123");
53     assert(true.to!S == "true");
54     assert(true.to!string == "true");
55 
56     assert((cast(S)"str")[] == "str");
57 }
58 
59 /// `mir.conv: to` extension.
60 version(mir_test)
61 @safe pure
62 unittest
63 {
64     import mir.conv: to;
65     import mir.small_string;
66     alias S = SmallString!32;
67 
68     auto str = S("str");
69     assert(str.to!(const(char)[]) == "str"); // GC allocated result
70     assert(str.to!(char[]) == "str"); // GC allocated result
71 }
72 
73 /// ditto
74 version(mir_test)
75 @safe pure
76 unittest
77 {
78     import mir.conv: to;
79     import mir.small_string;
80     alias S = SmallString!32;
81 
82     // Floating-point numbers are formatted to
83     // the shortest precise exponential notation.
84     assert(123.0.to!string == "123.0");
85     assert(123.to!(char[]) == "123");
86 
87     assert(S("str").to!string == "str"); // GC allocated result
88 }
89 
90 /// Concatenated string results
91 string text(string separator = "", Args...)(auto scope ref const Args args)
92     if (Args.length > 0)
93 {
94     import mir.utility: _expect;
95 
96     static if (Args.length == 1)
97     {
98         static if (is(immutable Args[0] == immutable typeof(null)))
99         {
100             return "null";
101         }
102         else
103         static if (is(Args[0] == enum))
104         {
105             import mir.enums: getEnumIndex, enumStrings;
106             uint id = void;
107             if (getEnumIndex(args[0], id)._expect(true))
108                 return enumStrings!(Args[0])[id];
109             assert(0);
110         }
111         else
112         static if (is(Unqual!(Args[0]) == bool))
113         {
114             return args[0] ? "true" : "false";
115         }
116         else
117         static if (is(args[0].toString : string))
118         {
119             return args[0].toString;
120         }
121         else
122         {
123             import mir.appender: scopedBuffer;
124             auto buffer = scopedBuffer!char;
125             buffer.print(args[0]);
126             return buffer.data.idup;
127         }
128     }
129     else
130     {
131         import mir.appender: scopedBuffer;
132         auto buffer = scopedBuffer!char;
133         foreach (i, ref arg; args)
134         {
135             buffer.print(arg);
136             static if (separator.length && i + 1 < args.length)
137             {
138                 buffer.printStaticString!char(separator);
139             }
140         }
141         return buffer.data.idup;
142     }
143 }
144 
145 ///
146 version(mir_test)
147 @safe pure nothrow unittest
148 {
149     const i = 100;
150     assert(text("str ", true, " ", i, " ", 124.1) == "str true 100 124.1", text("str ", true, " ", 100, " ", 124.1));
151     assert(text!" "("str", true, 100, 124.1, null) == "str true 100 124.1 null");
152     assert(text(null) == "null", text(null));
153 }
154 
155 import mir.format_impl;
156 
157 ///
158 struct GetData {}
159 
160 ///
161 enum getData = GetData();
162 
163 /++
164 +/
165 struct StringBuf(C, uint scopeSize = 256)
166     if (is(C == char) || is(C == wchar) || is(C == dchar))
167 {
168     import mir.appender: ScopedBuffer;
169 
170     ///
171     ScopedBuffer!(C, scopeSize) buffer;
172 
173     ///
174     alias buffer this;
175 
176     ///
177     mixin StreamFormatOp!C;
178 }
179 
180 ///ditto
181 auto stringBuf(C = char, uint scopeSize = 256)()
182     @trusted pure nothrow @nogc @property
183     if (is(C == char) || is(C == wchar) || is(C == dchar))
184 {
185     StringBuf!(C, scopeSize) buffer = void;
186     buffer.initialize;
187     return buffer;
188 }
189 
190 /++
191 +/
192 mixin template StreamFormatOp(C)
193 {
194     ///
195     ref typeof(this) opBinary(string op : "<<", T)(scope ref const T c) scope
196     {
197         print!C(this, c);
198         return this;
199     }
200 
201     ///
202     ref typeof(this) opBinary(string op : "<<", T)(scope const T c) scope
203     {
204         print!C(this, c);
205         return this;
206     }
207 
208     /// ditto
209     const(C)[] opBinary(string op : "<<", T : GetData)(scope const T c) scope
210     {
211         return buffer.data;
212     }
213 }
214 
215 ///
216 @safe pure nothrow @nogc
217 version (mir_test) unittest
218 {
219     auto name = "D";
220     auto ver = 2.0;
221     assert(stringBuf << "Hi " << name << ver << "!\n" << getData == "Hi D2.0!\n");
222 }
223 
224 ///
225 @safe pure nothrow @nogc
226 version (mir_test) unittest
227 {
228     auto name = "D"w;
229     auto ver = 2;
230     assert(stringBuf!wchar << "Hi "w << name << ver << "!\n"w << getData == "Hi D2!\n"w);
231 }
232 
233 ///
234 @safe pure nothrow @nogc
235 version (mir_test) unittest
236 {
237     auto name = "D"d;
238     auto ver = 2UL;
239     assert(stringBuf!dchar << "Hi "d  << name << ver << "!\n"d << getData == "Hi D2!\n"d);
240 }
241 
242 @safe pure nothrow @nogc
243 version (mir_test) unittest
244 {
245     assert(stringBuf << -1234567890 << getData == "-1234567890");
246 }
247 
248 /++
249 Mir's numeric format specification
250 
251 Note: the specification isn't complete an may be extended in the future.
252 +/
253 struct NumericSpec
254 {
255     ///
256     enum Format
257     {
258         /++
259         Human-frindly precise output.
260         Examples: `0.000001`, `600000.0`, but `1e-7` and `6e7`.
261         +/
262         human,
263         /++
264         Precise output with explicit exponent.
265         Examples: `1e-6`, `6e6`, `1.23456789e-100`.
266         +/
267         exponent,
268     }
269 
270     ///
271     Format format;
272 
273     /// Default valus is '\0' (no separators)
274     char separatorChar = '\0';
275 
276     /// Defaults to 'e'
277     char exponentChar = 'e';
278 
279     /// Adds '+' to positive numbers and `+0`.
280     bool plus;
281 
282     /// Separator count
283     ubyte separatorCount = 3;
284 
285     /++
286     Precise output with explicit exponent.
287     Examples: `1e-6`, `6e6`, `1.23456789e-100`.
288     +/
289     enum NumericSpec exponent = NumericSpec(Format.exponent);
290 
291     /++
292     Human-frindly precise output.
293     +/
294     enum NumericSpec human = NumericSpec(Format.human);
295 }
296 
297 // 16-bytes
298 /// C's compatible format specifier.
299 struct FormatSpec
300 {
301     ///
302     bool dash;
303     ///
304     bool plus;
305     ///
306     bool space;
307     ///
308     bool hash;
309     ///
310     bool zero;
311     ///
312     char format = 's';
313     ///
314     char separator = '\0';
315     ///
316     ubyte unitSize;
317     ///
318     int width;
319     ///
320     int precision = -1;
321 }
322 
323 /++
324 +/
325 enum SwitchLU : bool
326 {
327     ///
328     lower,
329     ///
330     upper,
331 }
332 
333 /++
334 Wrapper to format floating point numbers using C's library.
335 +/
336 struct FormattedFloating(T)
337     if(is(T == float) || is(T == double) || is(T == real))
338 {
339     ///
340     T value;
341     ///
342     FormatSpec spec;
343 
344     ///
345     void toString(C = char, W)(scope ref W w) scope const
346     if (isSomeChar!C)
347     {
348         C[512] buf = void;
349         auto n = printFloatingPoint(value, spec, buf);
350         w.put(buf[0 ..  n]);
351     }
352 }
353 
354 /// ditto
355 FormattedFloating!T withFormat(T)(const T value, FormatSpec spec)
356 {
357     version(LDC) pragma(inline);
358     return typeof(return)(value, spec);
359 }
360 
361 /++
362 +/
363 struct HexAddress(T)
364     if (isUnsigned!T && !is(T == enum))
365 {
366     ///
367     T value;
368     ///
369     SwitchLU switchLU = SwitchLU.upper;
370 
371     ///
372     void toString(C = char, W)(scope ref W w) scope const
373         if (isSomeChar!C)
374     {
375         enum N = T.sizeof * 2;
376         static if(isFastBuffer!W)
377         {
378             w.advance(printHexAddress(value, w.getStaticBuf!N, cast(bool) switchLU));
379         }
380         else
381         {
382             C[N] buf = void;
383             printHexAddress(value, buf, cast(bool) switchLU);
384             w.put(buf[]);
385         }
386     }
387 }
388 
389 ///ditto
390 HexAddress!T hexAddress(T)(const T value, SwitchLU switchLU = SwitchLU.upper)
391     if (isUnsigned!T && !is(T == enum))
392 {
393     return typeof(return)(value, switchLU);
394 }
395 
396 /++
397 Escaped string formats
398 +/
399 enum EscapeFormat
400 {
401     /// JSON escaped string format
402     json,
403     /// Amzn Ion CLOB format
404     ionClob,
405     /// Amzn Ion symbol format
406     ionSymbol,
407     /// Amzn Ion string format
408     ion,
409 }
410 
411 enum escapeFormatQuote(EscapeFormat escapeFormat) = escapeFormat == EscapeFormat.ionSymbol ? '\'' : '\"';
412 
413 /++
414 +/
415 void printEscaped(C, EscapeFormat escapeFormat = EscapeFormat.ion, W)(scope ref W w, scope const(C)[] str)
416     if (isOutputRange!(W, C))
417 {
418     import mir.utility: _expect;
419     foreach (C c; str)
420     {
421         if (_expect(c == escapeFormatQuote!escapeFormat || c == '\\', false))
422             goto E;
423         if (_expect(c < ' ', false))
424             goto C;
425         static if (escapeFormat == EscapeFormat.ionClob)
426         {
427             if (c >= 127)
428                 goto A;
429         }
430     P:
431         w.put(c);
432         continue;
433     E:
434         {
435             C[2] pair;
436             pair[0] = '\\';
437             pair[1] = c;
438             w.printStaticString!C(pair);
439             continue;
440         }
441     C:
442         switch (c)
443         {
444             static if (escapeFormat != EscapeFormat.json)
445             {
446                 case '\0':
447                     c = '0';
448                     goto E;
449                 case '\a':
450                     c = 'a';
451                     goto E;
452                 case '\v':
453                     c = 'v';
454                     goto E;
455             }
456             case '\b':
457                 c = 'b';
458                 goto E;
459             case '\t':
460                 c = 't';
461                 goto E;
462             case '\n':
463                 c = 'n';
464                 goto E;
465             case '\f':
466                 c = 'f';
467                 goto E;
468             case '\r':
469                 c = 'r';
470                 goto E;
471             default:
472     A:
473                 static if (escapeFormat == EscapeFormat.json)
474                     put_uXXXX!C(w, cast(char)c);
475                 else
476                     put_xXX!C(w, cast(char)c);
477         }
478     }
479     return;
480 }
481 
482 ///
483 @safe pure nothrow @nogc
484 version (mir_test) unittest
485 {
486     import mir.format: stringBuf;
487     auto w = stringBuf;
488     w.printEscaped("Hi \a\v\0\f\t\b \\\r\n" ~ `"@nogc"`);
489     assert(w.data == `Hi \a\v\0\f\t\b \\\r\n\"@nogc\"`);
490     w.reset;
491     w.printEscaped("\x03");
492     assert(w.data == `\x03`);
493 }
494 
495 ///
496 void printReplaced(C, W)(scope ref W w, scope const(C)[] str, C c, scope const(C)[] to)
497 {
498     import mir.string: scanLeftAny;
499 
500     while (str.length)
501     {
502         auto tailLen = str.scanLeftAny(c).length;
503         print(w, str[0 .. $ - tailLen]);
504         if (tailLen == 0)
505             break;
506         print(w, to);
507         str = str[$ - tailLen + 1 .. $];
508     }
509 }
510 
511 ///
512 @safe pure nothrow
513 unittest
514 {
515     import mir.test: should;
516     auto csv = stringBuf;
517     csv.put('"');
518     csv.printReplaced(`some string with " double quotes "!`, '"', `""`);
519     csv.put('"');
520     csv.data.should == `"some string with "" double quotes ""!"`;
521 }
522 
523 /++
524 Decodes `char` `c` to the form `u00XX`, where `XX` is 2 hexadecimal characters.
525 +/
526 void put_xXX(C = char, W)(scope ref W w, char c)
527     if (isSomeChar!C)
528 {
529     ubyte[2] spl;
530     spl[0] = c >> 4;
531     spl[1] = c & 0xF;
532     C[4] buffer;
533     buffer[0] = '\\';
534     buffer[1] = 'x';
535     buffer[2] = cast(ubyte)(spl[0] < 10 ? spl[0] + '0' : spl[0] - 10 + 'A');
536     buffer[3] = cast(ubyte)(spl[1] < 10 ? spl[1] + '0' : spl[1] - 10 + 'A');
537     return w.printStaticString(buffer);
538 }
539 
540 /++
541 Decodes `char` `c` to the form `\u00XX`, where `XX` is 2 hexadecimal characters.
542 +/
543 void put_uXXXX(C = char, W)(scope ref W w, char c)
544     if (isSomeChar!C)
545 {
546     ubyte[2] spl;
547     spl[0] = c >> 4;
548     spl[1] = c & 0xF;
549     C[6] buffer;
550     buffer[0] = '\\';
551     buffer[1] = 'u';
552     buffer[2] = '0';
553     buffer[3] = '0';
554     buffer[4] = cast(ubyte)(spl[0] < 10 ? spl[0] + '0' : spl[0] - 10 + 'A');
555     buffer[5] = cast(ubyte)(spl[1] < 10 ? spl[1] + '0' : spl[1] - 10 + 'A');
556     return w.printStaticString(buffer);
557 }
558 
559 /++
560 Decodes ushort `c` to the form `\uXXXX`, where `XXXX` is 2 hexadecimal characters.
561 +/
562 void put_uXXXX(C = char, W)(scope ref W w, ushort c)
563     if (isSomeChar!C)
564 {
565     ubyte[4] spl;
566     spl[0] = (c >> 12) & 0xF;
567     spl[1] = (c >> 8) & 0xF;
568     spl[2] = (c >> 4) & 0xF;
569     spl[3] = c & 0xF;
570     C[6] buffer;
571     buffer[0] = '\\';
572     buffer[1] = 'u';
573     buffer[2] = cast(ubyte)(spl[0] < 10 ? spl[0] + '0' : spl[0] - 10 + 'A');
574     buffer[3] = cast(ubyte)(spl[1] < 10 ? spl[1] + '0' : spl[1] - 10 + 'A');
575     buffer[4] = cast(ubyte)(spl[2] < 10 ? spl[2] + '0' : spl[2] - 10 + 'A');
576     buffer[5] = cast(ubyte)(spl[3] < 10 ? spl[3] + '0' : spl[3] - 10 + 'A');
577     return w.printStaticString(buffer);
578 }
579 
580 /++
581 Decodes uint `c` to the form `\UXXXXXXXX`, where `XXXXXXXX` is 2 hexadecimal characters.
582 +/
583 void put_UXXXXXXXX(C = char, W)(scope ref W w, uint c)
584     if (isSomeChar!C)
585 {
586     w.printStaticString!C(`\U`);
587     w.print!C(HexAddress!uint(cast(uint)c));
588 }
589 
590 ///
591 void printElement(C, EscapeFormat escapeFormat = EscapeFormat.ion, W)(scope ref W w, scope const(C)[] c)
592     if (isSomeChar!C)
593 {
594     static immutable C[1] quote = '\"';
595     w.printStaticString!C(quote);
596     w.printEscaped!(C, escapeFormat)(c);
597     w.printStaticString!C(quote);
598 }
599 
600 ///
601 void printElement(C = char, EscapeFormat escapeFormat = EscapeFormat.ion, W, T)(scope ref W w, scope auto ref const T c)
602     if (!isSomeString!T)
603 {
604     return w.print!C(c);
605 }
606 
607 /++
608 Multiargument overload.
609 +/
610 void print(C = char, W, Args...)(scope ref W w, auto scope ref const Args args)
611     if (isSomeChar!C && Args.length > 1)
612 {
613     foreach(i, ref c; args)
614         static if (i < Args.length - 1)
615             w.print!C(c);
616         else
617             return w.print!C(c);
618 }
619 
620 /// Prints enums
621 void print(C = char, W, T)(scope ref W w, scope const T c) @nogc
622     if (isSomeChar!C && is(T == enum))
623 {
624     import mir.enums: getEnumIndex, enumStrings;
625     import mir.utility: _expect;
626 
627     static assert(!is(OriginalType!T == enum));
628     uint index = void;
629     if (getEnumIndex(c, index)._expect(true))
630     {
631         w.put(enumStrings!T[index]);
632         return;
633     }
634     static immutable C[] str = T.stringof ~ "(";
635     w.put(str[]);
636     print!C(w, cast(OriginalType!T) c);
637     w.put(')');
638     return;
639 }
640 
641 ///
642 @safe pure nothrow @nogc
643 version (mir_test) unittest
644 {
645     enum Flag
646     {
647         no,
648         yes,
649     }
650 
651     import mir.appender: scopedBuffer;
652     auto w = scopedBuffer!char;
653     w.print(Flag.yes);
654     assert(w.data == "yes");
655 }
656 
657 /// Prints boolean
658 void print(C = char, W)(scope ref W w, bool c)
659     if (isSomeChar!C)
660 {
661     enum N = 5;
662     static if(isFastBuffer!W)
663     {
664         w.advance(printBoolean(c, w.getStaticBuf!N));
665     }
666     else
667     {
668         C[N] buf = void;
669         auto n = printBoolean(c, buf);
670         w.put(buf[0 .. n]);
671     }
672     return;
673 }
674 
675 ///
676 @safe pure nothrow @nogc
677 version (mir_test) unittest
678 {
679     import mir.appender: scopedBuffer;
680     auto w = scopedBuffer!char;
681     w.print(true);
682     assert(w.data == `true`);
683     w.reset;
684     w.print(false);
685     assert(w.data == `false`);
686 }
687 
688 /// Prints associative array
689 pragma(inline, false)
690 void print(C = char, W, V, K)(scope ref W w, scope const V[K] c)
691     if (isSomeChar!C)
692 {
693     enum C left = '[';
694     enum C right = ']';
695     enum C[2] sep = ", ";
696     enum C[2] mid = ": ";
697     w.put(left);
698     bool first = true;
699     foreach (ref key, ref value; c)
700     {
701         if (!first)
702             w.printStaticString!C(sep);
703         first = false;
704         w.printElement!C(key);
705         w.printStaticString!C(mid);
706         w.printElement!C(value);
707     }
708     w.put(right);
709     return;
710 }
711 
712 ///
713 @safe pure
714 version (mir_test) unittest
715 {
716     import mir.appender: scopedBuffer;
717     auto w = scopedBuffer!char;
718     w.print(["a": 1, "b": 2]);
719     assert(w.data == `["a": 1, "b": 2]` || w.data == `["b": 2, "a": 1]`);
720 }
721 
722 /// Prints null
723 void print(C = char, W, V)(scope ref W w, const V c)
724     if (is(V == typeof(null)))
725 {
726     enum C[4] Null = "null";
727     return w.printStaticString!C(Null);
728 }
729 
730 ///
731 @safe pure @nogc
732 version (mir_test) unittest
733 {
734     import mir.appender: scopedBuffer;
735     auto w = scopedBuffer!char;
736     w.print(null);
737     assert(w.data == `null`);
738 }
739 
740 /// Prints array
741 pragma(inline, false)
742 void printArray(C = char, W, T)(scope ref W w,
743     scope const(T)[] c,
744     scope const(C)[] lb = "[",
745     scope const(C)[] rb = "]",
746     scope const(C)[] sep = ", ",
747 )
748     if (isSomeChar!C && !isSomeChar!T)
749 {
750     w.put(lb);
751     bool first = true;
752     foreach (ref e; c)
753     {
754         if (!first)
755             w.put(sep);
756         first = false;
757         printElement!C(w, e);
758     }
759     w.put(rb);
760     return;
761 }
762 
763 /// ditto
764 pragma(inline, false)
765 void print(C = char, W, T)(scope ref W w,
766     scope const(T)[] c,
767 )
768     if (isSomeChar!C && !isSomeChar!T)
769 {
770     return printArray(w, c);
771 }
772 
773 ///
774 @safe pure nothrow @nogc
775 version (mir_test) unittest
776 {
777     import mir.appender: scopedBuffer;
778     auto w = scopedBuffer!char;
779     string[2] array = ["a\na", "b"];
780     w.print(array[]);
781     assert(w.data == `["a\na", "b"]`);
782 }
783 
784 /// Prints array as hex values
785 pragma(inline, false)
786 void printHexArray(C = char, W, T)(scope ref W w,
787     scope const(T)[] c,
788     scope const(C)[] lb = "",
789     scope const(C)[] rb = "",
790     scope const(C)[] sep = " ",
791 )
792     if (isSomeChar!C && !isSomeChar!T && isUnsigned!T)
793 {
794     w.put(lb);
795     bool first = true;
796     foreach (ref e; c)
797     {
798         if (!first)
799             w.put(sep);
800         first = false;
801         printElement!C(w, e.hexAddress);
802     }
803     w.put(rb);
804     return;
805 }
806 
807 ///
808 @safe pure nothrow @nogc
809 version (mir_test) unittest
810 {
811     import mir.test;
812     import mir.appender: scopedBuffer;
813     auto w = scopedBuffer!char;
814     ubyte[2] array = [0x34, 0x32];
815     w.printHexArray(array[]);
816     w.data.should == `34 32`;
817 }
818 
819 /// Prints escaped character in the form `'c'`.
820 pragma(inline, false)
821 void print(C = char, W)(scope ref W w, char c)
822     if (isSomeChar!C)
823 {
824     w.put('\'');
825     if (c >= ubyte.max)
826     {
827         w.printStaticString!C(`\u`);
828         w.print!C(HexAddress!ubyte(cast(ubyte)c));
829     }
830     else
831     if (c >= 0x20)
832     {
833         if (c < 0x7F)
834         {
835             if (c == '\'' || c == '\\')
836             {
837             L:
838                 w.put('\\');
839             }
840             w.put(c);
841         }
842         else
843         {
844         M:
845             w.printStaticString!C(`\x`);
846             w.print!C(HexAddress!ubyte(cast(ubyte)c));
847         }
848     }
849     else
850     {
851         switch(c)
852         {
853             case '\n': c = 'n'; goto L;
854             case '\r': c = 'r'; goto L;
855             case '\t': c = 't'; goto L;
856             case '\a': c = 'a'; goto L;
857             case '\b': c = 'b'; goto L;
858             case '\f': c = 'f'; goto L;
859             case '\v': c = 'v'; goto L;
860             case '\0': c = '0'; goto L;
861             default: goto M;
862         }
863     }
864     w.put('\'');
865     return;
866 }
867 
868 ///
869 @safe pure nothrow @nogc
870 version (mir_test) unittest
871 {
872     import mir.appender: scopedBuffer;
873     auto w = scopedBuffer!char;
874     w.print('\n');
875     w.print('\'');
876     w.print('a');
877     w.print('\xF4');
878     assert(w.data == `'\n''\'''a''\xF4'`);
879 }
880 
881 /// Prints escaped character in the form `'c'`.
882 pragma(inline, false)
883 void print(C = char, W)(scope ref W w, dchar c)
884     if (isSomeChar!C)
885 {
886     import std.uni: isGraphical;
887     if (c <= ubyte.max)
888         return print(w, cast(char) c);
889     w.put('\'');
890     if (c.isGraphical)
891     {
892         import std.utf: encode;
893         C[dchar.sizeof / C.sizeof] buf;
894         print!C(w, buf[0 .. encode(buf, c)]);
895     }
896     else
897     if (c <= ushort.max)
898     {
899         w.put_uXXXX!C(cast(ushort)c);
900     }
901     else
902     {
903         w.put_UXXXXXXXX!C(c);
904     }
905     w.put('\'');
906 }
907 
908 ///
909 @safe pure
910 version (mir_test) unittest
911 {
912     import mir.appender: scopedBuffer;
913     auto w = scopedBuffer!char;
914     w.print('щ');
915     w.print('\U0010FFFE');
916     assert(w.data == `'щ''\U0010FFFE'`);
917 }
918 
919 /// Prints some string
920 void print(C = char, W)(scope ref W w, scope const(C)[] c)
921     if (isSomeChar!C)
922 {
923     w.put(c);
924     return;
925 }
926 
927 /// Prints integers
928 void print(C = char, W, I)(scope ref W w, const I c)
929     if (isSomeChar!C && isIntegral!I && !is(I == enum))
930 {
931     static if (I.sizeof == 16)
932         enum N = 39;
933     else
934     static if (I.sizeof == 8)
935         enum N = 20;
936     else
937         enum N = 10;
938     C[N + !__traits(isUnsigned, I)] buf = void;
939     static if (__traits(isUnsigned, I))
940         auto n = printUnsignedToTail(c, buf);
941     else
942         auto n = printSignedToTail(c, buf);
943     w.put(buf[$ - n ..  $]);
944     return;
945 }
946 
947 /// Prints floating point numbers
948 void print(C = char, W, T)(scope ref W w, const T c, NumericSpec spec = NumericSpec.init)
949     if(isSomeChar!C && is(T == float) || is(T == double) || is(T == real))
950 {
951     import mir.bignum.decimal;
952     auto decimal = Decimal!(T.mant_dig < 64 ? 1 : 2)(c);
953     decimal.toString(w, spec);
954     return;
955 }
956 
957 /// Human friendly precise output (default)
958 version(mir_bignum_test)
959 @safe pure nothrow @nogc
960 unittest
961 {
962     auto spec = NumericSpec.human;
963     auto buffer = stringBuf;
964 
965     void check(double num, string value)
966     {
967         buffer.print(num, spec);
968         assert(buffer.data == value, value);
969         buffer.reset;
970     }
971 
972     check(-0.0, "-0.0");
973     check(0.0, "0.0");
974     check(-0.01, "-0.01");
975     check(0.0125, "0.0125");
976     check(0.000003, "0.000003");
977     check(-3e-7, "-3e-7");
978     check(123456.0, "123456.0");
979     check(123456.1, "123456.1");
980     check(12.3456, "12.3456");
981     check(-0.123456, "-0.123456");
982     check(0.1234567, "0.1234567");
983     check(0.01234567, "0.01234567");
984     check(0.001234567, "0.001234567");
985     check(1.234567e-4, "1.234567e-4");
986     check(-1234567.0, "-1.234567e+6");
987     check(123456.7890123, "123456.7890123");
988     check(1234567.890123, "1.234567890123e+6");
989     check(1234567890123.0, "1.234567890123e+12");
990     check(0.30000000000000004, "0.30000000000000004");
991     check(0.030000000000000002, "0.030000000000000002");
992     check(0.0030000000000000005, "0.0030000000000000005");
993     check(3.0000000000000003e-4, "3.0000000000000003e-4");
994     check(+double.nan, "nan");
995     check(-double.nan, "nan");
996     check(+double.infinity, "+inf");
997     check(-double.infinity, "-inf");
998 
999     spec.separatorChar = ',';
1000 
1001     check(-0.0, "-0.0");
1002     check(0.0, "0.0");
1003     check(-0.01, "-0.01");
1004     check(0.0125, "0.0125");
1005     check(0.000003, "0.000003");
1006     check(-3e-7, "-3e-7");
1007     check(123456.0, "123,456.0");
1008     check(123456e5, "12,345,600,000.0");
1009     check(123456.1, "123,456.1");
1010     check(12.3456, "12.3456");
1011     check(-0.123456, "-0.123456");
1012     check(0.1234567, "0.1234567");
1013     check(0.01234567, "0.01234567");
1014     check(0.001234567, "0.001234567");
1015     check(1.234567e-4, "0.0001234567");
1016     check(-1234567.0, "-1,234,567.0");
1017     check(123456.7890123, "123,456.7890123");
1018     check(1234567.890123, "1,234,567.890123");
1019     check(123456789012.0, "123,456,789,012.0");
1020     check(1234567890123.0, "1.234567890123e+12");
1021     check(0.30000000000000004, "0.30000000000000004");
1022     check(0.030000000000000002, "0.030000000000000002");
1023     check(0.0030000000000000005, "0.0030000000000000005");
1024     check(3.0000000000000003e-4, "0.00030000000000000003");
1025     check(3.0000000000000005e-6, "0.0000030000000000000005");
1026     check(3.0000000000000004e-7, "3.0000000000000004e-7");
1027     check(+double.nan, "nan");
1028     check(-double.nan, "nan");
1029     check(+double.infinity, "+inf");
1030     check(-double.infinity, "-inf");
1031 
1032     spec.separatorChar = '_';
1033     spec.separatorCount = 2;
1034     check(123456e5, "1_23_45_60_00_00.0");
1035 
1036     spec.plus = true;
1037     check(0.0125, "+0.0125");
1038     check(-0.0125, "-0.0125");
1039 }
1040 
1041 /// Prints structs and unions
1042 pragma(inline, false)
1043 void print(C = char, W, T)(scope ref W w, scope ref const T c)
1044     if (isSomeChar!C && is(T == struct) || is(T == union) && !is(T : NumericSpec))
1045 {
1046     import mir.algebraic: isVariant;
1047     static if (__traits(hasMember, T, "toString"))
1048     {
1049         static if (is(typeof(c.toString!C(w))))
1050             c.toString!C(w);
1051         else
1052         static if (isVariant!T || is(typeof(c.toString(w))))
1053             c.toString(w);
1054         else
1055         static if (is(typeof(c.toString((scope const(C)[] s) { w.put(s); }))))
1056             c.toString((scope const(C)[] s) { w.put(s); });
1057         else
1058         static if (is(typeof(w.put(c.toString))))
1059             w.put(c.toString);
1060         else
1061         {
1062             import std.format: FormatSpec;
1063             FormatSpec!char fmt;
1064 
1065             static if (is(typeof(c.toString(w, fmt))))
1066                 c.toString(w, fmt);
1067             else
1068             static if (is(typeof(c.toString((scope const(C)[] s) { w.put(s); }, fmt))))
1069                 c.toString((scope const(C)[] s) { w.put(s); }, fmt);
1070             else
1071             // workaround for types with mutable toString
1072             static if (is(typeof((*cast(T*)&c).toString(w, fmt))))
1073                 (*cast(T*)&c).toString(w, fmt);
1074             else
1075             static if (is(typeof((*cast(T*)&c).toString((scope const(C)[] s) { w.put(s); }, fmt))))
1076                 (*cast(T*)&c).toString((scope const(C)[] s) { w.put(s); }, fmt);
1077             else
1078             static if (is(typeof((*cast(T*)&c).toString(w))))
1079                 (*cast(T*)&c).toString(w);
1080             else
1081             static if (is(typeof((*cast(T*)&c).toString((scope const(C)[] s) { w.put(s); }))))
1082                 (*cast(T*)&c).toString((scope const(C)[] s) { w.put(s); });
1083             else
1084             static if (is(typeof(w.put((*cast(T*)&c).toString))))
1085                 w.put((*cast(T*)&c).toString);
1086             else
1087                 c.toString(w);
1088                 // static assert(0, T.stringof ~ ".toString definition is wrong: 'const' qualifier may be missing.");
1089         }
1090 
1091         return;
1092     }
1093     else
1094     static if (__traits(compiles, { scope const(C)[] string_of_c = c; }))
1095     {
1096         scope const(C)[] string_of_c = c;
1097         return w.print!C(string_of_c);
1098     }
1099     else
1100     static if (hasIterableLightConst!T)
1101     {
1102         enum C left = '[';
1103         enum C right = ']';
1104         enum C[2] sep = ", ";
1105         w.put(left);
1106         bool first = true;
1107         foreach (ref e; c.lightConst)
1108         {
1109             if (!first)
1110                 printStaticString!C(w, sep);
1111             first = false;
1112             print!C(w, e);
1113         }
1114         w.put(right);
1115         return;
1116     }
1117     else
1118     {
1119         enum C left = '(';
1120         enum C right = ')';
1121         enum C[2] sep = ", ";
1122         w.put(left);
1123         foreach (i, ref e; c.tupleof)
1124         {
1125             static if (i)
1126                 w.printStaticString!C(sep);
1127             print!C(w, e);
1128         }
1129         w.put(right);
1130         return;
1131     }
1132 }
1133 
1134 /// ditto
1135 // FUTURE: remove it
1136 pragma(inline, false)
1137 void print(C = char, W, T)(scope ref W w, scope const T c)
1138     if (isSomeChar!C && is(T == struct) || is(T == union))
1139 {
1140     return print!(C, W, T)(w, c);
1141 }
1142 
1143 ///
1144 @safe pure nothrow @nogc
1145 version (mir_test) unittest
1146 {
1147     static struct A { scope void toString(C, W)(scope ref W w) const { w.put(C('a')); } }
1148     static struct S { scope void toString(W)(scope ref W w) const { w.put("s"); } }
1149     static struct D { scope void toString(Dg)(scope Dg sink) const { sink("d"); } }
1150     static struct F { scope const(char)[] toString()() const return { return "f"; } }
1151     static struct G { const(char)[] s = "g"; alias s this; }
1152 
1153     import mir.appender: scopedBuffer;
1154     auto w = scopedBuffer!char;
1155     assert(stringBuf << A() << S() << D() << F() << G() << getData == "asdfg");
1156 }
1157 
1158 /// Prints classes and interfaces
1159 pragma(inline, false)
1160 void print(C = char, W, T)(scope ref W w, scope const T c)
1161     if (isSomeChar!C && is(T == class) || is(T == interface))
1162 {
1163     static if (__traits(hasMember, T, "toString") || __traits(compiles, { scope const(C)[] string_of_c = c; }))
1164     {
1165         if (c is null)
1166             return w.print(null);
1167         else
1168         static if (is(typeof(c.toString!C(w))))
1169         {
1170             c.toString!C(w);
1171             return;
1172         }
1173         else
1174         static if (is(typeof(c.toString(w))))
1175         {
1176             c.toString(w);
1177             return;
1178         }
1179         else
1180         static if (is(typeof(c.toString((scope const(C)[] s) { w.put(s); }))))
1181         {
1182             c.toString((scope const(C)[] s) { w.put(s); });
1183             return;
1184         }
1185         else
1186         static if (is(typeof(w.put(c.toString))))
1187         {
1188             w.put(c.toString);
1189             return;
1190         }
1191         else
1192         static if (__traits(compiles, { scope const(C)[] string_of_c = c; }))
1193         {
1194             scope const(C)[] string_of_c = c;
1195             return w.print!C(string_of_c);
1196         }
1197         else static assert(0, T.stringof ~ ".toString definition is wrong: 'const scope' qualifier may be missing.");
1198     }
1199     else
1200     static if (hasIterableLightConst!T)
1201     {
1202         enum C left = '[';
1203         enum C right = ']';
1204         enum C[2] sep = ", ";
1205         w.put(left);
1206         bool first = true;
1207         foreach (ref e; c.lightConst)
1208         {
1209             if (!first)
1210                 w.printStaticString!C(sep);
1211             first = false;
1212             print!C(w, e);
1213         }
1214         w.put(right);
1215         return;
1216     }
1217     else
1218     {
1219         w.put(T.stringof);
1220         return;
1221     }
1222 }
1223 
1224 ///
1225 @safe pure nothrow
1226 version (mir_test) unittest
1227 {
1228     static class A { void toString(C, W)(scope ref W w) const { w.put(C('a')); } }
1229     static class S { void toString(W)(scope ref W w) const { w.put("s"); } }
1230     static class D { void toString(Dg)(scope Dg sink) const { sink("d"); } }
1231     static class F { const(char)[] toString()() const return { return "f"; } }
1232     static class G { const(char)[] s = "g"; alias s this; }
1233 
1234     assert(stringBuf << new A() << new S() << new D() << new F() << new G() << getData == "asdfg");
1235 }
1236 
1237 ///
1238 void printStaticString(C, size_t N, W)(scope ref W w, scope ref const C[N] c)
1239     if (isSomeChar!C && is(C == char) || is(C == wchar) || is(C == dchar))
1240 {
1241     static if (isFastBuffer!W)
1242     {
1243         enum immutable(ForeachType!(typeof(w.getBuffer(size_t.init))))[] value = c;
1244         w.getStaticBuf!(value.length) = value;
1245         w.advance(c.length);
1246     }
1247     else
1248     {
1249         w.put(c[]);
1250     }
1251     return;
1252 }
1253 
1254 private template hasIterableLightConst(T)
1255 {
1256     static if (__traits(hasMember, T, "lightConst"))
1257     {
1258         enum hasIterableLightConst = isIterable!(ReturnType!((const T t) => t.lightConst));
1259     }
1260     else
1261     {
1262         enum hasIterableLightConst = false;
1263     }
1264 }
1265 
1266 private ref C[N] getStaticBuf(size_t N, C, W)(scope ref W w)
1267     if (isFastBuffer!W)
1268 {
1269     auto buf = w.getBuffer(N);
1270     assert(buf.length >= N);
1271     return buf.ptr[0 .. N];
1272 }
1273 
1274 private @trusted ref C[N] getStaticBuf(size_t N, C)(return scope ref C[] buf)
1275 {
1276     assert(buf.length >= N);
1277     return buf.ptr[0 .. N];
1278 }
1279 
1280 template isFastBuffer(W)
1281 {
1282     enum isFastBuffer = __traits(hasMember, W, "getBuffer") && __traits(hasMember, W, "advance");
1283 }
1284 
1285 ///
1286 void printZeroPad(C = char, W, I)(scope ref W w, const I c, size_t minimalLength)
1287     if (isSomeChar!C && isIntegral!I && !is(I == enum))
1288 {
1289     static if (I.sizeof == 16)
1290         enum N = 39;
1291     else
1292     static if (I.sizeof == 8)
1293         enum N = 20;
1294     else
1295         enum N = 10;
1296     C[N + !__traits(isUnsigned, I)] buf = void;
1297     static if (__traits(isUnsigned, I))
1298         auto n = printUnsignedToTail(c, buf);
1299     else
1300         auto n = printSignedToTail(c, buf);
1301     sizediff_t zeros = minimalLength - n;
1302 
1303     if (zeros > 0)
1304     {
1305         static if (!__traits(isUnsigned, I))
1306         {
1307             if (c < 0)
1308             {
1309                 n--;
1310                 w.put(C('-'));
1311             }
1312         }
1313         do w.put(C('0'));
1314         while(--zeros);
1315     }
1316     w.put(buf[$ - n ..  $]);
1317     return;
1318 }
1319 
1320 ///
1321 version (mir_test) unittest
1322 {
1323     import mir.appender;
1324     auto w = scopedBuffer!char;
1325 
1326     w.printZeroPad(-123, 5);
1327     w.put(' ');
1328     w.printZeroPad(123, 5);
1329 
1330     assert(w.data == "-0123 00123");
1331 }
1332 
1333 ///
1334 size_t printBoolean(C)(bool c, ref C[5] buf)
1335     if(is(C == char) || is(C == wchar) || is(C == dchar))
1336 {
1337     version(LDC) pragma(inline, true);
1338     if (c)
1339     {
1340         buf[0] = 't';
1341         buf[1] = 'r';
1342         buf[2] = 'u';
1343         buf[3] = 'e';
1344         return 4;
1345     }
1346     else
1347     {
1348         buf[0] = 'f';
1349         buf[1] = 'a';
1350         buf[2] = 'l';
1351         buf[3] = 's';
1352         buf[4] = 'e';
1353         return 5;
1354     }
1355 }
1356 
1357 
1358 /// Prints pointers
1359 void print(C = char, W, T)(scope ref W w, scope const T* c)
1360 {
1361     import mir.enums: getEnumIndex, enumStrings;
1362     import mir.utility: _expect;
1363     if (c is null)
1364         return w.print!C(null);
1365     return w.print!C(HexAddress!size_t((()@trusted=>cast(size_t)cast(const void*)c)()));
1366 }