1use std::str::FromStr;
2
3use chinese_number::{
4 from_usize_to_chinese_ten_thousand as usize_to_chinese, ChineseCase, ChineseVariant,
5};
6use comemo::Tracked;
7use ecow::{eco_format, EcoString, EcoVec};
8
9use crate::diag::SourceResult;
10use crate::engine::Engine;
11use crate::foundations::{cast, func, Context, Func, Str, Value};
12use crate::text::Case;
13
14#[func]
55pub fn numbering(
56 engine: &mut Engine,
57 context: Tracked<Context>,
58 numbering: Numbering,
83 #[variadic]
88 numbers: Vec<usize>,
89) -> SourceResult<Value> {
90 numbering.apply(engine, context, &numbers)
91}
92
93#[derive(Debug, Clone, PartialEq, Hash)]
95pub enum Numbering {
96 Pattern(NumberingPattern),
98 Func(Func),
100}
101
102impl Numbering {
103 pub fn apply(
105 &self,
106 engine: &mut Engine,
107 context: Tracked<Context>,
108 numbers: &[usize],
109 ) -> SourceResult<Value> {
110 Ok(match self {
111 Self::Pattern(pattern) => Value::Str(pattern.apply(numbers).into()),
112 Self::Func(func) => func.call(engine, context, numbers.iter().copied())?,
113 })
114 }
115
116 pub fn trimmed(mut self) -> Self {
118 if let Self::Pattern(pattern) = &mut self {
119 pattern.trimmed = true;
120 }
121 self
122 }
123}
124
125impl From<NumberingPattern> for Numbering {
126 fn from(pattern: NumberingPattern) -> Self {
127 Self::Pattern(pattern)
128 }
129}
130
131cast! {
132 Numbering,
133 self => match self {
134 Self::Pattern(pattern) => pattern.into_value(),
135 Self::Func(func) => func.into_value(),
136 },
137 v: NumberingPattern => Self::Pattern(v),
138 v: Func => Self::Func(v),
139}
140
141#[derive(Debug, Clone, Eq, PartialEq, Hash)]
151pub struct NumberingPattern {
152 pub pieces: EcoVec<(EcoString, NumberingKind)>,
153 pub suffix: EcoString,
154 trimmed: bool,
155}
156
157impl NumberingPattern {
158 pub fn apply(&self, numbers: &[usize]) -> EcoString {
160 let mut fmt = EcoString::new();
161 let mut numbers = numbers.iter();
162
163 for (i, ((prefix, kind), &n)) in self.pieces.iter().zip(&mut numbers).enumerate()
164 {
165 if i > 0 || !self.trimmed {
166 fmt.push_str(prefix);
167 }
168 fmt.push_str(&kind.apply(n));
169 }
170
171 for ((prefix, kind), &n) in self.pieces.last().into_iter().cycle().zip(numbers) {
172 if prefix.is_empty() {
173 fmt.push_str(&self.suffix);
174 } else {
175 fmt.push_str(prefix);
176 }
177 fmt.push_str(&kind.apply(n));
178 }
179
180 if !self.trimmed {
181 fmt.push_str(&self.suffix);
182 }
183
184 fmt
185 }
186
187 pub fn apply_kth(&self, k: usize, number: usize) -> EcoString {
189 let mut fmt = EcoString::new();
190 if let Some((prefix, _)) = self.pieces.first() {
191 fmt.push_str(prefix);
192 }
193 if let Some((_, kind)) = self
194 .pieces
195 .iter()
196 .chain(self.pieces.last().into_iter().cycle())
197 .nth(k)
198 {
199 fmt.push_str(&kind.apply(number));
200 }
201 fmt.push_str(&self.suffix);
202 fmt
203 }
204
205 pub fn pieces(&self) -> usize {
207 self.pieces.len()
208 }
209}
210
211impl FromStr for NumberingPattern {
212 type Err = &'static str;
213
214 fn from_str(pattern: &str) -> Result<Self, Self::Err> {
215 let mut pieces = EcoVec::new();
216 let mut handled = 0;
217
218 for (i, c) in pattern.char_indices() {
219 let Some(kind) = NumberingKind::from_char(c) else {
220 continue;
221 };
222
223 let prefix = pattern[handled..i].into();
224 pieces.push((prefix, kind));
225 handled = c.len_utf8() + i;
226 }
227
228 let suffix = pattern[handled..].into();
229 if pieces.is_empty() {
230 return Err("invalid numbering pattern");
231 }
232
233 Ok(Self { pieces, suffix, trimmed: false })
234 }
235}
236
237cast! {
238 NumberingPattern,
239 self => {
240 let mut pat = EcoString::new();
241 for (prefix, kind) in &self.pieces {
242 pat.push_str(prefix);
243 pat.push(kind.to_char());
244 }
245 pat.push_str(&self.suffix);
246 pat.into_value()
247 },
248 v: Str => v.parse()?,
249}
250
251#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
253pub enum NumberingKind {
254 Arabic,
256 LowerLatin,
258 UpperLatin,
260 LowerRoman,
262 UpperRoman,
264 LowerGreek,
266 UpperGreek,
268 Symbol,
271 Hebrew,
273 LowerSimplifiedChinese,
276 UpperSimplifiedChinese,
279 #[allow(unused)]
284 LowerTraditionalChinese,
287 #[allow(unused)]
288 UpperTraditionalChinese,
291 HiraganaAiueo,
293 HiraganaIroha,
295 KatakanaAiueo,
297 KatakanaIroha,
299 KoreanJamo,
301 KoreanSyllable,
303 EasternArabic,
305 EasternArabicPersian,
307 DevanagariNumber,
309 BengaliNumber,
311 BengaliLetter,
313 CircledNumber,
315 DoubleCircledNumber,
317}
318
319impl NumberingKind {
320 pub fn from_char(c: char) -> Option<Self> {
322 Some(match c {
323 '1' => NumberingKind::Arabic,
324 'a' => NumberingKind::LowerLatin,
325 'A' => NumberingKind::UpperLatin,
326 'i' => NumberingKind::LowerRoman,
327 'I' => NumberingKind::UpperRoman,
328 'α' => NumberingKind::LowerGreek,
329 'Α' => NumberingKind::UpperGreek,
330 '*' => NumberingKind::Symbol,
331 'א' => NumberingKind::Hebrew,
332 '一' => NumberingKind::LowerSimplifiedChinese,
333 '壹' => NumberingKind::UpperSimplifiedChinese,
334 'あ' => NumberingKind::HiraganaAiueo,
335 'い' => NumberingKind::HiraganaIroha,
336 'ア' => NumberingKind::KatakanaAiueo,
337 'イ' => NumberingKind::KatakanaIroha,
338 'ㄱ' => NumberingKind::KoreanJamo,
339 '가' => NumberingKind::KoreanSyllable,
340 '\u{0661}' => NumberingKind::EasternArabic,
341 '\u{06F1}' => NumberingKind::EasternArabicPersian,
342 '\u{0967}' => NumberingKind::DevanagariNumber,
343 '\u{09E7}' => NumberingKind::BengaliNumber,
344 '\u{0995}' => NumberingKind::BengaliLetter,
345 '①' => NumberingKind::CircledNumber,
346 '⓵' => NumberingKind::DoubleCircledNumber,
347 _ => return None,
348 })
349 }
350
351 pub fn to_char(self) -> char {
353 match self {
354 Self::Arabic => '1',
355 Self::LowerLatin => 'a',
356 Self::UpperLatin => 'A',
357 Self::LowerRoman => 'i',
358 Self::UpperRoman => 'I',
359 Self::LowerGreek => 'α',
360 Self::UpperGreek => 'Α',
361 Self::Symbol => '*',
362 Self::Hebrew => 'א',
363 Self::LowerSimplifiedChinese | Self::LowerTraditionalChinese => '一',
364 Self::UpperSimplifiedChinese | Self::UpperTraditionalChinese => '壹',
365 Self::HiraganaAiueo => 'あ',
366 Self::HiraganaIroha => 'い',
367 Self::KatakanaAiueo => 'ア',
368 Self::KatakanaIroha => 'イ',
369 Self::KoreanJamo => 'ㄱ',
370 Self::KoreanSyllable => '가',
371 Self::EasternArabic => '\u{0661}',
372 Self::EasternArabicPersian => '\u{06F1}',
373 Self::DevanagariNumber => '\u{0967}',
374 Self::BengaliNumber => '\u{09E7}',
375 Self::BengaliLetter => '\u{0995}',
376 Self::CircledNumber => '①',
377 Self::DoubleCircledNumber => '⓵',
378 }
379 }
380
381 pub fn apply(self, n: usize) -> EcoString {
383 match self {
384 Self::Arabic => eco_format!("{n}"),
385 Self::LowerRoman => roman_numeral(n, Case::Lower),
386 Self::UpperRoman => roman_numeral(n, Case::Upper),
387 Self::LowerGreek => greek_numeral(n, Case::Lower),
388 Self::UpperGreek => greek_numeral(n, Case::Upper),
389 Self::Symbol => {
390 if n == 0 {
391 return '-'.into();
392 }
393
394 const SYMBOLS: &[char] = &['*', '†', '‡', '§', '¶', '‖'];
395 let symbol = SYMBOLS[(n - 1) % SYMBOLS.len()];
396 let amount = ((n - 1) / SYMBOLS.len()) + 1;
397 std::iter::repeat(symbol).take(amount).collect()
398 }
399 Self::Hebrew => hebrew_numeral(n),
400
401 Self::LowerLatin => zeroless(
402 [
403 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
404 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
405 ],
406 n,
407 ),
408 Self::UpperLatin => zeroless(
409 [
410 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
411 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
412 ],
413 n,
414 ),
415 Self::HiraganaAiueo => zeroless(
416 [
417 'あ', 'い', 'う', 'え', 'お', 'か', 'き', 'く', 'け', 'こ', 'さ',
418 'し', 'す', 'せ', 'そ', 'た', 'ち', 'つ', 'て', 'と', 'な', 'に',
419 'ぬ', 'ね', 'の', 'は', 'ひ', 'ふ', 'へ', 'ほ', 'ま', 'み', 'む',
420 'め', 'も', 'や', 'ゆ', 'よ', 'ら', 'り', 'る', 'れ', 'ろ', 'わ',
421 'を', 'ん',
422 ],
423 n,
424 ),
425 Self::HiraganaIroha => zeroless(
426 [
427 'い', 'ろ', 'は', 'に', 'ほ', 'へ', 'と', 'ち', 'り', 'ぬ', 'る',
428 'を', 'わ', 'か', 'よ', 'た', 'れ', 'そ', 'つ', 'ね', 'な', 'ら',
429 'む', 'う', 'ゐ', 'の', 'お', 'く', 'や', 'ま', 'け', 'ふ', 'こ',
430 'え', 'て', 'あ', 'さ', 'き', 'ゆ', 'め', 'み', 'し', 'ゑ', 'ひ',
431 'も', 'せ', 'す',
432 ],
433 n,
434 ),
435 Self::KatakanaAiueo => zeroless(
436 [
437 'ア', 'イ', 'ウ', 'エ', 'オ', 'カ', 'キ', 'ク', 'ケ', 'コ', 'サ',
438 'シ', 'ス', 'セ', 'ソ', 'タ', 'チ', 'ツ', 'テ', 'ト', 'ナ', 'ニ',
439 'ヌ', 'ネ', 'ノ', 'ハ', 'ヒ', 'フ', 'ヘ', 'ホ', 'マ', 'ミ', 'ム',
440 'メ', 'モ', 'ヤ', 'ユ', 'ヨ', 'ラ', 'リ', 'ル', 'レ', 'ロ', 'ワ',
441 'ヲ', 'ン',
442 ],
443 n,
444 ),
445 Self::KatakanaIroha => zeroless(
446 [
447 'イ', 'ロ', 'ハ', 'ニ', 'ホ', 'ヘ', 'ト', 'チ', 'リ', 'ヌ', 'ル',
448 'ヲ', 'ワ', 'カ', 'ヨ', 'タ', 'レ', 'ソ', 'ツ', 'ネ', 'ナ', 'ラ',
449 'ム', 'ウ', 'ヰ', 'ノ', 'オ', 'ク', 'ヤ', 'マ', 'ケ', 'フ', 'コ',
450 'エ', 'テ', 'ア', 'サ', 'キ', 'ユ', 'メ', 'ミ', 'シ', 'ヱ', 'ヒ',
451 'モ', 'セ', 'ス',
452 ],
453 n,
454 ),
455 Self::KoreanJamo => zeroless(
456 [
457 'ㄱ', 'ㄴ', 'ㄷ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅅ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ',
458 'ㅌ', 'ㅍ', 'ㅎ',
459 ],
460 n,
461 ),
462 Self::KoreanSyllable => zeroless(
463 [
464 '가', '나', '다', '라', '마', '바', '사', '아', '자', '차', '카',
465 '타', '파', '하',
466 ],
467 n,
468 ),
469 Self::BengaliLetter => zeroless(
470 [
471 'ক', 'খ', 'গ', 'ঘ', 'ঙ', 'চ', 'ছ', 'জ', 'ঝ', 'ঞ', 'ট', 'ঠ', 'ড', 'ঢ',
472 'ণ', 'ত', 'থ', 'দ', 'ধ', 'ন', 'প', 'ফ', 'ব', 'ভ', 'ম', 'য', 'র', 'ল',
473 'শ', 'ষ', 'স', 'হ',
474 ],
475 n,
476 ),
477 Self::CircledNumber => zeroless(
478 [
479 '①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩', '⑪', '⑫', '⑬', '⑭',
480 '⑮', '⑯', '⑰', '⑱', '⑲', '⑳', '㉑', '㉒', '㉓', '㉔', '㉕', '㉖',
481 '㉗', '㉘', '㉙', '㉚', '㉛', '㉜', '㉝', '㉞', '㉟', '㊱', '㊲',
482 '㊳', '㊴', '㊵', '㊶', '㊷', '㊸', '㊹', '㊺', '㊻', '㊼', '㊽',
483 '㊾', '㊿',
484 ],
485 n,
486 ),
487 Self::DoubleCircledNumber => {
488 zeroless(['⓵', '⓶', '⓷', '⓸', '⓹', '⓺', '⓻', '⓼', '⓽', '⓾'], n)
489 }
490
491 Self::LowerSimplifiedChinese => {
492 usize_to_chinese(ChineseVariant::Simple, ChineseCase::Lower, n).into()
493 }
494 Self::UpperSimplifiedChinese => {
495 usize_to_chinese(ChineseVariant::Simple, ChineseCase::Upper, n).into()
496 }
497 Self::LowerTraditionalChinese => {
498 usize_to_chinese(ChineseVariant::Traditional, ChineseCase::Lower, n)
499 .into()
500 }
501 Self::UpperTraditionalChinese => {
502 usize_to_chinese(ChineseVariant::Traditional, ChineseCase::Upper, n)
503 .into()
504 }
505
506 Self::EasternArabic => decimal('\u{0660}', n),
507 Self::EasternArabicPersian => decimal('\u{06F0}', n),
508 Self::DevanagariNumber => decimal('\u{0966}', n),
509 Self::BengaliNumber => decimal('\u{09E6}', n),
510 }
511 }
512}
513
514fn hebrew_numeral(mut n: usize) -> EcoString {
516 if n == 0 {
517 return '-'.into();
518 }
519 let mut fmt = EcoString::new();
520 'outer: for (name, value) in [
521 ('ת', 400),
522 ('ש', 300),
523 ('ר', 200),
524 ('ק', 100),
525 ('צ', 90),
526 ('פ', 80),
527 ('ע', 70),
528 ('ס', 60),
529 ('נ', 50),
530 ('מ', 40),
531 ('ל', 30),
532 ('כ', 20),
533 ('י', 10),
534 ('ט', 9),
535 ('ח', 8),
536 ('ז', 7),
537 ('ו', 6),
538 ('ה', 5),
539 ('ד', 4),
540 ('ג', 3),
541 ('ב', 2),
542 ('א', 1),
543 ] {
544 while n >= value {
545 match n {
546 15 => fmt.push_str("ט״ו"),
547 16 => fmt.push_str("ט״ז"),
548 _ => {
549 let append_geresh = n == value && fmt.is_empty();
550 if n == value && !fmt.is_empty() {
551 fmt.push('״');
552 }
553 fmt.push(name);
554 if append_geresh {
555 fmt.push('׳');
556 }
557
558 n -= value;
559 continue;
560 }
561 }
562 break 'outer;
563 }
564 }
565 fmt
566}
567
568fn roman_numeral(mut n: usize, case: Case) -> EcoString {
570 if n == 0 {
571 return match case {
572 Case::Lower => 'n'.into(),
573 Case::Upper => 'N'.into(),
574 };
575 }
576
577 let mut fmt = EcoString::new();
580 for &(name, value) in &[
581 ("M̅", 1000000),
582 ("D̅", 500000),
583 ("C̅", 100000),
584 ("L̅", 50000),
585 ("X̅", 10000),
586 ("V̅", 5000),
587 ("I̅V̅", 4000),
588 ("M", 1000),
589 ("CM", 900),
590 ("D", 500),
591 ("CD", 400),
592 ("C", 100),
593 ("XC", 90),
594 ("L", 50),
595 ("XL", 40),
596 ("X", 10),
597 ("IX", 9),
598 ("V", 5),
599 ("IV", 4),
600 ("I", 1),
601 ] {
602 while n >= value {
603 n -= value;
604 for c in name.chars() {
605 match case {
606 Case::Lower => fmt.extend(c.to_lowercase()),
607 Case::Upper => fmt.push(c),
608 }
609 }
610 }
611 }
612
613 fmt
614}
615
616fn greek_numeral(n: usize, case: Case) -> EcoString {
626 let thousands = [
627 ["͵α", "͵Α"],
628 ["͵β", "͵Β"],
629 ["͵γ", "͵Γ"],
630 ["͵δ", "͵Δ"],
631 ["͵ε", "͵Ε"],
632 ["͵ϛ", "͵Ϛ"],
633 ["͵ζ", "͵Ζ"],
634 ["͵η", "͵Η"],
635 ["͵θ", "͵Θ"],
636 ];
637 let hundreds = [
638 ["ρ", "Ρ"],
639 ["σ", "Σ"],
640 ["τ", "Τ"],
641 ["υ", "Υ"],
642 ["φ", "Φ"],
643 ["χ", "Χ"],
644 ["ψ", "Ψ"],
645 ["ω", "Ω"],
646 ["ϡ", "Ϡ"],
647 ];
648 let tens = [
649 ["ι", "Ι"],
650 ["κ", "Κ"],
651 ["λ", "Λ"],
652 ["μ", "Μ"],
653 ["ν", "Ν"],
654 ["ξ", "Ξ"],
655 ["ο", "Ο"],
656 ["π", "Π"],
657 ["ϙ", "Ϟ"],
658 ];
659 let ones = [
660 ["α", "Α"],
661 ["β", "Β"],
662 ["γ", "Γ"],
663 ["δ", "Δ"],
664 ["ε", "Ε"],
665 ["ϛ", "Ϛ"],
666 ["ζ", "Ζ"],
667 ["η", "Η"],
668 ["θ", "Θ"],
669 ];
670
671 if n == 0 {
672 return '𐆊'.into();
674 }
675
676 let mut fmt = EcoString::new();
677 let case = match case {
678 Case::Lower => 0,
679 Case::Upper => 1,
680 };
681
682 let mut decimal_digits: Vec<usize> = Vec::new();
684 let mut n = n;
685 while n > 0 {
686 decimal_digits.push(n % 10);
687 n /= 10;
688 }
689
690 while decimal_digits.len() % 4 != 0 {
692 decimal_digits.push(0);
693 }
694 decimal_digits.reverse();
695
696 let mut m_power = decimal_digits.len() / 4;
697
698 let get_m_prefix = |m_power: usize| {
702 if m_power == 0 {
703 None
704 } else {
705 assert!(m_power <= 9);
706 Some(ones[m_power - 1][0])
708 }
709 };
710
711 let mut previous_has_number = false;
712 for chunk in decimal_digits.chunks_exact(4) {
713 assert_eq!(chunk.len(), 4);
715
716 m_power = m_power.saturating_sub(1);
717
718 let (th, h, t, o) = (chunk[0], chunk[1], chunk[2], chunk[3]);
720 if th + h + t + o == 0 {
721 continue;
722 }
723
724 if previous_has_number {
725 fmt.push_str(", ");
726 }
727
728 if let Some(m_prefix) = get_m_prefix(m_power) {
729 fmt.push_str(m_prefix);
730 fmt.push_str("Μ");
731 }
732 if th != 0 {
733 let thousand_digit = thousands[th - 1][case];
734 fmt.push_str(thousand_digit);
735 }
736 if h != 0 {
737 let hundred_digit = hundreds[h - 1][case];
738 fmt.push_str(hundred_digit);
739 }
740 if t != 0 {
741 let ten_digit = tens[t - 1][case];
742 fmt.push_str(ten_digit);
743 }
744 if o != 0 {
745 let one_digit = ones[o - 1][case];
746 fmt.push_str(one_digit);
747 }
748 if th == 0 {
750 fmt.push_str("ʹ");
751 }
752 previous_has_number = true;
753 }
754 fmt
755}
756
757fn zeroless<const N_DIGITS: usize>(
782 alphabet: [char; N_DIGITS],
783 mut n: usize,
784) -> EcoString {
785 if n == 0 {
786 return '-'.into();
787 }
788 let mut cs = EcoString::new();
789 while n > 0 {
790 n -= 1;
791 cs.push(alphabet[n % N_DIGITS]);
792 n /= N_DIGITS;
793 }
794 cs.chars().rev().collect()
795}
796
797fn decimal(start: char, mut n: usize) -> EcoString {
801 if n == 0 {
802 return start.into();
803 }
804 let mut cs = EcoString::new();
805 while n > 0 {
806 cs.push(char::from_u32((start as u32) + ((n % 10) as u32)).unwrap());
807 n /= 10;
808 }
809 cs.chars().rev().collect()
810}