www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - D rawkz! -- custom writefln formats

reply "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
It's been a while since the last "D rocks!" post. So here's one.

I'm guessing that most D users don't realize extent of the flexibility
of std.format -- I know I didn't until I discovered this little gem
hidden in the docs (and then only implicitly!).

But first, a motivating example. Suppose you have some kind of data
structure, let's call it S, and at some point in your program, you want
to output it. The most obvious way, of course, is to implement a
toString() method:

	struct S {
		... // my sooper sekret data here!
		string toString() const pure  safe {
			// Typical implementation to minimize overhead
			// of constructing string
			auto app = appender!string();
			... // transform data into string
			return app.data;
		}
	}

	void main() {
		auto s = S();
		... // do wonderful stuff with s
		writeln(s);
	}

This is the "traditional" implementation, of course. A slight
optimization that's possible is to realize that there's an alternative
signature of toString() that alleviates the overhead of doing any string
allocations at all:

	struct S {
		// This method now takes a delegate to send data to.
		void toString(scope void delegate(const(char)[]) sink) const
		{
			// So you can write your data piecemeal to its
			// destination, without having to construct a
			// string and then return it.
			sink("prelude");
			sink(... /* beautiful prologue */);
			sink("concerto");
			sink(... /* beautiful body */);
			sink("finale");
			sink(... /* beautiful trailer */);

			// Look, ma! No string allocations needed!
		}
	}

So far so good. This is (or should be) all familiar ground.

But suppose now you want to write your data to, say, a backup file in
one format, but output your data to the user in another format. How
would you do this?

You could make toString() output one format, say the on-disk format,
then add another method, say toUserReadableString() for outputting the
other format. But this is ugly and non-extensible. What if you have a
whole bunch of other formats that need to be output? You'd be drowning
in toNetworkString(), toDatabaseString(), toHtmlEscapedString(), etc.,
etc., which bloats your data's API and isn't very maintainable to boot.

Here's where a little known feature of std.format comes in. Note that
when you write:

	S s;
	writeln(s);

This actually ultimately gets translated to the equivalent of:

	S s;
	writefln("%s", s);

Where the %s specifier, of course, means "convert to the standard string
representation". What is less known, though, is that this actually
translates to something like this:

	Writer w = ... /* writer object that outputs to stdout */
	FormatSpec!Char fmt = ... /* object representing the meaning of "%s" */
	s.toString((const(char)[] s) { w.put(s); }, fmt);

In human language, this means that "%s" gets translated into a
FormatSpec object containing "s" in its .spec field (and if you write,
say, "%10s", the 10 gets stored in the .width field, etc.), and then
this FormatSpec object gets passed to the toString method of the object
being formatted, if it is defined with the correct signature. To see
this in action, let's do this:

	struct S {
		void toString(scope void delegate(const(char)[]) sink,
			FormatSpec!char fmt) const
		{
			// This is for probing how std.format works
			// under the hood.
			writeln(fmt.spec);
		}
	}
	void main() {
		S s;

		// Wait -- what? What on earth are %i, %j, %k, and %l?!
		writeln("%i", s);	// Hmm, prints "i"!
		writeln("%j", s);	// Hmm, prints "j"!
		writeln("%k", s);	// Hmm, prints "k"!
		writeln("%l", s);	// Hmm, prints "l"!
	}

Do you see what's going on? The format specifiers are not hard-coded
into the library! You can invent your own specifiers, and they get
passed into the toString method. This allows us to do this:

	struct S {
		void toString(scope void delegate(const(char)[]) sink,
			FormatSpec!char fmt) const
		{
			switch(fmt.spec) {
			// Look, ma! I invented my own format specs!
			case "i":
				// output first format to sink
				break;
			case "j":
				// output second format to sink
				break;
			case "k":
				// output third format to sink
				break;
			case "l":
				// output fourth format to sink
				break;
			case "s":
				// output boring default string format
				break;
			default:
				throw new Exception(
					"Unknown format specifier: %" ~
					fmt.spec);
			}
		}
	}

Of course, FormatSpec contains much more than just the letter that
defines the specifier. It also contains field width, precision, etc.. So
you can implement your own handling for all of these parameters that are
specifiable in a writefln format string.

Here's a somewhat silly example to show the flexibility conferred:

	import std.format;
	import std.stdio;

	struct BoxPrinter {
		void toString(scope void delegate(const(char)[]) sink,
			FormatSpec!char fmt) const
		{
			if (fmt.spec == 'b') {
				// Draws a starry rectangle
				foreach (j; 0..fmt.precision) {
					foreach (i; 0..fmt.width) {
						sink("*");
					}
					sink("\n");
				}
			} else {
				// Boring old traditional string representation
				sink("BoxPrinter");
			}
		}
	}

	void main() {
		BoxPrinter box;
		writefln("%s", box);
		writefln("%6.5b", box);
		writefln("%3.2b", box);
		writefln("%7.4b", box);
	}

