I'm using the JSON library from Stig Brautaset(http://code.google.com/p/json-framework) and I need to serialize an NSDate. I was considering converting it into a string before JSONifying it, however, I ran into this weird behavior:
Why aren't these NSDates considered equal?
NSDate *d = [[NSDate alloc] init];
NSDate *dd = [NSDate dateWithString:[d description]];
NSLog(#"%#", d);
NSLog(#"%#", dd);
if( [d isEqualToDate:dd] ){
NSLog(#"Yay!");
}
When you describe the original date object you lose some sub-second precision from the original object — in other words, -description shaves off fractional seconds, and returns
A string representation of the receiver in the international format YYYY-MM-DD HH:MM:SS ±HHMM, where ±HHMM represents the time zone offset in hours and minutes from GMT
When you create a new date object based on the description, you get it in whole seconds because the string is only precise to a whole second. So -isEqualToDate: returns NO because there is a difference of a fraction of a second between your two date objects, which it's sensitive to.
This method detects sub-second differences between dates. If you want to compare dates with a less fine granularity, use timeIntervalSinceDate: to compare the two dates.
So you'd do something like this instead (NSTimeInterval measures in seconds):
if ([d timeIntervalSinceDate:dd] == 0) {
NSLog(#"Yay!");
}
isEqualToDate detects subseconds differences between dates, but the description method does not include subseconds.
Because they're not equivalent:
NSDate *d = [NSDate date];
NSDate *dd = [NSDate dateWithString:[d description]];
NSLog(#"%f", [d timeIntervalSinceReferenceDate]);
NSLog(#"%f", [dd timeIntervalSinceReferenceDate]);
Produces:
2011-04-28 11:58:11.873 EmptyFoundation[508:903] 325709891.867788
2011-04-28 11:58:11.874 EmptyFoundation[508:903] 325709891.000000
In other words, the +dateWithString: method does not maintain sub-second precision.
Related
I am sure this question came up before I am pulling my hair out. I have two dates - one from an Object on Parse.com and the other one local. I try to determine whether the remote object has been updated so that I can trigger actions locally.
When looking at the NSDate of both objects they seem identical but a comparison reveals that the remote object is newer - when checking the time internal (since1970) it becomes obvious that there is a difference but why? When I first created the local object all I did was
localObject.updatedAt = remoteObject.updatedAt //both NSDate
But when looking closer I get this:
Local Time Interval: 1411175940.000000
Local Time: 2014-09-20 01:19:00 +0000
Remote Time Interval: 1411175940.168000
Remote Time: 2014-09-20 01:19:00 +0000
Does anyone have an idea why that is and whether I can ignore this detail? Does iOS round up or something?
Adding more code:
#property (strong, nonatomic) NSDate *date;
...
PFQuery *query = [PFObject query];
[query whereKey:#"Product" equalTo:#"123456"]
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error)
{
self.date = objects[0].updatedAt;
NSTimeInterval localTime = [self.date timeIntervalSince1970];
NSTimeInterval remoteTime = [objects[0].updatedAt timeIntervalSince1970];
NSLog(#"Local Time Interval: %f", localTime);
NSLog(#"Local Time: %#", self.date);
NSLog(#"Remote Time Interval: %f", remoteTime);
NSLog(#"Remote Time: %#", objects[0].updatedAt);
}
else
{
NSLog(#"Error with query");
}
}];
That results in the console output above - and I don't understand why these dates are different.
I cannot explain why there is a difference, but the important thing to understand is that there can be a difference and that when comparing dates you have to use a tolerance value.
The Apple Date and Time Programming Guide has an example of how to compare two dates within a given tolerance:
To compare dates, you can use the isEqualToDate:, compare:,
laterDate:, and earlierDate: methods. These methods perform exact
comparisons, which means they detect sub-second differences between
dates. You may want to compare dates with a less fine granularity. For
example, you may want to consider two dates equal if they are within a
minute of each other. If this is the case, use timeIntervalSinceDate:
to compare the two dates. The following code fragment shows how to use
timeIntervalSinceDate: to see if two dates are within one minute (60
seconds) of each other.
if (fabs([date2 timeIntervalSinceDate:date1]) < 60) ...
It's up to you decide on the tolerance value, but something like 0.5 seconds seems reasonable:
+ (BOOL)date:(NSDate *)date1
equalsDate:(NSDate *)date2
{
return fabs([date2 timeIntervalSinceDate:date1]) < 0.5;
}
Parse stores dates as iso8601 format. This makes things very complex as Apple does not manage the format well. While the idea of the standard is awesome, until everyone plays by the same rules, anarchy rules..
I convert everything inbound from parse into usable format before attempting anything on their date time values..
Drop this into a library somewhere, and save yourself tons of headaches. This took weeks of searching and scratching to overcome.
+ (NSDate *)convertParseDate:(NSDate *)sourceDate {
NSDateFormatter *dateFormatter = [NSDateFormatter new];
NSString *input = (NSString *)sourceDate;
dateFormatter.dateFormat = #"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
// Always use this locale when parsing fixed format date strings
NSLocale* posix = [[NSLocale alloc] initWithLocaleIdentifier:#"en_US_POSIX"];
dateFormatter.locale = posix;
NSDate *convertedDate = [dateFormatter dateFromString:input];
assert(convertedDate != nil);
return convertedDate;
}
Below is a string represented a date
NSString *dateStr = #"2011-07-06";
And when I am trying to convert it to NSDate by doing :
NSDateFormatter *format = [[NSDateFormatter alloc] init];
[format setDateFormat:#"yyyy-MM-dd"];
NSDate *tmp = [format dateFromString:dateStr];
NSLog(#"tmp is %#",[tmp description]);
What I am getting from the console is
tmp is 2011-07-06 04:00:00 +0000
I dont understand why I am getting extra info :04:00:00 +0000 for the result
Please help if you experienced it before
Your code
NSString *dateStr = #"2011-07-06";
NSDateFormatter *format = [[NSDateFormatter alloc] init];
[format setDateFormat:#"yyyy-MM-dd"];
NSDate *tmp = [format dateFromString:dateStr]
will result in a NSDate object, that represents your local time at 0:00 — the beginning of the day.
but if you print a plain date object, it will result in a string that represents the time in GMT timezone, as internally all dates are normalized to that timezone.
As your string is 4 hours ahead, we can tell, that you most likely are in East Europe, maybe Moscow.
So if you want to see the string in your timezone, you need to use a NSDateFormatter to create it from the date object.
NSLog(#"tmp is %#",[formatter stringFromDate:tmp]);
to check, if it is correct, what I said, change the format to contain the time components.
formatter.format = [#"yyyy-MM-dd HH:mm"];
NSLog(#"tmp is %#",[formatter stringFromDate:tmp]);
The formatter will also take "oddities" like Leap Day, Leap Month (yes — those exits), Daylight Saving Times, Leap Seconds … in account — accordantly to the current calendar.
A great WWDC 2011 Video: Performing Calendar Calculations — a must-see for every cocoa developer.
BTW: to print a object with NSLog you dont need to call -description on it to pass in a string. NSLog will do this internally.
NSLog(#"tmp is %#", tmp);
is just fine.
The answer is simple, NSLog just converts the NSDate to a NSString, using its formatter with GMT (zero) timezone.
Your formatter is by default set to your default time zone, which is probably -4:00. When you print it out, NSLog converts it to 0:00, adding 4 hours.
In general, it's unsafe to parse dates without specifying their timezone.
Q1. NSDate *now = [NSDate date];
NSLog(#"Current date:%#",now);
The time shown is 4 hours ahead of the system time. I am wondering why is it so and how can I correct it?
Q2.. In C/C++, strings are treated as an array of characters. Is it the case in Objective-C also?
NSDate *now = [NSDate date];
NSLog(#"Current date:%#",now);
it's "now" not "date".
A1: NSLog(#"%#", now) is effectively the same as NSLog(#"%#", [now description]). The NSDate object doesn't care what the timezone is, so its description method will just give you the time in UTC. If you need to format with the right timezone and locale, you'll need to use an NSDateFormatter object to convert it to a nicely formatted string first.
A2: Yes and no, but mostly no. You can do this:
char *cString = "I am a C string";
to create a C string, which you can treat exactly as you would in C. That's something you very rarely see in Objective-C, though, except when it's absolutely necessary. The "normal" way to use strings is with instances of NSString or NSMutableString, which are fully-fledged objects:
NSString *normalString = #"I'm above all that."; (note the # symbol)
I'm trying to retrieve all the events for a single day from an instance of EKEventStore using eventsMatchingPredicate:, but as I read, the NSDate objects are by default set to GMT while the EKEventStore isn't. So my question is how do I change the timezone of the EKEventStore or adjust the NSDate objects so that the times aren't off for each timezone?
For example, I'm in GMT -0600, and clicking on January 16th and 17th in the TKCalendarMonthView I'm using for a calendar UI shows Martin Luther King Day on both dates. The start time is 6 AM on 16 January, and the end time is 5:59 AM on 17 January (as a result of my timezone), rather than beginning at 12:00 AM and lasting until 11:59 PM. The code used to retrieve events follows.
- (void)calendarMonthView:(TKCalendarMonthView *)monthView didSelectDate:(NSDate *)d {
// Update tableData with event data from date
[tableData removeAllObjects];
NSArray *a = [systemCalendar eventsMatchingPredicate:[systemCalendar predicateForEventsWithStartDate:d endDate:[NSDate dateWithTimeInterval:84600 sinceDate:d] calendars:nil]];
[tableData addObjectsFromArray:a];
[self.eventsTable reloadData];
}
Given that I'm on a short timeline, I came up with a solution, and it seems to work. My only concern is that I had to multiply the offset by -1 even though the time interval offset itself is negative. This doesn't make sense because we are trying to subtract from the NSDate rather than add to it. A positive number minus a negative number gives us a larger number, so I'm slightly worried about the GMT zones on the other side of the PM and wondering whether I should actually be multiplying all time intervals by -1. Anyone have any thoughts?
- (void)calendarMonthView:(TKCalendarMonthView *)monthView didSelectDate:(NSDate *)d {
[NSTimeZone resetSystemTimeZone];
NSTimeZone *tz = [NSTimeZone systemTimeZone];
NSArray *comps = [[tz description] componentsSeparatedByString:#" "];
NSTimeInterval offset = (NSTimeInterval)[[comps lastObject] floatValue];
if (offset < 0) {
offset *= -1;
}
NSDate *startDate = [d dateByAddingTimeInterval:offset];
NSArray *a = [systemCalendar eventsMatchingPredicate:[systemCalendar predicateForEventsWithStartDate:startDate endDate:[NSDate dateWithTimeInterval:84600 sinceDate:startDate] calendars:nil]];
NSLog(#"Events for the date: %#", a);
[tableData addObjectsFromArray:a];
[self.eventsTable reloadData];
}
how I can convert an epoch time value to NSDate. For example I use this value : 1310412600000. and I am in the EDT time zone.
When I try this :
NSString *bar = [[NSDate dateWithTimeIntervalSince1970:epoch] description];
I got a wrong value...
What is the good way? I spend a lot of time with that....
Thanks
Epoch time (also known as "Unix" and "POSIX" time) is the number of seconds since the start of 1970. You can convert epoch time to NSDate with this algorithm:
Instantiate NSDate object and pass the epoch time as a parameter to NSDate's initWithTimeIntervalSince1970 initializer. You're done. The NSDate object is set to the epoch time. NSDate stores times internally in the UTC time zone. It's up to you how it is displayed.
[Optional] Format your NSDate to the appropriate time zone with an NSDateFormatter.
Here's the code I used:
// Sample string epochTime is number of seconds since 1970
NSString *epochTime = #"1316461149";
// Convert NSString to NSTimeInterval
NSTimeInterval seconds = [epochTime doubleValue];
// (Step 1) Create NSDate object
NSDate *epochNSDate = [[NSDate alloc] initWithTimeIntervalSince1970:seconds];
NSLog (#"Epoch time %# equates to UTC %#", epochTime, epochNSDate);
// (Step 2) Use NSDateFormatter to display epochNSDate in local time zone
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:#"yyyy-MM-dd HH:mm:ss zzz"];
NSLog (#"Epoch time %# equates to %#", epochTime, [dateFormatter stringFromDate:epochNSDate]);
// (Just for interest) Display your current time zone
NSString *currentTimeZone = [[dateFormatter timeZone] abbreviation];
NSLog (#"(Your local time zone is: %#)", currentTimeZone);
Per the documentation, dateWithTimeIntervalSince1970: runs from January 1, 1970, 00:00 GMT. So you should be getting results four hours later than those you really want.
Hence the simplest thing — if you don't mind hard coding the offset from GMT to EDT — would seem to be:
[[NSDate dateWithTimeIntervalSince1970:epoch] dateByAddingTimeInterval:-240]
// or:
[NSDate dateWithTimeIntervalSince1970:epoch - 240]
Though to eliminate arbitrary constants (and be a little cleaner), you probably want something like:
NSTimeZone *EDTTimeZone = [NSTimeZone timeZoneWithAbbreviation:#"EDT"];
NSInteger secondsDifferenceFromGMT =
[EDTTimeZone secondsFromGMTForDate:[NSDate dateWithTimeIntervalSince1970:0]];
NSDate *startOfEpoch =
[NSDate dateWithTimeIntervalSince1970:secondsDifferenceFromGMT];
...
NSDate *newDate = [startOfEpoch dateByAddingTimeInterval:firstInterval]; // etc
Are you passing the value in seconds?
The method accepts value in seconds , not milliseconds.
http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/NSDate_Class/Reference/Reference.html