use core::str::FromStr;
use crate::{Error, NepaliDate, ParseErrorKind};
fn digit_at(b: &[u8], i: usize) -> Option<(u32, usize)> {
match *b.get(i)? {
c @ b'0'..=b'9' => Some((u32::from(c - b'0'), 1)),
0xE0 if b.get(i + 1) == Some(&0xA5) => match b.get(i + 2) {
Some(&c @ 0xA6..=0xAF) => Some((u32::from(c - 0xA6), 3)),
_ => None,
},
_ => None,
}
}
fn separator_at(b: &[u8], i: usize, dot: bool) -> Option<usize> {
match *b.get(i)? {
b'-' | b'/' | b'_' | b'\\' | b' ' | b'|' => Some(1),
b'.' if dot => Some(1),
0xE0 if b.get(i + 1) == Some(&0xA5) && b.get(i + 2) == Some(&0xA4) => Some(3),
_ => None,
}
}
fn validate(year: u32, month: u32, day: u32) -> Result<NepaliDate, Error> {
let year = u16::try_from(year).map_err(|_| Error::OutOfRange)?;
let month = u8::try_from(month).unwrap_or(u8::MAX);
let day = u8::try_from(day).unwrap_or(u8::MAX);
NepaliDate::new(year, month, day)
}
fn parse_error(kind: ParseErrorKind) -> Error {
Error::Parse(kind)
}
fn parse_strict(s: &str) -> Result<NepaliDate, Error> {
let b = s.as_bytes();
let mut groups = [0u32; 3];
let mut count = 0;
let mut digits = 0;
let mut i = 0;
while i < b.len() {
if let Some((value, len)) = digit_at(b, i) {
if digits == 0 {
if count == 3 {
return Err(parse_error(ParseErrorKind::WrongGroupCount));
}
count += 1;
}
digits += 1;
if digits > 4 {
return Err(parse_error(ParseErrorKind::NumberTooLong));
}
if let Some(g) = groups.get_mut(count - 1) {
*g = *g * 10 + value;
}
i += len;
} else if let Some(len) = separator_at(b, i, true) {
digits = 0;
i += len;
} else {
return Err(parse_error(ParseErrorKind::InvalidCharacter));
}
}
if count < 3 {
return Err(parse_error(ParseErrorKind::WrongGroupCount));
}
let [year, month, day] = groups;
validate(year, month, day)
}
impl FromStr for NepaliDate {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
parse_strict(s)
}
}
static MONTH_ALIASES: [(&str, u8); 80] = [
("baishakh", 1),
("baisakh", 1),
("baishak", 1),
("vaishakh", 1),
("vaisakh", 1),
("bai", 1),
("बैशाख", 1),
("वैशाख", 1),
("बैसाख", 1),
("jestha", 2),
("jeth", 2),
("jyestha", 2),
("jes", 2),
("जेठ", 2),
("जेष्ठ", 2),
("ज्येष्ठ", 2),
("ashad", 3),
("ashadh", 3),
("asar", 3),
("asadh", 3),
("asa", 3),
("असार", 3),
("आषाढ", 3),
("असाढ", 3),
("shrawan", 4),
("shravan", 4),
("srawan", 4),
("sawan", 4),
("saun", 4),
("shr", 4),
("साउन", 4),
("श्रावण", 4),
("सावन", 4),
("bhadra", 5),
("bhadau", 5),
("bha", 5),
("भदौ", 5),
("भाद्र", 5),
("भाद्रपद", 5),
("ashoj", 6),
("asoj", 6),
("ashwin", 6),
("aso", 6),
("असोज", 6),
("आश्विन", 6),
("असौज", 6),
("kartik", 7),
("kattik", 7),
("kar", 7),
("कार्तिक", 7),
("कात्तिक", 7),
("mangsir", 8),
("mansir", 8),
("margashirsha", 8),
("man", 8),
("मंसिर", 8),
("मङ्सिर", 8),
("मार्गशीर्ष", 8),
("poush", 9),
("paush", 9),
("push", 9),
("pus", 9),
("pou", 9),
("पुष", 9),
("पौष", 9),
("पुस", 9),
("magh", 10),
("mag", 10),
("माघ", 10),
("falgun", 11),
("phalgun", 11),
("fagun", 11),
("fal", 11),
("फागुन", 11),
("फाल्गुन", 11),
("chaitra", 12),
("chait", 12),
("cha", 12),
("चैत", 12),
("चैत्र", 12),
];
const KELVIN: [u8; 3] = [0xE2, 0x84, 0xAA];
fn fold_eq(token: &str, entry: &str) -> bool {
let (t, e) = (token.as_bytes(), entry.as_bytes());
let (mut i, mut j) = (0, 0);
while let Some(&c) = t.get(i) {
let (folded, width) = if t.get(i..i + 3) == Some(&KELVIN[..]) {
(b'k', 3)
} else {
(c.to_ascii_lowercase(), 1)
};
if e.get(j) != Some(&folded) {
return false;
}
i += width;
j += 1;
}
j == e.len()
}
static IGNORED_TOKENS: [&str; 12] = [
"bs",
"b.s.",
"b.s",
"vs",
"v.s.",
"v.s",
"बि.सं.",
"वि.सं.",
"बि.सं",
"वि.सं",
"गते",
"मिति",
];
#[derive(Default)]
struct Tokens {
numbers: [(u32, u8); 3],
number_count: usize,
month: Option<u8>,
month_count: usize,
unknown_word: bool,
too_long: bool,
}
impl Tokens {
fn push_number(&mut self, b: &[u8], start: usize, end: usize) {
let (mut value, mut digits, mut i) = (0u32, 0u8, start);
while i < end {
let Some((d, len)) = digit_at(b, i) else {
break;
};
digits = digits.saturating_add(1);
if digits <= 4 {
value = value * 10 + d;
}
i += len;
}
if digits == 0 {
return;
}
if digits > 4 {
self.too_long = true;
}
if let Some(slot) = self.numbers.get_mut(self.number_count) {
*slot = (value, digits);
}
self.number_count += 1;
}
fn push_word(&mut self, word: &str) {
if word.is_empty() || IGNORED_TOKENS.iter().any(|t| fold_eq(word, t)) {
return;
}
match MONTH_ALIASES.iter().find(|(alias, _)| fold_eq(word, alias)) {
Some(&(_, m)) => {
self.month = Some(m);
self.month_count += 1;
}
None => self.unknown_word = true,
}
}
}
fn tokenize(s: &str) -> Tokens {
let b = s.as_bytes();
let mut tokens = Tokens::default();
let is_sep = |i: usize| -> Option<usize> {
if b.get(i) == Some(&b',') {
Some(1)
} else {
separator_at(b, i, false)
}
};
let mut i = 0;
while i < b.len() {
if let Some(len) = is_sep(i) {
i += len;
continue;
}
let start = i;
let mut only_digits_and_dots = true;
while i < b.len() && is_sep(i).is_none() {
if let Some((_, len)) = digit_at(b, i) {
i += len;
} else {
if b.get(i) != Some(&b'.') {
only_digits_and_dots = false;
}
i += 1;
}
}
split_chunk(s, start, i, only_digits_and_dots, &mut tokens);
}
tokens
}
fn split_chunk(s: &str, start: usize, end: usize, only_digits_and_dots: bool, t: &mut Tokens) {
if !only_digits_and_dots {
if let Some(word) = s.get(start..end) {
t.push_word(word);
}
return;
}
let b = s.as_bytes();
let mut i = start;
while i < end {
if b.get(i) == Some(&b'.') {
i += 1;
continue;
}
let run = i;
while i < end && b.get(i) != Some(&b'.') {
i += digit_at(b, i).map_or(1, |(_, len)| len);
}
t.push_number(b, run, i);
}
}
fn parse_lenient_tokens(s: &str) -> Result<NepaliDate, Error> {
let t = tokenize(s);
if t.too_long {
return Err(parse_error(ParseErrorKind::NumberTooLong));
}
if t.unknown_word || t.month_count > 1 {
return Err(parse_error(ParseErrorKind::Unrecognized));
}
let [(n0, w0), (n1, w1), (n2, w2)] = t.numbers;
match (t.month, t.number_count) {
(Some(month), 2) => {
let (year, day, day_width) = match (w0 == 4, w1 == 4) {
(true, false) => (n0, n1, w1),
(false, true) => (n1, n0, w0),
_ => return Err(parse_error(ParseErrorKind::Ambiguous)),
};
if day_width > 2 {
return Err(parse_error(ParseErrorKind::Ambiguous));
}
validate(year, u32::from(month), day)
}
(None, 3) => {
if w0 == 4 {
validate(n0, n1, n2)
} else if w2 == 4 {
validate(n2, n1, n0)
} else {
Err(parse_error(ParseErrorKind::Ambiguous))
}
}
_ => Err(parse_error(ParseErrorKind::Unrecognized)),
}
}
impl NepaliDate {
#[must_use = "parsing may fail"]
pub fn parse_lenient(s: &str) -> Result<Self, Error> {
match parse_strict(s) {
Ok(d) => Ok(d),
Err(_) => parse_lenient_tokens(s),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn kind(k: ParseErrorKind) -> Result<NepaliDate, Error> {
Err(Error::Parse(k))
}
fn bs(y: u16, m: u8, d: u8) -> Result<NepaliDate, Error> {
NepaliDate::new(y, m, d)
}
#[test]
fn strict_examples() {
for s in [
"2080/05/15",
"2080-5-15",
" 2080 / 05 / 15 ",
"/2080/05/15/",
"2080।05।15",
"२०८०/०५/१५",
"2\u{0966}\u{096E}0/0\u{096B}/\u{0967}5",
] {
assert_eq!(s.parse::<NepaliDate>(), bs(2080, 5, 15), "{s:?}");
}
assert_eq!(
"".parse::<NepaliDate>(),
kind(ParseErrorKind::WrongGroupCount)
);
assert_eq!(
"2080/05/15/1".parse::<NepaliDate>(),
kind(ParseErrorKind::WrongGroupCount)
);
assert_eq!(
"12345/1/1".parse::<NepaliDate>(),
kind(ParseErrorKind::NumberTooLong)
);
assert_eq!(
"2080/05/1x".parse::<NepaliDate>(),
kind(ParseErrorKind::InvalidCharacter)
);
assert_eq!(
"2080,05,15".parse::<NepaliDate>(),
kind(ParseErrorKind::InvalidCharacter)
);
assert_eq!(
"2080/05/१क".parse::<NepaliDate>(),
kind(ParseErrorKind::InvalidCharacter)
);
assert_eq!(
"9999/9999/9999".parse::<NepaliDate>(),
Err(Error::OutOfRange)
);
assert_eq!(
"2080/9999/1".parse::<NepaliDate>(),
Err(Error::InvalidMonth(255))
);
assert!(matches!(
"2080/1/9999".parse::<NepaliDate>(),
Err(Error::InvalidDay { day: 255, .. })
));
}
#[test]
fn lenient_forms() {
let d = bs(2080, 4, 15);
for s in [
"15 Shrawan 2080",
"Shrawan 15, 2080",
"2080 Shrawan 15",
"15.04.2080",
"2080.04.15 B.S.",
"15 साउन, 2080 गते",
"१५ श्रावण २०८०",
"15 SHRAWAN 2080",
] {
assert_eq!(NepaliDate::parse_lenient(s), d, "{s:?}");
}
assert_eq!(
NepaliDate::parse_lenient(""),
kind(ParseErrorKind::Unrecognized)
);
assert_eq!(
NepaliDate::parse_lenient(" "),
kind(ParseErrorKind::Unrecognized)
);
assert_eq!(
NepaliDate::parse_lenient("80/05/15"),
kind(ParseErrorKind::Ambiguous)
);
assert_eq!(
NepaliDate::parse_lenient("15 shrawan 80"),
kind(ParseErrorKind::Ambiguous)
);
assert_eq!(
NepaliDate::parse_lenient("15 shrawan 080"),
kind(ParseErrorKind::Ambiguous)
);
assert_eq!(
NepaliDate::parse_lenient("2080 shrawan 2081"),
kind(ParseErrorKind::Ambiguous)
);
assert_eq!(
NepaliDate::parse_lenient("Shrawan Bhadra 2080"),
kind(ParseErrorKind::Unrecognized)
);
assert_eq!(
NepaliDate::parse_lenient("1 2 3 4 5"),
kind(ParseErrorKind::Unrecognized)
);
assert_eq!(
NepaliDate::parse_lenient("1 2 3 4 123456"),
kind(ParseErrorKind::NumberTooLong)
);
for s in [
"15th Shrawan 2080",
"Shr.15 2080",
"15.Shrawan.2080",
"2080.B.S. Shrawan 15",
] {
let parsed = NepaliDate::parse_lenient(s);
assert_eq!(parsed, kind(ParseErrorKind::Unrecognized), "{s:?}");
}
assert_eq!(
NepaliDate::parse_lenient("15/13/2080"),
Err(Error::InvalidMonth(13))
);
}
#[test]
fn aliases_are_lower_case_or_devanagari() {
for (alias, m) in MONTH_ALIASES {
assert!((1..=12).contains(&m));
assert!(!alias.bytes().any(|c| c.is_ascii_uppercase()), "{alias}");
}
for token in IGNORED_TOKENS {
assert!(!token.bytes().any(|c| c.is_ascii_uppercase()), "{token}");
}
}
#[test]
fn fold_eq_folds_like_str_lower_onto_ascii() {
assert!(fold_eq("SHRAWAN", "shrawan"));
assert!(fold_eq("ShRaWaN", "shrawan"));
assert!(
fold_eq("\u{212A}artik", "kartik"),
"the Kelvin sign folds to k"
);
assert!(
!fold_eq("\u{17F}hrawan", "shrawan"),
"the long s does not fold"
);
assert!(!fold_eq("\u{131}", "i"), "the dotless i does not fold");
assert!(!fold_eq("B\u{17F}", "bs"));
assert!(!fold_eq("shrawa", "shrawan"));
assert!(!fold_eq("shrawann", "shrawan"));
assert!(fold_eq("", ""));
assert!(fold_eq("श्रावण", "श्रावण"));
}
}