Skip to main content

latexml_post/
radix.rs

1//! Radix conversion utilities for generating labels and ID suffixes.
2//!
3//! Port of `LaTeXML::Util::Radix`.
4//! Generates labels in the sequence: a,b,...,z,aa,ab,...,az,ba,...,zz,aaa,...
5
6const LOWER: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
7const UPPER: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
8
9const GREEK_LOWER: &[char] = &[
10  '\u{03B1}', '\u{03B2}', '\u{03B3}', '\u{03B4}', '\u{03B5}', '\u{03B6}', '\u{03B7}', '\u{03B8}',
11  '\u{03B9}', '\u{03BA}', '\u{03BB}', '\u{03BC}', '\u{03BD}', '\u{03BE}', '\u{03BF}', '\u{03C0}',
12  '\u{03C1}', '\u{03C3}', '\u{03C4}', '\u{03C5}', '\u{03C6}', '\u{03C7}', '\u{03C8}', '\u{03C9}',
13];
14
15const GREEK_UPPER: &[char] = &[
16  '\u{0391}', '\u{0392}', '\u{0393}', '\u{0394}', '\u{0395}', '\u{0396}', '\u{0397}', '\u{0398}',
17  '\u{0399}', '\u{039A}', '\u{039B}', '\u{039C}', '\u{039D}', '\u{039E}', '\u{039F}', '\u{03A0}',
18  '\u{03A1}', '\u{03A3}', '\u{03A4}', '\u{03A5}', '\u{03A6}', '\u{03A7}', '\u{03A8}', '\u{03A9}',
19];
20
21/// Generic radix formatting: convert a 1-based number into a string
22/// using the given symbol set.
23///
24/// Produces: symbols[0], symbols[1], ..., symbols[n-1],
25///           symbols[0]symbols[0], symbols[0]symbols[1], ...
26fn radix_format_chars(mut number: u32, symbols: &[char]) -> String {
27  let mut result = String::new();
28  let base = symbols.len() as u32;
29  while number > 0 {
30    let idx = ((number - 1) % base) as usize;
31    result.insert(0, symbols[idx]);
32    number = (number - 1) / base;
33  }
34  result
35}
36
37fn radix_format_bytes(mut number: u32, symbols: &[u8]) -> String {
38  let mut result = Vec::new();
39  let base = symbols.len() as u32;
40  while number > 0 {
41    let idx = ((number - 1) % base) as usize;
42    result.insert(0, symbols[idx]);
43    number = (number - 1) / base;
44  }
45  String::from_utf8(result).unwrap()
46}
47
48/// Convert number to lowercase latin letters: 1→a, 2→b, ..., 26→z, 27→aa, ...
49pub fn radix_alpha(n: u32) -> String { radix_format_bytes(n, LOWER) }
50
51/// Convert number to uppercase latin letters: 1→A, 2→B, ..., 26→Z, 27→AA, ...
52pub fn radix_alpha_upper(n: u32) -> String { radix_format_bytes(n, UPPER) }
53
54/// Convert number to lowercase greek letters.
55pub fn radix_greek(n: u32) -> String { radix_format_chars(n, GREEK_LOWER) }
56
57/// Convert number to uppercase greek letters.
58pub fn radix_greek_upper(n: u32) -> String { radix_format_chars(n, GREEK_UPPER) }
59
60/// Convert number to lowercase roman numerals.
61pub fn radix_roman(mut n: u32) -> String {
62  let letters = ['i', 'v', 'x', 'l', 'c', 'd', 'm'];
63  let mut s = String::new();
64  let mut div: u32 = 1000;
65
66  // `n >= div` (not `n > div`) — `radix_roman(1000)` must produce "m".
67  if n >= div {
68    for _ in 0..(n / div) {
69      s.push('m');
70    }
71  }
72  let mut p: i32 = 4;
73
74  loop {
75    n %= div;
76    if n == 0 {
77      break;
78    }
79    div /= 10;
80    if div == 0 {
81      break;
82    }
83    let mut d = n / div;
84    if d % 5 == 4 {
85      s.push(letters[p as usize]);
86      d += 1;
87    }
88    if d > 4 {
89      s.push(letters[(p + (d / 5) as i32) as usize]);
90      d %= 5;
91    }
92    for _ in 0..d {
93      s.push(letters[p as usize]);
94    }
95    p -= 2;
96    if p < 0 {
97      break;
98    }
99  }
100  s
101}
102
103/// Convert number to uppercase roman numerals.
104pub fn radix_roman_upper(n: u32) -> String { radix_roman(n).to_uppercase() }
105
106#[cfg(test)]
107mod tests {
108  use super::*;
109
110  #[test]
111  fn test_radix_alpha() {
112    assert_eq!(radix_alpha(1), "a");
113    assert_eq!(radix_alpha(2), "b");
114    assert_eq!(radix_alpha(26), "z");
115    assert_eq!(radix_alpha(27), "aa");
116    assert_eq!(radix_alpha(28), "ab");
117    assert_eq!(radix_alpha(52), "az");
118    assert_eq!(radix_alpha(53), "ba");
119    assert_eq!(radix_alpha(702), "zz");
120    assert_eq!(radix_alpha(703), "aaa");
121  }
122
123  #[test]
124  fn test_radix_roman() {
125    assert_eq!(radix_roman(1), "i");
126    assert_eq!(radix_roman(4), "iv");
127    assert_eq!(radix_roman(9), "ix");
128    assert_eq!(radix_roman(14), "xiv");
129    assert_eq!(radix_roman(42), "xlii");
130    assert_eq!(radix_roman(1000), "m"); // boundary that was broken (`n > div` bug)
131    assert_eq!(radix_roman(1999), "mcmxcix");
132    assert_eq!(radix_roman(2000), "mm");
133    assert_eq!(radix_roman(3999), "mmmcmxcix");
134  }
135
136  #[test]
137  fn test_radix_alpha_upper() {
138    assert_eq!(radix_alpha_upper(1), "A");
139    assert_eq!(radix_alpha_upper(26), "Z");
140    assert_eq!(radix_alpha_upper(27), "AA");
141    assert_eq!(radix_alpha_upper(703), "AAA");
142  }
143
144  #[test]
145  fn test_radix_alpha_upper_vs_lower_case() {
146    // For all n > 0, upper version is lower version uppercased.
147    for n in 1..100 {
148      assert_eq!(
149        radix_alpha_upper(n),
150        radix_alpha(n).to_uppercase(),
151        "divergence at n={n}"
152      );
153    }
154  }
155
156  #[test]
157  fn test_radix_greek_basic() {
158    assert_eq!(radix_greek(1), "α");
159    assert_eq!(radix_greek(2), "β");
160    // 24th symbol is ω (the medial sigma was skipped).
161    assert_eq!(radix_greek(24), "ω");
162    // n=25 wraps: αα.
163    assert_eq!(radix_greek(25), "αα");
164  }
165
166  #[test]
167  fn test_radix_greek_upper_basic() {
168    assert_eq!(radix_greek_upper(1), "Α");
169    assert_eq!(radix_greek_upper(24), "Ω");
170    assert_eq!(radix_greek_upper(25), "ΑΑ");
171  }
172
173  #[test]
174  fn test_radix_roman_upper() {
175    assert_eq!(radix_roman_upper(1), "I");
176    assert_eq!(radix_roman_upper(4), "IV");
177    assert_eq!(radix_roman_upper(1999), "MCMXCIX");
178  }
179
180  #[test]
181  fn test_radix_alpha_zero_is_empty() {
182    // Zero produces empty; values > 0 always produce non-empty.
183    assert_eq!(radix_alpha(0), "");
184    assert_eq!(radix_alpha_upper(0), "");
185    assert_eq!(radix_greek(0), "");
186    assert_eq!(radix_roman(0), "");
187  }
188}