Swift NSDate showing UTC when printing in debugger

2019-09-20 09:50发布

问题:

I'm trying to write some code for a scheduling app and these schedules need to be based on the user's current time. Example, something is available between 5pm and 9pm on the current day. When I get the current time by using NSDate(), printing it results in utc: 2016-12-15 13:12:05 +0000. I know that it's in utc because if I add my local time zone offset in my head, it works out to be the actual time on my phone. The problem I'm having is when trying to create these time windows, sometimes the end time goes into the next day because there's no minus offset. Example, the user's time zone offset -5 and and it's currently 9pm local time. This will show 2am the next day which won't work because I need it to be the same day. How do you go about managing this kind of thing? I'm not sure if the print() function is even printing the right date. Is it adjusting to my machine's time zone? I need to see actual dates because I have to deal with time zones all over the world. I tried changing to a +5 times zone and all my scheduling code breaks. How can I ensure that my scheduler will always work with the user's local time?

If someone has done this kind of thing, it would be helpful if you can show how to create a start date, end date, and then how to compare with current date to see if it does or does not fall within the window. This needs to work for all time zones.

Edit: Here's something I don't understand. How do I handle dates that are in the form of a string like this: "2016-12-15T22:16:11+05:00" where the date and time is in "local time"? I know that the date is in local time because that's what was displayed on my phone when I generated it (on Dec 15 10pm). Now that I have that date, I want to know if it falls between a window of 8pm and 11pm on the 15th of December. How do I do that? When I convert that string to an NSDate, it coverts it to UTC (that's what I see in the debugger) and now the date is +5 hours from the local time (which goes into the next day). The problem now is that I'm comparing a date the lands on the next day with the current date, for example, the current date/time is Dec 15 9pm. But I need to know that the date represents Dec 15 10pm.

回答1:

The first, key thing to understand is that NSDate does not represent a "date" in any way that you're probably thinking about. It represents a singular instant in time (without consideration of relativistic effects or other whatnot; just what you'd intuitively think of as "an instant in time").

That means, for instance, that an NSDate has no time zone. It has no location. It does not, itself, know anything about "days" or "hours" or "months." It is just the number of seconds that have elapsed since a specific moment in time.

Why do I go on and on about this? Because NSDate is almost never what you want for a scheduling app. When you say "2:00p on March 15, 2017" you mean a nominal time based on some calendar. Because calendars may change over time (DST rules in fact do change over time in unpredictable ways), you cannot know precisely what instant any future nominal time represents. You can guess, but you can't know, because the rules that map names to instants may change before then. And of course, even knowing the rules, that nominal time will represent different instants depending on what time zone it must be applied to. It's even possible for a nominal time not to exist at all (since DST might skip over it), or for it to map to multiple instants (since DST might skip backwards).

If you're doing scheduling, you want to work with NSCalendar and NSDateComponents. These tools let you encode nominal times, apply time zones, and otherwise deal with the messiness of calendar systems. You should only convert these to NSDate when necessary, realizing that if it's a future date, the conversion is uncertain.

NSDateComponents also allows you to keep track of what you actually know about the date and what you don't, since most of it is optional. For example, it is possible to create an NSDateComponents with a nil time zone and no time. That allows you to express things like "Halloween is all of Oct 31, no matter what time zone you're in." But also express "this meeting is at 2:00p EST, which is 1:00p CST." Converting these things to NSDate prematurely invariably leads to Halloween starting at 10p on Oct 30, and other such nonsense.


Regarding your edit, I think all of your confusion is over what "2016-12-15T22:16:11+05:00" means. This is an absolute point in time. It's not local time. It's not UTC. It's a specific point in time that everyone will agree what that time is. Anyone in the world will agree that this is 1481822171 seconds after the Unix Epoch. People on the moon or on Mars would agree, too. This is an absolute point in time. The reason that's true is the "+05:00". That offsets the nominal time ("Dec 15, 2016 at 10:16:11pm") into an absolute time (1481822171 seconds after the Epoch).

When you turn this into an NSDate, the only information stored is 1481822171. There's nothing else. It's not "in a time zone." It's not "local." It's 1481822171. When you print the date's description, which is just for convenience, it prints "Dec 15, 2016, 12:16 PM" for me. But this is printed in local time just for convenience. This doesn't mean the NSDate is "in local time." If you want to print this, you should always use an NSDateFormatter and apply the time zone you want to apply. Never rely on description.

I know I keep saying the same thing over and over, but it seems to be the most difficult for people to really "get." Absolute times have no time zone. Time zones exist purely for nominal times because it makes humans happy. Nominal times have to do with how humans think about calendars. They have only limited relationship to how time works (which doesn't care about where you are on the planet, or which political system you're in and what calendar they prefer).

If you want to know if this is between some nominal times, you need to find out when those times actually are. So you'd do something like this:

// 8pm and 11pm on the 15th of December (in the +05:00 timezone).
var startComp = DateComponents()
startComp.year = 2016
startComp.month = 12
startComp.day = 15
startComp.hour = 20
startComp.timeZone = TimeZone(secondsFromGMT: 5*60*60)! // +05:00

var endComp = startComp
endComp.hour = 23

let calendar = Calendar(identifier: .gregorian)
let startDate = calendar.date(from: startComp)!
let endDate = calendar.date(from: endComp)!

let targetDate = ISO8601DateFormatter().date(from: "2016-12-15T22:16:11+05:00")!

startDate < targetDate && targetDate < endDate // true


回答2:

NSDate (or Date, in Swift 3) objects save a double that records an instant in time anywhere on the planet, independent of time zone. Under the covers, that double is the number of seconds since midnight January 1, 2001 in Greenwich UK (the "epoch date") but that is an arbitrary "zero point" and does not tie the NSDate object to a particular time zone.

You need to use a date formatter in order to display a date in your local time zone.

You could use the code:

print(NSDateFormatter.localizedStringFromDate(date,
  dateStyle: .MediumStyle,
  timeStyle: .MediumStyle))

That will display the date date in the user's local time zone and using their locale settings.

As for doing math on dates there are a number of methods that let you add time units to dates. Take a look at the docs for the NSCalendar class. For example take a look at the method nextDateAfterDate(_:matchingComponents:options:)

That will let you create an NSDate for 5 PM today, for example.