"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TimezoneOffsetTagnames = exports.TimezoneOffsetRE = exports.defaultVideosToUTC = exports.UnsetZoneName = exports.UnsetZone = exports.UnsetZoneOffsetMinutes = exports.ValidTimezoneOffsets = exports.ArchaicTimezoneOffsets = void 0;
exports.isUTC = isUTC;
exports.isZoneUnset = isZoneUnset;
exports.isZoneValid = isZoneValid;
exports.isZone = isZone;
exports.parseTimezoneOffsetMatch = parseTimezoneOffsetMatch;
exports.parseTimezoneOffsetToMinutes = parseTimezoneOffsetToMinutes;
exports.normalizeZone = normalizeZone;
exports.zoneToShortOffset = zoneToShortOffset;
exports.validTzOffsetMinutes = validTzOffsetMinutes;
exports.offsetMinutesToZoneName = offsetMinutesToZoneName;
exports.extractZone = extractZone;
exports.incrementZone = incrementZone;
exports.extractTzOffsetFromTags = extractTzOffsetFromTags;
exports.extractTzOffsetFromDatestamps = extractTzOffsetFromDatestamps;
exports.extractTzOffsetFromTimeStamp = extractTzOffsetFromTimeStamp;
exports.inferLikelyOffsetMinutes = inferLikelyOffsetMinutes;
exports.extractTzOffsetFromUTCOffset = extractTzOffsetFromUTCOffset;
exports.equivalentZones = equivalentZones;
exports.getZoneName = getZoneName;
const luxon_1 = require("luxon");
const Array_1 = require("./Array");
const BinaryField_1 = require("./BinaryField");
const CapturedAtTagNames_1 = require("./CapturedAtTagNames");
const DefaultExifToolOptions_1 = require("./DefaultExifToolOptions");
const ExifDate_1 = require("./ExifDate");
const ExifDateTime_1 = require("./ExifDateTime");
const ExifTime_1 = require("./ExifTime");
const Lazy_1 = require("./Lazy");
const Maybe_1 = require("./Maybe");
const Number_1 = require("./Number");
const Object_1 = require("./Object");
const Pick_1 = require("./Pick");
const Settings_1 = require("./Settings");
const String_1 = require("./String");
// When should we use "tz" vs "zone"?
// 1. "tz" refers to the entire database/system - the "tz database" (also called
//    tzdata or zoneinfo)
// 2. "zone" refers to a specific time zone entry within that database
// So... we really should be using "zone" everywhere, not "tz".
// Unique values from
// https://en.wikipedia.org/wiki/List_of_tz_database_time_zones, excluding those
// that have not been used for at least 50 years.
/**
 * Archaic timezone offsets that have not been in use since 1982 or earlier
 */
exports.ArchaicTimezoneOffsets = [
    "-10:30", // used by Hawaii 1896-1947
    "-04:30", // used by Venezuela 1912-1965 and 2007-2016
    "-00:44", // used by Liberia until 1972
    "-00:25:21", // Ireland 1880-1916 https://en.wikipedia.org/wiki/UTC%E2%88%9200:25:21
    "+00:20", // used by Netherlands until 1940
    "+00:30", // used by Switzerland until 1936
    "+01:24", // used by Warsaw until 1915
    "+01:30", // used by some southern African countries until 1903
    "+02:30", // archaic Moscow time
    "+04:51", // used by Bombay until 1955 https://en.wikipedia.org/wiki/UTC%2B04:51
    "+05:40", // used by Nepal until 1920
    "+07:20", // used by Singapore and Malaysia until 1941
    "+07:30", // used by Malaysia until 1982
];
/**
 * Valid timezone offsets that are currently in use, or used after 1982
 */
exports.ValidTimezoneOffsets = [
    // The UTC-12:00 timezone offset is used for uninhabited U.S. territories:
    //  - Baker Island (uninhabited)
    //  - Howland Island (uninhabited)
    // "-12:00", // not used for any populated land!
    "-11:00",
    "-10:00",
    "-09:30",
    "-09:00",
    "-08:30",
    "-08:00",
    "-07:00",
    "-06:00",
    "-05:00",
    "-04:00",
    "-03:30",
    "-03:00",
    "-02:30",
    "-02:00",
    "-01:00",
    "+00:00",
    "+01:00",
    "+02:00",
    "+03:00",
    "+03:30",
    "+04:00",
    "+04:30",
    "+05:00",
    "+05:30",
    "+05:45", // Nepal
    "+06:00",
    "+06:30",
    "+07:00",
    "+08:00",
    "+08:30", // used by North Korea until 2018
    "+08:45", // used by Western Australia, but not in tz database
    "+09:00",
    "+09:30",
    "+09:45", // used by Western Australia, but not in tz database
    "+10:00",
    "+10:30",
    "+11:00",
    "+12:00",
    "+12:45", // New Zealand islands
    "+13:00", // New Zealand and Antarctica
    "+13:45", // New Zealand islands
    "+14:00",
];
/**
 * Common, unique timezone abbreviations → UTC offset in minutes.
 *
 * Used as a last-resort fallback in {@link extractZone} when the only timezone
 * info in a string is a trailing abbreviation (e.g. "PDT", "EST").
 *
 * We map to **fixed offsets**, not IANA zone names, because:
 * 1. The abbreviation IS the offset — "PDT" always means UTC-7.
 * 2. IANA zones apply DST rules that can contradict the abbreviation (e.g. PST
 *    → America/Los_Angeles in June → Luxon returns UTC-7, not -8).
 * 3. Abbreviations don't uniquely identify locations (MST = Denver with DST /
 *    Phoenix without DST).
 *
 * Ambiguous abbreviations are **omitted** rather than guessed:
 * - CST: US Central (-6) / China (+8) / Cuba (-5)
 * - IST: India (+5:30) / Israel (+2) / Ireland (+1)
 * - BST: British Summer (+1) / Bangladesh (+6)
 * - AST: Atlantic (-4) / Arabia (+3)
 * - GST: Gulf (+4) / South Georgia (-2)
 * - AMT: Amazon (-4) / Armenia (+4)
 * - SST: Samoa (-11) / Singapore (+8)
 * - WST: West Samoa (-11) / Western Australia (+8)
 *
 * Sources: RFC 2822 §4.3, IANA tz database, POSIX TZ values, Wikipedia.
 */
