exiftool-vendored
Version:
Efficient, cross-platform access to ExifTool
988 lines • 71.6 kB
JavaScript
"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