///|
let registered_timezones : Map[String, DateTimeZone] = Map([])
///|
let named_timezone_cache : Map[String, DateTimeZone] = Map([])
///|
fn utc_timezone() -> DateTimeZone {
{
initial_offset_seconds: 0,
initial_abbreviation: Some("UTC"),
transitions: [],
footer: None,
dateutil_footer: false,
footer_start_seconds: None,
}
}
///|
/// Register supplied TZif bytes for string-based date timezone lookup. This
/// uses dateutil's 32-bit compatibility block, including its historical/future
/// fallback behavior. Use tzif_timezone directly for full 64-bit IANA rules.
/// Registration is explicit and works without host files, including in browsers.
pub fn register_timezone(name : String, data : Bytes) -> Unit raise DateError {
if name == "" || name == "UTC" || name.has_prefix(":") {
raise DateError::InvalidDate(
message="registered timezone needs a non-UTC name",
)
}
let zone = dateutil_tzif_timezone(data)
registered_timezones[name] = zone
ignore(named_timezone_cache.remove(name))
}
///|
/// Remove an explicit TZif registration. Previously constructed consumers keep
/// their captured rules; subsequent name lookups resume normal host resolution.
pub fn unregister_timezone(name : String) -> Unit {
ignore(registered_timezones.remove(name))
ignore(named_timezone_cache.remove(name))
}
///|
/// Resolve an explicit string, or the current timezone rc when omitted. Exact
/// "UTC" is portable. Other names use registered bytes, host TZif files, then
/// standard POSIX strings. Unavailable/unknown names raise DateError.
/// Construction and resolution do not read the numeric date epoch or clock.
pub fn get_timezone(name? : String) -> DateTimeZone raise DateError {
let name = name.unwrap_or(@config.timezone())
if name == "UTC" {
return utc_timezone()
}
if registered_timezones.get(name) is Some(zone) {
return zone
}
if name != "" && name != ":" {
if named_timezone_cache.get(name) is Some(zone) {
return zone
}
}
let zone = resolve_timezone_name(name)
if name != "" && name != ":" {
named_timezone_cache[name] = zone
}
zone
}
///|
fn resolve_timezone_name(raw : String) -> DateTimeZone raise DateError {
let name = if raw == "" { @env.get_env_var("TZ").unwrap_or("") } else { raw }
let name = if name.has_prefix(":") { name[1:].to_owned() } else { name }
if name == "" {
if @date_host.read_zone_file("/etc/localtime") is Some(data) {
return dateutil_tzif_timezone(data)
}
raise DateError::InvalidDate(message="host local timezone is unavailable")
}
if name == "UTC" {
return utc_timezone()
}
if registered_timezones.get(name) is Some(zone) {
return zone
}
if name.has_prefix("/") {
if @date_host.read_zone_file(name) is Some(data) {
return dateutil_tzif_timezone(data)
}
} else {
for
directory in [
"/usr/share/zoneinfo", "/usr/lib/zoneinfo", "/usr/share/lib/zoneinfo", "/etc/zoneinfo",
] {
let path = directory + "/" + name
for path in [path, path.replace_all(old=" ", new="_")] {
if @date_host.read_zone_file(path) is Some(data) {
let zone = Some(dateutil_tzif_timezone(data)) catch { _ => None }
if zone is Some(zone) {
return zone
}
}
}
}
if name.to_array().any(c => c >= '0' && c <= '9') {
let zone = Some(dateutil_posix_timezone(name)) catch { _ => None }
if zone is Some(zone) {
return zone
}
} else if name == "GMT" {
return utc_timezone()
}
}
raise DateError::InvalidDate(
message="invalid or unavailable timezone '\{raw}'",
)
}
///|
fn selected_timezone(
tz : String?,
timezone : DateTimeZone?,
) -> DateTimeZone raise DateError {
match (tz, timezone) {
(Some(_), Some(_)) =>
raise DateError::InvalidDate(message="use either tz or timezone")
(_, Some(zone)) => zone
_ => get_timezone(name?=tz)
}
}
///|
// DateFormatter captures a zone. Auto/Concise retain the original tz argument
// and resolve it when text is formatted; callbacks receive raw values instead.
priv enum FormatterTimezone {
Captured
DefaultTimezone
NamedTimezone(String)
}
///|
fn formatter_timezone_source(
tz : String?,
timezone : DateTimeZone?,
) -> FormatterTimezone raise DateError {
match (tz, timezone) {
(Some(_), Some(_)) =>
raise DateError::InvalidDate(message="use either tz or timezone")
(_, Some(_)) => Captured
(Some(name), None) => NamedTimezone(name)
(None, None) => DefaultTimezone
}
}
///|
fn timezone_digit(char : Int32) -> Bool {
char >= 48 && char <= 57
}
///|
fn timezone_number(
spec : String,
start : Int32,
) -> (Int32, Int32) raise DateError {
let mut index = start
let mut value = 0
while index < spec.length() && timezone_digit(spec[index].to_int()) {
value = value * 10 + spec[index].to_int() - '0'.to_int()
if value > 1000000 {
raise DateError::InvalidDate(message="timezone number out of range")
}
index += 1
}
if index == start {
raise DateError::InvalidDate(message="missing timezone number")
}
(value, index)
}
///|
fn timezone_abbreviation(spec : String, start : Int32) -> (String, Int32) {
let mut index = start
while index < spec.length() &&
!timezone_digit(spec[index].to_int()) &&
spec[index] != ':' &&
spec[index] != ',' &&
spec[index] != ';' &&
spec[index] != '-' &&
spec[index] != '+' {
index += 1
}
(spec[start:index].to_owned(), index)
}
///|
// dateutil accepts H/HH, HHMM or H:MM offsets, but not offset seconds.
// Transition clocks (after '/') additionally accept seconds.
fn timezone_string_offset(
spec : String,
start : Int32,
) -> (Int32?, Int32) raise DateError {
let mut index = start
if index >= spec.length() ||
(
!timezone_digit(spec[index].to_int()) &&
spec[index] != '+' &&
spec[index] != '-'
) {
return (None, index)
}
let mut sign = -1
if spec[index] == '+' || spec[index] == '-' {
if spec[index] == '-' {
sign = 1
}
index += 1
}
let digits = index
let (number, after) = timezone_number(spec, index)
index = after
let seconds = if index - digits == 4 {
number / 100 * 3600 + number % 100 * 60
} else if index < spec.length() && spec[index] == ':' {
if number > 24 {
raise DateError::InvalidDate(message="timezone offset out of range")
}
let (minute, after) = timezone_number(spec, index + 1)
index = after
number * 3600 + minute * 60
} else if index - digits <= 2 {
number * 3600
} else {
raise DateError::InvalidDate(message="invalid timezone offset")
}
(Some(sign * seconds), index)
}
///|
fn dateutil_posix_timezone(spec : String) -> DateTimeZone raise DateError {
let (standard_name, index) = timezone_abbreviation(spec, 0)
if standard_name == "" {
raise DateError::InvalidDate(message="missing timezone name")
}
let (standard, index) = timezone_string_offset(spec, index)
let mut standard = standard.unwrap_or(0)
if standard_name == "GMT" || standard_name == "UTC" {
standard = -standard
}
let (daylight_name, index) = timezone_abbreviation(spec, index)
let (daylight, index) = timezone_string_offset(spec, index)
if index < spec.length() && spec[index] != ',' && spec[index] != ';' {
raise DateError::InvalidDate(message="invalid timezone string suffix")
}
let rules = normalize_dateutil_rules(
spec[index:].to_owned().replace_all(old=";", new=","),
)
// Use the shared checked engine for GNU calendar rules, not its extra TZif
// clock-basis suffixes or signed transition-time extensions.
if rules
.to_array()
.any(char => {
!(timezone_digit(char.to_int()) ||
char == ',' ||
char == 'M' ||
char == 'J' ||
char == '.' ||
char == '/' ||
char == ':' ||
char == '-')
}) {
raise DateError::InvalidDate(
message="unsupported timezone transition syntax",
)
}
let synthetic = "STD0DST" + rules
let context = DateParserContext::from_posix(synthetic)
let template = match context.source {
Posix(rule) => rule
_ => abort("POSIX context invariant")
}
let has_dst = daylight_name != ""
let daylight = if has_dst {
Some(daylight.unwrap_or(standard + 3600))
} else {
None
}
validate_offset_seconds(standard)
if daylight is Some(offset) {
validate_offset_seconds(offset)
}
let rule : @tztypes.PosixTzRule = {
std_abbr: standard_name,
std_offset: standard,
dst_abbr: if has_dst {
Some(daylight_name)
} else {
None
},
dst_offset: daylight,
start_rule: if has_dst {
template.start_rule
} else {
None
},
end_rule: if has_dst {
template.end_rule
} else {
None
},
}
{
initial_offset_seconds: standard,
initial_abbreviation: Some(standard_name),
transitions: [],
footer: Some(rule),
dateutil_footer: true,
footer_start_seconds: None,
}
}
///|
fn normalize_dateutil_rules(rules : String) -> String raise DateError {
if rules == "" {
return rules
}
let result : Array[String] = []
for part in rules.split(",") {
let pieces = part
.to_owned()
.split("/")
.map(piece => piece.to_owned())
.to_array()
let date = if pieces[0].has_prefix("M") {
pieces[0].replace_all(old="-", new=".")
} else {
pieces[0]
}
if pieces.length() == 1 {
result.push(date)
continue
}
if pieces.length() != 2 {
raise DateError::InvalidDate(message="invalid timezone transition clock")
}
let clock = pieces[1]
let (number, after) = timezone_number(clock, 0)
let mut index = after
let mut seconds = if after == 4 {
number / 100 * 3600 + number % 100 * 60
} else {
if after > 2 && !(after < clock.length() && clock[after] == ':') {
raise DateError::InvalidDate(
message="invalid timezone transition clock",
)
}
if number > 167 {
raise DateError::InvalidDate(message="transition hour out of range")
}
number * 3600
}
if index < clock.length() && clock[index] == ':' {
let (minutes, next) = timezone_number(clock, index + 1)
seconds += minutes * 60
index = next
if index < clock.length() && clock[index] == ':' {
let (fraction, next) = timezone_number(clock, index + 1)
seconds += fraction
index = next
}
}
if index != clock.length() || seconds > 604799 {
raise DateError::InvalidDate(message="invalid timezone transition clock")
}
result.push(
date +
"/" +
(seconds / 3600).to_string() +
":" +
(seconds / 60 % 60).to_string() +
":" +
(seconds % 60).to_string(),
)
}
result.join(",")
}