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.
This commit is contained in:
David Richardson committed 2026-08-28 19:59:11 -06:00
1 parent 23dff002a4
commit a6969bc756
1 file changed
+19 -16
+19 -16
View File
@@ -109,28 +109,31 @@ var CPDateReferenceDate = new Date(Date.UTC(2001, 0, 1, 0, 0, 0, 0));
}
/*!
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).
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(new RegExp(format));
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"];
if (!d || d.length != 10)
[CPException raise:CPInvalidArgumentException
reason:"initWithString: the string must be in YYYY-MM-DD HH:MM:SS ±HHMM format"];
var date = new Date(d[1], d[2] - 1, d[3]),
timeZoneOffset = (Number(d[8]) * 60 + Number(d[9])) * (d[7] === '-' ? 1 : -1);
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]));
date.setHours(d[4]);
date.setMinutes(d[5]);
date.setSeconds(d[6]);
self = new Date(date.getTime() + (timeZoneOffset - date.getTimezoneOffset()) * 60 * 1000);
return self;
self = new Date(utcMillis + timeZoneOffsetMinutes * 60 * 1000);
return self;
}
- (CPTimeInterval)timeIntervalSinceDate:(CPDate)anotherDate