use std::iter;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Numeral {
Digit(char),
Position(u8),
}
pub(crate) type Classifier = fn(char) -> Option<Numeral>;
fn zeros_for(rank: u8) -> usize {
if rank <= 3 {
rank as usize
} else {
4 * (rank as usize - 3)
}
}
fn adjust_digits(num: &str, base: usize, digit: usize) -> String {
let width = base + digit;
let len = num.chars().count();
if width <= len {
return num.to_owned();
}
let mut padded = String::with_capacity(width);
padded.extend(iter::repeat_n('0', width - len));
padded.push_str(num);
padded
}
pub(crate) fn is_all_numeral(text: &str, classify: Classifier) -> bool {
text.chars().all(|c| classify(c).is_some())
}
pub(crate) fn to_arabic_numerals(from_str: &str, classify: Classifier) -> String {
if !is_all_numeral(from_str, classify) {
return from_str.to_owned();
}
let mut num_buf = String::new();
let mut digit = 0usize;
let chars = from_str.chars().rev().collect::<Vec<char>>();
let mut i = chars.iter().peekable();
while let Some(&c) = i.next() {
match classify(c) {
Some(Numeral::Digit(d)) => num_buf.insert(0, d),
Some(Numeral::Position(rank)) => {
let zeros = zeros_for(rank);
let base = if rank <= 3 { zeros } else { 0 };
if rank > 3 {
digit = zeros;
}
num_buf = adjust_digits(&num_buf, base, digit);
let preceded_by_higher = match i.peek().and_then(|&&prev| classify(prev)) {
Some(Numeral::Position(prev_rank)) => prev_rank > rank,
_ => false,
};
if i.peek().is_none() || preceded_by_higher {
num_buf.insert(0, '1');
}
}
None => {
num_buf.insert(0, c);
digit = 0;
}
}
}
num_buf
}
#[cfg(test)]
mod tests {
use crate::token_filter::numeral::{
Numeral, adjust_digits, is_all_numeral, to_arabic_numerals,
};
fn classify(c: char) -> Option<Numeral> {
let numeral = match c {
'0'..='9' => Numeral::Digit(c),
'一' => Numeral::Digit('1'),
'二' => Numeral::Digit('2'),
'十' => Numeral::Position(1),
'百' => Numeral::Position(2),
'万' => Numeral::Position(4),
_ => return None,
};
Some(numeral)
}
#[test]
fn test_adjust_digits_counts_characters_not_bytes() {
assert_eq!(adjust_digits("歳", 2, 0), "0歳");
assert_eq!(adjust_digits("十歳", 4, 0), "00十歳");
assert_eq!(adjust_digits("1", 2, 0), "01");
assert_eq!(adjust_digits("123", 2, 0), "123");
assert_eq!(adjust_digits("", 0, 4), "0000");
}
#[test]
fn test_is_all_numeral() {
assert!(is_all_numeral("二十一", classify));
assert!(is_all_numeral("", classify));
assert!(!is_all_numeral("二十歳", classify));
}
#[test]
fn test_to_arabic_numerals_returns_mixed_tokens_unchanged() {
assert_eq!(to_arabic_numerals("二十歳", classify), "二十歳");
assert_eq!(to_arabic_numerals("万歳", classify), "万歳");
}
#[test]
fn test_to_arabic_numerals_positions() {
assert_eq!(to_arabic_numerals("十", classify), "10");
assert_eq!(to_arabic_numerals("二十一", classify), "21");
assert_eq!(to_arabic_numerals("百", classify), "100");
assert_eq!(to_arabic_numerals("万", classify), "10000");
assert_eq!(to_arabic_numerals("十万", classify), "100000");
assert_eq!(to_arabic_numerals("二百万", classify), "2000000");
assert_eq!(to_arabic_numerals("百十", classify), "110");
assert_eq!(to_arabic_numerals("万十", classify), "10010");
}
}