How to get the number of days since NSDate's Reference Date? - swift

I saw in the Apple's documentation about NSDate that Date objects represents an invariant time interval relative to an absolute reference date. How can I get the number of days since that Reference Date?
Edit: I saw solutions for a similar question, but I'm searching for a solution that don't force me to enter specific dates.

You can use calendar method dateComponents from Date to Date and pass only the day component:
let date = Date(timeIntervalSinceReferenceDate: 0) // "2001-01-01 00:00:00 +0000"
let days = Calendar.current.dateComponents([.day], from: date, to: Date()).day // 5971

Related

Working with Date() in Swift and having issues adjusting timezones for storing and reading back in Firestore/Firebase

I am storing dates from a Swift Project in Firestore without any problem. Dates are converted into UTC format and stored in Firestore as a Timestamp. All good.
Then, back on clientside, I can read them back and apply the TimeZone.current and the date/time are adjusted accordingly based on the timezone the user is currently in.
So, as en example, a time originally of:
9:00 pm Melbourne time (which is GMT+10),
shows as 7:00 am if the user is in New York.
Great.
But I have some items that I want to adjust for timezones (as per above) and others I don't.
So say I have two items the same as the above example, but one is an alarm and I want to keep at the time it was originally set for regardless of the new timezone... So still keep it at 9:00 pm.
I have a Bool flag saved in my database to say ignoreTimezone but I'm lost as to how to do this in Swift when reading back the timestamp from Firestore in UTC format and get it back to the original 9:00 pm.
All the Q&A's I've found are all about converting timezones etc. but not really on this example of ignoring one and keeping the date and time set to the timezone they were originally set for.
Thanks in advance for any help and/or suggestions.
Question updated as recommended
I have now incorporated the suggested code. So have a calendar extension:
extension Calendar {
func offsetFromMidnight(for date: Date) -> TimeInterval {
return date.timeIntervalSince(startOfDay(for: date))
}
}
Then I carry out the recommended steps.
Take an offset from midnight, in this case, the current Date():
let offsetSinceMidnight = UInt64(Calendar.current.offsetFromMidnight(for: Date()))
This value is then stored on the server.
I'm currently in Melbourne (Australia), so the date and time item used for testing is July 9 # 2:00pm.
When it is retrieved on the client end in a different timezone, I'm using the recommended code:
//Create a calendar for the target timezone
guard let chicagoTimeZone = TimeZone(identifier: "America/Chicago") else { fatalError() }
var chicagoCalendar = Calendar(identifier: .gregorian)
chicagoCalendar.timeZone = chicagoTimeZone
//Calculate midngiht in the target calendar
let chicagoMidnight = chicagoCalendar.startOfDay(for: Date())
//calculate the same time-of-day in the new timezone
let adjustedChicagoTime = Date(timeInterval: TimeInterval(offsetSinceMidnight), since: chicagoMidnight)
The output is set to the correct time, 2:00pm in Chicago, but because of the differnent dates (Chicago is still July 8th), then the midnight timeinterval is being applied on the wrong date. So I get July 8 # 2:00pm.
I'm assuming I will also need to capture the original date components to apply the offsetSinceMidnight to a date in the newTimeZone that has matching date components??? Or is there a better approach to this?
Date objects store an instant in time, anywhere in the world. They don't capture the idea of a time-of-day regardless of time zone.
To do that I would suggest calculating an offsetFromMidnight value.
Edited to fix return value.
extension Calendar {
func offsetFromMidnight(for date: Date) -> TimeInterval {
return date.timeIntervalSince(startOfDay(for: date))
}
}
You'd call that function in the user's current calendar to get the seconds since midnight in the user's current time zone. Save that to your database. (You could round to a long integer with very little loss of precision.)
I happen to BE in the NYT time zone (EDT) so using that as the destination time zone won't work for me since it won't change anything. Instead, I'll show code to convert from my timezone to GMT:
//Run on user's local machine (in EDT in my case):
let offsetSinceMidnight = UInt64(Calendar.current.offsetFromMidnight(for: Date()))
//Save offset to FireStore
Then if you want that same time of day in a new timezone, you'd use code like this:
//Create a calendar for the target time zone (or the user's local time zone on the destination machine)
guard let gmt = TimeZone(abbreviation: "GMT") else { fatalError() }
var gmtCalendar = Calendar(identifier: .gregorian)
gmtCalendar.timeZone = gmt
//Read time offset from FireStore
let offsetFromNYC = Calendar.current.offsetFromMidnight(for: Date())
//Calculate midnight in target calendar
let gmtMidnight = gmtCalendar.startOfDay(for: Date())
//Calculate the same time-of-day in the GMT time zone
let gmtTimeToday = Date(timeInterval: TimeInterval(offsetSinceMidnight), since: gmtMidnight)
print(gmtTimeToday)
Note that the above will give you the same hours/minutes/seconds as the offsetFromMidnight time.
Edit:
If your goal is to set an alarm to the next future time-of-day in the local time zone, you'd need to add logic to check if the computed date/time is in the past and adjust:
//Change adjustedChicagoTime to a var
var adjustedChicagoTime = Date(timeInterval: TimeInterval(offsetSinceMidnight), since: chicagoMidnight)
//If the alarm time is in the past, add a day to the date.
if adjustedChicagoTime < Date() {
adjustedChicagoTime = Calendar.current.date(byAdding: .day,
value: 1, to: adjustedChicagoTime, wrappingComponents: false)
}
Edit #2:
After a back-and-forth, it sounds like you sometimes want to save a date and time that's independent of time zone, like 9:30 AM on 10 July. If I create that date in EDT, and you view it in Melborne, it's ALWAYS 9:30 AM on 10 July.
Other times, you want to upload and download dates & times that honor time zones.
In order to easily do both, I would suggest saving 2 different string date/time fields to FireStore, one with a time zone, and one without. The one with timezone (or rather offset from GMT) would capture a moment in time around the world, and could be converted to a local time.
The one without time zone would describe a day/month/year/hours/minutes in local time.
You could generate/parse those strings in Swift using date formatters like this:
let baseFormatString = "YYYY-MM-dd'T'HH:mm"
let timeZoneFormatString = baseFormatString + "ZZZ"
let noTimeZoneFormatter = DateFormatter()
noTimeZoneFormatter.dateFormat = baseFormatString
let timeZoneFormatter = DateFormatter()
timeZoneFormatter.dateFormat = timeZoneFormatString
Note that by default a date formatter uses the system's time zone, so the "no time zone formatter" would assume the local time zone. If you use it to convert a date string to a date, it will assume the date is in the local time zone.

