UNPKG

exiftool-vendored

Version:
988 lines 71.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const luxon_1 = require("luxon"); const ExifDateTime_1 = require("./ExifDateTime"); const Settings_1 = require("./Settings"); const Timezones_1 = require("./Timezones"); const _chai_spec_1 = require("./_chai.spec"); describe("Timezones", () => { afterEach(() => { Settings_1.Settings.reset(); }); describe("UnsetZone", () => { it("isValid", () => { (0, _chai_spec_1.expect)(Timezones_1.UnsetZone.isValid).to.eql(true); }); it("reasonableTzOffsetMinutes() returns undefined for UnsetZone", () => { (0, _chai_spec_1.expect)((0, Timezones_1.validTzOffsetMinutes)(Timezones_1.UnsetZoneOffsetMinutes)).to.eql(false); }); it("offsetMinutesToZoneName() returns undefined for UnsetZone", () => { (0, _chai_spec_1.expect)((0, Timezones_1.offsetMinutesToZoneName)(Timezones_1.UnsetZoneOffsetMinutes)).to.eql(undefined); }); it("extractOffset() returns undefined for UnsetZone", () => { (0, _chai_spec_1.expect)((0, Timezones_1.extractZone)("UTC-00:01")).to.eql(undefined); }); }); describe("normalizeZone()", () => { describe("rejects invalid inputs", () => { for (const invalid of [ undefined, "", new luxon_1.IANAZone("invalid"), "GMT/invalid", "BAD", "UTC+21", "UTC-15", "+BAD", "+123", "2014-07-19T12:05:19-09:00", ]) { it(`(${JSON.stringify(invalid)}) => undefined`, () => { (0, _chai_spec_1.expect)((0, Timezones_1.normalizeZone)(invalid)).to.eql(undefined); }); } }); describe("accepts valid inputs", () => { const ts = new Date("2023-01-01T01:00:00").getTime(); for (const ea of [ { z: "Z", exp: "+00:00" }, { z: "GMT", exp: "+00:00" }, { z: "Zulu", exp: "+00:00" }, { z: "UTC", exp: "+00:00" }, { z: "UCT", exp: "+00:00" }, { z: "UTC+0", exp: "+00:00" }, { z: "UTC+00:00", exp: "+00:00" }, { z: "UTC+07:00", exp: "+07:00" }, { z: "UTC+06:30", exp: "+06:30" }, // Changed from +07:30 which is archaic (Malaysia until 1982) { z: "UTC-03:00", exp: "-03:00" }, { z: "America/Los_Angeles", exp: "-08:00" }, { z: "America/Indiana/Indianapolis", exp: "-05:00" }, { z: "America/Kentucky/Louisville", exp: "-05:00" }, { z: "US/Hawaii", exp: "-10:00" }, { z: "Pacific/Honolulu", exp: "-10:00" }, { z: "Japan", exp: "+09:00" }, { z: "GB", exp: "+00:00" }, // FIRST THEY MISSPEL COLOUR, AND NOW THIS. BLIMEY. ]) { it(`(${JSON.stringify(ea.z)}) => ${ea.exp}`, () => { (0, _chai_spec_1.expect)((0, Timezones_1.normalizeZone)(ea.z)?.formatOffset(ts, "short")).to.eql(ea.exp); }); } }); describe("with archaic offsets", () => { beforeEach(() => { Settings_1.Settings.allowArchaicTimezoneOffsets.value = true; }); afterEach(() => { Settings_1.Settings.reset(); }); const archaicCases = [ { z: "UTC-10:30", desc: "Hawaii 1896-1947", exp: "-10:30" }, { z: "UTC-04:30", desc: "Venezuela 1912-1965, 2007-2016", exp: "-04:30", }, { z: "UTC+04:51", desc: "Bombay until 1955", exp: "+04:51" }, { z: "UTC+05:40", desc: "Nepal until 1920", exp: "+05:40" }, { z: "UTC+07:30", desc: "Malaysia until 1982", exp: "+07:30" }, ]; for (const { z, desc, exp } of archaicCases) { it(`accepts ${z} (${desc})`, () => { const ts = new Date("2023-01-01T01:00:00").getTime(); const result = (0, Timezones_1.normalizeZone)(z); (0, _chai_spec_1.expect)(result).to.not.eql(undefined); (0, _chai_spec_1.expect)(result?.formatOffset(ts, "short")).to.eql(exp); }); } }); describe("without archaic offsets", () => { beforeEach(() => { Settings_1.Settings.allowArchaicTimezoneOffsets.value = false; }); const archaicCases = [ { z: "UTC-10:30", desc: "Hawaii 1896-1947" }, { z: "UTC-04:30", desc: "Venezuela 1912-1965, 2007-2016" }, { z: "UTC+04:51", desc: "Bombay until 1955" }, { z: "UTC+05:40", desc: "Nepal until 1920" }, { z: "UTC+07:30", desc: "Malaysia until 1982" }, ]; for (const { z, desc } of archaicCases) { it(`rejects ${z} (${desc})`, () => { (0, _chai_spec_1.expect)((0, Timezones_1.normalizeZone)(z)).to.eql(undefined); }); } }); describe("numeric offset inputs", () => { it("accepts valid numeric offset (480 = UTC+8)", () => { const zone = (0, Timezones_1.normalizeZone)(480); (0, _chai_spec_1.expect)(zone?.name).to.eql("UTC+8"); }); it("accepts valid numeric offset (-300 = UTC-5)", () => { const zone = (0, Timezones_1.normalizeZone)(-300); (0, _chai_spec_1.expect)(zone?.name).to.eql("UTC-5"); }); it("accepts zero offset (0 = UTC)", () => { const zone = (0, Timezones_1.normalizeZone)(0); (0, _chai_spec_1.expect)(zone?.name).to.eql("UTC"); }); it("rejects invalid numeric offset (100 minutes)", () => { (0, _chai_spec_1.expect)((0, Timezones_1.normalizeZone)(100)).to.eql(undefined); }); it("rejects UnsetZone sentinel (-1)", () => { (0, _chai_spec_1.expect)((0, Timezones_1.normalizeZone)(-1)).to.eql(undefined); }); it("rejects offset beyond ±14 hours (1200 = 20 hours)", () => { (0, _chai_spec_1.expect)((0, Timezones_1.normalizeZone)(1200)).to.eql(undefined); }); it("rejects archaic offset when disabled (-630 = Hawaii -10:30)", () => { Settings_1.Settings.allowArchaicTimezoneOffsets.value = false; (0, _chai_spec_1.expect)((0, Timezones_1.normalizeZone)(-630)).to.eql(undefined); }); it("accepts archaic offset when enabled (-630 = Hawaii -10:30)", () => { Settings_1.Settings.allowArchaicTimezoneOffsets.value = true; const zone = (0, Timezones_1.normalizeZone)(-630); (0, _chai_spec_1.expect)(zone?.name).to.eql("UTC-10:30"); Settings_1.Settings.reset(); }); }); }); describe("extractOffset()", () => { function ozn(zone) { return { zone, tz: zone, src: "normalizeZone", }; } const arr = [ { s: "7", exp: "UTC+7" }, { s: "3:30", exp: "UTC+3:30" }, ]; const ex = [ { zone: "", exp: undefined }, { zone: "garbage", exp: undefined }, { zone: "+09:00", exp: ozn("UTC+9"), }, { zone: "America/Los_Angeles", exp: ozn("America/Los_Angeles"), }, ...arr.map(({ s, exp }) => ({ zone: "+" + s, exp: ozn(exp), })), ...arr.map(({ s, exp }) => ({ zone: "UTC+" + s, exp: ozn(exp) })), ...arr.map(({ s, exp }) => ({ zone: "-" + s, exp: ozn(exp.replace("+", "-")), })), ...arr.map(({ s, exp }) => ({ zone: "UTC-" + s, exp: ozn(exp.replace("+", "-")), })), { zone: ExifDateTime_1.ExifDateTime.fromEXIF("2014:07:19 12:05:19-09:00"), exp: { zone: "UTC-9", src: "ExifDateTime.zone" }, }, { zone: 3, exp: { zone: "UTC+3", src: "hourOffset" } }, { zone: -10, exp: { zone: "UTC-10", src: "hourOffset" } }, ]; for (const { zone, exp } of ex) { it(`("${zone}") => ${JSON.stringify(exp)}`, () => { (0, _chai_spec_1.expect)((0, Timezones_1.extractZone)(zone)).to.containSubset(exp); }); } }); describe("Unicode timezone signs", () => { it("should handle Unicode minus sign (U+2212) in offset", () => { const result = (0, Timezones_1.extractZone)("−08:00"); // Unicode minus (0, _chai_spec_1.expect)(result?.zone).to.equal("UTC-8"); (0, _chai_spec_1.expect)(result?.src).to.equal("normalizeZone"); }); it("should handle Unicode minus sign with UTC prefix", () => { const result = (0, Timezones_1.extractZone)("UTC−8"); // Unicode minus (0, _chai_spec_1.expect)(result?.zone).to.equal("UTC-8"); (0, _chai_spec_1.expect)(result?.src).to.equal("normalizeZone"); }); it("should handle Unicode minus in full timestamp", () => { const result = (0, Timezones_1.extractZone)("2023:01:15 10:30:00−08:00"); // Unicode minus (0, _chai_spec_1.expect)(result?.zone).to.equal("UTC-8"); (0, _chai_spec_1.expect)(result?.leftovers).to.equal("2023:01:15 10:30:00"); }); it("should handle archaic offset with Unicode minus", () => { Settings_1.Settings.allowArchaicTimezoneOffsets.value = true; const result = (0, Timezones_1.extractZone)("−00:44"); // Liberia historical offset with Unicode minus (0, _chai_spec_1.expect)(result?.zone).to.match(/UTC-0:44/); }); it("should normalize Unicode minus to ASCII minus in zone name", () => { const result = (0, Timezones_1.extractZone)("−08:00"); // Unicode minus input (0, _chai_spec_1.expect)(result?.zone).to.match(/^UTC-/); // ASCII minus in output (0, _chai_spec_1.expect)(result?.zone).to.not.match(/−/); // No Unicode minus in output }); }); describe("extractTzOffsetFromTags", () => { describe("with TimeZone", () => { for (const { tzo, exp } of [ { tzo: "+00:00", exp: "UTC" }, { tzo: "-9", exp: "UTC-9" }, { tzo: "-09:00", exp: "UTC-9" }, { tzo: "+5:30", exp: "UTC+5:30" }, { tzo: "+02:00", exp: "UTC+2" }, ]) { it(`({ TimeZone: ${tzo}}) => ${exp}`, () => { (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromTags)({ TimeZone: tzo })).to.eql({ zone: exp, tz: exp, src: "TimeZone", }); }); it(`({ OffsetTimeOriginal: ${tzo}}) => ${exp}`, () => { (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromTags)({ OffsetTimeOriginal: tzo })).to.eql({ zone: exp, tz: exp, src: "OffsetTimeOriginal", }); }); it(`({ TimeZoneOffset: ${tzo}}) => ${exp}`, () => { (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromTags)({ TimeZoneOffset: tzo })).to.eql({ zone: exp, tz: exp, src: "TimeZoneOffset", }); }); it(`${exp} normalizes to the same value`, () => { const zone = luxon_1.Info.normalizeZone(exp); (0, _chai_spec_1.expect)(zone.name).to.eql(exp); (0, _chai_spec_1.expect)(zone.isValid).to.eql(true); }); } }); }); describe("extractTzOffsetFromUTCOffset", () => { it("with DateTimeUTC offset by 4 hours and preceding by one second from DateTimeOriginal", () => { (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromUTCOffset)({ DateTimeOriginal: "2024:09:14 12:00:00", DateTimeUTC: "2024:09:14 15:59:00", // from a prior GPS fix? })).to.eql({ zone: "UTC-4", tz: "UTC-4", src: "offset between DateTimeOriginal and DateTimeUTC", }); }); it("with DateTimeUTC offset by 4 hours and lagging by one second from DateTimeOriginal", () => { (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromUTCOffset)({ DateTimeOriginal: "2024:09:14 12:00:00", DateTimeUTC: "2024:09:14 16:00:01", })).to.eql({ zone: "UTC-4", tz: "UTC-4", src: "offset between DateTimeOriginal and DateTimeUTC", }); }); it("with lagging GPSDateStamp & GPSTimeStamp and DateTimeOriginal in negative offset", () => { (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromUTCOffset)({ DateTimeOriginal: "2016:07:25 11:08:49", GPSDateStamp: "2016:07:25", GPSTimeStamp: "17:45:46", })).to.eql({ zone: "UTC-7", tz: "UTC-7", src: "offset between DateTimeOriginal and GPSDateTimeStamp", }); }); for (const tagname of [ "DateTimeUTC", "GPSDateTime", "SonyDateTime2", ]) { it(`with ${tagname} and created-at CreateDate with 7 hour offset`, () => { const obj = { CreateDate: "2014:07:19 12:05:19", }; obj[tagname] = "2014:07:19 19:05:19"; (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromUTCOffset)(obj)).to.eql({ zone: "UTC-7", tz: "UTC-7", src: "offset between CreateDate and " + tagname, }); }); it(`with lagging ${tagname} and CreateDate in positive whole-number offset`, () => { const obj = { CreateDate: "2016:07:18 09:54:03", }; obj[tagname] = "2016:07:18 07:41:01Z"; (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromUTCOffset)(obj)).to.eql({ zone: "UTC+2", tz: "UTC+2", src: "offset between CreateDate and " + tagname, }); }); it(`with lagging ${tagname} and SubSecCreateDate in positive :20 offset`, () => { const obj = { SubSecCreateDate: "2016:07:18 09:54:03", }; obj[tagname] = "2016:07:18 04:16:01"; (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromUTCOffset)(obj)).to.eql({ zone: "UTC+5:45", tz: "UTC+5:45", src: "offset between SubSecCreateDate and " + tagname, }); }); it(`with DateTimeUTC and very different created-at DateTime`, () => { const obj = { CreateDate: "2014:07:19 12:05:19", }; obj[tagname] = "2015:07:19 19:05:19"; (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromUTCOffset)(obj)).to.eql(undefined); }); } describe("with archaic timezone offsets", () => { it("rounds archaic Hawaii offset (-10:30) to nearest valid offset (-11:00) by default", () => { Settings_1.Settings.allowArchaicTimezoneOffsets.value = false; // Hawaii archaic offset is -10:30 (630 minutes) // With 30-minute tolerance, rounds to -11:00 (660 minutes) const result = (0, Timezones_1.extractTzOffsetFromUTCOffset)({ DateTimeOriginal: "2024:09:14 12:00:00", DateTimeUTC: "2024:09:14 22:30:00", }); (0, _chai_spec_1.expect)(result?.zone).to.eql("UTC-11"); }); it("accepts archaic Hawaii offset (-10:30) when enabled", () => { Settings_1.Settings.allowArchaicTimezoneOffsets.value = true; (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromUTCOffset)({ DateTimeOriginal: "2024:09:14 12:00:00", DateTimeUTC: "2024:09:14 22:30:00", })).to.eql({ zone: "UTC-10:30", tz: "UTC-10:30", src: "offset between DateTimeOriginal and DateTimeUTC", }); }); it("rounds archaic Venezuela offset (-04:30) to nearest valid offset (-05:00) by default", () => { Settings_1.Settings.allowArchaicTimezoneOffsets.value = false; // Venezuela archaic offset is -04:30 (270 minutes) // With 30-minute tolerance, rounds to -05:00 (300 minutes) const result = (0, Timezones_1.extractTzOffsetFromUTCOffset)({ CreateDate: "2024:09:14 12:00:00", GPSDateTime: "2024:09:14 16:30:00", }); (0, _chai_spec_1.expect)(result?.zone).to.eql("UTC-5"); }); it("accepts archaic Venezuela offset (-04:30) when enabled", () => { Settings_1.Settings.allowArchaicTimezoneOffsets.value = true; (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromUTCOffset)({ CreateDate: "2024:09:14 12:00:00", GPSDateTime: "2024:09:14 16:30:00", })).to.eql({ zone: "UTC-4:30", tz: "UTC-4:30", src: "offset between CreateDate and GPSDateTime", }); }); it("rounds archaic Bombay offset (+04:51) to nearest valid offset (+05:00) by default", () => { Settings_1.Settings.allowArchaicTimezoneOffsets.value = false; // +04:51 is only 9 minutes from +05:00, so it gets rounded const result = (0, Timezones_1.extractTzOffsetFromUTCOffset)({ DateTimeOriginal: "2024:09:14 12:00:00", DateTimeUTC: "2024:09:14 07:09:00", }); (0, _chai_spec_1.expect)(result?.zone).to.eql("UTC+5"); }); it("accepts exact archaic Bombay offset (+04:51) when enabled", () => { Settings_1.Settings.allowArchaicTimezoneOffsets.value = true; (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromUTCOffset)({ DateTimeOriginal: "2024:09:14 12:00:00", DateTimeUTC: "2024:09:14 07:09:00", })).to.eql({ zone: "UTC+4:51", tz: "UTC+4:51", src: "offset between DateTimeOriginal and DateTimeUTC", }); }); }); }); describe("Nikon Daylight Savings Time (#215)", () => { describe("America/Los_Angeles", () => { it("doesn't adjust for non-daylight-savings", () => { const tags = { CreateDate: "2021:11:07 11:22:33", TimeZone: "-08:00", DaylightSavings: "No", Make: "NIKON CORPORATION", }; (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromTags)(tags)).to.eql({ zone: "UTC-8", tz: "UTC-8", src: "TimeZone", }); }); it("adjusts forward by an hour for daylight-savings", () => { const tags = { CreateDate: "2021:07:07 11:22:33", TimeZone: "-08:00", DaylightSavings: "Yes", Make: "NIKON CORPORATION", }; (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromTags)(tags)).to.eql({ zone: "UTC-7", tz: "UTC-7", src: "TimeZone (adjusted for DaylightSavings)", }); }); it("DOESN'T adjust forward by an hour for daylight-savings if adjustment function returns null", () => { const tags = { CreateDate: "2021:07:07 11:22:33", TimeZone: "-08:00", DaylightSavings: "Yes", Make: "NIKON CORPORATION", }; (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromTags)(tags, { adjustTimeZoneIfDaylightSavings: () => undefined, })).to.eql({ zone: "UTC-8", tz: "UTC-8", src: "TimeZone", }); }); it("DOESN'T adjust forward by an hour for daylight-savings if NOT Nikon (by default)", () => { const tags = { CreateDate: "2021:07:07 11:22:33", TimeZone: "-08:00", DaylightSavings: "Yes", Make: "Canon", }; (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromTags)(tags)).to.containSubset({ zone: "UTC-8", tz: "UTC-8", src: "TimeZone", }); }); }); }); describe("Pacific/Auckland", () => { it("doesn't adjust for non-daylight-savings", () => { const tags = { CreateDate: "2021:11:07 11:22:33", TimeZone: "+12:00", DaylightSavings: "No", Make: "NIKON CORPORATION", }; (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromTags)(tags)).to.containSubset({ zone: "UTC+12", tz: "UTC+12", src: "TimeZone", }); }); it("adjusts forward by an hour for daylight-savings", () => { const tags = { CreateDate: "2021:07:07 11:22:33", TimeZone: "+12:00", DaylightSavings: "Yes", Make: "NIKON CORPORATION", }; (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromTags)(tags)).to.eql({ zone: "UTC+13", tz: "UTC+13", src: "TimeZone (adjusted for DaylightSavings)", }); }); it("DOESN'T adjust forward by an hour for daylight-savings if adjustment function returns null", () => { const tags = { CreateDate: "2021:07:07 11:22:33", TimeZone: "+12:00", DaylightSavings: "Yes", Make: "NIKON CORPORATION", }; (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromTags)(tags, { adjustTimeZoneIfDaylightSavings: () => undefined, })).to.containSubset({ zone: "UTC+12", tz: "UTC+12", src: "TimeZone", }); }); it("DOESN'T adjust forward by an hour for daylight-savings if NOT Nikon (by default)", () => { const tags = { CreateDate: "2021:07:07 11:22:33", TimeZone: "+12:00", DaylightSavings: "Yes", Make: "Apple, Inc.", }; (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromTags)(tags)).to.containSubset({ zone: "UTC+12", tz: "UTC+12", src: "TimeZone", }); }); }); describe("with problematic timezone strings", () => { const invalidTimeZones = [ { input: "UTC+25:00", desc: "hours > 24" }, { input: "UTC-25:00", desc: "negative hours < -24" }, { input: "UTC+10:60", desc: "minutes > 59" }, { input: "UTC+10:99", desc: "invalid minutes" }, { input: "UTC+:", desc: "missing values" }, { input: "UTC++10:00", desc: "double plus" }, { input: "UTC--10:00", desc: "double minus" }, { input: "UTC+10::00", desc: "double colon" }, { input: "UTC10:00", desc: "missing sign" }, ]; invalidTimeZones.forEach(({ input, desc }) => { it(`rejects ${desc}: ${input}`, () => { (0, _chai_spec_1.expect)((0, Timezones_1.normalizeZone)(input)).to.eql(undefined); }); }); }); describe("with fractional offsets", () => { const fractionalCases = [ { input: "+05:30", expected: "UTC+5:30" }, { input: "+05:45", expected: "UTC+5:45" }, { input: "-03:30", expected: "UTC-3:30" }, { input: "UTC+05:30", expected: "UTC+5:30" }, { input: "UTC+05:45", expected: "UTC+5:45" }, { input: "UTC-03:30", expected: "UTC-3:30" }, ]; fractionalCases.forEach(({ input, expected }) => { it(`correctly handles ${input}`, () => { const result = (0, Timezones_1.extractZone)(input); (0, _chai_spec_1.expect)(result?.tz).to.eql(expected); }); }); }); describe("timezone equivalence", () => { const equivalentPairs = [ { a: "UTC", b: "GMT" }, { a: "UTC+0", b: "Z" }, { a: "UTC+00:00", b: "Zulu" }, { a: "+00:00", b: "GMT" }, { a: "UTC+5", b: "UTC+05:00" }, { a: "UTC-8", b: "UTC-08:00" }, ]; equivalentPairs.forEach(({ a, b }) => { it(`considers ${a} and ${b} equivalent`, () => { const zoneA = (0, Timezones_1.normalizeZone)(a); const zoneB = (0, Timezones_1.normalizeZone)(b); (0, _chai_spec_1.expect)(zoneA?.equals(zoneB)).to.eql(true); }); }); }); describe("extractTzOffsetFromTags with complex scenarios", () => { it("handles multiple timezone tags with different values", () => { const tags = { TimeZone: "+09:00", TimeZoneOffset: "+07:00", }; // Should use first valid timezone found in order of precedence (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromTags)(tags)).to.containSubset({ zone: "UTC+9", tz: "UTC+9", src: "TimeZone", }); }); it("handles array values in TimeZoneOffset", () => { const tags = { TimeZoneOffset: ["-8", "-7"], // Some cameras provide multiple offsets }; (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromTags)(tags)).to.containSubset({ zone: "UTC-8", tz: "UTC-8", src: "TimeZoneOffset", }); }); it("handles null and undefined values gracefully", () => { const tags = { TimeZone: null, TimeZoneOffset: "", }; (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromTags)(tags)).to.eql(undefined); }); }); describe("Settings.allowArchaicTimezoneOffsets", () => { afterEach(() => { // Reset to default after each test Settings_1.Settings.reset(); }); describe("archaic offsets are rejected by default", () => { for (const offset of Timezones_1.ArchaicTimezoneOffsets) { it(`rejects ${offset} when allowArchaicTimezoneOffsets is false`, () => { Settings_1.Settings.allowArchaicTimezoneOffsets.value = false; (0, _chai_spec_1.expect)((0, Timezones_1.validTzOffsetMinutes)(offsetToMinutes(offset))).to.eql(false); (0, _chai_spec_1.expect)((0, Timezones_1.extractZone)(offset)).to.eql(undefined); }); } }); describe("archaic offsets are accepted when enabled", () => { for (const offset of Timezones_1.ArchaicTimezoneOffsets) { it(`accepts ${offset} when allowArchaicTimezoneOffsets is true`, () => { Settings_1.Settings.allowArchaicTimezoneOffsets.value = true; const minutes = offsetToMinutes(offset); (0, _chai_spec_1.expect)((0, Timezones_1.validTzOffsetMinutes)(minutes)).to.eql(true); const zoneName = (0, Timezones_1.offsetMinutesToZoneName)(minutes); (0, _chai_spec_1.expect)(zoneName).to.not.eql(undefined); }); } }); it("resetToDefaults() restores default behavior", () => { Settings_1.Settings.allowArchaicTimezoneOffsets.value = true; Settings_1.Settings.reset(); (0, _chai_spec_1.expect)(Settings_1.Settings.allowArchaicTimezoneOffsets.value).to.eql(false); // Should reject archaic offset after reset (0, _chai_spec_1.expect)((0, Timezones_1.extractZone)("-10:30")).to.eql(undefined); }); describe("specific archaic offset examples", () => { it("rejects Hawaii archaic offset (-10:30) by default", () => { Settings_1.Settings.allowArchaicTimezoneOffsets.value = false; const tags = { TimeZone: "-10:30" }; (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromTags)(tags)).to.eql(undefined); }); it("accepts Hawaii archaic offset (-10:30) when enabled", () => { Settings_1.Settings.allowArchaicTimezoneOffsets.value = true; const tags = { TimeZone: "-10:30" }; const result = (0, Timezones_1.extractTzOffsetFromTags)(tags); (0, _chai_spec_1.expect)(result?.zone).to.match(/UTC-10:30/); }); it("rejects Bombay archaic offset (+04:51) by default", () => { Settings_1.Settings.allowArchaicTimezoneOffsets.value = false; const tags = { TimeZone: "+04:51" }; (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromTags)(tags)).to.eql(undefined); }); it("accepts Bombay archaic offset (+04:51) when enabled", () => { Settings_1.Settings.allowArchaicTimezoneOffsets.value = true; const tags = { TimeZone: "+04:51" }; const result = (0, Timezones_1.extractTzOffsetFromTags)(tags); (0, _chai_spec_1.expect)(result?.zone).to.match(/UTC\+4:51/); }); it("rejects Ireland archaic offset (-00:25:21) by default", () => { Settings_1.Settings.allowArchaicTimezoneOffsets.value = false; const tags = { TimeZone: "-00:25:21" }; (0, _chai_spec_1.expect)((0, Timezones_1.extractTzOffsetFromTags)(tags)).to.eql(undefined); }); it("accepts Ireland archaic offset (-00:25:21) when enabled", () => { Settings_1.Settings.allowArchaicTimezoneOffsets.value = true; const tags = { TimeZone: "-00:25:21" }; const result = (0, Timezones_1.extractTzOffsetFromTags)(tags); // Note: Luxon zone names may not preserve seconds precision (0, _chai_spec_1.expect)(result).to.not.eql(undefined); (0, _chai_spec_1.expect)(result?.zone).to.match(/^UTC-0:/); }); }); describe("cache invalidation", () => { it("cache is invalidated when setting changes from false to true", () => { // Start with archaic offsets disabled Settings_1.Settings.allowArchaicTimezoneOffsets.value = false; const hawaiiMinutes = offsetToMinutes("-10:30"); (0, _chai_spec_1.expect)((0, Timezones_1.validTzOffsetMinutes)(hawaiiMinutes)).to.eql(false); // Enable archaic offsets Settings_1.Settings.allowArchaicTimezoneOffsets.value = true; // Cache should be invalidated, so now it should be valid (0, _chai_spec_1.expect)((0, Timezones_1.validTzOffsetMinutes)(hawaiiMinutes)).to.eql(true); }); it("cache is invalidated when setting changes from true to false", () => { // Start with archaic offsets enabled Settings_1.Settings.allowArchaicTimezoneOffsets.value = true; const bombayMinutes = offsetToMinutes("+04:51"); (0, _chai_spec_1.expect)((0, Timezones_1.validTzOffsetMinutes)(bombayMinutes)).to.eql(true); // Disable archaic offsets Settings_1.Settings.allowArchaicTimezoneOffsets.value = false; // Cache should be invalidated, so now it should be invalid (0, _chai_spec_1.expect)((0, Timezones_1.validTzOffsetMinutes)(bombayMinutes)).to.eql(false); }); it("cache works correctly after multiple setting toggles", () => { const venezuelaMinutes = offsetToMinutes("-04:30"); Settings_1.Settings.allowArchaicTimezoneOffsets.value = false; (0, _chai_spec_1.expect)((0, Timezones_1.validTzOffsetMinutes)(venezuelaMinutes)).to.eql(false); Settings_1.Settings.allowArchaicTimezoneOffsets.value = true; (0, _chai_spec_1.expect)((0, Timezones_1.validTzOffsetMinutes)(venezuelaMinutes)).to.eql(true); Settings_1.Settings.allowArchaicTimezoneOffsets.value = false; (0, _chai_spec_1.expect)((0, Timezones_1.validTzOffsetMinutes)(venezuelaMinutes)).to.eql(false); Settings_1.Settings.allowArchaicTimezoneOffsets.value = true; (0, _chai_spec_1.expect)((0, Timezones_1.validTzOffsetMinutes)(venezuelaMinutes)).to.eql(true); }); it("extractZone respects cache invalidation", () => { Settings_1.Settings.allowArchaicTimezoneOffsets.value = false; (0, _chai_spec_1.expect)((0, Timezones_1.extractZone)("+04:51")).to.eql(undefined); Settings_1.Settings.allowArchaicTimezoneOffsets.value = true; const result = (0, Timezones_1.extractZone)("+04:51"); (0, _chai_spec_1.expect)(result).to.not.eql(undefined); (0, _chai_spec_1.expect)(result?.zone).to.match(/UTC\+4:51/); Settings_1.Settings.allowArchaicTimezoneOffsets.value = false; (0, _chai_spec_1.expect)((0, Timezones_1.extractZone)("+04:51")).to.eql(undefined); }); it("offsetMinutesToZoneName respects cache invalidation", () => { const irelandMinutes = offsetToMinutes("-00:25:21"); Settings_1.Settings.allowArchaicTimezoneOffsets.value = false; (0, _chai_spec_1.expect)((0, Timezones_1.offsetMinutesToZoneName)(irelandMinutes)).to.eql(undefined); Settings_1.Settings.allowArchaicTimezoneOffsets.value = true; const zoneName = (0, Timezones_1.offsetMinutesToZoneName)(irelandMinutes); (0, _chai_spec_1.expect)(zoneName).to.not.eql(undefined); (0, _chai_spec_1.expect)(zoneName).to.match(/UTC-/); Settings_1.Settings.allowArchaicTimezoneOffsets.value = false; (0, _chai_spec_1.expect)((0, Timezones_1.offsetMinutesToZoneName)(irelandMinutes)).to.eql(undefined); }); }); }); describe("isUTC()", () => { it("returns true for UTC string", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)("UTC")).to.eql(true); }); it("returns true for GMT string", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)("GMT")).to.eql(true); }); it("returns true for Z", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)("Z")).to.eql(true); }); it("returns false for Zulu (must normalize first)", () => { // "Zulu" gets normalized to "UTC" in normalizeZone, but isUTC doesn't normalize (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)("Zulu")).to.eql(false); // After normalizing, it's UTC: const zone = (0, Timezones_1.normalizeZone)("Zulu"); (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)(zone)).to.eql(true); }); it("returns true for numeric 0", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)(0)).to.eql(true); }); it("returns true for numeric -0", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)(-0)).to.eql(true); }); it("returns true for +0", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)("+0")).to.eql(true); }); it("returns true for +00:00", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)("+00:00")).to.eql(true); }); it("returns true for UTC+0", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)("UTC+0")).to.eql(true); }); it("returns true for GMT+0", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)("GMT+0")).to.eql(true); }); it("returns true for UTC+00:00", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)("UTC+00:00")).to.eql(true); }); it("returns true for GMT+00:00", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)("GMT+00:00")).to.eql(true); }); it("returns true for UTC Zone instance", () => { const zone = luxon_1.Info.normalizeZone("UTC"); (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)(zone)).to.eql(true); }); it("returns false for America/New_York", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)("America/New_York")).to.eql(false); }); it("returns false for +08:00", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)("+08:00")).to.eql(false); }); it("returns false for numeric 480", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)(480)).to.eql(false); }); it("returns false for null", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)(null)).to.eql(false); }); it("returns false for undefined", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)(undefined)).to.eql(false); }); it("returns false for non-UTC Zone instance", () => { const zone = luxon_1.Info.normalizeZone("America/Los_Angeles"); (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)(zone)).to.eql(false); }); // New tests for expanded Zulus array it("returns true for string '0'", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)("0")).to.eql(true); }); it("returns true for Etc/UTC", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)("Etc/UTC")).to.eql(true); }); it("returns true for +00", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)("+00")).to.eql(true); }); it("returns true for -00", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)("-00")).to.eql(true); }); it("returns true for -00:00", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)("-00:00")).to.eql(true); }); it("returns false for non-zero numeric offsets", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)(1)).to.eql(false); (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)(7)).to.eql(false); (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)(-7)).to.eql(false); (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)(60)).to.eql(false); (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)(-60)).to.eql(false); }); // Systematic test of all ValidTimezoneOffsets describe("ValidTimezoneOffsets coverage", () => { for (const offset of Timezones_1.ValidTimezoneOffsets) { const expected = offset === "+00:00"; it(`isUTC("${offset}") === ${expected}`, () => { (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)(offset)).to.eql(expected); }); } }); }); describe("isZoneUnset()", () => { it("returns true for UnsetZone", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isZoneUnset)(Timezones_1.UnsetZone)).to.eql(true); }); it("returns false for UTC", () => { const zone = luxon_1.Info.normalizeZone("UTC"); if (zone) (0, _chai_spec_1.expect)((0, Timezones_1.isZoneUnset)(zone)).to.eql(false); }); it("returns false for UTC+8", () => { const zone = luxon_1.Info.normalizeZone("UTC+8"); if (zone) (0, _chai_spec_1.expect)((0, Timezones_1.isZoneUnset)(zone)).to.eql(false); }); it("returns false for IANA zone", () => { const zone = luxon_1.Info.normalizeZone("America/New_York"); if (zone) (0, _chai_spec_1.expect)((0, Timezones_1.isZoneUnset)(zone)).to.eql(false); }); }); describe("isZoneValid()", () => { it("returns true for UTC", () => { const zone = luxon_1.Info.normalizeZone("UTC"); (0, _chai_spec_1.expect)((0, Timezones_1.isZoneValid)(zone)).to.eql(true); }); it("returns true for UTC+8", () => { const zone = luxon_1.Info.normalizeZone("UTC+8"); (0, _chai_spec_1.expect)((0, Timezones_1.isZoneValid)(zone)).to.eql(true); }); it("returns true for America/Los_Angeles", () => { const zone = luxon_1.Info.normalizeZone("America/Los_Angeles"); (0, _chai_spec_1.expect)((0, Timezones_1.isZoneValid)(zone)).to.eql(true); }); it("returns false for UnsetZone", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isZoneValid)(Timezones_1.UnsetZone)).to.eql(false); }); it("returns false for invalid zone", () => { const zone = luxon_1.Info.normalizeZone("invalid"); (0, _chai_spec_1.expect)((0, Timezones_1.isZoneValid)(zone)).to.eql(false); }); it("returns false for null", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isZoneValid)(null)).to.eql(false); }); it("returns false for undefined", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isZoneValid)(undefined)).to.eql(false); }); it("returns false for zone beyond ±14 hours (UTC+20)", () => { // Create a zone with offset beyond valid range const zone = luxon_1.Info.normalizeZone("UTC+20"); (0, _chai_spec_1.expect)((0, Timezones_1.isZoneValid)(zone)).to.eql(false); }); }); describe("isZone()", () => { it("returns true for Zone instance from normalizeZone", () => { const zone = luxon_1.Info.normalizeZone("UTC"); (0, _chai_spec_1.expect)((0, Timezones_1.isZone)(zone)).to.eql(true); }); it("returns true for IANA Zone", () => { const zone = luxon_1.Info.normalizeZone("America/Los_Angeles"); (0, _chai_spec_1.expect)((0, Timezones_1.isZone)(zone)).to.eql(true); }); it("returns false for string", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isZone)("UTC")).to.eql(false); }); it("returns false for number", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isZone)(480)).to.eql(false); }); it("returns false for null", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isZone)(null)).to.eql(false); }); it("returns false for undefined", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isZone)(undefined)).to.eql(false); }); it("returns false for plain object", () => { (0, _chai_spec_1.expect)((0, Timezones_1.isZone)({ name: "UTC" })).to.eql(false); }); }); describe("zoneToShortOffset()", () => { it("converts UTC+8 to +08:00", () => { (0, _chai_spec_1.expect)((0, Timezones_1.zoneToShortOffset)("UTC+8")).to.eql("+08:00"); }); it("converts numeric 480 to +08:00", () => { (0, _chai_spec_1.expect)((0, Timezones_1.zoneToShortOffset)(480)).to.eql("+08:00"); }); it("converts UTC-3:30 to -03:30", () => { (0, _chai_spec_1.expect)((0, Timezones_1.zoneToShortOffset)("UTC-3:30")).to.eql("-03:30"); }); it("converts UTC to +00:00", () => { (0, _chai_spec_1.expect)((0, Timezones_1.zoneToShortOffset)("UTC")).to.eql("+00:00"); }); it("handles IANA zone with timestamp in winter (PST)", () => { const winter = new Date("2023-01-15").getTime(); (0, _chai_spec_1.expect)((0, Timezones_1.zoneToShortOffset)("America/Los_Angeles", winter)).to.eql("-08:00"); }); it("handles IANA zone with timestamp in summer (PDT)", () => { const summer = new Date("2023-07-15").getTime(); (0, _chai_spec_1.expect)((0, Timezones_1.zoneToShortOffset)("America/Los_Angeles", summer)).to.eql("-07:00"); }); it("returns empty string for invalid zone", () => { (0, _chai_spec_1.expect)((0, Timezones_1.zoneToShortOffset)("invalid")).to.eql(""); }); it("returns empty string for null", () => { (0, _chai_spec_1.expect)((0, Timezones_1.zoneToShortOffset)(null)).to.eql(""); }); it("returns empty string for undefined", () => { (0, _chai_spec_1.expect)((0, Timezones_1.zoneToShortOffset)(undefined)).to.eql(""); }); }); describe("incrementZone()", () => { it("increments UTC+8 by 60 minutes to UTC+9", () => { const result = (0, Timezones_1.incrementZone)("UTC+8", 60); (0, _chai_spec_1.expect)(result?.name).to.eql("UTC+9"); }); it("increments UTC-5 by -60 minutes to UTC-6", () => { const result = (0, Timezones_1.incrementZone)("UTC-5", -60); (0, _chai_spec_1.expect)(result?.name).to.eql("UTC-6"); }); it("increments UTC by 30 minutes to UTC+0:30", () => { const result = (0, Timezones_1.incrementZone)("UTC", 30); (0, _chai_spec_1.expect)(result?.name).to.eql("UTC+0:30"); }); it("increments numeric offset 480 by 60 to 540", () => { const result = (0, Timezones_1.incrementZone)(480, 60); (0, _chai_spec_1.expect)(result?.offset(0)).to.eql(540); }); it("returns undefined for IANA zone (not universal)", () => { const result = (0, Timezones_1.incrementZone)("America/Los_Angeles", 60); (0, _chai_spec_1.expect)(result).to.eql(undefined); }); it("returns undefined for invalid zone", () => { const result = (0, Timezones_1.incrementZone)("invalid", 60); (0, _chai_spec_1.expect)(result).to.eql(undefined); }); }); describe("inferLikelyOffsetMinutes()", () => { afterEach(() => { Settings_1.Settings.reset(); }); it("returns exact match for 480 (UTC+8)", () => { (0, _chai_spec_1.expect)((0, Timezones_1.inferLikelyOffsetMinutes)(480)).to.eql(480); }); it("returns exact match for -300 (UTC-5)", () => { (0, _chai_spec_1.expect)((0, Timezones_1.inferLikelyOffsetMinutes)(-300)).to.eql(-300); }); it("rounds 485 to 480 (within 30 min default threshold)", () => { (0, _chai_spec_1.expect)((0, Timezones_1.inferLikelyOffsetMinutes)(485)).to.eql(480); }); it("rounds -295 to -300 (within 30 min default threshold)", () => { (0, _chai_spec_1.expect)((0, Timezones_1.inferLikelyOffsetMinutes)(-295)).to.eql(-300); }); it("rounds 330.5 to 330 (UTC+5:30)", () => { (0, _chai_spec_1.expect)((0, Timezones_1.inferLikelyOffsetMinutes)(330.5)).to.eql(330); }); it("rounds 495 to 480 (within 30 min default threshold)", () => { (0, _chai_spec_1.expect)((0, Timezones_1.inferLikelyOffsetMinutes)(495)).to.eql(480); }); it("rounds 505 to 510 (within 30 min default threshold)", () => { (0, _chai_spec_1.expect)((0, Timezones_1.inferLikelyOffsetMinutes)(505)).to.eql(510); }); it("rounds 100 to 120 (UTC+2, within 30 min default threshold)", () => { // 100 minutes is 20 minutes from 120 (UTC+2), within 30-minute threshold (0, _chai_spec_1.expect)((0, Timezones_1.inferLikelyOffsetMinutes)(100)).to.eql(120); }); it("rounds 666 to 660 (UTC+11, within 30 min threshold)", () => { // 666 minutes is only 6 minutes from UTC+11 (660 minutes) (0, _chai_spec_1.expect)((0, Timez