Skip to main content

latexml_core/util/
radix.rs

1//! simple radix conversion utilities
2//!
3//! This module provides some simple utilities for radix conversion.
4//======================================================================
5// This isn't really any sort of general purpose Radix module,
6// probably the term "radix" is a misnomer here!
7// It is used to primarily generate labels, or uniquifying suffixes to make ID's,
8// Bibtex year tags like 2013a, etc  using alphabetic letters, or
9// perhaps greek, or even from a set of symbols.
10//
11// The general idea is simply to generate labels in the sequence:
12//   a,b,c,...y,z,aa,ab,ac,...az,ba,...zy,zz,aaa,aab,.... and so on.
13// I would assume that the usual advise is that it is bad style to pass,
14// or even approach "z";  However, this is an automaton, and things happen.
15//======================================================================
16
17const LETTERS: &[char] = &[
18  'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
19  't', 'u', 'v', 'w', 'x', 'y', 'z',
20];
21const UP_LETTERS: &[char] = &[
22  'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
23  'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
24];
25const GREEK: &[char] = &[
26  '\u{03B1}', '\u{03B2}', '\u{03B3}', '\u{03B4}', '\u{03B5}', '\u{03B6}', '\u{03B7}', '\u{03B8}',
27  '\u{03B9}', '\u{03BA}', '\u{03BB}', '\u{03BC}', '\u{03BD}', '\u{03BE}', '\u{03BF}', '\u{03C0}',
28  '\u{03C1}', '\u{03C3}', '\u{03C4}', '\u{03C5}', '\u{03C6}', '\u{03C7}', '\u{03C8}', '\u{03C9}',
29];
30const UP_GREEK: &[char] = &[
31  '\u{0391}', '\u{0392}', '\u{0393}', '\u{0394}', '\u{0395}', '\u{0396}', '\u{0397}', '\u{0398}',
32  '\u{0399}', '\u{039A}', '\u{039B}', '\u{039C}', '\u{039D}', '\u{039E}', '\u{039F}', '\u{03A0}',
33  '\u{03A1}', '\u{03A3}', '\u{03A4}', '\u{03A5}', '\u{03A6}', '\u{03A7}', '\u{03A8}', '\u{03A9}',
34];
35
36/// (Internal) Converts the number into one of the char symbols
37pub fn radix_format(mut number: i64, symbols: &[char]) -> String {
38  let mut chars: Vec<char> = Vec::new();
39  let max = symbols.len() as i64;
40  while number > 0 {
41    let index = (number - 1) % max;
42    chars.push(symbols[index as usize]);
43    number = (number - 1) / max;
44  }
45  chars.into_iter().rev().collect()
46}
47/// (Internal) Converts the number into one of the str symbols
48pub fn radix_format_str(mut number: i64, symbols: &[&str]) -> String {
49  let mut parts: Vec<&str> = Vec::new();
50  let max = symbols.len() as i64;
51  while number > 0 {
52    let index = (number - 1) % max;
53    parts.push(symbols[index as usize]);
54    number = (number - 1) / max;
55  }
56  parts.into_iter().rev().collect()
57}
58
59/// converts the number into one or more lowercase latin letters
60pub fn radix_alpha(n: i64) -> String { radix_format(n, LETTERS) }
61/// converts the number into one or more uppercase latin letters
62pub fn radix_up_alpha(n: i64) -> String { radix_format(n, UP_LETTERS) }
63/// converts the number into one or more lowercase greek letters
64pub fn radix_greek(n: i64) -> String { radix_format(n, GREEK) }
65
66/// converts the number into one or more uppercase greek letters
67pub fn radix_up_greek(n: i64) -> String { radix_format(n, UP_GREEK) }
68
69// Dumb place for this, but where else...
70// Note: This is one 'The TeX Way'! (bah!! hint: try a large number)
71// namely, it's very limited.... what happened to my much-improved version?
72const RMLETTERS: &[char] = &['i', 'v', 'x', 'l', 'c', 'd', 'm']; // [CONSTANT]
73/// converts the number as a lowercase roman numeral
74///
75/// Perl parity: `roman(n)` returns the empty string for n <= 0. TeX's
76/// `\romannumeral` also produces no output for non-positive input.
77pub fn radix_roman(mut n: i64) -> String {
78  if n <= 0 {
79    return String::new();
80  }
81  let mut s = String::new();
82  let mut div = 1000;
83  if n >= div {
84    s = (0..(n / div)).map(|_| 'm').collect::<String>();
85  }
86
87  let mut p = 4;
88  loop {
89    n %= div;
90    if n == 0 {
91      break;
92    }
93    div /= 10;
94    let mut d: i64 = n / div;
95    if d % 5 == 4 {
96      s.push(RMLETTERS[p]);
97      d += 1;
98    }
99    if d > 4 {
100      let index: usize = p + (d / 5) as usize;
101      s.push(RMLETTERS[index]);
102      d %= 5;
103    }
104    if d != 0 {
105      let ps = (0..d).map(|_| RMLETTERS[p]).collect::<String>();
106      s.push_str(&ps);
107    }
108    if p > 1 {
109      p -= 2;
110    } else {
111      p = 0;
112    }
113  }
114  s
115}
116
117/// converts the number as a uppercase roman numeral
118pub fn radix_up_roman(n: i64) -> String { radix_roman(n).to_uppercase() }
119
120#[cfg(test)]
121mod tests {
122  use super::*;
123
124  #[test]
125  fn roman_non_positive_empty() {
126    assert_eq!(radix_roman(0), "");
127    assert_eq!(radix_roman(-1), "");
128    assert_eq!(radix_roman(i64::MIN), "");
129  }
130
131  #[test]
132  fn roman_basic_cases() {
133    assert_eq!(radix_roman(1), "i");
134    assert_eq!(radix_roman(4), "iv");
135    assert_eq!(radix_roman(9), "ix");
136    assert_eq!(radix_roman(1000), "m");
137    assert_eq!(radix_roman(1999), "mcmxcix");
138  }
139
140  #[test]
141  fn alpha_edge_cases() {
142    assert_eq!(radix_alpha(0), "");
143    assert_eq!(radix_alpha(-5), "");
144    assert_eq!(radix_alpha(1), "a");
145    assert_eq!(radix_alpha(26), "z");
146    assert_eq!(radix_alpha(27), "aa");
147  }
148
149  #[test]
150  fn alpha_alphabet_progression() {
151    assert_eq!(radix_alpha(28), "ab");
152    assert_eq!(radix_alpha(52), "az");
153    assert_eq!(radix_alpha(53), "ba");
154    // 26*27 = 702 should be the last two-letter (zz).
155    assert_eq!(radix_alpha(26 * 26 + 26), "zz");
156    assert_eq!(radix_alpha(26 * 26 + 26 + 1), "aaa");
157  }
158
159  #[test]
160  fn up_alpha_basic() {
161    assert_eq!(radix_up_alpha(0), "");
162    assert_eq!(radix_up_alpha(1), "A");
163    assert_eq!(radix_up_alpha(26), "Z");
164    assert_eq!(radix_up_alpha(27), "AA");
165  }
166
167  #[test]
168  fn up_alpha_vs_alpha_case_only() {
169    // For all n, up_alpha(n) should equal alpha(n).to_uppercase().
170    for n in 0..60 {
171      assert_eq!(
172        radix_up_alpha(n),
173        radix_alpha(n).to_uppercase(),
174        "divergence at {n}"
175      );
176    }
177  }
178
179  #[test]
180  fn greek_basic() {
181    assert_eq!(radix_greek(0), "");
182    assert_eq!(radix_greek(1), "α");
183    assert_eq!(radix_greek(24), "ω"); // ω is the 24th (skip final-sigma)
184    assert_eq!(radix_greek(25), "αα");
185  }
186
187  #[test]
188  fn up_greek_basic() {
189    assert_eq!(radix_up_greek(0), "");
190    assert_eq!(radix_up_greek(1), "Α");
191    assert_eq!(radix_up_greek(24), "Ω");
192  }
193
194  #[test]
195  fn up_roman_cases() {
196    assert_eq!(radix_up_roman(0), "");
197    assert_eq!(radix_up_roman(1), "I");
198    assert_eq!(radix_up_roman(4), "IV");
199    assert_eq!(radix_up_roman(1000), "M");
200    assert_eq!(radix_up_roman(1999), "MCMXCIX");
201  }
202
203  #[test]
204  fn radix_format_str_multi_char_symbols() {
205    // radix_format_str takes &[&str], useful for abbreviations.
206    let syms = &["one", "two", "three"];
207    assert_eq!(radix_format_str(0, syms), "");
208    assert_eq!(radix_format_str(1, syms), "one");
209    assert_eq!(radix_format_str(3, syms), "three");
210    // n=4 overflows into second digit: (4-1)%3=0→"one", (4-1)/3=1; (1-1)%3=0→"one" → "oneone"
211    assert_eq!(radix_format_str(4, syms), "oneone");
212  }
213
214  #[test]
215  fn radix_format_custom_symbols() {
216    // Single-char radix_format: same generation as alpha but with a
217    // user-chosen alphabet.
218    let syms = &['A', 'B'];
219    assert_eq!(radix_format(1, syms), "A");
220    assert_eq!(radix_format(2, syms), "B");
221    // n=3 → (3-1)%2=0→'A', (3-1)/2=1→'A' → "AA"
222    assert_eq!(radix_format(3, syms), "AA");
223    assert_eq!(radix_format(4, syms), "AB");
224  }
225}