1use alloc::string::String;
13
14use azul_css::props::style::lists::StyleListStyleType;
15
16#[must_use]
20pub fn format_counter(value: i32, style: StyleListStyleType) -> String {
21 match style {
22 StyleListStyleType::None => String::new(),
23 StyleListStyleType::Disc => "•".to_string(),
24 StyleListStyleType::Circle => "◦".to_string(),
25 StyleListStyleType::Square => "▪".to_string(),
26 StyleListStyleType::Decimal => value.to_string(),
27 StyleListStyleType::DecimalLeadingZero => format!("{value:02}"),
28 StyleListStyleType::LowerAlpha => decimal_fallback(value, with_sign(value, |n| to_alphabetic(n, false))),
29 StyleListStyleType::UpperAlpha => decimal_fallback(value, with_sign(value, |n| to_alphabetic(n, true))),
30 StyleListStyleType::LowerRoman => with_sign(value, |n| to_roman(n, false)),
31 StyleListStyleType::UpperRoman => with_sign(value, |n| to_roman(n, true)),
32 StyleListStyleType::LowerGreek => decimal_fallback(value, with_sign(value, |n| to_greek(n, false))),
33 StyleListStyleType::UpperGreek => decimal_fallback(value, with_sign(value, |n| to_greek(n, true))),
34 }
35}
36
37fn decimal_fallback(value: i32, formatted: String) -> String {
41 if formatted.is_empty() {
42 value.to_string()
43 } else {
44 formatted
45 }
46}
47
48#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] fn with_sign<F: Fn(usize) -> String>(value: i32, format: F) -> String {
56 if value < 0 {
57 let magnitude = i64::from(value).unsigned_abs() as usize;
58 format!("-{}", format(magnitude))
59 } else {
60 format(value as usize)
61 }
62}
63
64#[allow(clippy::cast_possible_truncation)] pub(crate) fn to_alphabetic(mut num: usize, uppercase: bool) -> String {
69 if num == 0 {
70 return String::new();
71 }
72
73 let mut result = String::new();
74 let base = if uppercase { b'A' } else { b'a' };
75
76 while num > 0 {
77 let remainder = ((num - 1) % 26) as u8;
78 result.insert(0, (base + remainder) as char);
79 num = (num - 1) / 26;
80 }
81
82 result
83}
84
85pub(crate) fn to_roman(mut num: usize, uppercase: bool) -> String {
89 const MAX_ROMAN: usize = 3999;
90 if num == 0 {
91 return "0".to_string();
92 }
93 if num > MAX_ROMAN {
94 return num.to_string();
96 }
97
98 let numerals = [
99 (1000, "m"),
100 (900, "cm"),
101 (500, "d"),
102 (400, "cd"),
103 (100, "c"),
104 (90, "xc"),
105 (50, "l"),
106 (40, "xl"),
107 (10, "x"),
108 (9, "ix"),
109 (5, "v"),
110 (4, "iv"),
111 (1, "i"),
112 ];
113
114 let mut result = String::new();
115 for (value, numeral) in &numerals {
116 while num >= *value {
117 result.push_str(numeral);
118 num -= value;
119 }
120 }
121
122 if uppercase {
123 result.to_uppercase()
124 } else {
125 result
126 }
127}
128
129pub(crate) fn to_greek(num: usize, uppercase: bool) -> String {
134 const GREEK_LOWER: &[char] = &[
135 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'ο', 'π', 'ρ', 'σ',
136 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω',
137 ];
138 const GREEK_UPPER: &[char] = &[
139 'Α', 'Β', 'Γ', 'Δ', 'Ε', 'Ζ', 'Η', 'Θ', 'Ι', 'Κ', 'Λ', 'Μ', 'Ν', 'Ξ', 'Ο', 'Π', 'Ρ', 'Σ',
140 'Τ', 'Υ', 'Φ', 'Χ', 'Ψ', 'Ω',
141 ];
142
143 if num == 0 {
144 return String::new();
145 }
146
147 let letters = if uppercase { GREEK_UPPER } else { GREEK_LOWER };
148
149 if num <= letters.len() {
150 return letters[num - 1].to_string();
151 }
152
153 let mut result = String::new();
154 let mut remaining = num;
155 while remaining > 0 {
156 remaining -= 1;
157 result.insert(0, letters[remaining % letters.len()]);
158 remaining /= letters.len();
159 }
160 result
161}
162
163#[cfg(test)]
164#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
165mod autotest_generated {
166 use super::*;
167
168 const ALL_STYLES: [StyleListStyleType; 12] = [
175 StyleListStyleType::None,
176 StyleListStyleType::Disc,
177 StyleListStyleType::Circle,
178 StyleListStyleType::Square,
179 StyleListStyleType::Decimal,
180 StyleListStyleType::DecimalLeadingZero,
181 StyleListStyleType::LowerRoman,
182 StyleListStyleType::UpperRoman,
183 StyleListStyleType::LowerGreek,
184 StyleListStyleType::UpperGreek,
185 StyleListStyleType::LowerAlpha,
186 StyleListStyleType::UpperAlpha,
187 ];
188
189 const EDGE_VALUES: [i32; 12] = [
192 i32::MIN,
193 i32::MIN + 1,
194 -4000,
195 -3999,
196 -27,
197 -1,
198 0,
199 1,
200 26,
201 3999,
202 4000,
203 i32::MAX,
204 ];
205
206 const GREEK_LOWER_LETTERS: [char; 24] = [
207 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'ο', 'π', 'ρ', 'σ',
208 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω',
209 ];
210 const GREEK_UPPER_LETTERS: [char; 24] = [
211 'Α', 'Β', 'Γ', 'Δ', 'Ε', 'Ζ', 'Η', 'Θ', 'Ι', 'Κ', 'Λ', 'Μ', 'Ν', 'Ξ', 'Ο', 'Π', 'Ρ', 'Σ',
212 'Τ', 'Υ', 'Φ', 'Χ', 'Ψ', 'Ω',
213 ];
214
215 fn decode_alphabetic(s: &str, uppercase: bool) -> Option<u128> {
218 if s.is_empty() {
219 return None;
220 }
221 let base = if uppercase { b'A' } else { b'a' };
222 let mut acc: u128 = 0;
223 for b in s.bytes() {
224 if b < base || b >= base + 26 {
225 return None;
226 }
227 acc = acc * 26 + u128::from(b - base + 1);
228 }
229 Some(acc)
230 }
231
232 fn decode_greek(s: &str, uppercase: bool) -> Option<u128> {
234 let letters = if uppercase {
235 &GREEK_UPPER_LETTERS
236 } else {
237 &GREEK_LOWER_LETTERS
238 };
239 if s.is_empty() {
240 return None;
241 }
242 let mut acc: u128 = 0;
243 for c in s.chars() {
244 let idx = letters.iter().position(|l| *l == c)?;
245 acc = acc * 24 + (idx as u128 + 1);
246 }
247 Some(acc)
248 }
249
250 fn decode_roman(s: &str) -> Option<u32> {
252 fn digit(c: char) -> Option<i64> {
253 match c {
254 'i' => Some(1),
255 'v' => Some(5),
256 'x' => Some(10),
257 'l' => Some(50),
258 'c' => Some(100),
259 'd' => Some(500),
260 'm' => Some(1000),
261 _ => None,
262 }
263 }
264 if s.is_empty() {
265 return None;
266 }
267 let digits: Option<Vec<i64>> = s.chars().map(digit).collect();
268 let digits = digits?;
269 let mut total: i64 = 0;
272 for (i, d) in digits.iter().enumerate() {
273 if digits[i + 1..].iter().any(|next| next > d) {
275 total -= *d;
276 } else {
277 total += *d;
278 }
279 }
280 u32::try_from(total).ok()
281 }
282
283 #[test]
288 fn to_alphabetic_zero_is_empty_not_a_panic() {
289 assert_eq!(to_alphabetic(0, false), "");
292 assert_eq!(to_alphabetic(0, true), "");
293 }
294
295 #[test]
296 fn to_alphabetic_known_values() {
297 assert_eq!(to_alphabetic(1, false), "a");
298 assert_eq!(to_alphabetic(26, false), "z");
299 assert_eq!(to_alphabetic(27, false), "aa");
301 assert_eq!(to_alphabetic(28, false), "ab");
302 assert_eq!(to_alphabetic(52, false), "az");
303 assert_eq!(to_alphabetic(53, false), "ba");
304 assert_eq!(to_alphabetic(702, false), "zz");
305 assert_eq!(to_alphabetic(703, false), "aaa");
306 }
307
308 #[test]
309 fn to_alphabetic_uppercase_only_shifts_case() {
310 for n in 1..=1000usize {
311 let lower = to_alphabetic(n, false);
312 let upper = to_alphabetic(n, true);
313 assert_eq!(upper, lower.to_uppercase(), "case mismatch at {n}");
314 assert!(
315 upper.bytes().all(|b| b.is_ascii_uppercase()),
316 "non-uppercase byte at {n}: {upper}"
317 );
318 assert!(
319 lower.bytes().all(|b| b.is_ascii_lowercase()),
320 "non-lowercase byte at {n}: {lower}"
321 );
322 }
323 }
324
325 #[test]
326 fn to_alphabetic_round_trips_through_an_independent_decoder() {
327 for n in 1..=5000u128 {
328 for uppercase in [false, true] {
329 let encoded = to_alphabetic(n as usize, uppercase);
330 assert_eq!(
331 decode_alphabetic(&encoded, uppercase),
332 Some(n),
333 "round-trip failed for {n} (uppercase={uppercase}) -> {encoded}"
334 );
335 }
336 }
337 }
338
339 fn assert_all_distinct(markers: &[String], what: &str) {
341 let mut sorted: Vec<&String> = markers.iter().collect();
342 sorted.sort();
343 for pair in sorted.windows(2) {
344 assert_ne!(pair[0], pair[1], "duplicate {what} marker: {}", pair[0]);
345 }
346 }
347
348 #[test]
349 fn to_alphabetic_is_injective() {
350 let markers: Vec<String> = (1..=2000usize).map(|n| to_alphabetic(n, false)).collect();
352 assert_all_distinct(&markers, "alphabetic");
353 }
354
355 #[test]
356 fn to_alphabetic_usize_max_terminates_and_stays_ascii() {
357 let lower = to_alphabetic(usize::MAX, false);
360 let upper = to_alphabetic(usize::MAX, true);
361 assert!(!lower.is_empty());
362 assert!(lower.bytes().all(|b: u8| b.is_ascii_lowercase()));
363 assert!(upper.bytes().all(|b: u8| b.is_ascii_uppercase()));
364 assert_eq!(decode_alphabetic(&lower, false), Some(usize::MAX as u128));
366 assert_eq!(decode_alphabetic(&upper, true), Some(usize::MAX as u128));
367 assert_eq!(lower.len(), upper.len());
368 }
369
370 #[test]
371 fn to_alphabetic_magnitude_of_i32_min_does_not_panic() {
372 let magnitude = 2_147_483_648usize;
374 let s = to_alphabetic(magnitude, false);
375 assert!(!s.is_empty());
376 assert_eq!(decode_alphabetic(&s, false), Some(magnitude as u128));
377 }
378
379 #[test]
384 fn to_roman_zero_falls_back_to_decimal_zero() {
385 assert_eq!(to_roman(0, false), "0");
387 assert_eq!(to_roman(0, true), "0");
388 }
389
390 #[test]
391 fn to_roman_known_values() {
392 assert_eq!(to_roman(1, false), "i");
393 assert_eq!(to_roman(4, false), "iv");
394 assert_eq!(to_roman(9, false), "ix");
395 assert_eq!(to_roman(14, false), "xiv");
396 assert_eq!(to_roman(40, false), "xl");
397 assert_eq!(to_roman(90, false), "xc");
398 assert_eq!(to_roman(400, false), "cd");
399 assert_eq!(to_roman(900, false), "cm");
400 assert_eq!(to_roman(1990, false), "mcmxc");
401 assert_eq!(to_roman(2024, false), "mmxxiv");
402 assert_eq!(to_roman(3999, false), "mmmcmxcix");
403 assert_eq!(to_roman(2024, true), "MMXXIV");
404 assert_eq!(to_roman(3999, true), "MMMCMXCIX");
405 }
406
407 #[test]
408 fn to_roman_at_and_past_the_3999_cliff() {
409 assert_eq!(to_roman(3999, false), "mmmcmxcix");
411 assert_eq!(to_roman(4000, false), "4000");
414 assert_eq!(to_roman(4000, true), "4000");
415 assert_eq!(to_roman(4001, false), "4001");
416 }
417
418 #[test]
419 fn to_roman_usize_max_degrades_to_decimal() {
420 assert_eq!(to_roman(usize::MAX, false), usize::MAX.to_string());
421 assert_eq!(to_roman(usize::MAX, true), usize::MAX.to_string());
422 assert_eq!(to_roman(2_147_483_648, false), "2147483648");
424 }
425
426 #[test]
427 fn to_roman_round_trips_over_the_whole_representable_range() {
428 for n in 1..=3999u32 {
429 let lower = to_roman(n as usize, false);
430 let upper = to_roman(n as usize, true);
431 assert_eq!(
432 decode_roman(&lower),
433 Some(n),
434 "round-trip failed for {n} -> {lower}"
435 );
436 assert_eq!(upper, lower.to_uppercase(), "case mismatch at {n}");
437 assert!(
439 !lower.contains("iiii")
440 && !lower.contains("xxxx")
441 && !lower.contains("cccc")
442 && !lower.contains("mmmm"),
443 "malformed numeral at {n}: {lower}"
444 );
445 assert!(lower.bytes().all(|b| b"ivxlcdm".contains(&b)));
446 }
447 }
448
449 #[test]
454 fn to_greek_zero_is_empty_not_a_panic() {
455 assert_eq!(to_greek(0, false), "");
456 assert_eq!(to_greek(0, true), "");
457 }
458
459 #[test]
460 fn to_greek_known_values_and_wrap_boundary() {
461 assert_eq!(to_greek(1, false), "α");
462 assert_eq!(to_greek(2, false), "β");
463 assert_eq!(to_greek(24, false), "ω");
464 assert_eq!(to_greek(25, false), "αα");
466 assert_eq!(to_greek(26, false), "αβ");
467 assert_eq!(to_greek(48, false), "αω");
468 assert_eq!(to_greek(49, false), "βα");
469 assert_eq!(to_greek(1, true), "Α");
470 assert_eq!(to_greek(24, true), "Ω");
471 assert_eq!(to_greek(25, true), "ΑΑ");
472 }
473
474 #[test]
475 fn to_greek_emits_multibyte_chars_without_slicing_bugs() {
476 let s = to_greek(25, false);
480 assert_eq!(s.chars().count(), 2);
481 assert_eq!(s.len(), 4);
482 assert!(s.chars().all(|c| GREEK_LOWER_LETTERS.contains(&c)));
483 assert!(s.is_char_boundary(0) && s.is_char_boundary(2) && s.is_char_boundary(4));
484 }
485
486 #[test]
487 fn to_greek_round_trips_through_an_independent_decoder() {
488 for n in 1..=5000u128 {
489 for uppercase in [false, true] {
490 let encoded = to_greek(n as usize, uppercase);
491 assert_eq!(
492 decode_greek(&encoded, uppercase),
493 Some(n),
494 "round-trip failed for {n} (uppercase={uppercase}) -> {encoded}"
495 );
496 }
497 }
498 }
499
500 #[test]
501 fn to_greek_is_injective() {
502 let markers: Vec<String> = (1..=2000usize).map(|n| to_greek(n, true)).collect();
503 assert_all_distinct(&markers, "greek");
504 }
505
506 #[test]
507 fn to_greek_usize_max_terminates_and_stays_in_the_alphabet() {
508 let lower = to_greek(usize::MAX, false);
510 let upper = to_greek(usize::MAX, true);
511 assert!(!lower.is_empty());
512 assert!(lower.chars().all(|c| GREEK_LOWER_LETTERS.contains(&c)));
513 assert!(upper.chars().all(|c| GREEK_UPPER_LETTERS.contains(&c)));
514 assert_eq!(decode_greek(&lower, false), Some(usize::MAX as u128));
516 assert_eq!(decode_greek(&upper, true), Some(usize::MAX as u128));
517 assert_eq!(lower.chars().count(), upper.chars().count());
518 }
519
520 #[test]
525 fn with_sign_passes_the_magnitude_not_a_wrapped_cast() {
526 assert_eq!(with_sign(-3, |n| n.to_string()), "-3");
529 assert_eq!(with_sign(-1, |n| n.to_string()), "-1");
530 assert_eq!(with_sign(0, |n| n.to_string()), "0");
531 assert_eq!(with_sign(1, |n| n.to_string()), "1");
532 assert_eq!(with_sign(i32::MAX, |n| n.to_string()), "2147483647");
533 }
534
535 #[test]
536 fn with_sign_handles_i32_min_without_overflow() {
537 assert_eq!(with_sign(i32::MIN, |n| n.to_string()), "-2147483648");
539 assert_eq!(with_sign(i32::MIN + 1, |n| n.to_string()), "-2147483647");
540 }
541
542 #[test]
543 fn with_sign_zero_is_unsigned() {
544 let s = with_sign(0, |n| to_alphabetic(n, false));
546 assert!(!s.starts_with('-'), "produced a signed zero: {s}");
547 assert_eq!(s, "");
548 }
549
550 #[test]
551 fn with_sign_prefixes_exactly_one_minus() {
552 for v in [-1, -26, -3999, -4000, i32::MIN] {
553 let s = with_sign(v, |n| to_roman(n, false));
554 assert!(s.starts_with('-'), "missing sign for {v}: {s}");
555 assert_eq!(s.matches('-').count(), 1, "double sign for {v}: {s}");
556 assert!(s.len() > 1, "sign with no magnitude for {v}");
557 }
558 }
559
560 #[test]
561 fn with_sign_is_transparent_to_the_formatter_output() {
562 assert_eq!(with_sign(-5, |_| String::new()), "-");
565 assert_eq!(with_sign(5, |_| String::new()), "");
566 assert_eq!(with_sign(-5, |_| "αβγ".to_string()), "-αβγ");
568 }
569
570 #[test]
571 fn with_sign_negation_is_symmetric_across_the_range() {
572 for v in [1i32, 2, 26, 27, 3999, 4000, i32::MAX] {
573 let pos = with_sign(v, |n| to_alphabetic(n, false));
574 let neg = with_sign(-v, |n| to_alphabetic(n, false));
575 assert_eq!(neg, format!("-{pos}"), "asymmetric at {v}");
576 }
577 }
578
579 #[test]
584 fn decimal_fallback_replaces_empty_with_decimal() {
585 assert_eq!(decimal_fallback(0, String::new()), "0");
586 assert_eq!(decimal_fallback(-1, String::new()), "-1");
587 assert_eq!(decimal_fallback(i32::MAX, String::new()), "2147483647");
588 assert_eq!(decimal_fallback(i32::MIN, String::new()), "-2147483648");
589 }
590
591 #[test]
592 fn decimal_fallback_passes_non_empty_through_untouched() {
593 assert_eq!(decimal_fallback(5, "a".to_string()), "a");
596 assert_eq!(decimal_fallback(0, "z".to_string()), "z");
597 assert_eq!(decimal_fallback(0, "α".to_string()), "α");
598 assert_eq!(decimal_fallback(7, " ".to_string()), " ");
600 assert_eq!(decimal_fallback(7, "\0".to_string()), "\0");
602 }
603
604 #[test]
605 fn decimal_fallback_output_is_never_blank_for_any_i32() {
606 for v in EDGE_VALUES {
607 assert!(
608 !decimal_fallback(v, String::new()).is_empty(),
609 "blank marker for {v}"
610 );
611 }
612 }
613
614 #[test]
619 fn format_counter_no_panic_on_edge_values_for_every_style() {
620 for style in ALL_STYLES {
621 for v in EDGE_VALUES {
622 let s = format_counter(v, style);
623 if style == StyleListStyleType::None {
624 assert!(s.is_empty(), "`none` must render nothing, got {s:?}");
625 } else {
626 assert!(!s.is_empty(), "blank marker for {v} in {style:?}");
628 assert!(
630 !s.chars().all(char::is_whitespace),
631 "whitespace-only marker for {v} in {style:?}"
632 );
633 }
634 }
635 }
636 }
637
638 #[test]
639 fn format_counter_is_deterministic() {
640 for style in ALL_STYLES {
641 for v in EDGE_VALUES {
642 assert_eq!(format_counter(v, style), format_counter(v, style));
643 }
644 }
645 }
646
647 #[test]
648 fn format_counter_default_style_is_disc_and_ignores_the_value() {
649 assert_eq!(format_counter(0, StyleListStyleType::default()), "•");
650 for v in EDGE_VALUES {
651 assert_eq!(format_counter(v, StyleListStyleType::default()), "•");
652 }
653 }
654
655 #[test]
656 fn format_counter_bullet_styles_ignore_the_value() {
657 for (style, expected) in [
658 (StyleListStyleType::Disc, "•"),
659 (StyleListStyleType::Circle, "◦"),
660 (StyleListStyleType::Square, "▪"),
661 ] {
662 for v in EDGE_VALUES {
663 let s = format_counter(v, style);
664 assert_eq!(s, expected, "bullet changed with value {v}");
665 assert_eq!(s.chars().count(), 1);
666 }
667 }
668 }
669
670 #[test]
671 fn format_counter_decimal_matches_i32_display() {
672 for v in EDGE_VALUES {
673 assert_eq!(format_counter(v, StyleListStyleType::Decimal), v.to_string());
674 }
675 }
676
677 #[test]
678 fn format_counter_decimal_leading_zero_pads_single_digits() {
679 assert_eq!(
680 format_counter(0, StyleListStyleType::DecimalLeadingZero),
681 "00"
682 );
683 assert_eq!(
684 format_counter(5, StyleListStyleType::DecimalLeadingZero),
685 "05"
686 );
687 assert_eq!(
688 format_counter(9, StyleListStyleType::DecimalLeadingZero),
689 "09"
690 );
691 assert_eq!(
692 format_counter(10, StyleListStyleType::DecimalLeadingZero),
693 "10"
694 );
695 assert_eq!(
696 format_counter(100, StyleListStyleType::DecimalLeadingZero),
697 "100"
698 );
699 assert_eq!(
700 format_counter(i32::MAX, StyleListStyleType::DecimalLeadingZero),
701 "2147483647"
702 );
703 }
704
705 #[test]
706 fn format_counter_decimal_leading_zero_negative_current_behavior() {
707 assert_eq!(
712 format_counter(-5, StyleListStyleType::DecimalLeadingZero),
713 "-5"
714 );
715 assert_eq!(
716 format_counter(i32::MIN, StyleListStyleType::DecimalLeadingZero),
717 "-2147483648"
718 );
719 }
720
721 #[test]
722 fn format_counter_alpha_known_values_and_zero_fallback() {
723 assert_eq!(format_counter(1, StyleListStyleType::LowerAlpha), "a");
724 assert_eq!(format_counter(26, StyleListStyleType::LowerAlpha), "z");
725 assert_eq!(format_counter(27, StyleListStyleType::LowerAlpha), "aa");
726 assert_eq!(format_counter(1, StyleListStyleType::UpperAlpha), "A");
727 assert_eq!(format_counter(27, StyleListStyleType::UpperAlpha), "AA");
728 assert_eq!(format_counter(0, StyleListStyleType::LowerAlpha), "0");
730 assert_eq!(format_counter(0, StyleListStyleType::UpperAlpha), "0");
731 assert_eq!(format_counter(-1, StyleListStyleType::LowerAlpha), "-a");
733 assert_eq!(format_counter(-3, StyleListStyleType::UpperAlpha), "-C");
734 }
735
736 #[test]
737 fn format_counter_roman_known_values_and_limits() {
738 assert_eq!(format_counter(1, StyleListStyleType::LowerRoman), "i");
739 assert_eq!(format_counter(4, StyleListStyleType::LowerRoman), "iv");
740 assert_eq!(format_counter(2024, StyleListStyleType::UpperRoman), "MMXXIV");
741 assert_eq!(format_counter(3999, StyleListStyleType::LowerRoman), "mmmcmxcix");
742 assert_eq!(format_counter(4000, StyleListStyleType::LowerRoman), "4000");
744 assert_eq!(format_counter(4000, StyleListStyleType::UpperRoman), "4000");
745 assert_eq!(format_counter(0, StyleListStyleType::LowerRoman), "0");
747 assert_eq!(format_counter(0, StyleListStyleType::UpperRoman), "0");
748 assert_eq!(format_counter(-3, StyleListStyleType::LowerRoman), "-iii");
749 assert_eq!(format_counter(-14, StyleListStyleType::UpperRoman), "-XIV");
750 }
751
752 #[test]
753 fn format_counter_roman_at_i32_min_matches_signed_decimal() {
754 assert_eq!(
758 format_counter(i32::MIN, StyleListStyleType::LowerRoman),
759 "-2147483648"
760 );
761 assert_eq!(
762 format_counter(i32::MIN, StyleListStyleType::LowerRoman),
763 format_counter(i32::MIN, StyleListStyleType::Decimal)
764 );
765 }
766
767 #[test]
768 fn format_counter_greek_known_values_and_zero_fallback() {
769 assert_eq!(format_counter(1, StyleListStyleType::LowerGreek), "α");
770 assert_eq!(format_counter(24, StyleListStyleType::LowerGreek), "ω");
771 assert_eq!(format_counter(25, StyleListStyleType::LowerGreek), "αα");
772 assert_eq!(format_counter(1, StyleListStyleType::UpperGreek), "Α");
773 assert_eq!(format_counter(24, StyleListStyleType::UpperGreek), "Ω");
774 assert_eq!(format_counter(0, StyleListStyleType::LowerGreek), "0");
776 assert_eq!(format_counter(0, StyleListStyleType::UpperGreek), "0");
777 assert_eq!(format_counter(-2, StyleListStyleType::LowerGreek), "-β");
778 assert_eq!(format_counter(-1, StyleListStyleType::UpperGreek), "-Α");
779 }
780
781 #[test]
782 fn format_counter_negative_is_positive_with_a_minus_for_letter_styles() {
783 for style in [
786 StyleListStyleType::LowerAlpha,
787 StyleListStyleType::UpperAlpha,
788 StyleListStyleType::LowerRoman,
789 StyleListStyleType::UpperRoman,
790 StyleListStyleType::LowerGreek,
791 StyleListStyleType::UpperGreek,
792 ] {
793 for v in [1i32, 2, 3, 24, 25, 26, 27, 3999] {
794 let pos = format_counter(v, style);
795 let neg = format_counter(-v, style);
796 assert_eq!(neg, format!("-{pos}"), "asymmetric at {v} in {style:?}");
797 assert!(!pos.starts_with('-'), "positive gained a sign at {v}");
798 }
799 }
800 }
801
802 #[test]
803 fn format_counter_letter_styles_never_leak_huge_unsigned_markers() {
804 for style in [
807 StyleListStyleType::LowerAlpha,
808 StyleListStyleType::UpperGreek,
809 ] {
810 let s = format_counter(-1, style);
811 assert_eq!(s.chars().count(), 2, "suspiciously long marker: {s}");
812 }
813 }
814
815 #[test]
816 fn format_counter_marker_length_stays_bounded_at_i32_extremes() {
817 for style in ALL_STYLES {
819 for v in [i32::MIN, i32::MAX] {
820 let s = format_counter(v, style);
821 assert!(
822 s.chars().count() <= 32,
823 "marker for {v} in {style:?} is {} chars: {s}",
824 s.chars().count()
825 );
826 }
827 }
828 }
829}