const TzAbbreviationOffsetMinutes = {
    // ── UTC / GMT ──
    UTC: 0,
    GMT: 0,
    // ── North America ──
    // standard
    NST: -3 * 60 - 30, // Newfoundland Standard
    NDT: -2 * 60 - 30, // Newfoundland Daylight
    ADT: -3 * 60, //      Atlantic Daylight
    EST: -5 * 60, //      Eastern Standard
    EDT: -4 * 60, //      Eastern Daylight
    CDT: -5 * 60, //      Central Daylight
    MST: -7 * 60, //      Mountain Standard
    MDT: -6 * 60, //      Mountain Daylight
    PST: -8 * 60, //      Pacific Standard
    PDT: -7 * 60, //      Pacific Daylight
    AKST: -9 * 60, //     Alaska Standard
    AKDT: -8 * 60, //     Alaska Daylight
    HST: -10 * 60, //     Hawaii (no DST)
    // ── Europe ──
    WET: 0, //            Western European
    WEST: 1 * 60, //      Western European Summer
    CET: 1 * 60, //       Central European
    CEST: 2 * 60, //      Central European Summer
    EET: 2 * 60, //       Eastern European
    EEST: 3 * 60, //      Eastern European Summer
    MSK: 3 * 60, //       Moscow
    TRT: 3 * 60, //       Turkey
    // ── Africa ──
    WAT: 1 * 60, //       West Africa
    CAT: 2 * 60, //       Central Africa
    SAST: 2 * 60, //      South Africa Standard
    EAT: 3 * 60, //       East Africa
    // ── West / Central Asia ──
    IRST: 3 * 60 + 30, // Iran Standard
    IRDT: 4 * 60 + 30, // Iran Daylight
    AFT: 4 * 60 + 30, //  Afghanistan
    PKT: 5 * 60, //       Pakistan
    NPT: 5 * 60 + 45, //  Nepal
    MMT: 6 * 60 + 30, //  Myanmar
    ICT: 7 * 60, //       Indochina (Thailand, Vietnam, etc.)
    // ── East Asia ──
    SGT: 8 * 60, //       Singapore
    HKT: 8 * 60, //       Hong Kong
    PHT: 8 * 60, //       Philippines
    JST: 9 * 60, //       Japan Standard
    KST: 9 * 60, //       Korea Standard
    // ── Australia / Pacific ──
    AWST: 8 * 60, //      Australian Western Standard
    ACST: 9 * 60 + 30, // Australian Central Standard
    ACDT: 10 * 60 + 30, // Australian Central Daylight
    AEST: 10 * 60, //     Australian Eastern Standard
    AEDT: 11 * 60, //     Australian Eastern Daylight
    NZST: 12 * 60, //     New Zealand Standard
    NZDT: 13 * 60, //     New Zealand Daylight
    CHAST: 12 * 60 + 45, // Chatham Standard
    CHADT: 13 * 60 + 45, // Chatham Daylight
    // ── South America ──
    COT: -5 * 60, //      Colombia
    PET: -5 * 60, //      Peru
    VET: -4 * 60, //      Venezuela
    BOT: -4 * 60, //      Bolivia
    CLT: -4 * 60, //      Chile Standard
    CLST: -3 * 60, //     Chile Summer
    ART: -3 * 60, //      Argentina
    BRT: -3 * 60, //      Brasilia
    UYT: -3 * 60, //      Uruguay
    // ── Atlantic / Indian Ocean ──
    CVT: -1 * 60, //      Cape Verde
    AZOT: -1 * 60, //     Azores Standard
    MUT: 4 * 60, //       Mauritius
    SCT: 4 * 60, //       Seychelles
};
/**
 * Get all valid timezone offset minutes based on current settings.
 * Used by both validOffsetMinutes (Set) and likelyOffsetMinutes (Array).
 */
function getValidOffsetMinutes() {
    const offsets = [...exports.ValidTimezoneOffsets];
    if (Settings_1.Settings.allowArchaicTimezoneOffsets.value) {
        offsets.push(...exports.ArchaicTimezoneOffsets);
    }
    if (Settings_1.Settings.allowBakerIslandTime.value) {
        offsets.push("-12:00");
    }
    return offsets.map(parseTimezoneOffsetToMinutes).filter(Number_1.isNumber);
}
const validOffsetMinutes = (0, Lazy_1.lazy)(() => new Set(getValidOffsetMinutes()));
// Used for fuzzy matching in inferLikelyOffsetMinutes
const likelyOffsetMinutes = (0, Lazy_1.lazy)(() => getValidOffsetMinutes());
// Invalidate both caches when relevant settings change
function clearOffsetCaches() {
    validOffsetMinutes.clear();
    likelyOffsetMinutes.clear();
}
Settings_1.Settings.allowArchaicTimezoneOffsets.onChange(clearOffsetCaches);
Settings_1.Settings.allowBakerIslandTime.onChange(clearOffsetCaches);
/**
 * Zone instances with this offset are a placeholder for being "unset".
 */
exports.UnsetZoneOffsetMinutes = -1;
/**
 * This is a placeholder for dates where the zone is unknown/unset, because
 * Luxon doesn't officially support "unset" zones.
 */
exports.UnsetZone = luxon_1.Info.normalizeZone(exports.UnsetZoneOffsetMinutes);
/**
 * Zone instances with this name are a placeholder for being "unset".
 */
exports.UnsetZoneName = exports.UnsetZone.name;
const Zulus = [
    luxon_1.FixedOffsetZone.utcInstance,
    0,
    -0,
    "0", // String zero - some systems output this for numeric offsets
    "UTC",
    "GMT",
    "Z",
    "Etc/UTC", // Valid IANA timezone name for UTC
    "+0",
    "+00",
    "-00",
    "+00:00",
    "-00:00",
    "UTC+0",
    "GMT+0",
    "UTC+00:00",
    "GMT+00:00",
];
/**
 * Check if a timezone value represents UTC.
 *
 * Handles multiple UTC representations including Zone instances, strings, and
 * numeric offsets. Recognizes common UTC aliases like "GMT", "Z", "Zulu",
 * "+0", "+00:00", etc.
 *
 * @param zone - Timezone to check (Zone, string, or number)
 * @returns true if the zone represents UTC/GMT/Zulu
 *
 * @example
 * ```typescript
 * isUTC("UTC")              // true
 * isUTC("GMT")              // true
 * isUTC("Z")                // true
 * isUTC("Zulu")             // true
 * isUTC(0)                  // true
 * isUTC("+00:00")           // true
 * isUTC("UTC+0")            // true
 * isUTC("America/New_York") // false
 * isUTC("+08:00")           // false
 * ```
 */
function isUTC(zone) {
    if (zone == null) {
        return false;
    }
    if (typeof zone === "string" || typeof zone === "number") {
        return Zulus.includes(zone);
    }
    if (isZone(zone)) {
        return zone.isUniversal && zone.offset(Date.now()) === 0;
    }
    return false;
}
/**
 * Check if a Zone is the library's sentinel "unset" value.
 *
 * The library uses a special Zone instance to represent unknown/unset
 * timezones since Luxon doesn't officially support unset zones.
 *
 * @param zone - Zone instance to check
 * @returns true if the zone is the UnsetZone sentinel value
 *
 * @see {@link UnsetZone}
 * @see {@link UnsetZoneName}
 * @see {@link UnsetZoneOffsetMinutes}
 *
 * @example
 * ```typescript
 * isZoneUnset(UnsetZone)                    // true
 * isZoneUnset(Info.normalizeZone("UTC"))    // false
 * isZoneUnset(Info.normalizeZone("UTC+8"))  // false
 * ```
 */
