Why Is Calendar.Date(From: Datecomponents) Adding Time

Why is Calendar.date(from: DateComponents) adding time?

NSDate has some strange and undocumented behaviors for ancient dates. The change seems to have happened around 1895:

for year in 1890..<1900 {
// January 1 of each year @ 9AM
let dateComponents = DateComponents(
calendar: .current,
timeZone: Calendar.current.timeZone,
year: year,
month: 1,
day: 1,
hour: 9)

if dateComponents.isValidDate {
print(dateComponents.date!)
}
}

My calendar is Gregorian and timezone is EDT (UTC -0500). This is the output:

1890-01-01 14:17:32 +0000
1891-01-01 14:17:32 +0000
1892-01-01 14:17:32 +0000
1893-01-01 14:17:32 +0000
1894-01-01 14:17:32 +0000 // not correct
1895-01-01 14:00:00 +0000 // correct
1896-01-01 14:00:00 +0000
1897-01-01 14:00:00 +0000
1898-01-01 14:00:00 +0000
1899-01-01 14:00:00 +0000

So for the years prior to 1895, Apple somehow added 17 minutes and 32 second to my time. You got a different offset, which is likely due your locale settings.

I couldn't find anything historical event about the Gregorian calendar in 1895. This question mentions that Britain started to switch over to GMT and the Greenwich Observatory started adjusting date/time standards across the British Isles in the 1890s so that may have accounted for this offset. Perhaps someone can delve into the source code for Date / NSDate and figure it out?


If you want to use DateComponent to store a repeating schedule, use nextDate(after:matching:matchingPolicy:) to find the next occurance of your schedule:

let dateComponents = DateComponents(calendar: .current, timeZone: .current, hour: 9, weekday: 2)

// 9AM of the next Monday
let nextOccurance = Calendar.current.nextDate(after: Date(), matching: dateComponents, matchingPolicy: .nextTime)!

DateComponents giving wrong hour

I was able to reproduce the behaviour by using:

let from = Date(timeIntervalSince1970: 1630062455)
print(from) // 2021-08-27 11:07:35 +0000
let to = Date(timeIntervalSince1970: 1637110800)
print(to) // 2021-11-17 01:00:00 +0000
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(identifier: "Europe/London")!
let comp = calendar.dateComponents([.day, .hour, .minute, .second], from: from, to: to)
print(comp.day!, comp.hour!, comp.minute!, comp.second!)

The reason why this happens is because when doing dateComponents(_:from:to:), Calendar takes into account its timezone. After all, without a timezone, (almost) no date components would make sense - you would not be able to tell what hour a Date is, for example. A Date just represents an instant in time/n seconds since the epoch.

(In the case of Calendar.current, the timezone it uses is TimeZone.current)

Europe/London would go out of DST at some point between from and to. This means the calendar would calculate the difference in date components between:

from: 2021-08-27 12:07:35
to: 2021-11-17 01:00:00

Notice that the first time is 12:07:35, rather than 11:07:35. This is because at 2021-08-27 11:07:35 +0000, the local date time at Europe/London really is 2021-08-27 12:07:35.

To get your desired output, just change the calendar's timeZone to UTC:

var calendar = Calendar.current
calendar.timeZone = TimeZone(identifier: "UTC")!
let comp = calendar.dateComponents([.day, .hour, .minute, .second], from: from, to: to)

Swift DateComponents contains wrong hour value

The issue there is that you are adding the secondsFromGMT to your current timeZone. A date is just a point in time. The date (now) is the same anywhere in the world.

let nowDate = Date()
print(nowDate.description(with: .current)) // Saturday, February 27, 2021 at 4:51:10 PM Brasilia Standard Time
print(nowDate) // 2021-02-27 19:51:10 +0000 (+0000 means UTC time / zero seconds offset from GMT)

let triggerDate = nowDate + 30 // seconds
print(triggerDate) // 2021-02-27 19:51:40 +0000 (30 seconds after nowDate at UTC (GMT)

let dateFromTriggerDate = Calendar.current.dateComponents([.calendar, .year, .month, .day, .hour, .minute, .second], from: triggerDate).date!
print(dateFromTriggerDate.description(with: .current)) // Saturday, February 27, 2021 at 4:51:40 PM Brasilia Standard Time

day for Date() and Calendar.dateComponents don't match up

It's because of time zone difference. date will return the UTC time and date but calendar will return the date and time based on your device's time zone. If you need the day number in UTC just set the time zone of the calendar object to UTC after you create it:

let calendar = Calendar.current
calendar.timeZone = TimeZone(abbreviation: "UTC")!

Now it will always match the value that is returned by Date()

Date from Calendar.dateComponents returning nil in Swift

If you want to strip off the time portion of a date (set it to midnight), then you can use Calendar startOfDay:

