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

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.

Related

Calculate time in seconds between 2 NSDates

I cant calculate output from "old" and "now" NSDate. Here is the code:
NSLog(#"past is %#", past);
NSDate *now = [NSDate date];
NSLog(#"Now time is is %#", now);
NSTimeInterval distanceBetweenDates = [now timeIntervalSinceDate:past];
double secondsInAnHour = 3600;
NSInteger hoursBetweenDates = distanceBetweenDates / secondsInAnHour;
NSLog(#" Time between is %i", hoursBetweenDates);
Here is console output:
2015-11-11 18:52:35.608 TaskTimer[2578:130664] past is 2015-11-11 15:52:02 +0000
2015-11-11 18:52:35.608 TaskTimer[2578:130664] now is 2015-11-11 15:52:35 +0000
2015-11-11 18:52:35.609 TaskTimer[2578:130664] Time between is 0
I want to add, that last value is 0 even when time between two values more then couple of minutes. Why is it 0?
Because you're converting to an integer and it rounds down. Anything < a particular integer value will round down. And a few minutes is < 1 so you'll get 0 hours.
If you want to round to nearest then use round(distanceBetweenDates / secondsInAnHour), or to round up you would use ceil(distanceBetweenDates / secondsInAnHour) (though it will also round 2.1 up to 3)
There are 33 minutes between your 2 dates, which is a fraction of an hour.
NSInteger hoursBetweenDates = distanceBetweenDates / secondsInAnHour;
That statement is losing the fractional precision, and rounding down to 0, which is what you are displaying.
An alternative is to display in hh:mm:ss format using NSDateComponentsFormatter
NSDateComponentsFormatter *formatter = [[NSDateComponentsFormatter alloc] init];
formatter.allowedUnits = NSCalendarUnitHour | NSCalendarUnitMinute |
NSCalendarUnitSecond;
NSLog(#"Time between is %#", [formatter stringFromTimeInterval:distanceBetweenDates]);

How to get the day of week from a given number

I want to have the day of week name for a given number, here is the pseudo-code :
getDayStringForInt:0 = sunday
getDayStringForInt:1 = monday
getDayStringForInt:2 = tuesday
getDayStringForInt:3 = wenesday
getDayStringForInt:4 = thursday
getDayStringForInt:5 = friday
getDayStringForInt:6 = saturday
I have tried with the follow code, but some thing is not working ...
- (void) setPeriodicityDayOfWeek:(NSNumber *)dayOfWeek{
gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
dateFormatter = [[NSDateFormatter alloc] init];
NSLocale *frLocale = [[NSLocale alloc] initWithLocaleIdentifier:#"en_US"];
[dateFormatter setLocale:frLocale];
[gregorian setLocale:frLocale];
NSDate *today = [NSDate date];
NSDateComponents *nowComponents = [gregorian components:NSYearCalendarUnit | NSWeekCalendarUnit | NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit fromDate:today];
[nowComponents setWeekday:dayOfWeek];
NSDate *alertDate = [gregorian dateFromComponents:nowComponents];
[dateFormatter setDateFormat:#"EEEE"];
NSLog(#"Day Of Week : %# - Periodicity : %#", dayOfWeek, [dateFormatter stringFromDate:alertDate]);
alert.periodicity = [dateFormatter stringFromDate:alertDate];
}
My log is very strange :
Day Of Week : 0 - Periodicity : monday
Day Of Week : 1 - Periodicity : wenesday
Day Of Week : 2 - Periodicity : friday
Day Of Week : 3 - Periodicity : friday
Day Of Week : 4 - Periodicity : tuesday
Day Of Week : 5 - Periodicity : sunday
Day Of Week : 6 - Periodicity : sunday
Any idea ? any better solution ...
Since this has become the accepted answer, I'll post the "right" solution here too. Credits to Rob's answer.
The whole thing can simply be achieved using the [shortWeekdaySymbols][1] method of NSDateFormatter, so the full solution boils down to
- (NSString *)stringFromWeekday:(NSInteger)weekday {
NSDateFormatter * dateFormatter = [NSDateFormatter new];
dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:#"en_US"];
return dateFormatter.shortWeekdaySymbols[weekday];
}
Original answer
Beware, you're passing a pointer to NSNumber to a method that requires a NSInteger.
The compiler is not warning you since a pointer is indeed an integer, just not the one you would expect.
Consider this simple test:
- (void)foo:(NSInteger)a {
NSLog(#"%i", a);
}
- (void)yourMethod {
[self foo:#1]; // #1 is the boxed expression for [NSNumber numberWithInt:1]
}
This prints something like 185035664, which is the pointer value, i.e. NSNumber * when cast to NSInteger.
You should either use [dayOfWeek integerValue] or directly turn dayOfWeek into a NSInteger in your method signature.
Also I think you're getting something else wrong: from the doc of setWeekday:
Sets the number of weekday units for the receiver. Weekday units are
the numbers 1 through n, where n is the number of days in the week.
For example, in the Gregorian calendar, n is 7 and Sunday is
represented by 1.
Sunday is 1, so you'd better check the correspondence with your representation too.
See https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSDateFormatter_Class/Reference/Reference.html#//apple_ref/occ/instm/NSDateFormatter/weekdaySymbols
Simply use weekdaySymbols to retrieve the dayname.
Thanx to Every one, here is a clean response :
/**
* getting the day of week string for a given day of week number
*
* #param dayOfWeekNumber 0 return sunday, 6 return saturday
*
* #return a string corresponding at the given day of week.
*/
- (NSString*) getDayOfWeekStringForDayOfWeek:(NSInteger)dayOfWeek{
return [[dateFormatter shortWeekdaySymbols] objectAtIndex:dayOfWeek];
}

Overlap of a Date Range with Other Date Ranges

Good Evening,
I am trying to figure out how to count the number of days between date ranges by comparing the date ranges. For example, I have three given ranges:
range_1 01/01/2001 to 01/01/2002
range_2 01/02/2002 to 01/01/2003
range_3 01/02/2003 to 01/01/2004
If I compare my_date_range 12/12/2001 to 01/05/2002 with the ranges above, the result should show that between range_1 and my_date_range there are 19 days, between range_2 and my_date_range there are 5 days, and between range_3 and my_date_range there are 0 days.
In Excel this was easy, I would simply use:
=SUMPRODUCT(ISNUMBER(MATCH(ROW(INDIRECT(A1&":"&B1)),ROW(INDIRECT($C$1&":"&$D$1)),0))*1)
where A1 and B1 are the start and end dates the user enters, and C1 and D1 is one of the three date ranges. I would then use the same formula and compare A1 and B1 to the second date range, then the third.
But how is this translated into objective-c? (I am able to compare two dates and get the number of days between them.)
First you have to convert the date strings to NSDate values:
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:#"MM/dd/yyyy"];
NSDate *range1Start = [dateFormatter dateFromString:#"01/01/2001"];
NSDate *range1End = [dateFormatter dateFromString:#"01/01/2002"];
NSDate *userStart = [dateFormatter dateFromString:#"12/12/2001"];
NSDate *userEnd = [dateFormatter dateFromString:#"01/05/2002"];
Then you can compute the overlapping interval:
NSDate *overlapFrom = [range1Start laterDate:userStart];
NSDate *overlapTo = [range1End earlierDate:userEnd];
And finally the number of days between the start and end date of the overlapping interval:
NSInteger days;
if ([overlapFrom compare:overlapTo] > 0) {
// Date ranges do not overlap
days = 0;
} else {
NSCalendar *calendar = [NSCalendar currentCalendar];
NSDateComponents *comp = [calendar components:NSDayCalendarUnit fromDate:overlapFrom toDate:overlapTo options:0];
days = [comp day];
}
NSLog(#"%ld", (long)days);
In this example, the output is 20, because the difference between 12/12/2001 and 01/01/2002 is 20 days. You have to add 1 if both start and end day of the overlapping range should be counted.
Once you have your start/end dates as per Martin's answer, you can use the intersection(with) function on DateInterval:
let interval1 = DateInterval(start: start1, end: end1)
let interval2 = DateInterval(start: start2, end: end2)
let intersection = interval1.insection(with: interval2)
Also helpful if you just want to check for an overlap is interval1.intersects(interval2).

Is the iOS Do Not Disturb Bug Causing this Bug?

I am trying to find out if the recent Do Not Disturb bug (Ars Technica link) is affecting my code. I am trying to get the date and timestamp for midnight the previous Monday. Since I am writing this on a Monday, I would expect to get today's date (January 7, 2013), yet I am January 4, 2013.
I am following the guide posted by Apple, but I am trying to modify it to find the previous Monday, instead of Sunday.
+(NSTimeInterval)StartOfWeekMonday {
NSTimeInterval finalTime = -1;
NSInteger weekday;
NSDate *monday = 0;
NSCalendar *greg = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDateComponents *weekdays = [greg components:NSWeekdayCalendarUnit fromDate:[NSDate date]];
NSDateComponents *subtract = [[NSDateComponents alloc] init];
NSDateComponents *final = 0;
weekday = ( weekdays.weekday == 1 ) ? 6 : weekdays.weekday;
[subtract setDay:(0 - weekday - 1)];
monday = [greg dateByAddingComponents:subtract toDate:[NSDate date] options:0];
final = [greg components:(NSYearCalendarUnit|NSMonthCalendarUnit|NSDayCalendarUnit) fromDate:monday];
monday = [greg dateFromComponents:final];
#ifdef DEBUG
NSLog( #"%s - %d -> Weekday: %d", __FILE__, __LINE__, weekday );
NSLog( #"%s - %d -> Monday: %#", __FILE__, __LINE__, [monday descriptionWithLocale:[NSLocale currentLocale]] );
#endif
finalTime = [monday timeIntervalSince1970];
return finalTime;
}
My log out put below, the weekday is correct (I am writing this on a Monday), yet the date is obviously wrong, it should be Monday: Monday, January 7, 2013, 12:00 AM
2013-01-07 15:07:33.792 MY-APP[5524:14c03] Weekday: 2
2013-01-07 15:07:36.757 MY-APP[5524:14c03] Monday: Friday, January 4, 2013, 12:00:00 AM Eastern Standard Time
This is being in the simulator right now, but all my other date calculations are giving the expected values.
Is it likely that I am being affected by the recent Do Not Disturb Bug and will have to wait for tomorrow (January 8, 2013) to see the expected results, or am I missing something entirely different?
The issue was with how to calculate the subtraction. Either I completely mis understood the example provided by Apple, or it was not clear enough. The solution for calculating the subtraction was as follows:
weekday = ( weekdays.weekday == 1 ) ? 8 : weekdays.weekday;
toSubtract = ( 2 - weekday );
[subtract setDay:toSubtract];
Running various tests produced the proper "Monday." In the end, this had nothing to do with the date bug affecting Do Not Disturb.

LDAP Date to NSDate

I am trying to parse the pwdLastSet value from NSTask response when I do an ldapsearch. I've successfully extracted the value (129875475241190194) and I am trying to convert it to an NSDate Object.
Reference: http://www.chrisnowell.com/information_security_tools/date_converter/Windows_active_directory_date_converter.asp
I tried to extract the Javascript code from the page above and convert it but I am getting a different date.
int iYearsFrom1601to1970 = 1970 - 1601;
int iDaysFrom1601to1970 = iYearsFrom1601to1970 * 365;
iDaysFrom1601to1970 += (int)(iYearsFrom1601to1970 / 4); // leap years
iDaysFrom1601to1970 -= 3; // non-leap centuries (1700,1800,1900). 2000 is a leap century
float iSecondsFrom1601to1970 = iDaysFrom1601to1970 * 24 * 60 * 60;
int iTotalSecondsSince1601 = (int)(129875475241190194 / 10000000);
float iTotalSecondsSince1970 = iTotalSecondsSince1601 - iSecondsFrom1601to1970;
NSDate *date = [NSDate dateWithTimeIntervalSince1970:iTotalSecondsSince1970];
Any help would be appreciated.
Thanks!
Here's how I would do it:
NSDateComponents *base = [[NSDateComponents alloc] init];
[base setDay:1];
[base setMonth:1];
[base setYear:1601];
[base setEra:1]; // AD
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDate *baseDate = [gregorian dateFromComponents:base];
[base release];
[gregorian release];
NSTimeInterval timestamp = 129875475241190194.0 / 10000000.0;
NSDate *finalDate = [baseDate dateByAddingTimeInterval:timestamp];
This gives me a finalDate of 2012-07-24 03:58:22 +0000.
Since the timestamp is a time interval since Jan 1, 1601 at 00:00 UTC, you can use the -dateByAddingTimeInterval: method on NSDate to add the timestamp to the base date to get the final NSDate.
Once you've done that, you can run it through an NSDateFormatter to format it for display.
Assuming the, well, daring conversion between the basetimes is correct: actually looking at the warnings, instead of casting them away, might actually help:
int main(void)
{
int iTotalSecondsSince1601 = (129875475241190194 / 10000000);
return 0;
}
stieber#gatekeeper:~$ clang++ Test.cpp
Test.cpp:4:8: warning: implicit conversion from 'long' to 'int' changes value from 12987547524 to 102645636
....
That should account for a good deal of the difference...
Try this
NSTimeInterval value = 129875475241190194;
// instead of trying to compute seconds between 1601 and 1970
const NSTimeInterval EPOCH = 11644473600;
const NSTimeInterval NANO = 10000000;
NSTimeInterval seconds = value / NANO - EPOCH;
NSDate *answer = [NSDate dateWithTimeIntervalSince1970:seconds];
Also this is reason you don't want to calculate seconds since 1601: ...in the last millennium, 1600 and 2000 were leap years, but 1700, 1800 and 1900 were not. Excerpt from Wikipedia on Gregorian calendar.
The value for EPOCH is explained on Convert Active Directory "LastLogon:" time to (UNIX) readable time
.
Note: The information about accountExpires which starts from 12-31-1601 (11644473600). The values lastLogon and lastLogonTimeStamp however use 01-01-1601 as the date to calculate this value (11676009600).