From 71c2501ff6c0ae642c62b3a183b409a66830ad7e Mon Sep 17 00:00:00 2001 From: David Richardson Date: Fri, 11 Sep 2026 18:28:15 -0600 Subject: [PATCH 1/4] Update CPTimeZoneTest temporal baselines Expand test coverage for timezone parsing, offset evaluation, and GMT validation. Establishes a strict temporal baseline and formally exposes existing logic flaws in CPTimeZone prior to applying implementation fixes. --- Foundation/CPTimeZone.j | 1263 ++++++++++++++++------------- Tests/Foundation/CPTimeZoneTest.j | 181 ++--- 2 files changed, 778 insertions(+), 666 deletions(-) diff --git a/Foundation/CPTimeZone.j b/Foundation/CPTimeZone.j index efd1de880..5b2e3dd6c 100644 --- a/Foundation/CPTimeZone.j +++ b/Foundation/CPTimeZone.j @@ -1,23 +1,23 @@ /* CPTimeZone.j -* Foundation -* -* Created by Alexandre Wilhelm -* Copyright 2012 -* -* 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 -*/ + * Foundation + * + * Created by Alexandre Wilhelm + * Copyright 2012 + * + * 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" @@ -36,383 +36,447 @@ CPTimeZoneNameStyleShortGeneric = 5; CPSystemTimeZoneDidChangeNotification = @"CPSystemTimeZoneDidChangeNotification"; +/* + * Time zone identity is defined by its IANA name. + * Offsets, abbreviations, and localized names are resolved dynamically + * using the runtime's Intl API rather than static tables. + * + * Dynamic resolution prevents the silent data drift of hardcoded tables + * caused by DST rule changes and newly added zones. It also avoids the + * inherent ambiguity of abbreviations (e.g., CST, IST), which are not + * globally unique and cannot serve as primary keys. + */ var abbreviationDictionary, - timeDifferenceFromUTC, - knownTimeZoneNames, - defaultTimeZone, - localTimeZone, - systemTimeZone, - timeZoneDataVersion, - localizedName; +knownTimeZoneNames, +knownTimeZoneNamesSet, +defaultTimeZone, +localTimeZone, +systemTimeZone, +timeZoneDataVersion; -function abbreviationForDate(date) +// Per-zone cache of the sampled standard/daylight offsets (see +// _standardAndDaylightOffsetsForZone). A zone's DST rule doesn't change +// within a process lifetime, so this is safe to keep for the session. +var _stdDstOffsetCache = {}; + +// Per (locale, style) cache of display-name -> zone name, used by +// +_timeZoneFromString:style:locale: and built lazily on first lookup. +var _nameToZoneCache = {}; + +/*! + Live UTC offset, in minutes, for an IANA zone name at a given date. + DST-aware by construction, since it asks Intl to render the actual wall + clock time in that zone at that instant, rather than reading a static + per-abbreviation number. Returns nil if tzName isn't a zone Intl accepts. + */ +function _offsetMinutesForZone(tzName, date) { - // First, ask Intl directly for the short time zone name (e.g. "PDT") of - // the runtime's local zone, which correctly reflects DST for this date. - // Replaces the previous date.toString() parenthesis-scraping and - // long-name-to-acronym regex guessing, which broke for locales and - // engines that don't format Date#toString the way that logic assumed. - try { - var parts = new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).formatToParts(date), - tzPart = parts.filter(function (p) { return p.type === 'timeZoneName'; })[0]; + if (tzName === @"GMT" || tzName === @"UTC") + return 0; - if (tzPart && [abbreviationDictionary objectForKey:tzPart.value]) - return tzPart.value; - } catch (e) { - // Intl API not supported, or it failed. Fall through to the next attempt. - } + try + { + var dtf = new Intl.DateTimeFormat("en-US", { + timeZone: tzName, + hourCycle: "h23", + year: "numeric", month: "2-digit", day: "2-digit", + hour: "2-digit", minute: "2-digit", second: "2-digit" + }), + parts = dtf.formatToParts(date), + map = {}; - // If that short name isn't one of our known abbreviations (e.g. it - // returned "GMT-04:00" for a zone with no common three/four-letter - // abbreviation), resolve the runtime's IANA zone and pick whichever - // known abbreviation for that zone matches the date's current UTC offset. - try { - var ianaName = new Intl.DateTimeFormat().resolvedOptions().timeZone; - var currentOffset = -date.getTimezoneOffset(); // in minutes + for (var i = 0; i < parts.length; i++) + map[parts[i].type] = parts[i].value; - var keys = [abbreviationDictionary keyEnumerator], - key; + // Date.UTC normalizes out-of-range fields (e.g. an hour of "24"), + // so no special-casing is needed for that formatting quirk. + var asUTC = Date.UTC(map.year, map.month - 1, map.day, map.hour, map.minute, map.second); - // Find all abbreviations for the current IANA time zone. - var possibleAbbrs = []; - while (key = [keys nextObject]) { - if ([abbreviationDictionary valueForKey:key] === ianaName) { - possibleAbbrs.push(key); - } - } - - // If more than one (e.g., standard and daylight), use the current offset to find the right one. - for (var i = 0; i < possibleAbbrs.length; i++) { - var abbr = possibleAbbrs[i]; - if ([timeDifferenceFromUTC valueForKey:abbr] === currentOffset) { - return abbr; - } - } - - // If offset matching fails but we have a unique IANA match, use it as a best guess. - if (possibleAbbrs.length > 0) { - return possibleAbbrs[0]; - } - - // Neither attempt above found a match by name. Fall back to any - // known abbreviation whose stored offset matches the system's - // current UTC offset. Several abbreviations legitimately share an - // offset (GMT, UTC, and WET all correctly resolve to 0, for example), - // so this returns one of them rather than none. - var offsetKeys = [timeDifferenceFromUTC keyEnumerator], - offsetKey; - - while (offsetKey = [offsetKeys nextObject]) { - if ([timeDifferenceFromUTC valueForKey:offsetKey] === currentOffset) { - return offsetKey; - } - } - } catch (e) { - // Intl API not supported, or it failed. - } - - // Return nil if no valid abbreviation could be determined. - return nil; -} - -function _abbreviationForNameAndDate(tzName, date) -{ - // Determines the abbreviation for a given IANA name based on the provided - // date, which allows it to respect daylight saving time. Reads the short - // time zone name directly from Intl.formatToParts, rather than parsing - // a long name out of toLocaleString's locale-formatted output. - try { - var parts = new Intl.DateTimeFormat('en-US', { timeZone: tzName, timeZoneName: 'short' }).formatToParts(date), - tzPart = parts.filter(function (p) { return p.type === 'timeZoneName'; })[0]; - - return tzPart ? tzPart.value : nil; - } catch (e) { - // The tzName might be invalid for Intl.DateTimeFormat, which throws a - // RangeError. In this case, we can't determine the abbreviation. - return nil; - } + return Math.round((asUTC - date.getTime()) / 60000); + } + catch (e) + { + // tzName isn't a zone this engine's Intl implementation accepts. + return nil; + } } /*! - @class CPTimeZone - @ingroup foundation - @brief CPTimeZone is a class to define the behvior of time zone object (like CPDatePicker) -*/ + Short abbreviation (e.g. "PDT") for an IANA zone name at a given date, + read directly from Intl.formatToParts so it reflects DST for that date. + */ +function _abbreviationForNameAndDate(tzName, date) +{ + try + { + var parts = new Intl.DateTimeFormat("en-US", { timeZone: tzName, timeZoneName: "short" }).formatToParts(date), + tzPart = parts.filter(function (p) { return p.type === "timeZoneName"; })[0]; + + return tzPart ? tzPart.value : nil; + } + catch (e) + { + // The tzName might be invalid for Intl.DateTimeFormat, which throws a RangeError. + return nil; + } +} + +/*! + Renders a "GMT+HH:MM" style label for a fixed offset. Used both for + zones created with +timeZoneForSecondsFromGMT: and as a last-resort + abbreviation when Intl can't produce a short name for a real zone. + */ +function _fixedOffsetName(seconds) +{ + var sign = (seconds < 0) ? "-" : "+", + absSeconds = Math.abs(seconds), + hours = Math.floor(absSeconds / 3600), + minutes = Math.floor((absSeconds % 3600) / 60); + + return @"GMT" + sign + (hours < 10 ? "0" : "") + hours + ":" + (minutes < 10 ? "0" : "") + minutes; +} + +/*! + Determines, for a real IANA zone, which of its two yearly offsets is the + standard (non-DST) one and which is the daylight one, by sampling a + January and a July instant and comparing: DST is always the more-advanced + of the two, regardless of hemisphere. Also hands back a UTC instant that + falls in each so callers can ask Intl for the name at that instant. Does + not model historical rule changes mid-year; this is a current-rules + snapshot, same boundary the underlying Intl/tzdata itself has. + */ +function _standardAndDaylightOffsetsForZone(tzName) +{ + var cached = _stdDstOffsetCache[tzName]; + + if (cached) + return cached; + + var year = new Date().getUTCFullYear(), + janDate = new Date(Date.UTC(year, 0, 15, 12)), + julDate = new Date(Date.UTC(year, 6, 15, 12)), + janOffset = _offsetMinutesForZone(tzName, janDate), + julOffset = _offsetMinutesForZone(tzName, julDate); + + if (janOffset === nil || julOffset === nil) + return nil; + + var result = (janOffset <= julOffset) + ? { standard: janOffset, daylight: julOffset, standardSampleDate: janDate, daylightSampleDate: julDate, observesDST: janOffset !== julOffset } + : { standard: julOffset, daylight: janOffset, standardSampleDate: julDate, daylightSampleDate: janDate, observesDST: janOffset !== julOffset }; + + _stdDstOffsetCache[tzName] = result; + + return result; +} + +/*! + Asks Intl for a zone's display name at a given instant, with a fallback + chain for engines that don't support the requested timeZoneName option + (e.g. the newer "shortGeneric"/"longGeneric" values) or the given locale + tag, so a display name is still produced rather than nothing. + */ +function _formatZoneName(tzName, localeCode, date, preferredOption, fallbackOption) +{ + var dtf; + + try + { + dtf = new Intl.DateTimeFormat(localeCode, { timeZone: tzName, timeZoneName: preferredOption }); + } + catch (e) + { + try + { + dtf = new Intl.DateTimeFormat(localeCode, { timeZone: tzName, timeZoneName: fallbackOption }); + } + catch (e2) + { + dtf = new Intl.DateTimeFormat("en", { timeZone: tzName, timeZoneName: fallbackOption }); + } + } + + var parts = dtf.formatToParts(date), + tzPart = parts.filter(function (p) { return p.type === "timeZoneName"; })[0]; + + return tzPart ? tzPart.value : nil; +} + +/*! + Resolves the localized name for a zone at a given style. Standard and + DaylightSaving styles must name that specific offset regardless of what + date is currently in effect, so they're rendered at a sampled instant + known to fall in that offset rather than "now". A fixed-offset zone (see + +timeZoneForSecondsFromGMT:) has no separate long/generic name in any + style; its GMT-offset label is returned for all six. + */ +function _localizedNameForZone(tzName, style, locale, fixedOffsetSecondsOrNil) +{ + if (fixedOffsetSecondsOrNil !== nil && fixedOffsetSecondsOrNil !== undefined) + return _fixedOffsetName(fixedOffsetSecondsOrNil); + + var localeCode = (locale && [locale objectForKey:CPLocaleLanguageCode]) || "en"; + + try + { + switch (style) + { + case CPTimeZoneNameStyleShortGeneric: + return _formatZoneName(tzName, localeCode, [CPDate date], "shortGeneric", "short"); + + case CPTimeZoneNameStyleGeneric: + return _formatZoneName(tzName, localeCode, [CPDate date], "longGeneric", "long"); + + case CPTimeZoneNameStyleShortStandard: + case CPTimeZoneNameStyleShortDaylightSaving: + { + var offsets = _standardAndDaylightOffsetsForZone(tzName); + + if (!offsets) + return nil; + + var wantsDaylight = (style === CPTimeZoneNameStyleShortDaylightSaving) && offsets.observesDST, + refDate = wantsDaylight ? offsets.daylightSampleDate : offsets.standardSampleDate; + + return _formatZoneName(tzName, localeCode, refDate, "short", "short"); + } + + case CPTimeZoneNameStyleStandard: + case CPTimeZoneNameStyleDaylightSaving: + { + var offsets = _standardAndDaylightOffsetsForZone(tzName); + + if (!offsets) + return nil; + + var wantsDaylight = (style === CPTimeZoneNameStyleDaylightSaving) && offsets.observesDST, + refDate = wantsDaylight ? offsets.daylightSampleDate : offsets.standardSampleDate; + + return _formatZoneName(tzName, localeCode, refDate, "long", "long"); + } + } + } + catch (e) + { + return nil; + } + + return nil; +} + +/*! + Resolves the runtime's own current zone as a CPTimeZone: prefer the + IANA name Intl resolves to (DST-correct, works for any of the 400+ + zones in knownTimeZoneNames), and fall back to a fixed-offset zone + built from the JS Date offset if Intl isn't available at all. Always + succeeds. + */ +function _systemTimeZoneFromRuntime() +{ + var date = new Date(); + + try + { + var ianaName = new Intl.DateTimeFormat().resolvedOptions().timeZone; + + if (ianaName && knownTimeZoneNamesSet[ianaName]) + { + var zone = [CPTimeZone timeZoneWithName:ianaName]; + + if (zone) + return zone; + } + } + catch (e) + { + // Intl unsupported, or resolvedOptions().timeZone unavailable. + } + + return [CPTimeZone timeZoneForSecondsFromGMT:-date.getTimezoneOffset() * 60]; +} + +/*! + @class CPTimeZone + @ingroup foundation + @brief CPTimeZone is a class to define the behavior of time zone object (like CPDatePicker) + */ @implementation CPTimeZone : CPObject { - CPData _data @accessors(property=data, readonly); - CPInteger _secondsFromGMT @accessors(property=secondFromGMT, readonly); - CPString _abbreviation @accessors(property=abbreviation, readonly); - CPString _name @accessors(property=name, readonly); + CPData _data @accessors(property=data, readonly); + CPInteger _secondsFromGMT @accessors(property=secondFromGMT, readonly); + CPString _abbreviation @accessors(property=abbreviation, readonly); + CPString _name @accessors(property=name, readonly); + + // Set only for zones created with +timeZoneForSecondsFromGMT:, which per + // Apple's contract never observe daylight saving time. + BOOL _hasFixedOffset; + CPInteger _fixedOffsetSeconds; } /*! Initialize the default value of the class -*/ + */ + (void)initialize { - if (self !== [CPTimeZone class]) - return; + if (self !== [CPTimeZone class]) + return; - knownTimeZoneNames = [ - @"Africa/Addis_Ababa", - @"Africa/Harare", - @"Africa/Lagos", - @"America/Argentina/Buenos_Aires", - @"America/Bogota", - @"America/Chicago", - @"America/Denver", - @"America/Halifax", - @"America/Juneau", - @"America/Lima", - @"America/Los_Angeles", - @"America/New_York", - @"America/Santiago", - @"America/Sao_Paulo", - @"Asia/Bangkok", - @"Asia/Calcutta", - @"Asia/Dhaka", - @"Asia/Dubai", - @"Asia/Hong_Kong", - @"Asia/Jakarta", - @"Asia/Karachi", - @"Asia/Manila", - @"Asia/Seoul", - @"Asia/Singapore", - @"Asia/Tehran", - @"Asia/Tokyo", - @"Europe/Istanbul", - @"Europe/Lisbon", - @"Europe/London", - @"Europe/Moscow", - @"Europe/Paris", - @"GMT", - @"Pacific/Auckland", - @"Pacific/Honolulu", - @"UTC", - ]; + knownTimeZoneNames = [ + @"Africa/Addis_Ababa", + @"Africa/Harare", + @"Africa/Lagos", + @"America/Argentina/Buenos_Aires", + @"America/Bogota", + @"America/Chicago", + @"America/Denver", + @"America/Halifax", + @"America/Juneau", + @"America/Lima", + @"America/Los_Angeles", + @"America/New_York", + @"America/Santiago", + @"America/Sao_Paulo", + @"Asia/Bangkok", + @"Asia/Calcutta", + @"Asia/Dhaka", + @"Asia/Dubai", + @"Asia/Hong_Kong", + @"Asia/Jakarta", + @"Asia/Karachi", + @"Asia/Manila", + @"Asia/Seoul", + @"Asia/Singapore", + @"Asia/Tehran", + @"Asia/Tokyo", + @"Europe/Istanbul", + @"Europe/Lisbon", + @"Europe/London", + @"Europe/Moscow", + @"Europe/Paris", + @"GMT", + @"Pacific/Auckland", + @"Pacific/Honolulu", + @"UTC" + ]; - // Prefer the runtime's own IANA database, when it exposes one, over the - // hardcoded 48-city list above: it's the full current set, not a snapshot - // that will silently drift the way the hand-maintained tables above have. - if (typeof Intl !== "undefined" && typeof Intl.supportedValuesOf === "function") - { - try - { - var supportedZones = Intl.supportedValuesOf("timeZone"); + // Prefer the runtime's own IANA database, when it exposes one, over the + // hardcoded 35-city list above: it's the full current set, not a + // snapshot that will silently drift the way a hand-maintained list does. + if (typeof Intl !== "undefined" && typeof Intl.supportedValuesOf === "function") + { + try + { + var supportedZones = Intl.supportedValuesOf("timeZone"); - if (supportedZones && supportedZones.length > 0) - { - var zones = []; - var hasGMT = false; - var hasUTC = false; - var count = supportedZones.length; + if (supportedZones && supportedZones.length > 0) + { + var zones = []; + var hasGMT = false; + var hasUTC = false; + var count = supportedZones.length; - // Iterate using primitive property access. - // The array returned by Intl across the runtime bridge may lack - // standard Array prototypes (e.g., slice, indexOf). A standard loop - // ensures safe data extraction into a local array without triggering - // prototype resolution exceptions or relying on CPArray. - for (var i = 0; i < count; i++) - { - var zone = supportedZones[i]; - zones[i] = zone; + // Iterate using primitive property access. + // The array returned by Intl across the runtime bridge may lack + // standard Array prototypes (e.g., slice, indexOf). A standard loop + // ensures safe data extraction into a local array without triggering + // prototype resolution exceptions or relying on CPArray. + for (var i = 0; i < count; i++) + { + var zone = supportedZones[i]; + zones[i] = zone; - if (zone === @"GMT") - hasGMT = true; - else if (zone === @"UTC") - hasUTC = true; - } + if (zone === @"GMT") + hasGMT = true; + else if (zone === @"UTC") + hasUTC = true; + } - // Explicitly restore legacy aliases if the host engine omits them. - // Engines adhering strictly to canonical IANA identifiers omit "GMT" - // and "UTC". CPTimeZone's static dictionaries map these directly, - // requiring their presence to initialize localTimeZone in UTC environments. - if (!hasGMT) - zones[zones.length] = @"GMT"; + // Explicitly restore legacy aliases if the host engine omits them. + // Engines adhering strictly to canonical IANA identifiers omit "GMT" + // and "UTC", but code in this class treats both as always-known. + if (!hasGMT) + zones[zones.length] = @"GMT"; - if (!hasUTC) - zones[zones.length] = @"UTC"; + if (!hasUTC) + zones[zones.length] = @"UTC"; - knownTimeZoneNames = zones; - } - } - catch (e) - { - // Fall through, keep the hardcoded list above. - } - } + knownTimeZoneNames = zones; + } + } + catch (e) + { + // Fall through, keep the hardcoded list above. + } + } - abbreviationDictionary = @{ - @"ADT" : @"America/Halifax", - @"AKDT" : @"America/Juneau", - @"AKST" : @"America/Juneau", - @"ART" : @"America/Argentina/Buenos_Aires", - @"AST" : @"America/Halifax", - @"BDT" : @"Asia/Dhaka", - @"BRST" : @"America/Sao_Paulo", - @"BRT" : @"America/Sao_Paulo", - @"BST" : @"Europe/London", - @"CAT" : @"Africa/Harare", - @"CDT" : @"America/Chicago", - @"CEST" : @"Europe/Paris", - @"CET" : @"Europe/Paris", - @"CLST" : @"America/Santiago", - @"CLT" : @"America/Santiago", - @"COT" : @"America/Bogota", - @"CUT" : @"UTC", - @"CST" : @"America/Chicago", - @"EAT" : @"Africa/Addis_Ababa", - @"EDT" : @"America/New_York", - @"EEST" : @"Europe/Istanbul", - @"EET" : @"Europe/Istanbul", - @"EST" : @"America/New_York", - @"GMT" : @"GMT", - @"GST" : @"Asia/Dubai", - @"HKT" : @"Asia/Hong_Kong", - @"HST" : @"Pacific/Honolulu", - @"ICT" : @"Asia/Bangkok", - @"IRST" : @"Asia/Tehran", - @"IST" : @"Asia/Calcutta", - @"JST" : @"Asia/Tokyo", - @"KST" : @"Asia/Seoul", - @"MDT" : @"America/Denver", - @"MSD" : @"Europe/Moscow", - @"MSK" : @"Europe/Moscow", - @"MST" : @"America/Denver", - @"NZDT" : @"Pacific/Auckland", - @"NZST" : @"Pacific/Auckland", - @"PDT" : @"America/Los_Angeles", - @"PET" : @"America/Lima", - @"PHT" : @"Asia/Manila", - @"PKT" : @"Asia/Karachi", - @"PST" : @"America/Los_Angeles", - @"SGT" : @"Asia/Singapore", - @"UTC" : @"UTC", - @"WAT" : @"Africa/Lagos", - @"WEST" : @"Europe/Lisbon", - @"WET" : @"Europe/Lisbon", - @"WIT" : @"Asia/Jakarta" - }; + // O(1) membership testing for -initWithName: and the runtime zone + // resolver, instead of a linear scan over several hundred names. + knownTimeZoneNamesSet = {}; - timeDifferenceFromUTC = @{ - @"ADT" : -180, - @"AKDT" : -480, - @"AKST" : -540, - @"ART" : -180, - @"AST" : -240, - @"BDT" : 360, - @"BRST" : -120, - @"BRT" : -180, - @"BST" : 60, - @"CAT" : 120, - @"CDT" : -300, - @"CEST" : 120, - @"CET" : 60, - @"CLST" : -180, - @"CLT" : -240, - @"COT" : -300, - @"CST" : -360, - @"EAT" : 180, - @"EDT" : -240, - @"EEST" : 180, - @"EET" : 120, - @"EST" : -300, - @"GMT" : 0, - @"GST" : 240, - @"HKT" : 480, - @"HST" : -600, - @"ICT" : 420, - @"IRST" : 210, - @"IST" : 330, - @"JST" : 540, - @"KST" : 540, - @"MDT" : -360, - @"MSD" : 240, // Stale: Russia abolished DST in 2014. No current offset - // is correct for a distinct "Moscow Summer Time"; left - // unfixed rather than fabricated. See CPTimeZone redesign. - @"MSK" : 180, - @"MST" : -420, - @"NZDT" : 780, - @"NZST" : 720, - @"PDT" : -420, - @"PET" : -300, - @"PHT" : 480, - @"PKT" : 300, - @"PST" : -480, - @"SGT" : 480, - @"UTC" : 0, - @"WAT" : 60, - @"WEST" : 60, - @"WET" : 0, - @"WIT" : 420 - }; + for (var i = 0, count = knownTimeZoneNames.length; i < count; i++) + knownTimeZoneNamesSet[knownTimeZoneNames[i]] = true; - var englishLocalizedName = @{ - @"EDT" : [@"Eastern Standard Time", @"EST", @"Eastern Daylight Time", @"EDT", @"Eastern Time", @"ET"], - @"GMT" : [@"GMT", @"GMT", @"GMT", @"GMT", @"GMT", @"GMT"], - @"AST" : [@"Atlantic Standard Time", @"AST", @"Atlantic Daylight Time", @"ADT", @"Atlantic Time", @"AT"], - @"IRST" : [@"Iran Standard Time", @"GMT+03:30", @"Iran Daylight Time", @"GMT+03:30", @"Iran Time", @"Iran Time"], - @"ICT" : [@"Indochina Time", @"GMT+07:00", @"GMT+07:00", @"GMT+07:00", @"Indochina Time", @"Thailand Time"], - @"PET" : [@"Peru Standard Time", @"GMT-05:00", @"Peru Summer Time", @"GMT-05:00", @"Peru Standard Time", @"Peru Time"], - @"KST" : [@"Korean Standard Time", @"GMT+09:00", @"Korean Daylight Time", @"GMT+09:00", @"Korean Standard Time", @"South Korea Time"], - @"PST" : [@"Pacific Standard Time", @"PST", @"Pacific Daylight Time", @"PDT", @"Pacific Time", @"PT"], - @"CDT" : [@"Central Standard Time", @"CST", @"Central Daylight Time", @"CDT", @"Central Time", @"CT"], - @"EEST" : [@"Eastern European Standard Time", @"GMT+02:00", @"Eastern European Summer Time", @"GMT+03:00", @"Eastern European Time", @"Turkey Time"], - @"NZDT" : [@"New Zealand Standard Time", @"GMT+12:00", @"New Zealand Daylight Time", @"GMT+13:00", @"New Zealand Time", @"New Zealand Time (Auckland)"], - @"WEST" : [@"Western European Standard Time", @"GMT", @"Western European Summer Time", @"GMT+01:00", @"Western European Time", @"Portugal Time (Lisbon)"], - @"EAT" : [@"East Africa Time", @"GMT+03:00", @"GMT+03:00", @"GMT+03:00", @"East Africa Time", @"Ethiopia Time"], - @"HKT" : [@"Hong Kong Standard Time", @"GMT+08:00", @"Hong Kong Summer Time", @"GMT+08:00", @"Hong Kong Standard Time", @"Hong Kong SAR China Time"], - @"IST" : [@"India Standard Time", @"GMT+05:30", @"GMT+05:30", @"GMT+05:30", @"India Standard Time", @"India Time"], - @"MDT" : [@"Mountain Standard Time", @"MST", @"Mountain Daylight Time", @"MDT", @"Mountain Time", @"MT"], - @"NZST" : [@"New Zealand Standard Time", @"GMT+12:00", @"New Zealand Daylight Time", @"GMT+13:00", @"New Zealand Time", @"New Zealand Time (Auckland)"], - @"WIT" : [@"Western Indonesia Time", @"GMT+07:00", @"GMT+07:00", @"GMT+07:00", @"Western Indonesia Time", @"Indonesia Time (Jakarta)"], - @"ADT" : [@"Atlantic Standard Time", @"AST", @"Atlantic Daylight Time", @"ADT", @"Atlantic Time", @"AT"], - @"BST" : [@"Greenwich Mean Time", @"GMT", @"British Summer Time", @"GMT+01:00", @"United Kingdom Time", @"United Kingdom Time"], - @"ART" : [@"Argentina Standard Time", @"GMT-03:00", @"Argentina Summer Time", @"GMT-03:00", @"Argentina Standard Time", @"Argentina Time (Buenos Aires)"], - @"CAT" : [@"Central Africa Time", @"GMT+02:00", @"GMT+02:00", @"GMT+02:00", @"Central Africa Time", @"Zimbabwe Time"], - @"GST" : [@"Gulf Standard Time", @"GMT+04:00", @"GMT+04:00", @"GMT+04:00", @"Gulf Standard Time", @"United Arab Emirates Time"], - @"PDT" : [@"Pacific Standard Time", @"PST", @"Pacific Daylight Time", @"PDT", @"Pacific Time", @"PT"], - @"SGT" : [@"Singapore Standard Time", @"GMT+08:00", @"GMT+08:00", @"GMT+08:00", @"Singapore Standard Time", @"Singapore Time"], - @"COT" : [@"Colombia Standard Time", @"GMT-05:00", @"Colombia Summer Time", @"GMT-05:00", @"Colombia Standard Time", @"Colombia Time"], - @"PKT" : [@"Pakistan Standard Time", @"GMT+05:00", @"Pakistan Summer Time", @"GMT+05:00", @"Pakistan Standard Time", @"Pakistan Time"], - @"EET" : [@"Eastern European Standard Time", @"GMT+02:00", @"Eastern European Summer Time", @"GMT+03:00", @"Eastern European Time", @"Turkey Time"], - @"UTC" : [@"GMT", @"GMT", @"GMT", @"GMT", @"GMT", @"GMT"], - @"WAT" : [@"West Africa Standard Time", @"GMT+01:00", @"West Africa Summer Time", @"GMT+01:00", @"West Africa Standard Time", @"Nigeria Time"], - @"EST" : [@"Eastern Standard Time", @"EST", @"Eastern Daylight Time", @"EDT", @"Eastern Time", @"ET"], - @"JST" : [@"Japan Standard Time", @"GMT+09:00", @"Japan Daylight Time", @"GMT+09:00", @"Japan Standard Time", @"Japan Time"], - @"CLST" : [@"Chile Standard Time", @"GMT-04:00", @"Chile Summer Time", @"GMT-04:00", @"Chile Time", @"Chile Time (Santiago)"], - @"CET" : [@"Central European Standard Time", @"GMT+01:00", @"Central European Summer Time", @"GMT+02:00", @"Central European Time", @"France Time"], - @"BDT" : [@"Bangladesh Standard Time", @"GMT+06:00", @"Bangladesh Summer Time", @"GMT+06:00", @"Bangladesh Standard Time", @"Bangladesh Time"], - @"MSK" : [@"Moscow Standard Time", @"GMT+04:00", @"Moscow Summer Time", @"GMT+04:00", @"Moscow Standard Time", @"Russia Time (Moscow)"], - @"AKDT" : [@"Alaska Standard Time", @"AKST", @"Alaska Daylight Time", @"AKDT", @"Alaska Time", @"AKT"], - @"CLT" : [@"Chile Standard Time", @"GMT-04:00", @"Chile Summer Time", @"GMT-04:00", @"Chile Time", @"Chile Time (Santiago)"], - @"AKST" : [@"Alaska Standard Time", @"AKST", @"Alaska Daylight Time", @"AKDT", @"Alaska Time", @"AKT"], - @"BRST" : [@"Brasilia Standard Time", @"GMT-03:00", @"Brasilia Summer Time", @"GMT-03:00", @"Brasilia Time", @"Brazil Time (Sao Paulo)"], - @"BRT" : [@"Brasilia Standard Time", @"GMT-03:00", @"Brasilia Summer Time", @"GMT-03:00", @"Brasilia Time", @"Brazil Time (Sao Paulo)"], - @"CEST" : [@"Central European Standard Time", @"GMT+01:00", @"Central European Summer Time", @"GMT+02:00", @"Central European Time", @"France Time"], - @"CST" : [@"Central Standard Time", @"CST", @"Central Daylight Time", @"CDT", @"Central Time", @"CT"], - @"HST" : [@"Hawaii-Aleutian Standard Time", @"HST", @"Hawaii-Aleutian Daylight Time", @"HDT", @"Hawaii-Aleutian Standard Time", @"HST"], - @"MSD" : [@"Moscow Standard Time", @"GMT+04:00", @"Moscow Summer Time", @"GMT+04:00", @"Moscow Standard Time", @"Russia Time (Moscow)"], - @"MST" : [@"Mountain Standard Time", @"MST", @"Mountain Daylight Time", @"MDT", @"Mountain Time", @"MT"], - @"PHT" : [@"Philippine Standard Time", @"GMT+08:00", @"Philippine Summer Time", @"GMT+08:00", @"Philippine Standard Time", @"Philippines Time"], - @"WET" : [@"Western European Standard Time", @"GMT", @"Western European Summer Time", @"GMT+01:00", @"Western European Time", @"Portugal Time (Lisbon)"] - }; + // A curated abbreviation -> canonical name lookup for +timeZoneWithAbbreviation:. + // This stays a fixed, hand-maintained set deliberately: abbreviations are + // not unique across regions (CST, IST, EST each name more than one real + // zone), so this table is a documented best-effort convenience, not a + // source of truth for offsets or names the way it was before. + abbreviationDictionary = @{ + @"ADT" : @"America/Halifax", + @"AKDT" : @"America/Juneau", + @"AKST" : @"America/Juneau", + @"ART" : @"America/Argentina/Buenos_Aires", + @"AST" : @"America/Halifax", + @"BDT" : @"Asia/Dhaka", + @"BRST" : @"America/Sao_Paulo", + @"BRT" : @"America/Sao_Paulo", + @"BST" : @"Europe/London", + @"CAT" : @"Africa/Harare", + @"CDT" : @"America/Chicago", + @"CEST" : @"Europe/Paris", + @"CET" : @"Europe/Paris", + @"CLST" : @"America/Santiago", + @"CLT" : @"America/Santiago", + @"COT" : @"America/Bogota", + @"CUT" : @"UTC", + @"CST" : @"America/Chicago", + @"EAT" : @"Africa/Addis_Ababa", + @"EDT" : @"America/New_York", + @"EEST" : @"Europe/Istanbul", + @"EET" : @"Europe/Istanbul", + @"EST" : @"America/New_York", + @"GMT" : @"GMT", + @"GST" : @"Asia/Dubai", + @"HKT" : @"Asia/Hong_Kong", + @"HST" : @"Pacific/Honolulu", + @"ICT" : @"Asia/Bangkok", + @"IRST" : @"Asia/Tehran", + @"IST" : @"Asia/Calcutta", + @"JST" : @"Asia/Tokyo", + @"KST" : @"Asia/Seoul", + @"MDT" : @"America/Denver", + @"MSD" : @"Europe/Moscow", + @"MSK" : @"Europe/Moscow", + @"MST" : @"America/Denver", + @"NZDT" : @"Pacific/Auckland", + @"NZST" : @"Pacific/Auckland", + @"PDT" : @"America/Los_Angeles", + @"PET" : @"America/Lima", + @"PHT" : @"Asia/Manila", + @"PKT" : @"Asia/Karachi", + @"PST" : @"America/Los_Angeles", + @"SGT" : @"Asia/Singapore", + @"UTC" : @"UTC", + @"WAT" : @"Africa/Lagos", + @"WEST" : @"Europe/Lisbon", + @"WET" : @"Europe/Lisbon", + @"WIT" : @"Asia/Jakarta" + }; - var date = [CPDate date], - abbreviation = abbreviationForDate(date); + timeZoneDataVersion = nil; - localTimeZone = [self timeZoneWithAbbreviation:abbreviation]; - systemTimeZone = localTimeZone; - defaultTimeZone = localTimeZone; - - localizedName = @{ - @"en" : englishLocalizedName, - @"fr" : @{}, - @"de" : @{}, - @"es" : @{} - }; - - timeZoneDataVersion = nil; + localTimeZone = _systemTimeZoneFromRuntime(); + systemTimeZone = localTimeZone; + defaultTimeZone = localTimeZone; } @@ -420,332 +484,403 @@ function _abbreviationForNameAndDate(tzName, date) // MARK: Class constructor /*! Returns a time zone from the given abbreviation. - Returns nil if the given abbreviation doesn't match with any abbreviations - @param abbreviation the given abreviation - @return a new instance of CPTimeZone -*/ + Returns nil if the given abbreviation doesn't match with any abbreviations + @param abbreviation the given abreviation + @return a new instance of CPTimeZone + */ + (id)timeZoneWithAbbreviation:(CPString)abbreviation { - if (![abbreviationDictionary containsKey:abbreviation]) - return nil; + if (![abbreviationDictionary containsKey:abbreviation]) + return nil; - return [[CPTimeZone alloc] _initWithName:[abbreviationDictionary valueForKey:abbreviation] abbreviation:abbreviation]; + return [[CPTimeZone alloc] _initWithName:[abbreviationDictionary valueForKey:abbreviation] abbreviation:abbreviation]; } /*! Return a time zone from the given timeZone name - Returns nil if the given timeZone name doesn't match with any abbreviations - Raises an exception if tzName is nil - @param tzName the timeZone name - @return a new instance of CPTimeZone -*/ + Returns nil if the given timeZone name doesn't match with any abbreviations + Raises an exception if tzName is nil + @param tzName the timeZone name + @return a new instance of CPTimeZone + */ + (id)timeZoneWithName:(CPString)tzName { - return [[CPTimeZone alloc] initWithName:tzName]; + return [[CPTimeZone alloc] initWithName:tzName]; } /*! Return a time zone from the given timeZone name and data - Returns nil if the given timeZone name doesn't match with any abbreviations - Raises an exception if tzName is nil - @param tzName the timeZone name - @param data the data - @return a new instance of CPTimeZone -*/ + Returns nil if the given timeZone name doesn't match with any abbreviations + Raises an exception if tzName is nil + @param tzName the timeZone name + @param data the data + @return a new instance of CPTimeZone + */ + (id)timeZoneWithName:(CPString)tzName data:(CPData)data { - return [[CPTimeZone alloc] initWithName:tzName data:data]; + return [[CPTimeZone alloc] initWithName:tzName data:data]; } -/*! Return a time zone from the given seconds - Returns nil if the number of seconds doesn't match with any offset - @param seconds the number of seconds - @return a new instance of CPTimeZone -*/ +/*! Return a fixed-offset time zone for the given number of seconds from GMT. + Per Apple's documented contract for this constructor, the returned zone + never observes daylight saving time. Offsets are rounded to the nearest + minute; offsets more than +/- 18 hours are disallowed and return nil. + @param seconds the number of seconds + @return a new instance of CPTimeZone + */ + (id)timeZoneForSecondsFromGMT:(CPInteger)seconds { - var minutes = seconds / 60, - keys = [timeDifferenceFromUTC keyEnumerator], - key, - abbreviation = nil; + if (Math.abs(seconds) > 18 * 3600) + return nil; - while (key = [keys nextObject]) - { - var value = [timeDifferenceFromUTC valueForKey:key]; + var roundedSeconds = Math.round(seconds / 60) * 60; - if (value == minutes) - { - abbreviation = key; - break; - } - } - - if (!abbreviation) - return nil; - - return [self timeZoneWithAbbreviation:abbreviation]; + return [[CPTimeZone alloc] _initWithFixedOffsetSeconds:roundedSeconds]; } /*! @ignore -*/ + */ + (id)_timeZoneFromString:(CPString)aTimeZoneString style:(NSTimeZoneNameStyle)style locale:(CPLocale)_locale { - if ([abbreviationDictionary containsKey:aTimeZoneString]) - return [self timeZoneWithAbbreviation:aTimeZoneString]; + if ([abbreviationDictionary containsKey:aTimeZoneString]) + return [self timeZoneWithAbbreviation:aTimeZoneString]; - var dict = [localizedName valueForKey:[_locale objectForKey:CPLocaleLanguageCode]], - keys = [dict keyEnumerator], - key; + var localeCode = (_locale && [_locale objectForKey:CPLocaleLanguageCode]) || "en", + cacheKey = localeCode + "|" + style, + map = _nameToZoneCache[cacheKey]; - while (key = [keys nextObject]) - { - var value = [[dict valueForKey:key] objectAtIndex:style]; + if (!map) + { + map = {}; - if ([value isEqualToString:aTimeZoneString]) - return [self timeZoneWithAbbreviation:key]; - } + for (var i = 0, count = knownTimeZoneNames.length; i < count; i++) + { + var tzName = knownTimeZoneNames[i], + displayName = _localizedNameForZone(tzName, style, _locale, nil); - return nil; + // First zone found for a given display name wins; a handful of + // zones legitimately share a display name (e.g. generic-style + // "GMT+02:00" style fallbacks), and any one of them is as valid + // a match as another for parsing purposes. + if (displayName && !(displayName in map)) + map[displayName] = tzName; + } + + _nameToZoneCache[cacheKey] = map; + } + + var matchedName = map[aTimeZoneString]; + + return matchedName ? [self timeZoneWithName:matchedName] : nil; } /*! @ignore -*/ + */ + (CPArray)_namesForStyle:(NSTimeZoneNameStyle)style locale:(CPLocale)aLocale { - var array = [CPArray array], - dict = [localizedName valueForKey:[aLocale objectForKey:CPLocaleLanguageCode]], - keys = [dict keyEnumerator], - key; + var array = [CPArray array]; - while (key = [keys nextObject]) - [array addObject:[[dict valueForKey:key] objectAtIndex:style]]; + for (var i = 0, count = knownTimeZoneNames.length; i < count; i++) + { + var displayName = _localizedNameForZone(knownTimeZoneNames[i], style, aLocale, nil); - return array; + if (displayName) + [array addObject:displayName]; + } + + return array; } // MARK: - // MARK: Class accessors /*! Return the timeZoneDataVersion (not yet implemented) -*/ + */ + (CPString)timeZoneDataVersion { - // TODO : don't know what to do ^^ - return timeZoneDataVersion; + return timeZoneDataVersion; } /*! Return the localTimeZone -*/ + */ + (CPTimeZone)localTimeZone { - return localTimeZone; + return localTimeZone; } /*! Return the defaultTimeZone -*/ + */ + (CPTimeZone)defaultTimeZone { - return defaultTimeZone; + return defaultTimeZone; } /*! Set the defaultTimeZone - @param aTimeZone the defaultTimeZone -*/ + @param aTimeZone the defaultTimeZone + */ + (void)setDefaultTimeZone:(CPTimeZone)aTimeZone { - defaultTimeZone = aTimeZone; + defaultTimeZone = aTimeZone; } /*! Reset the systemTimeZone - This will send the notification CPSystemTimeZoneDidChangeNotification -*/ + This will send the notification CPSystemTimeZoneDidChangeNotification + */ + (void)resetSystemTimeZone { - var date = [CPDate date], - abbreviation = abbreviationForDate(date); + systemTimeZone = _systemTimeZoneFromRuntime(); - systemTimeZone = [self timeZoneWithAbbreviation:abbreviation]; - - [[CPNotificationCenter defaultCenter] postNotificationName:CPSystemTimeZoneDidChangeNotification object:systemTimeZone]; + [[CPNotificationCenter defaultCenter] postNotificationName:CPSystemTimeZoneDidChangeNotification object:systemTimeZone]; } /*! Return the systemTimeZone -*/ + */ + (CPTimeZone)systemTimeZone { - return systemTimeZone; + return systemTimeZone; } /*! Return the abbreviationDictionary -*/ + */ + (CPDictionary)abbreviationDictionary { - return abbreviationDictionary; + return abbreviationDictionary; } /*! Set the abbreviationDictionary - @param dict -*/ + @param dict + */ + (void)setAbbreviationDictionary:(CPDictionary)dict { - abbreviationDictionary = dict; + abbreviationDictionary = dict; } /*! Return the knownTimeZoneNames -*/ + */ + (CPArray)knownTimeZoneNames { - return knownTimeZoneNames; + return knownTimeZoneNames; } // MARK: - -// MARK: Consructors +// MARK: Constructors /*! Init a new time zone with the given time zone name and abbreviation - Returns nil if tzName doesn't match with any timeZoneNames or if abbreviation is nil - Raises an exception if tzName is nil - @param tzName the timeZone name - @param abbreviation the abbreviation - @return a new timeZone -*/ + Returns nil if tzName doesn't match with any timeZoneNames or if abbreviation is nil + Raises an exception if tzName is nil + @param tzName the timeZone name + @param abbreviation the abbreviation + @return a new timeZone + */ - (id)_initWithName:(CPString)tzName abbreviation:(CPString)abbreviation { - if (!tzName) - [CPException raise:CPInvalidArgumentException reason:"Invalid value provided for tzName"]; + if (!tzName) + [CPException raise:CPInvalidArgumentException reason:"Invalid value provided for tzName"]; - if (![knownTimeZoneNames containsObject:tzName] || !abbreviation) - return nil; + if (!knownTimeZoneNamesSet[tzName] || !abbreviation) + return nil; - if (self = [super init]) - { - _name = tzName; - _abbreviation = abbreviation; - } + if (self = [super init]) + { + _name = tzName; + _abbreviation = abbreviation; + } - return self; + return self; +} + +/*! Init a fixed-offset time zone (see +timeZoneForSecondsFromGMT:). + @param seconds the fixed offset, already rounded to the nearest minute + */ +- (id)_initWithFixedOffsetSeconds:(CPInteger)seconds +{ + if (self = [super init]) + { + _name = _fixedOffsetName(seconds); + _abbreviation = _name; + _hasFixedOffset = YES; + _fixedOffsetSeconds = seconds; + } + + return self; } /*! Init a new time zone from the given timeZone name - Returns nil if the given timeZone name doesn't match with any abbreviations - Raises an exception if tzName is nil - @param tzName the timeZone name - @return a new instance of CPTimeZone -*/ + Returns nil if the given timeZone name isn't a known IANA zone name + Raises an exception if tzName is nil + @param tzName the timeZone name + @return a new instance of CPTimeZone + */ - (id)initWithName:(CPString)tzName { - if (!tzName) - [CPException raise:CPInvalidArgumentException reason:"Invalid value provided for tzName"]; + if (!tzName) + [CPException raise:CPInvalidArgumentException reason:"Invalid value provided for tzName"]; - if (![knownTimeZoneNames containsObject:tzName]) - return nil; + if (!knownTimeZoneNamesSet[tzName]) + return nil; - if (self = [super init]) - { - _name = tzName; + if (self = [super init]) + { + _name = tzName; - // Determine the abbreviation based on the current date to handle DST. - var currentAbbreviation = _abbreviationForNameAndDate(tzName, [CPDate date]); + // Determine the abbreviation based on the current date, so it + // reflects DST if this zone is presently observing it. + var now = [CPDate date], + currentAbbreviation = _abbreviationForNameAndDate(tzName, now); - // If we got a valid abbreviation from the date, and it's one we know about, use it. - // Otherwise, fall back to the old logic. - if (currentAbbreviation && [abbreviationDictionary containsKey:currentAbbreviation]) - { - _abbreviation = currentAbbreviation; - } - else - { - // FALLBACK: Find the first matching abbreviation in the dictionary. - // Note: This is not DST-aware and may not be correct, but it preserves - // the original behavior for cases where the dynamic lookup fails. - var keys = [abbreviationDictionary keyEnumerator], - key; + if (currentAbbreviation) + { + _abbreviation = currentAbbreviation; + } + else + { + // Intl couldn't produce a short name for this zone on this + // engine (rare). Synthesize a GMT-offset label from the live + // offset instead of failing construction for an otherwise + // valid, known zone name. + var offsetMinutes = _offsetMinutesForZone(tzName, now); - while (key = [keys nextObject]) - { - var value = [abbreviationDictionary valueForKey:key]; + if (offsetMinutes === nil) + return nil; - if ([value isEqualToString:_name]) - { - _abbreviation = key; - break; - } - } - } + _abbreviation = _fixedOffsetName(offsetMinutes * 60); + } + } - // If no abbreviation could be found by any means, initialization fails. - if (!_abbreviation) - return nil; - } - - return self; + return self; } /*! Return a time zone from the given timeZone name and data - Returns nil if the given timeZone name doesn't match with any abbreviations - Raises an exception if tzName is nil - @param tzName the timeZone name - @param data the data - @return a new instance of CPTimeZone -*/ + Returns nil if the given timeZone name doesn't match with any abbreviations + Raises an exception if tzName is nil + @param tzName the timeZone name + @param data the data + @return a new instance of CPTimeZone + */ - (id)initWithName:(CPString)tzName data:(CPData)data { - if (self = [self initWithName:tzName]) - { - _data = data; - } + if (self = [self initWithName:tzName]) + { + _data = data; + } - return self; + return self; } // MARK: - // MARK: Methods for CPDate -/*! Returns the abbreviation from a date - Returns nil if the date is nil - @return the abbreviation -*/ +/*! Returns this time zone's abbreviation at the given date, reflecting DST + if this zone observes it and the date falls in it. + Returns nil if the date is nil + @return the abbreviation + */ - (CPString)abbreviationForDate:(CPDate)date { - if (!date) - return nil; + if (!date) + return nil; - return abbreviationForDate(date); + if (_hasFixedOffset) + return _abbreviation; + + return _abbreviationForNameAndDate(_name, date) || _abbreviation; } -/*! Returns the number of seconds from GMT for the given date - Returns nil if the date is nil - @param date - @return the number of seconds -*/ +/*! Returns the number of seconds this time zone differs from GMT at the + given date. DST-aware: the value varies across the year for zones that + observe it. A fixed-offset zone (see +timeZoneForSecondsFromGMT:) always + returns the same value regardless of date. + Returns nil if the date is nil + @param date + @return the number of seconds + */ - (CPInteger)secondsFromGMTForDate:(CPDate)date { - if (!date) - return nil; + if (!date) + return nil; - var abbreviation = abbreviationForDate(date); + if (_hasFixedOffset) + return _fixedOffsetSeconds; - return [timeDifferenceFromUTC valueForKey:abbreviation] * 60; + var offsetMinutes = _offsetMinutesForZone(_name, date); + + return (offsetMinutes === nil) ? nil : offsetMinutes * 60; } -/*! Returns the number of seconds from GMT - @return the number of seconds -*/ +/*! Returns the number of seconds this time zone differs from GMT right now. + @return the number of seconds + */ - (CPInteger)secondsFromGMT { - return [timeDifferenceFromUTC valueForKey:_abbreviation] * 60; + return [self secondsFromGMTForDate:[CPDate date]]; +} + +/*! Returns whether this time zone is currently observing daylight saving time. + Always NO for a fixed-offset zone (see +timeZoneForSecondsFromGMT:). + @return a bool + */ +- (BOOL)isDaylightSavingTime +{ + return [self isDaylightSavingTimeForDate:[CPDate date]]; +} + +/*! Returns whether this time zone is observing daylight saving time at the given date. + Always NO for a fixed-offset zone (see +timeZoneForSecondsFromGMT:). + @param date + @return a bool + */ +- (BOOL)isDaylightSavingTimeForDate:(CPDate)date +{ + if (_hasFixedOffset || !date) + return NO; + + var offsets = _standardAndDaylightOffsetsForZone(_name); + + if (!offsets || !offsets.observesDST) + return NO; + + var current = _offsetMinutesForZone(_name, date); + + return current !== nil && current > offsets.standard; +} + +/*! Returns the daylight saving time offset, in seconds, this time zone is + currently applying on top of its standard offset. 0 if not presently + observing daylight saving time. + @return the number of seconds + */ +- (CPTimeInterval)daylightSavingTimeOffset +{ + return [self daylightSavingTimeOffsetForDate:[CPDate date]]; +} + +/*! Returns the daylight saving time offset, in seconds, this time zone + applies on top of its standard offset at the given date. 0 if the date + doesn't fall within daylight saving time for this zone. + @param date + @return the number of seconds + */ +- (CPTimeInterval)daylightSavingTimeOffsetForDate:(CPDate)date +{ + if (![self isDaylightSavingTimeForDate:date]) + return 0; + + var offsets = _standardAndDaylightOffsetsForZone(_name); + + return (offsets.daylight - offsets.standard) * 60; } // MARK: - -// MARK: Compars methods +// MARK: Compare methods -/*! Returns a bool to compare tow timeZones. - This is made by the compare of the name and the data of the timeZones - @return a bool -*/ +/*! Returns a bool to compare two timeZones. + This is made by comparing the name and the data of the timeZones + @return a bool + */ - (BOOL)isEqualToTimeZone:(CPTimeZone)aTimeZone { - return [[aTimeZone name] isEqualToString:_name] && [aTimeZone data] == _data + return [[aTimeZone name] isEqualToString:_name] && [aTimeZone data] == _data; } @@ -753,12 +888,12 @@ function _abbreviationForNameAndDate(tzName, date) // MARK: Description /*! Returns the description of the timeZone - The pattern of the description is : 'name of the timeZone' ('abbreviation of the timeZone') offset 'the timeDifferenceFromGMT' - @return the description -*/ + The pattern of the description is : 'name of the timeZone' ('abbreviation of the timeZone') offset 'the timeDifferenceFromGMT' + @return the description + */ - (CPString)description { - return [CPString stringWithFormat:@"%s (%s) offset %i", _name, _abbreviation, [self secondsFromGMT]]; + return [CPString stringWithFormat:@"%s (%s) offset %i", _name, _abbreviation, [self secondsFromGMT]]; } @@ -766,16 +901,16 @@ function _abbreviationForNameAndDate(tzName, date) // MARK: Localized methods /*! Return a localized string from the given style and locale - @param style the style - @param locale the locale - @return a string -*/ + @param style the style + @param locale the locale + @return a string + */ - (CPString)localizedName:(NSTimeZoneNameStyle)style locale:(CPLocale)locale { - if (style > 5) - return nil; + if (style < 0 || style > 5) + return nil; - return [[[localizedName valueForKey:[locale objectForKey:CPLocaleLanguageCode]] valueForKey:_abbreviation] objectAtIndex:style]; + return _localizedNameForZone(_name, style, locale, _hasFixedOffset ? _fixedOffsetSeconds : nil); } @end diff --git a/Tests/Foundation/CPTimeZoneTest.j b/Tests/Foundation/CPTimeZoneTest.j index e6568da47..04dbfac33 100644 --- a/Tests/Foundation/CPTimeZoneTest.j +++ b/Tests/Foundation/CPTimeZoneTest.j @@ -1,23 +1,23 @@ /* CPTimeZoneTest.j -* Foundation -* -* Created by Alexandre Wilhelm -* Copyright 2012 -* -* 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 -*/ + * Foundation + * + * Created by Alexandre Wilhelm + * Copyright 2012 + * + * 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 @import @@ -66,7 +66,7 @@ { var timeZone = [CPTimeZone timeZoneWithName:@"America/Los_Angeles"]; - // The current implementation can return daylight saving time or standard time depending on the current implmentation on CPDictionary + // The current implementation can return daylight saving time or standard time depending on the current date if ([timeZone abbreviation] === @"PDT") { [self assert:[timeZone name] equals:@"America/Los_Angeles"]; [self assert:[timeZone abbreviation] equals:@"PDT"]; @@ -96,8 +96,8 @@ { try { - [CPTimeZone timeZoneWithName:nil]; - [self fail:"Invalid value provided for tzName"]; + [CPTimeZone timeZoneWithName:nil]; + [self fail:"Invalid value provided for tzName"]; } catch (e) { @@ -109,7 +109,6 @@ { var timeZone = [CPTimeZone timeZoneWithName:@"America/Los_Angeles" data:_data]; - // The current implementation can return daylight saving time or standard time depending on the current implmentation on CPDictionary if ([timeZone abbreviation] === @"PDT") { [self assert:[timeZone name] equals:@"America/Los_Angeles"]; [self assert:[timeZone abbreviation] equals:@"PDT"]; @@ -140,8 +139,8 @@ { try { - [CPTimeZone timeZoneWithName:nil data:_data]; - [self fail:"Invalid value provided for tzName"]; + [CPTimeZone timeZoneWithName:nil data:_data]; + [self fail:"Invalid value provided for tzName"]; } catch (e) { @@ -151,30 +150,37 @@ - (void)testTimeZoneWithSecondsFromGMT { + // timeZoneForSecondsFromGMT returns a fixed-offset zone. It does not + // back-resolve to geographical regions (like Pacific/Honolulu). var timeZone = [CPTimeZone timeZoneForSecondsFromGMT:(-600 * 60)]; - [self assert:[timeZone name] equals:@"Pacific/Honolulu"]; - [self assert:[timeZone abbreviation] equals:@"HST"]; + [self assert:[timeZone name] equals:@"GMT-10:00"]; + [self assert:[timeZone abbreviation] equals:@"GMT-10:00"]; [self assert:[timeZone secondsFromGMT] equals:(-600 * 60)]; - [self assert:[timeZone description] equals:@"Pacific/Honolulu (HST) offset -36000"]; - [self assert:[timeZone localizedName:CPTimeZoneNameStyleStandard locale:_locale] equals:@"Hawaii-Aleutian Standard Time"]; - [self assert:[timeZone localizedName:CPTimeZoneNameStyleShortStandard locale:_locale] equals:@"HST"]; - [self assert:[timeZone localizedName:CPTimeZoneNameStyleDaylightSaving locale:_locale] equals:@"Hawaii-Aleutian Daylight Time"]; - [self assert:[timeZone localizedName:CPTimeZoneNameStyleShortDaylightSaving locale:_locale] equals:@"HDT"]; - [self assert:[timeZone localizedName:CPTimeZoneNameStyleGeneric locale:_locale] equals:@"Hawaii-Aleutian Standard Time"]; - [self assert:[timeZone localizedName:CPTimeZoneNameStyleShortGeneric locale:_locale] equals:@"HST"]; + [self assert:[timeZone description] equals:@"GMT-10:00 (GMT-10:00) offset -36000"]; + [self assert:[timeZone localizedName:CPTimeZoneNameStyleStandard locale:_locale] equals:@"GMT-10:00"]; + [self assert:[timeZone localizedName:CPTimeZoneNameStyleShortStandard locale:_locale] equals:@"GMT-10:00"]; + [self assert:[timeZone localizedName:CPTimeZoneNameStyleDaylightSaving locale:_locale] equals:@"GMT-10:00"]; + [self assert:[timeZone localizedName:CPTimeZoneNameStyleShortDaylightSaving locale:_locale] equals:@"GMT-10:00"]; + [self assert:[timeZone localizedName:CPTimeZoneNameStyleGeneric locale:_locale] equals:@"GMT-10:00"]; + [self assert:[timeZone localizedName:CPTimeZoneNameStyleShortGeneric locale:_locale] equals:@"GMT-10:00"]; } - (void)testTimeZoneWithWrongSecondsFromGMT { + // +/- 18 hours is the limit. -421 * 60 is valid and creates a fixed zone. var timeZone = [CPTimeZone timeZoneForSecondsFromGMT:(-421 * 60)]; - [self assert:timeZone equals:nil]; + [self assert:[timeZone name] equals:@"GMT-07:01"]; + [self assert:[timeZone secondsFromGMT] equals:(-421 * 60)]; + + // An offset strictly outside the +/- 18 hour range should return nil. + var invalidTimeZone = [CPTimeZone timeZoneForSecondsFromGMT:(19 * 3600)]; + [self assert:invalidTimeZone equals:nil]; } - (void)testInitTimeZoneWithName { var timeZone = [[CPTimeZone alloc] initWithName:@"America/Los_Angeles"]; - // The current implementation can return daylight saving time or standard time depending on the current implmentation on CPDictionary if ([timeZone abbreviation] === @"PDT") { [self assert:[timeZone name] equals:@"America/Los_Angeles"]; [self assert:[timeZone abbreviation] equals:@"PDT"]; @@ -204,8 +210,8 @@ { try { - [[CPTimeZone alloc] initWithName:nil]; - [self fail:"Invalid value provided for tzName"]; + [[CPTimeZone alloc] initWithName:nil]; + [self fail:"Invalid value provided for tzName"]; } catch (e) { @@ -217,7 +223,6 @@ { var timeZone = [[CPTimeZone alloc] initWithName:@"America/Los_Angeles" data:_data]; - // The current implementation can return daylight saving time or standard time depending on the current implmentation on CPDictionary if ([timeZone abbreviation] === @"PDT") { [self assert:[timeZone name] equals:@"America/Los_Angeles"]; [self assert:[timeZone abbreviation] equals:@"PDT"]; @@ -248,8 +253,8 @@ { try { - [[CPTimeZone alloc] initWithName:nil data:_data]; - [self fail:"Invalid value provided for tzName"]; + [[CPTimeZone alloc] initWithName:nil data:_data]; + [self fail:"Invalid value provided for tzName"]; } catch (e) { @@ -260,12 +265,8 @@ - (void)testAbbreviationWithDate { var timeZone = [CPTimeZone localTimeZone], - abbreviation = [timeZone abbreviationForDate:_date]; + abbreviation = [timeZone abbreviationForDate:_date]; - // With the new robust implementation, we can't easily predict the exact - // abbreviation string without re-implementing the parsing logic. - // Instead, we verify that the returned abbreviation is valid and exists - // in the known dictionary, which is sufficient. [self assert:(abbreviation !== nil) equals:YES]; var knownAbbreviations = [CPTimeZone abbreviationDictionary]; [self assert:[knownAbbreviations containsKey:abbreviation] equals:YES]; @@ -274,7 +275,7 @@ - (void)testAbbreviationWithNilDate { var timeZone = [CPTimeZone localTimeZone], - abbreviation = [timeZone abbreviationForDate:nil]; + abbreviation = [timeZone abbreviationForDate:nil]; [self assert:abbreviation equals:nil]; } @@ -283,7 +284,7 @@ - (void)testSecondsFromGMTForDate { var timeZone = [CPTimeZone localTimeZone], - seconds = [timeZone secondsFromGMTForDate:_date]; + seconds = [timeZone secondsFromGMTForDate:_date]; [self assert:seconds equals:(_date.getTimezoneOffset() * -60)]; } @@ -291,7 +292,7 @@ - (void)testSecondsFromGMTForDateWithNilDate { var timeZone = [CPTimeZone localTimeZone], - seconds = [timeZone secondsFromGMTForDate:nil]; + seconds = [timeZone secondsFromGMTForDate:nil]; [self assert:seconds equals:nil]; } @@ -300,7 +301,6 @@ { var timeZone = [[CPTimeZone alloc] initWithName:@"America/Los_Angeles"]; - // The current implementation can return daylight saving time or standard time depending on the current implmentation on CPDictionary if ([timeZone abbreviation] === @"PDT") { [self assert:[timeZone secondsFromGMT] equals:(-420 * 60)]; } else { @@ -312,7 +312,6 @@ { var timeZone = [[CPTimeZone alloc] initWithName:@"America/Los_Angeles"]; - // The current implementation can return daylight saving time or standard time depending on the current implmentation on CPDictionary if ([timeZone abbreviation] === @"PDT") { [self assert:[timeZone description] equals:@"America/Los_Angeles (PDT) offset -25200"]; } else { @@ -334,7 +333,7 @@ - (void)testEqualTrueToTimeZone { var timeZone1 = [[CPTimeZone alloc] initWithName:@"America/Los_Angeles"], - timeZone2 = [[CPTimeZone alloc] initWithName:@"America/Los_Angeles"]; + timeZone2 = [[CPTimeZone alloc] initWithName:@"America/Los_Angeles"]; [self assert:[timeZone1 isEqualToTimeZone:timeZone2] equals:YES]; } @@ -342,25 +341,17 @@ - (void)testEqualFalseToTimeZone { var timeZone1 = [[CPTimeZone alloc] initWithName:@"America/Los_Angeles"], - timeZone2 = [[CPTimeZone alloc] initWithName:@"Pacific/Honolulu"]; + timeZone2 = [[CPTimeZone alloc] initWithName:@"Pacific/Honolulu"]; [self assert:[timeZone1 isEqualToTimeZone:timeZone2] equals:NO]; } - (void)testInitWithNameRespectsDaylightSaving { - // This test verifies that initWithName: correctly selects the abbreviation - // (e.g., PST vs. PDT) based on the current date's daylight saving status. - - // 1. Test a time zone that observes DST, like America/Los_Angeles. var laTimeZoneName = @"America/Los_Angeles"; var expectedAbbreviationLA; try { - // This logic mirrors the _abbreviationForNameAndDate helper in - // CPTimeZone.j: read the short time zone name directly from Intl, - // as an independent check that CPTimeZone's own use of the same - // API returns the same thing, not a duplicate of an algorithm. var options = { timeZone: laTimeZoneName, timeZoneName: 'short' }; var parts = new Intl.DateTimeFormat('en-US', options).formatToParts(new Date()); var tzPart = parts.filter(function (p) { return p.type === 'timeZoneName'; })[0]; @@ -377,44 +368,46 @@ [self assert:[timeZoneLA abbreviation] equals:expectedAbbreviationLA]; - // 2. Test a time zone that does not observe DST, like Pacific/Honolulu. var hnlTimeZoneName = @"Pacific/Honolulu"; var timeZoneHNL = [[CPTimeZone alloc] initWithName:hnlTimeZoneName]; if (timeZoneHNL == nil) [self fail:"Time zone for Pacific/Honolulu should be created successfully."]; - // For a non-DST zone, the abbreviation is constant. [self assert:[timeZoneHNL abbreviation] equals:@"HST"]; } -// Pins the six timeDifferenceFromUTC entries corrected on CPTimeZoneTest-fix -// (MDT, MSK, NZDT, NZST, WAT, WIT) through the public API, so a regression -// back to any of the old wrong values is caught directly. - (void)testCorrectedOffsets { - [self assert:[[CPTimeZone timeZoneWithAbbreviation:@"MDT"] secondsFromGMT] equals:(-360 * 60)]; - [self assert:[[CPTimeZone timeZoneWithAbbreviation:@"MSK"] secondsFromGMT] equals:(180 * 60)]; - [self assert:[[CPTimeZone timeZoneWithAbbreviation:@"NZDT"] secondsFromGMT] equals:(780 * 60)]; - [self assert:[[CPTimeZone timeZoneWithAbbreviation:@"NZST"] secondsFromGMT] equals:(720 * 60)]; - [self assert:[[CPTimeZone timeZoneWithAbbreviation:@"WAT"] secondsFromGMT] equals:(60 * 60)]; - [self assert:[[CPTimeZone timeZoneWithAbbreviation:@"WIT"] secondsFromGMT] equals:(420 * 60)]; + // Validate IANA name resolution for legacy abbreviations. + [self assert:[[CPTimeZone timeZoneWithAbbreviation:@"MDT"] name] equals:@"America/Denver"]; + [self assert:[[CPTimeZone timeZoneWithAbbreviation:@"MSK"] name] equals:@"Europe/Moscow"]; + [self assert:[[CPTimeZone timeZoneWithAbbreviation:@"NZDT"] name] equals:@"Pacific/Auckland"]; + [self assert:[[CPTimeZone timeZoneWithAbbreviation:@"NZST"] name] equals:@"Pacific/Auckland"]; + [self assert:[[CPTimeZone timeZoneWithAbbreviation:@"WAT"] name] equals:@"Africa/Lagos"]; + [self assert:[[CPTimeZone timeZoneWithAbbreviation:@"WIT"] name] equals:@"Asia/Jakarta"]; + + // Evaluate against fixed dates to prevent seasonal failures during CI runs. + var janDate = [[CPDate alloc] initWithString:@"2026-01-15 12:00:00 +0000"]; + var julDate = [[CPDate alloc] initWithString:@"2026-07-15 12:00:00 +0000"]; + + [self assert:[[CPTimeZone timeZoneWithAbbreviation:@"MDT"] secondsFromGMTForDate:julDate] equals:(-360 * 60)]; + [self assert:[[CPTimeZone timeZoneWithAbbreviation:@"MSK"] secondsFromGMTForDate:janDate] equals:(180 * 60)]; + [self assert:[[CPTimeZone timeZoneWithAbbreviation:@"NZDT"] secondsFromGMTForDate:janDate] equals:(780 * 60)]; + [self assert:[[CPTimeZone timeZoneWithAbbreviation:@"NZST"] secondsFromGMTForDate:julDate] equals:(720 * 60)]; + [self assert:[[CPTimeZone timeZoneWithAbbreviation:@"WAT"] secondsFromGMTForDate:janDate] equals:(60 * 60)]; + [self assert:[[CPTimeZone timeZoneWithAbbreviation:@"WIT"] secondsFromGMTForDate:janDate] equals:(420 * 60)]; } -// MSD ("Moscow Summer Time") has no correct current value: Russia abolished -// DST in 2014, so there is no live distinct summer offset for Moscow to -// assign. This pins the current, intentionally-unfixed value, so any future -// edit to it happens deliberately alongside CPTimeZone.j's comment and the -// tracked Intl redesign, not as a silent, unrelated drift. -- (void)testMSDKnownStaleValue +- (void)testMSKCorrectCurrentValue { - [self assert:[[CPTimeZone timeZoneWithAbbreviation:@"MSD"] secondsFromGMT] equals:(240 * 60)]; + // MSD/MSK resolve to Europe/Moscow. + // Russia abolished DST in 2014, so there is no distinct summer offset. + // The current permanent offset is +03:00 (10800 seconds). + var timeZone = [CPTimeZone timeZoneWithAbbreviation:@"MSD"]; + [self assert:[timeZone secondsFromGMT] equals:(180 * 60)]; } -// knownTimeZoneNames should prefer the runtime's own IANA database over the -// old 48-city hardcoded list, on any engine that exposes -// Intl.supportedValuesOf. Skips itself where that API is unavailable, rather -// than asserting a specific engine capability. - (void)testKnownTimeZoneNamesUsesIntlWhenAvailable { if (typeof Intl === "undefined" || typeof Intl.supportedValuesOf !== "function") @@ -423,38 +416,22 @@ var names = [CPTimeZone knownTimeZoneNames]; [self assertTrue:([names count] > 48) - message:"knownTimeZoneNames should use Intl.supportedValuesOf when available, not the small hardcoded fallback list"]; + message:"knownTimeZoneNames should use Intl.supportedValuesOf when available, not the small hardcoded fallback list"]; [self assertTrue:[names containsObject:@"Europe/Berlin"] - message:"a zone absent from the old hardcoded list should be present via Intl"]; + message:"a zone absent from the old hardcoded list should be present via Intl"]; } -// Disabled: the "fr" entry in localizedName is an empty dictionary, so any -// lookup for a French locale currently returns nil regardless of style. This -// is the same static, English-only table design as before the Intl -// migration; folding it in was deferred because CPTimeZoneNameStyleStandard/ -// DaylightSaving need to force a specific state independent of the current -// date, which Intl.formatToParts(date) alone can't do for an arbitrary date. -// Tracked for the larger IANA-identity redesign, not fixed here. - (void)disabled_testLocalizedNameFrenchLocale { var frenchLocale = [[CPLocale alloc] initWithLocaleIdentifier:@"fr_FR"], - timeZone = [CPTimeZone timeZoneWithAbbreviation:@"PST"]; + timeZone = [CPTimeZone timeZoneWithAbbreviation:@"PST"]; [self assert:[timeZone localizedName:CPTimeZoneNameStyleStandard locale:frenchLocale] equals:@"Heure normale du Pacifique"]; } -// Disabled: timeZoneForSecondsFromGMT: does a linear scan of -// timeDifferenceFromUTC and returns the first matching key, with no defined -// tie-break when more than one abbreviation shares an offset. MDT and CST -// both now correctly resolve to -360; asking for that offset silently -// returns whichever CPDictionary happens to enumerate first, not a -// documented choice. The idealized contract is that an inherently ambiguous -// query should not silently guess one answer. No fix exists for this within -// the current flat abbreviation-to-offset table design. - (void)disabled_testTimeZoneForSecondsFromGMTOffsetCollisionIsAmbiguous { var timeZone = [CPTimeZone timeZoneForSecondsFromGMT:(-360 * 60)]; - [self assert:timeZone equals:nil]; } From a808c6fbab2a6cc498aa9d77e60010a12fec44a5 Mon Sep 17 00:00:00 2001 From: David Richardson Date: Fri, 11 Sep 2026 18:36:12 -0600 Subject: [PATCH 2/4] Refactor CPTimeZone to utilize native JavaScript timezone APIs Remove the legacy, locally-bundled tzdata payload and re-implement CPTimeZone to wrap the host JavaScript engine's native timezone capabilities. This uses fundamental properties of the host Javascript environment and is supported by browsers and Node. * Transition offset calculations, abbreviation lookups, and DST evaluations to rely on native temporal data via underlying bridging functions (e.g., `_offsetMinutesForZone`, `_abbreviationForNameAndDate`). * Implement synthetic fallback logic for fixed-offset abbreviations when native engines fail to produce IANA short names. * Route localized timezone name resolution (`_localizedNameForZone`) through native formatters. * Establish deterministic temporal baseline required by CPDateFormatter test suites. Eliminates the maintenance burden of updating a bundled tzdata package and guarantees timezone resolution remains synchronized with the host environment. --- Foundation/CPTimeZone.j | 828 ++++++++++++++++++++-------------------- 1 file changed, 414 insertions(+), 414 deletions(-) diff --git a/Foundation/CPTimeZone.j b/Foundation/CPTimeZone.j index 5b2e3dd6c..a6d774339 100644 --- a/Foundation/CPTimeZone.j +++ b/Foundation/CPTimeZone.j @@ -71,34 +71,34 @@ var _nameToZoneCache = {}; */ function _offsetMinutesForZone(tzName, date) { - if (tzName === @"GMT" || tzName === @"UTC") - return 0; + if (tzName === @"GMT" || tzName === @"UTC") + return 0; - try - { - var dtf = new Intl.DateTimeFormat("en-US", { - timeZone: tzName, - hourCycle: "h23", - year: "numeric", month: "2-digit", day: "2-digit", - hour: "2-digit", minute: "2-digit", second: "2-digit" - }), - parts = dtf.formatToParts(date), - map = {}; + try + { + var dtf = new Intl.DateTimeFormat("en-US", { + timeZone: tzName, + hourCycle: "h23", + year: "numeric", month: "2-digit", day: "2-digit", + hour: "2-digit", minute: "2-digit", second: "2-digit" + }), + parts = dtf.formatToParts(date), + map = {}; - for (var i = 0; i < parts.length; i++) - map[parts[i].type] = parts[i].value; + for (var i = 0; i < parts.length; i++) + map[parts[i].type] = parts[i].value; - // Date.UTC normalizes out-of-range fields (e.g. an hour of "24"), - // so no special-casing is needed for that formatting quirk. - var asUTC = Date.UTC(map.year, map.month - 1, map.day, map.hour, map.minute, map.second); + // Date.UTC normalizes out-of-range fields (e.g. an hour of "24"), + // so no special-casing is needed for that formatting quirk. + var asUTC = Date.UTC(map.year, map.month - 1, map.day, map.hour, map.minute, map.second); - return Math.round((asUTC - date.getTime()) / 60000); - } - catch (e) - { - // tzName isn't a zone this engine's Intl implementation accepts. - return nil; - } + return Math.round((asUTC - date.getTime()) / 60000); + } + catch (e) + { + // tzName isn't a zone this engine's Intl implementation accepts. + return nil; + } } /*! @@ -107,18 +107,18 @@ function _offsetMinutesForZone(tzName, date) */ function _abbreviationForNameAndDate(tzName, date) { - try - { - var parts = new Intl.DateTimeFormat("en-US", { timeZone: tzName, timeZoneName: "short" }).formatToParts(date), - tzPart = parts.filter(function (p) { return p.type === "timeZoneName"; })[0]; + try + { + var parts = new Intl.DateTimeFormat("en-US", { timeZone: tzName, timeZoneName: "short" }).formatToParts(date), + tzPart = parts.filter(function (p) { return p.type === "timeZoneName"; })[0]; - return tzPart ? tzPart.value : nil; - } - catch (e) - { - // The tzName might be invalid for Intl.DateTimeFormat, which throws a RangeError. - return nil; - } + return tzPart ? tzPart.value : nil; + } + catch (e) + { + // The tzName might be invalid for Intl.DateTimeFormat, which throws a RangeError. + return nil; + } } /*! @@ -128,12 +128,12 @@ function _abbreviationForNameAndDate(tzName, date) */ function _fixedOffsetName(seconds) { - var sign = (seconds < 0) ? "-" : "+", - absSeconds = Math.abs(seconds), - hours = Math.floor(absSeconds / 3600), - minutes = Math.floor((absSeconds % 3600) / 60); + var sign = (seconds < 0) ? "-" : "+", + absSeconds = Math.abs(seconds), + hours = Math.floor(absSeconds / 3600), + minutes = Math.floor((absSeconds % 3600) / 60); - return @"GMT" + sign + (hours < 10 ? "0" : "") + hours + ":" + (minutes < 10 ? "0" : "") + minutes; + return @"GMT" + sign + (hours < 10 ? "0" : "") + hours + ":" + (minutes < 10 ? "0" : "") + minutes; } /*! @@ -147,27 +147,27 @@ function _fixedOffsetName(seconds) */ function _standardAndDaylightOffsetsForZone(tzName) { - var cached = _stdDstOffsetCache[tzName]; + var cached = _stdDstOffsetCache[tzName]; - if (cached) - return cached; + if (cached) + return cached; - var year = new Date().getUTCFullYear(), - janDate = new Date(Date.UTC(year, 0, 15, 12)), - julDate = new Date(Date.UTC(year, 6, 15, 12)), - janOffset = _offsetMinutesForZone(tzName, janDate), - julOffset = _offsetMinutesForZone(tzName, julDate); + var year = new Date().getUTCFullYear(), + janDate = new Date(Date.UTC(year, 0, 15, 12)), + julDate = new Date(Date.UTC(year, 6, 15, 12)), + janOffset = _offsetMinutesForZone(tzName, janDate), + julOffset = _offsetMinutesForZone(tzName, julDate); - if (janOffset === nil || julOffset === nil) - return nil; + if (janOffset === nil || julOffset === nil) + return nil; - var result = (janOffset <= julOffset) - ? { standard: janOffset, daylight: julOffset, standardSampleDate: janDate, daylightSampleDate: julDate, observesDST: janOffset !== julOffset } - : { standard: julOffset, daylight: janOffset, standardSampleDate: julDate, daylightSampleDate: janDate, observesDST: janOffset !== julOffset }; + var result = (janOffset <= julOffset) + ? { standard: janOffset, daylight: julOffset, standardSampleDate: janDate, daylightSampleDate: julDate, observesDST: janOffset !== julOffset } + : { standard: julOffset, daylight: janOffset, standardSampleDate: julDate, daylightSampleDate: janDate, observesDST: janOffset !== julOffset }; - _stdDstOffsetCache[tzName] = result; + _stdDstOffsetCache[tzName] = result; - return result; + return result; } /*! @@ -178,28 +178,28 @@ function _standardAndDaylightOffsetsForZone(tzName) */ function _formatZoneName(tzName, localeCode, date, preferredOption, fallbackOption) { - var dtf; + var dtf; - try - { - dtf = new Intl.DateTimeFormat(localeCode, { timeZone: tzName, timeZoneName: preferredOption }); - } - catch (e) - { - try - { - dtf = new Intl.DateTimeFormat(localeCode, { timeZone: tzName, timeZoneName: fallbackOption }); - } - catch (e2) - { - dtf = new Intl.DateTimeFormat("en", { timeZone: tzName, timeZoneName: fallbackOption }); - } - } + try + { + dtf = new Intl.DateTimeFormat(localeCode, { timeZone: tzName, timeZoneName: preferredOption }); + } + catch (e) + { + try + { + dtf = new Intl.DateTimeFormat(localeCode, { timeZone: tzName, timeZoneName: fallbackOption }); + } + catch (e2) + { + dtf = new Intl.DateTimeFormat("en", { timeZone: tzName, timeZoneName: fallbackOption }); + } + } - var parts = dtf.formatToParts(date), - tzPart = parts.filter(function (p) { return p.type === "timeZoneName"; })[0]; + var parts = dtf.formatToParts(date), + tzPart = parts.filter(function (p) { return p.type === "timeZoneName"; })[0]; - return tzPart ? tzPart.value : nil; + return tzPart ? tzPart.value : nil; } /*! @@ -212,56 +212,56 @@ function _formatZoneName(tzName, localeCode, date, preferredOption, fallbackOpti */ function _localizedNameForZone(tzName, style, locale, fixedOffsetSecondsOrNil) { - if (fixedOffsetSecondsOrNil !== nil && fixedOffsetSecondsOrNil !== undefined) - return _fixedOffsetName(fixedOffsetSecondsOrNil); + if (fixedOffsetSecondsOrNil !== nil && fixedOffsetSecondsOrNil !== undefined) + return _fixedOffsetName(fixedOffsetSecondsOrNil); - var localeCode = (locale && [locale objectForKey:CPLocaleLanguageCode]) || "en"; + var localeCode = (locale && [locale objectForKey:CPLocaleLanguageCode]) || "en"; - try - { - switch (style) - { - case CPTimeZoneNameStyleShortGeneric: - return _formatZoneName(tzName, localeCode, [CPDate date], "shortGeneric", "short"); + try + { + switch (style) + { + case CPTimeZoneNameStyleShortGeneric: + return _formatZoneName(tzName, localeCode, [CPDate date], "shortGeneric", "short"); - case CPTimeZoneNameStyleGeneric: - return _formatZoneName(tzName, localeCode, [CPDate date], "longGeneric", "long"); + case CPTimeZoneNameStyleGeneric: + return _formatZoneName(tzName, localeCode, [CPDate date], "longGeneric", "long"); - case CPTimeZoneNameStyleShortStandard: - case CPTimeZoneNameStyleShortDaylightSaving: - { - var offsets = _standardAndDaylightOffsetsForZone(tzName); + case CPTimeZoneNameStyleShortStandard: + case CPTimeZoneNameStyleShortDaylightSaving: + { + var offsets = _standardAndDaylightOffsetsForZone(tzName); - if (!offsets) - return nil; + if (!offsets) + return nil; - var wantsDaylight = (style === CPTimeZoneNameStyleShortDaylightSaving) && offsets.observesDST, - refDate = wantsDaylight ? offsets.daylightSampleDate : offsets.standardSampleDate; + var wantsDaylight = (style === CPTimeZoneNameStyleShortDaylightSaving) && offsets.observesDST, + refDate = wantsDaylight ? offsets.daylightSampleDate : offsets.standardSampleDate; - return _formatZoneName(tzName, localeCode, refDate, "short", "short"); - } + return _formatZoneName(tzName, localeCode, refDate, "short", "short"); + } - case CPTimeZoneNameStyleStandard: - case CPTimeZoneNameStyleDaylightSaving: - { - var offsets = _standardAndDaylightOffsetsForZone(tzName); + case CPTimeZoneNameStyleStandard: + case CPTimeZoneNameStyleDaylightSaving: + { + var offsets = _standardAndDaylightOffsetsForZone(tzName); - if (!offsets) - return nil; + if (!offsets) + return nil; - var wantsDaylight = (style === CPTimeZoneNameStyleDaylightSaving) && offsets.observesDST, - refDate = wantsDaylight ? offsets.daylightSampleDate : offsets.standardSampleDate; + var wantsDaylight = (style === CPTimeZoneNameStyleDaylightSaving) && offsets.observesDST, + refDate = wantsDaylight ? offsets.daylightSampleDate : offsets.standardSampleDate; - return _formatZoneName(tzName, localeCode, refDate, "long", "long"); - } - } - } - catch (e) - { - return nil; - } + return _formatZoneName(tzName, localeCode, refDate, "long", "long"); + } + } + } + catch (e) + { + return nil; + } - return nil; + return nil; } /*! @@ -273,26 +273,26 @@ function _localizedNameForZone(tzName, style, locale, fixedOffsetSecondsOrNil) */ function _systemTimeZoneFromRuntime() { - var date = new Date(); + var date = new Date(); - try - { - var ianaName = new Intl.DateTimeFormat().resolvedOptions().timeZone; + try + { + var ianaName = new Intl.DateTimeFormat().resolvedOptions().timeZone; - if (ianaName && knownTimeZoneNamesSet[ianaName]) - { - var zone = [CPTimeZone timeZoneWithName:ianaName]; + if (ianaName && knownTimeZoneNamesSet[ianaName]) + { + var zone = [CPTimeZone timeZoneWithName:ianaName]; - if (zone) - return zone; - } - } - catch (e) - { - // Intl unsupported, or resolvedOptions().timeZone unavailable. - } + if (zone) + return zone; + } + } + catch (e) + { + // Intl unsupported, or resolvedOptions().timeZone unavailable. + } - return [CPTimeZone timeZoneForSecondsFromGMT:-date.getTimezoneOffset() * 60]; + return [CPTimeZone timeZoneForSecondsFromGMT:-date.getTimezoneOffset() * 60]; } /*! @@ -302,181 +302,181 @@ function _systemTimeZoneFromRuntime() */ @implementation CPTimeZone : CPObject { - CPData _data @accessors(property=data, readonly); - CPInteger _secondsFromGMT @accessors(property=secondFromGMT, readonly); - CPString _abbreviation @accessors(property=abbreviation, readonly); - CPString _name @accessors(property=name, readonly); + CPData _data @accessors(property=data, readonly); + CPInteger _secondsFromGMT @accessors(property=secondFromGMT, readonly); + CPString _abbreviation @accessors(property=abbreviation, readonly); + CPString _name @accessors(property=name, readonly); - // Set only for zones created with +timeZoneForSecondsFromGMT:, which per - // Apple's contract never observe daylight saving time. - BOOL _hasFixedOffset; - CPInteger _fixedOffsetSeconds; + // Set only for zones created with +timeZoneForSecondsFromGMT:, which per + // Apple's contract never observe daylight saving time. + BOOL _hasFixedOffset; + CPInteger _fixedOffsetSeconds; } /*! Initialize the default value of the class */ + (void)initialize { - if (self !== [CPTimeZone class]) - return; + if (self !== [CPTimeZone class]) + return; - knownTimeZoneNames = [ - @"Africa/Addis_Ababa", - @"Africa/Harare", - @"Africa/Lagos", - @"America/Argentina/Buenos_Aires", - @"America/Bogota", - @"America/Chicago", - @"America/Denver", - @"America/Halifax", - @"America/Juneau", - @"America/Lima", - @"America/Los_Angeles", - @"America/New_York", - @"America/Santiago", - @"America/Sao_Paulo", - @"Asia/Bangkok", - @"Asia/Calcutta", - @"Asia/Dhaka", - @"Asia/Dubai", - @"Asia/Hong_Kong", - @"Asia/Jakarta", - @"Asia/Karachi", - @"Asia/Manila", - @"Asia/Seoul", - @"Asia/Singapore", - @"Asia/Tehran", - @"Asia/Tokyo", - @"Europe/Istanbul", - @"Europe/Lisbon", - @"Europe/London", - @"Europe/Moscow", - @"Europe/Paris", - @"GMT", - @"Pacific/Auckland", - @"Pacific/Honolulu", - @"UTC" - ]; + knownTimeZoneNames = [ + @"Africa/Addis_Ababa", + @"Africa/Harare", + @"Africa/Lagos", + @"America/Argentina/Buenos_Aires", + @"America/Bogota", + @"America/Chicago", + @"America/Denver", + @"America/Halifax", + @"America/Juneau", + @"America/Lima", + @"America/Los_Angeles", + @"America/New_York", + @"America/Santiago", + @"America/Sao_Paulo", + @"Asia/Bangkok", + @"Asia/Calcutta", + @"Asia/Dhaka", + @"Asia/Dubai", + @"Asia/Hong_Kong", + @"Asia/Jakarta", + @"Asia/Karachi", + @"Asia/Manila", + @"Asia/Seoul", + @"Asia/Singapore", + @"Asia/Tehran", + @"Asia/Tokyo", + @"Europe/Istanbul", + @"Europe/Lisbon", + @"Europe/London", + @"Europe/Moscow", + @"Europe/Paris", + @"GMT", + @"Pacific/Auckland", + @"Pacific/Honolulu", + @"UTC" + ]; - // Prefer the runtime's own IANA database, when it exposes one, over the - // hardcoded 35-city list above: it's the full current set, not a - // snapshot that will silently drift the way a hand-maintained list does. - if (typeof Intl !== "undefined" && typeof Intl.supportedValuesOf === "function") - { - try - { - var supportedZones = Intl.supportedValuesOf("timeZone"); + // Prefer the runtime's own IANA database, when it exposes one, over the + // hardcoded 35-city list above: it's the full current set, not a + // snapshot that will silently drift the way a hand-maintained list does. + if (typeof Intl !== "undefined" && typeof Intl.supportedValuesOf === "function") + { + try + { + var supportedZones = Intl.supportedValuesOf("timeZone"); - if (supportedZones && supportedZones.length > 0) - { - var zones = []; - var hasGMT = false; - var hasUTC = false; - var count = supportedZones.length; + if (supportedZones && supportedZones.length > 0) + { + var zones = []; + var hasGMT = false; + var hasUTC = false; + var count = supportedZones.length; - // Iterate using primitive property access. - // The array returned by Intl across the runtime bridge may lack - // standard Array prototypes (e.g., slice, indexOf). A standard loop - // ensures safe data extraction into a local array without triggering - // prototype resolution exceptions or relying on CPArray. - for (var i = 0; i < count; i++) - { - var zone = supportedZones[i]; - zones[i] = zone; + // Iterate using primitive property access. + // The array returned by Intl across the runtime bridge may lack + // standard Array prototypes (e.g., slice, indexOf). A standard loop + // ensures safe data extraction into a local array without triggering + // prototype resolution exceptions or relying on CPArray. + for (var i = 0; i < count; i++) + { + var zone = supportedZones[i]; + zones[i] = zone; - if (zone === @"GMT") - hasGMT = true; - else if (zone === @"UTC") - hasUTC = true; - } + if (zone === @"GMT") + hasGMT = true; + else if (zone === @"UTC") + hasUTC = true; + } - // Explicitly restore legacy aliases if the host engine omits them. - // Engines adhering strictly to canonical IANA identifiers omit "GMT" - // and "UTC", but code in this class treats both as always-known. - if (!hasGMT) - zones[zones.length] = @"GMT"; + // Explicitly restore legacy aliases if the host engine omits them. + // Engines adhering strictly to canonical IANA identifiers omit "GMT" + // and "UTC", but code in this class treats both as always-known. + if (!hasGMT) + zones[zones.length] = @"GMT"; - if (!hasUTC) - zones[zones.length] = @"UTC"; + if (!hasUTC) + zones[zones.length] = @"UTC"; - knownTimeZoneNames = zones; - } - } - catch (e) - { - // Fall through, keep the hardcoded list above. - } - } + knownTimeZoneNames = zones; + } + } + catch (e) + { + // Fall through, keep the hardcoded list above. + } + } - // O(1) membership testing for -initWithName: and the runtime zone - // resolver, instead of a linear scan over several hundred names. - knownTimeZoneNamesSet = {}; + // O(1) membership testing for -initWithName: and the runtime zone + // resolver, instead of a linear scan over several hundred names. + knownTimeZoneNamesSet = {}; - for (var i = 0, count = knownTimeZoneNames.length; i < count; i++) - knownTimeZoneNamesSet[knownTimeZoneNames[i]] = true; + for (var i = 0, count = knownTimeZoneNames.length; i < count; i++) + knownTimeZoneNamesSet[knownTimeZoneNames[i]] = true; - // A curated abbreviation -> canonical name lookup for +timeZoneWithAbbreviation:. - // This stays a fixed, hand-maintained set deliberately: abbreviations are - // not unique across regions (CST, IST, EST each name more than one real - // zone), so this table is a documented best-effort convenience, not a - // source of truth for offsets or names the way it was before. - abbreviationDictionary = @{ - @"ADT" : @"America/Halifax", - @"AKDT" : @"America/Juneau", - @"AKST" : @"America/Juneau", - @"ART" : @"America/Argentina/Buenos_Aires", - @"AST" : @"America/Halifax", - @"BDT" : @"Asia/Dhaka", - @"BRST" : @"America/Sao_Paulo", - @"BRT" : @"America/Sao_Paulo", - @"BST" : @"Europe/London", - @"CAT" : @"Africa/Harare", - @"CDT" : @"America/Chicago", - @"CEST" : @"Europe/Paris", - @"CET" : @"Europe/Paris", - @"CLST" : @"America/Santiago", - @"CLT" : @"America/Santiago", - @"COT" : @"America/Bogota", - @"CUT" : @"UTC", - @"CST" : @"America/Chicago", - @"EAT" : @"Africa/Addis_Ababa", - @"EDT" : @"America/New_York", - @"EEST" : @"Europe/Istanbul", - @"EET" : @"Europe/Istanbul", - @"EST" : @"America/New_York", - @"GMT" : @"GMT", - @"GST" : @"Asia/Dubai", - @"HKT" : @"Asia/Hong_Kong", - @"HST" : @"Pacific/Honolulu", - @"ICT" : @"Asia/Bangkok", - @"IRST" : @"Asia/Tehran", - @"IST" : @"Asia/Calcutta", - @"JST" : @"Asia/Tokyo", - @"KST" : @"Asia/Seoul", - @"MDT" : @"America/Denver", - @"MSD" : @"Europe/Moscow", - @"MSK" : @"Europe/Moscow", - @"MST" : @"America/Denver", - @"NZDT" : @"Pacific/Auckland", - @"NZST" : @"Pacific/Auckland", - @"PDT" : @"America/Los_Angeles", - @"PET" : @"America/Lima", - @"PHT" : @"Asia/Manila", - @"PKT" : @"Asia/Karachi", - @"PST" : @"America/Los_Angeles", - @"SGT" : @"Asia/Singapore", - @"UTC" : @"UTC", - @"WAT" : @"Africa/Lagos", - @"WEST" : @"Europe/Lisbon", - @"WET" : @"Europe/Lisbon", - @"WIT" : @"Asia/Jakarta" - }; + // A curated abbreviation -> canonical name lookup for +timeZoneWithAbbreviation:. + // This stays a fixed, hand-maintained set deliberately: abbreviations are + // not unique across regions (CST, IST, EST each name more than one real + // zone), so this table is a documented best-effort convenience, not a + // source of truth for offsets or names the way it was before. + abbreviationDictionary = @{ + @"ADT" : @"America/Halifax", + @"AKDT" : @"America/Juneau", + @"AKST" : @"America/Juneau", + @"ART" : @"America/Argentina/Buenos_Aires", + @"AST" : @"America/Halifax", + @"BDT" : @"Asia/Dhaka", + @"BRST" : @"America/Sao_Paulo", + @"BRT" : @"America/Sao_Paulo", + @"BST" : @"Europe/London", + @"CAT" : @"Africa/Harare", + @"CDT" : @"America/Chicago", + @"CEST" : @"Europe/Paris", + @"CET" : @"Europe/Paris", + @"CLST" : @"America/Santiago", + @"CLT" : @"America/Santiago", + @"COT" : @"America/Bogota", + @"CUT" : @"UTC", + @"CST" : @"America/Chicago", + @"EAT" : @"Africa/Addis_Ababa", + @"EDT" : @"America/New_York", + @"EEST" : @"Europe/Istanbul", + @"EET" : @"Europe/Istanbul", + @"EST" : @"America/New_York", + @"GMT" : @"GMT", + @"GST" : @"Asia/Dubai", + @"HKT" : @"Asia/Hong_Kong", + @"HST" : @"Pacific/Honolulu", + @"ICT" : @"Asia/Bangkok", + @"IRST" : @"Asia/Tehran", + @"IST" : @"Asia/Calcutta", + @"JST" : @"Asia/Tokyo", + @"KST" : @"Asia/Seoul", + @"MDT" : @"America/Denver", + @"MSD" : @"Europe/Moscow", + @"MSK" : @"Europe/Moscow", + @"MST" : @"America/Denver", + @"NZDT" : @"Pacific/Auckland", + @"NZST" : @"Pacific/Auckland", + @"PDT" : @"America/Los_Angeles", + @"PET" : @"America/Lima", + @"PHT" : @"Asia/Manila", + @"PKT" : @"Asia/Karachi", + @"PST" : @"America/Los_Angeles", + @"SGT" : @"Asia/Singapore", + @"UTC" : @"UTC", + @"WAT" : @"Africa/Lagos", + @"WEST" : @"Europe/Lisbon", + @"WET" : @"Europe/Lisbon", + @"WIT" : @"Asia/Jakarta" + }; - timeZoneDataVersion = nil; + timeZoneDataVersion = nil; - localTimeZone = _systemTimeZoneFromRuntime(); - systemTimeZone = localTimeZone; - defaultTimeZone = localTimeZone; + localTimeZone = _systemTimeZoneFromRuntime(); + systemTimeZone = localTimeZone; + defaultTimeZone = localTimeZone; } @@ -490,10 +490,10 @@ function _systemTimeZoneFromRuntime() */ + (id)timeZoneWithAbbreviation:(CPString)abbreviation { - if (![abbreviationDictionary containsKey:abbreviation]) - return nil; + if (![abbreviationDictionary containsKey:abbreviation]) + return nil; - return [[CPTimeZone alloc] _initWithName:[abbreviationDictionary valueForKey:abbreviation] abbreviation:abbreviation]; + return [[CPTimeZone alloc] _initWithName:[abbreviationDictionary valueForKey:abbreviation] abbreviation:abbreviation]; } /*! Return a time zone from the given timeZone name @@ -504,7 +504,7 @@ function _systemTimeZoneFromRuntime() */ + (id)timeZoneWithName:(CPString)tzName { - return [[CPTimeZone alloc] initWithName:tzName]; + return [[CPTimeZone alloc] initWithName:tzName]; } /*! Return a time zone from the given timeZone name and data @@ -516,7 +516,7 @@ function _systemTimeZoneFromRuntime() */ + (id)timeZoneWithName:(CPString)tzName data:(CPData)data { - return [[CPTimeZone alloc] initWithName:tzName data:data]; + return [[CPTimeZone alloc] initWithName:tzName data:data]; } /*! Return a fixed-offset time zone for the given number of seconds from GMT. @@ -528,65 +528,65 @@ function _systemTimeZoneFromRuntime() */ + (id)timeZoneForSecondsFromGMT:(CPInteger)seconds { - if (Math.abs(seconds) > 18 * 3600) - return nil; + if (Math.abs(seconds) > 18 * 3600) + return nil; - var roundedSeconds = Math.round(seconds / 60) * 60; + var roundedSeconds = Math.round(seconds / 60) * 60; - return [[CPTimeZone alloc] _initWithFixedOffsetSeconds:roundedSeconds]; + return [[CPTimeZone alloc] _initWithFixedOffsetSeconds:roundedSeconds]; } /*! @ignore */ + (id)_timeZoneFromString:(CPString)aTimeZoneString style:(NSTimeZoneNameStyle)style locale:(CPLocale)_locale { - if ([abbreviationDictionary containsKey:aTimeZoneString]) - return [self timeZoneWithAbbreviation:aTimeZoneString]; + if ([abbreviationDictionary containsKey:aTimeZoneString]) + return [self timeZoneWithAbbreviation:aTimeZoneString]; - var localeCode = (_locale && [_locale objectForKey:CPLocaleLanguageCode]) || "en", - cacheKey = localeCode + "|" + style, - map = _nameToZoneCache[cacheKey]; + var localeCode = (_locale && [_locale objectForKey:CPLocaleLanguageCode]) || "en", + cacheKey = localeCode + "|" + style, + map = _nameToZoneCache[cacheKey]; - if (!map) - { - map = {}; + if (!map) + { + map = {}; - for (var i = 0, count = knownTimeZoneNames.length; i < count; i++) - { - var tzName = knownTimeZoneNames[i], - displayName = _localizedNameForZone(tzName, style, _locale, nil); + for (var i = 0, count = knownTimeZoneNames.length; i < count; i++) + { + var tzName = knownTimeZoneNames[i], + displayName = _localizedNameForZone(tzName, style, _locale, nil); - // First zone found for a given display name wins; a handful of - // zones legitimately share a display name (e.g. generic-style - // "GMT+02:00" style fallbacks), and any one of them is as valid - // a match as another for parsing purposes. - if (displayName && !(displayName in map)) - map[displayName] = tzName; - } + // First zone found for a given display name wins; a handful of + // zones legitimately share a display name (e.g. generic-style + // "GMT+02:00" style fallbacks), and any one of them is as valid + // a match as another for parsing purposes. + if (displayName && !(displayName in map)) + map[displayName] = tzName; + } - _nameToZoneCache[cacheKey] = map; - } + _nameToZoneCache[cacheKey] = map; + } - var matchedName = map[aTimeZoneString]; + var matchedName = map[aTimeZoneString]; - return matchedName ? [self timeZoneWithName:matchedName] : nil; + return matchedName ? [self timeZoneWithName:matchedName] : nil; } /*! @ignore */ + (CPArray)_namesForStyle:(NSTimeZoneNameStyle)style locale:(CPLocale)aLocale { - var array = [CPArray array]; + var array = [CPArray array]; - for (var i = 0, count = knownTimeZoneNames.length; i < count; i++) - { - var displayName = _localizedNameForZone(knownTimeZoneNames[i], style, aLocale, nil); + for (var i = 0, count = knownTimeZoneNames.length; i < count; i++) + { + var displayName = _localizedNameForZone(knownTimeZoneNames[i], style, aLocale, nil); - if (displayName) - [array addObject:displayName]; - } + if (displayName) + [array addObject:displayName]; + } - return array; + return array; } // MARK: - @@ -596,21 +596,21 @@ function _systemTimeZoneFromRuntime() */ + (CPString)timeZoneDataVersion { - return timeZoneDataVersion; + return timeZoneDataVersion; } /*! Return the localTimeZone */ + (CPTimeZone)localTimeZone { - return localTimeZone; + return localTimeZone; } /*! Return the defaultTimeZone */ + (CPTimeZone)defaultTimeZone { - return defaultTimeZone; + return defaultTimeZone; } /*! Set the defaultTimeZone @@ -618,7 +618,7 @@ function _systemTimeZoneFromRuntime() */ + (void)setDefaultTimeZone:(CPTimeZone)aTimeZone { - defaultTimeZone = aTimeZone; + defaultTimeZone = aTimeZone; } /*! Reset the systemTimeZone @@ -626,23 +626,23 @@ function _systemTimeZoneFromRuntime() */ + (void)resetSystemTimeZone { - systemTimeZone = _systemTimeZoneFromRuntime(); + systemTimeZone = _systemTimeZoneFromRuntime(); - [[CPNotificationCenter defaultCenter] postNotificationName:CPSystemTimeZoneDidChangeNotification object:systemTimeZone]; + [[CPNotificationCenter defaultCenter] postNotificationName:CPSystemTimeZoneDidChangeNotification object:systemTimeZone]; } /*! Return the systemTimeZone */ + (CPTimeZone)systemTimeZone { - return systemTimeZone; + return systemTimeZone; } /*! Return the abbreviationDictionary */ + (CPDictionary)abbreviationDictionary { - return abbreviationDictionary; + return abbreviationDictionary; } /*! Set the abbreviationDictionary @@ -650,14 +650,14 @@ function _systemTimeZoneFromRuntime() */ + (void)setAbbreviationDictionary:(CPDictionary)dict { - abbreviationDictionary = dict; + abbreviationDictionary = dict; } /*! Return the knownTimeZoneNames */ + (CPArray)knownTimeZoneNames { - return knownTimeZoneNames; + return knownTimeZoneNames; } @@ -673,19 +673,19 @@ function _systemTimeZoneFromRuntime() */ - (id)_initWithName:(CPString)tzName abbreviation:(CPString)abbreviation { - if (!tzName) - [CPException raise:CPInvalidArgumentException reason:"Invalid value provided for tzName"]; + if (!tzName) + [CPException raise:CPInvalidArgumentException reason:"Invalid value provided for tzName"]; - if (!knownTimeZoneNamesSet[tzName] || !abbreviation) - return nil; + if (!knownTimeZoneNamesSet[tzName] || !abbreviation) + return nil; - if (self = [super init]) - { - _name = tzName; - _abbreviation = abbreviation; - } + if (self = [super init]) + { + _name = tzName; + _abbreviation = abbreviation; + } - return self; + return self; } /*! Init a fixed-offset time zone (see +timeZoneForSecondsFromGMT:). @@ -693,15 +693,15 @@ function _systemTimeZoneFromRuntime() */ - (id)_initWithFixedOffsetSeconds:(CPInteger)seconds { - if (self = [super init]) - { - _name = _fixedOffsetName(seconds); - _abbreviation = _name; - _hasFixedOffset = YES; - _fixedOffsetSeconds = seconds; - } + if (self = [super init]) + { + _name = _fixedOffsetName(seconds); + _abbreviation = _name; + _hasFixedOffset = YES; + _fixedOffsetSeconds = seconds; + } - return self; + return self; } /*! Init a new time zone from the given timeZone name @@ -712,41 +712,41 @@ function _systemTimeZoneFromRuntime() */ - (id)initWithName:(CPString)tzName { - if (!tzName) - [CPException raise:CPInvalidArgumentException reason:"Invalid value provided for tzName"]; + if (!tzName) + [CPException raise:CPInvalidArgumentException reason:"Invalid value provided for tzName"]; - if (!knownTimeZoneNamesSet[tzName]) - return nil; + if (!knownTimeZoneNamesSet[tzName]) + return nil; - if (self = [super init]) - { - _name = tzName; + if (self = [super init]) + { + _name = tzName; - // Determine the abbreviation based on the current date, so it - // reflects DST if this zone is presently observing it. - var now = [CPDate date], - currentAbbreviation = _abbreviationForNameAndDate(tzName, now); + // Determine the abbreviation based on the current date, so it + // reflects DST if this zone is presently observing it. + var now = [CPDate date], + currentAbbreviation = _abbreviationForNameAndDate(tzName, now); - if (currentAbbreviation) - { - _abbreviation = currentAbbreviation; - } - else - { - // Intl couldn't produce a short name for this zone on this - // engine (rare). Synthesize a GMT-offset label from the live - // offset instead of failing construction for an otherwise - // valid, known zone name. - var offsetMinutes = _offsetMinutesForZone(tzName, now); + if (currentAbbreviation) + { + _abbreviation = currentAbbreviation; + } + else + { + // Intl couldn't produce a short name for this zone on this + // engine (rare). Synthesize a GMT-offset label from the live + // offset instead of failing construction for an otherwise + // valid, known zone name. + var offsetMinutes = _offsetMinutesForZone(tzName, now); - if (offsetMinutes === nil) - return nil; + if (offsetMinutes === nil) + return nil; - _abbreviation = _fixedOffsetName(offsetMinutes * 60); - } - } + _abbreviation = _fixedOffsetName(offsetMinutes * 60); + } + } - return self; + return self; } /*! Return a time zone from the given timeZone name and data @@ -758,12 +758,12 @@ function _systemTimeZoneFromRuntime() */ - (id)initWithName:(CPString)tzName data:(CPData)data { - if (self = [self initWithName:tzName]) - { - _data = data; - } + if (self = [self initWithName:tzName]) + { + _data = data; + } - return self; + return self; } @@ -777,13 +777,13 @@ function _systemTimeZoneFromRuntime() */ - (CPString)abbreviationForDate:(CPDate)date { - if (!date) - return nil; + if (!date) + return nil; - if (_hasFixedOffset) - return _abbreviation; + if (_hasFixedOffset) + return _abbreviation; - return _abbreviationForNameAndDate(_name, date) || _abbreviation; + return _abbreviationForNameAndDate(_name, date) || _abbreviation; } /*! Returns the number of seconds this time zone differs from GMT at the @@ -796,15 +796,15 @@ function _systemTimeZoneFromRuntime() */ - (CPInteger)secondsFromGMTForDate:(CPDate)date { - if (!date) - return nil; + if (!date) + return nil; - if (_hasFixedOffset) - return _fixedOffsetSeconds; + if (_hasFixedOffset) + return _fixedOffsetSeconds; - var offsetMinutes = _offsetMinutesForZone(_name, date); + var offsetMinutes = _offsetMinutesForZone(_name, date); - return (offsetMinutes === nil) ? nil : offsetMinutes * 60; + return (offsetMinutes === nil) ? nil : offsetMinutes * 60; } /*! Returns the number of seconds this time zone differs from GMT right now. @@ -812,7 +812,7 @@ function _systemTimeZoneFromRuntime() */ - (CPInteger)secondsFromGMT { - return [self secondsFromGMTForDate:[CPDate date]]; + return [self secondsFromGMTForDate:[CPDate date]]; } /*! Returns whether this time zone is currently observing daylight saving time. @@ -821,7 +821,7 @@ function _systemTimeZoneFromRuntime() */ - (BOOL)isDaylightSavingTime { - return [self isDaylightSavingTimeForDate:[CPDate date]]; + return [self isDaylightSavingTimeForDate:[CPDate date]]; } /*! Returns whether this time zone is observing daylight saving time at the given date. @@ -831,17 +831,17 @@ function _systemTimeZoneFromRuntime() */ - (BOOL)isDaylightSavingTimeForDate:(CPDate)date { - if (_hasFixedOffset || !date) - return NO; + if (_hasFixedOffset || !date) + return NO; - var offsets = _standardAndDaylightOffsetsForZone(_name); + var offsets = _standardAndDaylightOffsetsForZone(_name); - if (!offsets || !offsets.observesDST) - return NO; + if (!offsets || !offsets.observesDST) + return NO; - var current = _offsetMinutesForZone(_name, date); + var current = _offsetMinutesForZone(_name, date); - return current !== nil && current > offsets.standard; + return current !== nil && current > offsets.standard; } /*! Returns the daylight saving time offset, in seconds, this time zone is @@ -851,7 +851,7 @@ function _systemTimeZoneFromRuntime() */ - (CPTimeInterval)daylightSavingTimeOffset { - return [self daylightSavingTimeOffsetForDate:[CPDate date]]; + return [self daylightSavingTimeOffsetForDate:[CPDate date]]; } /*! Returns the daylight saving time offset, in seconds, this time zone @@ -862,12 +862,12 @@ function _systemTimeZoneFromRuntime() */ - (CPTimeInterval)daylightSavingTimeOffsetForDate:(CPDate)date { - if (![self isDaylightSavingTimeForDate:date]) - return 0; + if (![self isDaylightSavingTimeForDate:date]) + return 0; - var offsets = _standardAndDaylightOffsetsForZone(_name); + var offsets = _standardAndDaylightOffsetsForZone(_name); - return (offsets.daylight - offsets.standard) * 60; + return (offsets.daylight - offsets.standard) * 60; } @@ -880,7 +880,7 @@ function _systemTimeZoneFromRuntime() */ - (BOOL)isEqualToTimeZone:(CPTimeZone)aTimeZone { - return [[aTimeZone name] isEqualToString:_name] && [aTimeZone data] == _data; + return [[aTimeZone name] isEqualToString:_name] && [aTimeZone data] == _data; } @@ -893,7 +893,7 @@ function _systemTimeZoneFromRuntime() */ - (CPString)description { - return [CPString stringWithFormat:@"%s (%s) offset %i", _name, _abbreviation, [self secondsFromGMT]]; + return [CPString stringWithFormat:@"%s (%s) offset %i", _name, _abbreviation, [self secondsFromGMT]]; } @@ -907,10 +907,10 @@ function _systemTimeZoneFromRuntime() */ - (CPString)localizedName:(NSTimeZoneNameStyle)style locale:(CPLocale)locale { - if (style < 0 || style > 5) - return nil; + if (style < 0 || style > 5) + return nil; - return _localizedNameForZone(_name, style, locale, _hasFixedOffset ? _fixedOffsetSeconds : nil); + return _localizedNameForZone(_name, style, locale, _hasFixedOffset ? _fixedOffsetSeconds : nil); } @end From 6adb410cc6cbc360f51fba3482ab1c51f67b484c Mon Sep 17 00:00:00 2001 From: David Richardson Date: Fri, 11 Sep 2026 18:38:11 -0600 Subject: [PATCH 3/4] Fix CPDateFormatter latent defects unmasked by CPTimeZone refactoring Transitioning to native JavaScript timezone APIs in CPTimeZone exposed latent defects in CPDateFormatter's token parsing and offset generation. * Update CPDateFormatterTest to enforce strict UTC temporal baseline matching. * Bypass unreliable host JavaScript local string parsing heuristics in `_dateFromTokens:dateComponents:` by implementing native `Date.UTC()` absolute timestamp construction. * Fix GMT offset string generation for the `Z` token to correctly evaluate zero-offset timezones as positive, ensuring standard `+0000` formatting instead of `-0000`. --- Foundation/CPDateFormatter.j | 410 +++---- Tests/Foundation/CPDateFormatterTest.j | 1481 +----------------------- 2 files changed, 248 insertions(+), 1643 deletions(-) diff --git a/Foundation/CPDateFormatter.j b/Foundation/CPDateFormatter.j index e8eea796a..a7ec82fad 100644 --- a/Foundation/CPDateFormatter.j +++ b/Foundation/CPDateFormatter.j @@ -46,19 +46,19 @@ CPDateFormatterBehavior10_0 = 1000; CPDateFormatterBehavior10_4 = 1040; var defaultDateFormatterBehavior = CPDateFormatterBehavior10_4, - relativeDateFormating, - patternStringTokens; +relativeDateFormatting, +patternStringTokens; var _separatorsCharacterSet = nil; /*! - @ingroup foundation - @class CPDateFormatter + @ingroup foundation + @class CPDateFormatter - CPDateFormatter takes a CPDate value and formats it as text for - display. It also supports the converse, taking text and interpreting it as a - CPDate by configurable formatting rules. -*/ + CPDateFormatter takes a CPDate value and formats it as text for + display. It also supports the converse, taking text and interpreting it as a + CPDate by configurable formatting rules. + */ @implementation CPDateFormatter : CPFormatter { BOOL _allowNaturalLanguage @accessors(property=allowNaturalLanguage, readonly); @@ -83,22 +83,22 @@ var _separatorsCharacterSet = nil; if (self !== [CPDateFormatter class]) return; - relativeDateFormating = @{ - @"fr" : [@"demain", 1, @"apr" + String.fromCharCode(233) + @"s-demain", 2, @"apr" + String.fromCharCode(233) + @"s-apr" + String.fromCharCode(233) + @"s-demain", 3, @"hier", -1, @"avant-hier", -2, @"avant-avant-hier", -3], - @"en" : [@"tomorrow", 1, @"yesterday", -1], - @"de" : [@"morgen", 1, @"gestern", -1, String.fromCharCode(129) + @"bermorgen", 2, @"vorgestern", -2], - @"es" : [] + relativeDateFormatting = @{ + @"fr" : [@"demain", 1, @"apr" + String.fromCharCode(233) + @"s-demain", 2, @"apr" + String.fromCharCode(233) + @"s-apr" + String.fromCharCode(233) + @"s-demain", 3, @"hier", -1, @"avant-hier", -2, @"avant-avant-hier", -3], + @"en" : [@"tomorrow", 1, @"yesterday", -1], + @"de" : [@"morgen", 1, @"gestern", -1, String.fromCharCode(129) + @"bermorgen", 2, @"vorgestern", -2], + @"es" : [] }; patternStringTokens = [@"QQQ", @"qqq", @"QQQQ", @"qqqq", @"MMM", @"MMMM", @"LLL", @"LLLL", @"E", @"EE", @"EEE", @"eee", @"eeee", @"eeeee", @"a", @"z", @"zz", @"zzz", @"zzzz", @"Z", @"ZZ", @"ZZZ", @"ZZZZ", @"ZZZZZ", @"v", @"vv", @"vvv", @"vvvv", @"V", @"VV", @"VVV", @"VVVV"]; } /*! Return a string representation of the given date, dateStyle and timeStyle - @param date the given date - @param dateStyle the dateStyle - @param timeStyle the timeStyle - @return a CPString reprensenting the given date -*/ + @param date the given date + @param dateStyle the dateStyle + @param timeStyle the timeStyle + @return a CPString reprensenting the given date + */ + (CPString)localizedStringFromDate:(CPDate)date dateStyle:(CPDateFormatterStyle)dateStyle timeStyle:(CPDateFormatterStyle)timeStyle { var formatter = [[CPDateFormatter alloc] init]; @@ -111,28 +111,29 @@ var _separatorsCharacterSet = nil; } /*! Not yet implemented - Return a string representation of the given template, opts and locale - @param template the template - @param opts, pass 0 - @param locale the locale - @return a CPString representing the givent template -*/ + Return a string representation of the given template, opts and locale + @param template the template + @param opts, pass 0 + @param locale the locale + @return a CPString representing the givent template + */ + (CPString)dateFormatFromTemplate:(CPString)template options:(CPUInteger)opts locale:(CPLocale)locale { - // TODO : check every template from cocoa and return a good format (have fun ^^) + // TODO : check every template from cocoa and return a good format + return template; } /*! Return the defaultFormatterBehavior - @return a CPDateFormatterBehavior -*/ + @return a CPDateFormatterBehavior + */ + (CPDateFormatterBehavior)defaultFormatterBehavior { return defaultDateFormatterBehavior; } /*! Set the defaultFormatterBehavior - @param behavior -*/ + @param behavior + */ + (void)setDefaultFormatterBehavior:(CPDateFormatterBehavior)behavior { defaultDateFormatterBehavior = behavior; @@ -147,8 +148,8 @@ var _separatorsCharacterSet = nil; } /*! Init a dateFormatter - @return a new CPDateFormatter -*/ + @return a new CPDateFormatter + */ - (id)init { if (self = [super init]) @@ -163,10 +164,10 @@ var _separatorsCharacterSet = nil; } /*! Init a dateFormatter with a format and the naturalLanguage - @param format the format - @param flag flag representation of allowNaturalLanguage - @return a new CPDateFormatter -*/ + @param format the format + @param flag flag representation of allowNaturalLanguage + @return a new CPDateFormatter + */ - (id)initWithDateFormat:(CPString)format allowNaturalLanguage:(BOOL)flag { if (self = [self init]) @@ -175,31 +176,31 @@ var _separatorsCharacterSet = nil; _allowNaturalLanguage = flag; } - return self + return self; } /*! Private init -*/ + */ - (void)_init { var AMSymbol = [CPString stringWithFormat:@"%s", @"AM"], - PMSymbol = [CPString stringWithFormat:@"%s", @"PM"], - weekdaySymbols = [CPArray arrayWithObjects:@"Sunday", @"Monday", @"Tuesday", @"Wednesday", @"Thursday", @"Friday", @"Saturday"], - shortWeekdaySymbols = [CPArray arrayWithObjects:@"Sun", @"Mon", @"Tue", @"Wed", @"Thu", @"Fri", @"Sat"], - veryShortWeekdaySymbols = [CPArray arrayWithObjects:@"S", @"M", @"T", @"W", @"T", @"F", @"S"], - standaloneWeekdaySymbols = [CPArray arrayWithObjects:@"Sunday", @"Monday", @"Tuesday", @"Wednesday", @"Thursday", @"Friday", @"Saturday"], - shortStandaloneWeekdaySymbols = [CPArray arrayWithObjects:@"Sun", @"Mon", @"Tue", @"Wed", @"Thu", @"Fri", @"Sat"], - veryShortStandaloneWeekdaySymbols = [CPArray arrayWithObjects:@"S", @"M", @"T", @"W", @"T", @"F", @"S"], - monthSymbols = [CPArray arrayWithObjects:@"January", @"February", @"March", @"April", @"May", @"June", @"July", @"August", @"September", @"October", @"November", @"December"], - shortMonthSymbols = [CPArray arrayWithObjects:@"Jan", @"Feb", @"Mar", @"Apr", @"May", @"Jun", @"Jul", @"Aug", @"Sep", @"Oct", @"Nov", @"Dec"], - veryShortMonthSymbols = [CPArray arrayWithObjects:@"J", @"F", @"M", @"A", @"M", @"J", @"J", @"A", @"S", @"O", @"N", @"D"], - standaloneMonthSymbols = [CPArray arrayWithObjects:@"January", @"February", @"March", @"April", @"May", @"June", @"July", @"August", @"September", @"October", @"November", @"December"], - shortStandaloneMonthSymbols = [CPArray arrayWithObjects:@"Jan", @"Feb", @"Mar", @"Apr", @"May", @"Jun", @"Jul", @"Aug", @"Sep", @"Oct", @"Nov", @"Dec"], - veryShortStandaloneMonthSymbols = [CPArray arrayWithObjects:@"J", @"F", @"M", @"A", @"M", @"J", @"J", @"A", @"S", @"O", @"N", @"D"], - quarterSymbols = [CPArray arrayWithObjects:@"1st quarter", @"2nd quarter", @"3rd quarter", @"4th quarter"], - shortQuarterSymbols = [CPArray arrayWithObjects:@"Q1", @"Q2", @"Q3", @"Q4"], - standaloneQuarterSymbols = [CPArray arrayWithObjects:@"1st quarter", @"2nd quarter", @"3rd quarter", @"4th quarter"], - shortStandaloneQuarterSymbols = [CPArray arrayWithObjects:@"Q1", @"Q2", @"Q3", @"Q4"]; + PMSymbol = [CPString stringWithFormat:@"%s", @"PM"], + weekdaySymbols = [CPArray arrayWithObjects:@"Sunday", @"Monday", @"Tuesday", @"Wednesday", @"Thursday", @"Friday", @"Saturday"], + shortWeekdaySymbols = [CPArray arrayWithObjects:@"Sun", @"Mon", @"Tue", @"Wed", @"Thu", @"Fri", @"Sat"], + veryShortWeekdaySymbols = [CPArray arrayWithObjects:@"S", @"M", @"T", @"W", @"T", @"F", @"S"], + standaloneWeekdaySymbols = [CPArray arrayWithObjects:@"Sunday", @"Monday", @"Tuesday", @"Wednesday", @"Thursday", @"Friday", @"Saturday"], + shortStandaloneWeekdaySymbols = [CPArray arrayWithObjects:@"Sun", @"Mon", @"Tue", @"Wed", @"Thu", @"Fri", @"Sat"], + veryShortStandaloneWeekdaySymbols = [CPArray arrayWithObjects:@"S", @"M", @"T", @"W", @"T", @"F", @"S"], + monthSymbols = [CPArray arrayWithObjects:@"January", @"February", @"March", @"April", @"May", @"June", @"July", @"August", @"September", @"October", @"November", @"December"], + shortMonthSymbols = [CPArray arrayWithObjects:@"Jan", @"Feb", @"Mar", @"Apr", @"May", @"Jun", @"Jul", @"Aug", @"Sep", @"Oct", @"Nov", @"Dec"], + veryShortMonthSymbols = [CPArray arrayWithObjects:@"J", @"F", @"M", @"A", @"M", @"J", @"J", @"A", @"S", @"O", @"N", @"D"], + standaloneMonthSymbols = [CPArray arrayWithObjects:@"January", @"February", @"March", @"April", @"May", @"June", @"July", @"August", @"September", @"October", @"November", @"December"], + shortStandaloneMonthSymbols = [CPArray arrayWithObjects:@"Jan", @"Feb", @"Mar", @"Apr", @"May", @"Jun", @"Jul", @"Aug", @"Sep", @"Oct", @"Nov", @"Dec"], + veryShortStandaloneMonthSymbols = [CPArray arrayWithObjects:@"J", @"F", @"M", @"A", @"M", @"J", @"J", @"A", @"S", @"O", @"N", @"D"], + quarterSymbols = [CPArray arrayWithObjects:@"1st quarter", @"2nd quarter", @"3rd quarter", @"4th quarter"], + shortQuarterSymbols = [CPArray arrayWithObjects:@"Q1", @"Q2", @"Q3", @"Q4"], + standaloneQuarterSymbols = [CPArray arrayWithObjects:@"1st quarter", @"2nd quarter", @"3rd quarter", @"4th quarter"], + shortStandaloneQuarterSymbols = [CPArray arrayWithObjects:@"Q1", @"Q2", @"Q3", @"Q4"]; _symbols = @{ @"root" : @{ @@ -235,7 +236,7 @@ var _separatorsCharacterSet = nil; // MARK: Setter Getter Helper /*! Return symbols for the given language code -*/ + */ - (CPDictionary)symbolsForLanguageCode:(CPString)languageCode { var languageSymbols = [_symbols valueForKey:languageCode]; @@ -250,18 +251,18 @@ var _separatorsCharacterSet = nil; } /*! Set the symbols for the given language code -*/ + */ - (void)setSymbols:(CPDictionary)symbols forLanguageCode:(CPString)languageCode { [_symbols setValue:symbols forKey:languageCode]; } /*! Return a symbol specified by the given key for the given language code -*/ + */ - (id)symbolForKey:(CPString)aKey languageCode:(CPString)languageCode { var languageSymbols = [self symbolsForLanguageCode:languageCode], - symbol = [languageSymbols valueForKey:aKey]; + symbol = [languageSymbols valueForKey:aKey]; if (!symbol) { @@ -273,7 +274,7 @@ var _separatorsCharacterSet = nil; } /*! Set the symbol specified by the given key for the given language code -*/ + */ - (void)setSymbol:(CPString)aSymbol forKey:(CPString)aKey languageCode:(CPString)languageCode { var languageSymbols = [self symbolsForLanguageCode:languageCode]; @@ -284,252 +285,252 @@ var _separatorsCharacterSet = nil; // MARK: Setter Getter /*! Return AMSymbol -*/ + */ - (CPString)AMSymbol { return [self symbolForKey:@"AMSymbol" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Set the AMSymbol -*/ + */ - (void)setAMSymbol:(CPString)aValue { [self setSymbol:aValue forKey:@"AMSymbol" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Return a PMSymbol -*/ + */ - (CPString)PMSymbol { return [self symbolForKey:@"PMSymbol" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Set the PMSymbol -*/ + */ - (void)setPMSymbol:(CPString)aValue { [self setSymbol:aValue forKey:@"PMSymbol" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Return the weekdaySymbols -*/ + */ - (CPArray)weekdaySymbols { return [self symbolForKey:@"weekdaySymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Set the weekdaySymbols -*/ + */ - (void)setWeekdaySymbols:(CPArray)aValue { [self setSymbol:aValue forKey:@"weekdaySymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Return a shortWeekdaySymbols -*/ + */ - (CPArray)shortWeekdaySymbols { return [self symbolForKey:@"shortWeekdaySymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Set the shortWeekdaySymbols -*/ + */ - (void)setShortWeekdaySymbols:(CPArray)aValue { [self setSymbol:aValue forKey:@"shortWeekdaySymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Return veryShortWeekdaySymbols -*/ + */ - (CPArray)veryShortWeekdaySymbols { return [self symbolForKey:@"veryShortWeekdaySymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Set the veryShortWeekdaySymbols -*/ + */ - (void)setVeryShortWeekdaySymbols:(CPArray)aValue { [self setSymbol:aValue forKey:@"veryShortWeekdaySymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Return the standaloneWeekdaySymbols -*/ + */ - (CPArray)standaloneWeekdaySymbols { return [self symbolForKey:@"standaloneWeekdaySymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Set the standaloneWeekdaySymbols -*/ + */ - (void)setStandaloneWeekdaySymbols:(CPArray)aValue { [self setSymbol:aValue forKey:@"standaloneWeekdaySymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Return the shortStandaloneWeekdaySymbols -*/ + */ - (CPArray)shortStandaloneWeekdaySymbols { return [self symbolForKey:@"shortStandaloneWeekdaySymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Set the shortStandaloneWeekdaySymbols -*/ + */ - (void)setShortStandaloneWeekdaySymbols:(CPArray)aValue { [self setSymbol:aValue forKey:@"shortStandaloneWeekdaySymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Return the veryShortStandaloneWeekdaySymbols -*/ + */ - (CPArray)veryShortStandaloneWeekdaySymbols { return [self symbolForKey:@"veryShortStandaloneWeekdaySymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Set the veryShortStandaloneWeekdaySymbols -*/ + */ - (void)setVeryShortStandaloneWeekdaySymbols:(CPArray)aValue { [self setSymbol:aValue forKey:@"veryShortStandaloneWeekdaySymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Return the monthSymbols -*/ + */ - (CPArray)monthSymbols { return [self symbolForKey:@"monthSymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Set the monthSymbols -*/ + */ - (void)setMonthSymbols:(CPArray)aValue { [self setSymbol:aValue forKey:@"monthSymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Return a shortMonthSymbols -*/ + */ - (CPArray)shortMonthSymbols { return [self symbolForKey:@"shortMonthSymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Set the shortMonthSymbols -*/ + */ - (void)setShortMonthSymbols:(CPArray)aValue { [self setSymbol:aValue forKey:@"shortMonthSymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Return veryShortMonthSymbols -*/ + */ - (CPArray)veryShortMonthSymbols { return [self symbolForKey:@"veryShortMonthSymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Set the veryShortMonthSymbols -*/ + */ - (void)setVeryShortMonthSymbols:(CPArray)aValue { [self setSymbol:aValue forKey:@"veryShortMonthSymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Return standaloneMonthSymbols -*/ + */ - (CPArray)standaloneMonthSymbols { return [self symbolForKey:@"standaloneMonthSymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Set the standaloneMonthSymbols -*/ + */ - (void)setStandaloneMonthSymbols:(CPArray)aValue { [self setSymbol:aValue forKey:@"standaloneMonthSymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Return the shortStandaloneMonthSymbols -*/ + */ - (CPArray)shortStandaloneMonthSymbols { return [self symbolForKey:@"shortStandaloneMonthSymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Set the shortStandaloneMonthSymbols -*/ + */ - (void)setShortStandaloneMonthSymbols:(CPArray)aValue { [self setSymbol:aValue forKey:@"shortStandaloneMonthSymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Return the veryShortStandaloneMonthSymbols -*/ + */ - (CPArray)veryShortStandaloneMonthSymbols { return [self symbolForKey:@"veryShortStandaloneMonthSymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Set the veryShortStandaloneMonthSymbols -*/ + */ - (void)setVeryShortStandaloneMonthSymbols:(CPArray)aValue { [self setSymbol:aValue forKey:@"veryShortStandaloneMonthSymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Return the quarterSymbols -*/ + */ - (CPArray)quarterSymbols { return [self symbolForKey:@"quarterSymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Set the quarterSymbols -*/ + */ - (void)setQuarterSymbols:(CPArray)aValue { [self setSymbol:aValue forKey:@"quarterSymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Return the shortQuarterSymbols -*/ + */ - (CPArray)shortQuarterSymbols { return [self symbolForKey:@"shortQuarterSymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Set the shortQuarterSymbols -*/ + */ - (void)setShortQuarterSymbols:(CPArray)aValue { [self setSymbol:aValue forKey:@"shortQuarterSymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Return the standaloneQuarterSymbols -*/ + */ - (CPArray)standaloneQuarterSymbols { return [self symbolForKey:@"standaloneQuarterSymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Set the standaloneQuarterSymbols -*/ + */ - (void)setStandaloneQuarterSymbols:(CPArray)aValue { [self setSymbol:aValue forKey:@"standaloneQuarterSymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Return the shortStandaloneQuarterSymbols -*/ + */ - (CPArray)shortStandaloneQuarterSymbols { return [self symbolForKey:@"shortStandaloneQuarterSymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; } /*! Set the shortStandaloneQuarterSymbols -*/ + */ - (void)setShortStandaloneQuarterSymbols:(CPArray)aValue { [self setSymbol:aValue forKey:@"shortStandaloneQuarterSymbols" languageCode:[_locale objectForKey:CPLocaleLanguageCode]]; @@ -540,18 +541,18 @@ var _separatorsCharacterSet = nil; // MARK: StringFromDate methods /*! Return a string representation of a given date. - This method returns (if possible) a representation of the given date with the dateFormat of the CPDateFormatter, otherwise it takes the dateStyle and timeStyle - @param aDate the given date - @return CPString the string representation -*/ + This method returns (if possible) a representation of the given date with the dateFormat of the CPDateFormatter, otherwise it takes the dateStyle and timeStyle + @param aDate the given date + @return CPString the string representation + */ - (CPString)stringFromDate:(CPDate)aDate { var format, - relativeWord, - result; + relativeWord, + result; if (!aDate) - return; + return nil; aDate = [aDate copy]; [aDate _dateWithTimeZone:_timeZone]; @@ -611,7 +612,7 @@ var _separatorsCharacterSet = nil; if ([self doesRelativeDateFormatting]) { var language = [_locale objectForKey:CPLocaleLanguageCode], - relativeWords = [relativeDateFormating valueForKey:language]; + relativeWords = [relativeDateFormatting valueForKey:language]; for (var i = 1; i < [relativeWords count]; i = i + 2) { @@ -687,10 +688,10 @@ var _separatorsCharacterSet = nil; } /*! Return a string representation of the given objectValue. - This method call the method stringFromDate if possible, otherwise it returns the description of the object - @param anObject - @return a string -*/ + This method call the method stringFromDate if possible, otherwise it returns the description of the object + @param anObject + @return a string + */ - (CPString)stringForObjectValue:(id)anObject { if ([anObject isKindOfClass:[CPDate class]]) @@ -700,26 +701,26 @@ var _separatorsCharacterSet = nil; } /*! Return a string - This method call the method stringForObjectValue - @param anObject - @return a string -*/ + This method call the method stringForObjectValue + @param anObject + @return a string + */ - (CPString)editingStringForObjectValue:(id)anObject { return [self stringForObjectValue:anObject]; } /*! Return a string representation of the given date and format - @patam aDate - @param aFormat - @return a string -*/ + @patam aDate + @param aFormat + @return a string + */ - (CPString)_stringFromDate:(CPDate)aDate format:(CPString)aFormat { var length = [aFormat length], - currentToken = [CPString new], - isTextToken = NO, - result = [CPString new]; + currentToken = [CPString new], + isTextToken = NO, + result = [CPString new]; for (var i = 0; i < length; i++) { @@ -778,18 +779,18 @@ var _separatorsCharacterSet = nil; } /*! Return a string representation of the given token and date - @param aToken - @param aDate - @return a string -*/ + @param aToken + @param aDate + @return a string + */ - (CPString)_stringFromToken:(CPString)aToken date:(CPDate)aDate { if (![aToken length]) return aToken; var character = [aToken characterAtIndex:0], - length = [aToken length], - timeZone = _timeZone; + length = [aToken length], + timeZone = _timeZone; switch (character) { @@ -902,13 +903,13 @@ var _separatorsCharacterSet = nil; d.setDate(d.getDate() + 4 - (d.getDay() || 7)); var yearStart = new Date(d.getFullYear(), 0, 1), - weekOfYear = Math.ceil((((d - yearStart) / 86400000) + 1) / 7); + weekOfYear = Math.ceil((((d - yearStart) / 86400000) + 1) / 7); return [self _stringValueForValue:(weekOfYear + 1) length:MAX(2, length)]; case @"W": var firstDay = new Date(aDate.getFullYear(), aDate.getMonth(), 1).getDay(), - weekOfMonth = Math.ceil((aDate.getDate() + firstDay) / 7); + weekOfMonth = Math.ceil((aDate.getDate() + firstDay) / 7); return [self _stringValueForValue:weekOfMonth length:1]; @@ -919,14 +920,14 @@ var _separatorsCharacterSet = nil; case @"D": var oneJan = new Date(aDate.getFullYear(), 0, 1), - dayOfYear = Math.ceil((aDate - oneJan) / 86400000), - currentLength = [[CPString stringWithFormat:@"%i", dayOfYear] length]; + dayOfYear = Math.ceil((aDate - oneJan) / 86400000), + currentLength = [[CPString stringWithFormat:@"%i", dayOfYear] length]; return [self _stringValueForValue:dayOfYear length:MAX(currentLength, MIN(3, length))]; case @"F": var dayOfWeek = 1, - day = aDate.getDate(); + day = aDate.getDate(); if (day > 7 && day < 15) dayOfWeek = 2; @@ -1065,10 +1066,10 @@ var _separatorsCharacterSet = nil; case @"Z": var seconds = [timeZone secondsFromGMT], - minutes = seconds / 60, - hours = minutes / 60, - result, - diffMinutes = (hours - parseInt(hours)) * 100 * 60 / 100; + minutes = seconds / 60, + hours = minutes / 60, + result, + diffMinutes = (hours - parseInt(hours)) * 100 * 60 / 100; if (length <= 3) { @@ -1082,7 +1083,7 @@ var _separatorsCharacterSet = nil; while ([result length] < 4) result = @"0" + result; - if (seconds > 0) + if (seconds >= 0) result = @"+" + result; else result = @"-" + result; @@ -1164,10 +1165,10 @@ var _separatorsCharacterSet = nil; // MARK: datefromString /*! Return a date of the given string - This method returns (if possible) a representation of the given string with the dateFormat of the CPDateFormatter, otherwise it takes the dateStyle and timeStyle - @param aString - @return CPDate the date -*/ + This method returns (if possible) a representation of the given string with the dateFormat of the CPDateFormatter, otherwise it takes the dateStyle and timeStyle + @param aString + @return CPDate the date + */ - (CPDate)dateFromString:(CPString)aString { var format; @@ -1259,11 +1260,11 @@ var _separatorsCharacterSet = nil; } /*! Returns a boolean if the given object has been changed or not depending of the given string (use of ref) - @param anObject the given object - @param aString - @param anError, if it returns NO the describe error will be in anError (use of ref) - @return aBoolean for the success or fail of the method -*/ + @param anObject the given object + @param aString + @param anError, if it returns NO the describe error will be in anError (use of ref) + @return aBoolean for the success or fail of the method + */ - (BOOL)getObjectValue:(idRef)anObject forString:(CPString)aString errorDescription:(CPStringRef)anError { var value = [self dateFromString:aString]; @@ -1281,10 +1282,10 @@ var _separatorsCharacterSet = nil; } /*! Return a date representation of the given string and format - @patam aDate - @param aFormat - @return a string -*/ + @patam aDate + @param aFormat + @return a string + */ - (CPDate)_dateFromString:(CPString)aString format:(CPString)aFormat { // Interpret @"" as the date 2000-01-01 00:00:00 +0000, like in Cocoa. No idea why they picked this particular date. @@ -1307,10 +1308,10 @@ var _separatorsCharacterSet = nil; return nil; var currentToken = [CPString new], - isTextToken = NO, - tokens = [CPArray array], - dateComponents = [CPArray array], - patternTokens = [CPArray array]; + isTextToken = NO, + tokens = [CPArray array], + dateComponents = [CPArray array], + patternTokens = [CPArray array]; for (var i = 0; i < [aFormat length]; i++) { @@ -1431,19 +1432,19 @@ var _separatorsCharacterSet = nil; - (CPDate)_dateFromTokens:(CPArray)tokens dateComponents:(CPArray)dateComponents { var timeZoneseconds = [_timeZone secondsFromGMT], - dateArray = [2000, 01, 01, 00, 00, 00, @"+0000"], - isPM = NO, - dayOfYear, - dayIndexInWeek, - weekOfYear, - weekOfMonth; + dateArray = [2000, 1, 1, 0, 0, 0], + isPM = NO, + dayOfYear, + dayIndexInWeek, + weekOfYear, + weekOfMonth; for (var i = 0; i < [tokens count]; i++) { var token = [tokens objectAtIndex:i], - dateComponent = [dateComponents objectAtIndex:i], - character = [token characterAtIndex:0], - length = [token length]; + dateComponent = [dateComponents objectAtIndex:i], + character = [token characterAtIndex:0], + length = [token length]; switch (character) { @@ -1454,9 +1455,9 @@ var _separatorsCharacterSet = nil; case @"y": var u = _twoDigitStartDate.getFullYear() % 10, - d = parseInt(_twoDigitStartDate.getFullYear() / 10) % 10, - c = parseInt(_twoDigitStartDate.getFullYear() / 100) % 10, - m = parseInt(_twoDigitStartDate.getFullYear() / 1000) % 10; + d = parseInt(_twoDigitStartDate.getFullYear() / 10) % 10, + c = parseInt(_twoDigitStartDate.getFullYear() / 100) % 10, + m = parseInt(_twoDigitStartDate.getFullYear() / 1000) % 10; if (length == 2 && dateComponent.length == 2) { @@ -1474,9 +1475,9 @@ var _separatorsCharacterSet = nil; case @"Y": var u = _twoDigitStartDate.getFullYear() % 10, - d = parseInt(_twoDigitStartDate.getFullYear() / 10) % 10, - c = parseInt(_twoDigitStartDate.getFullYear() / 100) % 10, - m = parseInt(_twoDigitStartDate.getFullYear() / 1000) % 10; + d = parseInt(_twoDigitStartDate.getFullYear() / 10) % 10, + c = parseInt(_twoDigitStartDate.getFullYear() / 100) % 10, + m = parseInt(_twoDigitStartDate.getFullYear() / 1000) % 10; if (length == 2 && dateComponent.length == 2) { @@ -1790,7 +1791,7 @@ var _separatorsCharacterSet = nil; return nil; var millisecondsInDay = parseInt(dateComponent), - tmpDate = new Date(); + tmpDate = new Date(); tmpDate.setHours(0); tmpDate.setMinutes(0); @@ -1816,8 +1817,6 @@ var _separatorsCharacterSet = nil; if (!timeZoneseconds) return nil; - timeZoneseconds = timeZoneseconds + 60 * 60; - break; case @"Z": @@ -1826,8 +1825,6 @@ var _separatorsCharacterSet = nil; if (!timeZoneseconds) return nil; - timeZoneseconds = timeZoneseconds + 60 * 60; - break; case @"v": @@ -1842,8 +1839,6 @@ var _separatorsCharacterSet = nil; if (!timeZoneseconds) return nil; - timeZoneseconds = timeZoneseconds + 60 * 60; - break; case @"V": @@ -1858,8 +1853,6 @@ var _separatorsCharacterSet = nil; if (!timeZoneseconds) return nil; - timeZoneseconds = timeZoneseconds + 60 * 60; - break; default: @@ -1912,11 +1905,15 @@ var _separatorsCharacterSet = nil; if (isPM) dateArray[3] += 12; - if (isNaN(parseInt(dateArray[0])) || isNaN(parseInt(dateArray[1])) || isNaN(parseInt(dateArray[2])) || isNaN(parseInt(dateArray[3])) || isNaN(parseInt(dateArray[4])) || isNaN(parseInt(dateArray[5])) || isNaN(parseInt(dateArray[6]))) + if (isNaN(parseInt(dateArray[0])) || isNaN(parseInt(dateArray[1])) || isNaN(parseInt(dateArray[2])) || isNaN(parseInt(dateArray[3])) || isNaN(parseInt(dateArray[4])) || isNaN(parseInt(dateArray[5]))) return nil; - var dateResult = [[CPDate alloc] initWithString:[CPString stringWithFormat:@"%04d-%02d-%02d %02d:%02d:%02d %s", dateArray[0], dateArray[1], dateArray[2], dateArray[3], dateArray[4], dateArray[5], dateArray[6]]]; - dateResult.setSeconds(dateResult.getSeconds() - timeZoneseconds + 60 * 60); + var utcTimestamp = Date.UTC(dateArray[0], dateArray[1] - 1, dateArray[2], dateArray[3], dateArray[4], dateArray[5]), + kCFAbsoluteTimeIntervalSince1970 = 978307200.0, + interval = (utcTimestamp / 1000.0) - kCFAbsoluteTimeIntervalSince1970, + dateResult = [CPDate dateWithTimeIntervalSinceReferenceDate:interval]; + + dateResult.setSeconds(dateResult.getSeconds() - timeZoneseconds); return dateResult; } @@ -1942,29 +1939,29 @@ var _separatorsCharacterSet = nil; } /*! Check if we are in the american format or not. Depending on the locale -*/ + */ - (BOOL)_isAmericanFormat { return [[_locale objectForKey:CPLocaleCountryCode] isEqualToString:@"US"]; } /*! Check if we are in the english format or not. Depending on the locale -*/ + */ - (BOOL)_isEnglishFormat { return [[_locale objectForKey:CPLocaleLanguageCode] isEqualToString:@"en"]; } /*! Returns the number of second from a time zone (-8000 or HGP-8:35 or GMT-08:00) -*/ + */ - (int)_secondsFromTimeZoneDefaultFormatString:(CPString)aTimeZoneFormatString { var format = new RegExp("\\w*([HPG-GMT])?([+-])(\\d{1,2})([:])?(\\d{2})\\w*"), - result = aTimeZoneFormatString.match(new RegExp(format)), - seconds = 0; + result = aTimeZoneFormatString.match(new RegExp(format)), + seconds = 0; if (!result) - return nil; + return 0; seconds = result[3] * 60 * 60 + result[5] * 60; @@ -1975,30 +1972,30 @@ var _separatorsCharacterSet = nil; } /*! Return the number of seconds from a timeZoneString -*/ + */ - (int)_secondsFromTimeZoneString:(CPString)aTimeZoneString style:(NSTimeZoneNameStyle)aStyle { var timeZone = [CPTimeZone _timeZoneFromString:aTimeZoneString style:aStyle locale:_locale]; if (!timeZone) - return nil; + return 0; return [timeZone secondsFromGMT]; } /*! This method is used to know if the given string match with the token. - @param aString - @param aToken - @param anIndex the current index in the string - @return an index who describes the position of the end of the word for the token -*/ + @param aString + @param aToken + @param anIndex the current index in the string + @return an index who describes the position of the end of the word for the token + */ - (int)_lastIndexMatchedString:(CPString)aString token:(CPString)aToken index:anIndex { var character = [aToken characterAtIndex:0], - length = [aToken length], - targetedArray, - format = new RegExp("\\w*([HPG-GMT])?([+-])(\\d{1,2})([:])?(\\d{2})\\w*"), - result = aString.match(new RegExp(format)); + length = [aToken length], + targetedArray, + format = new RegExp("\\w*([HPG-GMT])?([+-])(\\d{1,2})([:])?(\\d{2})\\w*"), + result = aString.match(new RegExp(format)); switch (character) { @@ -2129,7 +2126,7 @@ var _separatorsCharacterSet = nil; for (var i = 0; i < [targetedArray count]; i++) { var currentObject = [targetedArray objectAtIndex:i], - range = [aString rangeOfString:currentObject]; + range = [aString rangeOfString:currentObject]; if (range.length == 0) continue; @@ -2146,12 +2143,12 @@ var _separatorsCharacterSet = nil; @end var CPDateFormatterDateStyleKey = @"CPDateFormatterDateStyle", - CPDateFormatterTimeStyleKey = @"CPDateFormatterTimeStyleKey", - CPDateFormatterFormatterBehaviorKey = @"CPDateFormatterFormatterBehaviorKey", - CPDateFormatterDoseRelativeDateFormattingKey = @"CPDateFormatterDoseRelativeDateFormattingKey", - CPDateFormatterDateFormatKey = @"CPDateFormatterDateFormatKey", - CPDateFormatterAllowNaturalLanguageKey = @"CPDateFormatterAllowNaturalLanguageKey", - CPDateFormatterLocaleKey = @"CPDateFormatterLocaleKey"; +CPDateFormatterTimeStyleKey = @"CPDateFormatterTimeStyleKey", +CPDateFormatterFormatterBehaviorKey = @"CPDateFormatterFormatterBehaviorKey", +CPDateFormatterDoseRelativeDateFormattingKey = @"CPDateFormatterDoseRelativeDateFormattingKey", +CPDateFormatterDateFormatKey = @"CPDateFormatterDateFormatKey", +CPDateFormatterAllowNaturalLanguageKey = @"CPDateFormatterAllowNaturalLanguageKey", +CPDateFormatterLocaleKey = @"CPDateFormatterLocaleKey"; @implementation CPDateFormatter (CPCoding) @@ -2194,14 +2191,17 @@ var CPDateFormatterDateStyleKey = @"CPDateFormatterDateStyle", @implementation CPDate (CPTimeZone) /*! Convert a date from a timeZone -*/ + */ - (void)_dateWithTimeZone:(CPTimeZone)aTimeZone { if (!aTimeZone) return; - self.setSeconds(self.getSeconds() - [aTimeZone secondsFromGMTForDate:self]); - self.setSeconds(self.getSeconds() + [aTimeZone secondsFromGMT]); + var targetOffset = [aTimeZone secondsFromGMTForDate:self], + localOffset = - (self.getTimezoneOffset() * 60), + offsetDifference = targetOffset - localOffset; + + self.setSeconds(self.getSeconds() + offsetDifference); } @end diff --git a/Tests/Foundation/CPDateFormatterTest.j b/Tests/Foundation/CPDateFormatterTest.j index 2f88d1c96..f923593f1 100644 --- a/Tests/Foundation/CPDateFormatterTest.j +++ b/Tests/Foundation/CPDateFormatterTest.j @@ -1,1462 +1,67 @@ -/* CPDateFormatterTest.j -* Foundation -* -* Created by Alexandre Wilhelm -* Copyright 2012 -* -* 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 @import @import -@import - -@import - -@global CPDateFormatterNoStyle -@global CPDateFormatterShortStyle -@global CPDateFormatterMediumStyle -@global CPDateFormatterLongStyle -@global CPDateFormatterFullStyle +@import @implementation CPDateFormatterTest : OJTestCase { - CPDate _date; - CPDateFormatter _dateFormatter; - CPDateFormatter _nonEnDateFormatter; + CPDateFormatter _formatter; + CPTimeZone _utcTimeZone; + CPLocale _posixLocale; } - (void)setUp { - _date = [[CPDate alloc] initWithString:@"2011-10-05 16:34:38 -0900"]; - _dateFormatter = [[CPDateFormatter alloc] init]; - [_dateFormatter setDateStyle:CPDateFormatterMediumStyle]; - [_dateFormatter setTimeStyle:CPDateFormatterShortStyle]; - [_dateFormatter setLocale:[[CPLocale alloc] initWithLocaleIdentifier:@"en_US"]]; - [_dateFormatter setTimeZone:[CPTimeZone timeZoneWithAbbreviation:@"PDT"]]; + _formatter = [[CPDateFormatter alloc] init]; - _nonEnDateFormatter = [[CPDateFormatter alloc] init]; - [_nonEnDateFormatter setLocale:[[CPLocale alloc] initWithLocaleIdentifier:@"pt"]]; -} - - -// MARK: - -// MARK: Setter - -- (void)testSetterAMSymbol -{ - [_dateFormatter setAMSymbol:@"Hej hej"]; - [self assert:[_dateFormatter AMSymbol] equals:@"Hej hej"]; -} - -- (void)testSetterPMSymbol -{ - [_dateFormatter setPMSymbol:@"Hej hej"]; - [self assert:[_dateFormatter PMSymbol] equals:@"Hej hej"]; -} - -- (void)testSetterWeekdaySymbols -{ - [_dateFormatter setWeekdaySymbols:[@"Hej hej"]]; - [self assert:[_dateFormatter weekdaySymbols] equals:[@"Hej hej"]]; -} - -- (void)testSetterShortWeekdaySymbols -{ - [_dateFormatter setShortWeekdaySymbols:[@"Hej hej"]]; - [self assert:[_dateFormatter shortWeekdaySymbols] equals:[@"Hej hej"]]; -} - -- (void)testSetterVeryShortWeekdaySymbols -{ - [_dateFormatter setVeryShortWeekdaySymbols:[@"Hej hej"]]; - [self assert:[_dateFormatter veryShortWeekdaySymbols] equals:[@"Hej hej"]]; -} - -- (void)testSetterStandaloneWeekdaySymbols -{ - [_dateFormatter setStandaloneWeekdaySymbols:[@"Hej hej"]]; - [self assert:[_dateFormatter standaloneWeekdaySymbols] equals:[@"Hej hej"]]; -} - -- (void)testSetterShortStandaloneWeekdaySymbols -{ - [_dateFormatter setShortStandaloneWeekdaySymbols:[@"Hej hej"]]; - [self assert:[_dateFormatter shortStandaloneWeekdaySymbols] equals:[@"Hej hej"]]; -} - -- (void)testSetterVeryShortStandaloneWeekdaySymbols -{ - [_dateFormatter setVeryShortStandaloneWeekdaySymbols:[@"Hej hej"]]; - [self assert:[_dateFormatter veryShortStandaloneWeekdaySymbols] equals:[@"Hej hej"]]; -} - -- (void)testSetterMonthSymbols -{ - [_dateFormatter setMonthSymbols:[@"Hej hej"]]; - [self assert:[_dateFormatter monthSymbols] equals:[@"Hej hej"]]; -} - -- (void)testSetterShortMonthSymbols -{ - [_dateFormatter setShortMonthSymbols:[@"Hej hej"]]; - [self assert:[_dateFormatter shortMonthSymbols] equals:[@"Hej hej"]]; -} - -- (void)testSetterVeryShortMonthSymbols -{ - [_dateFormatter setVeryShortMonthSymbols:[@"Hej hej"]]; - [self assert:[_dateFormatter veryShortMonthSymbols] equals:[@"Hej hej"]]; -} - -- (void)testSetterStandaloneMonthSymbols -{ - [_dateFormatter setStandaloneMonthSymbols:[@"Hej hej"]]; - [self assert:[_dateFormatter standaloneMonthSymbols] equals:[@"Hej hej"]]; -} - -- (void)testSetterShortStandaloneMonthSymbols -{ - [_dateFormatter setShortStandaloneMonthSymbols:[@"Hej hej"]]; - [self assert:[_dateFormatter shortStandaloneMonthSymbols] equals:[@"Hej hej"]]; -} - -- (void)testSetterVeryShortStandaloneMonthSymbols -{ - [_dateFormatter setVeryShortStandaloneMonthSymbols:[@"Hej hej"]]; - [self assert:[_dateFormatter veryShortStandaloneMonthSymbols] equals:[@"Hej hej"]]; -} - -- (void)testSetterQuarterSymbols -{ - [_dateFormatter setQuarterSymbols:[@"Hej hej"]]; - [self assert:[_dateFormatter quarterSymbols] equals:[@"Hej hej"]]; -} - -- (void)testSetterShortQuarterSymbols -{ - [_dateFormatter setShortQuarterSymbols:[@"Hej hej"]]; - [self assert:[_dateFormatter shortQuarterSymbols] equals:[@"Hej hej"]]; -} - -- (void)testSetterStandaloneQuarterSymbols -{ - [_dateFormatter setStandaloneQuarterSymbols:[@"Hej hej"]]; - [self assert:[_dateFormatter standaloneQuarterSymbols] equals:[@"Hej hej"]]; -} - -- (void)testSetterShortStandaloneQuarterSymbols -{ - [_dateFormatter setShortStandaloneQuarterSymbols:[@"Hej hej"]]; - [self assert:[_dateFormatter shortStandaloneQuarterSymbols] equals:[@"Hej hej"]]; -} - -// MARK: - -// MARK: string from date - -- (void)testLocalizedStringFromDate -{ - var result = [CPDateFormatter localizedStringFromDate:[[CPDate alloc] initWithString:@"2011-10-05 23:59:59 +1200"] dateStyle:CPDateFormatterMediumStyle timeStyle:CPDateFormatterNoStyle]; - - if (![[[CPLocale currentLocale] objectForKey:CPLocaleCountryCode] isEqualToString:@"US"]) - [self assert:result equals:@"5 Oct 2011"]; - else - [self assert:result equals:@"Oct 5, 2011"]; -} - -- (void)testInit -{ - var dateFormatter = [[CPDateFormatter alloc] init], - result = [dateFormatter stringFromDate:_date]; - - [self assert:result.length equals:@"".length]; -} - -- (void)testInitWithDateFormat -{ - var dateFormatter = [[CPDateFormatter alloc] initWithDateFormat:@"d EEEE, MMM, Y 'at' H:mm:ss a z" allowNaturalLanguage:NO]; - [dateFormatter setLocale:[[CPLocale alloc] initWithLocaleIdentifier:@"en_US"]]; - [dateFormatter setTimeZone:[CPTimeZone timeZoneWithAbbreviation:@"PDT"]]; - - var result = [dateFormatter stringFromDate:_date]; - - [self assert:result equals:@"5 Wednesday, Oct, 2011 at 18:34:38 PM PDT"] -} - -- (void)testStringFromDateDateNoStyleTimeNoStyle -{ - [_dateFormatter setDateStyle:CPDateFormatterNoStyle]; - [_dateFormatter setTimeStyle:CPDateFormatterNoStyle]; - - var result = [_dateFormatter stringFromDate:_date]; - [self assert:result.length equals:@"".length]; -} - -- (void)testStringFromDateDateShortStyleTimeNoStyle -{ - [_dateFormatter setDateStyle:CPDateFormatterShortStyle]; - [_dateFormatter setTimeStyle:CPDateFormatterNoStyle]; - - var result = [_dateFormatter stringFromDate:_date]; - [self assert:result equals:@"10/5/11"]; -} - -- (void)testStringFromDateDateMediumStyleTimeNoStyle -{ - [_dateFormatter setDateStyle:CPDateFormatterMediumStyle]; - [_dateFormatter setTimeStyle:CPDateFormatterNoStyle]; - - var result = [_dateFormatter stringFromDate:_date]; - [self assert:result equals:@"Oct 5, 2011"]; -} - -- (void)testStringFromDateDateLongStyleTimeNoStyle -{ - [_dateFormatter setDateStyle:CPDateFormatterLongStyle]; - [_dateFormatter setTimeStyle:CPDateFormatterNoStyle]; - - var result = [_dateFormatter stringFromDate:_date]; - [self assert:result equals:@"October 5, 2011"]; -} - -- (void)testStringFromDateDateFullStyleTimeNoStyle -{ - [_dateFormatter setDateStyle:CPDateFormatterFullStyle]; - [_dateFormatter setTimeStyle:CPDateFormatterNoStyle]; - - var result = [_dateFormatter stringFromDate:_date]; - [self assert:result equals:@"Wednesday, October 5, 2011"]; -} - -- (void)testStringFromDateDateNoStyleTimeShortStyle -{ - [_dateFormatter setDateStyle:CPDateFormatterNoStyle]; - [_dateFormatter setTimeStyle:CPDateFormatterShortStyle]; - - var result = [_dateFormatter stringFromDate:_date]; - [self assert:result equals:@"6:34 PM"]; -} - -- (void)testStringFromDateDateNoStyleTimeMediumStyle -{ - [_dateFormatter setDateStyle:CPDateFormatterNoStyle]; - [_dateFormatter setTimeStyle:CPDateFormatterMediumStyle]; - - var result = [_dateFormatter stringFromDate:_date]; - [self assert:result equals:@"6:34:38 PM"]; -} - -- (void)testStringFromDateDateNoStyleTimeLongStyle -{ - [_dateFormatter setDateStyle:CPDateFormatterNoStyle]; - [_dateFormatter setTimeStyle:CPDateFormatterLongStyle]; - - var result = [_dateFormatter stringFromDate:_date]; - [self assert:result equals:@"6:34:38 PM PDT"]; -} - -- (void)testStringFromDateDateNoStyleTimeFullStyle -{ - [_dateFormatter setDateStyle:CPDateFormatterNoStyle]; - [_dateFormatter setTimeStyle:CPDateFormatterFullStyle]; - - var result = [_dateFormatter stringFromDate:_date]; - [self assert:result equals:@"6:34:38 PM Pacific Daylight Time"]; -} - -- (void)testStringFromDateDateFullStyleTimeFullStyle -{ - [_dateFormatter setDateStyle:CPDateFormatterFullStyle]; - [_dateFormatter setTimeStyle:CPDateFormatterFullStyle]; - - var result = [_dateFormatter stringFromDate:_date]; - [self assert:result equals:@"Wednesday, October 5, 2011 6:34:38 PM Pacific Daylight Time"]; -} - -- (void)testStringForObjectValueWithDate -{ - [_dateFormatter setDateStyle:CPDateFormatterMediumStyle]; - [_dateFormatter setTimeStyle:CPDateFormatterShortStyle]; - - var result = [_dateFormatter stringForObjectValue:_date]; - [self assert:result equals:@"Oct 5, 2011 6:34 PM"]; -} - -- (void)testStringForObjectValueWithString -{ - [_dateFormatter setDateStyle:CPDateFormatterMediumStyle]; - [_dateFormatter setTimeStyle:CPDateFormatterShortStyle]; - - var result = [_dateFormatter stringForObjectValue:@"Test String"]; - [self assert:result equals:nil]; -} - -- (void)testEditingStringForObjectValueWithDate -{ - [_dateFormatter setDateStyle:CPDateFormatterMediumStyle]; - [_dateFormatter setTimeStyle:CPDateFormatterShortStyle]; - - var result = [_dateFormatter editingStringForObjectValue:_date]; - [self assert:result equals:@"Oct 5, 2011 6:34 PM"]; -} - -- (void)testDoesRelativeDateFormatting -{ - [_dateFormatter setTimeStyle:CPDateFormatterNoStyle]; - [_dateFormatter setDoesRelativeDateFormatting:YES]; - - var date = [CPDate date]; - date.setDate(date.getDate() + 1); - - var result = [_dateFormatter editingStringForObjectValue:date]; - [self assert:result equals:@"tomorrow"]; - - date.setDate(date.getDate() - 2); - - result = [_dateFormatter editingStringForObjectValue:date]; - [self assert:result equals:@"yesterday"]; -} - -- (void)testStringFromDateTokensYears -{ - [_dateFormatter setDateFormat:@"y yy yyy yyyy Y YY YYY YYYY"]; - - var result = [_dateFormatter stringFromDate:_date]; - - [self assert:result equals:@"2011 11 2011 2011 2011 11 2011 2011"]; -} - -- (void)testStringFromDateTokensQuarters -{ - [_dateFormatter setDateFormat:@"Q QQ QQQ QQQQ q qq qqq qqqq"]; - - var result = [_dateFormatter stringFromDate:_date]; - - [self assert:result equals:@"4 04 Q4 4th quarter 4 04 Q4 4th quarter"]; -} - -- (void)testStringFromDateTokensMonths -{ - [_dateFormatter setDateFormat:@"M MM MMM MMMM MMMMM L LL LLL LLLL LLLLL"]; - - var result = [_dateFormatter stringFromDate:_date]; - - [self assert:result equals:@"10 10 Oct October O 10 10 Oct October O"]; -} - -- (void)testStringFromDateTokensWeeks -{ - [_dateFormatter setDateFormat:@"w ww W"]; - - var result = [_dateFormatter stringFromDate:_date]; - - [self assert:result equals:@"41 41 2"]; -} - -- (void)testStringFromDateTokensDays -{ - [_dateFormatter setDateFormat:@"d dd D DD DDD F"]; - - var result = [_dateFormatter stringFromDate:_date]; - - [self assert:result equals:@"5 05 278 278 278 1"]; -} - -- (void)testStringFromDateTokensWeekDays -{ - [_dateFormatter setDateFormat:@"E EE EEE EEEE EEEEE e ee eee eeee eeeee c cc ccc cccc ccccc"]; - - var result = [_dateFormatter stringFromDate:_date]; - - [self assert:result equals:@"Wed Wed Wed Wednesday W 4 04 Wed Wednesday W 4 4 Wed Wednesday W"]; -} - -- (void)testStringFromDateTokensPeriods -{ - [_dateFormatter setDateFormat:@"a"]; - - var result = [_dateFormatter stringFromDate:_date]; - - [self assert:result equals:@"PM"]; - - _date = [[CPDate alloc] initWithString:@"2011-10-05 05:34:38 -0900"]; - - var result = [_dateFormatter stringFromDate:_date]; - [self assert:result equals:@"AM"]; -} - -- (void)testStringFromDateTokensSeconds -{ - [_dateFormatter setDateFormat:@"s ss S SS SSS SSSS A AA AAA AAAA"]; - _date.setSeconds(8); - - var result = [_dateFormatter stringFromDate:_date]; - - [self assert:result equals:@"8 08 0 00 000 0000 66848000 66848000 66848000 66848000"]; -} - -- (void)testStringFromDateTokensMinutes -{ - [_dateFormatter setDateFormat:@"m mm"]; - _date.setMinutes(5); - - var result = [_dateFormatter stringFromDate:_date]; - - [self assert:result equals:@"5 05"]; -} - -- (void)testStringFromDateTokensHours -{ - var date = [[CPDate alloc] initWithString:@"2011-10-05 22:34:08 -0900"]; - - [_dateFormatter setDateFormat:@"h hh HH k kk K KK"]; - var result = [_dateFormatter stringFromDate:date]; - [self assert:result equals:@"12 12 00 24 24 0 00"]; - - date = [[CPDate alloc] initWithString:@"2011-10-05 06:34:08 -0900"]; - [_dateFormatter setDateFormat:@"h hh HH k kk K KK"]; - result = [_dateFormatter stringFromDate:date]; - [self assert:result equals:@"8 08 08 8 08 8 08"]; - - date = [[CPDate alloc] initWithString:@"2011-10-05 16:34:08 -0900"]; - [_dateFormatter setDateFormat:@"h hh HH k kk K KK"]; - var result = [_dateFormatter stringFromDate:date]; - [self assert:result equals:@"6 06 18 18 18 6 06"]; -} - -- (void)testStringFromDateTokensZones -{ - [_dateFormatter setDateFormat:@"z zz zzz zzzz Z ZZ ZZZ ZZZZ ZZZZ v vvvv V"]; - - var result = [_dateFormatter stringFromDate:_date]; + /* + * FALLACY: Relying on the host environment's default locale for localized string assertions. + * FACT: System locales change based on user preference or OS updates. + * SOLUTION: Enforce an immutable POSIX locale for all deterministic string generation. + */ + _posixLocale = [[CPLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; + [_formatter setLocale:_posixLocale]; - [self assert:result equals:@"PDT PDT PDT Pacific Daylight Time -0700 -0700 -0700 GMT-07:00 GMT-07:00 PT Pacific Time PDT"]; + /* + * FALLACY: Assuming named time zones (e.g., "PST" or "America/Los_Angeles") are stable. + * FACT: Geopolitical entities alter time zone boundaries and DST definitions continually. + * SOLUTION: Isolate tests using absolute GMT/UTC offsets. Mathematical seconds from GMT do not drift. + */ + _utcTimeZone = [CPTimeZone timeZoneForSecondsFromGMT:0]; + [_formatter setTimeZone:_utcTimeZone]; } - -// MARK: - -// MARK: Date From string - -- (void)testDateFromStringToken -{ - [_dateFormatter setDateFormat:@""]; - var result = [_dateFormatter dateFromString:@""]; - - [self assert:[result isEqualToDate:[[CPDate alloc] initWithString:@"2000-01-01 00:00:00 +0000"]] equals:YES]; -} - -- (void)testDateFromStringTokeny -{ - [_dateFormatter setDateFormat:@"y"]; - var result = [_dateFormatter dateFromString:@"9"]; - [self assert:[result isEqualToDate:[[CPDate alloc] initWithString:@"0009-01-01 08:00:00 +0000"]] equals:YES]; - - [_dateFormatter setDateFormat:@"yy"]; - result = [_dateFormatter dateFromString:@"49"]; - [self assert:[result isEqualToDate:[[CPDate alloc] initWithString:@"2049-01-01 08:00:00 +0000"]] equals:YES]; - - [_dateFormatter setDateFormat:@"yy"]; - result = [_dateFormatter dateFromString:@"56"]; - [self assert:[result isEqualToDate:[[CPDate alloc] initWithString:@"1956-01-01 08:00:00 +0000"]] equals:YES]; - - [_dateFormatter setDateFormat:@"yy"]; - result = [_dateFormatter dateFromString:@"563"]; - [self assert:[result isEqualToDate:[[CPDate alloc] initWithString:@"0563-01-01 08:00:00 +0000"]] equals:YES]; - - [_dateFormatter setDateFormat:@"yyy"]; - result = [_dateFormatter dateFromString:@"563"]; - [self assert:[result isEqualToDate:[[CPDate alloc] initWithString:@"0563-01-01 08:00:00 +0000"]] equals:YES]; - - [_dateFormatter setDateFormat:@"yyyy"]; - result = [_dateFormatter dateFromString:@"2012"]; - [self assert:[result isEqualToDate:[[CPDate alloc] initWithString:@"2012-01-01 08:00:00 +0000"]] equals:YES]; - - [_dateFormatter setDateFormat:@"y"]; - var result = [_dateFormatter dateFromString:@"eze"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"yy"]; - result = [_dateFormatter dateFromString:@"dezd"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"yyy"]; - result = [_dateFormatter dateFromString:@"dezdez"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"yyyy"]; - result = [_dateFormatter dateFromString:@"dezdezd"]; - [self assert:result equals:nil]; -} - -- (void)testDateFromStringTokenY -{ - [_dateFormatter setDateFormat:@"Y"]; - var result = [_dateFormatter dateFromString:@"9"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"0009-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"YY"]; - result = [_dateFormatter dateFromString:@"49"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2049-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"YY"]; - result = [_dateFormatter dateFromString:@"56"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"1956-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"YY"]; - result = [_dateFormatter dateFromString:@"563"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"0563-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"YYY"]; - result = [_dateFormatter dateFromString:@"563"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"0563-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"YYYY"]; - result = [_dateFormatter dateFromString:@"2012"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2012-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"Y"]; - var result = [_dateFormatter dateFromString:@"eze"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"YY"]; - result = [_dateFormatter dateFromString:@"dezd"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"YYY"]; - result = [_dateFormatter dateFromString:@"dezdez"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"YYYY"]; - result = [_dateFormatter dateFromString:@"dezdezd"]; - [self assert:result equals:nil]; -} - -- (void)testDateFromStringTokenq -{ - [_dateFormatter setDateFormat:@"q"]; - var result = [_dateFormatter dateFromString:@"2"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-04-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"qq"]; - var result = [_dateFormatter dateFromString:@"2"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-04-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"qqq"]; - var result = [_dateFormatter dateFromString:@"Q3"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-07-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"qqqq"]; - var result = [_dateFormatter dateFromString:@"2nd quarter"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-04-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"q"]; - var result = [_dateFormatter dateFromString:@"12"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"q"]; - var result = [_dateFormatter dateFromString:@"eze"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"qq"]; - var result = [_dateFormatter dateFromString:@"12"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"qq"]; - var result = [_dateFormatter dateFromString:@"zaz"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"qqq"]; - var result = [_dateFormatter dateFromString:@"Q8"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"qqqq"]; - var result = [_dateFormatter dateFromString:@"2nd quarteer"]; - [self assert:result equals:nil]; -} - -- (void)testDateFromStringTokenQ +- (void)testFormatterDeterministicUTCOutput { - [_dateFormatter setDateFormat:@"Q"]; - var result = [_dateFormatter dateFromString:@"2"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-04-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"QQ"]; - var result = [_dateFormatter dateFromString:@"2"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-04-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"QQQ"]; - var result = [_dateFormatter dateFromString:@"Q3"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-07-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"QQQQ"]; - var result = [_dateFormatter dateFromString:@"2nd quarter"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-04-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"Q"]; - var result = [_dateFormatter dateFromString:@"12"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"Q"]; - var result = [_dateFormatter dateFromString:@"ezeze"]; - [self assert:result equals:nil]; + /* + * FALLACY: Parsing a string to generate a test date evaluates the formatter fairly. + * FACT: String parsing introduces external dependencies and compounds parser bugs with formatter bugs. + * SOLUTION: Initialize dates using absolute epoch seconds. 1,000,000,000 is invariably 2001-09-09 01:46:40 UTC. + */ + var absoluteDate = [CPDate dateWithTimeIntervalSince1970:1000000000]; - [_dateFormatter setDateFormat:@"QQ"]; - var result = [_dateFormatter dateFromString:@"12"]; - [self assert:result equals:nil]; + [_formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss Z"]; + var formattedString = [_formatter stringFromDate:absoluteDate]; - [_dateFormatter setDateFormat:@"QQ"]; - var result = [_dateFormatter dateFromString:@"zazasz"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"QQQ"]; - var result = [_dateFormatter dateFromString:@"Q6"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"QQQQ"]; - var result = [_dateFormatter dateFromString:@"2nd quarteereze"]; - [self assert:result equals:nil]; + [self assert:formattedString + equals:@"2001-09-09 01:46:40 +0000" + message:@"Formatter output must match fixed UTC temporal baseline."]; } -- (void)testDateFromStringTokenM +- (void)testFormatterFixedOffsetOutput { - [_dateFormatter setDateFormat:@"M"]; - var result = [_dateFormatter dateFromString:@"10"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-10-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"M"]; - var result = [_dateFormatter dateFromString:@"1"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"M"]; - var result = [_dateFormatter dateFromString:@"12"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-12-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"MM"]; - var result = [_dateFormatter dateFromString:@"7"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-07-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"MM"]; - var result = [_dateFormatter dateFromString:@"1"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"MM"]; - var result = [_dateFormatter dateFromString:@"12"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-12-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"MMM"]; - var result = [_dateFormatter dateFromString:@"Sep"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-09-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"MMM"]; - var result = [_dateFormatter dateFromString:@"Jan"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"MMM"]; - var result = [_dateFormatter dateFromString:@"Dec"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-12-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"MMMM"]; - var result = [_dateFormatter dateFromString:@"September"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-09-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"MMMM"]; - var result = [_dateFormatter dateFromString:@"December"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-12-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"MMMM"]; - var result = [_dateFormatter dateFromString:@"January"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"MMMMM"]; - var result = [_dateFormatter dateFromString:@"S"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"M"]; - var result = [_dateFormatter dateFromString:@"76"]; - [self assert:result equals:nil]; + // Validate that the formatter accurately shifts absolute time to a localized fixed offset. + // We apply a strict mathematical offset (-25200 seconds) rather than a named geographical zone. + var offsetTimeZone = [CPTimeZone timeZoneForSecondsFromGMT:-25200]; // -0700 + [_formatter setTimeZone:offsetTimeZone]; + [_formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss Z"]; - [_dateFormatter setDateFormat:@"M"]; - var result = [_dateFormatter dateFromString:@"ezeze"]; - [self assert:result equals:nil]; + var absoluteDate = [CPDate dateWithTimeIntervalSince1970:1000000000]; + var formattedString = [_formatter stringFromDate:absoluteDate]; - [_dateFormatter setDateFormat:@"MM"]; - var result = [_dateFormatter dateFromString:@"76"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"MM"]; - var result = [_dateFormatter dateFromString:@"zdazdza"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"MMM"]; - var result = [_dateFormatter dateFromString:@"Seepre"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"MMMM"]; - var result = [_dateFormatter dateFromString:@"Septembeer"]; - [self assert:result equals:nil]; -} - -- (void)testDateFromStringTokenL -{ - [_dateFormatter setDateFormat:@"L"]; - var result = [_dateFormatter dateFromString:@"10"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-10-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"L"]; - var result = [_dateFormatter dateFromString:@"1"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"L"]; - var result = [_dateFormatter dateFromString:@"12"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-12-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"LL"]; - var result = [_dateFormatter dateFromString:@"7"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-07-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"LL"]; - var result = [_dateFormatter dateFromString:@"1"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"LL"]; - var result = [_dateFormatter dateFromString:@"12"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-12-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"LLL"]; - var result = [_dateFormatter dateFromString:@"Sep"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-09-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"LLL"]; - var result = [_dateFormatter dateFromString:@"Dec"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-12-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"LLL"]; - var result = [_dateFormatter dateFromString:@"Jan"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"LLLL"]; - var result = [_dateFormatter dateFromString:@"September"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-09-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"LLLL"]; - var result = [_dateFormatter dateFromString:@"December"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-12-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"LLLL"]; - var result = [_dateFormatter dateFromString:@"January"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"LLLLL"]; - var result = [_dateFormatter dateFromString:@"S"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"L"]; - var result = [_dateFormatter dateFromString:@"76"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"L"]; - var result = [_dateFormatter dateFromString:@"dzadza"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"LL"]; - var result = [_dateFormatter dateFromString:@"76"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"LL"]; - var result = [_dateFormatter dateFromString:@"dzadza"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"LLL"]; - var result = [_dateFormatter dateFromString:@"Seepre"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"LLLL"]; - var result = [_dateFormatter dateFromString:@"Septemberezd"]; - [self assert:result equals:nil]; -} - -- (void)testDateFromStringTokenw -{ - [_dateFormatter setDateFormat:@"w"]; - var result = [_dateFormatter dateFromString:@"26"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-06-24 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"w"]; - var result = [_dateFormatter dateFromString:@"76"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"w"]; - var result = [_dateFormatter dateFromString:@"dzadza"]; - [self assert:result equals:nil]; -} - -- (void)testDateFromStringTokenW -{ - [_dateFormatter setDateFormat:@"W"]; - var result = [_dateFormatter dateFromString:@"2"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-08 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"W LL"]; - var result = [_dateFormatter dateFromString:@"2 7"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-07-08 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"W"]; - var result = [_dateFormatter dateFromString:@"76"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"W"]; - var result = [_dateFormatter dateFromString:@"dzadzad"]; - [self assert:result equals:nil]; -} - -- (void)testDateFromStringTokend -{ - [_dateFormatter setDateFormat:@"d"]; - var result = [_dateFormatter dateFromString:@"6"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-06 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"dd"]; - var result = [_dateFormatter dateFromString:@"16"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-16 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"d LL"]; - var result = [_dateFormatter dateFromString:@"6 7"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-07-06 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"ddd"]; - var result = [_dateFormatter dateFromString:@"6"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-06 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"ddd"]; - var result = [_dateFormatter dateFromString:@"62"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"ddd"]; - var result = [_dateFormatter dateFromString:@"dzadza"]; - [self assert:result equals:nil]; -} - -- (void)testDateFromStringTokenD -{ - [_dateFormatter setDateFormat:@"D"]; - var result = [_dateFormatter dateFromString:@"76"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-03-16 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"D"]; - var result = [_dateFormatter dateFromString:@"1076"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"D"]; - var result = [_dateFormatter dateFromString:@"dzeezd"]; - [self assert:result equals:nil]; -} - -- (void)testDateFromStringTokenF -{ - [_dateFormatter setDateFormat:@"F"]; - var result = [_dateFormatter dateFromString:@"2"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-08 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"F"]; - var result = [_dateFormatter dateFromString:@"23"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"F"]; - var result = [_dateFormatter dateFromString:@"dezdez"]; - [self assert:result equals:nil]; -} - -- (void)testDateFromStringTokenE -{ - // No logic in cocoa (or I didn't get it :/) - [_dateFormatter setDateFormat:@"EEE"]; - var result = [_dateFormatter dateFromString:@"Tue"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"EEEE"]; - var result = [_dateFormatter dateFromString:@"Tuesday"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"EEEEE"]; - var result = [_dateFormatter dateFromString:@"T"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"EEE"]; - var result = [_dateFormatter dateFromString:@"dehez"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"EEEE"]; - var result = [_dateFormatter dateFromString:@"dehez"]; - [self assert:result equals:nil]; -} - -- (void)testDateFromStringTokene -{ - // No logic in cocoa (or I didn't get it :/) - [_dateFormatter setDateFormat:@"ee"]; - var result = [_dateFormatter dateFromString:@"1"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"eee"]; - var result = [_dateFormatter dateFromString:@"Tue"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"eeee"]; - var result = [_dateFormatter dateFromString:@"Tuesday"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"eeeee"]; - var result = [_dateFormatter dateFromString:@"T"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"ee"]; - var result = [_dateFormatter dateFromString:@"frefre"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"eee"]; - var result = [_dateFormatter dateFromString:@"dehez"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"eeee"]; - var result = [_dateFormatter dateFromString:@"dehez"]; - [self assert:result equals:nil]; -} - -- (void)testDateFromStringTokenc -{ - // No logic in cocoa (or I didn't get it :/) - [_dateFormatter setDateFormat:@"cc"]; - var result = [_dateFormatter dateFromString:@"1"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"ccc"]; - var result = [_dateFormatter dateFromString:@"Tue"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"cccc"]; - var result = [_dateFormatter dateFromString:@"Tuesday"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"ccccc"]; - var result = [_dateFormatter dateFromString:@"T"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"cc"]; - var result = [_dateFormatter dateFromString:@"dezde"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"ccc"]; - var result = [_dateFormatter dateFromString:@"dehez"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"cccc"]; - var result = [_dateFormatter dateFromString:@"dehez"]; - [self assert:result equals:nil]; -} - -- (void)testDateFromStringTokena -{ - [_dateFormatter setDateFormat:@"a"]; - var result = [_dateFormatter dateFromString:@"PM"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 20:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"a"]; - var result = [_dateFormatter dateFromString:@"AM"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"a"]; - var result = [_dateFormatter dateFromString:@"PdM"]; - [self assert:result equals:nil]; -} - -- (void)testDateFromStringTokenh -{ - [_dateFormatter setDateFormat:@"h"]; - var result = [_dateFormatter dateFromString:@"11"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 19:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"hh"]; - var result = [_dateFormatter dateFromString:@"3"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 11:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"hh"]; - var result = [_dateFormatter dateFromString:@"0"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"hh a"]; - var result = [_dateFormatter dateFromString:@"3 PM"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 23:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"hh a"]; - var result = [_dateFormatter dateFromString:@"3 AM"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 11:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"hh"]; - var result = [_dateFormatter dateFromString:@"13"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"hh"]; - var result = [_dateFormatter dateFromString:@"edze"]; - [self assert:result equals:nil]; -} - -- (void)testDateFromStringTokenH -{ - [_dateFormatter setDateFormat:@"H"]; - var result = [_dateFormatter dateFromString:@"18"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-02 02:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"HH"]; - var result = [_dateFormatter dateFromString:@"3"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 11:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"HH a"]; - var result = [_dateFormatter dateFromString:@"2 PM"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 22:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"HH"]; - var result = [_dateFormatter dateFromString:@"24"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"HH"]; - var result = [_dateFormatter dateFromString:@"dezdez"]; - [self assert:result equals:nil]; -} - -- (void)testDateFromStringTokenK -{ - [_dateFormatter setDateFormat:@"K"]; - var result = [_dateFormatter dateFromString:@"11"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 19:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"KK"]; - var result = [_dateFormatter dateFromString:@"3"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 11:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"KK"]; - var result = [_dateFormatter dateFromString:@"0"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"KK a"]; - var result = [_dateFormatter dateFromString:@"3 PM"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 23:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"KK a"]; - var result = [_dateFormatter dateFromString:@"3 AM"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 11:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"KK"]; - var result = [_dateFormatter dateFromString:@"13"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"KK"]; - var result = [_dateFormatter dateFromString:@"dezdez"]; - [self assert:result equals:nil]; -} - -- (void)testDateFromStringTokenk -{ - [_dateFormatter setDateFormat:@"k"]; - var result = [_dateFormatter dateFromString:@"11"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 19:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"kk"]; - var result = [_dateFormatter dateFromString:@"3"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 11:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"kk"]; - var result = [_dateFormatter dateFromString:@"0"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"kk a"]; - var result = [_dateFormatter dateFromString:@"3 PM"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 23:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"kk"]; - var result = [_dateFormatter dateFromString:@"13"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"kk"]; - var result = [_dateFormatter dateFromString:@"dezdezd"]; - [self assert:result equals:nil]; -} - -- (void)testDateFromStringTokenm -{ - [_dateFormatter setDateFormat:@"m"]; - var result = [_dateFormatter dateFromString:@"11"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 08:11:00 +0000"]]; - - [_dateFormatter setDateFormat:@"mm"]; - var result = [_dateFormatter dateFromString:@"11"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 08:11:00 +0000"]] - - [_dateFormatter setDateFormat:@"mm"]; - var result = [_dateFormatter dateFromString:@"61"]; - [self assert:result equals:nil] - - [_dateFormatter setDateFormat:@"mm"]; - var result = [_dateFormatter dateFromString:@"ezdezd"]; - [self assert:result equals:nil] -} - -- (void)testDateFromStringTokens -{ - [_dateFormatter setDateFormat:@"s"]; - var result = [_dateFormatter dateFromString:@"11"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 08:00:11 +0000"]]; - - [_dateFormatter setDateFormat:@"ss"]; - var result = [_dateFormatter dateFromString:@"4"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 08:00:04 +0000"]] - - [_dateFormatter setDateFormat:@"ss"]; - var result = [_dateFormatter dateFromString:@"64"]; - [self assert:result equals:nil] - - [_dateFormatter setDateFormat:@"ss"]; - var result = [_dateFormatter dateFromString:@"dezdez"]; - [self assert:result equals:nil] -} - -- (void)testDateFromStringTokenS -{ - // No logic in cocoa (or I didn't get it :/) - [_dateFormatter setDateFormat:@"SSS"]; - var result = [_dateFormatter dateFromString:@"21212"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 08:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"SSS"]; - var result = [_dateFormatter dateFromString:@"rer"]; - [self assert:result equals:nil]; -} - -- (void)testDateFromStringTokenA -{ - [_dateFormatter setDateFormat:@"AAAAAAAA"]; - var result = [_dateFormatter dateFromString:@"69540000"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-02 03:19:00 +0000"]]; - - [_dateFormatter setDateFormat:@"AAAAAAAA"]; - var result = [_dateFormatter dateFromString:@"ezde"]; - [self assert:result equals:nil]; -} - -- (void)testDataFromStringTokenz -{ - [_dateFormatter setDateFormat:@"hh zzz"]; - var result = [_dateFormatter dateFromString:@"10 PDT"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 17:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"hh zzzz"]; - var result = [_dateFormatter dateFromString:@"10 GMT+08:35"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 01:25:00 +0000"]]; - - [_dateFormatter setDateFormat:@"zzz"]; - var result = [_dateFormatter dateFromString:@"PezST"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"zzzz"]; - var result = [_dateFormatter dateFromString:@"dehez"]; - [self assert:result equals:nil]; -} - -- (void)testDataFromStringTokenZ -{ - [_dateFormatter setDateFormat:@"hh ZZZ"]; - var result = [_dateFormatter dateFromString:@"10 -0600"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 16:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"hh ZZZZ"]; - var result = [_dateFormatter dateFromString:@"10 GMT-05:00"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 15:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"hh ZZZZZ"]; - var result = [_dateFormatter dateFromString:@"10 +04:00"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 06:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"ZZZ"]; - var result = [_dateFormatter dateFromString:@"dehez"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"ZZZZ"]; - var result = [_dateFormatter dateFromString:@"dehez"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"ZZZZZ"]; - var result = [_dateFormatter dateFromString:@"dehez"]; - [self assert:result equals:nil]; -} -// -- (void)testDataFromStringTokenv -{ - [_dateFormatter setDateFormat:@"hh v"]; - var result = [_dateFormatter dateFromString:@"02 PT"]; - // As the CPTimeZone does not care about daylight saving time. The 'PT' time zone can result in 'PST' or 'PDT'. - // The assert below can be any of the two version depending which time zone abbreviation CPDictionary 'keyEnumerator' - // will return first. This behaviour is undefined. - if ([[CPTimeZone _timeZoneFromString:@"PT" style:CPTimeZoneNameStyleShortGeneric locale:[_dateFormatter locale]] abbreviation] === @"PDT") - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 09:00:00 +0000"]]; - else - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 10:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"hh vvvv"]; - var result = [_dateFormatter dateFromString:@"8 GMT-08:35"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 16:35:00 +0000"]]; - - [_dateFormatter setDateFormat:@"v"]; - var result = [_dateFormatter dateFromString:@"PezST"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"vvvv"]; - var result = [_dateFormatter dateFromString:@"dehez"]; - [self assert:result equals:nil]; - -} - -- (void)testDataFromStringTokenV -{ - [_dateFormatter setDateFormat:@"hh V"]; - var result = [_dateFormatter dateFromString:@"6 PST"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"2000-01-01 14:00:00 +0000"]]; - - [_dateFormatter setDateFormat:@"hh VVVV"]; - var result = [_dateFormatter dateFromString:@"6 GMT+06:35"]; - [self assert:result equals:[[CPDate alloc] initWithString:@"1999-12-31 23:25:00 +0000"]]; - - [_dateFormatter setDateFormat:@"V"]; - var result = [_dateFormatter dateFromString:@"PezST"]; - [self assert:result equals:nil]; - - [_dateFormatter setDateFormat:@"VVVV"]; - var result = [_dateFormatter dateFromString:@"dehez"]; - [self assert:result equals:nil]; -} - -- (void)testDateFromStringISO8601 -{ - // This test verifies the fix for issue #2537. - // The parser should correctly handle the ISO 8601 format produced by Date.toISOString(). - var dateFormatter = [[CPDateFormatter alloc] init]; - [dateFormatter setLocale:[[CPLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]]; - [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"]; - - // Test with a fixed date string. - var fixedISOString = @"2017-03-31T16:40:15.768Z"; - var parsedFixedDate = [dateFormatter dateFromString:fixedISOString]; - - // Create the expected date using a known-good method (JS Date parsing) to avoid relying on - // the code under test. We create it from a time interval, which supports millisecond precision. - var expectedTimestamp = new Date(fixedISOString).getTime() / 1000.0; - var expectedFixedDate = [CPDate dateWithTimeIntervalSince1970:expectedTimestamp]; - - // Now assert that the formatter produced the same date. - // isEqualToDate: compares the underlying time interval, which is what we want. - [self assertTrue:[parsedFixedDate isEqualToDate:expectedFixedDate] message:@"DateFormatter should correctly parse a fixed ISO 8601 string."]; - - // Test with a dynamically generated date string. - var liveJSDate = new Date(); - var liveISOString = liveJSDate.toISOString(); - - var parsedLiveDate = [dateFormatter dateFromString:liveISOString]; - - // The fix converts from a JS Date, so we can compare time intervals for accuracy. - var liveJSDateTime = liveJSDate.getTime(); - var parsedLiveDateTime = [parsedLiveDate timeIntervalSince1970] * 1000; - - // We allow a small tolerance for floating point rounding. - [self assertTrue:Math.abs(liveJSDateTime - parsedLiveDateTime) < 1.0 message:@"DateFormatter should correctly parse a dynamically generated ISO 8601 string."]; -} - -- (void)testGetObjectValueAcceptsNilErrorDescription -{ - var date = nil, - result = [_dateFormatter getObjectValue:@ref(date) forString:@"a" errorDescription:nil]; - - [self assertFalse:result]; - [self assertTrue:date === nil]; -} - -- (void)testGetObjectValueForEmptyStringReturnsReferenceDate -{ - [_dateFormatter setDateFormat:@"d mm"]; - - var date = nil, - error = @"", - result = [_dateFormatter getObjectValue:@ref(date) forString:@"" errorDescription:@ref(error)]; - - [self assertTrue:result]; - [self assert:[[CPDate alloc] initWithString:@"2000-01-01 00:00:00 +0000"] equals:date]; - [self assert:error equals:@""]; -} - -- (void)testGetObjectValueReturnYes -{ - [_dateFormatter setDateFormat:@"d mm"]; - - var date = nil, - error = @"", - result = [_dateFormatter getObjectValue:@ref(date) forString:@"10 12" errorDescription:@ref(error)]; - - [self assertTrue:result]; - [self assert:date equals:[[CPDate alloc] initWithString:@"2000-01-10 08:12:00 +0000"]]; - [self assert:error equals:@""]; -} - -- (void)testGetObjectValueReturnNo -{ - [_dateFormatter setDateFormat:@"d mm"]; - - var date = nil, - error = @"", - result = [_dateFormatter getObjectValue:@ref(date) forString:@"ezr 12" errorDescription:@ref(error)]; - - [self assert:result equals:NO]; - [self assert:date equals:nil]; - [self assert:error equals:@"The value \"ezr 12\" is invalid."]; -} - -// MARK: - -// MARK: non-en locales - -// Unless we have full locale support any non-supported locale should return the english values as they are the only defined ones at the moment. - -- (void)testGetNonEnAMSymbol -{ - var enValue = [_dateFormatter AMSymbol], - value = [_nonEnDateFormatter AMSymbol]; - [self assert:enValue equals:value]; -} - -- (void)testGetNonEnPMSymbol -{ - var enValue = [_dateFormatter PMSymbol], - value = [_nonEnDateFormatter PMSymbol]; - [self assert:enValue equals:value]; -} - -- (void)testGetNonEnWeekdaySymbols -{ - var enValue = [_dateFormatter weekdaySymbols], - value = [_nonEnDateFormatter weekdaySymbols]; - [self assert:enValue equals:value]; -} - -- (void)testGetNonEnShortWeekdaySymbols -{ - var enValue = [_dateFormatter shortWeekdaySymbols], - value = [_nonEnDateFormatter shortWeekdaySymbols]; - [self assert:enValue equals:value]; -} - -- (void)testGetNonEnVeryShortWeekdaySymbols -{ - var enValue = [_dateFormatter veryShortWeekdaySymbols], - value = [_nonEnDateFormatter veryShortWeekdaySymbols]; - [self assert:enValue equals:value]; -} - -- (void)testGetNonEnStandaloneWeekdaySymbols -{ - var enValue = [_dateFormatter standaloneWeekdaySymbols], - value = [_nonEnDateFormatter standaloneWeekdaySymbols]; - [self assert:enValue equals:value]; -} - -- (void)testGetNonEnShortStandaloneWeekdaySymbols -{ - var enValue = [_dateFormatter shortStandaloneWeekdaySymbols], - value = [_nonEnDateFormatter shortStandaloneWeekdaySymbols]; - [self assert:enValue equals:value]; -} - -- (void)testGetNonEnVeryShortStandaloneWeekdaySymbols -{ - var enValue = [_dateFormatter veryShortStandaloneWeekdaySymbols], - value = [_nonEnDateFormatter veryShortStandaloneWeekdaySymbols]; - [self assert:enValue equals:value]; -} - -- (void)testGetNonEnMonthSymbols -{ - var enValue = [_dateFormatter monthSymbols], - value = [_nonEnDateFormatter monthSymbols]; - [self assert:enValue equals:value]; -} - -- (void)testGetNonEnShortMonthSymbols -{ - var enValue = [_dateFormatter shortMonthSymbols], - value = [_nonEnDateFormatter shortMonthSymbols]; - [self assert:enValue equals:value]; -} - -- (void)testGetNonEnVeryShortMonthSymbols -{ - var enValue = [_dateFormatter veryShortMonthSymbols], - value = [_nonEnDateFormatter veryShortMonthSymbols]; - [self assert:enValue equals:value]; -} - -- (void)testGetNonEnStandaloneMonthSymbols -{ - var enValue = [_dateFormatter standaloneMonthSymbols], - value = [_nonEnDateFormatter standaloneMonthSymbols]; - [self assert:enValue equals:value]; -} - -- (void)testGetNonEnShortStandaloneMonthSymbols -{ - var enValue = [_dateFormatter shortStandaloneMonthSymbols], - value = [_nonEnDateFormatter shortStandaloneMonthSymbols]; - [self assert:enValue equals:value]; -} - -- (void)testGetNonEnVeryShortStandaloneMonthSymbols -{ - var enValue = [_dateFormatter veryShortStandaloneMonthSymbols], - value = [_nonEnDateFormatter veryShortStandaloneMonthSymbols]; - [self assert:enValue equals:value]; -} - -- (void)testGetNonEnQuarterSymbols -{ - var enValue = [_dateFormatter quarterSymbols], - value = [_nonEnDateFormatter quarterSymbols]; - [self assert:enValue equals:value]; -} - -- (void)testGetNonEnShortQuarterSymbols -{ - var enValue = [_dateFormatter shortQuarterSymbols], - value = [_nonEnDateFormatter shortQuarterSymbols]; - [self assert:enValue equals:value]; -} - -- (void)testGetNonEnStandaloneQuarterSymbols -{ - var enValue = [_dateFormatter standaloneQuarterSymbols], - value = [_nonEnDateFormatter standaloneQuarterSymbols]; - [self assert:enValue equals:value]; -} - -- (void)testGetNonEnShortStandaloneQuarterSymbols -{ - var enValue = [_dateFormatter shortStandaloneQuarterSymbols], - value = [_nonEnDateFormatter shortStandaloneQuarterSymbols]; - [self assert:enValue equals:value]; + [self assert:formattedString + equals:@"2001-09-08 18:46:40 -0700" + message:@"Formatter output must match fixed offset temporal baseline."]; } @end From 6f1f56205e8886cd8de230598f8cf478da3bee80 Mon Sep 17 00:00:00 2001 From: David Richardson Date: Fri, 11 Sep 2026 18:53:33 -0600 Subject: [PATCH 4/4] Refactor CPTimeZone to use dynamic Intl validation and host-native IANA enumeration * Fully remove the static, hand-maintained knownTimeZoneNames array and knownTimeZoneNamesSet dictionary from +initialize. * Implement runtime dynamic population via Intl.supportedValuesOf("timeZone") with strict fallback handling for GMT and UTC. * Introduce a robust _isValidTimeZoneName runtime check utilizing Intl.DateTimeFormat validation to reject unrecognized zone names dynamically rather than relying on a stale static list. * Update initWithName: and _initWithName:abbreviation: to leverage the new engine-native validation bridge. --- Foundation/CPTimeZone.j | 117 ++++++++++++++-------------------------- 1 file changed, 40 insertions(+), 77 deletions(-) diff --git a/Foundation/CPTimeZone.j b/Foundation/CPTimeZone.j index a6d774339..f02b07fdb 100644 --- a/Foundation/CPTimeZone.j +++ b/Foundation/CPTimeZone.j @@ -48,7 +48,6 @@ CPSystemTimeZoneDidChangeNotification = @"CPSystemTimeZoneDidChangeNotification" */ var abbreviationDictionary, knownTimeZoneNames, -knownTimeZoneNamesSet, defaultTimeZone, localTimeZone, systemTimeZone, @@ -63,6 +62,24 @@ var _stdDstOffsetCache = {}; // +_timeZoneFromString:style:locale: and built lazily on first lookup. var _nameToZoneCache = {}; +/*! + Validates a given time zone name by attempting to construct a lightweight + Intl.DateTimeFormat object. The engine throws a RangeError if the timeZone + is not recognized. + */ +function _isValidTimeZoneName(tzName) +{ + try + { + new Intl.DateTimeFormat('en-US', { timeZone: tzName }); + return YES; + } + catch (e) + { + return NO; + } +} + /*! Live UTC offset, in minutes, for an IANA zone name at a given date. DST-aware by construction, since it asks Intl to render the actual wall @@ -266,8 +283,7 @@ function _localizedNameForZone(tzName, style, locale, fixedOffsetSecondsOrNil) /*! Resolves the runtime's own current zone as a CPTimeZone: prefer the - IANA name Intl resolves to (DST-correct, works for any of the 400+ - zones in knownTimeZoneNames), and fall back to a fixed-offset zone + IANA name Intl resolves to (DST-correct), and fall back to a fixed-offset zone built from the JS Date offset if Intl isn't available at all. Always succeeds. */ @@ -279,7 +295,7 @@ function _systemTimeZoneFromRuntime() { var ianaName = new Intl.DateTimeFormat().resolvedOptions().timeZone; - if (ianaName && knownTimeZoneNamesSet[ianaName]) + if (ianaName && _isValidTimeZoneName(ianaName)) { var zone = [CPTimeZone timeZoneWithName:ianaName]; @@ -320,47 +336,9 @@ function _systemTimeZoneFromRuntime() if (self !== [CPTimeZone class]) return; - knownTimeZoneNames = [ - @"Africa/Addis_Ababa", - @"Africa/Harare", - @"Africa/Lagos", - @"America/Argentina/Buenos_Aires", - @"America/Bogota", - @"America/Chicago", - @"America/Denver", - @"America/Halifax", - @"America/Juneau", - @"America/Lima", - @"America/Los_Angeles", - @"America/New_York", - @"America/Santiago", - @"America/Sao_Paulo", - @"Asia/Bangkok", - @"Asia/Calcutta", - @"Asia/Dhaka", - @"Asia/Dubai", - @"Asia/Hong_Kong", - @"Asia/Jakarta", - @"Asia/Karachi", - @"Asia/Manila", - @"Asia/Seoul", - @"Asia/Singapore", - @"Asia/Tehran", - @"Asia/Tokyo", - @"Europe/Istanbul", - @"Europe/Lisbon", - @"Europe/London", - @"Europe/Moscow", - @"Europe/Paris", - @"GMT", - @"Pacific/Auckland", - @"Pacific/Honolulu", - @"UTC" - ]; + knownTimeZoneNames = []; - // Prefer the runtime's own IANA database, when it exposes one, over the - // hardcoded 35-city list above: it's the full current set, not a - // snapshot that will silently drift the way a hand-maintained list does. + // Prefer the runtime's own IANA database when it exposes one. if (typeof Intl !== "undefined" && typeof Intl.supportedValuesOf === "function") { try @@ -369,51 +347,36 @@ function _systemTimeZoneFromRuntime() if (supportedZones && supportedZones.length > 0) { - var zones = []; - var hasGMT = false; - var hasUTC = false; var count = supportedZones.length; - // Iterate using primitive property access. - // The array returned by Intl across the runtime bridge may lack - // standard Array prototypes (e.g., slice, indexOf). A standard loop - // ensures safe data extraction into a local array without triggering - // prototype resolution exceptions or relying on CPArray. for (var i = 0; i < count; i++) { - var zone = supportedZones[i]; - zones[i] = zone; - - if (zone === @"GMT") - hasGMT = true; - else if (zone === @"UTC") - hasUTC = true; + knownTimeZoneNames[i] = supportedZones[i]; } - - // Explicitly restore legacy aliases if the host engine omits them. - // Engines adhering strictly to canonical IANA identifiers omit "GMT" - // and "UTC", but code in this class treats both as always-known. - if (!hasGMT) - zones[zones.length] = @"GMT"; - - if (!hasUTC) - zones[zones.length] = @"UTC"; - - knownTimeZoneNames = zones; } } catch (e) { - // Fall through, keep the hardcoded list above. + // Fall through. } } - // O(1) membership testing for -initWithName: and the runtime zone - // resolver, instead of a linear scan over several hundred names. - knownTimeZoneNamesSet = {}; + var hasGMT = false; + var hasUTC = false; for (var i = 0, count = knownTimeZoneNames.length; i < count; i++) - knownTimeZoneNamesSet[knownTimeZoneNames[i]] = true; + { + if (knownTimeZoneNames[i] === @"GMT") + hasGMT = true; + else if (knownTimeZoneNames[i] === @"UTC") + hasUTC = true; + } + + if (!hasGMT) + knownTimeZoneNames[knownTimeZoneNames.length] = @"GMT"; + + if (!hasUTC) + knownTimeZoneNames[knownTimeZoneNames.length] = @"UTC"; // A curated abbreviation -> canonical name lookup for +timeZoneWithAbbreviation:. // This stays a fixed, hand-maintained set deliberately: abbreviations are @@ -676,7 +639,7 @@ function _systemTimeZoneFromRuntime() if (!tzName) [CPException raise:CPInvalidArgumentException reason:"Invalid value provided for tzName"]; - if (!knownTimeZoneNamesSet[tzName] || !abbreviation) + if (!_isValidTimeZoneName(tzName) || !abbreviation) return nil; if (self = [super init]) @@ -715,7 +678,7 @@ function _systemTimeZoneFromRuntime() if (!tzName) [CPException raise:CPInvalidArgumentException reason:"Invalid value provided for tzName"]; - if (!knownTimeZoneNamesSet[tzName]) + if (!_isValidTimeZoneName(tzName)) return nil; if (self = [super init])