diff --git a/Foundation/CPTimeZone.j b/Foundation/CPTimeZone.j index 7d09843e7..add2aace3 100644 --- a/Foundation/CPTimeZone.j +++ b/Foundation/CPTimeZone.j @@ -105,6 +105,35 @@ function abbreviationForDate(date) // Return nil if no valid abbreviation could be determined. return nil; } + +function _abbreviationForNameAndDate(tzName, date) +{ + // This is a helper function based on the existing `abbreviationForDate`. + // It determines the abbreviation for a given IANA name based on the provided date, + // which allows it to respect daylight saving time. + try { + var options = { + timeZone: tzName, + timeZoneName: 'long' + }; + // The 'en-US' locale provides a predictable format for parsing. + var dateString = date.toLocaleString('en-US', options); + + // This regex is copied from the global 'abbreviationForDate' function. + // It strips the date and time, leaving the long time zone name. + var longTZName = dateString.replace(/^([0]?\d|[1][0-2])\/((?:[0]?|[1-2])\d|[3][0-1])\/([2][01]|[1][6-9])\d{2}(,?\s*([0]?\d|[1][0-2])(\:[0-5]\d){1,2})*\s*([aApP][mM]{0,2})?\s*/, ""); + + // Create the abbreviation from the long name (e.g., "Pacific Daylight Time" -> "PDT") + var abbreviation = longTZName.split(" ").map(function(l) { return l[0]}).join(""); + + return abbreviation; + } catch (e) { + // The tzName might be invalid for toLocaleString, which throws a RangeError. + // In this case, we can't determine the abbreviation. + return nil; + } +} + /*! @class CPTimeZone @ingroup foundation @@ -574,19 +603,38 @@ function abbreviationForDate(date) { _name = tzName; - var keys = [abbreviationDictionary keyEnumerator], - key; + // Determine the abbreviation based on the current date to handle DST. + var currentAbbreviation = _abbreviationForNameAndDate(tzName, [CPDate date]); - while (key = [keys nextObject]) + // 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]) { - var value = [abbreviationDictionary valueForKey:key]; + _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 ([value isEqualToString:_name]) + while (key = [keys nextObject]) { - _abbreviation = key; - break; + var value = [abbreviationDictionary valueForKey:key]; + + if ([value isEqualToString:_name]) + { + _abbreviation = key; + break; + } } } + + // If no abbreviation could be found by any means, initialization fails. + if (!_abbreviation) + return nil; } return self; diff --git a/Tests/Foundation/CPTimeZoneTest.j b/Tests/Foundation/CPTimeZoneTest.j index 82bda2e83..bd8724e31 100644 --- a/Tests/Foundation/CPTimeZoneTest.j +++ b/Tests/Foundation/CPTimeZoneTest.j @@ -347,22 +347,42 @@ [self assert:[timeZone1 isEqualToTimeZone:timeZone2] equals:NO]; } -- (void)testSystemTimeZonesAreNotNilInModernBrowsers +- (void)testInitWithNameRespectsDaylightSaving { - // This test verifies the fix for issue #2855, where modern browsers - // could cause system time zone methods to return nil. - var local = [CPTimeZone localTimeZone]; - [self assert:(local !== nil) equals:YES]; + // This test verifies that initWithName: correctly selects the abbreviation + // (e.g., PST vs. PDT) based on the current date's daylight saving status. - var system = [CPTimeZone systemTimeZone]; - [self assert:(system !== nil) equals:YES]; + // 1. Test a time zone that observes DST, like America/Los_Angeles. + var laTimeZoneName = @"America/Los_Angeles"; + var expectedAbbreviationLA; - var defaultTZ = [CPTimeZone defaultTimeZone]; - [self assert:(defaultTZ !== nil) equals:YES]; + try { + // This logic mimics the _abbreviationForNameAndDate helper function in CPTimeZone.j + var options = { timeZone: laTimeZoneName, timeZoneName: 'long' }; + var dateString = (new Date()).toLocaleString('en-US', options); + var longTZName = dateString.replace(/^([0]?\d|[1][0-2])\/((?:[0]?|[1-2])\d|[3][0-1])\/([2][01]|[1][6-9])\d{2}(,?\s*([0]?\d|[1][0-2])(\:[0-5]\d){1,2})*\s*([aApP][mM]{0,2})?\s*/, ""); + expectedAbbreviationLA = longTZName.split(" ").map(function(l) { return l[0]}).join(""); + } catch (e) { + [self fail:"Could not determine expected abbreviation for America/Los_Angeles"]; + return; + } - // Also check that they are all the same object, as per initialization logic - [self assert:local equals:system]; - [self assert:local equals:defaultTZ]; + var timeZoneLA = [[CPTimeZone alloc] initWithName:laTimeZoneName]; + + if (timeZoneLA == nil) + [self fail:"Time zone for America/Los_Angeles should be created successfully."]; + + [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"]; } @end