How to ask a user for an accurate date in a date format

So I have a program which I need to ask a user for a date and time. But that date has to either be in the future or the the very far past. And I have tried a UIDatePicker but have found out that a date picker isn't the best way to tend to this sort of problem because it requires a lot of scrolling, and the it doesn't load past a certain date for some reason. I was thinking of a UIPickerView that asks for Year, Month, Day, and Time. But I don't know how to do it because lets say that the month is September there is only 30 days so then how can I stop the user from selecting 31. So if anyone can help me that would be amazing. And if you have any questions or need for code just ask. Oh and by the way I would like it in a date format where I can compare it to other date.
Given the year and month, you can calculate the array of possible day values to be supplied to your picker like so:
let calendar = Calendar.current
let components = DateComponents(year: year, month: month, day: 1)
guard let date = calendar.date(from: components),
let range = calendar.range(of: .day, in: .month, for: date) else { return }
let days = Array(range)
I would like it in a date format where I can compare it to other date.
Once you have the year, month, and day, hour (converted to 24 hour value, from 0 to 23), you can build the Date object like so:
let calendar = Calendar.current
let components = DateComponents(year: year, month: month, day: day, hour: hour, minute: minute)
guard let date = calendar.date(from: components) else { return }
That date can now be compared to any other Date object.

How do I "combine" an un-zoned date and a time zone?