let date = Calendar.current.startOfDay(for: Date())

This will give you midnight local time for the current date.

If you want midnight of the current date for a different timezone, create a new Calendar instance and set its timeZone as needed.

Creating a date with DateComponents

A couple of observations:

  1. In the Gregorian calendar, weekday = 1 means Sunday; weekday = 2 means Monday; etc. You can look at calendar.maximumRange(of: .weekday) to get the range of valid values, and you can look at calendar.weekdaySymbols to see what these weekDay values mean (e.g. “Sun”, “Mon”, “Tue”, “Wed”, “Thu”, “Fri”, and “Sat”).

  2. You said:

    I also want to have the entire day and not 16:00.

    A Date object references a moment in time. So it can’t represent an “entire day”. But it can represent midnight (and midnight in your time zone is likely 4pm in GMT/UTC/Zulu).

    You can, alternatively, return a DateInterval, which does represent a range of time.

    func interval(ofWeek week: Int, in year: Int) -> DateInterval {
    let calendar = Calendar.current

    let date = DateComponents(calendar: calendar, weekOfYear: week, yearForWeekOfYear: year).date!
    return calendar.dateInterval(of: .weekOfYear, for: date)!
    }

    And then

    let formatter = DateIntervalFormatter()
    formatter.dateStyle = .short
    formatter.timeStyle = .short

    let year = Calendar.current.component(.year, from: Date())
    let dateInterval = interval(ofWeek: 2, in: year)
    print(formatter.string(from: dateInterval))

    In a US locale, the interval starts on January 6th:

    1/6/19, 12:00 AM – 1/13/19, 12:00 AM

    Whereas in a German locale, the interval starts on the 7th:

    07.01.19, 00:00 – 14.01.19, 00:00

  3. If you want the start of the first day of the week and the last day of the week, you can do:

    func startAndEndDate(ofWeek week: Int, in year: Int) -> (Date, Date) {
    let date = DateComponents(calendar: calendar, weekOfYear: week, yearForWeekOfYear: year).date!
    let lastDate = calendar.date(byAdding: .day, value: 6, to: date)!

    return (date, lastDate)
    }

    And then

    let formatter = DateFormatter()
    formatter.dateStyle = .short

    let year = Calendar.current.component(.year, from: Date())
    let (start, end) = startAndEndDate(ofWeek: 2, in: year)
    print(formatter.string(from: start), "-", formatter.string(from: end))

Swift: unexpected behavior when setting DateComponents year

If you log now and dc you will see the problem. now is being created from a Date. This fills in all of the date components including yearForWeekOfYear and several of the weekday related components. These components are causing modDate to come out incorrectly.

newDate works as expected because only the specific components are being set.

You can get modDate to come out correctly if you reset some of the extra components. Specifically, adding:

now.yearForWeekOfYear = nil

just before creating modDate will result in the expected date for modDate. Of course the best solution is to create a new instance of DateComponents and use specific values from the previous DateComponents as needed:

let mod = DateComponents()
mod.timeZone = now.timeZone
mod.year = 2010
mod.month = 2
mod.day = 24
mod.hour = now.hour
mod.minute = 0
mod.second = now.second
print("\nModified Date:")
print("\(mod.month!)/\(mod.day!)/\(mod.year!) \(mod.hour!):\(mod.minute!):\(mod.second!) \(mod.timeZone!)")
let modDate = calendar.date(from: mod)
print("\(modDate!)")

Why does `ordinality(of: .day, in: .era, for: date)` give the same result for 2 dates in different time zones?

Martin's comment about calendar calculations over long intervals giving unexpected results is as good an answer as any as to why it doesn't work.

I did come up with code that calculates the desired difference in calendar date values between 2 dates expressed in specific time zones:

let date = Date()

guard let nycTimeZone = TimeZone(abbreviation: "EST"),
let nzTimeZone = TimeZone(abbreviation: "NZDT") else {
fatalError()
}
var nycCalendar = Calendar(identifier: .gregorian)
nycCalendar.timeZone = nycTimeZone
var nzCalendar = Calendar(identifier: .gregorian)
nzCalendar.timeZone = nzTimeZone

let now = Date()


let nycDateComponents = nycCalendar.dateComponents([.month, .day, .year], from: now)
let nzDateComponents = nzCalendar.dateComponents([.month, .day, .year], from: now)

let difference = Calendar.current.dateComponents([.day],
from: nycDateComponents,
to: nzDateComponents)

let daysDifference = difference.days

First I convert the 2 dates to month/day/year DateComponents values using calendars set to their specific time zone.

Then I use the Calendar function dateComponents(_:from:to:), which lets you calculate the difference between 2 DateComponents values, in whatever units you want to use to compare them. (days, in this case)



Related Topics



Leave a reply



Submit