///|
/// A date formatter that converts numeric UTC day values to strings in a fixed
/// offset or a rule zone. `%z` emits `+HHMM[SS]`; `%Z` uses the TZif
/// designation when available, otherwise `UTC` or `UTC+HH:MM[:SS]`.
pub struct DateFormatter {
format : String
offset_minutes : Int32
timezone : DateTimeZone?
priv timezone_source : FormatterTimezone
priv mode : DateFormatMode
priv usetex : Bool
}
///|
/// Construct a DateFormatter, capturing tz or the current timezone rc. Invalid
/// names raise here. Supply either a string tz or a typed timezone, not both.
///
/// ```mbt check
/// test {
/// let formatter = @date.date_formatter("%H:%M %Z", tz="XST-5:45")
/// assert_eq(formatter.format_value(0.0), "05:45 XST")
/// }
/// ```
pub fn date_formatter(
format : String,
tz? : String,
timezone? : DateTimeZone,
) -> DateFormatter raise DateError {
let zone = selected_timezone(tz, timezone)
{
format,
offset_minutes: zone.initial_offset_minutes(),
timezone: Some(zone),
timezone_source: Captured,
mode: Fixed,
usetex: false,
}
}
///|
/// Return a copy that formats instants in a fixed offset east of UTC.
/// Reject offsets outside `-1439..1439` minutes.
///
/// ```mbt check
/// test {
/// let formatter = @date.date_formatter("%H:%M %z %Z").with_offset(345)
/// assert_eq(formatter.offset_minutes(), 345)
/// assert_eq(formatter.format_value(0.0), "05:45 +0545 UTC+05:45")
/// }
/// ```
pub fn DateFormatter::with_offset(
self : DateFormatter,
offset_minutes : Int32,
) -> DateFormatter raise DateError {
validate_offset(offset_minutes)
{ ..self.copy(), offset_minutes, timezone: None, timezone_source: Captured, }
}
///|
/// Return a copy that formats UTC instants using a transition/TZif zone.
/// Label conversion retains historical seconds and future footer rules.
pub fn DateFormatter::with_timezone(
self : DateFormatter,
timezone : DateTimeZone,
) -> DateFormatter {
{
..self.copy(),
offset_minutes: timezone.initial_offset_minutes(),
timezone: Some(timezone),
timezone_source: Captured,
}
}
///|
pub fn DateFormatter::format(self : DateFormatter) -> String {
self.format
}
///|
/// Return whole offset minutes (truncated toward zero for TZif history).
/// When a zone is configured, use `timezone` for exact instant-dependent offsets.
pub fn DateFormatter::offset_minutes(self : DateFormatter) -> Int32 {
self.offset_minutes
}
///|
/// Return the initially configured zone, if any. Auto/Concise with omitted or
/// named tz re-resolve it while formatting; use with_timezone for a fixed binding.
pub fn DateFormatter::timezone(self : DateFormatter) -> DateTimeZone? {
self.timezone
}
///|
pub fn DateFormatter::format_value(
self : DateFormatter,
value : Double,
) -> String {
try {
if self.mode is Auto(_) {
return self.auto_value(value, None)
}
let text = format_date(self.converted_date(value), self.format)
if self.usetex {
wrap_date_in_tex(text)
} else {
text
}
} catch {
_ => Double::to_string(value)
}
}
///|
fn choose_format(span : Double) -> String {
if span < 1.0 / 86400.0 {
"%H:%M:%S.%f"
} else if span < 1.0 / 24.0 {
"%H:%M:%S"
} else if span < 1.0 {
"%H:%M"
} else if span < 60.0 {
"%Y-%m-%d"
} else if span < 365.0 * 2.0 {
"%Y-%m"
} else {
"%Y"
}
}
///|
/// Legacy UTC helper choosing a readable format for a numeric date range.
pub fn auto_date_formatter(lower : Double, upper : Double) -> DateFormatter {
{
format: choose_format(Double::abs(upper - lower)),
offset_minutes: 0,
timezone: None,
timezone_source: Captured,
mode: Fixed,
usetex: false,
}
}
///|
/// Legacy UTC/fixed-offset helper formatting an explicitly supplied tick list.
/// An invalid offset returns an empty list, as with `date_tick_values`.
pub fn format_date_ticks(
values : Array[Double],
format~ : String = "",
offset_minutes~ : Int32 = 0,
timezone? : DateTimeZone,
) -> Array[String] {
let selected_offset = try {
validate_offset(offset_minutes)
offset_minutes
} catch {
_ => return []
}
let selected = if format == "" {
if values.length() < 2 {
"%Y-%m-%d"
} else {
choose_format(Double::abs(values[values.length() - 1] - values[0]))
}
} else {
format
}
let formatter = {
format: selected,
offset_minutes: selected_offset,
timezone,
timezone_source: Captured,
mode: Fixed,
usetex: false,
}
let result = Array(capacity=values.length())
for value in values {
result.push(formatter.format_value(value))
}
result
}