How expensive is DateFormatter

⋅ 13 min read ⋅ DateFormatter Optimization Swift

Part one in a series on DateFormatter performance. In the first part, we will answer how expensive is DateFormatter and which operation is costly.

  1. How expensive is DateFormatter
  2. How to use DateFormatter in Swift

If you are working on iOS for long enough, there is a chance that you might have known one performance tip about DateFormatter. That is, creating it is an expensive operation.

Creating DateFormatter is an expensive operation.

Everyone around me seems to know this tip. I know it, even though I can't remember where I got this tip from, maybe one of WWDC sessions. Since then, I'm fully aware every time I use DateFormatter.

What I don't know is how slow it is and which part that it is expensive. We are going to find out together in this article. I will use XCTest's measure method (open func measure(_ block: () -> Void)) as a tool to gauge the performance. It might not be the right tool, but it can give us a rough idea of how expensive each operation is.

The following is a boilerplate of how we are going to test

class DateFormatterTests: XCTestCase {
let numberOfIterations = 1000

func testHypothesis() {
self.measure {
for _ in (0..<numberOfIterations) {
// Our hypothesis
}
}
}
}

Experiment #1: How expensive is DateFormatter creation?

To test this, we try to compare the creation of DateFormatter instance with others. My candidates are DateFormatter, Date, NSString, and UIView.

func testDateFormatterCreation() throws {
self.measure {
for _ in (0..<numberOfIterations) {
let df = DateFormatter()
}
}
}

func testDateCreation() throws {
self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
}
}
}

func testNSStringCreation() throws {
self.measure {
for _ in (0..<numberOfIterations) {
let foo = NSString(format: "foo")
}
}
}

func testViewCreation() throws {
self.measure {
for _ in (0..<numberOfIterations) {
let view = UIView()
}
}
}

Result:

Creation Time(milliseconds)
Date 0.834
DateFormatter 1.370
NSString 2.220
UIView 4.880

As you can see, there is no significant time difference between DateFormatter and other instances.

Experiment #2: How expensive is DateFormatter when using

Since the initialization process is not a problem, we will try to call common methods that we always use with DateFormatter, date(from: String) and string(from: Date).

func testDateFormatterCreationAndPrint() throws {
self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
let df = DateFormatter()
let dateString = df.string(from: date)
}
}
}

func testDateFormatterCreationAndPrintWithCache() throws {
let df = DateFormatter()

self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
let dateString = df.string(from: date)
}
}
}

func testDateFormatterCreationAndParse() throws {
self.measure {
for _ in (0..<numberOfIterations) {
let df = DateFormatter()
let date = df.date(from: "30/01/2020")
}
}
}

func testDateFormatterCreationAndParseWithCache() throws {
let df = DateFormatter()

self.measure {
for _ in (0..<numberOfIterations) {
let date = df.date(from: "30/01/2020")
}
}
}

Result:

Creation Time(milliseconds)
string(from: Date) 44.100
string(from: Date) with reuse DateFormatter 1.870
date(from: String) 90.800
date(from: String) with reuse DateFormatter 30.400

The result surprised me. Since initializing of DateFormatter is 1.37 milliseconds and calling of string(from: Date) with reuse DateFormatter is 1.87, I expected testDateFormatterCreationAndPrint to be somewhere around 3.24 milliseconds (1.37 + 1.87). Turn out it took much more than that, ~14x more to be exact (44.1).

I guess that DateFormatter has some lazy implementation, which will be executing once it needs to do the actual calculation. Moving DateFormatter out of the running loop significantly reduce execution time because we avoid expensive setup after each call of string(from: Date).

From the result, we can get a rough estimation of each operation.

Operations Time(milliseconds)
DateFormatter set up 42.230 ~ 60.400 (44.1 - 1.87 and 90.8 - 30.4)
string(from: Date) 1.870
date(from: String) 30.400

Let's go back to our previous results and update them with our new information.

Updated Result:

Creation Time(milliseconds)
Date 0.834
DateFormatter 1.370 to 42.230 ~ 60.400
NSString 2.220
UIView 4.880

It becomes more clear why Apple said DateFormatter creation is expensive. It isn't visible at first because of the lazy implementation of DateFormatter.

Experiment #3: How expensive is DateFormatter with customization

Now we know that there is some processing cost of the first initializing DateFormatter. Let's see if changing property like calendar, timezone, locale, dateFormat, and dateStyle would cause the same cost or not.

func testDateFormatterCreationAndSetCalendarAndPrint() throws {
self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
let df = DateFormatter()
df.calendar = Calendar(identifier: .buddhist)
let dateString = df.string(from: date)
}
}
}


func testDateFormatterCreationAndSetCalendarAndPrintWithCache() throws {
let df = DateFormatter()

self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
df.calendar = Calendar(identifier: .buddhist)
let dateString = df.string(from: date)
}
}
}

