DateIntervalFormatter is a Foundation class in iOS and macOS designed for formatting date ranges into localized strings: “Jul 21–25, 2024”, “July 21–25, 2024” or “21–25 July 2024”. According to Apple Developer Documentation, 2024, DateIntervalFormatter automatically determines which date components to repeat (if the range is within one month, the month is shown once), and localizes separators and word order. Unlike manually concatenating two formatted dates, this class takes into account the grammatical rules of the language. The class is available since iOS 8 and macOS 10.10.
Key Takeaways
DateIntervalFormatter is a subclass of Formatter in Foundation that takes a DateInterval object (a range between two dates) and returns a localized string describing that range. For example, for the range July 21 to July 25, 2024, the result would be “Jul 21–25, 2024” for en_US. The class smartly groups repeating elements: if both dates are in the same month, the month name is shown once.
The main value of DateIntervalFormatter is automatic handling of range grammar. When formatting manually, a developer would have to compare start/end month/year, choose a separator (dash, “to”, “bis”), and handle declension. DateIntervalFormatter does this automatically based on CLDR (Common Locale Data Repository) data, reducing bugs in international applications, especially for languages with complex grammar.
According to Unicode CLDR v44 (2024), date interval formatting rules vary significantly between languages. German uses the preposition “bis” (21.–25. Juli 2024), French uses “au” (du 21 au 25 juillet 2024), Japanese uses a hyphen (2024 Jul 21–25). DateIntervalFormatter covers all these variations without additional code on the developer side.
The working principle of DateIntervalFormatter is based on analyzing two dates forming a range and selecting the optimal string representation considering the locale. The class compares date components (year, month, day) and makes grouping decisions: if the year is the same, it is shown once; if the month is the same, it is shown once; if days differ, a day range is shown.
| Range | en_US |
|---|---|
| Single day | Jul 21, 2024 |
| Within a month | Jul 21–25, 2024 |
| Different months | Jul 21 – Aug 5, 2024 |
| Different years | Dec 25, 2024 – Jan 5, 2025 |
| With time | Jul 21, 2024, 2:30–4:45 PM |
Grouping logic is implemented based on CLDR templates. The formatter checks equality of each date component (Calendar.Component: year, month, day) in order from largest to smallest. Once a component differs, all smaller components are shown for both dates. For example, for July 21–25: year matches, month matches, day differs → month is shown once, days are shown for each date.
Important note: DateIntervalFormatter does not support customizing the separator via API. The separator is determined by the locale and cannot be overridden. If a non-standard separator is required (e.g., “through” instead of “to”), you would need to format dates manually using DateFormatter and concatenate strings with a custom separator.
DateIntervalFormatter provides two sets of styles: dateStyle and timeStyle for each end of the range. These styles work similarly to DateFormatter: .none hides the component, .short gives a short format (7/21/2024), .medium (Jul 21, 2024), .long (July 21, 2024), .full (Sunday, July 21, 2024). By combining dateStyle and timeStyle, you can achieve different levels of detail.
Important: dateStyle and timeStyle apply to both ends of the range equally. You cannot set different styles for the start and end (e.g., .medium for start and .short for end). If different styles are needed, you must format each date with a separate DateFormatter and concatenate the results.
import Foundation
let formatter = DateIntervalFormatter()
// Different style combinations
let startDate = Date()
let endDate = Date().addingTimeInterval(345600) // +4 days
let interval = DateInterval(start: startDate, end: endDate)
// Date only, medium format
formatter.dateStyle = .medium
formatter.timeStyle = .none
print("Date only: \(formatter.string(from: interval))")
// Short format with time
formatter.dateStyle = .short
formatter.timeStyle = .short
print("Short with time: \(formatter.string(from: interval))")
// Full format
formatter.dateStyle = .full
formatter.timeStyle = .none
print("Full date: \(formatter.string(from: interval))")
Basic usage of DateIntervalFormatter includes creating an instance, setting styles, and calling string(from:). The method accepts DateInterval, a structure with start and end properties. DateInterval can be created from two Date values or initialized with Duration (iOS 16+). After formatting, the result is ready for UI display without additional processing.
import Foundation
let calendar = Calendar.current
let now = Date()
// Specific interval examples
let examples: [(title: String, start: Date, end: Date)] = [
(
"2-hour event",
now,
now.addingTimeInterval(7200)
),
(
"Week-long vacation",
now,
now.addingTimeInterval(604800)
),
(
"Cross-year period",
calendar.date(from: DateComponents(
year: 2024, month: 12, day: 25
))!,
calendar.date(from: DateComponents(
year: 2025, month: 1, day: 5
))!
)
]
let intervalFormatter = DateIntervalFormatter()
intervalFormatter.dateStyle = .medium
intervalFormatter.timeStyle = .short
for (title, start, end) in examples {
let interval = DateInterval(start: start, end: end)
let result = intervalFormatter.string(from: interval)
print("\(title): \(result)")
}
// Format via Calendar (iOS 16+)
if let nextWeek = calendar.date(
byAdding: .day, value: 7, to: now
) {
let weekInterval = DateInterval(start: now, end: nextWeek)
print("Next 7 days: \(intervalFormatter.string(from: weekInterval))")
}
DateIntervalFormatter and Calendar Booking: in booking applications (hotels, tickets), date intervals are a key UI element. DateIntervalFormatter correctly handles cross-year intervals (e.g., December 28 to January 3), grouping month and year for each date separately. For such scenarios, it is recommended to use dateStyle = .medium and timeStyle = .none, the most readable format for dates without time.
import Foundation
let formatter = DateIntervalFormatter()
formatter.dateStyle = .long
formatter.timeStyle = .none
// Booking examples
struct Booking {
let checkIn: Date
let checkOut: Date
var formattedInterval: String {
let interval = DateInterval(start: checkIn, end: checkOut)
return formatter.string(from: interval)
}
}
let booking = Booking(
checkIn: calendar.date(from: DateComponents(
year: 2024, month: 7, day: 21
))!,
checkOut: calendar.date(from: DateComponents(
year: 2024, month: 7, day: 25
))!
)
print("Booking: \(booking.formattedInterval)")
// Use with different locales
let enFormatter = DateIntervalFormatter()
enFormatter.dateStyle = .medium
enFormatter.timeStyle = .none
enFormatter.locale = Locale(identifier: "en_US")
print("English: \(enFormatter.string(from: interval))")
DateInterval vs DateIntervalFormatter: DateInterval is a structure representing a date range (start and end). DateIntervalFormatter is a formatter that converts this structure into a string. DateInterval also supports contains(Date) checks and intersection(with:), which is useful for booking logic (checking date overlap).
DateIntervalFormatter localizes output through the locale property. By default, Locale.current is used, the device locale. To display intervals in another language (e.g., English content with a Russian interface), set the locale explicitly. Unlike DateFormatter, DateIntervalFormatter does not require setting locale for server-side data. It always uses its internal CLDR-based grouping logic.
Language differences in date interval formatting are significantly more complex than for single dates. English uses a simple dash: Jul 21–25. French uses the preposition “au”: du 21 au 25 juillet. German uses a period after the number and a dash: 21.–25. Juli. Japanese uses a hyphen without prepositions: Jul 21–25. DateIntervalFormatter covers all these variants.
import Foundation
let formatter = DateIntervalFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .none
let start = Date()
let end = Date().addingTimeInterval(259200) // +3 days
let interval = DateInterval(start: start, end: end)
// Demonstrate different locales
let localeIds = ["ru_RU", "en_US", "de_DE", "fr_FR", "ja_JP", "zh_CN"]
for localeId in localeIds {
formatter.locale = Locale(identifier: localeId)
print("\(localeId): \(formatter.string(from: interval))")
}
// Localization for time intervals
formatter.dateStyle = .none
formatter.timeStyle = .short
let todayStart = Date()
let todayEnd = Date().addingTimeInterval(3600)
let timeInterval = DateInterval(start: todayStart, end: todayEnd)
for localeId in ["ru_RU", "en_US", "de_DE"] {
formatter.locale = Locale(identifier: localeId)
print("\(localeId) time: \(formatter.string(from: timeInterval))")
}
Localization specifics: for English, DateIntervalFormatter uses the dash separator for short formats. For long formats, it uses “to” or a dash depending on the template. The format choice depends on the context: for a booking list use .medium, for detailed view use .long or .full.
DateIntervalFormatter fills the niche of formatting date ranges, which is not covered by other Foundation classes. DateFormatter formats single dates, RelativeDateTimeFormatter formats relative dates (ago/in), DateComponentsFormatter formats duration (2 hours 30 minutes). Each of these classes solves a specific task and cannot be used interchangeably.
| Class | Purpose | Output Example |
|---|---|---|
| DateIntervalFormatter | Date range | Jul 21–25, 2024 |
| DateFormatter | Single date | Jul 21, 2024 |
| RelativeDateTimeFormatter | Relative time | 3 days ago |
| DateComponentsFormatter | Duration | 2 hours 30 minutes |
| ISO8601DateFormatter | ISO 8601 format | 2024-07-21T14:30:00Z |
When to use DateIntervalFormatter: for displaying date ranges in UI — hotel booking, events, date filtering, operation history. For calculating duration between dates, use DateComponentsFormatter, not DateIntervalFormatter, the latter is not designed for displaying duration. DateFormatter should only be used for single dates, not for concatenating two formatted dates via string concatenation, as this leads to loss of separator localization.
Edge case: DateIntervalFormatter does not support displaying open-ended intervals (without an end date). If the end of the range is unknown (e.g., “from July 21”), use DateFormatter with the preposition “from”. Also, DateIntervalFormatter does not support intervals with time specified for different days. Time is applied to each date separately, which may look redundant for short intervals.
Frequently Asked Questions
DateIntervalFormatter is a Foundation class for formatting date ranges into localized strings. Available since iOS 8, it automatically groups repeating components (month, year) and selects the correct separator for the language.
It compares components from year to day. If the year matches, it is shown once. If the month matches, it is shown once. Differing days are shown as a range. This gives “Jul 21–25, 2024” instead of “Jul 21, 2024 – Jul 25, 2024”.
No, the separator is determined by the locale and cannot be customized via API. For a custom separator (e.g., “through” instead of “to”), format each date with a separate DateFormatter and concatenate strings manually.
DateIntervalFormatter formats a date range (two dates) as a single unit with component grouping. DateFormatter formats a single date. For ranges, always use DateIntervalFormatter as it provides correct separator localization.
Automatically — DateIntervalFormatter detects that years differ and outputs full dates for both ends: “Dec 25, 2024 – Jan 5, 2025”. No additional configuration is required, just set the dateStyle.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also