import { FixedOffsetZone , Info } from "luxon" ; import { compact } from "./Array" ; import { MinuteMs } from "./DateTime" ; import { ExifDateTime } from "./ExifDateTime" ; import { first , firstDefinedThunk , map , Maybe } from "./Maybe" ; import { isNumber } from "./Number" ; import { blank , isString , pad2 } from "./String" ; export const MaxTzOffsetHours = 14 ; export const UnsetZoneOffsetMinutes = - 1 ; export const UnsetZone = FixedOffsetZone . instance ( UnsetZoneOffsetMinutes ) ; export const UnsetZoneName = UnsetZone . name ; export function reasonableTzOffsetMinutes ( tzOffsetMinutes ) { return ( isNumber ( tzOffsetMinutes ) && Math . abs ( tzOffsetMinutes ) < MaxTzOffsetHours * 60 ) ; } /**
 * Returns a "zone name" (used by `luxon`) that encodes the given offset.
 */ export function offsetMinutesToZoneName ( offsetMinutes ) { if ( offsetMinutes == null || ! isNumber ( offsetMinutes ) || offsetMinutes === UnsetZoneOffsetMinutes ) return undefined ; if ( offsetMinutes === 0 ) return "UTC" ; const sign = offsetMinutes < 0 ? "-" : "+" ; const absMinutes = Math . abs ( offsetMinutes ) ; if ( absMinutes > MaxTzOffsetHours * 60 ) return undefined ; const hours = Math . floor ( absMinutes / 60 ) ; const minutes = Math . abs ( absMinutes % 60 ) ; return ( `UTC${ sign }` + ( minutes === 0 ? `${ pad2 ( hours ) }` : `${ pad2 ( hours ) }:${ pad2 ( minutes ) }` ) ) ; } function dtToMs ( s , defaultZone ? ) { return map ( ExifDateTime . fromExifStrict ( s , defaultZone ) , ( dt ) => dt . toDate ( ) . getTime ( ) ) ; } function utcToMs ( s ) { return dtToMs ( s , "UTC" ) ; } function tzHourToOffset ( n ) { return isNumber ( n ) && reasonableTzOffsetMinutes ( n * 60 ) ? offsetMinutesToZoneName ( n * 60 ) : undefined ; } const tzRe = /(?:UTC)?([+-]?)(\d\d?)(?::(\d\d))?/ ; export interface TzSrc { tz : string ; src : string ; } /**
 * Parse a timezone offset and return the offset minutes
 */ export function extractOffset ( tz ) { if ( tz == null || blank ( tz ) ) { return undefined ; } if ( isString ( tz ) && Info . isValidIANAZone ( tz ) ) { return { tz , src : "validIANAZone" } ; } return map ( tzRe . exec ( tz ) , ( m ) => map ( offsetMinutesToZoneName ( ( m [ 1 ] === "-" ? - 1 : 1 ) * ( parseInt ( m [ 2 ] ! ) * 60 + parseInt ( m [ 3 ] ?? "0" ) ) ) , ( ea ) => ( { tz : ea , src : "offsetMinutesToZoneName" } ) ) ) ; } export function extractTzOffsetFromTags ( t ) { return firstDefinedThunk ( [ ( ) => first ( [ "TimeZone" , "OffsetTime" , "OffsetTimeOriginal" , "OffsetTimeDigitized" , "TimeZoneOffset" , ] , ( tagName ) => map ( extractOffset ( ( t as any ) [ tagName ] ) , ( ea ) => ( { tz : ea . tz , src : ea . src + " from " + tagName , } ) ) ) , ( ) => map ( t . TimeZoneOffset , ( value ) => map ( tzHourToOffset ( Array . isArray ( value ) ? value [ 0 ] : value ) , ( tz ) => ( { tz , src : "TimeZoneOffset" , } ) ) ) , ] ) ; } function firstUtcMs ( tags , tagNames ) { return first ( tagNames , ( tagName ) => map ( utcToMs ( tags [ tagName ] ) , ( utcMs ) => ( { tagName , utcMs } ) ) ) ; } const TzBoundaryMinutes = 30 ; export function inferLikelyOffsetMinutes ( deltaMs ) { return TzBoundaryMinutes * Math . floor ( deltaMs / MinuteMs / TzBoundaryMinutes ) ; } export function extractTzOffsetFromUTCOffset ( t ) { const gpsStamps = compact ( [ t . GPSDateStamp , t . GPSTimeStamp ] ) ; const GPSDateTimeStamp = gpsStamps . length === 2 ? gpsStamps . join ( " " ) : undefined ; const utc = firstUtcMs ( { ... t , GPSDateTimeStamp } , [ "GPSDateTime" , "DateTimeUTC" , "GPSDateTimeStamp" , ] ) ; const dt = firstUtcMs ( t , [ "SubSecDateTimeOriginal" , "DateTimeOriginal" , "SubSecCreateDate" , "CreateDate" , "SubSecMediaCreateDate" , "MediaCreateDate" , "DateTimeCreated" , ] ) ; if ( utc == null || dt == null ) return ; const offsetMinutes = inferLikelyOffsetMinutes ( dt . utcMs - utc . utcMs ) ; return map ( offsetMinutesToZoneName ( offsetMinutes ) , ( tz ) => ( { tz , src : `offset between ${ dt . tagName } and ${ utc . tagName }` , } ) ) ; }