function isZoneUnset(zone) {
    return (isZone(zone) &&
        zone.isUniversal &&
        zone.offset(0) === exports.UnsetZoneOffsetMinutes);
}
/**
 * Type guard to check if a Zone is valid and usable.
 *
 * A zone is considered valid if it:
 * - Is not null/undefined
 * - Has `isValid === true` (Luxon requirement)
 * - Is not the library's UnsetZone sentinel
 * - Has an offset within ±14 hours (the valid range for real-world timezones)
 *
 * This is the canonical validation check used throughout the library.
 *
 * @param zone - Zone to validate
 * @returns true if the zone is valid and usable (type guard)
 *
 * @example
 * ```typescript
 * const zone = Info.normalizeZone("America/Los_Angeles")
 * if (isZoneValid(zone)) {
 *   // TypeScript knows zone is Zone (not Zone | undefined)
 *   console.log(zone.name)
 * }
 *
 * isZoneValid(Info.normalizeZone("invalid"))  // false
 * isZoneValid(Info.normalizeZone("UTC+8"))    // true
 * isZoneValid(UnsetZone)                      // false
 * isZoneValid(Info.normalizeZone("UTC+20"))   // false (beyond ±14 hours)
 * ```
 */
function isZoneValid(zone) {
    // If the zone is not a
    return (isZone(zone) &&
        !isZoneUnset(zone) &&
        zone.isValid && // < this was already validated by isZone() but we're being explicit here
        (!zone.isUniversal || validTzOffsetMinutes(zone.offset(Date.now()))) // < Assume Luxon will validate IANA zones, so it's only on us to check fixed-offset zones
    );
}
/**
 * Type guard to check if a value is a **valid** Luxon Zone instance.
 *
 * Note that this **includes** the {@link UnsetZone} sentinel value.
 *
 * Checks both `instanceof Zone` and constructor name to handle cross-module
 * Zone instances that may not pass instanceof checks.
 *
 * @param value - Value to check
 * @returns true if the value is a Zone instance (type guard)
 *
 * @example
 * ```typescript
 * import { Info } from "luxon"
 *
 * const zone = Info.normalizeZone("UTC+8")
 * if (isZone(zone)) {
 *   // TypeScript knows zone is Zone (not unknown)
 *   console.log(zone.offset(Date.now()))
 * }
 *
 * isZone(Info.normalizeZone("UTC"))  // true
 * isZone("UTC")                      // false
 * isZone(480)                        // false
 * isZone(null)                       // false
 * ```
 */
function isZone(value) {
    if (!(0, Object_1.isObject)(value))
        return false;
    if (value instanceof luxon_1.Zone) {
        return value.isValid;
    }
    // Duck-typing fallback for cross-module Zone instances
    const z = value;
    return (z.isValid === true &&
        // These are the Zone properties/methods we care about:
        typeof z.isUniversal === "boolean" &&
        typeof z.formatOffset === "function");
}
/**
 * If `tzSource` matches this value, the tags are from a video, and we had to
 * resort to assuming time fields are in UTC.
 * @see https://github.com/photostructure/exiftool-vendored.js/issues/113
 */
exports.defaultVideosToUTC = "defaultVideosToUTC";
// https://en.wikipedia.org/wiki/List_of_tz_database_time_zones -- note that
// "WET" and "W-SU" are full TZs (!!!), and "America/Indiana/Indianapolis" is
// also a thing.
const IanaFormatRE = /^\w{2,15}(?:\/\w{3,15}){0,2}$/;
/**
 * Regex for parsing timezone offset strings.
 *
 * Supports both ISO 8601 extended format (`+08:00`) and basic format (`+0800`).
 * The colon between hours and minutes is optional (`:?`) for compatibility with
 * multiple standards.
 *
 * ## Timezone Offset Format Standards
 *
 * | Standard              | Format     | Colon Required? |
 * |-----------------------|------------|-----------------|
 * | ISO 8601 Basic        | `-0300`    | No              |
 * | ISO 8601 Extended     | `-03:00`   | Yes             |
 * | RFC 3339              | `-03:00`   | **Yes** (strict)|
 * | EXIF 2.31+ OffsetTime | `+01:00`   | Yes             |
 * | IPTC                  | `+0300`    | **No**          |
 *
 * While ISO 8601 allows both formats, **mixing styles is discouraged**. A
 * timestamp using extended format for date/time (e.g., `2025:04:27 19:47:08`)
 * should use extended format for the offset (`-03:00`), not basic (`-0300`).
 *
 * ExifTool accepts both formats on input but normalizes to extended format
 * (with colon) on output.
 *
 * @see https://en.wikipedia.org/wiki/ISO_8601 - ISO 8601 standard
 * @see https://datatracker.ietf.org/doc/html/rfc3339 - RFC 3339 (colon required)
 * @see https://lists.gnu.org/archive/html/bug-coreutils/2013-06/msg00023.html -
 *      Discussion on colon requirement for extended format
 */
const OffsetStringRE = /^(?:UTC|GMT)?(?<sign>[+−-])(?<hours>\d{1,2})(?::?(?<minutes>\d{2})(?::(?<seconds>\d{2}))?)?$/;
const MinusRE = /[−-]/;
/**
 * Composable regex pattern for matching timezone offsets.
 *
 * Designed for embedding in larger patterns (no ^ or $ anchors).
 * Matches UTC/GMT/Z or signed offsets in both ISO 8601 extended format
 * (+08:00, -05:30) and basic format (+0800, -0530).
 *
 * Named capture groups:
 * - `tz_utc`: Matches "Z", "UTC", or "GMT"
 * - `tz_sign`: The sign character (+, -, or Unicode minus −)
 * - `tz_hours`: Hour offset (1-2 digits)
 * - `tz_minutes`: Optional minute offset (2 digits)
 *
 * @example
 * ```typescript
 * // Concatenate with other patterns
 * const dateTimeRE = new RegExp(
 *   `(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})${TimezoneOffsetRE.source}`
 * )
 *
 * // Use standalone
 * const match = TimezoneOffsetRE.exec("2023-01-15T10:30:00-08:00")
 * if (match?.groups) {
 *   const { tz_sign, tz_hours, tz_minutes } = match.groups
 *   // tz_sign = "-", tz_hours = "08", tz_minutes = "00"
 * }
 * ```
 */
exports.TimezoneOffsetRE = /(?:(?<tz_utc>Z|UTC|GMT)|(?<tz_sign>[+−-])(?<tz_hours>[01]?\d)(?::?(?<tz_minutes>\d\d))?)/;
/**
 * Parse timezone offset from a regex match result.
 *
 * Use with {@link TimezoneOffsetRE} to extract offset minutes from a match.
 *
 * @param match - RegExp exec result from TimezoneOffsetRE
 * @returns Parsed offset info, or undefined if match is invalid
 *
 * @example
 * ```typescript
 * const match = TimezoneOffsetRE.exec("2023-01-15T10:30:00-08:00")
 * const result = parseTimezoneOffsetMatch(match)
 * // { offsetMinutes: -480, isUtc: false }
 * ```
 */
