use crate::{
Utc2k,
Utc2kFormatError,
};
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub(super) enum Component {
Year(Style, Padding),
Month(Style, Padding),
Day(Style, Padding),
Hour(Style, Padding),
Minute(Padding),
Second(Padding),
Ordinal(Padding),
Period(Style),
Unixtime,
Literal(u8),
}
impl Component {
#[inline]
pub(super) fn format_date(date: Utc2k, fmt: &str)
-> Result<String, Utc2kFormatError> {
if ! fmt.is_ascii() { return Err(Utc2kFormatError::NotAscii); }
let mut out = String::with_capacity(64); let mut buf = U32DigitBuffer::DEFAULT;
let mut fmt = fmt.as_bytes();
while let Some((next, rest)) = Self::parse(fmt)? {
fmt = rest;
match next {
Self::Year(style, pad) =>
if matches!(style, Style::Main) {
out.push_str(date.y.as_str_full());
}
else { buf.write2(date.y as u32, pad, &mut out); },
Self::Month(style, pad) => match style {
Style::Main => { buf.write2(u32::from(date.m), pad, &mut out); },
Style::Alt1 => { out.push_str(date.m.as_str()); },
Style::Alt2 => { out.push_str(date.m.abbreviation()); },
},
Self::Day(style, pad) => match style {
Style::Main => { buf.write2(u32::from(date.d), pad, &mut out); },
Style::Alt1 => { out.push_str(date.weekday().as_str()); },
Style::Alt2 => { out.push_str(date.weekday().abbreviation()); },
},
Self::Hour(style, pad) => {
let hh = u32::from(
if matches!(style, Style::Main) { date.hh }
else { date.hour_12() }
);
buf.write2(hh, pad, &mut out);
},
Self::Minute(pad) => {
buf.write2(u32::from(date.mm), pad, &mut out);
},
Self::Second(pad) => {
buf.write2(u32::from(date.ss), pad, &mut out);
},
Self::Ordinal(pad) => {
buf.write3(u32::from(date.ordinal()), pad, &mut out);
},
Self::Period(style) => {
let p = date.hour_period();
match style {
Style::Main => { out.push_str(p.as_str(false)); },
Style::Alt1 => { out.push_str(p.as_str_ap()); },
Style::Alt2 => { out.push_str(p.as_str(true)); },
}
},
Self::Unixtime => { out.extend(buf.format(date.unixtime())); },
Self::Literal(v) => { out.push(v as char); },
}
}
Ok(out)
}
}
impl Component {
const fn parse(mut raw: &[u8]) -> Result<Option<(Self, &[u8])>, Utc2kFormatError> {
let mut style = Style::Main;
let mut padding = Padding::Zero;
macro_rules! parse_props {
( $fn:ident ) => (
loop {
match Modifier::$fn(raw) {
Ok((Some(Modifier::Padding(v)), rest)) => {
padding = v;
raw = rest;
},
Ok((Some(Modifier::Style(v)), rest)) => {
style = v;
raw = rest;
},
Ok((None, rest)) => {
raw = rest;
break;
},
Err(e) => return Err(e),
}
}
);
}
match raw {
[ b'[', b'[', rest @ .. ] => return Ok(Some((Self::Literal(b'['), rest))),
[ b'[', rest @ .. ] => {
raw = rest;
},
[ n, rest @ .. ] => return Ok(Some((Self::Literal(*n), rest))),
[] => return Ok(None),
}
#[expect(unused_assignments, reason = "Macro made me do it.")]
match raw.trim_ascii_start() {
[ b'y', b'e', b'a', b'r', rest @ .. ] => {
raw = rest;
parse_props!(parse_year);
Ok(Some((Self::Year(style, padding), raw)))
},
[ b'm', b'o', b'n', b't', b'h', rest @ .. ] => {
raw = rest;
parse_props!(parse_month);
Ok(Some((Self::Month(style, padding), raw)))
},
[ b'd', b'a', b'y', rest @ .. ] => {
raw = rest;
parse_props!(parse_day);
Ok(Some((Self::Day(style, padding), raw)))
},
[ b'h', b'o', b'u', b'r', rest @ .. ] => {
raw = rest;
parse_props!(parse_hour);
Ok(Some((Self::Hour(style, padding), raw)))
},
[ b'm', b'i', b'n', b'u', b't', b'e', rest @ .. ] => {
raw = rest;
parse_props!(parse_minute);
Ok(Some((Self::Minute(padding), raw)))
},
[ b's', b'e', b'c', b'o', b'n', b'd', rest @ .. ] => {
raw = rest;
parse_props!(parse_second);
Ok(Some((Self::Second(padding), raw)))
},
[ b'o', b'r', b'd', b'i', b'n', b'a', b'l', rest @ .. ] => {
raw = rest;
parse_props!(parse_ordinal);
Ok(Some((Self::Ordinal(padding), raw)))
},
[ b'p', b'e', b'r', b'i', b'o', b'd', rest @ .. ] => {
raw = rest;
parse_props!(parse_period);
Ok(Some((Self::Period(style), raw)))
},
[ b'u', b'n', b'i', b'x', b't', b'i', b'm', b'e', rest @ .. ] => match rest.trim_ascii_start() {
[ b']', rest @ .. ] => Ok(Some((Self::Unixtime, rest))),
[ b'@', .. ] => Err(Utc2kFormatError::InvalidModifier("The [unixtime] component has no modifiers.")),
_ => Err(Utc2kFormatError::Eof),
},
_ => Err(Utc2kFormatError::InvalidComponent),
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum Modifier {
Padding(Padding),
Style(Style),
}
macro_rules! parse_prop {
(
$fn:ident
$( [ $( $byte:literal ),+ ] $kind:ident $flag:ident, )+
$comp:literal $( $prop:literal )+,
$expected:ident $(,)?
) => (
/// # Parse Property.
///
/// Parse a single @modifier, returning the associated flag (if any),
const fn $fn(raw: &[u8])
-> Result<(Option<Self>, &[u8]), Utc2kFormatError> {
match raw.trim_ascii_start() {
$(
[ $( $byte, )+ rest @ .. ] => Ok((Some(Self::$kind($kind::$flag)), rest)),
)+
[ b']', rest @ .. ] => Ok((None, rest)),
[ b'@', .. ] => Err(Utc2kFormatError::InvalidModifier(concat!(
"Expected [", $comp, "… ",
$( concat!($prop, ", "), )+
" …].",
))),
_ => Err(Utc2kFormatError::Eof),
}
}
);
}
impl Modifier {
parse_prop! {
parse_year
[ b'@', b'2' ] Style Alt1,
[ b'@', b's', b'p', b'a', b'c', b'e' ] Padding Space,
[ b'@', b't', b'r', b'i', b'm' ] Padding Trim,
"year" "@2" "@space" "@trim",
Year,
}
parse_prop! {
parse_month
[ b'@', b'n', b'a', b'm', b'e' ] Style Alt1,
[ b'@', b'a', b'b', b'b', b'r' ] Style Alt2,
[ b'@', b's', b'p', b'a', b'c', b'e' ] Padding Space,
[ b'@', b't', b'r', b'i', b'm' ] Padding Trim,
"month" "@name" "@abbr" "@space" "@trim",
Month,
}
parse_prop! {
parse_day
[ b'@', b'n', b'a', b'm', b'e' ] Style Alt1,
[ b'@', b'a', b'b', b'b', b'r' ] Style Alt2,
[ b'@', b's', b'p', b'a', b'c', b'e' ] Padding Space,
[ b'@', b't', b'r', b'i', b'm' ] Padding Trim,
"day" "@name" "@abbr" "@space" "@trim",
Day,
}
parse_prop! {
parse_hour
[ b'@', b'1', b'2' ] Style Alt1,
[ b'@', b's', b'p', b'a', b'c', b'e' ] Padding Space,
[ b'@', b't', b'r', b'i', b'm' ] Padding Trim,
"hour" "@12" "@space" "@trim",
Hour,
}
parse_prop! {
parse_minute
[ b'@', b's', b'p', b'a', b'c', b'e' ] Padding Space,
[ b'@', b't', b'r', b'i', b'm' ] Padding Trim,
"minute" "@space" "@trim",
Minute,
}
parse_prop! {
parse_second
[ b'@', b's', b'p', b'a', b'c', b'e' ] Padding Space,
[ b'@', b't', b'r', b'i', b'm' ] Padding Trim,
"second" "@space" "@trim",
Second,
}
parse_prop! {
parse_ordinal
[ b'@', b's', b'p', b'a', b'c', b'e' ] Padding Space,
[ b'@', b't', b'r', b'i', b'm' ] Padding Trim,
"ordinal" "@space" "@trim",
Ordinal,
}
parse_prop! {
parse_period
[ b'@', b'a', b'p' ] Style Alt1,
[ b'@', b'u', b'p', b'p', b'e', b'r' ] Style Alt2,
"period" "@ap" "@upper",
Period,
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub(super) enum Padding {
Zero,
Space,
Trim,
}
impl Padding {
const fn as_char(self) -> Option<char> {
match self {
Self::Zero => Some('0'),
Self::Space => Some(' '),
Self::Trim => None,
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub(super) enum Style {
Main,
Alt1,
Alt2,
}
#[derive(Debug, Clone, Copy)]
struct U32DigitBuffer([char; 10]);
impl U32DigitBuffer {
const DEFAULT: Self = Self(['0'; 10]);
#[expect(clippy::cast_possible_truncation, reason = "False positive.")]
const fn format(&mut self, mut num: u32) -> &[char] {
let mut from = self.0.len();
while 9 < num && 0 < from {
from -= 1;
self.0[from] = ((num % 10) as u8 ^ b'0') as char;
num /= 10;
}
from -= 1;
self.0[from] = (num as u8 ^ b'0') as char;
let (_, b) = self.0.split_at(from);
b
}
#[inline]
fn write2(&mut self, num: u32, pad: Padding, out: &mut String) {
let num = self.format(num);
if num.len() == 1 {
match pad {
Padding::Zero => { out.push('0'); },
Padding::Space => { out.push(' '); },
Padding::Trim => {},
}
}
out.extend(num);
}
fn write3(&mut self, num: u32, pad: Padding, out: &mut String) {
let num = self.format(num);
let diff = 3_usize.saturating_sub(num.len());
if diff != 0 && let Some(pad) = pad.as_char() {
out.push(pad);
if diff == 2 { out.push(pad); } }
out.extend(num);
}
}