use crate::cldr::{calendar_spec, CalendarSpec};
use alloc::string::{String, ToString};
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DateTime {
pub year: i32,
pub month: u8,
pub day: u8,
pub hour: u8,
pub minute: u8,
pub second: u8,
}
impl DateTime {
#[must_use]
pub fn to_iso8601(&self) -> String {
alloc::format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
self.year,
self.month,
self.day,
self.hour,
self.minute,
self.second
)
}
#[must_use]
pub fn weekday(&self) -> u8 {
crate::calendar::day_of_week(self.year as i64, self.month as i64, self.day as i64)
}
#[must_use]
pub fn add_seconds(&self, delta: i64) -> DateTime {
let jdn =
crate::calendar::gregorian_to_jdn(self.year as i64, self.month as i64, self.day as i64);
let total = jdn
.saturating_mul(86_400)
.saturating_add(self.hour as i64 * 3600)
.saturating_add(self.minute as i64 * 60)
.saturating_add(self.second as i64)
.saturating_add(delta);
let (new_jdn, sod) = (total.div_euclid(86_400), total.rem_euclid(86_400));
let (y, m, d) = crate::calendar::jdn_to_gregorian(new_jdn);
DateTime {
year: y as i32,
month: m as u8,
day: d as u8,
hour: (sod / 3600) as u8,
minute: (sod % 3600 / 60) as u8,
second: (sod % 60) as u8,
}
}
#[must_use]
pub fn add_days(&self, delta: i64) -> DateTime {
self.add_seconds(delta * 86_400)
}
#[must_use]
pub fn parse_iso8601(s: &str) -> Option<DateTime> {
let s = s.trim().trim_end_matches('Z');
let (date, time) = match s.split_once(['T', ' ']) {
Some((d, t)) => (d, Some(t)),
None => (s, None),
};
let mut dp = date.split('-');
let year: i32 = dp.next()?.parse().ok()?;
let month: u8 = dp.next()?.parse().ok()?;
let day: u8 = dp.next()?.parse().ok()?;
if dp.next().is_some() || !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return None;
}
let (hour, minute, second) = match time {
None => (0, 0, 0),
Some(t) => {
let t = t.split(['+', '-']).next().unwrap_or(t);
let mut tp = t.split(':');
let h: u8 = tp.next()?.parse().ok()?;
let mi: u8 = tp.next()?.parse().ok()?;
let se: u8 = match tp.next() {
Some(x) => x.parse().ok()?,
None => 0,
};
(h, mi, se)
}
};
Some(DateTime {
year,
month,
day,
hour,
minute,
second,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DateStyle {
Full,
Long,
Medium,
Short,
}
impl DateStyle {
fn idx(self) -> usize {
match self {
DateStyle::Full => 0,
DateStyle::Long => 1,
DateStyle::Medium => 2,
DateStyle::Short => 3,
}
}
}
fn weekday(dt: &DateTime) -> usize {
let t = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
let m = (dt.month as i64).clamp(1, 12);
let d = dt.day as i64;
let y = if m < 3 {
dt.year as i64 - 1
} else {
dt.year as i64
};
(y + y / 4 - y / 100 + y / 400 + t[(m - 1) as usize] + d).rem_euclid(7) as usize
}
fn two(n: i64) -> String {
alloc::format!("{n:02}")
}
fn field(field: char, n: usize, dt: &DateTime, s: &CalendarSpec) -> String {
let m = dt.month as usize;
let mi = m.clamp(1, 12) - 1;
match field {
'y' | 'Y' => {
if n == 2 {
two((dt.year.rem_euclid(100)) as i64)
} else {
dt.year.to_string()
}
}
'M' | 'L' => match n {
1 => m.to_string(),
2 => two(m as i64),
3 => s.months_abbr[mi].to_string(),
_ => s.months_wide[mi].to_string(),
},
'd' => {
if n >= 2 {
two(dt.day as i64)
} else {
dt.day.to_string()
}
}
'E' | 'e' | 'c' => {
let w = weekday(dt);
if n >= 4 {
s.days_wide[w].to_string()
} else {
s.days_abbr[w].to_string()
}
}
'h' => {
let h = ((dt.hour as u16 + 11) % 12) + 1; if n >= 2 {
two(h as i64)
} else {
h.to_string()
}
}
'H' => {
if n >= 2 {
two(dt.hour as i64)
} else {
dt.hour.to_string()
}
}
'm' => {
if n >= 2 {
two(dt.minute as i64)
} else {
dt.minute.to_string()
}
}
's' => {
if n >= 2 {
two(dt.second as i64)
} else {
dt.second.to_string()
}
}
'a' | 'b' => if dt.hour < 12 { s.am } else { s.pm }.to_string(),
_ => String::new(), }
}
fn render(pattern: &str, dt: &DateTime, s: &CalendarSpec) -> String {
let c: Vec<char> = pattern.chars().collect();
let mut out = String::new();
let mut i = 0;
while i < c.len() {
let ch = c[i];
if ch == '\'' {
i += 1;
if i < c.len() && c[i] == '\'' {
out.push('\'');
i += 1;
continue;
}
while i < c.len() && c[i] != '\'' {
out.push(c[i]);
i += 1;
}
i += 1; } else if ch.is_ascii_alphabetic() {
let start = i;
while i < c.len() && c[i] == ch {
i += 1;
}
out.push_str(&field(ch, i - start, dt, s));
} else {
out.push(ch);
i += 1;
}
}
out
}
fn spec(lang: &str) -> CalendarSpec {
let norm: String = lang
.chars()
.map(|c| {
if c == '_' {
'-'
} else {
c.to_ascii_lowercase()
}
})
.collect();
let mut end = norm.len();
loop {
if let Some(s) = calendar_spec(&norm[..end]) {
return s;
}
match norm[..end].rfind('-') {
Some(i) => end = i,
None => return calendar_spec("en").expect("root calendar present"),
}
}
}
#[must_use]
pub fn format_date(lang: &str, dt: &DateTime, style: DateStyle) -> String {
let s = spec(lang);
render(s.date[style.idx()], dt, &s)
}
#[must_use]
pub fn format_time(lang: &str, dt: &DateTime, style: DateStyle) -> String {
let s = spec(lang);
render(s.time[style.idx()], dt, &s)
}
#[must_use]
pub fn format_skeleton(lang: &str, dt: &DateTime, skeleton: &str) -> String {
let s = spec(lang);
let norm: String = lang
.chars()
.map(|c| {
if c == '_' {
'-'
} else {
c.to_ascii_lowercase()
}
})
.collect();
let mut end = norm.len();
let pattern = loop {
if let Some(p) = crate::cldr::skeleton_pattern(&norm[..end], skeleton) {
break p;
}
match norm[..end].rfind('-') {
Some(i) => end = i,
None => break crate::cldr::skeleton_pattern("en", skeleton).unwrap_or(s.date[2]),
}
};
render(pattern, dt, &s)
}
fn render_alt(
cal: &crate::cldr::AltCalSpec,
style: DateStyle,
year: i64,
month: i64,
day: i64,
jdn: i64,
greg: &CalendarSpec,
) -> String {
let wd = ((jdn.rem_euclid(7) + 1) % 7) as usize; let c: Vec<char> = cal.date[style.idx()].chars().collect();
let mut out = String::new();
let mut i = 0;
while i < c.len() {
let ch = c[i];
if ch == '\'' {
i += 1;
if i < c.len() && c[i] == '\'' {
out.push('\'');
i += 1;
continue;
}
while i < c.len() && c[i] != '\'' {
out.push(c[i]);
i += 1;
}
i += 1;
} else if ch.is_ascii_alphabetic() {
let start = i;
while i < c.len() && c[i] == ch {
i += 1;
}
let (n, m) = (i - start, month as usize);
let mi = m.clamp(1, 12) - 1; match ch {
'y' | 'Y' => out.push_str(&year.to_string()),
'M' | 'L' => match n {
1 => out.push_str(&m.to_string()),
2 => out.push_str(&two(m as i64)),
3 => out.push_str(cal.months_abbr[mi]),
_ => out.push_str(cal.months_wide[mi]),
},
'd' => out.push_str(&day.to_string()),
'E' | 'e' | 'c' => out.push_str(if n >= 4 {
greg.days_wide[wd]
} else {
greg.days_abbr[wd]
}),
'G' => out.push_str(cal.era),
_ => {}
}
} else {
out.push(ch);
i += 1;
}
}
out
}
fn alt_spec(lang: &str, f: fn(&str) -> Option<crate::cldr::AltCalSpec>) -> crate::cldr::AltCalSpec {
let norm: String = lang
.chars()
.map(|c| {
if c == '_' {
'-'
} else {
c.to_ascii_lowercase()
}
})
.collect();
let mut end = norm.len();
loop {
if let Some(s) = f(&norm[..end]) {
return s;
}
match norm[..end].rfind('-') {
Some(i) => end = i,
None => return f("en").expect("root calendar present"),
}
}
}
#[must_use]
pub fn format_islamic_date(
lang: &str,
year: i64,
month: i64,
day: i64,
style: DateStyle,
) -> String {
let cal = alt_spec(lang, crate::cldr::islamic_spec);
let jdn = crate::calendar::islamic_to_jdn(year, month, day);
render_alt(&cal, style, year, month, day, jdn, &spec(lang))
}
#[must_use]
pub fn format_persian_date(
lang: &str,
year: i64,
month: i64,
day: i64,
style: DateStyle,
) -> String {
let cal = alt_spec(lang, crate::cldr::persian_spec);
let jdn = crate::calendar::persian_to_jdn(year, month, day);
render_alt(&cal, style, year, month, day, jdn, &spec(lang))
}
#[must_use]
pub fn format_gmt_offset(lang: &str, offset_minutes: i32) -> String {
let norm: String = lang
.chars()
.map(|c| {
if c == '_' {
'-'
} else {
c.to_ascii_lowercase()
}
})
.collect();
let mut end = norm.len();
let tz = loop {
if let Some(t) = crate::cldr::tz_spec(&norm[..end]) {
break t;
}
match norm[..end].rfind('-') {
Some(i) => end = i,
None => break crate::cldr::tz_spec("en").expect("root tz present"),
}
};
if offset_minutes == 0 {
return String::from(tz.zero);
}
let (pos, neg) = tz.hour.split_once(';').unwrap_or((tz.hour, tz.hour));
let sub = if offset_minutes >= 0 { pos } else { neg };
let (h, m) = (
offset_minutes.unsigned_abs() / 60,
offset_minutes.unsigned_abs() % 60,
);
let body = sub
.replace("HH", &alloc::format!("{h:02}"))
.replace("mm", &alloc::format!("{m:02}"));
tz.gmt.replace("{0}", &body)
}
#[must_use]
pub fn format_datetime(
lang: &str,
dt: &DateTime,
date_style: DateStyle,
time_style: DateStyle,
) -> String {
let s = spec(lang);
let date = render(s.date[date_style.idx()], dt, &s);
let time = render(s.time[time_style.idx()], dt, &s);
s.datetime[date_style.idx()]
.replace("{1}", &date)
.replace("{0}", &time)
}