Error with how NSCalendar dateByAddingComponents handles Daylight Savings - dst

I'm having a hard time understanding an exception to how dateByAddingComponents handles Daylight Savings. I've read through the Apple Date and Time Programming Guide and expected dateByAddingComponents to take into account DST changes. However, on the date of the DST change, its not working for me.
Here's the code:
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
[gregorian setTimeZone:[NSTimeZone localTimeZone]];
NSDateComponents *midnight = [gregorian components:(NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit | NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit) fromDate:self.currentDate];
midnight.hour = 0;
midnight.minute = 0;
midnight.second = 0;
NSDate *startDate = [gregorian dateFromComponents:midnight];
NSDateComponents *offSetComponents = [[NSDateComponents alloc] init];
[offSetComponents setDay:1];
NSDate *endDate = [gregorian dateByAddingComponents:offSetComponents toDate:startDate options:0];
//Calculate start time from config (hours/min from seconds)
int startTimeInMinutes = self.club.clubConfiguration.startTime.integerValue;
int startTimeHours = startTimeInMinutes / 60;
int startTimeMins = startTimeInMinutes % 60;
NSLog(#"---- startTimeHours %i", startTimeHours);
NSLog(#"---- startTImeMins %i", startTimeMins);
NSDateComponents *offSetComponents2 = [[NSDateComponents alloc] init];
[offSetComponents2 setHour:startTimeHours];
[offSetComponents2 setMinute:startTimeMins];
NSDate *firstTeeTime = [gregorian dateByAddingComponents:offSetComponents2 toDate:startDate options:0];
Explanation:
I'm getting a startTimeInMinutes from the server that I use to calculate the firstTeeTime. For example, I'm expecting to add 6 hours to the startDate (12am in my use case) and get 6am (localTimeZone).
Using dateByAddingComponents works both before and after the DST change however, on the day of DST change Sunday Nov 3, I'm getting 5am.
Theory: Since there are actually 2 2am's on Sunday Nov 3rd, I may have to account for that? If thats the case, I'd have to write some logic to account for the actual day of DST change and add an offset if appropriate using daylightSavingTimeOffsetForDate.
What am I missing???
EDIT: Ok, I decided to work around the issue by determining if today was the DST change and add/remove an hour offset. Feels kinda like I'm missing something here about NSDate however, it works. Hope this helps someone else out there scratching their heads all morning.
Work Around Code:
////// Work around for DST
NSTimeZone *currentZone = [gregorian timeZone];
NSDate *dstTransitionDate = [currentZone nextDaylightSavingTimeTransitionAfterDate:startDate];
NSTimeInterval dstOffset = [currentZone daylightSavingTimeOffsetForDate:endDate];
NSDateComponents *startDateComponents = [gregorian components:(NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit) fromDate:startDate];
NSDateComponents *dstTransitionDateComponents = [gregorian components:(NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit) fromDate:dstTransitionDate];
int offset = 0;
if ( [startDateComponents year] == [dstTransitionDateComponents year] &&
[startDateComponents month] == [dstTransitionDateComponents month] &&
[startDateComponents day] == [dstTransitionDateComponents day])
{
if (dstOffset > 0){
offset = -1;
} else {
offset = 1;
}
}
//////

I totally sympathize with you, as I have fallen into the same trap. First, I scratched my head at the results and docs, then attempted a fool's errand by attempting to roll out a custom "adding date components" logic.
Then, came across your answer, which worked beautifully, and my tests finally pass:
// assert!
XCTAssertEqual(dateFormatter.stringFromDate(dstSwitch.date), "11/1/15, 12:00 AM")
// Offseting the date should just work
do {
let dstOffset = dstSwitch + 3.hours
XCTAssertEqual(dateFormatter.stringFromDate(dstOffset.date), "11/1/15, 3:00 AM")
}
... But hang on! Words of wisdom from #RobNapier set me on the right path ... We are adding 6 hours, which means a person at the event will only spend six hours of their life. If the start date is midnight, they will adjust their watches at 2 AM back to 1 AM, and therefor spend the lost hour in oblivion according to the calendar.
So... If the event truly starts at midnight, and ends at 6 AM, heed Rob's words and don't use the duration. Use the exact date components. Because that duration is actually 7 hours.
For fun, I came to that realization when my tests bothered me. Why should I only add the offset on the same day? Why does changing days "just works"? ... Liiiight bulb.
And, btw, my test are for a library I am rolling out that should end the short comings of all date calculation and end the confusion .. With really concise syntax for swift. Will be available as Datez, part of Kitz.

Related

Why is Excel serial date-time to NSDate conversion two days, one hour and 40 minutes ahead?

I'm converting dates from an Excel spreadsheet to NSDate's, but for some reason they always come out two days ahead: Sundays come out as Tuesdays, etc.
My conversion method is based on the following info from cpearson.com:
Excel stores dates and times as a number representing the number of
days since 1900-Jan-0, plus a fractional portion of a 24 hour day:
ddddd.tttttt . This is called a serial date, or serial date-time.
(...) The integer portion of the number, ddddd, represents the number
of days since 1900-Jan-0. (...) The fractional portion of the number,
ttttt, represents the fractional portion of a 24 hour day. For
example, 6:00 AM is stored as 0.25, or 25% of a 24 hour day.
Similarly, 6PM is stored at 0.75, or 75% percent of a 24 hour day.
- (NSDate *)dateFromExcelSerialDate:(double)serialdate
{
if (serialdate == 0)
return nil;
NSTimeInterval theTimeInterval;
NSInteger numberOfSecondsInOneDay = 86400;
double integral;
double fractional = modf(serialdate, &integral);
NSLog(#"%# %# \r serialdate = %f, integral = %f, fractional = %f",
[self class], NSStringFromSelector(_cmd),
serialdate, integral, fractional);
theTimeInterval = integral * numberOfSecondsInOneDay; //number of days
if (fractional > 0) {
theTimeInterval += numberOfSecondsInOneDay / fractional; //portion of one day
}
NSCalendar *nl_gregorianCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
NSTimeZone *nl_timezone = [[NSTimeZone alloc] initWithName:#"Europe/Amsterdam"];
[nl_gregorianCalendar setTimeZone:nl_timezone];
NSDateComponents *excelBaseDateComps = [[NSDateComponents alloc] init];
[excelBaseDateComps setMonth:1];
[excelBaseDateComps setDay:1];
[excelBaseDateComps setHour:00];
[excelBaseDateComps setMinute:00];
[excelBaseDateComps setTimeZone:nl_timezone];
[excelBaseDateComps setYear:1900];
NSDate *excelBaseDate = [nl_gregorianCalendar dateFromComponents:excelBaseDateComps];
NSDate *inputDate = [NSDate dateWithTimeInterval:theTimeInterval sinceDate:excelBaseDate];
NSLog(#"%# %# \r serialdate %f, theTimeInterval = %f \r inputDate = %#",
[self class], NSStringFromSelector(_cmd),
serialdate, theTimeInterval,
[self.nl_dateFormatter stringFromDate:inputDate]);
return inputDate;
}
The spreadsheet was produced in the Netherlands, presumably on a Dutch version of Microsoft Excel.
Spreadsheet date Sunday July 6, 2014 00:00 yields the following results:
dateFromExcelSerialDate:
serialdate = 41826.000000, integral = 41826.000000, fractional =
0.000000 theTimeInterval = 3613766400.000000 inputDate = 08 jul. 2014 01:40
Similarly, Sunday July 13, 2014 00:00 yields:
serialdate = 41833.000000, integral = 41833.000000, fractional =
0.000000 theTimeInterval = 3614371200.000000 inputDate = 15 jul. 2014 01:40
I can correct the output by subtracting 2 days, one hour and 40 minutes:
theTimeInterval -= ((60 * 60 * 24 * 2) + (60*60) + (60*40));
but I have no idea how robust that is.
That difference of two days made me think it had something to do with leap year corrections, so I tried to let the calendar do the calculations by adding the NSTimeInterval seconds to the excelBaseDate, like so:
NSDateComponents *comps = [[NSDateComponents alloc] init];
[comps setSecond:theInterval];
NSDate *inputDate = [nl_gregorianCalendar dateByAddingComponents:comps
toDate:excelBaseDate
options:0];
Strangely enough, that gave me dates somewhere in the 1870's. Who knows what is going on?
there are two things here:
your start date is 1900-Jan-1 but your referred description clearly says: the reference is 1900-Jan-0 – you may add an extra day here;
year 1900 was not a leap-year – you may add an extra day here;
I guess, this is pretty much the reason why you get two extra days every occasion.
Microsoft knows about that, see more about the topic here.

NSDatePicker with date in yy to yyyy format

I have as NSDatePicker, where I enter 0023 and I expect it to change it to 2023. My logic is to convert the yy to yyyy based on +-50 years.
But the default behavior of NSDatePicker changes it to 0023 etc.
What I need to do to show in yyyy format with nearest 50 years range.
Is there any way to do it through Interface Builder or through codes.
Your help will be highly appreciable.
It does not "change" 0023 to 0023, it leaves it at 0023, which is correct the correct behaviour. You'd need to manually check and fix this yourself. Maybe like (untested):
NSCalendar *calendar = [NSCalendar currentCalendar];
NSDateComponents *components = [calendar
components:NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit
fromDate:myPossiblyIncorrectDate
];
NSDate *correctedDate;
if ([components year] < 100) {
NSUInteger currentYearLastDigits = 13; // Insert logic to get current year here.
NSUInteger yearLastDigits = [components year];
if (yearLastDigits > currentYearLastDigits) {
[components setYear:1900 + yearLastDigits];
} else {
[components setYear:2000 + yearLastDigits];
}
correctedDate = [calendar dateFromComponents:components];
} else {
correctedDate = myPossiblyIncorrectDate;
}
Depending on how exact you need this to be, you might want to get/set more components. See the NSCalendar constants.
But it would be better to catch the wrong year number before the number is even interpreted as year number since the date might be invalid otherwise.

How to show only objects from current date and past dates JSON

Alright, here is the skinny. I've searched on this but haven't found this question quite right. (I'm writing this in xcode fyi)
I have been tasked with something that should be easy and it probably is but I'm having trouble getting started...
I need to download JSON data and it is a list of objects that only have three items, date, tite, contents. Basically 3 strings. The date is not the date created but the date the whole object is intended for. The users are wanting to be able to preload a few weeks at a time w/o people being able to see the future dates.
I can download the JSON data no problem, I can display it in a list, not a problem. I am not certain how to make the program ignore objects with a date not equal to today.
Anyone have any insight into this? Below is what one of the test objects looks like.
#type: "Devotional",
#url: "http://api.storageroomapp.com/accounts/50ff0ad80f66027d84001680/collections/51c1ba270f6602499f0000ae/entries/51c1bc390f6602442c0002ec",
#collection_url: "http://api.storageroomapp.com/accounts/50ff0ad80f66027d84001680/collections/51c1ba270f6602499f0000ae",
#version: 2,
#trash: false,
#created_at: "2013-06-19T14:12:09Z",
#updated_at: "2013-07-08T19:38:51Z",
devotional_date: "2013-07-08",
devotional_title: "Test Today Devotional",
devotional_body: "This is a test of what Today's Daily Devotional would look like!"
},
Thanks in advance for any help.
Here is the basic idea for doing this comparison:
- (void)testDateIsToday {
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDate *now = [NSDate date];
NSInteger dateUnits = NSDayCalendarUnit | NSMonthCalendarUnit | NSYearCalendarUnit;
NSDateComponents *dateParts = [gregorian components:dateUnits fromDate:now];
NSString *dateString = #"2013-07-08"; // Date from your JSON
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.dateFormat = #"yyyy-MM-dd";
NSDate *requestedDate = [dateFormatter dateFromString:dateString];
NSDateComponents *requestedParts = [gregorian components:dateUnits fromDate:requestedDate];
NSLog(#"Equal? %i", [[gregorian dateFromComponents:requestedParts] compare:[gregorian dateFromComponents:dateParts]] == NSOrderedSame);
}
Note that this will do the comparison based on the time zone the device it runs on is set in, which I'm guessing will not impact you since you don't specify time zone in your JSON. Using a calendar and date components is the safest way to do such a comparison.

How to check if the current time is within a specified range in ios? [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Determine if current local time is between two times (ignoring the date portion)
In iOS, how can I do the following:
I have two NSDate objects that represent the opening and closing times for a store. The times within these objects are accurate but the date is unspecified (the store opens and closes at the same time regardless of the date). How can I check if the current time falls between in this time frame?
Note, if it would help for the opening and closing times to be in another format other than NSDate objects, I'm fine with that. Currently, I'm just reading in a date string such as "12:30" from a file and using date formatter to create a matching NSDate object.
Update: Note that this solution is specific to your case and assumes that store opening hours don't span two days. For example it won't work if the opening hour goes from Monday 9pm to Tuesday 10am. Since 10pm is after 9pm but not before 10am (within a day). So keep that in mind.
I cooked up a function which will tell you if the time of one date is between two other dates (it ignores the year, month and day). There's also a second helper function which gives you a new NSDate with the year, month and day components "neutralized" (eg. set to some static value).
The idea is to set the year, month and day components to be the same between all dates so that the comparison will only rely on the time.
I'm not sure if it's the most efficient approach, but it works.
- (NSDate *)dateByNeutralizingDateComponentsOfDate:(NSDate *)originalDate {
NSCalendar *gregorian = [[[NSCalendar alloc]
initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
// Get the components for this date
NSDateComponents *components = [gregorian components: (NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit | NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit) fromDate: originalDate];
// Set the year, month and day to some values (the values are arbitrary)
[components setYear:2000];
[components setMonth:1];
[components setDay:1];
return [gregorian dateFromComponents:components];
}
- (BOOL)isTimeOfDate:(NSDate *)targetDate betweenStartDate:(NSDate *)startDate andEndDate:(NSDate *)endDate {
if (!targetDate || !startDate || !endDate) {
return NO;
}
// Make sure all the dates have the same date component.
NSDate *newStartDate = [self dateByNeutralizingDateComponentsOfDate:startDate];
NSDate *newEndDate = [self dateByNeutralizingDateComponentsOfDate:endDate];
NSDate *newTargetDate = [self dateByNeutralizingDateComponentsOfDate:targetDate];
// Compare the target with the start and end dates
NSComparisonResult compareTargetToStart = [newTargetDate compare:newStartDate];
NSComparisonResult compareTargetToEnd = [newTargetDate compare:newEndDate];
return (compareTargetToStart == NSOrderedDescending && compareTargetToEnd == NSOrderedAscending);
}
I used this code to test it. You can see that the year, month and days are set to some random values and don't affect the time checking.
NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
[dateFormatter setDateFormat:#"yyyy:MM:dd HH:mm:ss"];
NSDate *openingDate = [dateFormatter dateFromString:#"2012:03:12 12:30:12"];
NSDate *closingDate = [dateFormatter dateFromString:#"1983:11:01 17:12:00"];
NSDate *targetDate = [dateFormatter dateFromString:#"2034:09:24 14:15:54"];
if ([self isTimeOfDate:targetDate betweenStartDate:openingDate andEndDate:closingDate]) {
NSLog(#"TARGET IS INSIDE!");
}else {
NSLog(#"TARGET IS NOT INSIDE!");
}

NSDatePicker timezone weirdness

I have an NSDatePicker in my nib file. In code, I set it's timezone to be the GMT calendar's timezone:
[datePicker setTimeZone:[calendar timeZone]];
I only really care about the time (hours, minutes) on the datepicker which I populate programmatically:
NSDate *anyDate = [NSDate date];
NSDateComponents *components = [calendar components:(NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit) fromDate:anyDate];
[components setHour:hours];
[components setMinute:minutes];
NSDate *newDate = [calendar dateFromComponents:components];
[datePicker setDateValue:newDate];
This correctly has the desired effect of setting the date pickers time to the time I want. So if hours is 8 and minutes is 30, the date picker shows 8:30. However, if I type an 8 into the hour field of the date picker it displays as a 3. Something weird is going on with timezones somewhere but I'm not sure where...
Seems like I'm not the first person to stumble across this issue. The solution, for those who are interested, I found here http://www.cocoabuilder.com/archive/cocoa/305254-nsdatepicker-weirdness-with-time.html. Apparently if you set the timezone of a date picker but not the calendar, you get this issue. The resolution is this:
[datePicker setTimeZone:[calendar timeZone]];
[datePicker setCalendar:calendar];