use chrono::format::{Fixed, Item, Numeric, Pad};
const fn num(numeric: Numeric) -> Item<'static> {
Item::Numeric(numeric, Pad::None)
}
const fn num0(numeric: Numeric) -> Item<'static> {
Item::Numeric(numeric, Pad::Zero)
}
const fn nums(numeric: Numeric) -> Item<'static> {
Item::Numeric(numeric, Pad::Space)
}
const fn fixed(fixed: Fixed) -> Item<'static> {
Item::Fixed(fixed)
}
#[derive(Clone, Debug)]
pub struct CustomStrftimeItems<'a> {
remainder: &'a str,
queue: &'static [Item<'static>],
julian_day: f64,
}
impl<'a> CustomStrftimeItems<'a> {
pub const fn new(s: &'a str, julian_day: f64) -> CustomStrftimeItems<'a> {
CustomStrftimeItems {
remainder: s,
queue: &[],
julian_day,
}
}
}
impl<'a> Iterator for CustomStrftimeItems<'a> {
type Item = Item<'a>;
fn next(&mut self) -> Option<Item<'a>> {
if let Some((item, remainder)) = self.queue.split_first() {
self.queue = remainder;
return Some(item.clone());
}
let (remainder, item) = self.parse_next_item(self.remainder)?;
self.remainder = remainder;
Some(item)
}
}
impl<'a> CustomStrftimeItems<'a> {
fn parse_next_item(&mut self, mut remainder: &'a str) -> Option<(&'a str, Item<'a>)> {
use Item::{Literal, Space};
use Numeric::*;
match remainder.chars().next() {
None => None,
Some('%') => {
remainder = &remainder[1..];
macro_rules! next {
() => {
match remainder.chars().next() {
Some(x) => {
remainder = &remainder[x.len_utf8()..];
x
}
None => return Some((remainder, Item::Error)), }
};
}
let spec = next!();
let pad_override = match spec {
'-' => Some(Pad::None),
'0' => Some(Pad::Zero),
'_' => Some(Pad::Space),
_ => None,
};
let spec = if pad_override.is_some() {
next!()
} else {
spec
};
macro_rules! queue {
[$head:expr, $($tail:expr),+ $(,)*] => ({
const QUEUE: &'static [Item<'static>] = &[$($tail),+];
self.queue = QUEUE;
$head
})
}
let item = match spec {
'd' => num0(Day),
'e' => nums(Day),
'f' => {
queue![num0(Second), fixed(Fixed::Nanosecond3)]
}
'F' => queue![
num0(Year),
Literal("-"),
num0(Month),
Literal("-"),
num0(Day)
],
'G' => num0(IsoYear),
'g' => num0(IsoYearMod100),
'H' => num0(Hour),
'I' => num0(Hour12),
'j' => num0(Ordinal),
'k' => nums(Hour),
'l' => nums(Hour12),
'm' => num0(Month),
'M' => num0(Minute),
'p' => fixed(Fixed::UpperAmPm),
'P' => fixed(Fixed::LowerAmPm),
'R' => queue![num0(Hour), Literal(":"), num0(Minute)],
's' => num(Timestamp),
'S' => num0(Second),
'T' => {
queue![
num0(Hour),
Literal(":"),
num0(Minute),
Literal(":"),
num0(Second)
]
}
'U' => num0(WeekFromSun),
'u' => num(WeekdayFromMon),
'V' => num0(IsoWeek),
'w' => num(NumDaysFromSun),
'W' => num0(WeekFromMon),
'Y' => num0(Year),
'%' => Literal("%"),
'J' => {
let rendered = format_julian_day(self.julian_day, pad_override);
return Some((remainder, Item::OwnedLiteral(rendered.into_boxed_str())));
}
_ => Item::Error, };
if let Some(new_pad) = pad_override {
match item {
Item::Numeric(ref kind, _pad) if self.queue.is_empty() => {
Some((remainder, Item::Numeric(kind.clone(), new_pad)))
}
_ => Some((remainder, Item::Error)),
}
} else {
Some((remainder, item))
}
}
Some(c) if c.is_whitespace() => {
let nextspec = remainder
.find(|c: char| !c.is_whitespace())
.unwrap_or(remainder.len());
assert!(nextspec > 0);
let item = Space(&remainder[..nextspec]);
remainder = &remainder[nextspec..];
Some((remainder, item))
}
_ => {
let nextspec = remainder
.find(|c: char| c.is_whitespace() || c == '%')
.unwrap_or(remainder.len());
assert!(nextspec > 0);
let item = Literal(&remainder[..nextspec]);
remainder = &remainder[nextspec..];
Some((remainder, item))
}
}
}
}
const JULIAN_DAY_MIN_INTEGER_WIDTH: usize = 9;
const JULIAN_DAY_FRACTIONAL_DIGITS: usize = 9;
fn format_julian_day(julian_day: f64, pad_override: Option<Pad>) -> String {
const FRAC: usize = JULIAN_DAY_FRACTIONAL_DIGITS;
match pad_override {
None | Some(Pad::None) => format!("{julian_day:.FRAC$}"),
Some(Pad::Zero) => {
let width = JULIAN_DAY_MIN_INTEGER_WIDTH + 1 + FRAC;
format!("{julian_day:0width$.FRAC$}")
}
Some(Pad::Space) => {
let width = JULIAN_DAY_MIN_INTEGER_WIDTH + 1 + FRAC;
format!("{julian_day:width$.FRAC$}")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::NaiveDate;
use std::fmt::Write;
fn format(dt: chrono::NaiveDateTime, fmt: &str, julian_day: f64) -> String {
let items = CustomStrftimeItems::new(fmt, julian_day);
let mut formatted = String::new();
write!(formatted, "{}", dt.format_with_items(items)).unwrap();
formatted
}
fn j2000() -> (chrono::NaiveDateTime, f64) {
let dt = NaiveDate::from_ymd_opt(2000, 1, 1)
.unwrap()
.and_hms_opt(12, 0, 0)
.unwrap();
(dt, 2451545.0)
}
#[test]
fn test_julian_day_default_is_unpadded() {
let (dt, jd) = j2000();
assert_eq!(format(dt, "%J", jd), "2451545.000000000");
}
#[test]
fn test_julian_day_escaping_is_unaffected_by_substitution() {
let (dt, jd) = j2000();
assert_eq!(format(dt, "%%J", jd), "%J");
}
#[test]
fn test_julian_day_pad_override_honors_width() {
let (dt, jd) = j2000();
let zero_padded = format(dt, "%0J", jd);
assert_eq!(zero_padded, "002451545.000000000");
let space_padded = format(dt, "%_J", jd);
assert_eq!(space_padded, " 2451545.000000000");
let unpadded = format(dt, "%-J", jd);
assert_eq!(unpadded, "2451545.000000000");
let default = format(dt, "%J", jd);
assert!(zero_padded.len() > default.len());
assert!(space_padded.len() > default.len());
assert_eq!(zero_padded.len(), space_padded.len());
}
#[test]
fn test_pad_override_now_works_for_other_specifiers_too() {
let dt = NaiveDate::from_ymd_opt(2024, 3, 5)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap();
assert_eq!(format(dt, "%d", 0.0), "05"); assert_eq!(format(dt, "%-d", 0.0), "5"); assert_eq!(format(dt, "%_d", 0.0), " 5"); }
}