/** Implementation for NSCalendarDate for GNUstep Copyright (C) 1996, 1998, 1999, 2000, 2002 Free Software Foundation, Inc. Author: Scott Christley Date: October 1996 Author: Richard Frith-Macdonald Date: September 2002 This file is part of the GNUstep Base Library. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111 USA. NSCalendarDate class reference $Date$ $Revision$ */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "GSPrivate.h" // Absolute Gregorian date for NSDate reference date Jan 01 2001 // // N = 1; // day of month // N = N + 0; // days in prior months for year // N = N + // days this year // + 365 * (year - 1) // days in previous years ignoring leap days // + (year - 1)/4 // Julian leap days before this year... // - (year - 1)/100 // ...minus prior century years... // + (year - 1)/400 // ...plus prior years divisible by 400 #define GREGORIAN_REFERENCE 730486 @class GSTimeZone; @class GSAbsTimeZone; static NSString *cformat = @"%Y-%m-%d %H:%M:%S %z"; static NSTimeZone *localTZ = nil; static Class absClass; static Class dstClass; static SEL offSEL; static int (*offIMP)(id, SEL, id); static int (*absOffIMP)(id, SEL, id); static int (*dstOffIMP)(id, SEL, id); static SEL abrSEL; static NSString* (*abrIMP)(id, SEL, id); static NSString* (*absAbrIMP)(id, SEL, id); static NSString* (*dstAbrIMP)(id, SEL, id); /* * Return the offset from GMT for a date in a timezone ... * Optimize for the local timezone, and less so for the other * base library time zone classes. */ static inline int offset(NSTimeZone *tz, NSDate *d) { if (tz == nil) { return 0; } if (tz == localTZ && offIMP != 0) { return (*offIMP)(tz, offSEL, d); } else { Class c = GSObjCClass(tz); if (c == dstClass && dstOffIMP != 0) { return (*dstOffIMP)(tz, offSEL, d); } if (c == absClass && absOffIMP != 0) { return (*absOffIMP)(tz, offSEL, d); } return [tz secondsFromGMTForDate: d]; } } /* * Return the offset from GMT for a date in a timezone ... * Optimize for the local timezone, and less so for the other * base library time zone classes. */ static inline NSString* abbrev(NSTimeZone *tz, NSDate *d) { if (tz == nil) { return @"GMT"; } if (tz == localTZ && abrIMP != 0) { return (*abrIMP)(tz, abrSEL, d); } else { Class c = GSObjCClass(tz); if (c == dstClass && dstAbrIMP != 0) { return (*dstAbrIMP)(tz, abrSEL, d); } if (c == absClass && absAbrIMP != 0) { return (*absAbrIMP)(tz, abrSEL, d); } return [tz abbreviationForDate: d]; } } static inline int lastDayOfGregorianMonth(int month, int year) { switch (month) { case 2: if ((((year % 4) == 0) && ((year % 100) != 0)) || ((year % 400) == 0)) return 29; else return 28; case 4: case 6: case 9: case 11: return 30; default: return 31; } } static inline int absoluteGregorianDay(int day, int month, int year) { int m, N; N = day; // day of month for (m = month - 1; m > 0; m--) // days in prior months this year N = N + lastDayOfGregorianMonth(m, year); return (N // days this year + 365 * (year - 1) // days in previous years ignoring leap days + (year - 1)/4 // Julian leap days before this year... - (year - 1)/100 // ...minus prior century years... + (year - 1)/400); // ...plus prior years divisible by 400 } static int dayOfCommonEra(NSTimeInterval when) { double a; int r; // Get reference date in terms of days a = when / 86400.0; // Offset by Gregorian reference a += GREGORIAN_REFERENCE; r = (int)a; return r; } static void gregorianDateFromAbsolute(int abs, int *day, int *month, int *year) { // Search forward year by year from approximate year *year = abs/366; while (abs >= absoluteGregorianDay(1, 1, (*year)+1)) { (*year)++; } // Search forward month by month from January (*month) = 1; while (abs > absoluteGregorianDay(lastDayOfGregorianMonth(*month, *year), *month, *year)) { (*month)++; } *day = abs - absoluteGregorianDay(1, *month, *year) + 1; } /** * Convert a broken out time specification into a time interval * since the reference date.
* External - so NSDate and others can use it. */ NSTimeInterval GSTime(int day, int month, int year, int hour, int minute, int second, int mil) { NSTimeInterval a; a = (NSTimeInterval)absoluteGregorianDay(day, month, year); // Calculate date as GMT a -= GREGORIAN_REFERENCE; a = (NSTimeInterval)a * 86400; a += hour * 3600; a += minute * 60; a += second; a += mil/1000.0; return a; } /** * Convert a time interval since the reference date into broken out * elements.
* External - so NSDate and others can use it. */ void GSBreakTime(NSTimeInterval when, int *year, int *month, int *day, int *hour, int *minute, int *second, int *mil) { int h, m, dayOfEra; double a, b, c, d; // Get reference date in terms of days a = when / 86400.0; // Offset by Gregorian reference a += GREGORIAN_REFERENCE; // result is the day of common era. dayOfEra = (int)a; // Calculate year, month, and day gregorianDateFromAbsolute(dayOfEra, day, month, year); // Calculate hour, minute, and seconds d = dayOfEra - GREGORIAN_REFERENCE; d *= 86400; a = abs(d - when); b = a / 3600; *hour = (int)b; h = *hour; h = h * 3600; b = a - h; b = b / 60; *minute = (int)b; m = *minute; m = m * 60; c = a - h - m; *second = (int)c; *mil = (a - h - m - c) * 1000; } @class NSGDate; @implementation NSCalendarDate + (void) initialize { if (self == [NSCalendarDate class]) { [self setVersion: 1]; localTZ = RETAIN([NSTimeZone localTimeZone]); dstClass = [GSTimeZone class]; absClass = [GSAbsTimeZone class]; offSEL = @selector(secondsFromGMTForDate:); offIMP = (int (*)(id,SEL,id)) [localTZ methodForSelector: offSEL]; dstOffIMP = (int (*)(id,SEL,id)) [dstClass instanceMethodForSelector: offSEL]; absOffIMP = (int (*)(id,SEL,id)) [absClass instanceMethodForSelector: offSEL]; abrSEL = @selector(abbreviationForDate:); abrIMP = (NSString* (*)(id,SEL,id)) [localTZ methodForSelector: abrSEL]; dstAbrIMP = (NSString* (*)(id,SEL,id)) [dstClass instanceMethodForSelector: abrSEL]; absAbrIMP = (NSString* (*)(id,SEL,id)) [absClass instanceMethodForSelector: abrSEL]; behavior_class_add_class(self, [NSGDate class]); } } /** * Getting an NSCalendar Date */ + (id) calendarDate { id d = [[self alloc] init]; return AUTORELEASE(d); } + (id) dateWithString: (NSString *)description calendarFormat: (NSString *)format { NSCalendarDate *d = [[self alloc] initWithString: description calendarFormat: format]; return AUTORELEASE(d); } + (id) dateWithString: (NSString *)description calendarFormat: (NSString *)format locale: (NSDictionary *)dictionary { NSCalendarDate *d = [[self alloc] initWithString: description calendarFormat: format locale: dictionary]; return AUTORELEASE(d); } + (id) dateWithYear: (int)year month: (unsigned int)month day: (unsigned int)day hour: (unsigned int)hour minute: (unsigned int)minute second: (unsigned int)second timeZone: (NSTimeZone *)aTimeZone { NSCalendarDate *d = [[self alloc] initWithYear: year month: month day: day hour: hour minute: minute second: second timeZone: aTimeZone]; return AUTORELEASE(d); } - (id) addTimeInterval: (NSTimeInterval)seconds { id newObj = [[self class] dateWithTimeIntervalSinceReferenceDate: [self timeIntervalSinceReferenceDate] + seconds]; [newObj setTimeZone: [self timeZoneDetail]]; return newObj; } - (Class) classForCoder { return [self class]; } - (id) replacementObjectForPortCoder: (NSPortCoder*)aRmc { return self; } - (void) encodeWithCoder: (NSCoder*)coder { [coder encodeValueOfObjCType: @encode(NSTimeInterval) at: &_seconds_since_ref]; [coder encodeObject: _calendar_format]; [coder encodeObject: _time_zone]; } - (id) initWithCoder: (NSCoder*)coder { [coder decodeValueOfObjCType: @encode(NSTimeInterval) at: &_seconds_since_ref]; [coder decodeValueOfObjCType: @encode(id) at: &_calendar_format]; [coder decodeValueOfObjCType: @encode(id) at: &_time_zone]; return self; } - (void) dealloc { RELEASE(_calendar_format); RELEASE(_time_zone); [super dealloc]; } /* * Initializing an NSCalendar Date */ - (id) initWithString: (NSString *)description { // +++ What is the locale? return [self initWithString: description calendarFormat: cformat locale: nil]; } - (id) initWithString: (NSString *)description calendarFormat: (NSString *)format { // ++ What is the locale? return [self initWithString: description calendarFormat: format locale: nil]; } /* * read up to the specified number of characters, terminating at a non-digit * except for leading whitespace characters. */ static inline int getDigits(const char *from, char *to, int limit) { int i = 0; int j = 0; BOOL foundDigit = NO; while (i < limit) { if (isdigit(from[i])) { to[j++] = from[i]; foundDigit = YES; } else if (isspace(from[i])) { if (foundDigit == YES) { break; } } else { break; } i++; } to[j] = '\0'; return i; } #define hadY 1 #define hadM 2 #define hadD 4 #define hadh 8 #define hadm 16 #define hads 32 #define hadw 64 - (id) initWithString: (NSString *)description calendarFormat: (NSString *)fmt locale: (NSDictionary *)locale { // If description does not match this format exactly, this method returns nil if ([description length] == 0) { // Autorelease self because it isn't done by the calling function // [[NSCalendarDate alloc] initWithString:calendarFormat:locale:]; AUTORELEASE(self); return nil; } else { int year = 0, month = 1, day = 1; int hour = 0, min = 0, sec = 0; NSTimeZone *tz = localTZ; BOOL ampm = NO; BOOL twelveHrClock = NO; int julianWeeks = -1, weekStartsMonday = 0, dayOfWeek = -1; const char *source = [description cString]; unsigned sourceLen = strlen(source); unichar *format; unsigned formatLen; unsigned formatIdx = 0; unsigned sourceIdx = 0; char tmpStr[20]; int tmpIdx; unsigned had = 0; int pos; BOOL hadPercent = NO; NSString *dForm; NSString *tForm; NSString *TForm; NSMutableData *fd; BOOL changedFormat = NO; if (locale == nil) { locale = GSUserDefaultsDictionaryRepresentation(); } if (fmt == nil) { fmt = [locale objectForKey: NSTimeDateFormatString]; if (fmt == nil) fmt = @""; } TForm = [locale objectForKey: NSTimeDateFormatString]; if (TForm == nil) TForm = @"%X %x"; dForm = [locale objectForKey: NSShortDateFormatString]; if (dForm == nil) dForm = @"%y-%m-%d"; tForm = [locale objectForKey: NSTimeFormatString]; if (tForm == nil) tForm = @"%H-%M-%S"; /* * Get format into a buffer, leaving room for expansion in case it has * escapes that need to be converted. */ formatLen = [fmt length]; fd = [[NSMutableData alloc] initWithLength: (formatLen + 32) * sizeof(unichar)]; format = (unichar*)[fd mutableBytes]; [fmt getCharacters: format]; /* * Expand any sequences to their basic components. */ for (pos = 0; pos < formatLen; pos++) { unichar c = format[pos]; if (c == '%') { if (hadPercent == YES) { hadPercent = NO; } else { hadPercent = YES; } } else { if (hadPercent == YES) { NSString *sub = nil; if (c == 'c') { sub = TForm; } else if (c == 'R') { sub = @"%H:%M"; } else if (c == 'r') { sub = @"%I:%M:%S %p"; } else if (c == 'X') { sub = tForm; } else if (c == 'x') { sub = dForm; } if (sub != nil) { unsigned sLen = [sub length]; unsigned i; if (sLen > 2) { [fd setLength: (formatLen + sLen - 2) * sizeof(unichar)]; format = (unichar*)[fd mutableBytes]; for (i = formatLen-1; i > pos; i--) { format[i+sLen-2] = format[i]; } } else { for (i = pos+1; i < formatLen; i++) { format[i+sLen-2] = format[i]; } [fd setLength: (formatLen + sLen - 2) * sizeof(unichar)]; format = (unichar*)[fd mutableBytes]; } [sub getCharacters: &format[pos-1]]; formatLen += sLen - 2; changedFormat = YES; pos -= 2; // Re-parse the newly substituted data. } } hadPercent = NO; } } /* * Set up calendar format. */ if (changedFormat == YES) { fmt = [NSString stringWithCharacters: format length: formatLen]; } ASSIGN(_calendar_format, fmt); // // WARNING: // %F, does NOT work. // and the underlying call has granularity to the second. // -Most locale stuff is dubious at best. // -Long day and month names depend on a non-alpha character after the // last digit to work. // // The strftime specifiers as used by OpenStep + %U. // // %% literal % character // %a abbreviated weekday name according to locale // %A full weekday name according to locale // %b abbreviated month name according to locale // %B full month name according to locale // %c same as '%X %x' // %d day of month as decimal number // %e same as %d without leading zero (you get a leading space instead) // %F milliseconds as a decimal number // %H hour as a decimal number using 24-hour clock // %I hour as a decimal number using 12-hour clock // %j day of year as a decimal number // %m month as decimal number // %M minute as decimal number // %p 'am' or 'pm' // %S second as decimal number // %U week of the current year as decimal number (Sunday first day) // %W week of the current year as decimal number (Monday first day) // %w day of the week as decimal number (Sunday = 0) // %x date with date representation for locale // %X time with time representation for locale // %y year as a decimal number without century // %Y year as a decimal number with century // %z time zone offset in hours and minutes from GMT (HHMM) // %Z time zone abbreviation while (formatIdx < formatLen) { if (format[formatIdx] != '%') { // If it's not a format specifier, ignore it. if (isspace(format[formatIdx])) { // Skip any amount of white space. while (source[sourceIdx] != 0 && isspace(source[sourceIdx])) { sourceIdx++; } } else { if (sourceIdx < sourceLen) { if (source[sourceIdx] != format[formatIdx]) { NSLog(@"Expected literal '%c' but got '%c'", format[formatIdx], source[sourceIdx]); } sourceIdx++; } } } else { // Skip '%' formatIdx++; switch (format[formatIdx]) { case '%': // skip literal % if (sourceIdx < sourceLen) { if (source[sourceIdx] != '%') { NSLog(@"Expected literal '%' but got '%c'", source[sourceIdx]); } sourceIdx++; } break; case 'a': // Are Short names three chars in all locales????? tmpStr[0] = toupper(source[sourceIdx]); if (sourceIdx < sourceLen) sourceIdx++; tmpStr[1] = tolower(source[sourceIdx]); if (sourceIdx < sourceLen) sourceIdx++; tmpStr[2] = tolower(source[sourceIdx]); if (sourceIdx < sourceLen) sourceIdx++; tmpStr[3] = '\0'; { NSString *currDay; NSArray *dayNames; currDay = [NSString stringWithCString: tmpStr]; dayNames = [locale objectForKey: NSShortWeekDayNameArray]; for (tmpIdx = 0; tmpIdx < 7; tmpIdx++) { if ([[dayNames objectAtIndex: tmpIdx] isEqual: currDay] == YES) { break; } } dayOfWeek = tmpIdx; had |= hadw; } break; case 'A': for (tmpIdx = sourceIdx; tmpIdx < sourceLen; tmpIdx++) { if (isalpha(source[tmpIdx])) { tmpStr[tmpIdx - sourceIdx] = source[tmpIdx]; } else { break; } } tmpStr[tmpIdx - sourceIdx] = '\0'; sourceIdx += tmpIdx - sourceIdx; { NSString *currDay; NSArray *dayNames; currDay = [NSString stringWithCString: tmpStr]; dayNames = [locale objectForKey: NSWeekDayNameArray]; for (tmpIdx = 0; tmpIdx < 7; tmpIdx++) { if ([[dayNames objectAtIndex: tmpIdx] isEqual: currDay] == YES) { break; } } dayOfWeek = tmpIdx; had |= hadw; } break; case 'b': // Are Short names three chars in all locales????? tmpStr[0] = toupper(source[sourceIdx]); if (sourceIdx < sourceLen) sourceIdx++; tmpStr[1] = tolower(source[sourceIdx]); if (sourceIdx < sourceLen) sourceIdx++; tmpStr[2] = tolower(source[sourceIdx]); if (sourceIdx < sourceLen) sourceIdx++; tmpStr[3] = '\0'; { NSString *currMonth; NSArray *monthNames; currMonth = [NSString stringWithCString: tmpStr]; monthNames = [locale objectForKey: NSShortMonthNameArray]; for (tmpIdx = 0; tmpIdx < 12; tmpIdx++) { if ([[monthNames objectAtIndex: tmpIdx] isEqual: currMonth] == YES) { break; } } month = tmpIdx+1; had |= hadM; } break; case 'B': for (tmpIdx = sourceIdx; tmpIdx < sourceLen; tmpIdx++) { if (isalpha(source[tmpIdx])) { tmpStr[tmpIdx - sourceIdx] = source[tmpIdx]; } else { break; } } tmpStr[tmpIdx - sourceIdx] = '\0'; sourceIdx += tmpIdx - sourceIdx; { NSString *currMonth; NSArray *monthNames; currMonth = [NSString stringWithCString: tmpStr]; monthNames = [locale objectForKey: NSMonthNameArray]; for (tmpIdx = 0; tmpIdx < 12; tmpIdx++) { if ([[monthNames objectAtIndex: tmpIdx] isEqual: currMonth] == YES) { break; } } month = tmpIdx+1; had |= hadM; } break; case 'd': // fall through case 'e': sourceIdx += getDigits(&source[sourceIdx], tmpStr, 2); day = atoi(tmpStr); had |= hadD; break; case 'F': NSLog(@"%F format ignored when creating date"); break; case 'I': // fall through twelveHrClock = YES; case 'H': sourceIdx += getDigits(&source[sourceIdx], tmpStr, 2); hour = atoi(tmpStr); had |= hadh; break; case 'j': sourceIdx += getDigits(&source[sourceIdx], tmpStr, 3); day = atoi(tmpStr); had |= hadD; break; case 'm': sourceIdx += getDigits(&source[sourceIdx], tmpStr, 2); month = atoi(tmpStr); had |= hadM; break; case 'M': sourceIdx += getDigits(&source[sourceIdx], tmpStr, 2); min = atoi(tmpStr); had |= hadm; break; case 'p': // Questionable assumption that all am/pm indicators are 2 // characters and in upper case.... tmpStr[0] = toupper(source[sourceIdx]); if (sourceIdx < sourceLen) sourceIdx++; tmpStr[1] = toupper(source[sourceIdx]); if (sourceIdx < sourceLen) sourceIdx++; tmpStr[2] = '\0'; { NSString *currAMPM; NSArray *amPMNames; currAMPM = [NSString stringWithCString: tmpStr]; amPMNames = [locale objectForKey: NSAMPMDesignation]; /* * The time addition is handled below because this * indicator only modifies the time on a 12hour clock. */ if ([[amPMNames objectAtIndex: 1] isEqual: currAMPM] == YES) { ampm = YES; } } break; case 'S': sourceIdx += getDigits(&source[sourceIdx], tmpStr, 2); sec = atoi(tmpStr); had |= hads; break; case 'w': sourceIdx += getDigits(&source[sourceIdx], tmpStr, 1); dayOfWeek = atoi(tmpStr); had |= hadw; break; case 'W': // Fall through weekStartsMonday = 1; case 'U': sourceIdx += getDigits(&source[sourceIdx], tmpStr, 1); julianWeeks = atoi(tmpStr); break; // case 'x': // break; // case 'X': // break; case 'y': sourceIdx += getDigits(&source[sourceIdx], tmpStr, 2); year = atoi(tmpStr); if (year >= 70) { year += 1900; } else { year += 2000; } had |= hadY; break; case 'Y': sourceIdx += getDigits(&source[sourceIdx], tmpStr, 4); year = atoi(tmpStr); had |= hadY; break; case 'z': { int sign = 1; int zone; if (source[sourceIdx] == '+') { sourceIdx++; } else if (source[sourceIdx] == '-') { sign = -1; sourceIdx++; } sourceIdx += getDigits(&source[sourceIdx], tmpStr, 4); zone = atoi(tmpStr) * sign; if ((tz = [NSTimeZone timeZoneForSecondsFromGMT: (zone / 100 * 60 + (zone % 100)) * 60]) == nil) { tz = localTZ; } } break; case 'Z': for (tmpIdx = sourceIdx; tmpIdx < sourceLen; tmpIdx++) { if (isalpha(source[tmpIdx]) || source[tmpIdx] == '-' || source[tmpIdx] == '+') { tmpStr[tmpIdx - sourceIdx] = source[tmpIdx]; } else { break; } } tmpStr[tmpIdx - sourceIdx] = '\0'; sourceIdx += tmpIdx - sourceIdx; { NSString *z = [NSString stringWithCString: tmpStr]; tz = [NSTimeZone timeZoneWithName: z]; if (tz == nil) { tz = [NSTimeZone timeZoneWithAbbreviation: z]; } if (tz == nil) { tz = localTZ; } } break; default: [NSException raise: NSInvalidArgumentException format: @"Invalid NSCalendar date, " @"specifier %c not recognized in format %@", format[formatIdx], fmt]; } } formatIdx++; } RELEASE(fd); if (tz == nil) { tz = localTZ; } if (twelveHrClock == YES) { if (ampm == YES && hour != 12) { hour += 12; } } if (julianWeeks != -1) { NSTimeZone *gmtZone; NSCalendarDate *d; int currDay; gmtZone = [NSTimeZone timeZoneForSecondsFromGMT: 0]; if ((had & (hadY|hadw)) != (hadY|hadw)) { NSCalendarDate *now = [NSCalendarDate date]; [now setTimeZone: gmtZone]; if ((had | hadY) == 0) { year = [now yearOfCommonEra]; had |= hadY; } if ((had | hadw) == 0) { dayOfWeek = [now dayOfWeek]; had |= hadw; } } d = [NSCalendarDate dateWithYear: year month: 1 day: 1 hour: 0 minute: 0 second: 0 timeZone: gmtZone]; currDay = [d dayOfWeek]; /* * The julian weeks are either sunday relative or monday relative * but all of the day of week specifiers are sunday relative. * This means that if no day of week specifier was used the week * starts on monday. */ if (dayOfWeek == -1) { if (weekStartsMonday) { dayOfWeek = 1; } else { dayOfWeek = 0; } } day = dayOfWeek + (julianWeeks * 7 - (currDay - 1)); had |= hadD; } return [self initWithYear: year month: month day: day hour: hour minute: min second: sec timeZone: tz]; } } /** * Returns an NSCalendarDate instance with the given year, month, day, * hour, minute, and second, using aTimeZone.
* The year includes the century (ie you can't just say '02' when you * mean '2002').
* The month is in the range 1 to 12,
* The day is in the range 1 to 31,
* The hour is in the range 0 to 23,
* The minute is in the range 0 to 59,
* The second is in the range 0 to 59.
* If aTimeZone is nil, the [NSTimeZone+localTimeZone] value is used. *

* GNUstep checks the validity of the method arguments, and unless * the base library was built with 'warn=no' it generates a warning * for bad values. It tries to use those bad values to generate a * date anyway though, rather than failing (this also appears to be * the behavior of MacOS-X). *

* The algorithm GNUstep uses to create the date is this ...
* * * Convert the broken out date values into a time interval since * the reference date, as if those values represent a GMT date/time. * * * Ask the time zone for the offset from GMT at the resulting date, * and apply that offset to the time interval ... so get the value * for the specified timezone. * * * Ask the time zone for the offset from GMT at the new date ... * in case the new date is in a different daylight savings time * band from the original date. If this offset differs from the * previous one, apply the difference so that the result is * corrected for daylight savings. This is the final result used. * * * After establishing the time interval we will use and completing * initialisation, we ask the time zone for the offset from GMT again. * If it is not the same as the last time, then the time specified by * the broken out date does not really exist ... since it's in the * period lost by the transition to daylight savings. The resulting * date is therefore not the date that was actually asked for, but is * the best approximation we can do. If the base library was not * built with 'warn=no' then a warning message is logged for this * condition. * * */ - (id) initWithYear: (int)year month: (unsigned int)month day: (unsigned int)day hour: (unsigned int)hour minute: (unsigned int)minute second: (unsigned int)second timeZone: (NSTimeZone *)aTimeZone { int c; NSTimeInterval s; NSTimeInterval oldOffset; NSTimeInterval newOffset; if (month < 1 || month > 12) { NSWarnMLog(@"invalid month given - %u", month); } c = lastDayOfGregorianMonth(month, year); if (day < 1 || day > c) { NSWarnMLog(@"invalid day given - %u", day); } if (hour > 23) { NSWarnMLog(@"invalid hour given - %u", hour); } if (minute > 59) { NSWarnMLog(@"invalid minute given - %u", minute); } if (second > 59) { NSWarnMLog(@"invalid second given - %u", second); } // Calculate date as GMT s = GSTime(day, month, year, hour, minute, second, 0); // Assign time zone detail if (aTimeZone == nil) { _time_zone = localTZ; // retain is a no-op for the local timezone. } else { _time_zone = RETAIN(aTimeZone); } _calendar_format = cformat; _seconds_since_ref = s; /* * Adjust date so it is correct for time zone. */ oldOffset = offset(_time_zone, self); s -= oldOffset; _seconds_since_ref = s; /* * See if we need to adjust for daylight savings time */ newOffset = offset(_time_zone, self); if (oldOffset != newOffset) { s -= (newOffset - oldOffset); _seconds_since_ref = s; oldOffset = offset(_time_zone, self); /* * If the adjustment puts us in another offset, we must be in the * non-existent period at the start of daylight savings time. */ if (oldOffset != newOffset) { NSWarnMLog(@"init non-existent time at start of daylight savings"); } } return self; } /** * Initialises the receiver with the specified interval since the * reference date. Uses th standard format string "%Y-%m-%d %H:%M:%S %z" * and the default time zone. */ - (id) initWithTimeIntervalSinceReferenceDate: (NSTimeInterval)seconds { _seconds_since_ref = seconds; if (_calendar_format == nil) { _calendar_format = cformat; } if (_time_zone == nil) { _time_zone = localTZ; // retain is a no-op for the local timezone. } return self; } - (int) dayOfCommonEra { NSTimeInterval when; when = _seconds_since_ref + offset(_time_zone, self); return dayOfCommonEra(when); } - (int) dayOfMonth { int m, d, y; NSTimeInterval when; when = _seconds_since_ref + offset(_time_zone, self); gregorianDateFromAbsolute(dayOfCommonEra(when), &d, &m, &y); return d; } - (int) dayOfWeek { int d; NSTimeInterval when; when = _seconds_since_ref + offset(_time_zone, self); d = dayOfCommonEra(when); /* The era started on a sunday. Did we always have a seven day week? Did we lose week days changing from Julian to Gregorian? AFAIK seven days a week is ok for all reasonable dates. */ d = d % 7; if (d < 0) d += 7; return d; } - (int) dayOfYear { int m, d, y, days, i; NSTimeInterval when; when = _seconds_since_ref + offset(_time_zone, self); gregorianDateFromAbsolute(dayOfCommonEra(when), &d, &m, &y); days = d; for (i = m - 1; i > 0; i--) // days in prior months this year days = days + lastDayOfGregorianMonth(i, y); return days; } - (int) hourOfDay { int h; double a, d; NSTimeInterval when; when = _seconds_since_ref + offset(_time_zone, self); d = dayOfCommonEra(when); d -= GREGORIAN_REFERENCE; d *= 86400; a = abs(d - (_seconds_since_ref + offset(_time_zone, self))); a = a / 3600; h = (int)a; // There is a small chance of getting // it right at the stroke of midnight if (h == 24) h = 0; return h; } - (int) minuteOfHour { int h, m; double a, b, d; NSTimeInterval when; when = _seconds_since_ref + offset(_time_zone, self); d = dayOfCommonEra(when); d -= GREGORIAN_REFERENCE; d *= 86400; a = abs(d - (_seconds_since_ref + offset(_time_zone, self))); b = a / 3600; h = (int)b; h = h * 3600; b = a - h; b = b / 60; m = (int)b; return m; } - (int) monthOfYear { int m, d, y; NSTimeInterval when; when = _seconds_since_ref + offset(_time_zone, self); gregorianDateFromAbsolute(dayOfCommonEra(when), &d, &m, &y); return m; } - (int) secondOfMinute { int h, m, s; double a, b, c, d; NSTimeInterval when; when = _seconds_since_ref + offset(_time_zone, self); d = dayOfCommonEra(when); d -= GREGORIAN_REFERENCE; d *= 86400; a = abs(d - (_seconds_since_ref + offset(_time_zone, self))); b = a / 3600; h = (int)b; h = h * 3600; b = a - h; b = b / 60; m = (int)b; m = m * 60; c = a - h - m; s = (int)c; return s; } - (int) yearOfCommonEra { int m, d, y; NSTimeInterval when; when = _seconds_since_ref + offset(_time_zone, self); gregorianDateFromAbsolute(dayOfCommonEra(when), &d, &m, &y); return y; } /** * This method exists solely for conformance to the OpenStep spec. * Its use is deprecated ... it simply calls * -dateByAddingYears:months:days:hours:minutes:seconds: */ - (NSCalendarDate*) addYear: (int)year month: (int)month day: (int)day hour: (int)hour minute: (int)minute second: (int)second { return [self dateByAddingYears: year months: month days: day hours: hour minutes: minute seconds: second]; } // Getting String Descriptions of Dates - (NSString*) description { return [self descriptionWithCalendarFormat: _calendar_format locale: nil]; } - (NSString*) descriptionWithCalendarFormat: (NSString *)format { return [self descriptionWithCalendarFormat: format locale: nil]; } #define UNIX_REFERENCE_INTERVAL -978307200.0 - (NSString*) descriptionWithCalendarFormat: (NSString*)format locale: (NSDictionary*)locale { char buf[1024]; const char *f; int lf; BOOL mtag = NO, dtag = NO, ycent = NO; BOOL mname = NO, dname = NO; double s; int yd = 0, md = 0, mnd = 0, sd = 0, dom = -1, dow = -1, doy = -1; int hd = 0, nhd, mil; int i, j, k, z; if (locale == nil) locale = GSUserDefaultsDictionaryRepresentation(); if (format == nil) format = [locale objectForKey: NSTimeDateFormatString]; // If the format is nil then return an empty string if (!format) return @""; f = [format cString]; lf = strlen(f); GSBreakTime(_seconds_since_ref + offset(_time_zone, self), &yd, &md, &dom, &hd, &mnd, &sd, &mil); nhd = hd; // The strftime specifiers // %a abbreviated weekday name according to locale // %A full weekday name according to locale // %b abbreviated month name according to locale // %B full month name according to locale // %d day of month as decimal number (leading zero) // %e day of month as decimal number (leading space) // %F milliseconds (000 to 999) // %H hour as a decimal number using 24-hour clock // %I hour as a decimal number using 12-hour clock // %j day of year as a decimal number // %m month as decimal number // %M minute as decimal number // %p 'am' or 'pm' // %S second as decimal number // %U week of the current year as decimal number (Sunday first day) // %W week of the current year as decimal number (Monday first day) // %w day of the week as decimal number (Sunday = 0) // %y year as a decimal number without century // %Y year as a decimal number with century // %z time zone offset (HHMM) // %Z time zone // %% literal % character // Find the order of date elements // and translate format string into printf ready string j = 0; for (i = 0;i < lf; ++i) { // Only care about a format specifier if (f[i] == '%') { // check the character that comes after switch (f[i+1]) { // literal % case '%': ++i; buf[j] = f[i]; ++j; break; // is it the year case 'Y': ycent = YES; case 'y': ++i; if (ycent) k = VSPRINTF_LENGTH(sprintf(&(buf[j]), "%04d", yd)); else k = VSPRINTF_LENGTH(sprintf(&(buf[j]), "%02d", yd % 100)); j += k; break; // is it the month case 'b': mname = YES; case 'B': mtag = YES; // Month is character string case 'm': ++i; if (mtag) { NSArray *months; NSString *name; if (mname) months = [locale objectForKey: NSShortMonthNameArray]; else months = [locale objectForKey: NSMonthNameArray]; name = [months objectAtIndex: md-1]; if (name) k = VSPRINTF_LENGTH(sprintf(&(buf[j]), "%s", [name cString])); else k = VSPRINTF_LENGTH(sprintf(&(buf[j]), "%02d", md)); } else k = VSPRINTF_LENGTH(sprintf(&(buf[j]), "%02d", md)); j += k; break; case 'd': // day of month ++i; k = VSPRINTF_LENGTH(sprintf(&(buf[j]), "%02d", dom)); j += k; break; case 'e': // day of month ++i; k = VSPRINTF_LENGTH(sprintf(&(buf[j]), "%2d", dom)); j += k; break; case 'F': // milliseconds s = ([self dayOfCommonEra] - GREGORIAN_REFERENCE) * 86400.0; s -= (_seconds_since_ref + offset(_time_zone, self)); s = fabs(s); s -= floor(s); ++i; k = VSPRINTF_LENGTH(sprintf(&(buf[j]), "%03d", (int)(s*1000))); j += k; break; case 'j': // day of year if (doy < 0) doy = [self dayOfYear]; ++i; k = VSPRINTF_LENGTH(sprintf(&(buf[j]), "%02d", doy)); j += k; break; // is it the week-day case 'a': dname = YES; case 'A': dtag = YES; // Day is character string case 'w': { ++i; if (dow < 0) dow = [self dayOfWeek]; if (dtag) { NSArray *days; NSString *name; if (dname) days = [locale objectForKey: NSShortWeekDayNameArray]; else days = [locale objectForKey: NSWeekDayNameArray]; name = [days objectAtIndex: dow]; if (name) k = VSPRINTF_LENGTH(sprintf(&(buf[j]), "%s", [name cString])); else k = VSPRINTF_LENGTH(sprintf(&(buf[j]), "%01d", dow)); } else k = VSPRINTF_LENGTH(sprintf(&(buf[j]), "%01d", dow)); j += k; } break; // is it the hour case 'I': nhd = hd % 12; // 12 hour clock if (hd == 12) nhd = 12; // 12pm not 0pm case 'H': ++i; k = VSPRINTF_LENGTH(sprintf(&(buf[j]), "%02d", nhd)); j += k; break; // is it the minute case 'M': ++i; k = VSPRINTF_LENGTH(sprintf(&(buf[j]), "%02d", mnd)); j += k; break; // is it the second case 'S': ++i; k = VSPRINTF_LENGTH(sprintf(&(buf[j]), "%02d", sd)); j += k; break; // Is it the am/pm indicator case 'p': { NSArray *a = [locale objectForKey: NSAMPMDesignation]; NSString *ampm; ++i; if (hd >= 12) { if ([a count] > 1) ampm = [a objectAtIndex: 1]; else ampm = @"pm"; } else { if ([a count] > 0) ampm = [a objectAtIndex: 0]; else ampm = @"am"; } k = VSPRINTF_LENGTH(sprintf(&(buf[j]), [ampm cString])); j += k; } break; // is it the zone name case 'Z': ++i; k = VSPRINTF_LENGTH(sprintf(&(buf[j]), "%s", [abbrev(_time_zone, self) UTF8String])); j += k; break; case 'z': ++i; z = offset(_time_zone, self); if (z < 0) { z = -z; z /= 60; k = VSPRINTF_LENGTH(sprintf(&(buf[j]),"-%02d%02d",z/60,z%60)); } else { z /= 60; k = VSPRINTF_LENGTH(sprintf(&(buf[j]),"+%02d%02d",z/60,z%60)); } j += k; break; // Anything else is unknown so just copy default: buf[j] = f[i]; ++i; ++j; buf[j] = f[i]; ++i; ++j; break; } } else { buf[j] = f[i]; ++j; } } buf[j] = '\0'; return [NSString stringWithCString: buf]; } - (id) copyWithZone: (NSZone*)zone { NSCalendarDate *newDate; if (NSShouldRetainWithZone(self, zone)) { newDate = RETAIN(self); } else { newDate = (NSCalendarDate*)NSCopyObject(self, 0, zone); if (newDate != nil) { if (_calendar_format != cformat) { newDate->_calendar_format = [_calendar_format copyWithZone: zone]; } if (_time_zone != localTZ) { newDate->_time_zone = RETAIN(_time_zone); } } } return newDate; } - (NSString*) descriptionWithLocale: (NSDictionary *)locale { return [self descriptionWithCalendarFormat: _calendar_format locale: locale]; } // Getting and Setting Calendar Formats - (NSString*) calendarFormat { return _calendar_format; } - (void) setCalendarFormat: (NSString *)format { RELEASE(_calendar_format); _calendar_format = [format copyWithZone: [self zone]]; } // Getting and Setting Time Zones - (void) setTimeZone: (NSTimeZone *)aTimeZone { ASSIGN(_time_zone, aTimeZone); } - (NSTimeZone*) timeZone { return _time_zone; } - (NSTimeZoneDetail*) timeZoneDetail { NSTimeZoneDetail *detail = [_time_zone timeZoneDetailForDate: self]; return detail; } @end // // Routines for manipulating Gregorian dates // // The following code is based upon the source code in // ``Calendrical Calculations'' by Nachum Dershowitz and Edward M. Reingold, // Software---Practice & Experience, vol. 20, no. 9 (September, 1990), // pp. 899--928. // @implementation NSCalendarDate (GregorianDate) - (int) lastDayOfGregorianMonth: (int)month year: (int)year { return lastDayOfGregorianMonth(month, year); } - (int) absoluteGregorianDay: (int)day month: (int)month year: (int)year { return absoluteGregorianDay(day, month, year); } - (void) gregorianDateFromAbsolute: (int)d day: (int *)day month: (int *)month year: (int *)year { // Search forward year by year from approximate year *year = d/366; while (d >= absoluteGregorianDay(1, 1, (*year)+1)) (*year)++; // Search forward month by month from January (*month) = 1; while (d > absoluteGregorianDay(lastDayOfGregorianMonth(*month, *year), *month, *year)) (*month)++; *day = d - absoluteGregorianDay(1, *month, *year) + 1; } @end @implementation NSCalendarDate (OPENSTEP) /** *

Returns a calendar date formed by adding the specified offsets to the * receiver. The offsets are added in order, years, then months, then * days, then hours then minutes then seconds, so if you add 1 month and * forty days to 20th September, the result will be 9th November. *

*

This method understands leap years and tries to adjust for daylight * savings time changes so that it preserves expected clock time. *

*/ - (NSCalendarDate*) dateByAddingYears: (int)years months: (int)months days: (int)days hours: (int)hours minutes: (int)minutes seconds: (int)seconds { NSCalendarDate *c; NSTimeInterval s; NSTimeInterval oldOffset; NSTimeInterval newOffset; int i, year, month, day, hour, minute, second, mil; oldOffset = offset(_time_zone, self); /* * Break into components in GMT time zone. */ GSBreakTime(_seconds_since_ref, &year, &month, &day, &hour, &minute, &second, &mil); while (years != 0 || months != 0 || days != 0 || hours != 0 || minutes != 0 || seconds != 0) { year += years; years = 0; month += months; months = 0; while (month > 12) { year++; month -= 12; } while (month < 1) { year--; month += 12; } day += days; days = 0; if (day > 28) { i = lastDayOfGregorianMonth(month, year); while (day > i) { day -= i; if (month < 12) { month++; } else { month = 1; year++; } i = lastDayOfGregorianMonth(month, year); } } else { while (day < 1) { if (month == 1) { year--; month = 12; } else { month--; } day += lastDayOfGregorianMonth(month, year); } } hour += hours; hours = 0; days += hour/24; hour %= 24; if (hour < 0) { days--; hour += 24; } minute += minutes; minutes = 0; hours += minute/60; minute %= 60; if (minute < 0) { hours++; minute += 60; } second += seconds; seconds = 0; minutes += second/60; second %= 60; if (second < 0) { minutes--; second += 60; } } /* * Reassemble in GMT time zone. */ s = GSTime(day, month, year, hour, minute, second, mil); c = [NSCalendarDate alloc]; c->_calendar_format = cformat; c->_time_zone = RETAIN([self timeZone]); c->_seconds_since_ref = s; /* * Adjust date to try to maintain the time of day over * a daylight savings time boundary if necessary. */ newOffset = offset(_time_zone, c); if (newOffset != oldOffset) { NSTimeInterval tmpOffset = newOffset; s -= (newOffset - oldOffset); c->_seconds_since_ref = s; /* * If the date we have lies within a missing hour at a * daylight savings time transition, we use the original * date rather than the adjusted one. */ newOffset = offset(_time_zone, c); if (newOffset == oldOffset) { s += (tmpOffset - oldOffset); c->_seconds_since_ref = s; } } return AUTORELEASE(c); } - (void) years: (int*)years months: (int*)months days: (int*)days hours: (int*)hours minutes: (int*)minutes seconds: (int*)seconds sinceDate: (NSDate*)date { NSCalendarDate *start; NSCalendarDate *end; NSCalendarDate *tmp; int diff; int extra; int sign; int mil; int syear, smonth, sday, shour, sminute, ssecond; int eyear, emonth, eday, ehour, eminute, esecond; /* FIXME What if the two dates are in different time zones? How about daylight savings time? */ if ([date isKindOfClass: [NSCalendarDate class]]) tmp = (NSCalendarDate*)RETAIN(date); else if ([date isKindOfClass: [NSDate class]]) tmp = [[NSCalendarDate alloc] initWithTimeIntervalSinceReferenceDate: [date timeIntervalSinceReferenceDate]]; else [NSException raise: NSInvalidArgumentException format: @"%@ invalid date given - %@", NSStringFromSelector(_cmd), date]; end = (NSCalendarDate*)[self laterDate: tmp]; if (end == self) { start = tmp; sign = 1; } else { start = self; sign = -1; } GSBreakTime(start->_seconds_since_ref + offset(start->_time_zone, start), &syear, &smonth, &sday, &shour, &sminute, &ssecond, &mil); GSBreakTime(end->_seconds_since_ref + offset(end->_time_zone, end), &eyear, &emonth, &eday, &ehour, &eminute, &esecond, &mil); /* Calculate year difference and leave any remaining months in 'extra' */ diff = eyear - syear; extra = 0; if (emonth < smonth) { diff--; extra += 12; } if (years) *years = sign*diff; else extra += diff*12; /* Calculate month difference and leave any remaining days in 'extra' */ diff = emonth - smonth + extra; extra = 0; if (eday < sday) { diff--; extra = [end lastDayOfGregorianMonth: smonth year: syear]; } if (months) *months = sign*diff; else { while (diff--) { int tmpmonth = emonth - diff - 1; int tmpyear = eyear; while (tmpmonth < 1) { tmpmonth += 12; tmpyear--; } extra += lastDayOfGregorianMonth(tmpmonth, tmpyear); } } /* Calculate day difference and leave any remaining hours in 'extra' */ diff = eday - sday + extra; extra = 0; if (ehour < shour) { diff--; extra = 24; } if (days) *days = sign*diff; else extra += diff*24; /* Calculate hour difference and leave any remaining minutes in 'extra' */ diff = ehour - shour + extra; extra = 0; if (eminute < sminute) { diff--; extra = 60; } if (hours) *hours = sign*diff; else extra += diff*60; /* Calculate minute difference and leave any remaining seconds in 'extra' */ diff = eminute - sminute + extra; extra = 0; if (esecond < ssecond) { diff--; extra = 60; } if (minutes) *minutes = sign*diff; else extra += diff*60; diff = esecond - ssecond + extra; if (seconds) *seconds = sign*diff; RELEASE(tmp); } @end