function parseTimezoneOffsetMatch(match) {
    if (match?.groups == null)
        return;
    const { tz_utc, tz_sign, tz_hours, tz_minutes } = match.groups;
    if (tz_utc != null) {
        return { offsetMinutes: 0, isUtc: true };
    }
    if (tz_sign == null || tz_hours == null)
        return;
    const h = (0, Number_1.toInt)(tz_hours);
    const m = (0, Number_1.toInt)(tz_minutes) ?? 0;
    if (h == null || h < 0 || h > 14 || m < 0 || m >= 60)
        return;
    const signValue = MinusRE.test(tz_sign) ? -1 : 1;
    return {
        offsetMinutes: signValue * (h * 60 + m),
        isUtc: false,
    };
}
/**
 * Parse a timezone offset string to offset minutes.
 *
 * Accepts multiple formats:
 * - ISO 8601: "+08:00", "-05:30", "Z"
 * - Luxon format: "UTC+8", "GMT-5"
 * - UTC variants: "UTC", "GMT", "Zulu"
 *
 * Supports seconds for archaic offsets like "-00:25:21" (Ireland 1880-1916).
 *
 * **Note:** Does NOT validate that the offset is a real-world timezone offset.
 * Use {@link validTzOffsetMinutes} for validation.
 *
 * @param str - Timezone offset string
 * @returns Offset in minutes, or undefined if invalid
 *
 * @example
 * ```typescript
 * parseTimezoneOffsetToMinutes("+08:00")     // 480
 * parseTimezoneOffsetToMinutes("UTC-5")      // -300
 * parseTimezoneOffsetToMinutes("Z")          // 0
 * parseTimezoneOffsetToMinutes("-00:25:21")  // -25.35 (archaic Ireland)
 * parseTimezoneOffsetToMinutes("invalid")   // undefined
 * ```
 */
function parseTimezoneOffsetToMinutes(str) {
    if (isUTC(str))
        return 0;
    const match = OffsetStringRE.exec(str);
    if (match?.groups == null)
        return;
    const { hours, minutes, seconds, sign } = match.groups;
    if (hours == null || sign == null)
        return;
    const h = (0, Number_1.toInt)(hours);
    const m = (0, Number_1.toInt)(minutes) ?? 0;
    const s = (0, Number_1.toInt)(seconds) ?? 0;
    if (h == null || h < 0 || h > 14 || m < 0 || m >= 60 || s < 0 || s >= 60)
        return;
    // Handle both ASCII minus (-) and Unicode minus (−, U+2212)
    const signValue = MinusRE.test(sign) ? -1 : 1;
    return signValue * (h * 60 + m + s / 60);
}
/**
 * Normalize a timezone input to a valid Luxon Zone.
 *
 * Accepts multiple input formats and returns a validated Zone instance, or
 * undefined if the input cannot be normalized to a valid timezone.
 *
 * Supported input formats:
 * - **Numbers**: Timezone offset in minutes (e.g., 480 = UTC+8, -300 = UTC-5)
 * - **Strings**: ISO offsets ("+08:00", "-05:00"), IANA zones
 *   ("America/Los_Angeles"), UTC variants ("UTC", "GMT", "Z", "Zulu")
 * - **Zone instances**: Validated and returned if valid
 *
 * The function respects Settings:
 * - {@link Settings.allowArchaicTimezoneOffsets} for pre-1982 offsets
 * - {@link Settings.allowBakerIslandTime} for UTC-12:00
 *
 * @param input - Timezone in various formats
 * @returns Valid Zone instance, or undefined if invalid
 *
 * @example
 * ```typescript
 * // Numbers (offset in minutes)
 * normalizeZone(480)?.name     // "UTC+8"
 * normalizeZone(-300)?.name    // "UTC-5"
 * normalizeZone(0)?.name       // "UTC"
 *
 * // ISO offset strings
 * normalizeZone("+08:00")?.name      // "UTC+8"
 * normalizeZone("-05:30")?.name      // "UTC-5:30"
 * normalizeZone("UTC+7")?.name       // "UTC+7"
 *
 * // IANA timezone names
 * normalizeZone("America/Los_Angeles")?.name  // "America/Los_Angeles"
 * normalizeZone("Asia/Tokyo")?.name           // "Asia/Tokyo"
 *
 * // UTC aliases
 * normalizeZone("UTC")?.name   // "UTC"
 * normalizeZone("GMT")?.name   // "UTC"
 * normalizeZone("Z")?.name     // "UTC"
 * normalizeZone("Zulu")?.name  // "UTC"
 *
 * // Invalid inputs return undefined
 * normalizeZone("invalid")     // undefined
 * normalizeZone("+25:00")      // undefined (beyond ±14 hours)
 * normalizeZone(1200)          // undefined (20 hours, beyond ±14 hours)
 * normalizeZone(100)           // undefined (not a valid timezone offset)
 * normalizeZone(-1)            // undefined (UnsetZone sentinel)
 * normalizeZone(null)          // undefined
 * ```
 */
function normalizeZone(input) {
    if (input == null ||
        (0, String_1.blank)(input) ||
        (!(0, Number_1.isNumber)(input) && !(0, String_1.isString)(input) && !isZone(input)) ||
        input === exports.UnsetZone ||
        input === exports.UnsetZoneOffsetMinutes ||
        isZoneUnset(input)) {
        return;
    }
    // wrapped in a try/catch as Luxon.settings.throwOnInvalid may be true:
    try {
        // This test and short-circuit may not be necessary, but it's cheap and
        // explicit:
        if (isUTC(input)) {
            return luxon_1.FixedOffsetZone.utcInstance;
        }
        // TypeScript-friendly: after line 386's type check, we know input is one of these
        let z = input;
        if ((0, Number_1.isNumber)(z) && !validTzOffsetMinutes(z)) {
            return;
        }
        if ((0, String_1.isString)(z)) {
            let s = z;
            z = s = s.replace(/^(?:Zulu|Z|GMT)(?:\b|$)/i, "UTC");
            // We also don't need to tease Info.normalizeZone with obviously
            // non-offset inputs:
            if ((0, String_1.blank)(s))
                return;
            const fixed = parseTimezoneOffsetToMinutes(s);
            if (fixed != null && validTzOffsetMinutes(fixed)) {
                return luxon_1.Info.normalizeZone(fixed);
            }
            if (!IanaFormatRE.test(s)) {
                return;
            }
        }
        const result = luxon_1.Info.normalizeZone(z);
        return isZoneValid(result) && result.name !== exports.UnsetZoneName
            ? result
            : undefined;
    }
    catch {
        return;
    }
}
/**
 * Convert a timezone to its short offset format (e.g., "+08:00", "-05:00").
 *
 * Useful for displaying timezone offsets in a standardized format. For IANA
 * zones with daylight saving time, provide a timestamp to get the correct
 * offset for that moment.
 *
 * @param zone - Timezone as Zone, string, or offset in minutes
 * @param ts - Optional timestamp (milliseconds) for IANA zone offset calculation.
 *             Defaults to current time if not provided.
 * @returns Zone offset in "+HH:MM" format, or "" if zone is invalid
 *
 * @example
 * ```typescript
 * // Fixed offsets
 * zoneToShortOffset("UTC+8")      // "+08:00"
 * zoneToShortOffset(480)          // "+08:00"
 * zoneToShortOffset("UTC-5:30")   // "-05:30"
 *
 * // IANA zones (offset depends on DST)
 * const winter = new Date("2023-01-15").getTime()
 * const summer = new Date("2023-07-15").getTime()
 * zoneToShortOffset("America/Los_Angeles", winter)  // "-08:00" (PST)
 * zoneToShortOffset("America/Los_Angeles", summer)  // "-07:00" (PDT)
 *
 * // Invalid zones return empty string
 * zoneToShortOffset("invalid")  // ""
 * zoneToShortOffset(null)       // ""
 * ```
 */
