use chrono::{LocalResult, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Timelike, Utc};
use chrono_tz::Tz;
use thiserror::Error;
use super::TemporalValue;
pub struct TemporalCreationUtils;
impl TemporalCreationUtils {
#[must_use]
pub const fn new() -> Self {
Self
}
pub fn create(&self, fields: &[i32]) -> Result<TemporalValue, TemporalCreationError> {
if !matches!(fields.len(), 3 | 5 | 6 | 7) {
return Err(invalid("Temporal create requires 3, 5, 6 or 7 fields"));
}
let date = NaiveDate::from_ymd_opt(fields[0], to_u32(fields[1])?, to_u32(fields[2])?)
.ok_or_else(|| invalid("Invalid value for Year/MonthOfYear/DayOfMonth"))?;
if fields.len() == 3 {
return Ok(TemporalValue::LocalDate(date));
}
let time = NaiveTime::from_hms_nano_opt(
to_u32(fields[3])?,
to_u32(fields[4])?,
fields.get(5).copied().map_or(Ok(0), to_u32)?,
fields.get(6).copied().map_or(Ok(0), to_u32)?,
)
.ok_or_else(|| invalid("Invalid value for Hour/Minute/Second/NanoOfSecond"))?;
Ok(TemporalValue::LocalDateTime(NaiveDateTime::new(date, time)))
}
pub fn create_date(
&self,
text: &str,
pattern: Option<&str>,
) -> Result<TemporalValue, TemporalCreationError> {
let pattern = pattern.map_or("%Y-%m-%d".to_owned(), process_pattern);
NaiveDate::parse_from_str(text, &pattern)
.map(TemporalValue::LocalDate)
.map_err(|error| invalid(error.to_string()))
}
pub fn create_date_time(
&self,
text: &str,
pattern: Option<&str>,
) -> Result<TemporalValue, TemporalCreationError> {
let pattern = pattern.map_or("%Y-%m-%dT%H:%M:%S".to_owned(), process_pattern);
NaiveDateTime::parse_from_str(text, &pattern)
.map(TemporalValue::LocalDateTime)
.map_err(|error| invalid(error.to_string()))
}
#[must_use]
pub fn create_now(&self) -> TemporalValue {
TemporalValue::LocalDateTime(Utc::now().with_timezone(&default_zone()).naive_local())
}
pub fn create_now_for_time_zone(
&self,
zone_id: &str,
) -> Result<TemporalValue, TemporalCreationError> {
let zone = parse_zone(zone_id)?;
Ok(TemporalValue::ZonedDateTime(
Utc::now().with_timezone(&zone),
))
}
#[must_use]
pub fn create_today(&self) -> TemporalValue {
TemporalValue::LocalDate(Utc::now().with_timezone(&default_zone()).date_naive())
}
pub fn create_today_for_time_zone(
&self,
zone_id: &str,
) -> Result<TemporalValue, TemporalCreationError> {
let zone = parse_zone(zone_id)?;
let now = Utc::now().with_timezone(&zone);
let midnight = now
.date_naive()
.and_hms_nano_opt(0, 0, 0, 0)
.expect("midnight is valid");
let value = match zone.from_local_datetime(&midnight) {
LocalResult::Single(value) | LocalResult::Ambiguous(value, _) => value,
LocalResult::None => zone
.from_local_datetime(&(midnight + chrono::Duration::hours(1)))
.earliest()
.ok_or_else(|| invalid("Cannot resolve local midnight in ZoneId"))?,
};
Ok(TemporalValue::ZonedDateTime(
value
.with_hour(0)
.and_then(|value| value.with_minute(0))
.and_then(|value| value.with_second(0))
.and_then(|value| value.with_nanosecond(0))
.unwrap_or(value),
))
}
}
impl Default for TemporalCreationUtils {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Error)]
#[error("{message}")]
pub struct TemporalCreationError {
message: String,
}
fn invalid(message: impl Into<String>) -> TemporalCreationError {
TemporalCreationError {
message: message.into(),
}
}
fn to_u32(value: i32) -> Result<u32, TemporalCreationError> {
u32::try_from(value).map_err(|_| invalid(format!("Invalid negative temporal field: {value}")))
}
fn parse_zone(value: &str) -> Result<Tz, TemporalCreationError> {
value
.parse()
.map_err(|_| invalid(format!("Unknown time-zone ID: {value}")))
}
fn default_zone() -> Tz {
std::env::var("TZ")
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(chrono_tz::UTC)
}
pub(crate) fn process_pattern(pattern: &str) -> String {
let mut output = String::new();
let mut chars = pattern.chars().peekable();
let mut quoted = false;
while let Some(ch) = chars.next() {
if ch == '\'' {
if chars.peek() == Some(&'\'') {
chars.next();
output.push('\'');
} else {
quoted = !quoted;
}
continue;
}
if quoted {
output.push(ch);
continue;
}
let mut count = 1;
while chars.peek() == Some(&ch) {
chars.next();
count += 1;
}
let directive = match ch {
'y' | 'u' if count == 2 => "%y",
'y' | 'u' => "%Y",
'M' if count >= 4 => "%B",
'M' if count == 3 => "%b",
'M' => "%m",
'd' if count == 2 => "%d",
'd' => "%-d",
'E' if count >= 4 => "%A",
'E' => "%a",
'H' => "%H",
'h' => "%I",
'm' => "%M",
's' => "%S",
'S' => {
output.push_str("__THYMELEAF_FRACTION_");
output.push_str(&count.to_string());
output.push_str("__");
continue;
}
'n' => "%f",
'a' => "%p",
'X' | 'x' | 'Z' => "%:z",
'z' => "%Z",
_ => {
for _ in 0..count {
output.push(ch);
}
continue;
}
};
output.push_str(directive);
}
output
}