How to create custom `IntegerFormatStyle` formatter in Foundation?

Is it possible to create custom styles with the new Foundation formatters that were introduced in WWDC 2021? Currently, you could format numbers like this:

print(40.formatted(.percent))
// 40%
    
print(40.formatted(.currency(code: "eur")))
// €40.00
    
print(0.2948485.formatted(.number.precision(.significantDigits(2))))
// 0.29

I want to create a new formatter that takes a number and converts to a formatted date component string. It would be great if this is already built in but I could not figure out the correct syntax if it exists. I would like to do something like this:

print(25.formatted(.number.dateComponent(style: .short, field: .minutes)))
// 25 min

For reference, this could be done the "old" way like this:

static let formatter: DateComponentsFormatter = {
    let formatter = DateComponentsFormatter()
    formatter.unitsStyle = .short
    formatter.allowedUnits = [.minute]
    return formatter
}()

print(Self.formatter.string(for: DateComponents(minute: 25)))
// 25 min

Can this be achieved in Xcode 13 / Swift 5.5 yet?

1 Like

You can create your own format styles. Here’s a simple example that reverses the digits of an integer:

struct ReversedIntegerStyle: FormatStyle {
  typealias FormatInput = Int
  typealias FormatOutput = String

  func format(_ value: Int) -> String {
    String(String(value).reversed())
  }
}

extension FormatStyle where Self == ReversedIntegerStyle {
  static var reversed: ReversedIntegerStyle { .init() }
}

25.formatted(.reversed) // "25"
(-42).formatted(.reversed) // "24-"

I haven’t tried to replicate your desired output.

3 Likes

Whoa thanks for the head start!! Below is my attempt in case it helps anyone:

struct MinuteFormatStyle: FormatStyle {
    let style: DateComponentsFormatter.UnitsStyle

    func format(_ value: Int) -> String? {
        let formatter = DateComponentsFormatter()
        formatter.unitsStyle = style
        formatter.allowedUnits = [.minute]

        return formatter.string(for: DateComponents(minute: value))
    }
}

extension FormatStyle where Self == MinuteFormatStyle {
    static func minute(style: DateComponentsFormatter.UnitsStyle) -> Self {
        MinuteFormatStyle(style: style)
    }
}

extension DateComponentsFormatter.UnitsStyle: Codable, Hashable {}

25.formatted(.minute(style: .short)) // "25 mins"

Although, there's a lot of things I don't like about my implementation. What intrigued me most about the WWDC session is we no longer have to cache formatters statically which we were forced to do over the years to get around the heavy formatter instantiation. I was hoping this could be leveraged in the API. Maybe that's why FormatStyle needs to be Codable?, but nothing for certain about this.

Is it possible to make ReversedIntegerStyle generic so it can be use for anything using String(describing:)?

Something like this:

struct ReversedStyle<Subject>: FormatStyle {
// no need for this, can just be inferred
//  typealias FormatInput = Subject
//  typealias FormatOutput = String

  func format(_ value: Subject) -> String {
      String(String(describing: value).reversed())
  }
}

// works!
25.formatted(ReversedStyle())
(-42).formatted(ReversedStyle())
"abcdefg".formatted(ReversedStyle())
(123.456).formatted(ReversedStyle())

// how to do this for static memeber lookup syntax?
extension FormatStyle where Self == ReversedStyle<???> {
    static var reversed: ReversedStyle<???> { .init() }
}

25.formatted(.reversed)
(-42).formatted(.reversed)

I get it to work:

extension FormatStyle where Self == ReversedStyle<Any> {
    static func reversed<Subject>() -> ReversedStyle<Subject> { .init() }
}

25.formatted(.reversed())
(-42).formatted(.reversed())

var can't be generic? I had to make it a static func

I thought I heard "we have converted every existing formatters"?

Yes, there is Date.ComponentsFormatStyle

import Foundation

// all FormatStyle implementations:
// https://developer.apple.com/documentation/foundation/formatstyle?language=_5#relationships
// Date.ComponentsFormatStyle
// https://developer.apple.com/documentation/foundation/date/componentsformatstyle?language=_5

let now = Date.now
// why this doesn't work?
(now..<Date(timeInterval: 25 * 60, since: now)).formatted(.components(style: .abbreviated, fields: [.minute]))
//                                                                                                       ^^^^^^^  <==== doesn't like this?!
(now..<Date(timeInterval: 25 * 60, since: now)).formatted(Date.ComponentsFormatStyle(style: .abbreviated, fields: [.minute]))
// 25 min

I suppose you could store the formatter in a static variable to keep it alive, or even implement some kind of lookup table for different formatter configurations if that’s important for your use case.

(I never know if instantiating or (re-)configuring the formatter is the expensive operation, but I suspect it’s often the latter. If that’s true, just keeping the instance around may not help with performance if your formatted implementation regularly reconfigures it.)

1 Like

Shouldn’t this be used instead?

https://developer.apple.com/documentation/foundation/date/componentsformatstyle?language=_5

The caching is all internal to the new formatting implementation at this time. Mostly because designing the huge API surface of all the styles themselves was enough of a challenge! Exposing the caches themselves so you can use it in a scenario like this is an interesting idea. I'll file a bug for you.

3 Likes