Files
cappuccino/Foundation/CPDate.j
T
David Richardson a6969bc756 Fix CPDate string initialization to respect explicit time zone offsets
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.
2026-08-28 19:59:11 -06:00

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;
}