func testDateFormatterCreationAndSetTimezoneAndPrint() throws {
self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
let df = DateFormatter()
df.timeZone = TimeZone(secondsFromGMT: 60 * 60 * 7)
let dateString = df.string(from: date)
}
}
}


func testDateFormatterCreationAndSetTimezoneAndPrintWithCache() throws {
let df = DateFormatter()

self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
df.timeZone = TimeZone(secondsFromGMT: 60 * 60 * 7)
let dateString = df.string(from: date)
}
}
}

func testDateFormatterCreationAndSetLocaleAndPrint() throws {
self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
let df = DateFormatter()
df.locale = Locale(identifier: "th")
let dateString = df.string(from: date)
}
}
}


func testDateFormatterCreationAndSetLocaleAndPrintWithCache() throws {
let df = DateFormatter()

self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
df.locale = Locale(identifier: "th")
let dateString = df.string(from: date)
}
}
}

func testDateFormatterCreationAndSetDateFormatAndPrint() throws {
self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
let df = DateFormatter()
df.dateFormat = "dd"
let dateString = df.string(from: date)
}
}
}


func testDateFormatterCreationAndSetDateFormatAndPrintWithCache() throws {
let df = DateFormatter()

self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
df.dateFormat = "dd"
let dateString = df.string(from: date)
}
}
}

func testDateFormatterCreationAndSetDateStyleAndPrint() throws {
self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
let df = DateFormatter()
df.dateStyle = .long
df.timeStyle = .long
let dateString = df.string(from: date)
}
}
}

func testDateFormatterCreationAndSetDateStyleAndPrintWithCache() throws {
let df = DateFormatter()

self.measure {
for _ in (0..<numberOfIterations) {
let date = Date()
df.dateStyle = .long
df.timeStyle = .long
let dateString = df.string(from: date)
}
}
}

Results:

Operations Time(milliseconds)
Set calendar 164.000
Set calendar with reuse DateFormatter 14.100
Set timeZone 129.000
Set timeZone with reuse DateFormatter 5.430
Set locale 47.200
Set locale with reuse DateFormatter 2.130
Set dateFormat 55.100
Set dateFormat with reuse DateFormatter 2.680
Set dateStyle and timeStyle 84.300
Set dateStyle and timeStyle with reuse DateFormatter 5.120

Reference:

Operations Time(milliseconds)
string(from: Date) 44.100
string(from: Date) with reuse DateFormatter 1.870

We can tell that each property setting can take an extra amount of processing time from the result.

Estimation of extra processing time needed for each operation.

Operation without reuse of DateFormatter - 90.8 (string(from: Date))

Operations Time(milliseconds)
Set calendar 119.900
Set locale 3.100
Set timezone 84.900
Set dateFormat 11.000
Set dateStyle and timeStyle 40.200

One thing to highlight here is that this execution time doesn't add up when we reuse DateFormatter, which means there is some kind of checking that prevents reconfiguring DateFormatter if the same value is set.

I suspect that there is something like this, which is the right thing to do.

var calendar: Calendar! {
didSet {
guard calendar != oldValue else {
return
}
// Expensive operation
}
}

Experiment #4: How expensive is changing DateFormatter properties

To get the cost of changing DateFormatter property, we need to avoid setting the same value. I redesign our test to something like this.

We reduce the number of iterations by two (as we double the operations) and change value twice, so the next loop would gurantee that setting value will trigger the reconfiguration.

func testDateFormatterCreationAndSetTimezoneAndPrintBackAndForth() throws {
self.measure {
for _ in (0..<numberOfIterations/2) {
let date = Date()
let df = DateFormatter()

df.timeZone = TimeZone(secondsFromGMT: 60 * 60 * 7)
_ = df.string(from: date)

df.timeZone = TimeZone(secondsFromGMT: 60 * 60 * 6)
_ = df.string(from: date)
}
}
}


func testDateFormatterCreationAndSetTimezoneAndPrintWithCacheBackAndForth() throws {
let df = DateFormatter()

self.measure {
for _ in (0..<numberOfIterations/2) {
let date = Date()
df.timeZone = TimeZone(secondsFromGMT: 60 * 60 * 7)
_ = df.string(from: date)

df.timeZone = TimeZone(secondsFromGMT: 60 * 60 * 6)
_ = df.string(from: date)
}
}
}

func testDateFormatterCreationAndSetDateFormatAndPrintBackAndForth() throws {
self.measure {
for _ in (0..<numberOfIterations/2) {
let date = Date()
let df = DateFormatter()
df.dateFormat = "dd"
let dateString = df.string(from: date)

df.dateFormat = "dd MM"
let dateString2 = df.string(from: date)
}
}
}


