use std::sync::LazyLock;
use crate::calculation::duration::{Duration, DurationUnit};
use crate::calculation::years::find_most_likely_ad_year;
use crate::core::regex::{JsRegex, RegexMatch};
use crate::core::utils::js::{js_parse_float, js_parse_int, js_substring_from};
use crate::core::utils::pattern::{dict_get, dict_keys, js_len, match_any_pattern, repeated_timeunit_pattern, Dictionary};
use crate::js_regex;
pub static WEEKDAY_DICTIONARY: &Dictionary<i64> = &[
("chủ nhật", 0),
("cn", 0),
("thứ hai", 1),
("t2", 1),
("thứ ba", 2),
("t3", 2),
("thứ tư", 3),
("t4", 3),
("thứ năm", 4),
("t5", 4),
("thứ sáu", 5),
("t6", 5),
("thứ bảy", 6),
("t7", 6),
];
pub static MONTH_DICTIONARY: &Dictionary<i64> = &[
("tháng 1", 1),
("tháng một", 1),
("tháng giêng", 1),
("tháng 2", 2),
("tháng hai", 2),
("tháng 3", 3),
("tháng ba", 3),
("tháng 4", 4),
("tháng tư", 4),
("tháng 5", 5),
("tháng năm", 5),
("tháng 6", 6),
("tháng sáu", 6),
("tháng 7", 7),
("tháng bảy", 7),
("tháng 8", 8),
("tháng tám", 8),
("tháng 9", 9),
("tháng chín", 9),
("tháng 10", 10),
("tháng mười", 10),
("tháng 11", 11),
("tháng mười một", 11),
("tháng 12", 12),
("tháng mười hai", 12),
("tháng chạp", 12),
];
pub static INTEGER_WORD_DICTIONARY: &Dictionary<i64> = &[
("một", 1),
("hai", 2),
("ba", 3),
("bốn", 4),
("năm", 5),
("sáu", 6),
("bảy", 7),
("tám", 8),
("chín", 9),
("mười", 10),
("mười một", 11),
("mười hai", 12),
];
pub static TIME_UNIT_DICTIONARY: &Dictionary<DurationUnit> = &[
("giây", DurationUnit::Second),
("phút", DurationUnit::Minute),
("giờ", DurationUnit::Hour),
("ngày", DurationUnit::Day),
("tuần", DurationUnit::Week),
("tháng", DurationUnit::Month),
("năm", DurationUnit::Year),
];
pub static NUMBER_PATTERN: LazyLock<String> = LazyLock::new(|| {
format!(
r"(?:{}|[0-9]+|[0-9]+\.[0-9]+)",
match_any_pattern(dict_keys(INTEGER_WORD_DICTIONARY))
)
});
pub fn parse_number_pattern(m: &str) -> f64 {
let num = m.to_lowercase();
if let Some(v) = dict_get(INTEGER_WORD_DICTIONARY, &num) {
return v as f64;
}
js_parse_float(&num)
}
pub const YEAR_PATTERN: &str = r"(?:[0-9]{1,4}(?:\s*TCN)?)";
pub fn parse_year(m: &str) -> Option<i64> {
let upper = m.to_uppercase();
let digits: String = m.chars().filter(|c| c.is_ascii_digit()).collect();
let num = js_parse_int(&digits);
if js_regex!(r"TCN").is_match(&upper) {
return num.map(|n| -n);
}
num.map(find_most_likely_ad_year)
}
static SINGLE_TIME_UNIT_PATTERN: LazyLock<String> = LazyLock::new(|| {
format!(
r"({})\s{{0,5}}({})\s{{0,5}}",
*NUMBER_PATTERN,
match_any_pattern(dict_keys(TIME_UNIT_DICTIONARY))
)
});
static SINGLE_TIME_UNIT_REGEX: LazyLock<JsRegex> = LazyLock::new(|| JsRegex::new(&SINGLE_TIME_UNIT_PATTERN, "i"));
pub static TIME_UNITS_PATTERN: LazyLock<String> =
LazyLock::new(|| repeated_timeunit_pattern("", &SINGLE_TIME_UNIT_PATTERN, None));
pub fn parse_duration(timeunit_text: &str) -> Duration {
let mut fragments = Duration::new();
let mut remaining = timeunit_text.to_string();
let mut found = SINGLE_TIME_UNIT_REGEX.exec(&remaining);
while let Some(m) = found {
collect_fragment(&mut fragments, &m);
remaining = js_substring_from(&remaining, js_len(m.text())).to_string();
found = SINGLE_TIME_UNIT_REGEX.exec(&remaining);
}
fragments
}
fn collect_fragment(fragments: &mut Duration, m: &RegexMatch) {
let num = parse_number_pattern(m.get(1).unwrap_or(""));
if let Some(unit) = dict_get(TIME_UNIT_DICTIONARY, &m.get(2).unwrap_or("").to_lowercase()) {
fragments.set(unit, num);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dict_is<V: Copy + PartialEq + std::fmt::Debug>(d: &Dictionary<V>, expect: &[(&str, V)]) {
assert_eq!(d, expect);
}
fn num(input: &str, expect: Option<f64>) {
let v = parse_number_pattern(input);
match expect {
Some(e) => assert_eq!(v, e),
None => assert!(v.is_nan()),
}
}
fn dur(input: &str, expect: &[(DurationUnit, f64)]) {
let d = parse_duration(input);
assert_eq!(d.iter().collect::<Vec<_>>(), expect.to_vec());
}
#[test]
fn weekday_dictionary_matches_upstream() {
dict_is(
WEEKDAY_DICTIONARY,
&[
("chủ nhật", 0),
("cn", 0),
("thứ hai", 1),
("t2", 1),
("thứ ba", 2),
("t3", 2),
("thứ tư", 3),
("t4", 3),
("thứ năm", 4),
("t5", 4),
("thứ sáu", 5),
("t6", 5),
("thứ bảy", 6),
("t7", 6),
],
);
}
#[test]
fn month_dictionary_matches_upstream() {
dict_is(
MONTH_DICTIONARY,
&[
("tháng 1", 1),
("tháng một", 1),
("tháng giêng", 1),
("tháng 2", 2),
("tháng hai", 2),
("tháng 3", 3),
("tháng ba", 3),
("tháng 4", 4),
("tháng tư", 4),
("tháng 5", 5),
("tháng năm", 5),
("tháng 6", 6),
("tháng sáu", 6),
("tháng 7", 7),
("tháng bảy", 7),
("tháng 8", 8),
("tháng tám", 8),
("tháng 9", 9),
("tháng chín", 9),
("tháng 10", 10),
("tháng mười", 10),
("tháng 11", 11),
("tháng mười một", 11),
("tháng 12", 12),
("tháng mười hai", 12),
("tháng chạp", 12),
],
);
}
#[test]
fn integer_word_dictionary_matches_upstream() {
dict_is(
INTEGER_WORD_DICTIONARY,
&[
("một", 1),
("hai", 2),
("ba", 3),
("bốn", 4),
("năm", 5),
("sáu", 6),
("bảy", 7),
("tám", 8),
("chín", 9),
("mười", 10),
("mười một", 11),
("mười hai", 12),
],
);
}
#[test]
fn time_unit_dictionary_matches_upstream() {
dict_is(
TIME_UNIT_DICTIONARY,
&[
("giây", DurationUnit::Second),
("phút", DurationUnit::Minute),
("giờ", DurationUnit::Hour),
("ngày", DurationUnit::Day),
("tuần", DurationUnit::Week),
("tháng", DurationUnit::Month),
("năm", DurationUnit::Year),
],
);
}
#[test]
fn number_pattern_matches_upstream() {
assert_eq!(
*NUMBER_PATTERN,
r"(?:(?:mười một|mười hai|chín|mười|một|hai|bốn|năm|sáu|bảy|tám|ba)|[0-9]+|[0-9]+\.[0-9]+)"
);
}
#[test]
fn parse_number_pattern_case_1() {
num("0", Some(0.0));
}
#[test]
fn parse_number_pattern_case_2() {
num("ba", Some(3.0));
}
#[test]
fn parse_number_pattern_case_3() {
num("1", Some(1.0));
}
#[test]
fn parse_number_pattern_case_4() {
num("cn", None);
}
#[test]
fn parse_number_pattern_case_5() {
num("2", Some(2.0));
}
#[test]
fn parse_number_pattern_case_6() {
num("bảy", Some(7.0));
}
#[test]
fn parse_number_pattern_case_7() {
num("7 giờ", Some(7.0));
}
#[test]
fn parse_number_pattern_case_8() {
num("1 giờ ", Some(1.0));
}
#[test]
fn year_pattern_matches_upstream() {
assert_eq!(YEAR_PATTERN, r"(?:[0-9]{1,4}(?:\s*TCN)?)");
}
#[test]
fn parse_year_case_1() {
assert_eq!(parse_year("0"), Some(2000));
}
#[test]
fn parse_year_case_2() {
assert_eq!(parse_year("ba"), None);
}
#[test]
fn parse_year_case_3() {
assert_eq!(parse_year("1"), Some(2001));
}
#[test]
fn parse_year_case_4() {
assert_eq!(parse_year("2"), Some(2002));
}
#[test]
fn parse_year_case_5() {
assert_eq!(parse_year("t2"), Some(2002));
}
#[test]
fn parse_year_case_6() {
assert_eq!(parse_year("t3"), Some(2003));
}
#[test]
fn parse_year_case_7() {
assert_eq!(parse_year("t4"), Some(2004));
}
#[test]
fn time_units_pattern_matches_upstream() {
assert_eq!(
*TIME_UNITS_PATTERN,
r"(?:(?:(?:mười một|mười hai|chín|mười|một|hai|bốn|năm|sáu|bảy|tám|ba)|[0-9]+|[0-9]+\.[0-9]+))\s{0,5}(?:(?:tháng|giây|phút|ngày|tuần|giờ|năm))\s{0,5}(?:\s{0,5},?\s{0,5}(?:(?:(?:mười một|mười hai|chín|mười|một|hai|bốn|năm|sáu|bảy|tám|ba)|[0-9]+|[0-9]+\.[0-9]+))\s{0,5}(?:(?:tháng|giây|phút|ngày|tuần|giờ|năm))\s{0,5}){0,10}"
);
}
#[test]
fn parse_duration_case_1() {
dur("0", &[]);
}
#[test]
fn parse_duration_case_2() {
dur("7 giờ", &[(DurationUnit::Hour, 7.0)]);
}
#[test]
fn parse_duration_case_3() {
dur("1 giờ ", &[(DurationUnit::Hour, 1.0)]);
}
#[test]
fn parse_duration_case_4() {
dur("1 năm ", &[(DurationUnit::Year, 1.0)]);
}
#[test]
fn parse_duration_case_5() {
dur("ba ngày ", &[(DurationUnit::Day, 3.0)]);
}
#[test]
fn parse_duration_case_6() {
dur("hai tuần ", &[(DurationUnit::Week, 2.0)]);
}
#[test]
fn parse_duration_case_7() {
dur("một tháng ", &[(DurationUnit::Month, 1.0)]);
}
}