function zoneToShortOffset(zone, ts) {
    return normalizeZone(zone)?.formatOffset(ts ?? Date.now(), "short") ?? "";
}
/**
 * Type guard to check if a numeric offset (in minutes) represents a valid timezone.
 *
 * Validates that the offset:
 * - Is a number (not null/undefined)
 * - Is not the UnsetZone sentinel value (-1)
 * - Matches a real-world timezone offset (respects Settings for archaic offsets)
 *
 * Use this for exact validation without rounding. For error-tolerant rounding to
 * the nearest valid offset, use {@link inferLikelyOffsetMinutes} instead.
 *
 * @param tzOffsetMinutes - Offset in minutes to validate (e.g., 480 for UTC+8)
 * @returns true if the offset is exactly valid (type guard)
 *
 * @see {@link inferLikelyOffsetMinutes} for error-tolerant rounding
 *
 * @example
 * ```typescript
 * validTzOffsetMinutes(480)    // true (UTC+8)
 * validTzOffsetMinutes(-300)   // true (UTC-5)
 * validTzOffsetMinutes(330)    // true (UTC+5:30, India)
 * validTzOffsetMinutes(345)    // true (UTC+5:45, Nepal)
 *
 * validTzOffsetMinutes(481)    // false (not a valid timezone)
 * validTzOffsetMinutes(-1)     // false (UnsetZone sentinel)
 * validTzOffsetMinutes(null)   // false
 *
 * // Archaic offsets require Settings
 * Settings.allowArchaicTimezoneOffsets.value = false
 * validTzOffsetMinutes(-630)   // false (Hawaii -10:30, archaic)
 *
 * Settings.allowArchaicTimezoneOffsets.value = true
 * validTzOffsetMinutes(-630)   // true (Hawaii -10:30, archaic)
 * ```
 */
function validTzOffsetMinutes(tzOffsetMinutes) {
    return (tzOffsetMinutes != null &&
        (0, Number_1.isNumber)(tzOffsetMinutes) &&
        tzOffsetMinutes !== exports.UnsetZoneOffsetMinutes &&
        validOffsetMinutes().has(tzOffsetMinutes));
}
/**
 * Returns a "zone name" (used by `luxon`) that encodes the given offset.
 * @param offsetMinutes - The timezone offset in minutes from UTC
 * @returns The zone name (e.g., "UTC", "UTC+8", "UTC-5:30") or undefined if invalid
 */
function offsetMinutesToZoneName(offsetMinutes) {
    if (!validTzOffsetMinutes(offsetMinutes)) {
        return undefined;
    }
    if (offsetMinutes === 0)
        return "UTC";
    const sign = offsetMinutes < 0 ? "-" : "+";
    const absMinutes = Math.abs(offsetMinutes);
    const hours = Math.floor(absMinutes / 60);
    const minutes = Math.abs(absMinutes % 60);
    // luxon now renders simple hour offsets without padding:
    return `UTC${sign}` + hours + (minutes === 0 ? "" : `:${(0, String_1.pad2)(minutes)}`);
}
function tzHourToOffset(n) {
    return (0, Number_1.isNumber)(n) && validTzOffsetMinutes(n * 60)
        ? offsetMinutesToZoneName(n * 60)
        : undefined;
}
// Accept "Z", "UTC+2", "UTC+02", "UTC+2:00", "UTC+02:00", "+2", "+02",
// "+02:00", and "+0200" (ISO 8601 basic format). Also accepts seconds like
// "-00:25:21" for archaic offsets. Handles Unicode minus (−, U+2212).
const tzRe = /(?<Z>Z)|((UTC)?(?<sign>[+−-])(?<hours>\d\d?)(?::?(?<minutes>\d\d)(?::(?<seconds>\d\d))?)?)$/;
function extractOffsetFromHours(hourOffset) {
    return (0, Number_1.isNumber)(hourOffset)
        ? (0, Maybe_1.map)(tzHourToOffset(hourOffset), (zone) => ({
            zone,
            tz: zone,
            src: "hourOffset",
        }))
        : Array.isArray(hourOffset)
            ? extractOffsetFromHours(hourOffset[0])
            : undefined;
}
/**
 * Extract timezone information from various value types.
 *
 * Handles multiple input formats and performs intelligent parsing:
 * - **Strings**: ISO offsets ("+08:00"), IANA zones, UTC variants, timestamps
 *   with embedded timezones ("2023:01:15 10:30:00-08:00")
 * - **Numbers**: Hour offsets (e.g., -8 for UTC-8)
 * - **Arrays**: Uses first non-null value
 * - **ExifDateTime/ExifTime instances**: Extracts their zone property
 *
 * By default, strips timezone abbreviations (PST, PDT, etc.) as they are
 * ambiguous. Returns provenance information indicating which parsing method
 * succeeded.
 *
 * Supports Unicode minus signs (−, U+2212) and plus-minus signs (±, U+00B1)
 * in addition to ASCII +/-.
 *
 * @param value - Value to extract timezone from
 * @param opts.stripTZA - Whether to strip timezone abbreviations (default: true).
 *                        TZAs like "PST" are ambiguous and usually stripped.
 * @returns TzSrc with zone name and provenance, or undefined if no timezone found
 *
 * @example
 * ```typescript
 * // ISO offset strings
 * extractZone("+08:00")
 * // { zone: "UTC+8", tz: "UTC+8", src: "offsetMinutesToZoneName" }
 *
 * extractZone("UTC-5:30")
 * // { zone: "UTC-5:30", tz: "UTC-5:30", src: "normalizeZone" }
 *
 * // IANA zone names
 * extractZone("America/Los_Angeles")
 * // { zone: "America/Los_Angeles", tz: "America/Los_Angeles", src: "normalizeZone" }
 *
 * // Timestamps with embedded timezones
 * extractZone("2023:01:15 10:30:00-08:00")
 * // { zone: "UTC-8", tz: "UTC-8", src: "offsetMinutesToZoneName",
 * //   leftovers: "2023:01:15 10:30:00" }
 *
 * // Unicode minus signs
 * extractZone("−08:00")  // Unicode minus (U+2212)
 * // { zone: "UTC-8", tz: "UTC-8", src: "offsetMinutesToZoneName" }
 *
 * // Numeric hour offsets
 * extractZone(-8)
 * // { zone: "UTC-8", tz: "UTC-8", src: "hourOffset" }
 *
 * // Arrays (uses first non-null)
 * extractZone([null, "+05:30", undefined])
 * // { zone: "UTC+5:30", tz: "UTC+5:30", src: "offsetMinutesToZoneName" }
 *
 * // Strips timezone abbreviations by default
 * extractZone("2023:01:15 10:30:00-08:00 PST")
 * // { zone: "UTC-8", tz: "UTC-8", src: "offsetMinutesToZoneName",
 * //   leftovers: "2023:01:15 10:30:00" }
 *
 * // Invalid inputs return undefined
 * extractZone("invalid")  // undefined
 * extractZone(null)       // undefined
 * ```
 */
