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:
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 atcalendar.weekdaySymbols
to see what theseweekDay
values mean (e.g. “Sun”, “Mon”, “Tue”, “Wed”, “Thu”, “Fri”, and “Sat”).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
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
How to Handle Hash Collisions for Dictionaries in Swift
Why Does Swift Not Allow Stored Properties in Extensions
Swift Operator "*" Throwing Error on Two Ints
How to Implement Protocol Methods That Return Covariant Selfs
Multi-Face Detection in Realitykit
Compare Three Values for Equality
Uitextview Font to Always Be Fixed Size
Xcode 12 Beta and iOS 14: Weird Console Logs "Objc[5551]: Class ... Is Implemented in Both"
Swift 4: Nsfilenamespboardtype Not Available. What to Use Instead for Registerfordraggedtypes
Swift 2: !, ? -" Value of Optional Type "..." Not Unwrapped"
How to Handle Two Different Types in an Array in Swift for a Uitableview
Open a Filedialog in Swiftui on MACos
Using Uiapplicationdelegateadaptor to Get Callbacks from Userdidacceptcloudkitsharewith Not Working
Swift 2 to 3 Migration Dispatch_Get_Global_Queue
Swift Mutable Set: Duplicate Element Found
Why Swift Throws Error When Using Optional Param in Closure Func