use chrono::{
DateTime, Datelike, LocalResult, NaiveDate, NaiveDateTime, Offset, TimeZone, Timelike, Utc,
};
use chrono_tz::Tz;
use thiserror::Error;
use crate::expression::TemplateValue;
use crate::util::Locale;
use super::{TemporalKind, TemporalValue};
pub struct TemporalObjects;
pub(crate) fn java_short_zone(target: &TemporalValue, default_zone: &Tz) -> String {
match target {
TemporalValue::OffsetDateTime(value) => fixed_offset_zone(value.offset()),
TemporalValue::OffsetTime(_, offset) => fixed_offset_zone(offset),
TemporalValue::ZonedDateTime(value) => tz_zone(value.timezone()),
_ => tz_zone(*default_zone),
}
}
pub(crate) fn fixed_offset_zone(offset: &chrono::FixedOffset) -> String {
if offset.local_minus_utc() == 0 {
"Z".to_owned()
} else {
chrono::Utc::now()
.with_timezone(offset)
.format("%:z")
.to_string()
}
}
pub(crate) fn tz_zone(zone: Tz) -> String {
if zone == Tz::UTC {
"Z".to_owned()
} else {
chrono::Utc::now()
.with_timezone(&zone)
.format("%Z")
.to_string()
}
}
impl TemporalObjects {
pub fn temporal(
value: Option<&TemplateValue>,
) -> Result<Option<&TemporalValue>, TemporalError> {
match value {
None | Some(TemplateValue::Null) => Ok(None),
Some(TemplateValue::Object(object)) => object
.as_any()
.downcast_ref::<TemporalValue>()
.map(Some)
.ok_or_else(|| {
invalid(format!(
"Cannot normalize class \"{}\" as a date",
object.class_name()
))
}),
Some(value) => Err(invalid(format!(
"Cannot normalize class \"{}\" as a date",
value.class_name()
))),
}
}
pub fn formatter_for(target: &TemporalValue, locale: &Locale) -> Result<String, TemporalError> {
let language = locale.get_language().to_string_lossy();
Ok(match target.kind() {
TemporalKind::Instant => "%Y-%m-%dT%H:%M:%S%.fZ".to_owned(),
TemporalKind::LocalDate => localized_date_pattern(&language),
TemporalKind::LocalDateTime => localized_datetime_pattern(&language),
TemporalKind::ZonedDateTime => {
let zone = match target {
TemporalValue::ZonedDateTime(value) => tz_zone(value.timezone()),
_ => unreachable!("ZonedDateTime kind implies ZonedDateTime value"),
};
if language == "zh" {
format!("%Y年%m月%d日 {zone} %H:%M:%S")
} else if language == "de" {
format!("%-d. %B %Y, %H:%M:%S {zone}")
} else {
format!("%B %-d, %Y, %-I:%M:%S %p {zone}")
}
}
TemporalKind::LocalTime => {
if language == "zh" || language == "de" {
"%H:%M:%S".to_owned()
} else {
"%-I:%M:%S %p".to_owned()
}
}
TemporalKind::OffsetDateTime => localized_datetime_pattern(&language),
TemporalKind::OffsetTime => "%H:%M:%S".to_owned(),
TemporalKind::Year => "%Y".to_owned(),
TemporalKind::YearMonth => {
if Self::should_display_year_before_month(locale) {
"%Y %B".to_owned()
} else {
"%B %Y".to_owned()
}
}
})
}
pub fn zoned_time(
target: &TemporalValue,
default_zone_id: Tz,
) -> Result<DateTime<Tz>, TemporalError> {
let today = Utc::now().with_timezone(&default_zone_id).date_naive();
let local = match target {
TemporalValue::Instant(value) => return Ok(value.with_timezone(&default_zone_id)),
TemporalValue::LocalDate(value) => value.and_hms_opt(0, 0, 0).expect("midnight"),
TemporalValue::LocalDateTime(value) => *value,
TemporalValue::LocalTime(value) => NaiveDateTime::new(today, *value),
TemporalValue::OffsetDateTime(value) => {
return Ok(value.with_timezone(&Utc).with_timezone(&default_zone_id));
}
TemporalValue::OffsetTime(value, offset) => {
let fixed = today
.and_time(*value)
.and_local_timezone(*offset)
.single()
.ok_or_else(|| invalid("Cannot resolve OffsetTime"))?;
return Ok(fixed.with_timezone(&Utc).with_timezone(&default_zone_id));
}
TemporalValue::Year(year) => NaiveDate::from_ymd_opt(*year, 1, 1)
.ok_or_else(|| invalid("Invalid Year"))?
.and_hms_opt(0, 0, 0)
.expect("midnight"),
TemporalValue::YearMonth(year, month) => NaiveDate::from_ymd_opt(*year, *month, 1)
.ok_or_else(|| invalid("Invalid YearMonth"))?
.and_hms_opt(0, 0, 0)
.expect("midnight"),
TemporalValue::ZonedDateTime(value) => return Ok(*value),
};
match default_zone_id.from_local_datetime(&local) {
LocalResult::Single(value) | LocalResult::Ambiguous(value, _) => Ok(value),
LocalResult::None => Err(invalid("Local temporal is in a ZoneId gap")),
}
}
pub fn date_fields(target: &TemporalValue) -> Result<(i32, u32, u32, u32), TemporalError> {
let date = match target {
TemporalValue::LocalDate(value) => *value,
TemporalValue::LocalDateTime(value) => value.date(),
TemporalValue::OffsetDateTime(value) => value.date_naive(),
TemporalValue::ZonedDateTime(value) => value.date_naive(),
TemporalValue::Year(year) => NaiveDate::from_ymd_opt(*year, 1, 1).expect("valid year"),
TemporalValue::YearMonth(year, month) => {
NaiveDate::from_ymd_opt(*year, *month, 1).expect("valid year-month")
}
_ => return Err(invalid("Unsupported field: DayOfMonth")),
};
Ok((
date.year(),
date.month(),
date.day(),
date.weekday().number_from_monday(),
))
}
pub fn time_fields(target: &TemporalValue) -> Result<(u32, u32, u32, u32), TemporalError> {
let time = match target {
TemporalValue::LocalTime(value) | TemporalValue::OffsetTime(value, _) => *value,
TemporalValue::LocalDateTime(value) => value.time(),
TemporalValue::OffsetDateTime(value) => value.time(),
TemporalValue::ZonedDateTime(value) => value.time(),
_ => return Err(invalid("Unsupported field: HourOfDay")),
};
Ok((time.hour(), time.minute(), time.second(), time.nanosecond()))
}
#[must_use]
pub fn offset_seconds(target: &TemporalValue, default_zone_id: Tz) -> i32 {
match target {
TemporalValue::OffsetDateTime(value) => value.offset().local_minus_utc(),
TemporalValue::OffsetTime(_, offset) => offset.local_minus_utc(),
TemporalValue::ZonedDateTime(value) => value.offset().fix().local_minus_utc(),
_ => Utc::now()
.with_timezone(&default_zone_id)
.offset()
.fix()
.local_minus_utc(),
}
}
fn should_display_year_before_month(locale: &Locale) -> bool {
matches!(
locale.get_country().to_string_lossy().as_str(),
"BT" | "CA" | "CN" | "KP" | "KR" | "TW" | "HU" | "IR" | "JP" | "LT" | "MN"
)
}
}
fn localized_date_pattern(language: &str) -> String {
if language == "zh" {
"%Y年%m月%d日".to_owned()
} else if language == "de" {
"%-d. %B %Y".to_owned()
} else {
"%B %-d, %Y".to_owned()
}
}
fn localized_datetime_pattern(language: &str) -> String {
if language == "zh" {
"%Y年%m月%d日 %H:%M:%S".to_owned()
} else if language == "de" {
"%-d. %B %Y, %H:%M:%S".to_owned()
} else {
"%B %-d, %Y, %-I:%M:%S %p".to_owned()
}
}
#[derive(Debug, Error)]
#[error("{message}")]
pub struct TemporalError {
message: String,
}
fn invalid(message: impl Into<String>) -> TemporalError {
TemporalError {
message: message.into(),
}
}