My question is very similar to Get "time with time zone" from "time without time zone" and the time zone name (I think). I just want to do it in Swift.
Anyway, I am trying to write a function with the following signature:
func combine(_ date: Date, with timeZone: TimeZone) -> Date?
What it does is that it basically takes in a date and returns a "zoned" date. If the date does not exist in the time zone, it returns nil.
To avoid being an XY question, here is a screenshot:
I'm asking the user for a date and a time zone and I want to combine these two into one single Date.
I'll try my best to explain. I will express dates in the format of timeIntervalFrom1970 to make it as clear as possible.
Say I pass in 0 as the date and GMT-1 as the time zone, it'll return 3600. 0 is 1970-1-1 00:00:00. 1970-1-1 00:00:00 in GMT-1 is 1970-1-1 01:00:00 in GMT, which is 3600.
This is how I tried to implement this:
return date.addingTimeInterval(-timeZone.secondsFromGMT(for: date))
This seems to work most but not all of the time. However, I don't think it returns the correct results if DST gets involved and the whole thing becomes messy. It also feels "math-ish". I would prefer an approach without math, using only the Foundation API methods.
So, How can I implement this method?
In your example, the API gives you a Date, but you want to interpret
that as "2017/08/18 8:08" in some given time zone. Assuming that
the eureka forms UI element uses the timezone of the current calendar
for display, you can convert the date to DateComponents, and back
to a Date with a different timezone.
func combine(_ date: Date, with timeZone: TimeZone) -> Date? {
var cal = Calendar.current
let comp = cal.dateComponents([.era, .year, .month, .day, .hour, .minute, .second], from: date)
cal.timeZone = timeZone
return cal.date(from: comp)
}
nil will be returned if the day/time combination does not exist
in the other timezone.

How to get date range with a month of calendar with DateToolsSwift?

So the purpose is to create a month view calendar with collection view. Today is the starting point, so I will get the first and last day of month in calendar from today.
func setDate(date: Date) {
let firstDate = date.start(of: .month)
let lastDate = date.end(of: .month)
}
Say today is 2017-07-19, instead of first date return 25th of June it return 30th of June and last date return 31st of July instead of 5th of August.
I could solve this by using the pure Swift Date Library, but I believe that this task could be solved by DateTools. I just don't know how.
By the way I'm using Swift 3.
DateTools Swift

Swift: Get correct time zone from Date Picker?

I am trying to get the correct time zone from the date picker in swift using time formatter, it's not working. I'm getting UTC, not EST.
1) If I print dateFormatter.stringFromDate(datePicker) I get EST, but
2) I don't need a string, I need an NSDate in EST so
3) I can use it to get the timeIntervalSinceDate(NSDate) in EST.
My trick of trying to take it from string back to NSDate as seen below didn't work. It's still in UTC and the time interval since date is not right.
dateFormatter.locale = NSLocale.currentLocale()
dateFormatter.timeZone = NSTimeZone.localTimeZone()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let date: NSDate = dateFormatter.dateFromString(dateFormatter.stringFromDate(datePicker))!
print(date)
print(date.timeIntervalSinceDate(datePicker))
The above answer is totally wrong. Date picker report the date in system locale anytime, so, if the datePicker shows an 08:00 Time and you are GMT+2, the property date of the picker will be 06:00.
So for have the absolute value of the datePicker you have to pass to him the UTC time zone in view did load with:
datePicker.timeZone = TimeZone.init(identifier: "UTC")
Now, the date property of the picker will be the expected and choosen one.
You cannot "get a time zone" from a date picker. You can just get a date. The date will be independent on the current time zone of the device.
Perhaps you think you have a different date, but actually, there is no such thing as a "UTC date" or "EST date". Instead, there is only one date, and you use date formatters to display them for various time zones.
Note that there is quite a bit of redundancy in your code. The default locale and time zone of a date formatter are already the same values that you set. Also, when you have a method that returns a NSDate you do not have annotate the constant with : NSDate, making your code more verbose and cluttered.
Note that if you print a date the console will always show UTC. e.g.
let date = NSDate() // Nov 10, 9:44 PM
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "YYYY-MM-dd hh:mm a"
let dateString = dateFormatter.stringFromDate(date) // "2015-11-10 09:44 PM"
print(date) // "2015-11-10 20:44:54 +0000\n"
To set the TimeZone of a DatePicker to UTC use:
datePicker.timeZone = TimeZone.init(identifier: "UTC")
Notice the camelcase notation of "timeZone".
Unfortunately I don't have enough credit to comment on the last post, which has it almost right, so I had to create a new answer.
A little trivia: TimeZone has been around since iOS 2.0 as is stated here:
https://developer.apple.com/documentation/uikit/uidatepicker/1615976-timezone
Those who are trying to find a solution and are not able to wrap their head around the exact issue. Here is what something I tried:
Use time.addTimeInterval(-14400) function, where -14400 is the 4 hours difference. So if you want UTC to EST do this.
Something like this:-
var time = Date() // assuming you have this in UTC
time.addTimeInterval(-14400)
You can also use addingTimeInterval function which returns you the new date.
I know this is a little wonky cause we're manually doing this, but hope it helps someone.