exiftool-vendored 33.2.0 → 33.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/ExifTool.d.ts +2 -1
- package/dist/ExifTool.js +16 -1
- package/dist/ExifTool.js.map +1 -1
- package/dist/Settings.d.ts +16 -0
- package/dist/Settings.js +16 -0
- package/dist/Settings.js.map +1 -1
- package/dist/Timezones.d.ts +554 -13
- package/dist/Timezones.js +641 -78
- package/dist/Timezones.js.map +1 -1
- package/package.json +5 -5
package/dist/Timezones.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TimezoneOffsetTagnames = exports.defaultVideosToUTC = exports.UnsetZoneName = exports.UnsetZone = exports.UnsetZoneOffsetMinutes = exports.ValidTimezoneOffsets = exports.ArchaicTimezoneOffsets = void 0;
|
|
3
|
+
exports.TimezoneOffsetTagnames = exports.TimezoneOffsetRE = exports.defaultVideosToUTC = exports.UnsetZoneName = exports.UnsetZone = exports.UnsetZoneOffsetMinutes = exports.ValidTimezoneOffsets = exports.ArchaicTimezoneOffsets = void 0;
|
|
4
4
|
exports.isUTC = isUTC;
|
|
5
5
|
exports.isZoneUnset = isZoneUnset;
|
|
6
6
|
exports.isZoneValid = isZoneValid;
|
|
7
7
|
exports.isZone = isZone;
|
|
8
|
+
exports.parseTimezoneOffsetMatch = parseTimezoneOffsetMatch;
|
|
9
|
+
exports.parseTimezoneOffsetToMinutes = parseTimezoneOffsetToMinutes;
|
|
8
10
|
exports.normalizeZone = normalizeZone;
|
|
9
11
|
exports.zoneToShortOffset = zoneToShortOffset;
|
|
10
12
|
exports.validTzOffsetMinutes = validTzOffsetMinutes;
|
|
@@ -110,14 +112,11 @@ exports.ValidTimezoneOffsets = [
|
|
|
110
112
|
"+13:45", // New Zealand islands
|
|
111
113
|
"+14:00",
|
|
112
114
|
];
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return sign * (h * 60 + m + s / 60);
|
|
119
|
-
}
|
|
120
|
-
const validOffsetMinutes = (0, Lazy_1.lazy)(() => {
|
|
115
|
+
/**
|
|
116
|
+
* Get all valid timezone offset minutes based on current settings.
|
|
117
|
+
* Used by both validOffsetMinutes (Set) and likelyOffsetMinutes (Array).
|
|
118
|
+
*/
|
|
119
|
+
function getValidOffsetMinutes() {
|
|
121
120
|
const offsets = [...exports.ValidTimezoneOffsets];
|
|
122
121
|
if (Settings_1.Settings.allowArchaicTimezoneOffsets.value) {
|
|
123
122
|
offsets.push(...exports.ArchaicTimezoneOffsets);
|
|
@@ -125,15 +124,18 @@ const validOffsetMinutes = (0, Lazy_1.lazy)(() => {
|
|
|
125
124
|
if (Settings_1.Settings.allowBakerIslandTime.value) {
|
|
126
125
|
offsets.push("-12:00");
|
|
127
126
|
}
|
|
128
|
-
return
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
127
|
+
return offsets.map(parseTimezoneOffsetToMinutes).filter(Number_1.isNumber);
|
|
128
|
+
}
|
|
129
|
+
const validOffsetMinutes = (0, Lazy_1.lazy)(() => new Set(getValidOffsetMinutes()));
|
|
130
|
+
// Used for fuzzy matching in inferLikelyOffsetMinutes
|
|
131
|
+
const likelyOffsetMinutes = (0, Lazy_1.lazy)(() => getValidOffsetMinutes());
|
|
132
|
+
// Invalidate both caches when relevant settings change
|
|
133
|
+
function clearOffsetCaches() {
|
|
135
134
|
validOffsetMinutes.clear();
|
|
136
|
-
|
|
135
|
+
likelyOffsetMinutes.clear();
|
|
136
|
+
}
|
|
137
|
+
Settings_1.Settings.allowArchaicTimezoneOffsets.onChange(clearOffsetCaches);
|
|
138
|
+
Settings_1.Settings.allowBakerIslandTime.onChange(clearOffsetCaches);
|
|
137
139
|
/**
|
|
138
140
|
* Zone instances with this offset are a placeholder for being "unset".
|
|
139
141
|
*/
|
|
@@ -151,16 +153,44 @@ const Zulus = [
|
|
|
151
153
|
luxon_1.FixedOffsetZone.utcInstance,
|
|
152
154
|
0,
|
|
153
155
|
-0,
|
|
156
|
+
"0", // String zero - some systems output this for numeric offsets
|
|
154
157
|
"UTC",
|
|
155
158
|
"GMT",
|
|
156
159
|
"Z",
|
|
160
|
+
"Etc/UTC", // Valid IANA timezone name for UTC
|
|
157
161
|
"+0",
|
|
162
|
+
"+00",
|
|
163
|
+
"-00",
|
|
158
164
|
"+00:00",
|
|
165
|
+
"-00:00",
|
|
159
166
|
"UTC+0",
|
|
160
167
|
"GMT+0",
|
|
161
168
|
"UTC+00:00",
|
|
162
169
|
"GMT+00:00",
|
|
163
170
|
];
|
|
171
|
+
/**
|
|
172
|
+
* Check if a timezone value represents UTC.
|
|
173
|
+
*
|
|
174
|
+
* Handles multiple UTC representations including Zone instances, strings, and
|
|
175
|
+
* numeric offsets. Recognizes common UTC aliases like "GMT", "Z", "Zulu",
|
|
176
|
+
* "+0", "+00:00", etc.
|
|
177
|
+
*
|
|
178
|
+
* @param zone - Timezone to check (Zone, string, or number)
|
|
179
|
+
* @returns true if the zone represents UTC/GMT/Zulu
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* ```typescript
|
|
183
|
+
* isUTC("UTC") // true
|
|
184
|
+
* isUTC("GMT") // true
|
|
185
|
+
* isUTC("Z") // true
|
|
186
|
+
* isUTC("Zulu") // true
|
|
187
|
+
* isUTC(0) // true
|
|
188
|
+
* isUTC("+00:00") // true
|
|
189
|
+
* isUTC("UTC+0") // true
|
|
190
|
+
* isUTC("America/New_York") // false
|
|
191
|
+
* isUTC("+08:00") // false
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
164
194
|
function isUTC(zone) {
|
|
165
195
|
if (zone == null) {
|
|
166
196
|
return false;
|
|
@@ -168,22 +198,105 @@ function isUTC(zone) {
|
|
|
168
198
|
if (typeof zone === "string" || typeof zone === "number") {
|
|
169
199
|
return Zulus.includes(zone);
|
|
170
200
|
}
|
|
171
|
-
if (zone
|
|
201
|
+
if (isZone(zone)) {
|
|
172
202
|
return zone.isUniversal && zone.offset(Date.now()) === 0;
|
|
173
203
|
}
|
|
174
204
|
return false;
|
|
175
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Check if a Zone is the library's sentinel "unset" value.
|
|
208
|
+
*
|
|
209
|
+
* The library uses a special Zone instance to represent unknown/unset
|
|
210
|
+
* timezones since Luxon doesn't officially support unset zones.
|
|
211
|
+
*
|
|
212
|
+
* @param zone - Zone instance to check
|
|
213
|
+
* @returns true if the zone is the UnsetZone sentinel value
|
|
214
|
+
*
|
|
215
|
+
* @see {@link UnsetZone}
|
|
216
|
+
* @see {@link UnsetZoneName}
|
|
217
|
+
* @see {@link UnsetZoneOffsetMinutes}
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* ```typescript
|
|
221
|
+
* isZoneUnset(UnsetZone) // true
|
|
222
|
+
* isZoneUnset(Info.normalizeZone("UTC")) // false
|
|
223
|
+
* isZoneUnset(Info.normalizeZone("UTC+8")) // false
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
176
226
|
function isZoneUnset(zone) {
|
|
177
|
-
return zone
|
|
227
|
+
return (isZone(zone) &&
|
|
228
|
+
zone.isUniversal &&
|
|
229
|
+
zone.offset(0) === exports.UnsetZoneOffsetMinutes);
|
|
178
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Type guard to check if a Zone is valid and usable.
|
|
233
|
+
*
|
|
234
|
+
* A zone is considered valid if it:
|
|
235
|
+
* - Is not null/undefined
|
|
236
|
+
* - Has `isValid === true` (Luxon requirement)
|
|
237
|
+
* - Is not the library's UnsetZone sentinel
|
|
238
|
+
* - Has an offset within ±14 hours (the valid range for real-world timezones)
|
|
239
|
+
*
|
|
240
|
+
* This is the canonical validation check used throughout the library.
|
|
241
|
+
*
|
|
242
|
+
* @param zone - Zone to validate
|
|
243
|
+
* @returns true if the zone is valid and usable (type guard)
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```typescript
|
|
247
|
+
* const zone = Info.normalizeZone("America/Los_Angeles")
|
|
248
|
+
* if (isZoneValid(zone)) {
|
|
249
|
+
* // TypeScript knows zone is Zone (not Zone | undefined)
|
|
250
|
+
* console.log(zone.name)
|
|
251
|
+
* }
|
|
252
|
+
*
|
|
253
|
+
* isZoneValid(Info.normalizeZone("invalid")) // false
|
|
254
|
+
* isZoneValid(Info.normalizeZone("UTC+8")) // true
|
|
255
|
+
* isZoneValid(UnsetZone) // false
|
|
256
|
+
* isZoneValid(Info.normalizeZone("UTC+20")) // false (beyond ±14 hours)
|
|
257
|
+
* ```
|
|
258
|
+
*/
|
|
179
259
|
function isZoneValid(zone) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
260
|
+
// Note: isZone() already verifies zone.isValid
|
|
261
|
+
if (!isZone(zone) || isZoneUnset(zone)) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
// For fixed offset zones, verify the offset is in our valid set
|
|
265
|
+
if (zone.isUniversal) {
|
|
266
|
+
return validTzOffsetMinutes(zone.offset(Date.now()));
|
|
267
|
+
}
|
|
268
|
+
// For IANA zones, we trust that Luxon has validated them
|
|
269
|
+
return true;
|
|
184
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* Type guard to check if a value is a Luxon Zone instance.
|
|
273
|
+
*
|
|
274
|
+
* Checks both `instanceof Zone` and constructor name to handle cross-module
|
|
275
|
+
* Zone instances that may not pass instanceof checks.
|
|
276
|
+
*
|
|
277
|
+
* @param zone - Value to check
|
|
278
|
+
* @returns true if the value is a Zone instance (type guard)
|
|
279
|
+
*
|
|
280
|
+
* @example
|
|
281
|
+
* ```typescript
|
|
282
|
+
* import { Info } from "luxon"
|
|
283
|
+
*
|
|
284
|
+
* const zone = Info.normalizeZone("UTC+8")
|
|
285
|
+
* if (isZone(zone)) {
|
|
286
|
+
* // TypeScript knows zone is Zone (not unknown)
|
|
287
|
+
* console.log(zone.offset(Date.now()))
|
|
288
|
+
* }
|
|
289
|
+
*
|
|
290
|
+
* isZone(Info.normalizeZone("UTC")) // true
|
|
291
|
+
* isZone("UTC") // false
|
|
292
|
+
* isZone(480) // false
|
|
293
|
+
* isZone(null) // false
|
|
294
|
+
* ```
|
|
295
|
+
*/
|
|
185
296
|
function isZone(zone) {
|
|
186
|
-
return ((0, Object_1.isObject)(zone) &&
|
|
297
|
+
return ((0, Object_1.isObject)(zone) &&
|
|
298
|
+
(zone instanceof luxon_1.Zone || zone.constructor.name === "Zone") &&
|
|
299
|
+
zone.isValid);
|
|
187
300
|
}
|
|
188
301
|
/**
|
|
189
302
|
* If `tzSource` matches this value, the tags are from a video, and we had to
|
|
@@ -195,37 +308,184 @@ exports.defaultVideosToUTC = "defaultVideosToUTC";
|
|
|
195
308
|
// "WET" and "W-SU" are full TZs (!!!), and "America/Indiana/Indianapolis" is
|
|
196
309
|
// also a thing.
|
|
197
310
|
const IanaFormatRE = /^\w{2,15}(?:\/\w{3,15}){0,2}$/;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
311
|
+
const OffsetStringRE = /^(?:UTC|GMT)?(?<sign>[+−-])(?<hours>\d{1,2})(?::(?<minutes>\d{2})(?::(?<seconds>\d{2}))?)?$/;
|
|
312
|
+
const MinusRE = /[−-]/;
|
|
313
|
+
/**
|
|
314
|
+
* Composable regex pattern for matching timezone offsets.
|
|
315
|
+
*
|
|
316
|
+
* Designed for embedding in larger patterns (no ^ or $ anchors).
|
|
317
|
+
* Matches UTC/GMT/Z or signed offsets like +08:00, -05:30.
|
|
318
|
+
*
|
|
319
|
+
* Named capture groups:
|
|
320
|
+
* - `tz_utc`: Matches "Z", "UTC", or "GMT"
|
|
321
|
+
* - `tz_sign`: The sign character (+, -, or Unicode minus −)
|
|
322
|
+
* - `tz_hours`: Hour offset (1-2 digits)
|
|
323
|
+
* - `tz_minutes`: Optional minute offset (2 digits)
|
|
324
|
+
*
|
|
325
|
+
* @example
|
|
326
|
+
* ```typescript
|
|
327
|
+
* // Concatenate with other patterns
|
|
328
|
+
* const dateTimeRE = new RegExp(
|
|
329
|
+
* `(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})${TimezoneOffsetRE.source}`
|
|
330
|
+
* )
|
|
331
|
+
*
|
|
332
|
+
* // Use standalone
|
|
333
|
+
* const match = TimezoneOffsetRE.exec("2023-01-15T10:30:00-08:00")
|
|
334
|
+
* if (match?.groups) {
|
|
335
|
+
* const { tz_sign, tz_hours, tz_minutes } = match.groups
|
|
336
|
+
* // tz_sign = "-", tz_hours = "08", tz_minutes = "00"
|
|
337
|
+
* }
|
|
338
|
+
* ```
|
|
339
|
+
*/
|
|
340
|
+
exports.TimezoneOffsetRE = /(?:(?<tz_utc>Z|UTC|GMT)|(?<tz_sign>[+−-])(?<tz_hours>[01]?\d)(?::(?<tz_minutes>\d\d))?)/;
|
|
341
|
+
/**
|
|
342
|
+
* Parse timezone offset from a regex match result.
|
|
343
|
+
*
|
|
344
|
+
* Use with {@link TimezoneOffsetRE} to extract offset minutes from a match.
|
|
345
|
+
*
|
|
346
|
+
* @param match - RegExp exec result from TimezoneOffsetRE
|
|
347
|
+
* @returns Parsed offset info, or undefined if match is invalid
|
|
348
|
+
*
|
|
349
|
+
* @example
|
|
350
|
+
* ```typescript
|
|
351
|
+
* const match = TimezoneOffsetRE.exec("2023-01-15T10:30:00-08:00")
|
|
352
|
+
* const result = parseTimezoneOffsetMatch(match)
|
|
353
|
+
* // { offsetMinutes: -480, isUtc: false }
|
|
354
|
+
* ```
|
|
355
|
+
*/
|
|
356
|
+
function parseTimezoneOffsetMatch(match) {
|
|
357
|
+
if (match?.groups == null)
|
|
204
358
|
return;
|
|
205
|
-
const
|
|
206
|
-
|
|
359
|
+
const { tz_utc, tz_sign, tz_hours, tz_minutes } = match.groups;
|
|
360
|
+
if (tz_utc != null) {
|
|
361
|
+
return { offsetMinutes: 0, isUtc: true };
|
|
362
|
+
}
|
|
363
|
+
if (tz_sign == null || tz_hours == null)
|
|
364
|
+
return;
|
|
365
|
+
const h = (0, Number_1.toInt)(tz_hours);
|
|
366
|
+
const m = (0, Number_1.toInt)(tz_minutes) ?? 0;
|
|
207
367
|
if (h == null || h < 0 || h > 14 || m < 0 || m >= 60)
|
|
208
368
|
return;
|
|
209
|
-
const
|
|
210
|
-
return
|
|
369
|
+
const signValue = MinusRE.test(tz_sign) ? -1 : 1;
|
|
370
|
+
return {
|
|
371
|
+
offsetMinutes: signValue * (h * 60 + m),
|
|
372
|
+
isUtc: false,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Parse a timezone offset string to offset minutes.
|
|
377
|
+
*
|
|
378
|
+
* Accepts multiple formats:
|
|
379
|
+
* - ISO 8601: "+08:00", "-05:30", "Z"
|
|
380
|
+
* - Luxon format: "UTC+8", "GMT-5"
|
|
381
|
+
* - UTC variants: "UTC", "GMT", "Zulu"
|
|
382
|
+
*
|
|
383
|
+
* Supports seconds for archaic offsets like "-00:25:21" (Ireland 1880-1916).
|
|
384
|
+
*
|
|
385
|
+
* **Note:** Does NOT validate that the offset is a real-world timezone offset.
|
|
386
|
+
* Use {@link validTzOffsetMinutes} for validation.
|
|
387
|
+
*
|
|
388
|
+
* @param str - Timezone offset string
|
|
389
|
+
* @returns Offset in minutes, or undefined if invalid
|
|
390
|
+
*
|
|
391
|
+
* @example
|
|
392
|
+
* ```typescript
|
|
393
|
+
* parseTimezoneOffsetToMinutes("+08:00") // 480
|
|
394
|
+
* parseTimezoneOffsetToMinutes("UTC-5") // -300
|
|
395
|
+
* parseTimezoneOffsetToMinutes("Z") // 0
|
|
396
|
+
* parseTimezoneOffsetToMinutes("-00:25:21") // -25.35 (archaic Ireland)
|
|
397
|
+
* parseTimezoneOffsetToMinutes("invalid") // undefined
|
|
398
|
+
* ```
|
|
399
|
+
*/
|
|
400
|
+
function parseTimezoneOffsetToMinutes(str) {
|
|
401
|
+
if (isUTC(str))
|
|
402
|
+
return 0;
|
|
403
|
+
const match = OffsetStringRE.exec(str);
|
|
404
|
+
if (match?.groups == null)
|
|
405
|
+
return;
|
|
406
|
+
const { hours, minutes, seconds, sign } = match.groups;
|
|
407
|
+
if (hours == null || sign == null)
|
|
408
|
+
return;
|
|
409
|
+
const h = (0, Number_1.toInt)(hours);
|
|
410
|
+
const m = (0, Number_1.toInt)(minutes) ?? 0;
|
|
411
|
+
const s = (0, Number_1.toInt)(seconds) ?? 0;
|
|
412
|
+
if (h == null || h < 0 || h > 14 || m < 0 || m >= 60 || s < 0 || s >= 60)
|
|
413
|
+
return;
|
|
414
|
+
// Handle both ASCII minus (-) and Unicode minus (−, U+2212)
|
|
415
|
+
const signValue = MinusRE.test(sign) ? -1 : 1;
|
|
416
|
+
return signValue * (h * 60 + m + s / 60);
|
|
211
417
|
}
|
|
212
418
|
/**
|
|
213
|
-
*
|
|
214
|
-
*
|
|
419
|
+
* Normalize a timezone input to a valid Luxon Zone.
|
|
420
|
+
*
|
|
421
|
+
* Accepts multiple input formats and returns a validated Zone instance, or
|
|
422
|
+
* undefined if the input cannot be normalized to a valid timezone.
|
|
423
|
+
*
|
|
424
|
+
* Supported input formats:
|
|
425
|
+
* - **Numbers**: Timezone offset in minutes (e.g., 480 = UTC+8, -300 = UTC-5)
|
|
426
|
+
* - **Strings**: ISO offsets ("+08:00", "-05:00"), IANA zones
|
|
427
|
+
* ("America/Los_Angeles"), UTC variants ("UTC", "GMT", "Z", "Zulu")
|
|
428
|
+
* - **Zone instances**: Validated and returned if valid
|
|
429
|
+
*
|
|
430
|
+
* The function respects Settings:
|
|
431
|
+
* - {@link Settings.allowArchaicTimezoneOffsets} for pre-1982 offsets
|
|
432
|
+
* - {@link Settings.allowBakerIslandTime} for UTC-12:00
|
|
433
|
+
*
|
|
434
|
+
* @param input - Timezone in various formats
|
|
435
|
+
* @returns Valid Zone instance, or undefined if invalid
|
|
436
|
+
*
|
|
437
|
+
* @example
|
|
438
|
+
* ```typescript
|
|
439
|
+
* // Numbers (offset in minutes)
|
|
440
|
+
* normalizeZone(480)?.name // "UTC+8"
|
|
441
|
+
* normalizeZone(-300)?.name // "UTC-5"
|
|
442
|
+
* normalizeZone(0)?.name // "UTC"
|
|
443
|
+
*
|
|
444
|
+
* // ISO offset strings
|
|
445
|
+
* normalizeZone("+08:00")?.name // "UTC+8"
|
|
446
|
+
* normalizeZone("-05:30")?.name // "UTC-5:30"
|
|
447
|
+
* normalizeZone("UTC+7")?.name // "UTC+7"
|
|
448
|
+
*
|
|
449
|
+
* // IANA timezone names
|
|
450
|
+
* normalizeZone("America/Los_Angeles")?.name // "America/Los_Angeles"
|
|
451
|
+
* normalizeZone("Asia/Tokyo")?.name // "Asia/Tokyo"
|
|
452
|
+
*
|
|
453
|
+
* // UTC aliases
|
|
454
|
+
* normalizeZone("UTC")?.name // "UTC"
|
|
455
|
+
* normalizeZone("GMT")?.name // "UTC"
|
|
456
|
+
* normalizeZone("Z")?.name // "UTC"
|
|
457
|
+
* normalizeZone("Zulu")?.name // "UTC"
|
|
458
|
+
*
|
|
459
|
+
* // Invalid inputs return undefined
|
|
460
|
+
* normalizeZone("invalid") // undefined
|
|
461
|
+
* normalizeZone("+25:00") // undefined (beyond ±14 hours)
|
|
462
|
+
* normalizeZone(1200) // undefined (20 hours, beyond ±14 hours)
|
|
463
|
+
* normalizeZone(100) // undefined (not a valid timezone offset)
|
|
464
|
+
* normalizeZone(-1) // undefined (UnsetZone sentinel)
|
|
465
|
+
* normalizeZone(null) // undefined
|
|
466
|
+
* ```
|
|
215
467
|
*/
|
|
216
468
|
function normalizeZone(input) {
|
|
217
469
|
if (input == null ||
|
|
218
470
|
(0, String_1.blank)(input) ||
|
|
219
|
-
(!(0, Number_1.isNumber)(input) && !(0, String_1.isString)(input) && !isZone(input))
|
|
471
|
+
(!(0, Number_1.isNumber)(input) && !(0, String_1.isString)(input) && !isZone(input)) ||
|
|
472
|
+
input === exports.UnsetZone ||
|
|
473
|
+
input === exports.UnsetZoneOffsetMinutes ||
|
|
474
|
+
isZoneUnset(input)) {
|
|
220
475
|
return;
|
|
221
476
|
}
|
|
222
477
|
// wrapped in a try/catch as Luxon.settings.throwOnInvalid may be true:
|
|
223
478
|
try {
|
|
224
479
|
// This test and short-circuit may not be necessary, but it's cheap and
|
|
225
480
|
// explicit:
|
|
226
|
-
if (isUTC(input))
|
|
481
|
+
if (isUTC(input)) {
|
|
227
482
|
return luxon_1.FixedOffsetZone.utcInstance;
|
|
483
|
+
}
|
|
484
|
+
// TypeScript-friendly: after line 386's type check, we know input is one of these
|
|
228
485
|
let z = input;
|
|
486
|
+
if ((0, Number_1.isNumber)(z) && !validTzOffsetMinutes(z)) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
229
489
|
if ((0, String_1.isString)(z)) {
|
|
230
490
|
let s = z;
|
|
231
491
|
z = s = s.replace(/^(?:Zulu|Z|GMT)(?:\b|$)/i, "UTC");
|
|
@@ -233,8 +493,8 @@ function normalizeZone(input) {
|
|
|
233
493
|
// non-offset inputs:
|
|
234
494
|
if ((0, String_1.blank)(s))
|
|
235
495
|
return;
|
|
236
|
-
const fixed =
|
|
237
|
-
if (fixed != null) {
|
|
496
|
+
const fixed = parseTimezoneOffsetToMinutes(s);
|
|
497
|
+
if (fixed != null && validTzOffsetMinutes(fixed)) {
|
|
238
498
|
return luxon_1.Info.normalizeZone(fixed);
|
|
239
499
|
}
|
|
240
500
|
if (!IanaFormatRE.test(s)) {
|
|
@@ -251,13 +511,73 @@ function normalizeZone(input) {
|
|
|
251
511
|
}
|
|
252
512
|
}
|
|
253
513
|
/**
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
514
|
+
* Convert a timezone to its short offset format (e.g., "+08:00", "-05:00").
|
|
515
|
+
*
|
|
516
|
+
* Useful for displaying timezone offsets in a standardized format. For IANA
|
|
517
|
+
* zones with daylight saving time, provide a timestamp to get the correct
|
|
518
|
+
* offset for that moment.
|
|
519
|
+
*
|
|
520
|
+
* @param zone - Timezone as Zone, string, or offset in minutes
|
|
521
|
+
* @param ts - Optional timestamp (milliseconds) for IANA zone offset calculation.
|
|
522
|
+
* Defaults to current time if not provided.
|
|
523
|
+
* @returns Zone offset in "+HH:MM" format, or "" if zone is invalid
|
|
524
|
+
*
|
|
525
|
+
* @example
|
|
526
|
+
* ```typescript
|
|
527
|
+
* // Fixed offsets
|
|
528
|
+
* zoneToShortOffset("UTC+8") // "+08:00"
|
|
529
|
+
* zoneToShortOffset(480) // "+08:00"
|
|
530
|
+
* zoneToShortOffset("UTC-5:30") // "-05:30"
|
|
531
|
+
*
|
|
532
|
+
* // IANA zones (offset depends on DST)
|
|
533
|
+
* const winter = new Date("2023-01-15").getTime()
|
|
534
|
+
* const summer = new Date("2023-07-15").getTime()
|
|
535
|
+
* zoneToShortOffset("America/Los_Angeles", winter) // "-08:00" (PST)
|
|
536
|
+
* zoneToShortOffset("America/Los_Angeles", summer) // "-07:00" (PDT)
|
|
537
|
+
*
|
|
538
|
+
* // Invalid zones return empty string
|
|
539
|
+
* zoneToShortOffset("invalid") // ""
|
|
540
|
+
* zoneToShortOffset(null) // ""
|
|
541
|
+
* ```
|
|
257
542
|
*/
|
|
258
543
|
function zoneToShortOffset(zone, ts) {
|
|
259
544
|
return normalizeZone(zone)?.formatOffset(ts ?? Date.now(), "short") ?? "";
|
|
260
545
|
}
|
|
546
|
+
/**
|
|
547
|
+
* Type guard to check if a numeric offset (in minutes) represents a valid timezone.
|
|
548
|
+
*
|
|
549
|
+
* Validates that the offset:
|
|
550
|
+
* - Is a number (not null/undefined)
|
|
551
|
+
* - Is not the UnsetZone sentinel value (-1)
|
|
552
|
+
* - Matches a real-world timezone offset (respects Settings for archaic offsets)
|
|
553
|
+
*
|
|
554
|
+
* Use this for exact validation without rounding. For error-tolerant rounding to
|
|
555
|
+
* the nearest valid offset, use {@link inferLikelyOffsetMinutes} instead.
|
|
556
|
+
*
|
|
557
|
+
* @param tzOffsetMinutes - Offset in minutes to validate (e.g., 480 for UTC+8)
|
|
558
|
+
* @returns true if the offset is exactly valid (type guard)
|
|
559
|
+
*
|
|
560
|
+
* @see {@link inferLikelyOffsetMinutes} for error-tolerant rounding
|
|
561
|
+
*
|
|
562
|
+
* @example
|
|
563
|
+
* ```typescript
|
|
564
|
+
* validTzOffsetMinutes(480) // true (UTC+8)
|
|
565
|
+
* validTzOffsetMinutes(-300) // true (UTC-5)
|
|
566
|
+
* validTzOffsetMinutes(330) // true (UTC+5:30, India)
|
|
567
|
+
* validTzOffsetMinutes(345) // true (UTC+5:45, Nepal)
|
|
568
|
+
*
|
|
569
|
+
* validTzOffsetMinutes(481) // false (not a valid timezone)
|
|
570
|
+
* validTzOffsetMinutes(-1) // false (UnsetZone sentinel)
|
|
571
|
+
* validTzOffsetMinutes(null) // false
|
|
572
|
+
*
|
|
573
|
+
* // Archaic offsets require Settings
|
|
574
|
+
* Settings.allowArchaicTimezoneOffsets.value = false
|
|
575
|
+
* validTzOffsetMinutes(-630) // false (Hawaii -10:30, archaic)
|
|
576
|
+
*
|
|
577
|
+
* Settings.allowArchaicTimezoneOffsets.value = true
|
|
578
|
+
* validTzOffsetMinutes(-630) // true (Hawaii -10:30, archaic)
|
|
579
|
+
* ```
|
|
580
|
+
*/
|
|
261
581
|
function validTzOffsetMinutes(tzOffsetMinutes) {
|
|
262
582
|
return (tzOffsetMinutes != null &&
|
|
263
583
|
(0, Number_1.isNumber)(tzOffsetMinutes) &&
|
|
@@ -287,8 +607,9 @@ function tzHourToOffset(n) {
|
|
|
287
607
|
}
|
|
288
608
|
// Accept "Z", "UTC+2", "UTC+02", "UTC+2:00", "UTC+02:00", "+2", "+02", and
|
|
289
609
|
// "+02:00". Also accepts seconds like "-00:25:21" for archaic offsets.
|
|
610
|
+
// Handles Unicode minus (−, U+2212)
|
|
290
611
|
// Require the sign (+ or -) and a ":" separator if there are minutes.
|
|
291
|
-
const tzRe = /(?<Z>Z)|((UTC)?(?<sign>[
|
|
612
|
+
const tzRe = /(?<Z>Z)|((UTC)?(?<sign>[+−-])(?<hours>\d\d?)(?::(?<minutes>\d\d)(?::(?<seconds>\d\d))?)?)$/;
|
|
292
613
|
function extractOffsetFromHours(hourOffset) {
|
|
293
614
|
return (0, Number_1.isNumber)(hourOffset)
|
|
294
615
|
? (0, Maybe_1.map)(tzHourToOffset(hourOffset), (zone) => ({
|
|
@@ -301,12 +622,66 @@ function extractOffsetFromHours(hourOffset) {
|
|
|
301
622
|
: undefined;
|
|
302
623
|
}
|
|
303
624
|
/**
|
|
304
|
-
*
|
|
625
|
+
* Extract timezone information from various value types.
|
|
626
|
+
*
|
|
627
|
+
* Handles multiple input formats and performs intelligent parsing:
|
|
628
|
+
* - **Strings**: ISO offsets ("+08:00"), IANA zones, UTC variants, timestamps
|
|
629
|
+
* with embedded timezones ("2023:01:15 10:30:00-08:00")
|
|
630
|
+
* - **Numbers**: Hour offsets (e.g., -8 for UTC-8)
|
|
631
|
+
* - **Arrays**: Uses first non-null value
|
|
632
|
+
* - **ExifDateTime/ExifTime instances**: Extracts their zone property
|
|
633
|
+
*
|
|
634
|
+
* By default, strips timezone abbreviations (PST, PDT, etc.) as they are
|
|
635
|
+
* ambiguous. Returns provenance information indicating which parsing method
|
|
636
|
+
* succeeded.
|
|
637
|
+
*
|
|
638
|
+
* Supports Unicode minus signs (−, U+2212) and plus-minus signs (±, U+00B1)
|
|
639
|
+
* in addition to ASCII +/-.
|
|
640
|
+
*
|
|
641
|
+
* @param value - Value to extract timezone from
|
|
642
|
+
* @param opts.stripTZA - Whether to strip timezone abbreviations (default: true).
|
|
643
|
+
* TZAs like "PST" are ambiguous and usually stripped.
|
|
644
|
+
* @returns TzSrc with zone name and provenance, or undefined if no timezone found
|
|
645
|
+
*
|
|
646
|
+
* @example
|
|
647
|
+
* ```typescript
|
|
648
|
+
* // ISO offset strings
|
|
649
|
+
* extractZone("+08:00")
|
|
650
|
+
* // { zone: "UTC+8", tz: "UTC+8", src: "offsetMinutesToZoneName" }
|
|
651
|
+
*
|
|
652
|
+
* extractZone("UTC-5:30")
|
|
653
|
+
* // { zone: "UTC-5:30", tz: "UTC-5:30", src: "normalizeZone" }
|
|
654
|
+
*
|
|
655
|
+
* // IANA zone names
|
|
656
|
+
* extractZone("America/Los_Angeles")
|
|
657
|
+
* // { zone: "America/Los_Angeles", tz: "America/Los_Angeles", src: "normalizeZone" }
|
|
658
|
+
*
|
|
659
|
+
* // Timestamps with embedded timezones
|
|
660
|
+
* extractZone("2023:01:15 10:30:00-08:00")
|
|
661
|
+
* // { zone: "UTC-8", tz: "UTC-8", src: "offsetMinutesToZoneName",
|
|
662
|
+
* // leftovers: "2023:01:15 10:30:00" }
|
|
663
|
+
*
|
|
664
|
+
* // Unicode minus signs
|
|
665
|
+
* extractZone("−08:00") // Unicode minus (U+2212)
|
|
666
|
+
* // { zone: "UTC-8", tz: "UTC-8", src: "offsetMinutesToZoneName" }
|
|
667
|
+
*
|
|
668
|
+
* // Numeric hour offsets
|
|
669
|
+
* extractZone(-8)
|
|
670
|
+
* // { zone: "UTC-8", tz: "UTC-8", src: "hourOffset" }
|
|
305
671
|
*
|
|
306
|
-
*
|
|
307
|
-
* (
|
|
672
|
+
* // Arrays (uses first non-null)
|
|
673
|
+
* extractZone([null, "+05:30", undefined])
|
|
674
|
+
* // { zone: "UTC+5:30", tz: "UTC+5:30", src: "offsetMinutesToZoneName" }
|
|
308
675
|
*
|
|
309
|
-
*
|
|
676
|
+
* // Strips timezone abbreviations by default
|
|
677
|
+
* extractZone("2023:01:15 10:30:00-08:00 PST")
|
|
678
|
+
* // { zone: "UTC-8", tz: "UTC-8", src: "offsetMinutesToZoneName",
|
|
679
|
+
* // leftovers: "2023:01:15 10:30:00" }
|
|
680
|
+
*
|
|
681
|
+
* // Invalid inputs return undefined
|
|
682
|
+
* extractZone("invalid") // undefined
|
|
683
|
+
* extractZone(null) // undefined
|
|
684
|
+
* ```
|
|
310
685
|
*/
|
|
311
686
|
function extractZone(value, opts) {
|
|
312
687
|
if (value == null ||
|
|
@@ -375,7 +750,8 @@ function extractZone(value, opts) {
|
|
|
375
750
|
const hours = parseInt(capturedGroups.hours ?? "0");
|
|
376
751
|
const minutes = parseInt(capturedGroups.minutes ?? "0");
|
|
377
752
|
const seconds = parseInt(capturedGroups.seconds ?? "0");
|
|
378
|
-
|
|
753
|
+
// Handle both ASCII minus (-) and Unicode minus (−, U+2212)
|
|
754
|
+
const sign = MinusRE.test(capturedGroups.sign ?? "") ? -1 : 1;
|
|
379
755
|
const offsetMinutes = sign * (hours * 60 + minutes + seconds / 60);
|
|
380
756
|
const zone = offsetMinutesToZoneName(offsetMinutes);
|
|
381
757
|
if (zone != null) {
|
|
@@ -417,6 +793,47 @@ function incrementZone(z, minutes) {
|
|
|
417
793
|
? luxon_1.FixedOffsetZone.instance(fixed + minutes)
|
|
418
794
|
: undefined;
|
|
419
795
|
}
|
|
796
|
+
/**
|
|
797
|
+
* Extract timezone offset from standard EXIF timezone tags.
|
|
798
|
+
*
|
|
799
|
+
* Checks timezone tags in priority order:
|
|
800
|
+
* 1. TimeZone
|
|
801
|
+
* 2. OffsetTimeOriginal (for DateTimeOriginal)
|
|
802
|
+
* 3. OffsetTimeDigitized (for CreateDate)
|
|
803
|
+
* 4. TimeZoneOffset
|
|
804
|
+
*
|
|
805
|
+
* Handles camera-specific quirks like Nikon's DaylightSavings tag, which
|
|
806
|
+
* requires adjusting the TimeZone offset forward by one hour during DST.
|
|
807
|
+
*
|
|
808
|
+
* @param t - EXIF tags object
|
|
809
|
+
* @param opts.adjustTimeZoneIfDaylightSavings - Optional function to adjust
|
|
810
|
+
* timezone for DST. Defaults to handling Nikon's DaylightSavings quirk.
|
|
811
|
+
* @returns TzSrc with zone and provenance, or undefined if no timezone found
|
|
812
|
+
*
|
|
813
|
+
* @see {@link TimezoneOffsetTagnames} for the list of tags checked
|
|
814
|
+
* @see https://github.com/photostructure/exiftool-vendored.js/issues/215
|
|
815
|
+
*
|
|
816
|
+
* @example
|
|
817
|
+
* ```typescript
|
|
818
|
+
* const tags = await exiftool.read("photo.jpg")
|
|
819
|
+
*
|
|
820
|
+
* const tzSrc = extractTzOffsetFromTags(tags)
|
|
821
|
+
* if (tzSrc) {
|
|
822
|
+
* console.log(`Timezone: ${tzSrc.zone}`)
|
|
823
|
+
* console.log(`Source: ${tzSrc.src}`) // e.g., "OffsetTimeOriginal"
|
|
824
|
+
* }
|
|
825
|
+
*
|
|
826
|
+
* // Nikon DST handling
|
|
827
|
+
* const nikonTags = {
|
|
828
|
+
* TimeZone: "-08:00",
|
|
829
|
+
* DaylightSavings: "Yes",
|
|
830
|
+
* Make: "NIKON CORPORATION"
|
|
831
|
+
* }
|
|
832
|
+
* extractTzOffsetFromTags(nikonTags)
|
|
833
|
+
* // { zone: "UTC-7", tz: "UTC-7",
|
|
834
|
+
* // src: "TimeZone (adjusted for DaylightSavings)" }
|
|
835
|
+
* ```
|
|
836
|
+
*/
|
|
420
837
|
function extractTzOffsetFromTags(t, opts) {
|
|
421
838
|
const adjustFn = opts?.adjustTimeZoneIfDaylightSavings ??
|
|
422
839
|
DefaultExifToolOptions_1.defaultAdjustTimeZoneIfDaylightSavings;
|
|
@@ -485,33 +902,69 @@ function extractTzOffsetFromTimeStamp(t, opts) {
|
|
|
485
902
|
}
|
|
486
903
|
return;
|
|
487
904
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
905
|
+
/**
|
|
906
|
+
* Round an arbitrary offset to the nearest valid timezone offset.
|
|
907
|
+
*
|
|
908
|
+
* This is error-tolerant timezone inference, useful for:
|
|
909
|
+
* - GPS-based timezone calculation (where GPS time drift may cause errors)
|
|
910
|
+
* - Handling clock drift in timestamp comparisons
|
|
911
|
+
* - Fuzzy timezone matching
|
|
912
|
+
*
|
|
913
|
+
* By default, uses {@link Settings.maxValidOffsetMinutes} (30 minutes) as the
|
|
914
|
+
* maximum distance from a valid timezone. This threshold handles GPS acquisition
|
|
915
|
+
* lag and clock drift while preventing false matches.
|
|
916
|
+
*
|
|
917
|
+
* Respects Settings for archaic offsets, Baker Island time, and max offset tolerance.
|
|
918
|
+
*
|
|
919
|
+
* @param deltaMinutes - Offset in minutes to round (can be fractional)
|
|
920
|
+
* @param maxValidOffsetMinutes - Maximum distance (in minutes) from a valid
|
|
921
|
+
* timezone to accept. Defaults to {@link Settings.maxValidOffsetMinutes}.
|
|
922
|
+
* @returns Nearest valid offset in minutes, or undefined if too far from any
|
|
923
|
+
* valid timezone
|
|
924
|
+
*
|
|
925
|
+
* @see {@link validTzOffsetMinutes} for exact validation without rounding
|
|
926
|
+
* @see {@link Settings.maxValidOffsetMinutes} to configure the default threshold
|
|
927
|
+
*
|
|
928
|
+
* @example
|
|
929
|
+
* ```typescript
|
|
930
|
+
* // Exact matches
|
|
931
|
+
* inferLikelyOffsetMinutes(480) // 480 (UTC+8, exact)
|
|
932
|
+
* inferLikelyOffsetMinutes(-300) // -300 (UTC-5, exact)
|
|
933
|
+
*
|
|
934
|
+
* // Rounding within default threshold (30 minutes)
|
|
935
|
+
* inferLikelyOffsetMinutes(485) // 480 (UTC+8, rounded from 485)
|
|
936
|
+
* inferLikelyOffsetMinutes(-295) // -300 (UTC-5, rounded from -295)
|
|
937
|
+
* inferLikelyOffsetMinutes(330.5) // 330 (UTC+5:30, rounded)
|
|
938
|
+
*
|
|
939
|
+
* // GPS-based inference with clock drift (within 30 min default)
|
|
940
|
+
* const gpsTime = "2023:01:15 19:30:45" // UTC
|
|
941
|
+
* const localTime = "2023:01:15 11:32:12" // Local with 1.5min drift
|
|
942
|
+
* const deltaMinutes = 480 + 1.5 // ~481.5 minutes
|
|
943
|
+
* inferLikelyOffsetMinutes(deltaMinutes) // 480 (UTC+8)
|
|
944
|
+
*
|
|
945
|
+
* // GPS lag up to 23 minutes still works (within 30 min threshold)
|
|
946
|
+
* inferLikelyOffsetMinutes(443) // 420 (UTC-7, ~23 min from actual)
|
|
947
|
+
*
|
|
948
|
+
* // Beyond threshold returns undefined
|
|
949
|
+
* inferLikelyOffsetMinutes(100) // undefined (not near any valid offset)
|
|
950
|
+
*
|
|
951
|
+
* // Custom threshold
|
|
952
|
+
* inferLikelyOffsetMinutes(495, 30) // 480 (UTC+8 with 30min threshold)
|
|
953
|
+
* inferLikelyOffsetMinutes(495, 15) // undefined (beyond 15min threshold)
|
|
954
|
+
*
|
|
955
|
+
* // Adjust global default
|
|
956
|
+
* Settings.maxValidOffsetMinutes.value = 15 // Stricter matching
|
|
957
|
+
* inferLikelyOffsetMinutes(443) // undefined (beyond 15min threshold)
|
|
958
|
+
* ```
|
|
959
|
+
*/
|
|
960
|
+
function inferLikelyOffsetMinutes(deltaMinutes, maxValidOffsetMinutes = Settings_1.Settings.maxValidOffsetMinutes.value) {
|
|
961
|
+
return deltaMinutes == null
|
|
962
|
+
? undefined
|
|
963
|
+
: (0, Array_1.leastBy)(likelyOffsetMinutes(), (ea) => {
|
|
964
|
+
const diff = Math.abs(ea - deltaMinutes);
|
|
965
|
+
// Reject timezone offsets more than maxValidOffsetMinutes minutes away:
|
|
966
|
+
return diff > maxValidOffsetMinutes ? undefined : diff;
|
|
967
|
+
});
|
|
515
968
|
}
|
|
516
969
|
/**
|
|
517
970
|
* Convert blank strings to undefined.
|
|
@@ -519,6 +972,62 @@ function inferLikelyOffsetMinutes(deltaMinutes) {
|
|
|
519
972
|
function toNotBlank(x) {
|
|
520
973
|
return x == null || (typeof x === "string" && (0, String_1.blank)(x)) ? undefined : x;
|
|
521
974
|
}
|
|
975
|
+
/**
|
|
976
|
+
* Infer timezone offset by comparing local time with GPS/UTC time.
|
|
977
|
+
*
|
|
978
|
+
* Calculates the timezone by finding the difference between:
|
|
979
|
+
* - A "captured at" timestamp (DateTimeOriginal, CreateDate, etc.) assumed to
|
|
980
|
+
* be in local time
|
|
981
|
+
* - A UTC timestamp (GPSDateTime, DateTimeUTC, or combined GPSDateStamp +
|
|
982
|
+
* GPSTimeStamp)
|
|
983
|
+
*
|
|
984
|
+
* Uses {@link inferLikelyOffsetMinutes} to handle minor clock drift and round
|
|
985
|
+
* to the nearest valid timezone offset.
|
|
986
|
+
*
|
|
987
|
+
* This is a fallback when explicit timezone tags are not available.
|
|
988
|
+
*
|
|
989
|
+
* @param t - Tags object with timestamp fields
|
|
990
|
+
* @returns TzSrc with inferred timezone and provenance, or undefined if
|
|
991
|
+
* inference is not possible
|
|
992
|
+
*
|
|
993
|
+
* @see {@link extractTzOffsetFromTags} to check explicit timezone tags first
|
|
994
|
+
*
|
|
995
|
+
* @example
|
|
996
|
+
* ```typescript
|
|
997
|
+
* // GPS-based inference
|
|
998
|
+
* const tags = {
|
|
999
|
+
* DateTimeOriginal: "2023:01:15 11:30:00", // Local time (PST)
|
|
1000
|
+
* GPSDateTime: "2023:01:15 19:30:00" // UTC
|
|
1001
|
+
* }
|
|
1002
|
+
* extractTzOffsetFromUTCOffset(tags)
|
|
1003
|
+
* // { zone: "UTC-8", tz: "UTC-8",
|
|
1004
|
+
* // src: "offset between DateTimeOriginal and GPSDateTime" }
|
|
1005
|
+
*
|
|
1006
|
+
* // DateTimeUTC-based inference
|
|
1007
|
+
* const tags2 = {
|
|
1008
|
+
* CreateDate: "2023:07:20 14:15:30", // Local time (JST)
|
|
1009
|
+
* DateTimeUTC: "2023:07:20 05:15:30" // UTC
|
|
1010
|
+
* }
|
|
1011
|
+
* extractTzOffsetFromUTCOffset(tags2)
|
|
1012
|
+
* // { zone: "UTC+9", tz: "UTC+9",
|
|
1013
|
+
* // src: "offset between CreateDate and DateTimeUTC" }
|
|
1014
|
+
*
|
|
1015
|
+
* // Handles clock drift
|
|
1016
|
+
* const tags3 = {
|
|
1017
|
+
* DateTimeOriginal: "2023:01:15 11:30:45", // Local with drift
|
|
1018
|
+
* GPSDateTime: "2023:01:15 19:29:58" // UTC (old GPS fix)
|
|
1019
|
+
* }
|
|
1020
|
+
* extractTzOffsetFromUTCOffset(tags3)
|
|
1021
|
+
* // Still infers UTC-8 despite ~1 minute drift
|
|
1022
|
+
*
|
|
1023
|
+
* // No UTC timestamp available
|
|
1024
|
+
* const tags4 = {
|
|
1025
|
+
* DateTimeOriginal: "2023:01:15 11:30:00"
|
|
1026
|
+
* // No GPS or UTC timestamp
|
|
1027
|
+
* }
|
|
1028
|
+
* extractTzOffsetFromUTCOffset(tags4) // undefined
|
|
1029
|
+
* ```
|
|
1030
|
+
*/
|
|
522
1031
|
function extractTzOffsetFromUTCOffset(t) {
|
|
523
1032
|
const utcSources = {
|
|
524
1033
|
...(0, Pick_1.pick)(t, "GPSDateTime", "DateTimeUTC", "SonyDateTime2"),
|
|
@@ -571,12 +1080,66 @@ function extractTzOffsetFromUTCOffset(t) {
|
|
|
571
1080
|
src: `offset between ${dt.tagName} and ${utc.tagName}`,
|
|
572
1081
|
}));
|
|
573
1082
|
}
|
|
574
|
-
|
|
1083
|
+
/**
|
|
1084
|
+
* Check if two timezone values are equivalent at a specific point in time.
|
|
1085
|
+
*
|
|
1086
|
+
* Two zones are considered equivalent if they:
|
|
1087
|
+
* - Are the same zone (via Luxon's Zone.equals()), OR
|
|
1088
|
+
* - Have the same offset at the specified timestamp
|
|
1089
|
+
*
|
|
1090
|
+
* This is useful for:
|
|
1091
|
+
* - De-duplicating timezone records
|
|
1092
|
+
* - Comparing zones in different formats ("UTC+5" vs "UTC+05:00")
|
|
1093
|
+
* - Matching IANA zones to their offset at a specific time
|
|
1094
|
+
*
|
|
1095
|
+
* For IANA zones with DST, you can specify a timestamp to evaluate equivalence
|
|
1096
|
+
* at that moment. This is important when comparing historical records or future
|
|
1097
|
+
* events where DST transitions matter.
|
|
1098
|
+
*
|
|
1099
|
+
* @param a - First timezone (Zone, string, or offset in minutes)
|
|
1100
|
+
* @param b - Second timezone (Zone, string, or offset in minutes)
|
|
1101
|
+
* @param at - Timestamp in milliseconds to evaluate zone offsets.
|
|
1102
|
+
* Defaults to current time (Date.now()).
|
|
1103
|
+
* @returns true if zones are equivalent at the specified time
|
|
1104
|
+
*
|
|
1105
|
+
* @example
|
|
1106
|
+
* ```typescript
|
|
1107
|
+
* // Same zone, different formats
|
|
1108
|
+
* equivalentZones("UTC+5", "UTC+05:00") // true
|
|
1109
|
+
* equivalentZones("UTC-8", -480) // true (480 minutes = 8 hours)
|
|
1110
|
+
* equivalentZones("GMT", "UTC") // true
|
|
1111
|
+
* equivalentZones("Z", 0) // true
|
|
1112
|
+
*
|
|
1113
|
+
* // IANA zones matched by current offset (default behavior)
|
|
1114
|
+
* equivalentZones("America/New_York", "UTC-5") // true in winter (EST)
|
|
1115
|
+
* equivalentZones("America/New_York", "UTC-4") // true in summer (EDT)
|
|
1116
|
+
*
|
|
1117
|
+
* // IANA zones at specific times
|
|
1118
|
+
* const winter = new Date("2023-01-15").getTime()
|
|
1119
|
+
* const summer = new Date("2023-07-15").getTime()
|
|
1120
|
+
* equivalentZones("America/New_York", "UTC-5", winter) // true (EST)
|
|
1121
|
+
* equivalentZones("America/New_York", "UTC-4", winter) // false (not EDT in winter)
|
|
1122
|
+
* equivalentZones("America/New_York", "UTC-4", summer) // true (EDT)
|
|
1123
|
+
* equivalentZones("America/New_York", "UTC-5", summer) // false (not EST in summer)
|
|
1124
|
+
*
|
|
1125
|
+
* // Compare two IANA zones at a specific time
|
|
1126
|
+
* equivalentZones("America/New_York", "America/Toronto", winter) // true (both EST)
|
|
1127
|
+
* equivalentZones("America/New_York", "America/Los_Angeles", winter) // false (EST vs PST)
|
|
1128
|
+
*
|
|
1129
|
+
* // Different zones
|
|
1130
|
+
* equivalentZones("UTC+8", "UTC+9") // false
|
|
1131
|
+
*
|
|
1132
|
+
* // Invalid zones return false
|
|
1133
|
+
* equivalentZones("invalid", "UTC") // false
|
|
1134
|
+
* equivalentZones(null, "UTC") // false
|
|
1135
|
+
* ```
|
|
1136
|
+
*/
|
|
1137
|
+
function equivalentZones(a, b, at = Date.now()) {
|
|
575
1138
|
const az = normalizeZone(a);
|
|
576
1139
|
const bz = normalizeZone(b);
|
|
577
1140
|
return (az != null &&
|
|
578
1141
|
bz != null &&
|
|
579
|
-
(az.equals(bz) || az.offset(
|
|
1142
|
+
(az.equals(bz) || az.offset(at) === bz.offset(at)));
|
|
580
1143
|
}
|
|
581
1144
|
function getZoneName(args = {}) {
|
|
582
1145
|
const result = normalizeZone(args.zone)?.name ??
|