use core::fmt;
use crate::{Lang, NepaliDate};
const STACK_STR_CAPACITY: usize = 128;
struct StackStr {
bytes: [u8; STACK_STR_CAPACITY],
len: usize,
}
impl fmt::Write for StackStr {
fn write_str(&mut self, s: &str) -> fmt::Result {
let end = self.len.checked_add(s.len()).ok_or(fmt::Error)?;
let dest = self.bytes.get_mut(self.len..end).ok_or(fmt::Error)?;
dest.copy_from_slice(s.as_bytes());
self.len = end;
Ok(())
}
}
pub(crate) fn write_once(
f: &mut fmt::Formatter<'_>,
write: impl Fn(&mut dyn fmt::Write) -> fmt::Result,
) -> fmt::Result {
let mut buf = StackStr {
bytes: [0; STACK_STR_CAPACITY],
len: 0,
};
if write(&mut buf).is_ok() {
if let Some(s) = buf
.bytes
.get(..buf.len)
.and_then(|b| core::str::from_utf8(b).ok())
{
return f.write_str(s);
}
}
write(f)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum DateOrder {
#[default]
Ymd,
Ydm,
Myd,
Mdy,
Dym,
Dmy,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Separator {
#[default]
Slash,
Backslash,
Dot,
Underscore,
Dash,
Space,
}
impl Separator {
#[must_use]
pub const fn as_char(self) -> char {
match self {
Separator::Slash => '/',
Separator::Backslash => '\\',
Separator::Dot => '.',
Separator::Underscore => '_',
Separator::Dash => '-',
Separator::Space => ' ',
}
}
const fn as_str(self) -> &'static str {
match self {
Separator::Slash => "/",
Separator::Backslash => "\\",
Separator::Dot => ".",
Separator::Underscore => "_",
Separator::Dash => "-",
Separator::Space => " ",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct LongDate {
pub weekday: bool,
pub year: bool,
pub pad: bool,
}
impl Default for LongDate {
fn default() -> Self {
Self {
weekday: false,
year: true,
pad: true,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct Digits {
n: u32,
min_width: u8,
lang: Lang,
}
const EN_DIGITS: [&str; 10] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
static NE_DIGITS: [&str; 10] = ["०", "१", "२", "३", "४", "५", "६", "७", "८", "९"];
impl Digits {
pub(crate) const fn new(n: u32, min_width: u8, lang: Lang) -> Self {
Self { n, min_width, lang }
}
#[allow(clippy::indexing_slicing, clippy::cast_possible_truncation)]
pub fn write_to<W: fmt::Write + ?Sized>(&self, out: &mut W) -> fmt::Result {
let table = match self.lang {
Lang::En => &EN_DIGITS,
Lang::Ne => &NE_DIGITS,
};
let mut digits = [0u8; 10];
let mut count = 0;
let mut n = self.n;
loop {
digits[count] = (n % 10) as u8;
count += 1;
n /= 10;
if n == 0 {
break;
}
}
for _ in count..usize::from(self.min_width) {
out.write_str(table[0])?;
}
for &d in digits[..count].iter().rev() {
out.write_str(table[usize::from(d)])?;
}
Ok(())
}
}
impl fmt::Display for Digits {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_once(f, |out| self.write_to(out))
}
}
fn year<W: fmt::Write + ?Sized>(
out: &mut W,
date: NepaliDate,
width: u8,
lang: Lang,
) -> fmt::Result {
lang.digits(u32::from(date.year()), width).write_to(out)
}
fn month<W: fmt::Write + ?Sized>(
out: &mut W,
date: NepaliDate,
width: u8,
lang: Lang,
) -> fmt::Result {
lang.digits(u32::from(date.month_number()), width)
.write_to(out)
}
fn day<W: fmt::Write + ?Sized>(
out: &mut W,
date: NepaliDate,
width: u8,
lang: Lang,
) -> fmt::Result {
lang.digits(u32::from(date.day()), width).write_to(out)
}
#[derive(Clone, Copy, Debug)]
pub struct ShortFormat {
date: NepaliDate,
order: DateOrder,
sep: Separator,
pad: bool,
lang: Lang,
}
impl ShortFormat {
pub fn write_to<W: fmt::Write + ?Sized>(&self, out: &mut W) -> fmt::Result {
let (y, md) = if self.pad { (4, 2) } else { (1, 1) };
let (d, lang, sep) = (self.date, self.lang, self.sep.as_str());
let parts: [Part; 3] = match self.order {
DateOrder::Ymd => [Part::Y, Part::M, Part::D],
DateOrder::Ydm => [Part::Y, Part::D, Part::M],
DateOrder::Myd => [Part::M, Part::Y, Part::D],
DateOrder::Mdy => [Part::M, Part::D, Part::Y],
DateOrder::Dym => [Part::D, Part::Y, Part::M],
DateOrder::Dmy => [Part::D, Part::M, Part::Y],
};
for (i, part) in parts.into_iter().enumerate() {
if i > 0 {
out.write_str(sep)?;
}
match part {
Part::Y => year(out, d, y, lang)?,
Part::M => month(out, d, md, lang)?,
Part::D => day(out, d, md, lang)?,
}
}
Ok(())
}
}
#[derive(Clone, Copy)]
enum Part {
Y,
M,
D,
}
impl fmt::Display for ShortFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_once(f, |out| self.write_to(out))
}
}
#[derive(Clone, Copy, Debug)]
pub struct LongFormat {
date: NepaliDate,
opts: LongDate,
lang: Lang,
}
impl LongFormat {
pub fn write_to<W: fmt::Write + ?Sized>(&self, out: &mut W) -> fmt::Result {
let (d, lang) = (self.date, self.lang);
if self.opts.weekday {
out.write_str(d.weekday().name(lang))?;
out.write_str(", ")?;
}
out.write_str(d.month().name(lang))?;
out.write_str(" ")?;
day(out, d, if self.opts.pad { 2 } else { 1 }, lang)?;
if self.opts.year {
out.write_str(", ")?;
year(out, d, 4, lang)?;
}
Ok(())
}
}
impl fmt::Display for LongFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_once(f, |out| self.write_to(out))
}
}
#[derive(Clone, Copy, Debug)]
pub struct PatternFormat<'a> {
date: NepaliDate,
pattern: &'a str,
lang: Lang,
}
impl PatternFormat<'_> {
pub fn write_to<W: fmt::Write + ?Sized>(&self, out: &mut W) -> fmt::Result {
let (d, lang) = (self.date, self.lang);
match self.pattern {
"" | "G" | "g" | "d" => {
return d
.short(DateOrder::Ymd, Separator::Slash, true, lang)
.write_to(out);
}
"D" => return d.long(LongDate::default(), lang).write_to(out),
"s" => {
return d
.short(DateOrder::Ymd, Separator::Dash, true, lang)
.write_to(out);
}
_ => {}
}
write_custom(out, d, self.pattern, lang)
}
}
impl fmt::Display for PatternFormat<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_once(f, |out| self.write_to(out))
}
}
fn write_custom<W: fmt::Write + ?Sized>(
out: &mut W,
d: NepaliDate,
pattern: &str,
lang: Lang,
) -> fmt::Result {
let bytes = pattern.as_bytes();
let len = bytes.len();
let at = |i: usize| bytes.get(i).copied();
let run_of = |start: usize, b: u8| {
let mut end = start;
while at(end) == Some(b) {
end += 1;
}
end - start
};
let mut i = 0;
while let Some(b) = at(i) {
match b {
b'\\' if i + 1 == len => i += 1,
b'\\' => {
let mut end = i + 2;
while !pattern.is_char_boundary(end) {
end += 1;
}
out.write_str(pattern.get(i + 1..end).unwrap_or(""))?;
i = end;
}
b'\'' => {
let start = i + 1;
let mut end = start;
while at(end).is_some_and(|c| c != b'\'') {
end += 1;
}
out.write_str(pattern.get(start..end).unwrap_or(""))?;
i = if end < len { end + 1 } else { end };
}
b'y' => {
let run = run_of(i, b'y');
if run >= 4 {
year(out, d, 4, lang)?;
} else {
lang.digits(u32::from(d.year() % 100), 2).write_to(out)?;
}
i += run;
}
b'M' => {
let run = run_of(i, b'M');
match run {
1 => month(out, d, 1, lang)?,
2 => month(out, d, 2, lang)?,
3 => out.write_str(d.month().short_name(lang))?,
_ => out.write_str(d.month().name(lang))?,
}
i += run;
}
b'd' => {
let run = run_of(i, b'd');
day(out, d, if run == 2 { 2 } else { 1 }, lang)?;
i += run;
}
_ => {
let mut end = i + 1;
while at(end).is_some_and(|c| !matches!(c, b'\\' | b'\'' | b'y' | b'M' | b'd')) {
end += 1;
}
out.write_str(pattern.get(i..end).unwrap_or(""))?;
i = end;
}
}
}
Ok(())
}
impl NepaliDate {
#[must_use]
pub const fn short(
self,
order: DateOrder,
sep: Separator,
pad: bool,
lang: Lang,
) -> ShortFormat {
ShortFormat {
date: self,
order,
sep,
pad,
lang,
}
}
#[must_use]
pub const fn long(self, opts: LongDate, lang: Lang) -> LongFormat {
LongFormat {
date: self,
opts,
lang,
}
}
#[must_use]
pub const fn format_pattern(self, pattern: &str, lang: Lang) -> PatternFormat<'_> {
PatternFormat {
date: self,
pattern,
lang,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Buf {
bytes: [u8; 128],
len: usize,
}
impl Buf {
fn new() -> Self {
Self {
bytes: [0; 128],
len: 0,
}
}
fn written(&self) -> &[u8] {
self.bytes.get(..self.len).unwrap_or(&[])
}
}
impl fmt::Write for Buf {
fn write_str(&mut self, s: &str) -> fmt::Result {
let end = self.len + s.len();
let dest = self.bytes.get_mut(self.len..end).ok_or(fmt::Error)?;
dest.copy_from_slice(s.as_bytes());
self.len = end;
Ok(())
}
}
fn bs(y: u16, m: u8, d: u8) -> NepaliDate {
NepaliDate::new(y, m, d).unwrap_or(NepaliDate::MIN)
}
fn check(write: impl Fn(&mut Buf) -> fmt::Result, expected: &str) {
let mut buf = Buf::new();
assert_eq!(write(&mut buf), Ok(()));
assert_eq!(buf.written(), expected.as_bytes(), "{expected}");
}
#[test]
fn digits() {
let cases = [
(Lang::En, 0, 0, "0"),
(Lang::En, 0, 3, "000"),
(Lang::En, u32::MAX, 0, "4294967295"),
(Lang::En, 123, 2, "123"),
(Lang::Ne, 1_234_567_890, 0, "१२३४५६७८९०"),
(Lang::Ne, 83, 2, "८३"),
(Lang::Ne, 0, 2, "००"),
];
for (lang, n, width, expected) in cases {
check(|b| lang.digits(n, width).write_to(b), expected);
}
}
#[test]
fn short_examples() {
let d = bs(2080, 5, 15);
check(
|b| {
d.short(DateOrder::Ymd, Separator::Dash, true, Lang::En)
.write_to(b)
},
"2080-05-15",
);
check(
|b| {
d.short(DateOrder::Dmy, Separator::Dash, false, Lang::En)
.write_to(b)
},
"15-5-2080",
);
check(
|b| {
d.short(DateOrder::Ymd, Separator::Slash, true, Lang::Ne)
.write_to(b)
},
"२०८०/०५/१५",
);
let orders = [
(DateOrder::Ydm, "2080/15/05"),
(DateOrder::Myd, "05/2080/15"),
(DateOrder::Mdy, "05/15/2080"),
(DateOrder::Dym, "15/2080/05"),
];
for (order, expected) in orders {
check(
|b| d.short(order, Separator::Slash, true, Lang::En).write_to(b),
expected,
);
}
let seps = [
(Separator::Backslash, "2080\\05\\15"),
(Separator::Dot, "2080.05.15"),
(Separator::Underscore, "2080_05_15"),
(Separator::Space, "2080 05 15"),
];
for (sep, expected) in seps {
check(
|b| d.short(DateOrder::Ymd, sep, true, Lang::En).write_to(b),
expected,
);
}
}
#[test]
fn long_examples() {
let weekday = LongDate {
weekday: true,
..LongDate::default()
};
let short = LongDate {
weekday: true,
year: false,
pad: false,
};
let cases = [
(
bs(2081, 4, 15),
LongDate::default(),
"Shrawan 15, 2081",
"साउन १५, २०८१",
),
(
bs(2081, 4, 15),
weekday,
"Tuesday, Shrawan 15, 2081",
"मङ्गलवार, साउन १५, २०८१",
),
(
NepaliDate::MIN,
LongDate::default(),
"Baishakh 01, 1901",
"बैशाख ०१, १९०१",
),
(bs(2079, 2, 6), short, "Friday, Jestha 6", "शुक्रवार, जेठ ६"),
];
for (d, opts, en, ne) in cases {
check(|b| d.long(opts, Lang::En).write_to(b), en);
check(|b| d.long(opts, Lang::Ne).write_to(b), ne);
}
}
#[test]
fn pattern_examples() {
let d = bs(2081, 4, 15);
let cases = [
("yyyy-MM-dd", "2081-04-15"),
("M/d/yyyy", "4/15/2081"),
("yyyy'BS'", "2081BS"),
("yyyy\\M\\M-\\d\\d", "2081MM-dd"),
("MMM yyyy", "Shr 2081"),
("dd.MM.yy", "15.04.81"),
("", "2081/04/15"),
("G", "2081/04/15"),
("g", "2081/04/15"),
("d", "2081/04/15"),
("D", "Shrawan 15, 2081"),
("s", "2081-04-15"),
("ddd", "15"),
("y", "81"),
("yyyyy", "2081"),
("MMMMM", "Shrawan"),
("'unterminated", "unterminated"),
("yyyy\\", "2081"),
("\\साउन", "साउन"),
("Gd", "G15"),
("साल yyyy", "साल 2081"),
];
for (pattern, expected) in cases {
check(
|b| d.format_pattern(pattern, Lang::En).write_to(b),
expected,
);
}
check(
|b| d.format_pattern("s", Lang::Ne).write_to(b),
"२०८१-०४-१५",
);
check(|b| d.format_pattern("MMM", Lang::Ne).write_to(b), "साउन");
check(|b| d.format_pattern("'y' yy", Lang::Ne).write_to(b), "y ८१");
}
#[test]
fn display_writes_through_write_to() {
use core::fmt::Write;
let d = bs(2081, 4, 15);
let mut buf = Buf::new();
assert_eq!(write!(buf, "{d}"), Ok(()));
assert_eq!(buf.written(), b"2081/04/15");
}
#[test]
fn full_buffer_is_an_error_not_a_panic() {
const LONG: &str = "0123456789012345678901234567890123456789012345678901234567890123456789 0123456789012345678901234567890123456789012345678901234567890123456789";
let mut buf = Buf::new();
let d = bs(2081, 4, 15);
assert_eq!(
d.format_pattern(LONG, Lang::En).write_to(&mut buf),
Err(fmt::Error)
);
}
}