Here's the output:

	BoxPrinter
	******
	******
	******
	******
	******
	
	***
	***
	
	*******
	*******
	*******
	*******
	

As you can see, the width and precision parts of the custom %b specifier
has been reinterpreted into the dimensions of the box that will be
printed.  And when you specify %s, a traditional innocent-looking string
is printed instead. In effect, we have implemented our own custom format
specifier.

D rocks!!

Oh, and did I mention that D rocks?


T

-- 
Do not reason with the unreasonable; you lose by definition.
Jan 16 2013
next sibling parent =?UTF-8?B?QWxpIMOHZWhyZWxp?= <acehreli yahoo.com> writes:
On 01/16/2013 10:13 AM, H. S. Teoh wrote:

... lots of presentation material deleted... :)

 Do you see what's going on?
Maybe, maybe not, but I would like to listen to this at DConf 2013. ;) Seriously though, this is part of the material that you will be presenting there, right?
 D rocks!!
We know that! :p Ali
Jan 16 2013
prev sibling next sibling parent "monarch_dodra" <monarchdodra gmail.com> writes:
On Wednesday, 16 January 2013 at 18:15:21 UTC, H. S. Teoh wrote:
 D rocks!!

 Oh, and did I mention that D rocks?


 T
Niiiice. This was something that always bothered me about writefln for user types: Sure, you can print user types, but how do you customize output? Well, now I guess I know :)
Jan 16 2013
prev sibling next sibling parent reply "mist" <none none.none> writes:
Wow, cool stuff indeed.
Preparing for DConf? :)
Jan 16 2013
parent reply "Matej Nanut" <matejnanut gmail.com> writes:
On Wednesday, 16 January 2013 at 18:35:07 UTC, mist wrote:
 Wow, cool stuff indeed.
 Preparing for DConf? :)
That's really awesome. You should write a separate article about this so it doesn't get lost in the forums!
Jan 16 2013
parent reply Philippe Sigaud <philippe.sigaud gmail.com> writes:
On Wed, Jan 16, 2013 at 8:43 PM, Matej Nanut <matejnanut gmail.com> wrote:
 On Wednesday, 16 January 2013 at 18:35:07 UTC, mist wrote:
 Wow, cool stuff indeed.
 Preparing for DConf? :)
That's really awesome. You should write a separate article about this so it doesn't get lost in the forums!
What about creating a new page on the Wiki (D Rawks) and putting small articles in them? That way, we can all do it without having a website and newcomers can be shown its content.
Jan 16 2013
parent "Joshua Niehus" <jm.niehus gmail.com> writes:
On Wednesday, 16 January 2013 at 19:49:55 UTC, Philippe Sigaud 
wrote:
 What about creating a new page on the Wiki (D Rawks) and 
 putting small
 articles in them? That way, we can all do it without having a 
 website
 and newcomers can be shown its content.
http://wiki.dlang.org/D_Rocks
Jan 16 2013
prev sibling next sibling parent Walter Bright <newshound2 digitalmars.com> writes:
On 1/16/2013 10:13 AM, H. S. Teoh wrote:
 It's been a while since the last "D rocks!" post. So here's one.
This is most excellent. Please, please do this as a blog posting or such, and we can link to it on Reddit! Your article deserves a much wider audience than this n.g.
Jan 16 2013
prev sibling next sibling parent "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Wed, Jan 16, 2013 at 08:49:34PM +0100, Philippe Sigaud wrote:
[...]
 What about creating a new page on the Wiki (D Rawks) and putting small
 articles in them? That way, we can all do it without having a website
 and newcomers can be shown its content.
On Wed, Jan 16, 2013 at 11:47:44AM -0800, Walter Bright wrote: [...]
 This is most excellent. Please, please do this as a blog posting or
 such, and we can link to it on Reddit! Your article deserves a much
 wider audience than this n.g.
http://wiki.dlang.org/Defining_custom_print_format_specifiers T -- It is widely believed that reinventing the wheel is a waste of time; but I disagree: without wheel reinventers, we would be still be stuck with wooden horse-cart wheels.
Jan 16 2013
prev sibling parent Walter Bright <newshound2 digitalmars.com> writes:
On 1/16/2013 10:13 AM, H. S. Teoh wrote:
 It's been a while since the last "D rocks!" post. So here's one.

 I'm guessing that most D users don't realize extent of the flexibility
 of std.format -- I know I didn't until I discovered this little gem
 hidden in the docs (and then only implicitly!).
Now on reddit! http://www.reddit.com/r/programming/comments/16riz9/creating_custom_print_format_specifiers_in_d/
Jan 17 2013