Uncategorized

Date Formatters

Date formatters have been around for a while. I’ve recently started trying out the new formatted method on Date. It’s pretty slick. Its neat how you can build up a format in a functional style.

One thing I was interested was the performance difference between DateFormatter and the new formatted method. So I created a bunch of unit tests to measure their performance:

final class FormatterTests: XCTestCase {

    
    private func iterate(numIterations: Int = 10000, codeToRun: () -> ()) {
        for _ in 0..<numIterations {
            codeToRun()
        }
    }
    
    func test_oldFormatter_whereCreatedFormattedAndUsedEveryIteration_noFormatting() {
        measure {
            iterate {
                let formatter = DateFormatter()
                let date = Date()
                let _ = formatter.string(from: date)
            }
        }
    }
    
    func test_oldFormatter_whereCreatedFormattedAndUsedEveryIteration_withMediumFormatting() {
        measure {
            iterate {
                let formatter = DateFormatter()
                let date = Date()
                formatter.dateStyle = .medium
                let _ = formatter.string(from: date)
            }
        }
    }
    
    func test_oldFormatter_whereCreatedFormattedAndUsedEveryIteration_withCustomFormatting() {
        measure {
            iterate {
                let formatter = DateFormatter()
                let date = Date()
                formatter.dateFormat = "MM/dd/YYYY"
                let _ = formatter.string(from: date)
            }
        }
    }
    
    func test_oldFormatter_whereCreatedFormattedAndUsedEveryIteration_ISO8601() {
        measure {
            iterate {
                let formatter = ISO8601DateFormatter()
                let date = Date()
                let _ = formatter.string(from: date)
            }
        }
    }
    
    func test_oldFormatter_whereCreatedFormattedOutsideOfLoop_noFormatting() {
        measure {
            let formatter = DateFormatter()
            let date = Date()
            
            iterate {
                let _ = formatter.string(from: date)
            }
        }
    }
    
    func test_oldFormatter_whereCreatedFormattedOutsideOfLoop_withMediumFormatting() {
        measure {
            let formatter = DateFormatter()
            let date = Date()
            formatter.dateStyle = .medium
            
            iterate {
                let _ = formatter.string(from: date)
            }
        }
    }
    
    func test_oldFormatter_whereCreatedFormattedOutsideOfLoop_withCustomFormatting() {
        measure {
            let formatter = DateFormatter()
            let date = Date()
            formatter.dateFormat = "MM/dd/YYYY"
            
            iterate {
                let _ = formatter.string(from: date)
            }
        }
    }
    
    func test_oldFormatter_whereCreatedFormattedAndUsedOustsideOfLoop_ISO8601() {
        measure {
            let formatter = ISO8601DateFormatter()
            let date = Date()
            iterate {
                let _ = formatter.string(from: date)
            }
        }
    }

    
    func test_newFormatter_whereCreatedFormattedAndUsedEveryIteration_noFormatting() {
        measure {
            iterate {
                let date = Date()
                let _ = date.formatted()
            }
        }
    }
    
    func test_newFormatter_whereCreatedFormattedAndUsedEveryIteration_withMediumFormatting() {
        measure {
            iterate {
                let date = Date()
            
                let _ = date.formatted(.dateTime.year().month().day()) // Dec 9, 2022
            }
        }
    }
    
    func test_newFormatter_whereCreatedFormattedAndUsedEveryIteration_withCustomFormatting() {
        measure {
            iterate {
                let date = Date()
                let _ = date.formatted(date: .numeric, time: .omitted)
            }
        }
    }
    
    func test_newFormatter_whereCreatedFormattedAndUsedEveryIteration_withISO8601() {
        measure {
            iterate {
                let date = Date()
                let _ = date.formatted(.iso8601)
            }
        }
    }
    
    func test_newFormatter_whereCreatedFormattedOutsideOfLoop_noFormatting() {
        measure {
            let date = Date()
            iterate {
                let _ = date.formatted()
            }
        }
    }
    
    func test_newFormatter_whereCreatedFormattedOutsideOfLoop_withMediumFormatting() {
        measure {
            let date = Date()
            let format = Date.FormatStyle.dateTime.month().day().year()
            iterate {
                let _ = date.formatted(format)
            }
        }
    }
    
    func test_newFormatter_whereCreatedFormattedOutsideOfLoop_withCustomFormatting() {
        measure {
            let date = Date()
            iterate {
                let _ = date.formatted(date: .numeric, time: .omitted)
            }
        }
    }
    
    func test_newFormatter_whereCreatedFormattedOutsideOfLoop_withISO8601() {
        measure {
            let date = Date()
            iterate {
                let _ = date.formatted(.iso8601)
            }
        }
    }

}

There are a bunch of different tests there and a single helper method. The helper method just called my testing code in a for loop X amount of times (set to 10,000 above). The tests test for different date formats:

  1. Unformatted: What happens when the formatter is simply created and used.
  2. Medium format: This is a preset format that looks something like this: Jan 18, 2022
  3. Custom format: This format sets a custom format with the string “MM/dd/YYYY” and yields 01/18/2022.
  4. ISO8601 format: This is the standard ISO 8601 format.

These formats are tested in four scenarios:

  1. Using DateFormatter, where the formatter is created in every loop iteration.
  2. Using DateFormatter, where the formatter is outside the loop, and just use to render the string from the date in every loop iteration.
  3. Using the new formatted method on date, where the everything is created in every loop iteration.
  4. Using the new formatted method on date, where the date is created out of the loop and (when possible) so is the formatting style.

Results

Below is a sample of my results. Note, that actual times would very by machine, number of loops, etc.

A few Interesting points:

  1. Notice using creating the old formatter is significantly slower when you recreate the formatter ever time you use it.
  2. Because the new formatter doesn’t allow creating much beforehand (with the exception for the medium format) the times inside and outside the loops are essentially identical.
  3. The ISO8601 format is slowest with the old formatter, but extremely fast with the new.
  4. Reusing an old formatter is significantly faster than using the new formatter. Probably because all the work is able to be done outside the loop and simply be reused.

Conclusions

The new formatted method on date is nifty and easy to use. However, if you are concerned about performance, it is better to create a date formatter and reuse it.