typst_library/model/
numbering.rs

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/// Applies a numbering to a sequence of numbers.
15///
16/// A numbering defines how a sequence of numbers should be displayed as
17/// content. It is defined either through a pattern string or an arbitrary
18/// function.
19///
20/// A numbering pattern consists of counting symbols, for which the actual
21/// number is substituted, their prefixes, and one suffix. The prefixes and the
22/// suffix are repeated as-is.
23///
24/// # Example
25/// ```example
26/// #numbering("1.1)", 1, 2, 3) \
27/// #numbering("1.a.i", 1, 2) \
28/// #numbering("I – 1", 12, 2) \
29/// #numbering(
30///   (..nums) => nums
31///     .pos()
32///     .map(str)
33///     .join(".") + ")",
34///   1, 2, 3,
35/// )
36/// ```
37///
38/// # Numbering patterns and numbering functions
39/// There are multiple instances where you can provide a numbering pattern or
40/// function in Typst. For example, when defining how to number
41/// [headings]($heading) or [figures]($figure). Every time, the expected format
42/// is the same as the one described below for the
43/// [`numbering`]($numbering.numbering) parameter.
44///
45/// The following example illustrates that a numbering function is just a
46/// regular [function] that accepts numbers and returns [`content`].
47/// ```example
48/// #let unary(.., last) = "|" * last
49/// #set heading(numbering: unary)
50/// = First heading
51/// = Second heading
52/// = Third heading
53/// ```
54#[func]
55pub fn numbering(
56    engine: &mut Engine,
57    context: Tracked<Context>,
58    /// Defines how the numbering works.
59    ///
60    /// **Counting symbols** are `1`, `a`, `A`, `i`, `I`, `α`, `Α`, `一`, `壹`,
61    /// `あ`, `い`, `ア`, `イ`, `א`, `가`, `ㄱ`, `*`, `١`, `۱`, `१`, `১`, `ক`,
62    /// `①`, and `⓵`. They are replaced by the number in the sequence,
63    /// preserving the original case.
64    ///
65    /// The `*` character means that symbols should be used to count, in the
66    /// order of `*`, `†`, `‡`, `§`, `¶`, `‖`. If there are more than six
67    /// items, the number is represented using repeated symbols.
68    ///
69    /// **Suffixes** are all characters after the last counting symbol. They are
70    /// repeated as-is at the end of any rendered number.
71    ///
72    /// **Prefixes** are all characters that are neither counting symbols nor
73    /// suffixes. They are repeated as-is at in front of their rendered
74    /// equivalent of their counting symbol.
75    ///
76    /// This parameter can also be an arbitrary function that gets each number
77    /// as an individual argument. When given a function, the `numbering`
78    /// function just forwards the arguments to that function. While this is not
79    /// particularly useful in itself, it means that you can just give arbitrary
80    /// numberings to the `numbering` function without caring whether they are
81    /// defined as a pattern or function.
82    numbering: Numbering,
83    /// The numbers to apply the numbering to. Must be positive.
84    ///
85    /// If `numbering` is a pattern and more numbers than counting symbols are
86    /// given, the last counting symbol with its prefix is repeated.
87    #[variadic]
88    numbers: Vec<usize>,
89) -> SourceResult<Value> {
90    numbering.apply(engine, context, &numbers)
91}
92
93/// How to number a sequence of things.
94#[derive(Debug, Clone, PartialEq, Hash)]
95pub enum Numbering {
96    /// A pattern with prefix, numbering, lower / upper case and suffix.
97    Pattern(NumberingPattern),
98    /// A closure mapping from an item's number to content.
99    Func(Func),
100}
101
102impl Numbering {
103    /// Apply the pattern to the given numbers.
104    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    /// Trim the prefix suffix if this is a pattern.
117    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/// How to turn a number into text.
142///
143/// A pattern consists of a prefix, followed by one of the counter symbols (see
144/// [`numbering()`] docs), and then a suffix.
145///
146/// Examples of valid patterns:
147/// - `1)`
148/// - `a.`
149/// - `(I)`
150#[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    /// Apply the pattern to the given number.
159    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    /// Apply only the k-th segment of the pattern to a number.
188    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    /// How many counting symbols this pattern has.
206    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/// Different kinds of numberings.
252#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
253pub enum NumberingKind {
254    /// Arabic numerals (1, 2, 3, etc.).
255    Arabic,
256    /// Lowercase Latin letters (a, b, c, etc.). Items beyond z use base-26.
257    LowerLatin,
258    /// Uppercase Latin letters (A, B, C, etc.). Items beyond Z use base-26.
259    UpperLatin,
260    /// Lowercase Roman numerals (i, ii, iii, etc.).
261    LowerRoman,
262    /// Uppercase Roman numerals (I, II, III, etc.).
263    UpperRoman,
264    /// Lowercase Greek numerals (Α, Β, Γ, etc.).
265    LowerGreek,
266    /// Uppercase Greek numerals (α, β, γ, etc.).
267    UpperGreek,
268    /// Paragraph/note-like symbols: *, †, ‡, §, ¶, and ‖. Further items use
269    /// repeated symbols.
270    Symbol,
271    /// Hebrew numerals, including Geresh/Gershayim.
272    Hebrew,
273    /// Simplified Chinese standard numerals. This corresponds to the
274    /// `ChineseCase::Lower` variant.
275    LowerSimplifiedChinese,
276    /// Simplified Chinese "banknote" numerals. This corresponds to the
277    /// `ChineseCase::Upper` variant.
278    UpperSimplifiedChinese,
279    // TODO: Pick the numbering pattern based on languages choice.
280    // As the first character of Simplified and Traditional Chinese numbering
281    // are the same, we are unable to determine if the context requires
282    // Simplified or Traditional by only looking at this character.
283    #[allow(unused)]
284    /// Traditional Chinese standard numerals. This corresponds to the
285    /// `ChineseCase::Lower` variant.
286    LowerTraditionalChinese,
287    #[allow(unused)]
288    /// Traditional Chinese "banknote" numerals. This corresponds to the
289    /// `ChineseCase::Upper` variant.
290    UpperTraditionalChinese,
291    /// Hiragana in the gojūon order. Includes n but excludes wi and we.
292    HiraganaAiueo,
293    /// Hiragana in the iroha order. Includes wi and we but excludes n.
294    HiraganaIroha,
295    /// Katakana in the gojūon order. Includes n but excludes wi and we.
296    KatakanaAiueo,
297    /// Katakana in the iroha order. Includes wi and we but excludes n.
298    KatakanaIroha,
299    /// Korean jamo (ㄱ, ㄴ, ㄷ, etc.).
300    KoreanJamo,
301    /// Korean syllables (가, 나, 다, etc.).
302    KoreanSyllable,
303    /// Eastern Arabic numerals, used in some Arabic-speaking countries.
304    EasternArabic,
305    /// The variant of Eastern Arabic numerals used in Persian and Urdu.
306    EasternArabicPersian,
307    /// Devanagari numerals.
308    DevanagariNumber,
309    /// Bengali numerals.
310    BengaliNumber,
311    /// Bengali letters (ক, খ, গ, ...কক, কখ etc.).
312    BengaliLetter,
313    /// Circled numbers (①, ②, ③, etc.), up to 50.
314    CircledNumber,
315    /// Double-circled numbers (⓵, ⓶, ⓷, etc.), up to 10.
316    DoubleCircledNumber,
317}
318
319impl NumberingKind {
320    /// Create a numbering kind from a representative character.
321    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    /// The representative character for this numbering kind.
352    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    /// Apply the numbering to the given number.
382    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
514/// Stringify an integer to a Hebrew number.
515fn 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
568/// Stringify an integer to a Roman numeral.
569fn 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    // Adapted from Yann Villessuzanne's roman.rs under the
578    // Unlicense, at https://github.com/linfir/roman.rs/
579    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
616/// Stringify an integer to Greek numbers.
617///
618/// Greek numbers use the Greek Alphabet to represent numbers; it is based on 10
619/// (decimal). Here we implement the single digit M power representation from
620/// [The Greek Number Converter][convert] and also described in
621/// [Greek Numbers][numbers].
622///
623/// [converter]: https://www.russellcottrell.com/greek/utilities/GreekNumberConverter.htm
624/// [numbers]: https://mathshistory.st-andrews.ac.uk/HistTopics/Greek_numbers/
625fn 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        // Greek Zero Sign
673        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    // Extract a list of decimal digits from the number
683    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    // Pad the digits with leading zeros to ensure we can form groups of 4
691    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    // M are used to represent 10000, M_power = 2 means 10000^2 = 10000 0000
699    // The prefix of M is also made of Greek numerals but only be single digits, so it is 9 at max. This enables us
700    // to represent up to (10000)^(9 + 1) - 1 = 10^40 -1  (9,999,999,999,999,999,999,999,999,999,999,999,999,999)
701    let get_m_prefix = |m_power: usize| {
702        if m_power == 0 {
703            None
704        } else {
705            assert!(m_power <= 9);
706            // the prefix of M is a single digit lowercase
707            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        // chunk must be exact 4 item
714        assert_eq!(chunk.len(), 4);
715
716        m_power = m_power.saturating_sub(1);
717
718        // `th`ousan, `h`undred, `t`en and `o`ne
719        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 we do not have thousan, we need to append 'ʹ' at the end.
749        if th == 0 {
750            fmt.push_str("ʹ");
751        }
752        previous_has_number = true;
753    }
754    fmt
755}
756
757/// Stringify a number using a base-N counting system with no zero digit.
758///
759/// This is best explained by example. Suppose our digits are 'A', 'B', and 'C'.
760/// We would get the following:
761///
762/// ```text
763///  1 =>   "A"
764///  2 =>   "B"
765///  3 =>   "C"
766///  4 =>  "AA"
767///  5 =>  "AB"
768///  6 =>  "AC"
769///  7 =>  "BA"
770///  8 =>  "BB"
771///  9 =>  "BC"
772/// 10 =>  "CA"
773/// 11 =>  "CB"
774/// 12 =>  "CC"
775/// 13 => "AAA"
776///    etc.
777/// ```
778///
779/// You might be familiar with this scheme from the way spreadsheet software
780/// tends to label its columns.
781fn 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
797/// Stringify a number using a base-10 counting system with a zero digit.
798///
799/// This function assumes that the digits occupy contiguous codepoints.
800fn 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}