///|
priv enum DateConverterMode {
AutoConverter
ConciseConverter(Array[String]?, Array[String]?, Array[String]?, Bool)
}
///|
/// Typed date conversion and default axis configuration. DateTime values carry
/// explicit UTC offsets; empty arrays have no inferred unit.
pub struct DateConverter {
priv mode : DateConverterMode
priv multiples : Bool
}
///|
/// Configure automatic date labels with the original default interval tables.
///
/// ```mbt check
/// test {
/// let converter = @date.DateConverter::new()
/// let info = converter.axisinfo()
/// let values = converter.convert([
/// @date.datetime(2024, 1, 1),
/// @date.datetime(2024, 1, 2, hour=16),
/// ])
/// let ticks = info.major_locator().tick_values(values[0], values[1])
/// assert_true(!info.major_formatter().format_values(ticks).is_empty())
/// }
/// ```
pub fn DateConverter::new(interval_multiples~ : Bool = true) -> DateConverter {
{ mode: AutoConverter, multiples: interval_multiples, }
}
///|
pub fn DateConverter::copy(self : DateConverter) -> DateConverter {
let mode = match self.mode {
AutoConverter => AutoConverter
ConciseConverter(formats, zeros, offsets, show) =>
ConciseConverter(
formats.map(values => values.copy()),
zeros.map(values => values.copy()),
offsets.map(values => values.copy()),
show,
)
}
{ mode, multiples: self.multiples, }
}
///|
pub fn DateConverter::interval_multiples(self : DateConverter) -> Bool {
self.multiples
}
///|
/// Convert flat, typed datetime values through the shared epoch implementation.
/// The inferred label timezone never changes numeric instants.
pub fn DateConverter::convert(
_self : DateConverter,
values : Array[DateTime],
) -> Array[Double] {
values.map(value => value.to_ordinal())
}
///|
pub fn DateConverter::convert_value(
_self : DateConverter,
value : DateTime,
) -> Double {
value.to_ordinal()
}
///|
/// Retain a rule zone, or construct the value's exact fixed-second offset unit.
/// This does not read or lock the numeric epoch.
pub fn DateConverter::default_unit(
_self : DateConverter,
value : DateTime,
) -> DateTimeZone {
match value.timezone {
Some(zone) => zone
None => try! fixed_timezone_seconds(value.offset_seconds)
}
}
///|
/// Infer the first date's unit. There are no missing or heterogeneous entries
/// in Array[DateTime]; an empty array has no unit.
pub fn DateConverter::default_units(
self : DateConverter,
values : Array[DateTime],
) -> DateTimeZone? {
if values.is_empty() {
None
} else {
Some(self.default_unit(values[0]))
}
}
///|
/// Return fresh default locators and formatter, without converting or locking
/// the epoch. Limits remain Gregorian date values until the axis needs them.
pub fn DateConverter::axisinfo(
self : DateConverter,
unit? : DateTimeZone,
) -> DateAxisInfo raise DateError {
let automatic = AutoDateLocator::new(
interval_multiples=self.multiples,
timezone?=unit,
)
let automatic = match unit {
Some(zone) => automatic.with_timezone(zone)
None => automatic
}
let locator = automatic.as_locator()
let formatter = match self.mode {
AutoConverter =>
AutoDateFormatter::new(locator~, timezone?=unit).as_formatter()
ConciseConverter(formats, zero_formats, offset_formats, show_offset) =>
concise_date_formatter(
timezone?=unit,
formats?,
zero_formats?,
offset_formats?,
show_offset~,
).as_formatter()
}
let formatter = match unit {
Some(zone) => formatter.with_timezone(zone)
None => formatter
}
{
locator,
formatter,
label: "",
default_limits: (datetime(1970, 1, 1), datetime(1970, 1, 2)),
}
}
///|
/// Dispatch using the current date.converter and date.interval_multiples rc.
/// Already constructed converters and enabled axes retain their snapshots.
pub fn default_date_converter() -> DateConverter {
let multiples = @config.date_interval_multiples()
if @config.date_converter() == "concise" {
ConciseDateConverter::new(interval_multiples=multiples).as_converter()
} else {
DateConverter::new(interval_multiples=multiples)
}
}