function extractZone(value, opts) {
    if (value == null ||
        typeof value === "boolean" ||
        value instanceof BinaryField_1.BinaryField ||
        value instanceof ExifDate_1.ExifDate) {
        return;
    }
    if (Array.isArray(value)) {
        // we only ever care about the first non-null value
        return extractZone(value.find((ea) => ea != null));
    }
    if (value instanceof ExifDateTime_1.ExifDateTime || value instanceof ExifTime_1.ExifTime) {
        return value.zone == null
            ? undefined
            : {
                zone: value.zone,
                tz: value.zone,
                src: value.constructor.name + ".zone",
            };
    }
    if ((0, Number_1.isNumber)(value)) {
        return extractOffsetFromHours(value);
    }
    if (typeof value !== "string" || (0, String_1.blank)(value)) {
        // don't accept ExifDate, boolean, BinaryField, ResourceEvent, Struct, or
        // Version instances:
        return;
    }
    {
        // If value is a proper timezone name, this may be easy!
        const z = normalizeZone(value);
        if (z != null) {
            return { zone: z.name, tz: z.name, src: "normalizeZone" };
        }
    }
    let str = value.trim();
    // Some EXIF datetime will "over-specify" and include both the utc offset
    // *and* the "time zone abbreviation"/TZA, like "PST" or "PDT". TZAs are
    // between 2 (AT) and 5 (WEST) characters.
    let strippedTza;
    if (opts?.stripTZA !== false &&
        // We only want to strip off the TZA if the input _doesn't_ end with "UTC"
        // or "Z"
        !/[.\d\s](?:UTC|Z)$/.test(str)) {
        const tzaMatch = str.match(/\s([a-z]{2,5})$/i);
        if (tzaMatch != null) {
            strippedTza = tzaMatch[1];
            str = str.slice(0, tzaMatch.index);
        }
    }
    {
        if ((0, String_1.blank)(str))
            return;
        const z = normalizeZone(str);
        if (z != null) {
            return { zone: z.name, tz: z.name, src: "normalizeZone" };
        }
    }
    const match = tzRe.exec(str);
    const capturedGroups = match?.groups;
    if (match != null && capturedGroups != null) {
        const leftovers = str.slice(0, match.index);
        if (capturedGroups.Z === "Z")
            return {
                zone: "UTC",
                tz: "UTC",
                src: "Z",
                ...((0, String_1.blank)(leftovers) ? {} : { leftovers }),
            };
        const hours = parseInt(capturedGroups.hours ?? "0");
        const minutes = parseInt(capturedGroups.minutes ?? "0");
        const seconds = parseInt(capturedGroups.seconds ?? "0");
        // Handle both ASCII minus (-) and Unicode minus (−, U+2212)
        const sign = MinusRE.test(capturedGroups.sign ?? "") ? -1 : 1;
        const offsetMinutes = sign * (hours * 60 + minutes + seconds / 60);
        const zone = offsetMinutesToZoneName(offsetMinutes);
        if (zone != null) {
            return {
                zone,
                tz: zone,
                src: "offsetMinutesToZoneName",
                ...((0, String_1.blank)(leftovers) ? {} : { leftovers }),
            };
        }
    }
    // Last resort: if we stripped a TZA but found no numeric offset, try
    // resolving the abbreviation itself (e.g. "PDT" → UTC-7).
    // User-provided overrides (Settings) take precedence over built-ins.
    if (strippedTza != null) {
        const key = strippedTza.toUpperCase();
        const offsetMinutes = Settings_1.Settings.tzAbbreviationOffsets.value[key] ??
            TzAbbreviationOffsetMinutes[key];
        if (offsetMinutes != null) {
            const zone = offsetMinutesToZoneName(offsetMinutes);
            if (zone != null) {
                return {
                    zone,
                    tz: zone,
                    src: "tzAbbreviation",
                    ...((0, String_1.blank)(str) ? {} : { leftovers: str }),
                };
            }
        }
    }
    return;
}
exports.TimezoneOffsetTagnames = [
    "TimeZone",
    // We **don't** look at "OffsetTime", as that is the offset for `ModifyDate`,
    // which is the _file_ modification time. See
    // https://github.com/photostructure/exiftool-vendored.js/issues/220 for
    // details.
    // time zone for DateTimeOriginal, "-08:00"
    "OffsetTimeOriginal",
    // time zone for CreateDate, "-08:00"
    "OffsetTimeDigitized",
    // srsly who came up with these wholly inconsistent tag names? _why not just
    // prefix tag names with "Offset"?!11_ SADNESS AND WOE
    // 1 or 2 values: 1. The time zone offset of DateTimeOriginal from GMT in
    // hours, 2. If present, the time zone offset of ModifyDate (which we
    // ignore) @see https://www.exiftool.org/TagNames/EXIF.html
    "TimeZoneOffset", // number | number[] | string
    // We DON'T use "GeolocationTimezone" here, as at this layer in the glue
    // factory we don't have access to the ExifTool option "ignoreZeroZeroLatLon"
];
function incrementZone(z, minutes) {
    const norm = normalizeZone(z);
    if (true !== norm?.isUniversal)
        return;
    const fixed = norm.offset(Date.now()); // < arg doesn't matter, it's universal
    return (0, Number_1.isNumber)(fixed)
        ? luxon_1.FixedOffsetZone.instance(fixed + minutes)
        : undefined;
}
/**
 * Extract timezone offset from standard EXIF timezone tags.
 *
 * Checks timezone tags in priority order:
 * 1. TimeZone
 * 2. OffsetTimeOriginal (for DateTimeOriginal)
 * 3. OffsetTimeDigitized (for CreateDate)
 * 4. TimeZoneOffset
 *
 * Handles camera-specific quirks like Nikon's DaylightSavings tag, which
 * requires adjusting the TimeZone offset forward by one hour during DST.
 *
 * @param t - EXIF tags object
 * @param opts.adjustTimeZoneIfDaylightSavings - Optional function to adjust
 *        timezone for DST. Defaults to handling Nikon's DaylightSavings quirk.
 * @returns TzSrc with zone and provenance, or undefined if no timezone found
 *
 * @see {@link TimezoneOffsetTagnames} for the list of tags checked
 * @see https://github.com/photostructure/exiftool-vendored.js/issues/215
 *
 * @example
 * ```typescript
 * const tags = await exiftool.read("photo.jpg")
 *
 * const tzSrc = extractTzOffsetFromTags(tags)
 * if (tzSrc) {
 *   console.log(`Timezone: ${tzSrc.zone}`)
 *   console.log(`Source: ${tzSrc.src}`)  // e.g., "OffsetTimeOriginal"
 * }
 *
 * // Nikon DST handling
 * const nikonTags = {
 *   TimeZone: "-08:00",
 *   DaylightSavings: "Yes",
 *   Make: "NIKON CORPORATION"
 * }
 * extractTzOffsetFromTags(nikonTags)
 * // { zone: "UTC-7", tz: "UTC-7",
 * //   src: "TimeZone (adjusted for DaylightSavings)" }
 * ```
 */
