DateComponents doesn't seem to honor year - ios

I'm attempting to convert a Date to a DateComponents, change a few properties, and get a new Date back. I've written the following code:
import Foundation
var components = Calendar.current.dateComponents(in: .current, from: Date())
components.day = 7
components.month = 3
components.year = 1900
DateFormatter.localizedString(from: components.date!, dateStyle: .long, timeStyle: .long)
I've tested this in the America/Denver time zone/locale on an iOS Simulator on iOS 15.0 as well as a Swift REPL on my Mac running the latest macOS Big Sur and Xcode 13.0, and in both places, I get approximately the following output at the time of this writing:
March 7, 2021 at 9:38:13 AM MST
Everything about this is as expected, except for the year. I had explicitly set the year to be 1900, but the year in the output is 2021. How can I make DateComponents honor the year when generating a date, or how can I do this same kind of thing manually so it'll actually work?

If you get all components from a date with dateComponents(in:from:), you have to set also yearForWeekOfYear accordingly
components.yearForWeekOfYear = 1900
Or specify only the date and time components including calendar and timeZone you really need for example
var components = Calendar.current.dateComponents([.calendar, .timeZone, .year, .month, .day, .hour, .minute, .second], from: Date())
components.day = 7
components.month = 3
components.year = 1900
DateFormatter.localizedString(from: components.date!, dateStyle: .long, timeStyle: .long)

Note that you are getting all the date components, including weekOfYear, weekOfMonth, dayOfWeek, and most importantly, yearForWeekOfYear. When you set the date components to 1900-03-07, you did not set all those components correctly either, and when resolving a Date from a DateComponents that has such conflicting components, the algorithm just so happens to prefer the year they got from yearForWeekOfYear.
If you set those fields to nil, you will get the expected result:
var components = Calendar.current.dateComponents(in: .current, from: Date())
components.day = 7
components.month = 3
components.year = 1900
components.yearForWeekOfYear = nil
There will still be conflicts in the date components though, and although this doesn't cause a crash, I'm not sure if this behaviour will change in the future. I suggest that you get just the components you need. For example:
var components = Calendar.current.dateComponents([
.calendar, .timeZone, .era, .year, .month, .day, .hour, .minute, .second, .nanosecond
], from: Date())

Related

Setting Swift Date minutes with more than 60 returns nil

Very new to IOS, I need to set a Date hour and minutes, I do like this:
let date = Calendar.current.date(bySettingHour: prayerTimeHour, minute: prayerTimeMinutes + Int(adjustement)! , second: 0, of: Date())!
prayerTimeMinutes + Int(adjustement)! in some cases are more than 60 so the app crash because the Date is nil
Let the Calendar do all the math for you. There is no such time as "09:61" and Calendar is telling you so.
// It's not clear that you want the current calendar here. It may not be Gregorian.
let calendar = Calendar(identifier: .gregorian)
// Using ! means you're promising prayerTimeHour and prayerTimeMinutes are valid.
let baseDate = calendar.date(bySettingHour: prayerTimeHour,
minute: prayerTimeMinutes,
second: 0,
of: Date())!
// ! should be safe because the Gregorian calendar will have a date for any number
// of minutes into the future
let adjustedData = calendar.date(byAdding: .minute,
value: adjustement,
to: baseDate)!

Infinite loop on Calendar.current.date(bySetting:value:of:)

I just ran into something strange with the Calendar API when trying to get the first day of a particular month.
import Foundation
//2017-12-01 23:00:00 +0000
let date = Date(timeIntervalSinceReferenceDate: 533862000.0)
let secondDate = Calendar.current.date(bySetting: .day, value: 2, of: date)
let thirdDate = Calendar.current.date(bySetting: .day, value: 1, of: date)
print("Passed")
In a Swift 4 Playground, the property observer on the right displays values for date and secondDate, but enters in an infinite loop when evaluating the let thirdDate = ... expression.
Activity Indicator reports of 99% CPU usage and RAM usage increases at around +100MB/second, and so does the Debug Navigator in XCode when ran inside an iOS project (until the app is killed at around 1.3GB RAM usage)
This is not happening for all dates, but is consistent with this particular date.
I've gone through the docs of Calendar but I can't figure out why this happens.
I came up with a workaround like this :
let date = Calendar.current.startOfDay(for: someDate)
var dc = Calendar.current.dateComponents([.month, .year, .day], from: date)
dc.day = 1
let firstDayOfMonth = Calendar.current.date(from: dc)
But I'd very much like to get an explanation as to why Calendar.current.date(bySetting: .day, value: 1, of: date) is failing that hard. Is there something I missed here?
Edit: it happens for the timezone GMT+2, but doesn't anymore if I change my system settings to GMT+1.

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

