Swift 5 Interpolation Part 3: Dates and Number Formatters

I’ve already posted about using string interpolation to customize Optional presentation and for coercing integer values via radix and to conditionalize text, and yet there is still so much more you can do with this new language feature.

Consider formatters. Both Swift and Cocoa/Cocoa touch support a number of these, ranging from numbers and currency to dates and times. They are a natural interpolation fit.

For example, dates are easy right out of the box. Just pass a formatter and you’re done. Note that I skip the formatter label. You’ll see why in just a second.

public extension String.StringInterpolation {
  mutating func appendInterpolation(_ value: Date, _ formatter: DateFormatter) {
    appendLiteral(formatter.string(from: value))
  }
}

For dates, it’s more about how do you create formatters than about interpolating them. Here’s the design I’m using right now:

public extension DateFormatter {
  /// Returns an initialized `DateFormatter` instance
  static func format(date: Style, time: Style) -> DateFormatter {
    let formatter = DateFormatter()
    formatter.locale = Locale.current
    (formatter.dateStyle, formatter.timeStyle) = (date, time)
    return formatter
  }
}

For example:

print("\(Date(), .format(date: .short, time: .none))") // 12/16/18

I left off the formatter: label because the call to format documents the role of the second argument.

I initially went with a convenience initializer, along the same lines, but the calls were uglier:

print("\(Date(), DateFormatter(date: .none, time: .long))") // 8:20:15 AM MST

I also tried static preset properties and option sets, as well as dateStyle:timeStyle: interpolation arguments as but none of them really felt as readable as the format(date:time:) approach.

Number formats are a bit harder as they only work with NSNumber, which means you either have to create interpolators for both BinaryInteger and FloatingPoint, or you have to use some magic. Brent Royal-Gordon tried helping me work with _ObjectiveCBridgeable but I couldn’t get it to work. For some reason, Swift just wouldn’t pick up, for example, Double.pi as a bridgeable value.

For right now, I have this, which I’m not particularly happy with:

public extension String.StringInterpolation {
  /// Interpolates a floating point value using a
  /// number formatter.
  mutating func appendInterpolation<Value: FloatingPoint>(_ number: Value, formatter: NumberFormatter) {
    if
      let value = number as? NSNumber,
      let string = formatter.string(from: value) {
      appendLiteral(string)
    } else {
      appendLiteral("Unformattable<\(number)>")
    }
  }
  
  /// Interpolates an integer value using a number formatter
  mutating func appendInterpolation<Value: BinaryInteger>(_ number: Value, formatter: NumberFormatter) {
    if
      let value = number as? NSNumber,
      let string = formatter.string(from: value) {
      appendLiteral(string)
    } else {
      appendLiteral("Unformattable<\(number)>")
    }
  }
}

Creating number and currency formatters with NumberFormatter() is an ugly and tedious task. For example, here’s a formatter that sets rounding, localization, padding, and digit count:

let formatter = NumberFormatter()
formatter.localizesFormat = true
formatter.roundingMode = .halfUp
formatter.minimumFractionDigits = 1
formatter.maximumFractionDigits = 4
formatter.paddingPosition = .beforePrefix
formatter.paddingCharacter = "0"
formatter.minimumIntegerDigits = 5

"\(Double.pi, formatter: formatter)" // 00003.1416
"\(5, formatter: formatter)" // 00005.0

There really should be better and Swiftier ways to express these formatting preferences. For one thing, the minimum and maximum stuff could be expressed as ranges specific to the whole and fractional parts. I’m not convinced there should be just one formatter type devoted to handling integers, floating point numbers, and currency. Why not three?

Integers, to my best understanding, don’t need to worry about localization. And why not throw radix control into the IntegerFormatter mix while you’re at it? I’d imagine a well-designed integer formatter would easily replace my radix/prefix/bytewise/width interpolation that I used in a recent post.

In fact, I think I’ll design exactly that. Off to Xcode…

Some fantastic coverage of interpolation from Olivier Halligon here: Part 1 and Part 2.

My prosaic kicking the wheels repo is over at GitHub.

(And I’m trying to redesign Olivier’s attributed strings because somehow I feel they should be enumeration cases with associated value payloads rather than static members.)

3 Comments

  • I’d understood that formatters are expensive to initialize (certainly date formats are That of which Man is not meant to know); and therefore should be cached if possible. I’ve been known to keep a Dictionary to be populated on demand for fraction digits.

    Is this no longer the case, or negligible nowadays? I’d be glad to reduce (or confine) the noise.

  • If you’d like to format strings using enigma with associated value payloads, you may be able to use BonMot, which has a setup along those lines. I’m certainly eager to see how it works with interpolation! You can find it here.

  • Oops, “enigma” → “enums.”