module JosieHealth::Utils module Timezone # Common timezone abbreviation aliases -> IANA timezone names ALIASES = { # Europe "cet" => "Europe/Berlin", "cest" => "Europe/Berlin", "wet" => "Europe/London", "gmt" => "Europe/London", "bst" => "Europe/London", "eet" => "Europe/Helsinki", "eest" => "Europe/Helsinki", # Americas "est" => "America/New_York", "edt" => "America/New_York", "cst" => "America/Chicago", "cdt" => "America/Chicago", "mst" => "America/Denver", "mdt" => "America/Denver", "pst" => "America/Los_Angeles", "pdt" => "America/Los_Angeles", # Asia/Pacific "jst" => "Asia/Tokyo", "kst" => "Asia/Seoul", "cst8" => "Asia/Shanghai", "hkt" => "Asia/Hong_Kong", "ist" => "Asia/Kolkata", "aest" => "Australia/Sydney", "aedt" => "Australia/Sydney", "awst" => "Australia/Perth", "nzst" => "Pacific/Auckland", "nzdt" => "Pacific/Auckland", # UTC "utc" => "UTC", "z" => "UTC", } # Resolve timezone abbreviation to IANA name # Returns nil if not a known alias def self.resolve_alias(input : String) : String? ALIASES[input.downcase.strip]? end # Check if input is a known timezone alias def self.alias?(input : String) : Bool ALIASES.has_key?(input.downcase.strip) end # Parse UTC offset format (+02:00, -05:00, +2, -5) # Returns tuple of (sign, hours, minutes) or nil if invalid def self.parse_offset(input : String) : NamedTuple(sign: String, hours: Int32, minutes: Int32)? if match = input.match(/^([+-])(\d{1,2})(?::?(\d{2}))?$/) sign = match[1] hours = match[2].to_i minutes = match[3]?.try(&.to_i) || 0 return nil if hours > 14 || minutes > 59 {sign: sign, hours: hours, minutes: minutes} end end # Format offset as string (e.g., "+02:00") def self.format_offset(sign : String, hours : Int32, minutes : Int32) : String "#{sign}#{hours.to_s.rjust(2, '0')}:#{minutes.to_s.rjust(2, '0')}" end # Validate if input is a valid IANA timezone name def self.valid_iana?(input : String) : Bool begin Time::Location.load(input) true rescue Time::Location::InvalidLocationNameError false end end # Get current UTC offset string for a timezone def self.current_offset(tz : String) : String # Handle stored offset format (+02:00) if tz.match(/^[+-]\d{2}:\d{2}$/) return "UTC#{tz}" end begin location = Time::Location.load(tz) now = Time.local(location) offset_seconds = now.offset hours = offset_seconds // 3600 minutes = (offset_seconds.abs % 3600) // 60 sign = offset_seconds >= 0 ? "+" : "-" "UTC#{sign}#{hours.abs.to_s.rjust(2, '0')}:#{minutes.to_s.rjust(2, '0')}" rescue "UTC" end end # All known timezone aliases def self.all_aliases : Array(String) ALIASES.keys end end end