1use crate::error::{Error, Result};
17use std::fmt;
18use std::str::FromStr;
19
20const BASE: u8 = 36;
22
23#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
29pub struct Rank(String);
30
31impl Rank {
32 #[must_use]
34 pub fn as_str(&self) -> &str {
35 &self.0
36 }
37
38 pub fn parse(s: &str) -> Result<Rank> {
63 let bytes = s.as_bytes();
64 let valid = !bytes.is_empty()
65 && bytes.iter().all(|&b| digit_value(b).is_some())
66 && bytes.last() != Some(&b'0');
67 if !valid {
68 return Err(Error::InvalidRank(s.to_string()));
69 }
70 Ok(Rank(s.to_string()))
71 }
72
73 #[must_use = "handle an invalid rank bound"]
79 pub fn between(lo: Option<&Rank>, hi: Option<&Rank>) -> Result<Rank> {
80 if matches!((lo, hi), (Some(l), Some(h)) if l >= h) {
81 let lower = lo.map_or_else(|| "<open>".to_string(), ToString::to_string);
82 let upper = hi.map_or_else(|| "<open>".to_string(), ToString::to_string);
83 return Err(Error::InvalidRank(format!(
84 "between requires lo < hi (lo={lower:?}, hi={upper:?})"
85 )));
86 }
87 Ok(Self::between_unchecked(lo, hi))
88 }
89
90 fn between_unchecked(lo: Option<&Rank>, hi: Option<&Rank>) -> Rank {
92 let lo_digits: Vec<u8> = lo.map(|r| digits_of(r.as_str())).unwrap_or_default();
93 let (hi_int, hi_digits): (u8, Vec<u8>) = match hi {
95 Some(r) => (0, digits_of(r.as_str())),
96 None => (1, Vec::new()),
97 };
98
99 let (int_sum, frac_sum) = add_frac(&lo_digits, &hi_digits, hi_int);
100 let mut mid = half_frac(int_sum, &frac_sum);
101
102 while mid.last() == Some(&0) {
104 mid.pop();
105 }
106 Rank(digits_to_string(&mid))
107 }
108
109 #[must_use]
123 pub fn after(prev: Option<&Rank>) -> Rank {
124 let digits = match prev {
125 None => vec![BASE / 2],
127 Some(r) => {
128 let mut d = digits_of(r.as_str());
129 match d.iter().rposition(|&v| v < BASE - 1) {
130 Some(i) => {
131 d[i] += 1;
132 d.truncate(i + 1);
133 d
134 }
135 None => {
137 d.push(BASE / 2);
138 d
139 }
140 }
141 }
142 };
143 Rank(digits_to_string(&digits))
144 }
145
146 #[must_use]
154 pub fn before(next: Option<&Rank>) -> Rank {
155 Self::between_unchecked(None, next)
158 }
159
160 #[must_use]
169 pub fn rebalance(count: usize) -> Vec<Rank> {
170 if count == 0 {
171 return Vec::new();
172 }
173
174 let (width, capacity) = rebalance_layout(count);
175 let count_u128 = count as u128;
176 let mut out = Vec::with_capacity(count);
177 for index in 0..count {
178 let ordinal = ((index as u128 + 1) * capacity) / (count_u128 + 1);
181 out.push(rank_at_ordinal(ordinal, width));
182 }
183 out
184 }
185
186 #[must_use]
194 pub fn rebalance_width(count: usize) -> usize {
195 if count == 0 {
196 return 0;
197 }
198 rebalance_layout(count).0
199 }
200}
201
202fn rebalance_layout(count: usize) -> (usize, u128) {
205 let target = count as u128;
206 let mut width = 1;
207 let mut prefix_space = 1u128;
208 loop {
209 let capacity = prefix_space.saturating_mul(u128::from(BASE - 1));
210 if capacity >= target {
211 return (width, capacity);
212 }
213 prefix_space = prefix_space.saturating_mul(u128::from(BASE));
214 width += 1;
215 }
216}
217
218fn rank_at_ordinal(ordinal: u128, width: usize) -> Rank {
224 let last_digit = ordinal % u128::from(BASE - 1) + 1;
225 let mut prefix = ordinal / u128::from(BASE - 1);
226 let mut digits = vec![0; width];
227 for position in (0..width - 1).rev() {
228 digits[position] = (prefix % u128::from(BASE)) as u8;
229 prefix /= u128::from(BASE);
230 }
231 digits[width - 1] = last_digit as u8;
232 Rank(digits_to_string(&digits))
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
237pub struct RankStats {
238 pub count: usize,
240 pub max_len: usize,
242 pub total_len: usize,
244}
245
246impl RankStats {
247 pub fn collect<'a, I>(ranks: I) -> RankStats
249 where
250 I: IntoIterator<Item = &'a Rank>,
251 {
252 let mut stats = RankStats::default();
253 for rank in ranks {
254 let len = rank.as_str().len();
255 stats.count += 1;
256 stats.total_len += len;
257 stats.max_len = stats.max_len.max(len);
258 }
259 stats
260 }
261
262 #[must_use]
264 pub fn average_len(&self) -> f64 {
265 if self.count == 0 {
266 0.0
267 } else {
268 self.total_len as f64 / self.count as f64
269 }
270 }
271
272 #[must_use]
274 pub fn should_rebalance(&self, max_len_threshold: usize) -> bool {
275 self.max_len > max_len_threshold
276 }
277}
278
279impl fmt::Display for Rank {
280 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281 f.write_str(&self.0)
282 }
283}
284
285impl FromStr for Rank {
286 type Err = Error;
287
288 fn from_str(s: &str) -> Result<Self> {
289 Rank::parse(s)
290 }
291}
292
293fn digits_to_string(digits: &[u8]) -> String {
295 digits.iter().map(|&v| value_digit(v) as char).collect()
296}
297
298fn digit_value(c: u8) -> Option<u8> {
300 match c {
301 b'0'..=b'9' => Some(c - b'0'),
302 b'a'..=b'z' => Some(c - b'a' + 10),
303 _ => None,
304 }
305}
306
307fn value_digit(v: u8) -> u8 {
309 const DIGITS: &[u8; 36] = b"0123456789abcdefghijklmnopqrstuvwxyz";
310 DIGITS[v as usize]
311}
312
313fn digits_of(s: &str) -> Vec<u8> {
315 s.bytes().filter_map(digit_value).collect()
316}
317
318fn add_frac(a: &[u8], b: &[u8], b_int: u8) -> (u8, Vec<u8>) {
322 let n = a.len().max(b.len());
323 let mut frac = vec![0u8; n];
324 let mut carry = 0u8;
325 for i in (0..n).rev() {
326 let da = a.get(i).copied().unwrap_or(0);
327 let db = b.get(i).copied().unwrap_or(0);
328 let sum = da + db + carry;
329 frac[i] = sum % BASE;
330 carry = sum / BASE;
331 }
332 (b_int + carry, frac)
333}
334
335fn half_frac(int: u8, frac: &[u8]) -> Vec<u8> {
340 debug_assert!(int / 2 == 0, "rank value out of expected range");
341 let mut out = Vec::with_capacity(frac.len() + 1);
342 let mut rem = int % 2;
343 for &d in frac {
344 let cur = rem * BASE + d;
345 out.push(cur / 2);
346 rem = cur % 2;
347 }
348 if rem != 0 {
350 out.push(BASE / 2);
351 }
352 out
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358 use proptest::prelude::*;
359
360 proptest! {
361 #[test]
362 fn generated_ranks_preserve_canonical_order_and_uniqueness(steps in 1usize..200) {
363 let mut ranks = Vec::with_capacity(steps);
364 let mut previous = None;
365 for _ in 0..steps {
366 let next = Rank::after(previous.as_ref());
367 prop_assert!(Rank::parse(next.as_str()).is_ok());
368 if let Some(previous) = previous {
369 prop_assert!(previous < next);
370 }
371 ranks.push(next.clone());
372 previous = Some(next);
373 }
374 let unique = ranks.iter().collect::<std::collections::HashSet<_>>();
375 prop_assert_eq!(unique.len(), ranks.len());
376 }
377 }
378
379 fn r(s: &str) -> Rank {
380 Rank::parse(s).expect("valid rank")
381 }
382
383 #[test]
384 fn parse_accepts_base36_and_rejects_others() {
385 assert!(Rank::parse("i").is_ok());
386 assert!(Rank::parse("0z9a").is_ok());
387 assert!(matches!(Rank::parse(""), Err(Error::InvalidRank(_))));
388 assert!(matches!(Rank::parse("AB"), Err(Error::InvalidRank(_)))); assert!(matches!(Rank::parse("a-b"), Err(Error::InvalidRank(_))));
390 assert!(matches!(Rank::parse("0"), Err(Error::InvalidRank(_))));
392 assert!(matches!(Rank::parse("000"), Err(Error::InvalidRank(_))));
393 assert!(Rank::parse("01").is_ok()); }
395
396 #[test]
397 fn parse_rejects_trailing_zero_for_canonical_form() {
398 assert!(Rank::parse("1").is_ok());
402 assert!(matches!(Rank::parse("10"), Err(Error::InvalidRank(_))));
403 assert!(matches!(Rank::parse("100"), Err(Error::InvalidRank(_))));
404 assert!(matches!(Rank::parse("iz0"), Err(Error::InvalidRank(_))));
405 assert!(Rank::parse("101").is_ok());
407 }
408
409 #[test]
410 fn generated_ranks_are_always_canonical() {
411 let a = Rank::between(None, None).expect("open bounds produce a rank");
413 let b = Rank::after(Some(&a));
414 let c = Rank::before(Some(&a));
415 for g in [&a, &b, &c] {
416 assert!(
417 Rank::parse(g.as_str()).is_ok(),
418 "generated rank must be canonical: {g}"
419 );
420 }
421 }
422
423 #[test]
424 fn display_roundtrips() {
425 assert_eq!(r("1i").to_string(), "1i");
426 assert_eq!("1i".parse::<Rank>().unwrap(), r("1i"));
427 }
428
429 #[test]
430 fn first_rank_is_deterministic_middle() {
431 assert_eq!(
433 Rank::between(None, None).expect("open bounds produce a rank"),
434 r("i")
435 );
436 }
437
438 #[test]
439 fn after_produces_strictly_greater() {
440 let a = Rank::after(None);
441 let b = Rank::after(Some(&a));
442 let c = Rank::after(Some(&b));
443 assert!(a < b, "{a} < {b}");
444 assert!(b < c, "{b} < {c}");
445 }
446
447 #[test]
448 fn before_produces_strictly_smaller() {
449 let a = Rank::between(None, None).expect("open bounds produce a rank");
450 let b = Rank::between(None, Some(&a)).expect("valid ascending bounds");
451 assert!(b < a, "{b} < {a}");
452 }
453
454 #[test]
455 fn before_api_is_symmetric_to_after() {
456 assert_eq!(Rank::before(None), Rank::after(None));
458 assert_eq!(Rank::before(None), r("i"));
459 let mut next = Rank::before(None);
461 for _ in 0..50 {
462 let prev = Rank::before(Some(&next));
463 assert!(prev < next, "{prev} < {next}");
464 next = prev;
465 }
466 }
467
468 #[test]
469 fn between_is_strictly_ordered() {
470 let lo = r("1");
471 let hi = r("2");
472 let mid = Rank::between(Some(&lo), Some(&hi)).expect("valid ascending bounds");
473 assert!(lo < mid && mid < hi, "{lo} < {mid} < {hi}");
474 }
475
476 #[test]
477 fn between_rejects_violated_precondition() {
478 let r = Rank::between(None, None).expect("open bounds produce a rank"); let error = Rank::between(Some(&r), Some(&r)).expect_err("equal bounds are rejected");
481 assert!(matches!(error, Error::InvalidRank(message) if message.contains("lo < hi")));
482 }
483
484 #[test]
485 fn repeated_bisection_stays_between_and_unique() {
486 let lo = Rank::between(None, None).expect("open bounds produce a rank"); let hi = Rank::after(Some(&lo)); let mut left = lo.clone();
490 let mut seen = std::collections::HashSet::new();
491 seen.insert(lo.clone());
492 seen.insert(hi.clone());
493 for n in 0..100 {
494 let mid = Rank::between(Some(&left), Some(&hi)).expect("valid ascending bounds");
495 assert!(left < mid && mid < hi, "iter {n}: {left} < {mid} < {hi}");
496 assert!(seen.insert(mid.clone()), "iter {n}: duplicate {mid}");
497 left = mid;
498 }
499 }
500
501 #[test]
502 fn sequential_appends_sort_in_insertion_order() {
503 let mut ranks = Vec::new();
505 let mut prev: Option<Rank> = None;
506 for _ in 0..50 {
507 let next = Rank::after(prev.as_ref());
508 ranks.push(next.clone());
509 prev = Some(next);
510 }
511 let mut sorted = ranks.clone();
512 sorted.sort();
513 assert_eq!(ranks, sorted, "append order must equal sorted order");
514 let unique: std::collections::HashSet<_> = ranks.iter().collect();
516 assert_eq!(unique.len(), ranks.len());
517 }
518
519 #[test]
520 fn append_growth_is_bounded_over_many_appends() {
521 let mut prev: Option<Rank> = None;
525 let mut ranks: Vec<Rank> = Vec::with_capacity(1000);
526 let mut max_len = 0usize;
527 for _ in 0..1000 {
528 let next = Rank::after(prev.as_ref());
529 max_len = max_len.max(next.as_str().len());
530 ranks.push(next.clone());
531 prev = Some(next);
532 }
533 for w in ranks.windows(2) {
535 assert!(
536 w[0] < w[1],
537 "append must be strictly increasing: {} < {}",
538 w[0],
539 w[1]
540 );
541 }
542 let unique: std::collections::HashSet<_> = ranks.iter().collect();
544 assert_eq!(unique.len(), ranks.len(), "appended ranks must be unique");
545 assert!(
547 max_len <= 70,
548 "append growth not bounded: max_len = {max_len}"
549 );
550 }
551
552 #[test]
553 fn rebalance_preserves_order_and_shortens_ranks() {
554 let lo = Rank::between(None, None).expect("open bounds produce a rank");
556 let hi = Rank::after(Some(&lo));
557 let mut left = lo.clone();
558 let mut bloated = Vec::new();
559 for _ in 0..500 {
560 let mid = Rank::between(Some(&left), Some(&hi)).expect("valid ascending bounds");
561 bloated.push(mid.clone());
562 left = mid;
563 }
564 let bloated_max = bloated.iter().map(|r| r.as_str().len()).max().unwrap();
565
566 let balanced = Rank::rebalance(bloated.len());
568 assert_eq!(balanced.len(), bloated.len(), "same cardinality");
569 for w in balanced.windows(2) {
571 assert!(w[0] < w[1], "rebalanced ranks must be sorted");
572 }
573 let unique: std::collections::HashSet<_> = balanced.iter().collect();
574 assert_eq!(
575 unique.len(),
576 balanced.len(),
577 "rebalanced ranks must be unique"
578 );
579 let balanced_max = balanced.iter().map(|r| r.as_str().len()).max().unwrap();
581 assert!(
582 balanced_max < bloated_max,
583 "rebalance must shorten: {balanced_max} < {bloated_max}"
584 );
585 assert!(balanced_max <= 70, "rebalanced max_len = {balanced_max}");
586 }
587
588 #[test]
589 fn rebalance_zero_is_empty() {
590 assert!(Rank::rebalance(0).is_empty());
591 }
592
593 #[test]
594 fn rebalance_width_matches_generated_width() {
595 for count in [0usize, 1, 2, 35, 36, 37, 1_000, 1_296, 1_297] {
599 let generated = RankStats::collect(Rank::rebalance(count).iter()).max_len;
600 assert_eq!(
601 Rank::rebalance_width(count),
602 generated,
603 "rebalance_width({count}) must equal the generated max width"
604 );
605 }
606 }
607
608 #[test]
609 fn rebalance_uses_minimal_fixed_width_even_spacing() {
610 for (count, expected_width) in [(0, 0), (1, 1), (36, 2), (1_000, 2)] {
611 let ranks = Rank::rebalance(count);
612 let stats = RankStats::collect(ranks.iter());
613
614 assert_eq!(stats.count, count);
615 assert_eq!(stats.max_len, expected_width);
616 assert_eq!(stats.total_len, count * expected_width);
617 assert_eq!(stats.average_len(), expected_width as f64);
618 assert!(
619 ranks
620 .iter()
621 .all(|rank| rank.as_str().len() == expected_width),
622 "all ranks for {count} items must use one fixed width"
623 );
624 assert!(
625 ranks.iter().all(|rank| Rank::parse(rank.as_str()).is_ok()),
626 "all rebalanced ranks must remain canonical"
627 );
628 assert!(
629 ranks.windows(2).all(|window| window[0] < window[1]),
630 "rebalanced ranks must be strictly increasing"
631 );
632 }
633 }
634
635 #[test]
636 fn rank_stats_reports_max_and_average() {
637 let ranks = vec![r("i"), r("zz"), r("1")]; let stats = RankStats::collect(&ranks);
640 assert_eq!(stats.count, 3);
641 assert_eq!(stats.max_len, 2);
642 assert_eq!(stats.total_len, 4);
643 assert!((stats.average_len() - 4.0 / 3.0).abs() < 1e-9);
644 assert!(stats.should_rebalance(1), "max_len 2 > 1");
646 assert!(!stats.should_rebalance(2), "max_len 2 not > 2");
647 let empty = RankStats::collect(std::iter::empty::<&Rank>());
649 assert_eq!(empty.count, 0);
650 assert_eq!(empty.average_len(), 0.0);
651 assert!(!empty.should_rebalance(0));
652 }
653
654 #[test]
655 fn generated_ranks_have_no_trailing_zero() {
656 for _ in 0..20 {
657 let a = Rank::between(None, None).expect("open bounds produce a rank");
658 let b = Rank::between(None, Some(&a)).expect("valid ascending bounds");
659 assert_ne!(b.as_str().as_bytes().last(), Some(&b'0'));
660 }
661 }
662
663 struct Lcg(u64);
666 impl Lcg {
667 fn next_usize(&mut self, bound: usize) -> usize {
668 self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1);
669 ((self.0 >> 33) as usize) % bound
670 }
671 }
672
673 #[test]
674 fn between_property_holds_for_random_insertions() {
675 let mut rng = Lcg(0x9E3779B97F4A7C15);
680 let mut ranks = vec![Rank::after(None)];
682 ranks.push(Rank::after(Some(&ranks[0])));
683 ranks.push(Rank::after(Some(&ranks[1])));
684
685 for _ in 0..2_000 {
686 let i = rng.next_usize(ranks.len() - 1);
688 let (lo, hi) = (ranks[i].clone(), ranks[i + 1].clone());
689 let mid = Rank::between(Some(&lo), Some(&hi)).expect("valid ascending bounds");
690
691 assert!(lo < mid && mid < hi, "expected {lo} < {mid} < {hi}");
692 assert_eq!(
693 Rank::parse(mid.as_str()),
694 Ok(mid.clone()),
695 "{mid} canonical"
696 );
697 ranks.insert(i + 1, mid);
698 }
699
700 for pair in ranks.windows(2) {
702 assert!(pair[0] < pair[1], "order broke: {} !< {}", pair[0], pair[1]);
703 }
704 let unique: std::collections::BTreeSet<&str> = ranks.iter().map(Rank::as_str).collect();
705 assert_eq!(unique.len(), ranks.len(), "ranks must stay unique");
706 }
707}