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