function extractTzOffsetFromTags(t, opts) {
    const adjustFn = opts?.adjustTimeZoneIfDaylightSavings ??
        DefaultExifToolOptions_1.defaultAdjustTimeZoneIfDaylightSavings;
    for (const tagName of exports.TimezoneOffsetTagnames) {
        const offset = extractZone(t[tagName]);
        if (offset == null)
            continue;
        // UGH. See https://github.com/photostructure/exiftool-vendored.js/issues/215
        const minutes = adjustFn(t, offset.tz);
        if (minutes != null) {
            const adjustedZone = incrementZone(offset.tz, minutes);
            if (adjustedZone != null)
                return {
                    zone: adjustedZone.name,
                    tz: adjustedZone.name,
                    src: tagName + " (adjusted for DaylightSavings)",
                };
        }
        // No fancy adjustments needed, just return the extracted zone:
        return { ...offset, src: tagName };
    }
    return;
}
function extractTzOffsetFromDatestamps(t, opts) {
    if (opts?.inferTimezoneFromDatestamps === true) {
        for (const tagName of opts.inferTimezoneFromDatestampTags ?? []) {
            if (t[tagName] != null) {
                const offset = extractZone(t[tagName]);
                // Some applications (looking at you, Google Takeout) will add a
                // spurious "+00:00" timezone offset to random datestamp tags, so
                // ignore UTC offsets here.
                if (offset != null && !isUTC(offset.tz)) {
                    return { ...offset, src: tagName };
                }
            }
        }
    }
    return;
}
function extractTzOffsetFromTimeStamp(t, opts) {
    if (opts?.inferTimezoneFromTimeStamp !== true)
        return;
    const ts = ExifDateTime_1.ExifDateTime.from(t.TimeStamp);
    if (ts == null)
        return;
    for (const tagName of opts.inferTimezoneFromDatestampTags ?? []) {
        const v = t[tagName];
        if (!(0, String_1.isString)(v) && !(v instanceof ExifDateTime_1.ExifDateTime))
            continue;
        const ea = ExifDateTime_1.ExifDateTime.from(v);
        if (ea == null)
            continue;
        if (ea.zone != null) {
            return { zone: ea.zone, tz: ea.zone, src: tagName };
        }
        const deltaMinutes = Math.floor((ea.toEpochSeconds("UTC") - ts.toEpochSeconds()) / 60);
        const likelyOffsetZone = inferLikelyOffsetMinutes(deltaMinutes);
        const zone = offsetMinutesToZoneName(likelyOffsetZone);
        if (zone != null) {
            return {
                zone,
                tz: zone,
                src: "offset between " + tagName + " and TimeStamp",
            };
        }
    }
    return;
}
/**
 * Round an arbitrary offset to the nearest valid timezone offset.
 *
 * This is error-tolerant timezone inference, useful for:
 * - GPS-based timezone calculation (where GPS time drift may cause errors)
 * - Handling clock drift in timestamp comparisons
 * - Fuzzy timezone matching
 *
 * By default, uses {@link Settings.maxValidOffsetMinutes} (30 minutes) as the
 * maximum distance from a valid timezone. This threshold handles GPS acquisition
 * lag and clock drift while preventing false matches.
 *
 * Respects Settings for archaic offsets, Baker Island time, and max offset tolerance.
 *
 * @param deltaMinutes - Offset in minutes to round (can be fractional)
 * @param maxValidOffsetMinutes - Maximum distance (in minutes) from a valid
 *        timezone to accept. Defaults to {@link Settings.maxValidOffsetMinutes}.
 * @returns Nearest valid offset in minutes, or undefined if too far from any
 *          valid timezone
 *
 * @see {@link validTzOffsetMinutes} for exact validation without rounding
 * @see {@link Settings.maxValidOffsetMinutes} to configure the default threshold
 *
 * @example
 * ```typescript
 * // Exact matches
 * inferLikelyOffsetMinutes(480)      // 480 (UTC+8, exact)
 * inferLikelyOffsetMinutes(-300)     // -300 (UTC-5, exact)
 *
 * // Rounding within default threshold (30 minutes)
 * inferLikelyOffsetMinutes(485)      // 480 (UTC+8, rounded from 485)
 * inferLikelyOffsetMinutes(-295)     // -300 (UTC-5, rounded from -295)
 * inferLikelyOffsetMinutes(330.5)    // 330 (UTC+5:30, rounded)
 *
 * // GPS-based inference with clock drift (within 30 min default)
 * const gpsTime = "2023:01:15 19:30:45"  // UTC
 * const localTime = "2023:01:15 11:32:12"  // Local with 1.5min drift
 * const deltaMinutes = 480 + 1.5  // ~481.5 minutes
 * inferLikelyOffsetMinutes(deltaMinutes)  // 480 (UTC+8)
 *
 * // GPS lag up to 23 minutes still works (within 30 min threshold)
 * inferLikelyOffsetMinutes(443)      // 420 (UTC-7, ~23 min from actual)
 *
 * // Beyond threshold returns undefined
 * inferLikelyOffsetMinutes(100)      // undefined (not near any valid offset)
 *
 * // Custom threshold
 * inferLikelyOffsetMinutes(495, 30)  // 480 (UTC+8 with 30min threshold)
 * inferLikelyOffsetMinutes(495, 15)  // undefined (beyond 15min threshold)
 *
 * // Adjust global default
 * Settings.maxValidOffsetMinutes.value = 15  // Stricter matching
 * inferLikelyOffsetMinutes(443)      // undefined (beyond 15min threshold)
 * ```
 */
function inferLikelyOffsetMinutes(deltaMinutes, maxValidOffsetMinutes = Settings_1.Settings.maxValidOffsetMinutes.value) {
    return deltaMinutes == null
        ? undefined
        : (0, Array_1.leastBy)(likelyOffsetMinutes(), (ea) => {
            const diff = Math.abs(ea - deltaMinutes);
            // Reject timezone offsets more than maxValidOffsetMinutes minutes away:
            return diff > maxValidOffsetMinutes ? undefined : diff;
        });
}
/**
 * Convert blank strings to undefined.
 */
