digitalmars.D - Improvement on format strings, take two.
Here's some code which allow to embed the variable name inside the format string, the syntax is "text %{variable} text " and "text %08d{variable2} text" (inspired from Ruby) IMHO this is better than the normal D format string as the 'embedding' of the variable improves readability: with this syntax one rarely forget a space or a comma around the variable for example. The code is below, it's main problem is that it doesn't handle correctly non-const char[] variable (if you know how to detect const char[], please tell me). The code needs improvement in the format string detection to allow '% d{}' and to allow to mix printf style format string and %{}: currently the mix provides weird result: don't mix them. renoX import std.stdio; template FindChar(char[] A, char B) { static if (A.length == 0) { const int FindChar = -1; } else static if (A[0] == B) { const int FindChar = 0; } else static if (-1 == FindChar!(A[1..$], B)) { const int FindChar = -1; } else { const int FindChar = 1 + FindChar!(A[1..$], B); } } // match the .*{ part in a %.*{, without any % or space in the .* template FindRestFmt(char[] A) { static if (A.length == 0) { const int FindRestFmt = -1; } else static if (A[0] == '{') { const int FindRestFmt = 0; } else static if (A[0] == ' ') { // TODO in fact % {x} should be authorised: equivalent to "% d",x const int FindRestFmt = -1; } else static if (A[0] == '%') { const int FindRestFmt = -1; } else static if (-1 == FindRestFmt!(A[1..$])) { const int FindRestFmt = -1; } else { const int FindRestFmt = 1 + FindRestFmt!(A[1..$]); } } // find a %.*{ without any % or space in the .* template FindStartFmt(char[] A) { static if (A.length == 0) { const int FindStartFmt = -1; } else static if (A[0] != '%') { const int FindStartFmt = -1; } else static if (-1 == FindRestFmt!(A[1..$])) { const int FindStartFmt = -1; } else { const int FindStartFmt = 1 + FindRestFmt!(A[1..$]); } } template FmtString(char[] F, A...) { // static pragma(msg, "FmtString in, F is '"~F~"'"); static if (F.length == 0) const char[] FmtString = "\"," ~ Fmt!(A); else static if (F.length == 1) const char[] FmtString = F[0] ~ "\"," ~ Fmt!(A); // need to escape the %% otherwise doesn't work correctly. else static if (F[0..2] == "%%") //TODO must do full parsing of format strings const char[] FmtString = "%%" ~ FmtString!(F[2..$],A); else static if (FindStartFmt!(F) != -1) static if (FindChar!(F,'}') < FindStartFmt!(F)) static assert(0, "format %[format]{<var>} incorrect in '" ~ F ~ "'"); else static if (F[1] == '{') const char[] FmtString = "%s\"," ~ F[2..FindChar!(F,'}')] ~ ",\"" ~ FmtString!(F[(1+FindChar!(F,'}'))..$],A); else const char[] FmtString = F[0..FindStartFmt!(F)] ~ "\"," ~ F[1+FindStartFmt!(F)..FindChar!(F,'}')] ~ ",\"" ~ FmtString!(F[1+FindChar!(F,'}')..$],A); else const char[] FmtString = F[0] ~ FmtString!(F[1..$],A); // static pragma(msg, "FmtString out is '"~FmtString~"'"); } template Fmt(A...) { static if (A.length == 0) const char[] Fmt = ""; else static if (is(typeof(A[0]) : char[])) //TODO: how to parse static string only?? const char[] Fmt = "\"" ~ FmtString!(A[0], A[1..$]); else const char[] Fmt = A[0].stringof ~ "," ~ Fmt!(A[1..$]); // static pragma(msg, "Fmt out is '"~Fmt~"'"); } template Putf(A...) { // 0..$-1 to remove the last ',' const char[] Putf = "writef(" ~ Fmt!(A)[0..$-1] ~ ");"; // static pragma(msg, "Putf out is'"~Putf~"'"); } int main() { int x = 10; int y = 20; const char[] csimple = "const bonjour simple"; char[] simple = "bonjour simple"; // text containg % char[] text="percent d: %d percent{x}: %{x}"; const char[] ctext="const percent d: %d percent{x}: %{x}"; writef("BEFORE'"); mixin(Putf!("coucou simple")); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!("ah que","coucou")); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!(x)); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!("foo\n",x,y)); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!(x,"foo\n")); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!(csimple)); // works for const. // mixin(Putf!(simple)); Doesn't work for non-const char. writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!("%{simple}")); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!("%s{simple}")); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!("a is %x",10)); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!("test double percent escape: should seen one percent then {x}: %%{x}")); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!("test avant %{x} apres")); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!("test format hexa:%08x{x}after")); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!("test text containing percent: '%{text}' apres",30)); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!(ctext,30)); //TODO fix the erroneous inversion between the %d and %{x} replacement. // mixin(Putf!(text,30)); Doesn't work for non-const string. writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!("test avant '%{text}'"," apres %{x} encore apres ",x,y)); writef("'AFTER\n"); return 0; }
Feb 22 2007
A new version, works now also with non const char[] parameter thanks to mario pernici who gave me the tip. The syntax of the format string is slightly changed, now you need to write %s{} or %d{}, '%{' is just the escape sequence for '{'. It needs further testing / polishing, still it's not too bad, the only annoying part is the syntax at the call site instead of writef(..), mixin(Puts!(..)) which is quite ugly.. Is-there a way to improve this by 'hiding' the mixin? renoX import std.stdio; template Relay(int index, int incr = 1) { static if (index == -1) const int Relay = -1; else const int Relay = incr + index; } template FindChar(char[] A, char B) { static if (A.length == 0) const int FindChar = -1; else static if (A[0] == B) const int FindChar = 0; else const int FindChar = Relay!(FindChar!(A[1..$], B)); } /* Match format string: '%' Flags Width Precision FormatChar Width: empty | Integer | '*' Precision: empty | '.' | '.' Integer | '.*' Integer: Digit | Digit Integer Digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' FormatChar: 's' | 'b' | 'd' | 'o' | 'x' | 'X' | 'e' | 'E' | 'f' | 'F' | 'g' | 'G' | 'a' | 'A' */ template MatchFmt(char[] A) { static if (A.length == 0) const int MatchFmt = -1; else static if (A[0] == '%') const int MatchFmt = Relay!(MatchFmtFlags!(A[1..$])); else const int MatchFmt = -1; } Flags */ template MatchFmtFlags(char[] A) { static if (A.length == 0) const int MatchFmtFlags = -1; (A[0] == '0') || (A[0] == ' ')) const int MatchFmtFlags = Relay!(MatchFmtFlags!(A[1..$])); else const int MatchFmtFlags = MatchFmtWidth!(A); } /* Width: empty | Integer | '*' */ template MatchFmtWidth(char[] A) { static if (A.length == 0) const int MatchFmtWidth = -1; else static if (A[0] == '*') const int MatchFmtWidth = Relay!(MatchFmtPrecision!(A[1..$])); else static if (MatchUinteger!(A) > 0) const int MatchFmtWidth = Relay!(MatchFmtPrecision!(A[MatchUinteger!(A)..$]),MatchUinteger!(A)); else const int MatchFmtWidth = MatchFmtPrecision!(A); } /* Precision: empty | '.' | '.' Integer | '.*' */ template MatchFmtPrecision(char[] A) { static if (A.length == 0) const int MatchFmtPrecision = -1; else static if (A[0] == '.') { static if (MatchUinteger!(A[1..$]) > 0) { const int MatchFmtPrecision = Relay!(MatchFmtChar!(A[1+MatchUinteger!(A[1..$])..$]), 1+MatchUinteger!(A[1..$])); } else { const int MatchFmtPrecision = Relay!(MatchFmtChar!(A[1..$])); } } else static if (MatchUinteger!(A) > 0) const int MatchFmtPrecision = Relay!(MatchFmtChar!(A[MatchUinteger!(A)..$]), MatchUinteger!(A)); else const int MatchFmtPrecision = MatchFmtChar!(A); } /* Integer: Digit | Digit Integer Digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' */ template MatchUinteger(char[] s) { static if (s.length == 0) { const int MatchUinteger = -1; } // allow 0 for first digit or not? else static if ((s[0] >= '0') && (s[0] <= '9')) { const int MatchUinteger = Relay!(MatchRestUinteger!(s[1..$])); } else { const int MatchUinteger = -1; } } template MatchRestUinteger(char[] s) { static if (s.length == 0) { const int MatchRestUinteger = 0; } else static if ((s[0] >= '0') && (s[0] <= '9')) { const int MatchRestUinteger = Relay!(MatchRestUinteger!(s[1..$])); } else { const int MatchRestUinteger = 0; } } /* FormatChar: 's' | 'b' | 'd' | 'o' | 'x' | 'X' | 'e' | 'E' | 'f' | 'F' | 'g' | 'G' | 'a' | 'A' */ template MatchFmtChar(char[] A) { static if (A.length == 0) const int MatchFmtChar = -1; else static if ((A[0] == 's') || (A[0] == 'b') || (A[0] == 'd') || (A[0] == 'o') || (A[0] == 'x') || (A[0] == 'X') || (A[0] == 'e') || (A[0] == 'E') || (A[0] == 'f') || (A[0] == 'F') || (A[0] == 'g') || (A[0] == 'G') || (A[0] == 'a') || (A[0] == 'A')) const int MatchFmtChar = 0; else const int MatchFmtChar = -1; } template FmtString(char[] F, A...) { // static pragma(msg, "FmtString in, F is '"~F~"'"); static if (F.length == 0) const char[] FmtString = "\"," ~ Fmt!(A); else static if (F.length == 1) const char[] FmtString = F[0] ~ "\"," ~ Fmt!(A); else static if (F[0] == '%') { // need to escape the %% otherwise doesn't work correctly. static if (F[1] == '%') const char[] FmtString = "%%" ~ FmtString!(F[2..$],A); // now { needs an escape sequence: \{ doesn't work in dmd so %{ else static if (F[1] == '{') const char[] FmtString = "{" ~ FmtString!(F[2..$],A); // is-it a format string? else static if (MatchFmt!(F) != -1) { // new format string %[..]{<variable>} static if (FindChar!(F,'{') == 1+MatchFmt!(F)) { static if (FindChar!(F,'}') <= FindChar!(F,'{')) static assert(0, "bad format string %[format]{<var>} in '" ~ F ~ "'"); const char[] FmtString = F[0..FindChar!(F,'{')] ~ "\"," ~ F[1+FindChar!(F,'{')..FindChar!(F,'}')] ~ ",\"" ~ FmtString!(F[(1+FindChar!(F,'}'))..$],A); } else // normal format string: move things around so that old and new format cohabitate peacefully. const char[] FmtString = F[0..1+MatchFmt!(F)] ~ "\"," ~ A[0].stringof ~ ",\"" ~ FmtString!(F[1+MatchFmt!(F)..$],A[1..$]); } else static assert(0, "bad format string in '" ~ F ~ "'"); } else const char[] FmtString = F[0] ~ FmtString!(F[1..$],A); // static pragma(msg, "FmtString out is '"~FmtString~"'"); } template Fmt(A...) { static if (A.length == 0) const char[] Fmt = ""; // magic incantation to parse only const char[] format string else static if (is(typeof(A[0]) : char[]) && !is(typeof(&(A[0]))) && is(typeof(A[0]))) const char[] Fmt = "\"" ~ FmtString!(A[0], A[1..$]); else const char[] Fmt = A[0].stringof ~ "," ~ Fmt!(A[1..$]); static pragma(msg, "Fmt out is '"~Fmt~"'"); } template Putf(A...) { // 0..$-1 to remove the last ',' const char[] Putf = "writef(" ~ Fmt!(A)[0..$-1] ~ ");"; // static pragma(msg, "Putf out is'"~Putf~"'"); } int main() { int x = 10; int y = 20; const char[] csimple = "const bonjour simple"; char[] simple = "bonjour simple"; // text containing % char[] text="percentd: %d percentd{x}: %d{x}"; const char[] ctext="const percent d: %d percentd{x}: %d{x}"; writef("BEFORE'"); assert(Putf!("simple") == "writef(\"simple\");"); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!("ah que","coucou")); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!(x)); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!("foo\n",x,y)); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!("coucou %d simple",10)); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!(x,"foo\n")); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!(csimple)); // works for const. writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!(simple)); //now works also with non-const char: do not parse them writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!("should be {simple}: %{simple}")); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!("%s{simple}")); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!("10 is %d{x}")); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!("a is %x",10)); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!("test double percent escape: should seen one percent then {x}: %%{x}")); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!("test avant %{x} apres")); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!("test format hexa:%08x{x}after")); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!("test text containing percent: '%{text}' apres",30)); writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!(ctext,30)); //Done fix the erroneous invertion between the %d and %{x} replacement. writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!(text,30)); // Does now work for non-const string. writef("'AFTER\n"); writef("BEFORE'"); mixin(Putf!("test avant '%{text}'"," apres %{x} encore apres ",x,y)); writef("'AFTER\n"); return 0; }
Feb 22 2007