SkillAgentSearch skills...

JATemplate

String formatting that’s convenient and less evil than printf-style formatting.

Install / Use

/learn @JensAyton/JATemplate
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

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 up template in Localizable.strings in the same manner as NSLocalizedString(), then expands substitution expressions in the resulting template using the provided parameters.
  • NSString *JATExpandLiteral(NSString *template, ...) — Like JATExpand(), but skips the localization step.
  • NSString *JATExpandFromTable(NSString *template, NSString *table, ...) — Like JATExpand(), but looks up the template in the specified .strings file (like NSLocalizedStringFromTable()). The table name should not include the .strings extension.
  • NSString *JATExpandFromTableInBundle(NSString *template, NSString *table, NSBundle *bundle ...) — Like JATExpand(), but looks up the template in the specified .strings file and bundle (like NSLocalizedStringFromTableInBundle()).
  • NSString *JATExpandWithParameters(NSString *template, NSDictionary *parameters) – Like JATExpand(), but passes the parameters in a dictionary. “Positional” parameters in this case are looked up using NSNumbers as keys.
  • NSString *JATExpandLiteralWithParameters(NSString *template, NSDictionary *parameters) – Like JATExpandWithParameters(), but without the localization step.
  • NSString *JATExpandFromTableWithParameters(NSString *template, NSString *table, NSDictionary *parameters) and NSString *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 to NSLog().
  • void JATPrint(NSString *template, ...) and void JATPrintLiteral(NSString *template, ...) – Write to stdout, like printf().
  • void JATErrorPrint(NSString *template, ...) and void JATErrorPrintLiteral(NSString *template, ...) – Write to stderr, like fprintf(stderr, ...).
  • JATAssert(condition, template, ...) and JATCAssert(condition, template, ...) — wrappers for NSAssert() and NSCAssert() 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.

  • -jatemplateCoerceToString returns an NSString *. 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 for NSString to return self, for NSNumber to use NSNumberFormatterDecimalStyle, for NSNull to return @"(null)", and for NSArray to return a comma-separated list.
  • -jatemplateCoerceToNumber returns an NSNumber *. The default implementation will look for methods -(double)doubleValue, -(float)floatValue, -(NSInteger)integerValue or -(int)intValue, in that order. If none of these is found, it returns nil, which causes expansion to fail. It is overridden by NSNumber to return self.
  • -jatemplateCoerceToBoolean returns an NSNumber * which is treated as a boolean. The default implementation calls -(BOOL)boolValue if implemented, otherwise returns nil. Overridden by NSNull to 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

View on GitHub
GitHub Stars30
CategoryDevelopment
Updated6y ago
Forks1

Languages

Objective-C

Security Score

60/100

Audited on Apr 26, 2019

No findings