mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-09-28 21:27:40 +00:00
The initWithString: method previously initialized the underlying JavaScript Date object using the local-time constructor (new Date(year, month, day)). This caused the resulting absolute time to be corrupted by the host engine's local time zone configuration, effectively ignoring the explicit time zone offset provided in the initialization string. The implementation was a fundamental violation of the canonical contract, as established by NSDate and to be followed by CPDate. This commit refactors the parsing logic to use Date.UTC(), guaranteeing that the parsed components are evaluated independently of the host environment. Additionally, it eliminates a redundant RegExp allocation during pattern matching and updates the API documentation to explicitly state that offset resolution is decoupled from both CPTimeZone and the host's local time rules.
312 lines
8.3 KiB
Plaintext
312 lines
8.3 KiB
Plaintext
/*
|
|
* CPDate.j
|
|
* Foundation
|
|
*
|
|
* Created by Thomas Robinson.
|
|
* Copyright 2008, 280 North, Inc.
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 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
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
@import "CPObject.j"
|
|
@import "CPString.j"
|
|
@import "CPException.j"
|
|
|
|
var CPDateReferenceDate = new Date(Date.UTC(2001, 0, 1, 0, 0, 0, 0));
|
|
|
|
/*!
|
|
@class CPDate
|
|
@ingroup foundation
|
|
@brief A representation of a single point in time.
|
|
*/
|
|
@implementation CPDate : CPObject
|
|
{
|
|
}
|
|
|
|
+ (id)alloc
|
|
{
|
|
var result = new Date;
|
|
result.isa = [self class];
|
|
return result;
|
|
}
|
|
|
|
+ (id)date
|
|
{
|
|
return [[self alloc] init];
|
|
}
|
|
|
|
+ (id)dateWithTimeIntervalSinceNow:(CPTimeInterval)seconds
|
|
{
|
|
return [[CPDate alloc] initWithTimeIntervalSinceNow:seconds];
|
|
}
|
|
|
|
+ (id)dateWithTimeIntervalSince1970:(CPTimeInterval)seconds
|
|
{
|
|
return [[CPDate alloc] initWithTimeIntervalSince1970:seconds];
|
|
}
|
|
|
|
+ (id)dateWithTimeIntervalSinceReferenceDate:(CPTimeInterval)seconds
|
|
{
|
|
return [[CPDate alloc] initWithTimeIntervalSinceReferenceDate:seconds];
|
|
}
|
|
|
|
+ (id)distantPast
|
|
{
|
|
return [CPDate dateWithTimeIntervalSinceReferenceDate:-63113817600.0];
|
|
}
|
|
|
|
+ (id)distantFuture
|
|
{
|
|
return [CPDate dateWithTimeIntervalSinceReferenceDate:63113990400.0];
|
|
}
|
|
|
|
- (id)initWithTimeIntervalSinceNow:(CPTimeInterval)seconds
|
|
{
|
|
if (!_isNumberType(seconds))
|
|
CPLog.warn(@"The parameter of the method initWithTimeIntervalSinceNow: should be an integer or a float");
|
|
|
|
self = new Date((new Date()).getTime() + seconds * 1000);
|
|
return self;
|
|
}
|
|
|
|
- (id)initWithTimeIntervalSince1970:(CPTimeInterval)seconds
|
|
{
|
|
if (!_isNumberType(seconds))
|
|
CPLog.warn(@"The parameter of the method initWithTimeIntervalSince1970: should be an integer or a float");
|
|
|
|
self = new Date(seconds * 1000);
|
|
return self;
|
|
}
|
|
|
|
- (id)initWithTimeIntervalSinceReferenceDate:(CPTimeInterval)seconds
|
|
{
|
|
if (!_isNumberType(seconds))
|
|
CPLog.warn(@"The parameter of the method initWithTimeIntervalSinceReferenceDate: should be an integer or a float");
|
|
|
|
self = [self initWithTimeInterval:seconds sinceDate:CPDateReferenceDate];
|
|
return self;
|
|
}
|
|
|
|
- (id)initWithTimeInterval:(CPTimeInterval)seconds sinceDate:(CPDate)refDate
|
|
{
|
|
if (!_isNumberType(seconds))
|
|
CPLog.warn(@"The parameter of the method initWithTimeInterval:sinceDate: should be an integer or a float");
|
|
|
|
self = new Date(refDate.getTime() + seconds * 1000);
|
|
return self;
|
|
}
|
|
|
|
/*!
|
|
Returns a CPDate initialized with a date and time specified by the given
|
|
string in international date format YYYY-MM-DD HH:MM:SS ±HHMM (e.g.
|
|
2009-11-17 17:52:04 +0000).
|
|
|
|
The offset is taken verbatim from the string; the result does not depend
|
|
on the host's local time zone or any CPTimeZone data. Callers needing an
|
|
offset derived from an actual IANA zone (e.g. accounting for DST) are
|
|
responsible for resolving it themselves via the browser's Intl API before
|
|
constructing the string.
|
|
*/
|
|
- (id)initWithString:(CPString)description
|
|
{
|
|
var format = new RegExp("(\\d{4})-(\\d{2})-(\\d{2}) (\\d{2}):(\\d{2}):(\\d{2}) ([-+])(\\d{2})(\\d{2})"),
|
|
d = description.match(format);
|
|
|
|
if (!d || d.length != 10)
|
|
[CPException raise:CPInvalidArgumentException
|
|
reason:"initWithString: the string must be in YYYY-MM-DD HH:MM:SS ±HHMM format"];
|
|
|
|
var timeZoneOffsetMinutes = (Number(d[8]) * 60 + Number(d[9])) * (d[7] === '-' ? 1 : -1),
|
|
utcMillis = Date.UTC(Number(d[1]), Number(d[2]) - 1, Number(d[3]),
|
|
Number(d[4]), Number(d[5]), Number(d[6]));
|
|
|
|
self = new Date(utcMillis + timeZoneOffsetMinutes * 60 * 1000);
|
|
return self;
|
|
}
|
|
|
|
- (CPTimeInterval)timeIntervalSinceDate:(CPDate)anotherDate
|
|
{
|
|
return (self.getTime() - anotherDate.getTime()) / 1000.0;
|
|
}
|
|
|
|
- (CPTimeInterval)timeIntervalSinceNow
|
|
{
|
|
return [self timeIntervalSinceDate:[CPDate date]];
|
|
}
|
|
|
|
- (CPTimeInterval)timeIntervalSince1970
|
|
{
|
|
return self.getTime() / 1000.0;
|
|
}
|
|
|
|
- (CPTimeInterval)timeIntervalSinceReferenceDate
|
|
{
|
|
return (self.getTime() - CPDateReferenceDate.getTime()) / 1000.0;
|
|
}
|
|
|
|
+ (CPTimeInterval)timeIntervalSinceReferenceDate
|
|
{
|
|
return [[CPDate date] timeIntervalSinceReferenceDate];
|
|
}
|
|
|
|
/**
|
|
Return a new date representing the receiver's time plus the given interval into the future,
|
|
or into the past for a negative interval.
|
|
*/
|
|
- (id)dateByAddingTimeInterval:(CPTimeInterval)seconds
|
|
{
|
|
return [[CPDate alloc] initWithTimeInterval:seconds sinceDate:self];
|
|
}
|
|
|
|
- (BOOL)isEqual:(CPDate)aDate
|
|
{
|
|
if (self === aDate)
|
|
return YES;
|
|
|
|
if (!aDate || ![aDate isKindOfClass:[CPDate class]])
|
|
return NO;
|
|
|
|
return [self isEqualToDate:aDate];
|
|
}
|
|
|
|
- (BOOL)isEqualToDate:(CPDate)aDate
|
|
{
|
|
if (!aDate)
|
|
return NO;
|
|
|
|
return !(self < aDate || self > aDate);
|
|
}
|
|
|
|
- (CPComparisonResult)compare:(CPDate)anotherDate
|
|
{
|
|
return (self > anotherDate) ? CPOrderedDescending : ((self < anotherDate) ? CPOrderedAscending : CPOrderedSame);
|
|
}
|
|
|
|
- (CPDate)earlierDate:(CPDate)anotherDate
|
|
{
|
|
return (self < anotherDate) ? self : anotherDate;
|
|
}
|
|
|
|
- (CPDate)laterDate:(CPDate)anotherDate
|
|
{
|
|
return (self > anotherDate) ? self : anotherDate;
|
|
}
|
|
|
|
/*!
|
|
Returns timezone offset as a string in ±HHMM format
|
|
*/
|
|
+ (CPString)timezoneOffsetString:(int)timezoneOffset
|
|
{
|
|
var offset = -timezoneOffset,
|
|
positive = offset >= 0,
|
|
hours = positive ? FLOOR(offset / 60) : CEIL(offset / 60),
|
|
minutes = offset - hours * 60;
|
|
return [CPString stringWithFormat:@"%s%02d%02d", positive ? "+" : "-", ABS(hours), ABS(minutes)];
|
|
}
|
|
|
|
/*!
|
|
Returns the date as a string in the international format
|
|
YYYY-MM-DD HH:MM:SS ±HHMM.
|
|
*/
|
|
- (CPString)description
|
|
{
|
|
return [CPString stringWithFormat:@"%04d-%02d-%02d %02d:%02d:%02d %s", self.getFullYear(), self.getMonth() + 1, self.getDate(), self.getHours(), self.getMinutes(), self.getSeconds(), [CPDate timezoneOffsetString:self.getTimezoneOffset()]];
|
|
}
|
|
|
|
- (id)copy
|
|
{
|
|
return new Date(self.getTime());
|
|
}
|
|
|
|
@end
|
|
|
|
var CPDateTimeKey = @"CPDateTimeKey";
|
|
|
|
@implementation CPDate (CPCoding)
|
|
|
|
- (id)initWithCoder:(CPCoder)aCoder
|
|
{
|
|
if (self)
|
|
{
|
|
self.setTime([aCoder decodeIntForKey:CPDateTimeKey]);
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)encodeWithCoder:(CPCoder)aCoder
|
|
{
|
|
[aCoder encodeInt:self.getTime() forKey:CPDateTimeKey];
|
|
}
|
|
|
|
@end
|
|
|
|
// Based on 'Universal JavaScript Date.parse for ISO 8601' available at https://github.com/csnover/js-iso8601.
|
|
var numericKeys = [1, 4, 5, 6, 7, 10, 11];
|
|
|
|
Date.parseISO8601 = function (date)
|
|
{
|
|
var timestamp,
|
|
struct,
|
|
minutesOffset = 0;
|
|
|
|
// First, check for native parsing.
|
|
timestamp = Date.parse(date);
|
|
|
|
if (isNaN(timestamp) && (struct = new RegExp("^(\\d{4}|[+\\-]\\d{6})(?:-(\\d{2})(?:-(\\d{2}))?)?(?:T(\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.(\\d{3}))?)?(?:(Z)|([+\\-])(\\d{2})(?::(\\d{2}))?)?)?$").exec(date)))
|
|
{
|
|
// avoid NaN timestamps caused by "undefined" values being passed to Date.UTC
|
|
for (var i = 0, k; (k = numericKeys[i]); ++i)
|
|
struct[k] = +struct[k] || 0;
|
|
|
|
// allow undefined days and months
|
|
struct[2] = (+struct[2] || 1) - 1;
|
|
struct[3] = +struct[3] || 1;
|
|
|
|
if (struct[8] !== 'Z' && struct[9] !== undefined)
|
|
{
|
|
minutesOffset = struct[10] * 60 + struct[11];
|
|
|
|
if (struct[9] === '+')
|
|
minutesOffset = 0 - minutesOffset;
|
|
}
|
|
|
|
return Date.UTC(struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7]);
|
|
}
|
|
|
|
return timestamp;
|
|
};
|
|
|
|
if (Date.prototype.isa !== CPDate)
|
|
{
|
|
Object.defineProperties(Date.prototype,
|
|
{
|
|
isa:
|
|
{
|
|
value: CPDate,
|
|
enumerable: false,
|
|
writable: true
|
|
}
|
|
});
|
|
}
|
|
|
|
function _isNumberType(value)
|
|
{
|
|
if (typeof value === 'number')
|
|
return YES;
|
|
else
|
|
return NO;
|
|
}
|