func testDateFormatterCreationAndSetDateFormatAndPrintWithCacheBackAndForth() throws {
let df = DateFormatter()

self.measure {
for _ in (0..<numberOfIterations/2) {
let date = Date()
df.dateFormat = "dd"
let dateString = df.string(from: date)

df.dateFormat = "dd MM"
let dateString2 = df.string(from: date)
}
}
}

func testDateFormatterCreationAndSetCalendarAndPrintBackAndForth() throws {
self.measure {
for _ in (0..<numberOfIterations/2) {
let date = Date()
let df = DateFormatter()
df.calendar = Calendar(identifier: .buddhist)
let dateString = df.string(from: date)
df.calendar = Calendar(identifier: .gregorian)
let dateString2 = df.string(from: date)
}
}
}


func testDateFormatterCreationAndSetCalendarAndPrintWithCacheBackAndForth() throws {
let df = DateFormatter()

self.measure {
for _ in (0..<numberOfIterations/2) {
let date = Date()
df.calendar = Calendar(identifier: .buddhist)
let dateString = df.string(from: date)
df.calendar = Calendar(identifier: .gregorian)
let dateString2 = df.string(from: date)
}
}
}

func testDateFormatterCreationAndSetLocaleAndPrintBackAndForth() throws {
self.measure {
for _ in (0..<numberOfIterations/2) {
let date = Date()
let df = DateFormatter()

df.locale = Locale(identifier: "th_TH")
let dateString = df.string(from: date)

df.locale = Locale(identifier: "en_US")
let dateString2 = df.string(from: date)
}
}
}


func testDateFormatterCreationAndSetLocaleAndPrintWithCacheBackAndForth() throws {
let df = DateFormatter()

self.measure {
for _ in (0..<numberOfIterations/2) {
let date = Date()
df.locale = Locale(identifier: "th_TH")
let dateString = df.string(from: date)

df.locale = Locale(identifier: "en_US")
let dateString2 = df.string(from: date)
}
}
}

func testDateFormatterCreationAndSetDateStyleAndPrintBackAndForth() throws {
self.measure {
for _ in (0..<numberOfIterations/2) {
let date = Date()
let df = DateFormatter()

df.dateStyle = .long
df.timeStyle = .long
let dateString = df.string(from: date)

df.dateStyle = .short
df.timeStyle = .short
let dateString2 = df.string(from: date)
}
}
}


func testDateFormatterCreationAndSetDateStyleAndPrintWithCacheBackAndForth() throws {
let df = DateFormatter()

self.measure {
for _ in (0..<numberOfIterations/2) {
let date = Date()
df.dateStyle = .long
df.timeStyle = .long
let dateString = df.string(from: date)

df.dateStyle = .short
df.timeStyle = .short
let dateString2 = df.string(from: date)
}
}
}

Results:

Operations Time(milliseconds)
Set and change timeZone 105.000
Set and change timeZone with reuse DateFormatter 81.600
Set and change dateFormat 32.500
Set and change dateFormat with reuse DateFormatter 2.440
Set and change calendar 138.000
Set and change calendar with reuse DateFormatter 101.000
Set and change locale 48.200
Set and change locale with reuse DateFormatter 48.500
Set and change dateStyle 70.900
Set and change dateStyle with reuse DateFormatter 69.300

Reference of setting property without reuse of DateFormatter:

Operations Time(milliseconds)
Set timeZone 129.000
Set dateFormat 55.100
Set calendar 164.000
Set locale 47.200
Set dateStyle 84.300

As you can see, changing some properties back and forth can cost us the same amount of time no matter we reuse DateFormatter or not.

Conclusion

This is quite a long article full of stats and numbers. The key take away from all these stats are:

  1. DateFormatter is expensive to create.
  2. DateFormatter is expensive to change.
  3. Subsequence use of string(from: Date) is cheap.
  4. Subsequence use of date(from: String) isn't cheap.

Rough estimation of operations:

Operations Time(milliseconds)
DateFormatter creation 42.230 ~ 60.400
Change timeZone 81.600
Change dateFormat 2.440
Change calendar 101.000
Change locale 48.500
Change dateStyle 69.300
Use of date(from: String) 30.400
Use of string(from: Date) 1.870

When I say expensive, it doesn't mean your app would cause a memory warning just by using DateFormatter. The hardware of new devices is far superior to the past. You might not get a noticeable lag even when you don't reuse DateFormatter, but I think it is good to save as much as you can even you have plenty of resources to do so.

In the next article, we will use this knowledge and see how we should use DateFormatter in our code.


Read more article about DateFormatter, Optimization, Swift, or see all available topic

Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.

If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.

Become a patron Tweet Share
Previous
How to get the first N elements of array in Swift

Learn a few ways to do it and things you should know when using them.

Next
How to fix "Build input file cannot be found" error in Xcode

There might be several reasons that cause this error. I will share one solution that fixes the one that happened to me the most.

← Home