///|
pub(all) enum DateParseWarning {
UnknownTimezone(String)
} derive(Debug, Eq)
///|
pub extend DateParseWarning with @debug.Debug::{to_repr}
///|
pub extend DateParseWarning with Eq::{equal, not_equal}
///|
/// Default dateutil grammar with an explicit clock/local-zone context. The
/// reference year and local-name descriptor are captured at construction.
pub struct DateStringParser {
priv context : DateParserContext
priv reference : Int32
priv snapshot : ParserLocalSnapshot
priv mut diagnostics : Array[DateParseWarning]
}
///|
/// Construct with host-local rules, or provide an explicit portable context.
///
/// ```mbt check
/// test {
/// let context = @date.DateParserContext::from_posix(
/// "EST5EDT,M3.2.0/2,M11.1.0/2",
/// clock=fn() { 1781524800000L },
/// )
/// let parser = @date.DateStringParser::new(context~)
/// let value = @date.datestr2num("Nov 3, 2024 01:30 EST", parser~)
/// assert_eq(@date.num2date(value).hour(), 6)
/// }
/// ```
pub fn DateStringParser::new(
context? : DateParserContext,
reference_year? : Int32,
) -> DateStringParser raise DateError {
let context = context.unwrap_or_else(() => DateParserContext::system())
let millis = (context.clock)()
let current = context.local_now(millis)
let reference = reference_year.unwrap_or(current.year)
if reference < 1 || reference > 9999 {
raise DateError::InvalidDate(
message="parser reference year must be in 1..9999",
)
}
{ context, reference, snapshot: context.snapshot(millis), diagnostics: [], }
}
///|
pub fn DateStringParser::copy(self : DateStringParser) -> DateStringParser {
{ ..self, diagnostics: self.diagnostics.copy(), }
}
///|
pub fn DateStringParser::reference_year(self : DateStringParser) -> Int32 {
self.reference
}
///|
pub fn DateStringParser::local_names(self : DateStringParser) -> Array[String] {
[self.snapshot.standard_name, self.snapshot.daylight_name]
}
///|
pub fn DateStringParser::local_offsets(
self : DateStringParser,
) -> (Int32, Int32) {
(self.snapshot.standard, self.snapshot.daylight)
}
///|
/// Diagnostics from the last operation, including warnings emitted before a
/// later parse/conversion error. Returned arrays do not alias parser state.
pub fn DateStringParser::warnings(
self : DateStringParser,
) -> Array[DateParseWarning] {
self.diagnostics.copy()
}
///|
priv struct ParsedStringDate {
value : DateTime
aware : Bool
fold : Bool
}
///|
fn DateStringParser::parse_record(
self : DateStringParser,
text : String,
default_date : DateTime?,
) -> ParsedStringDate raise DateError {
try {
let fallback = match default_date {
Some(value) => value
None => {
let now = self.context.local_now((self.context.clock)())
datetime(now.year, now.month, now.day)
}
}
let fields = parser_select(text, self.reference)
let naive = parser_build_naive(fields, fallback)
let fold = if fields.day is None && fields.weekday is Some(_) {
false
} else {
parser_default_fold(fallback)
}
let inherited = default_date is Some(_)
if fields.tzname is Some(name) &&
[self.snapshot.standard_name, self.snapshot.daylight_name].contains(name) {
let (offset, actual_name) = self.context.local_info(
naive,
self.snapshot,
fold,
)
let (offset, actual_name) = if actual_name != name {
let (later_offset, later_name) = self.context.local_info(
naive,
self.snapshot,
true,
)
if later_name == name {
(later_offset, later_name)
} else {
(offset, actual_name)
}
} else {
(offset, actual_name)
}
if actual_name != name && parser_utc(name) {
return {
value: parser_set_offset(naive, 0, Some("UTC")),
aware: true,
fold,
}
}
return {
value: parser_set_offset(naive, offset, Some(actual_name)),
aware: true,
fold,
}
}
if fields.tzoffset is Some(offset) {
return {
value: parser_set_offset(
naive,
offset,
if offset == 0 {
Some("UTC")
} else {
fields.tzname
},
),
aware: true,
fold,
}
}
if fields.tzname is Some(name) {
self.diagnostics.push(UnknownTimezone(name))
}
{ value: naive, aware: inherited, fold, }
} catch {
DateError::InvalidDate(message~) =>
raise DateError::InvalidDate(message=message + ": " + text)
DateError::InvalidFormat(message~) =>
raise DateError::InvalidFormat(message=message + ": " + text)
}
}
///|
pub fn DateStringParser::parse(
self : DateStringParser,
text : String,
default_date? : DateTime,
) -> DateTime raise DateError {
self.diagnostics = []
self.parse_record(text, default_date).value
}
///|
pub fn DateStringParser::parse_many(
self : DateStringParser,
texts : Array[String],
default_date? : DateTime,
) -> Array[DateTime] raise DateError {
self.diagnostics = []
texts.map(text => self.parse_record(text, default_date).value)
}
///|
fn parser_check_utc(value : DateTime) -> Unit raise DateError {
let instant = rruleset_instant(value)
if instant < -62135596800000000L || instant >= 253402300800000000L {
raise DateError::InvalidDate(
message="parsed aware UTC date is outside years 1..9999",
)
}
}
///|
fn parser_encode(value : DateTime) -> Double raise DateError {
parser_check_utc(value)
ignore(selected_epoch())
value.to_ordinal()
}
///|
pub fn DateStringParser::to_number(
self : DateStringParser,
text : String,
default_date? : DateTime,
) -> Double raise DateError {
self.diagnostics = []
parser_encode(self.parse_record(text, default_date).value)
}
///|
/// Preserve datestr2num's explicit-default scalar loop and no-default NumPy
/// vectorization probe, conversion order, and first-aware mixed-array behavior.
pub fn DateStringParser::to_numbers(
self : DateStringParser,
texts : Array[String],
default_date? : DateTime,
) -> Array[Double] raise DateError {
self.diagnostics = []
if texts.is_empty() {
return []
}
if default_date is Some(_) {
return texts.map(text => {
parser_encode(self.parse_record(text, default_date).value)
})
}
ignore(self.parse_record(texts[0], None))
let records = texts.map(text => self.parse_record(text, None))
let values = if records[0].aware {
records.map(record => {
let value = if record.aware {
record.value
} else {
self.context.interpret_naive(record.value, record.fold)
}
parser_check_utc(value)
value
})
} else {
records.map(record => record.value)
}
ignore(selected_epoch())
values.map(value => value.to_ordinal())
}
///|
let default_string_parser : Ref[DateStringParser?] = Ref(None)
///|
/// Diagnostics from the last default free-helper operation. Reading them does
/// not construct a parser or access the clock/epoch. Explicit parser instances
/// expose their independent warnings() arrays.
pub fn date_parse_warnings() -> Array[DateParseWarning] {
match default_string_parser.val {
Some(parser) => parser.warnings()
None => []
}
}
///|
fn selected_string_parser() -> DateStringParser raise DateError {
match default_string_parser.val {
Some(parser) => parser
None => {
let parser = DateStringParser::new()
default_string_parser.val = Some(parser)
parser
}
}
}
///|
/// Convert a general date string through the fixed default dateutil grammar.
pub fn datestr2num(
text : String,
default_date? : DateTime,
parser? : DateStringParser,
) -> Double raise DateError {
parser.unwrap_or_else(selected_string_parser).to_number(text, default_date?)
}
///|
/// Flat-array counterpart of datestr2num. Empty input does not select the host
/// parser or read/lock the numeric epoch.
pub fn datestrs2num(
texts : Array[String],
default_date? : DateTime,
parser? : DateStringParser,
) -> Array[Double] raise DateError {
if texts.is_empty() {
return []
}
parser.unwrap_or_else(selected_string_parser).to_numbers(texts, default_date?)
}