function toNotBlank(x) {
    return x == null || (typeof x === "string" && (0, String_1.blank)(x)) ? undefined : x;
}
/**
 * Infer timezone offset by comparing local time with GPS/UTC time.
 *
 * Calculates the timezone by finding the difference between:
 * - A "captured at" timestamp (DateTimeOriginal, CreateDate, etc.) assumed to
 *   be in local time
 * - A UTC timestamp (GPSDateTime, DateTimeUTC, or combined GPSDateStamp +
 *   GPSTimeStamp)
 *
 * Uses {@link inferLikelyOffsetMinutes} to handle minor clock drift and round
 * to the nearest valid timezone offset.
 *
 * This is a fallback when explicit timezone tags are not available.
 *
 * @param t - Tags object with timestamp fields
 * @returns TzSrc with inferred timezone and provenance, or undefined if
 *          inference is not possible
 *
 * @see {@link extractTzOffsetFromTags} to check explicit timezone tags first
 *
 * @example
 * ```typescript
 * // GPS-based inference
 * const tags = {
 *   DateTimeOriginal: "2023:01:15 11:30:00",  // Local time (PST)
 *   GPSDateTime: "2023:01:15 19:30:00"        // UTC
 * }
 * extractTzOffsetFromUTCOffset(tags)
 * // { zone: "UTC-8", tz: "UTC-8",
 * //   src: "offset between DateTimeOriginal and GPSDateTime" }
 *
 * // DateTimeUTC-based inference
 * const tags2 = {
 *   CreateDate: "2023:07:20 14:15:30",  // Local time (JST)
 *   DateTimeUTC: "2023:07:20 05:15:30"  // UTC
 * }
 * extractTzOffsetFromUTCOffset(tags2)
 * // { zone: "UTC+9", tz: "UTC+9",
 * //   src: "offset between CreateDate and DateTimeUTC" }
 *
 * // Handles clock drift
 * const tags3 = {
 *   DateTimeOriginal: "2023:01:15 11:30:45",  // Local with drift
 *   GPSDateTime: "2023:01:15 19:29:58"        // UTC (old GPS fix)
 * }
 * extractTzOffsetFromUTCOffset(tags3)
 * // Still infers UTC-8 despite ~1 minute drift
 *
 * // No UTC timestamp available
 * const tags4 = {
 *   DateTimeOriginal: "2023:01:15 11:30:00"
 *   // No GPS or UTC timestamp
 * }
 * extractTzOffsetFromUTCOffset(tags4)  // undefined
 * ```
 */
function extractTzOffsetFromUTCOffset(t) {
    const utcSources = {
        ...(0, Pick_1.pick)(t, "GPSDateTime", "DateTimeUTC", "SonyDateTime2"),
        GPSDateTimeStamp: (0, Maybe_1.map2)(toNotBlank(t.GPSDateStamp), // Example: "2022:04:13"
        toNotBlank(t.GPSTimeStamp), // Example: "23:59:41.001"
        (a, b) => a + " " + b),
    };
    // We can always assume these are in UTC:
    const utc = (0, Maybe_1.first)([
        "GPSDateTime",
        "DateTimeUTC",
        "GPSDateTimeStamp",
        "SonyDateTime2",
    ], (tagName) => {
        const v = utcSources[tagName];
        const edt = v instanceof ExifDateTime_1.ExifDateTime ? v : ExifDateTime_1.ExifDateTime.fromExifStrict(v);
        const s = edt != null && (edt.zone == null || isUTC(edt.zone))
            ? edt.setZone("UTC", { keepLocalTime: true })?.toEpochSeconds()
            : undefined;
        return s != null
            ? {
                tagName,
                s,
            }
            : undefined;
    });
    if (utc == null)
        return;
    // If we can find any of these without a zone, the timezone should be the
    // offset between this time and the GPS time.
    const dt = (0, Maybe_1.first)(CapturedAtTagNames_1.CapturedAtTagNames, (tagName) => {
        const edt = ExifDateTime_1.ExifDateTime.fromExifStrict(t[tagName]);
        const s = edt != null && edt.zone == null
            ? edt.setZone("UTC", { keepLocalTime: true })?.toEpochSeconds()
            : undefined;
        return s != null
            ? {
                tagName,
                s,
            }
            : undefined;
    });
    if (dt == null)
        return;
    const diffSeconds = dt.s - utc.s;
    const offsetMinutes = inferLikelyOffsetMinutes(diffSeconds / 60);
    return (0, Maybe_1.map)(offsetMinutesToZoneName(offsetMinutes), (zone) => ({
        zone,
        tz: zone,
        src: `offset between ${dt.tagName} and ${utc.tagName}`,
    }));
}
/**
 * Check if two timezone values are equivalent at a specific point in time.
 *
 * Two zones are considered equivalent if they:
 * - Are the same zone (via Luxon's Zone.equals()), OR
 * - Have the same offset at the specified timestamp
 *
 * This is useful for:
 * - De-duplicating timezone records
 * - Comparing zones in different formats ("UTC+5" vs "UTC+05:00")
 * - Matching IANA zones to their offset at a specific time
 *
 * For IANA zones with DST, you can specify a timestamp to evaluate equivalence
 * at that moment. This is important when comparing historical records or future
 * events where DST transitions matter.
 *
 * @param a - First timezone (Zone, string, or offset in minutes)
 * @param b - Second timezone (Zone, string, or offset in minutes)
 * @param at - Timestamp in milliseconds to evaluate zone offsets.
 *             Defaults to current time (Date.now()).
 * @returns true if zones are equivalent at the specified time
 *
 * @example
 * ```typescript
 * // Same zone, different formats
 * equivalentZones("UTC+5", "UTC+05:00")     // true
 * equivalentZones("UTC-8", -480)            // true (480 minutes = 8 hours)
 * equivalentZones("GMT", "UTC")             // true
 * equivalentZones("Z", 0)                   // true
 *
 * // IANA zones matched by current offset (default behavior)
 * equivalentZones("America/New_York", "UTC-5")  // true in winter (EST)
 * equivalentZones("America/New_York", "UTC-4")  // true in summer (EDT)
 *
 * // IANA zones at specific times
 * const winter = new Date("2023-01-15").getTime()
 * const summer = new Date("2023-07-15").getTime()
 * equivalentZones("America/New_York", "UTC-5", winter)  // true (EST)
 * equivalentZones("America/New_York", "UTC-4", winter)  // false (not EDT in winter)
 * equivalentZones("America/New_York", "UTC-4", summer)  // true (EDT)
 * equivalentZones("America/New_York", "UTC-5", summer)  // false (not EST in summer)
 *
 * // Compare two IANA zones at a specific time
 * equivalentZones("America/New_York", "America/Toronto", winter)  // true (both EST)
 * equivalentZones("America/New_York", "America/Los_Angeles", winter)  // false (EST vs PST)
 *
 * // Different zones
 * equivalentZones("UTC+8", "UTC+9")         // false
 *
 * // Invalid zones return false
 * equivalentZones("invalid", "UTC")         // false
 * equivalentZones(null, "UTC")              // false
 * ```
 */
function equivalentZones(a, b, at = Date.now()) {
    const az = normalizeZone(a);
    const bz = normalizeZone(b);
    return (az != null &&
        bz != null &&
        (az.equals(bz) || az.offset(at) === bz.offset(at)));
}
function getZoneName(args = {}) {
    const result = normalizeZone(args.zone)?.name ??
        normalizeZone(args.zoneName)?.name ??
        offsetMinutesToZoneName(args.tzoffsetMinutes);
    return (0, String_1.blank)(result) || result === exports.UnsetZoneName ? undefined : result;
}
//# sourceMappingURL=Timezones.js.map