Consider the following code:
import UIKit
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 nycDayOfEra = nycCalendar.ordinality(of: .day, in: .era, for: now)
let nycDayOfYear = nycCalendar.ordinality(of: .day, in: .year, for: now)
var nzDayOfEra = nzCalendar.ordinality(of: .day, in: .era, for: now)
var nzDayOfYear = nzCalendar.ordinality(of: .day, in: .year, for: now)
As I write this, NYC time and Aukland NZ time give different days. That's the case I'm interested in.
With the code above, the results for nycDayOfYear and nzDayOfYear are different (as of this writing I get nycDayOfYear=42 and nzDayOfYear=43.)
That is as expected, and as desired. (I was working to answer a "how do I calculate the number of days of difference in two Dates evaluated in different time zones?" question.)
However, it would take a bunch of messy adjustments to make the above day-of-year calculation and figure out the number of days of difference between those local dates when they span year boundaries.
I therefore tried to do the calculations using ordinality(of: .day, in: .era, for: date).
However, the calculations based on calendar era give the same value regardless of the time zone of the calendar used to make do the calculation.
Why is that?
What would be a simpler way to calculate the number of calendar days difference between two dates WHEN EXPRESSED IN DIFFERENT LOCAL TIME ZONES? Like I said, my code that calculates the day of year would need additional logic added to handle dates that span calendar year boundaries.
Note that this is a different question than "How many days difference is there between 2 dates". In my question I want both dates to be expressed in different local time zones, and I'm interested in the difference in the calendar date of each of those date values.
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)

DateComponents giving wrong date

I am trying to convert today's date using DateComponents:
let calendar = Calendar.current
//add today's date
var todayDate = Date()
var dateComponents = calendar.dateComponents([.day, .month, .year], from: todayDate)
dateComponents.timeZone = TimeZone.current
todayDate = calendar.date(from: dateComponents)!
While debugging I found that after declaring todayDate, its value was 2016-11-11 07:44:44 +0000. After using dateComponents, the value changed to 2016-11-10 18:30:00 +0000. Whereas according to my location, the day should be 11th November, and the time should be somewhere between 1 or 2 PM. Why is this happening?
You haven't specified a timezone, nor hours minutes and seconds in your components. So, all these values are assumed to be zero. The resulting time is at midnight GMT on the year, month and day that you specified.
When you printed the result, you specified your current timezone, which appears to be 5h 30m different.
I'm guessing you are in India.

How to get the correct time Using NSDate with components in swift

When i get the components like seconds in:
NSCalendar.currentCalendar().components(.Second, fromDate: savedDate, toDate: todayDate, options: []).second
I get the seconds since that date.
But i am trying to get all the components like days and hours it gives me the total in each one of them.
If i get the hours lets say 36h and the days will show one day.
1 I want it to show 1 day and 12 hours.
2. How to start a counter or a timer with these components?
You have to specify all units in the components parameter you want to be considered.
let components = NSCalendar.currentCalendar().components([.Second, .Minute, .Hour, .Day], fromDate: savedDate, toDate: todayDate, options: [])
print(components)
Alternatively use NSDateComponentsFormatter, it can create a (localized) string. Specify the units to be displayed and the style.
let formatter = NSDateComponentsFormatter()
formatter.allowedUnits = [.Day, .Hour, .Minute, .Second ]
formatter.unitsStyle = .Full
let dateString = formatter.stringFromDate(savedDate, toDate: todayDate)!
print(dateString)
Regarding your second question let the counter count in seconds and format the seconds for human reading accordingly.

Resources