Date component extraction shifts by one day across DST boundary
0 reputation · 10 Feb 2022, 18:07 UTC
0 reputation · 10 Feb 2022, 18:07 UTC
Extract consistent year, month, and day components from an NSDate for a user's configured region, then format the same date for display using NSDateFormatter.
NSCalendar and NSDateFormatter each accept an NSTimeZone independently. When the calendar's time zone differs from the formatter's time zone — for example, a calendar pinned to UTC while the formatter uses the device's local zone — component extraction can yield a day value that disagrees with the formatted string during daylight-saving transitions. The documentation does not prescribe a canonical pattern for keeping these two time zones in sync, and legacy codebases often mix GMT and UTC identifiers, which behave differently on older Foundation releases.
NSCalendar component extraction and NSDateFormatter string output always agree on the calendar day for a given NSDate?GMT versus UTC identifier choice affect this alignment on macOS 10.14 and iOS 12?35275 reputation · 11 Feb 2022, 01:30 UTC
Use the same NSTimeZone for the NSCalendar and the NSDateFormatter whenever the extracted year/month/day and the formatted string must describe the same calendar day. On macOS 10.14 and iOS 12, the one-day shift is almost always caused by the two objects using different time zones, not by NSDate itself or by DST rules.
An NSDate is an absolute instant. NSCalendar.dateComponents(from:) and NSDateFormatter.string(from:) both convert that instant into calendar fields or text using their own timeZone. If the calendar is pinned to UTC while the formatter uses the device zone, the same instant can produce different day values. DST makes this visible because the local offset changes, so the local day boundary moves relative to UTC.
The symptom matches a calendar/formatter zone mismatch: one object is using GMT/UTC and the other is using a regional zone such as America/New_York. The fix is not to special-case DST dates; it is to make the zone explicit and shared for the operation that must agree.
NSTimeZone and assign it to both calendar.timeZone and formatter.timeZone.Yes, but only when they intentionally answer different questions. A UTC calendar can answer “what UTC day is this instant?” while a local formatter answers “what local day should the user see?” Those outputs should not be expected to match. If one UI label needs both, derive both from the same NSDate using the same zone, or produce the local string from the local components.
On those versions, "GMT" and "UTC" are generally offset-zero zones without DST, so the identifier choice is not the likely cause of the shift. The risk is older SDKs, iOS 11 and earlier, or custom time zone databases. Avoid mixing hard-coded "GMT"/"UTC" with regional identifiers when you want DST awareness; use the same IANA identifier for both calendar and formatter.
Test with an instant near a DST transition. With both objects on the same zone, the day from dateComponents should match the day in formatter.string(from:). Then repeat with the calendar on UTC and the formatter on a regional zone to confirm the outputs intentionally differ.
let zone = TimeZone(identifier: "America/New_York")!
calendar.timeZone = zone
formatter.timeZone = zone
let components = calendar.dateComponents([.year, .month, .day], from: date)
let text = formatter.string(from: date)
// Assert components.day matches the day parsed from text.
This assumes Foundation on macOS 10.14/iOS 12, the Gregorian calendar, no third-party time zone database, and matching locales. If the formatter locale differs from the calendar locale, month names can differ even when the day matches. If you target iOS 11 or earlier or use custom time zone data, verify the GMT/UTC behavior separately; that single detail would change the GMT/UTC guidance.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.