///|
priv enum ParserZoneSource {
Tzif(@tztypes.TzifData)
Posix(@tztypes.PosixTzRule)
}
///|
/// Clock and host-local rules for date-string parsing. Explicit contexts avoid
/// system files and use the same TZif/POSIX engine as ordinary date axes.
pub struct DateParserContext {
priv source : ParserZoneSource
priv clock : () -> Int64
priv local_override : ParserLocalSnapshot?
}
///|
fn parser_clock() -> Int64 {
@env.now().reinterpret_as_int64()
}
///|
/// Supply raw TZif bytes and, optionally, a Unix-millisecond clock. Construction
/// does not read the clock or lock the numeric date epoch.
pub fn DateParserContext::from_tzif(
data : Bytes,
clock? : () -> Int64,
) -> DateParserContext raise DateError {
{
source: Tzif(date_tzif_data(data)),
clock: clock.unwrap_or(parser_clock),
local_override: None,
}
}
///|
/// Supply a POSIX TZ specification, including standard/daylight names and rules.
pub fn DateParserContext::from_posix(
spec : String,
clock? : () -> Int64,
) -> DateParserContext raise DateError {
let data = @utf8.encode("\n" + spec + "\n")
let rule = match @tzparser.parse_footer(data, offset=0, version=b'3') {
Ok(Some(rule)) => rule
_ => raise DateError::InvalidDate(message="invalid parser POSIX timezone")
}
validate_offset_seconds(rule.std_offset)
if rule.dst_offset is Some(offset) {
validate_offset_seconds(offset)
}
{
source: Posix(rule),
clock: clock.unwrap_or(parser_clock),
local_override: None,
}
}
///|
pub fn DateParserContext::utc(clock? : () -> Int64) -> DateParserContext {
try! DateParserContext::from_posix("UTC0", clock?)
}
///|
/// Resolve TZ/TZDIR or /etc/localtime using the runtime's file capability.
/// Unavailable host-local rules require an explicit context; no UTC fallback.
pub fn DateParserContext::system(
clock? : () -> Int64,
) -> DateParserContext raise DateError {
match @env.get_env_var("TZ") {
Some(raw) => {
let spec = if raw.has_prefix(":") { raw[1:].to_owned() } else { raw }
if spec == "" {
return DateParserContext::utc(clock?)
}
let paths = if spec.has_prefix("/") {
[spec]
} else {
[
@env.get_env_var("TZDIR").unwrap_or("/usr/share/zoneinfo") +
"/" +
spec,
"/usr/share/lib/zoneinfo/" + spec,
"/usr/lib/zoneinfo/" + spec,
]
}
for path in paths {
if @date_host.read_zone_file(path) is Some(data) {
return DateParserContext::from_tzif(data, clock?)
}
}
return DateParserContext::from_posix(spec, clock?)
}
None => {
if @date_host.read_zone_file("/etc/localtime") is Some(data) {
return DateParserContext::from_tzif(data, clock?)
}
raise DateError::InvalidDate(
message="host local timezone is unavailable; supply DateParserContext",
)
}
}
}
///|
fn DateParserContext::info(
self : DateParserContext,
seconds : Int64,
) -> (Int32, Bool, String) raise DateError {
match self.source {
Tzif(data) =>
match @tzif.active_offset(data, seconds) {
Ok(info) => (info.utoff, info.is_dst, info.abbr)
Err(_) =>
raise DateError::InvalidDate(
message="parser timezone query is outside its supported range",
)
}
Posix(rule) => {
if rule.dst_offset is None {
return (rule.std_offset, false, rule.std_abbr)
}
match @tzposix.apply_posix_rule(rule, seconds) {
Ok(info) => info
Err(_) =>
raise DateError::InvalidDate(
message="parser POSIX timezone query failed",
)
}
}
}
}
///|
fn DateParserContext::local_now(
self : DateParserContext,
millis : Int64,
) -> DateTime raise DateError {
if millis < -62135683200000L || millis >= 253402387200000L {
raise DateError::InvalidDate(
message="parser clock is outside years 1..9999",
)
}
let seconds = locator_floor_div64(millis, 1000L)
let (offset, _, _) = self.info(seconds)
let wall_microseconds = millis * 1000L + Int64::from_int(offset) * 1_000_000L
if wall_microseconds < -62135596800000000L ||
wall_microseconds >= 253402300800000000L {
raise DateError::InvalidDate(
message="parser local clock is outside years 1..9999",
)
}
locator_datetime_microseconds(wall_microseconds, 0)
}
///|
priv struct ParserLocalSnapshot {
standard : Int32
daylight : Int32
standard_name : String
daylight_name : String
}
///|
fn parser_short_name(name : String) -> String {
let chars = name.to_array()
String::from_array(chars[:Int32::min(9, chars.length())])
}
///|
fn DateParserContext::snapshot(
self : DateParserContext,
millis : Int64,
) -> ParserLocalSnapshot raise DateError {
if self.local_override is Some(snapshot) {
return snapshot
}
let seconds = locator_floor_div64(millis, 1000L)
let january = locator_floor_div64(seconds, 31_557_600L) * 31_557_600L
let (jan, _, jan_name) = self.info(january)
let (july, _, july_name) = self.info(january + 15_778_800L)
if jan > july {
{
standard: july,
daylight: jan,
standard_name: parser_short_name(july_name),
daylight_name: parser_short_name(jan_name),
}
} else {
{
standard: jan,
daylight: july,
standard_name: parser_short_name(jan_name),
daylight_name: parser_short_name(july_name),
}
}
}
///|
/// Supply a host's explicit standard/daylight descriptor instead of the pinned
/// CPython sampling profile. Rules still provide actual clock and DST queries.
/// The two names are copied at construction; no clock or epoch is read.
pub fn DateParserContext::with_local_info(
self : DateParserContext,
standard_seconds : Int32,
daylight_seconds : Int32,
names : Array[String],
) -> DateParserContext raise DateError {
validate_offset_seconds(standard_seconds)
validate_offset_seconds(daylight_seconds)
if names.length() != 2 {
raise DateError::InvalidDate(
message="parser local names must contain standard and daylight entries",
)
}
{
..self,
local_override: Some({
standard: standard_seconds,
daylight: daylight_seconds,
standard_name: names[0],
daylight_name: names[1],
}),
}
}