1 /** 2 * Functor-powered lazy @nogc text formatting. 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <ae@cy.md> 12 */ 13 14 module ae.utils.text.functor; 15 16 import std.format : formattedWrite, formatValue, FormatSpec; 17 import std.functional : forward; 18 import std.range.primitives : isOutputRange; 19 20 import ae.utils.functor.composition : isFunctor, select, seq; 21 import ae.utils.functor.primitives : functor; 22 import ae.utils.meta : tupleMap, I; 23 24 /// Given zero or more values, returns a functor which retains a copy of these values; 25 /// the functor can later be called with a sink, which will make it write the values out. 26 /// The returned functor's signature varies depending on whether a 27 /// format string is specified, but either way compatible with 28 /// `toString` signatures accepted by `formattedWrite` 29 /// If a format string is specified, that will be used to format the values; 30 /// otherwise, a format string will be accepted at call time. 31 /// For details, see accepted `toString` signatures in the 32 /// "Structs, Unions, Classes, and Interfaces" section of 33 /// https://dlang.org/phobos/std_format_write.html. 34 template formattingFunctor( 35 string fmt = null, 36 int line = __LINE__, // https://issues.dlang.org/show_bug.cgi?id=23904 37 T...) 38 { 39 static if (fmt) 40 alias fun = 41 (T values, ref w) 42 { 43 w.formattedWrite!fmt(values); 44 }; 45 else 46 alias fun = 47 (T values, ref w, const ref fmt) 48 { 49 foreach (ref value; values) 50 w.formatValue(value, fmt); 51 }; 52 53 auto formattingFunctor(auto ref T values) 54 { 55 return functor!fun(forward!values); 56 } 57 } 58 59 /// 60 unittest 61 { 62 import std.array : appender; 63 import std.format : singleSpec; 64 65 auto a = appender!string; 66 auto spec = "%03d".singleSpec; 67 formattingFunctor(5)(a, spec); 68 assert(a.data == "005"); 69 } 70 71 /// 72 unittest 73 { 74 import std.array : appender; 75 auto a = appender!string; 76 formattingFunctor!"%03d"(5)(a); // or `&a.put!(const(char)[])` 77 assert(a.data == "005"); 78 } 79 80 /// Constructs a stringifiable object from a functor. 81 auto stringifiable(F)(F functor) 82 if (isFunctor!F) 83 { 84 // std.format uses speculative compilation to detect compatibility. 85 // As such, any error in the function will just cause the 86 // object to be silently stringified as "Stringifiable(Functor(...))". 87 // To avoid that, try an explicit instantiation here to 88 // get detailed information about any errors in the function. 89 debug if (false) 90 { 91 // Because std.format accepts any one of several signatures, 92 // try all valid combinations to first check that at least one 93 // is accepted. 94 FormatSpec!char fc; 95 FormatSpec!wchar fw; 96 FormatSpec!dchar fd; 97 struct DummyWriter(Char) { void put(Char c) {} } 98 DummyWriter!char wc; 99 DummyWriter!wchar ww; 100 DummyWriter!dchar wd; 101 void dummySink(const(char)[]) {} 102 static if( 103 !is(typeof(functor(wc, fc))) && 104 !is(typeof(functor(ww, fw))) && 105 !is(typeof(functor(wd, fd))) && 106 !is(typeof(functor(wc))) && 107 !is(typeof(functor(ww))) && 108 !is(typeof(functor(wd))) && 109 !is(typeof(functor(&dummySink)))) 110 { 111 // None were valid; try non-speculatively with the simplest one: 112 pragma(msg, "Functor ", F.stringof, " does not successfully instantiate with any toString signatures."); 113 pragma(msg, "Attempting to non-speculatively instantiate with delegate sink:"); 114 functor(&dummySink); 115 } 116 } 117 118 static struct Stringifiable 119 { 120 F functor; 121 122 void toString(this This, Writer, Char)(ref Writer writer, const ref FormatSpec!Char fmt) 123 if (isOutputRange!(Writer, Char)) 124 { 125 functor(writer, fmt); 126 } 127 128 void toString(this This, Writer)(ref Writer writer) 129 { 130 functor(writer); 131 } 132 133 void toString(this This)(scope void delegate(const(char)[]) sink) 134 { 135 functor(sink); 136 } 137 } 138 return Stringifiable(functor); 139 } 140 141 /// 142 unittest 143 { 144 import std.conv : text; 145 auto f = (void delegate(const(char)[]) sink) => sink("Hello"); 146 assert(stringifiable(f).text == "Hello", stringifiable(f).text); 147 } 148 149 /// Constructs a stringifiable object from a value 150 /// (i.e., a lazily formatted object). 151 /// Combines `formattingFunctor` and `stringifiable`. 152 auto formatted(string fmt = null, T...)(auto ref T values) 153 { 154 return values 155 .formattingFunctor!fmt() 156 .stringifiable; 157 } 158 159 /// 160 unittest 161 { 162 import std.conv : text; 163 import std.format : format; 164 assert(formatted(5).text == "5"); 165 assert(formatted!"%03d"(5).text == "005"); 166 assert(format!"%s%s%s"("<", formatted!"%x"(64), ">") == "<40>"); 167 assert(format!"<%03d>"(formatted(5)) == "<005>"); 168 } 169 170 /// Constructs a functor type from a function alias, and wraps it into 171 /// a stringifiable object. Can be used to create stringifiable 172 /// widgets which need a sink for more complex behavior. 173 template stringifiable(alias fun, T...) 174 { 175 auto stringifiable()(auto ref T values) 176 { 177 return values 178 .functor!fun() 179 .I!(.stringifiable); 180 } 181 } 182 183 /// 184 unittest 185 { 186 alias humanSize = stringifiable!( 187 (size, sink) 188 { 189 import std.format : formattedWrite; 190 if (!size) 191 // You would otherwise need to wrap everything in fmtIf: 192 return sink("0"); 193 static immutable prefixChars = " KMGTPEZY"; 194 size_t power = 0; 195 while (size > 1000 && power + 1 < prefixChars.length) 196 size /= 1024, power++; 197 sink.formattedWrite!"%s %sB"(size, prefixChars[power]); 198 }, real); 199 200 import std.conv : text; 201 assert(humanSize(0).text == "0"); 202 assert(humanSize(8192).text == "8 KB"); 203 } 204 205 /// Returns an object which, depending on a condition, is stringified 206 /// as one of two objects. 207 /// The two branches should themselves be passed as nullary functors, 208 /// to enable lazy evaluation. 209 /// Combines `formattingFunctor`, `stringifiable`, and `select`. 210 auto fmtIf(string fmt = null, Cond, T, F)(Cond cond, T t, F f) @nogc 211 if (isFunctor!T && isFunctor!F) 212 { 213 // Store the value-returning functor into a new functor, which will accept a sink. 214 // When the new functor is called, evaluate the value-returning functor, 215 // put the value into a formatting functor, and immediately call it with the sink. 216 217 // Must be explicitly static due to 218 // https://issues.dlang.org/show_bug.cgi?id=23896 : 219 static void fun(X, Sink...)(X x, auto ref Sink sink) 220 { 221 x().formattingFunctor!fmt()(forward!sink); 222 } 223 return select( 224 cond, 225 functor!fun(t), 226 functor!fun(f), 227 ).stringifiable; 228 } 229 230 /// 231 unittest 232 { 233 import std.conv : text; 234 assert(fmtIf(true , () => 5, () => "apple").text == "5"); 235 assert(fmtIf(false, () => 5, () => "apple").text == "apple"); 236 237 // Scope lazy when? https://issues.dlang.org/show_bug.cgi?id=12647 238 auto division(int a, int b) { return fmtIf(b != 0, () => a / b, () => "NaN"); } 239 assert(division(4, 2).text == "2"); 240 assert(division(4, 0).text == "NaN"); 241 } 242 243 /// Returns an object which is stringified as all of the given objects 244 /// in sequence. In essence, a lazy `std.conv.text`. 245 /// Combines `formattingFunctor`, `stringifiable`, and `seq`. 246 auto fmtSeq(string fmt = "%s", Values...)(Values values) @nogc 247 { 248 return 249 values 250 .tupleMap!((ref value) => formattingFunctor!fmt(value)).expand 251 .seq 252 .stringifiable; 253 } 254 255 unittest 256 { 257 import std.conv : text; 258 assert(fmtSeq(5, " ", "apple").text == "5 apple"); 259 } 260