JATemplate
String formatting that’s convenient and less evil than printf-style formatting.
Install / Use
/learn @JensAyton/JATemplateREADME
Manifesto
Software that communicates with users often needs to insert dynamic data into strings for presentation. Cocoa Foundation’s solution for this is printf()–style formatting, which is fundamentally unsuitable for the task, for two reasons:
- There are many formatting options, none of which are suitable for producing well-formatted prose text.
- The interpretation of data on the stack, including its length, is specified in the formatting string itself. This means that format strings loaded from data can crash your application. This is problematic for integrated localization, and a deal-breaker for other use cases such as sandboxed plug-ins.
The C standard library has a third problem: the %n specifier can be used to write arbitrary data onto the stack, which is serious business. Foundation does not implement %n, but malicious format strings can still be used to read data you didn’t intend to expose, or simply crash your app.
In short, I feel that printf() and +[NSString stringWithFormat:] should be deprecated. For producing text in formal languages for computer consumption, I suggest a fully-fledged template system such as MGTemplateEngine. But for logging, presenting alerts, and hacking together command-line tools, printf()-style formatting wins on convenience. This is an attempt at beating printf()on its own ground.
JATemplate
JATemplate provides a family of macros and functions for inserting variables into strings. Convenience wrappers are provided for using it in conjunction with NSLog(), NSAssert() and -[NSMutableString stringByAppendingString:].
JATemplate is currently experimental. The syntax and operators are in flux, and I’m not satisfied with the robustness of the parser. That said, fuzz testing has repeatedly found a crashing bug in CoreFoundation and/or ICU, but no crashes, assertions or unexpected warnings in JATemplate itself. It is certainly far safer than +[NSString stringWithFormat:].
To date, it has only been tested on Mac OS X 10.8 with ARC. Some formatting operators have known incompatibilities with Mac OS X 10.7 and iOS 5.
Basic usage
NSString *flavor = @"strawberry";
NSString *size = @"large";
unsigned scoopCount = 3;
NSString *message = JATExpand(@"My {size} ice cream tastes of {flavor} and has {scoopCount} scoops!", flavor, size, scoopCount);
Because easy internationalization is a central goal in the design, JATExpand() looks up the format string (template) in Localizable.strings by default. There are variants to control this behaviour.
Templates can directly refer to variables by name, but they only have access to variables specified at the call site. Parameters can also be referred to by position; in the example, {0} could be used instead of {flavor}. Name references are less error-prone and easier to localize, but positional references allow you to refer to an expression without creating a temporary variable. This is particularly useful in logging and assertions, which are less likely to be localized anyway.
The default behaviour for numerical parameters is to format them with NSNumberFormatter’s NSNumberFormatterDecimalStyle. If scoopCount is set to 1000 in the example above, it is printed as 1,000 in English locales.
Parameters may be Objective-C objects, any C number type, C strings, C++ std::strings (in Objective-C++), NSPoints, NSSizes, NSRects, NSRanges, CFStrings, CFNumbers or CFBooleans. Support for other types can easily be added; see Customization below.
The most important feature of the design is that even though JATExpand() et al. are variadic, the number of arguments passed is fixed at compile time, and their types are all known. If a format string that refers to a non-existent parameter, either by name or by index, it will simply not be expanded.
Formatting
The formatting of expanded parameters can be modified by appending formatting operators, separated by a pipe character:
NSString *intensifier = @"really";
NSString *message = JATExpand(@"I {intensifier|uppercase} like ice cream!", intensifier);
// Produces “I REALLY like ice cream!”
Multiple operators can be chained together in the obvious fashion. Operators may optionally take an argument, separated by a colon. By convention, operators that need to split the argument into parts use semicolons as a separator.
NSString *message = JATExpand(@"Pi is about {0|round|num:spellout}.", M_PI);
// Produces “Pi is about three.”
// BUG 2013-02-01: Some people’s ice cream only has one scoop. :-(
// FIX: support pluralization.
NSString *message = JATExpand(@"My {size} ice cream tastes of {flavor} and has {scoopCount} {scoopCount|plural:scoop.;scoops!}", flavor, size, scoopCount);
For the full set of built-in operators, see Built-in operators below. The num: operator and the pluralization operators are particularly important.
Variants
The full list of string expanding functions and macros, and their notional signatures, is as follows. All variadic arguments (...) actually take a series of zero or more objects, and are type safe (as much as pointers in C are in general).
NSString *JATExpand(NSString *template, ...)— Looks uptemplatein Localizable.strings in the same manner asNSLocalizedString(), then expands substitution expressions in the resulting template using the provided parameters.NSString *JATExpandLiteral(NSString *template, ...)— LikeJATExpand(), but skips the localization step.NSString *JATExpandFromTable(NSString *template, NSString *table, ...)— LikeJATExpand(), but looks up the template in the specified .strings file (likeNSLocalizedStringFromTable()). The table name should not include the .strings extension.NSString *JATExpandFromTableInBundle(NSString *template, NSString *table, NSBundle *bundle ...)— LikeJATExpand(), but looks up the template in the specified .strings file and bundle (likeNSLocalizedStringFromTableInBundle()).NSString *JATExpandWithParameters(NSString *template, NSDictionary *parameters)– LikeJATExpand(), but passes the parameters in a dictionary. “Positional” parameters in this case are looked up usingNSNumbers as keys.NSString *JATExpandLiteralWithParameters(NSString *template, NSDictionary *parameters)– LikeJATExpandWithParameters(), but without the localization step.NSString *JATExpandFromTableWithParameters(NSString *template, NSString *table, NSDictionary *parameters)andNSString *JATExpandFromTableInBundleWithParameters(NSString *template, NSString *table, NSBundle *bundle, NSDictionary *parameters)— they exist.void JATAppend(NSMutableString *string, NSString *template, ...),void JATAppendLiteral(NSMutableString *string, NSString *template, ...),void JATAppendFromTable(NSMutableString *string, NSString *template, NSString *table, ...),void JATAppendFromTableInBundle(NSMutableString *string, NSString *template, NSString *table, NSBundle *bundle, ...)— append an expanded template to a mutable string; Equivalent to[string appendString:JATExpand*(template, ...)].void JATLog(NSString *template, ...)— performs non-localized expansion and sends the result toNSLog().void JATPrint(NSString *template, ...)andvoid JATPrintLiteral(NSString *template, ...)– Write to stdout, likeprintf().void JATErrorPrint(NSString *template, ...)andvoid JATErrorPrintLiteral(NSString *template, ...)– Write to stderr, likefprintf(stderr, ...).JATAssert(condition, template, ...)andJATCAssert(condition, template, ...)— wrappers forNSAssert()andNSCAssert()which perform template expansion on failure.
Customization
There are three major ways to customize JATemplate: custom coercion methods, custom operators, and custom casting handlers.
The three coercion methods in the protocol <JATCoercible> are used by operators and the template expansion system to interpret parameters as particular types. They are implemented on NSObject and a few other classes, but can be overridden to customize the treatment of your own classes.
-jatemplateCoerceToStringreturns anNSString *. In addition to being used by operators, it is used by the template expander to produce the final string that will be inserted into the template. The default implementation calls-description. It is overridden forNSStringto returnself, forNSNumberto useNSNumberFormatterDecimalStyle, forNSNullto return@"(null)", and forNSArrayto return a comma-separated list.-jatemplateCoerceToNumberreturns anNSNumber *. The default implementation will look for methods-(double)doubleValue,-(float)floatValue,-(NSInteger)integerValueor-(int)intValue, in that order. If none of these is found, it returnsnil, which causes expansion to fail. It is overridden byNSNumberto returnself.-jatemplateCoerceToBooleanreturns anNSNumber *which is treated as a boolean. The default implementation calls-(BOOL)boolValueif implemented, otherwise returnsnil. Overridden byNSNullto return@NO.
Operators are implemented as methods following this template:
-(id <JATCoercible>)jatemplatePerform_{operator}_withArgument:(NSString *)argument
variables:(NSDictionary *)variables
The receiver is the object being formatted – either one of the parameters to the template or the result of a previous operator in a chain. (For a nil parameter, the operator message is sent to [NSNull null].) The argument is the string following the colon in the operator invocation, or nil if there was no colon. The variables dictionary contains all the parameters to the expansion operation; named parameters are addressed with NSString keys, and positiona
