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
187fn rebalance_layout(count: usize) -> (usize, u128) {
190 let target = count as u128;
191 let mut width = 1;
192 let mut prefix_space = 1u128;
193 loop {
194 let capacity = prefix_space.saturating_mul(u128::from(BASE - 1));
195 if capacity >= target {
196 return (width, capacity);
197 }
198 prefix_space = prefix_space.saturating_mul(u128::from(BASE));
199 width += 1;
200 }
201}
202
203fn rank_at_ordinal(ordinal: u128, width: usize) -> Rank {
209 let last_digit = ordinal % u128::from(BASE - 1) + 1;
210 let mut prefix = ordinal / u128::from(BASE - 1);
211 let mut digits = vec![0; width];
212 for position in (0..width - 1).rev() {
213 digits[position] = (prefix % u128::from(BASE)) as u8;
214 prefix /= u128::from(BASE);
215 }
216 digits[width - 1] = last_digit as u8;
217 Rank(digits_to_string(&digits))
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
222pub struct RankStats {
223 pub count: usize,
225 pub max_len: usize,
227 pub total_len: usize,
229}
230
231impl RankStats {
232 pub fn collect<'a, I>(ranks: I) -> RankStats
234 where
235 I: IntoIterator<Item = &'a Rank>,
236 {
237 let mut stats = RankStats::default();
238 for rank in ranks {
239 let len = rank.as_str().len();
240 stats.count += 1;
241 stats.total_len += len;
242 stats.max_len = stats.max_len.max(len);
243 }
244 stats
245 }
246
247 #[must_use]
249 pub fn average_len(&self) -> f64 {
250 if self.count == 0 {
251 0.0
252 } else {
253 self.total_len as f64 / self.count as f64
254 }
255 }
256
257 #[must_use]
259 pub fn should_rebalance(&self, max_len_threshold: usize) -> bool {
260 self.max_len > max_len_threshold
261 }
262}
263
264impl fmt::Display for Rank {
265 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266 f.write_str(&self.0)
267 }
268}
269
270impl FromStr for Rank {
271 type Err = Error;
272
273 fn from_str(s: &str) -> Result<Self> {
274 Rank::parse(s)
275 }
276}
277
278fn digits_to_string(digits: &[u8]) -> String {
280 digits.iter().map(|&v| value_digit(v) as char).collect()
281}
282
283fn digit_value(c: u8) -> Option<u8> {
285 match c {
286 b'0'..=b'9' => Some(c - b'0'),
287 b'a'..=b'z' => Some(c - b'a' + 10),
288 _ => None,
289 }
290}
291
292fn value_digit(v: u8) -> u8 {
294 const DIGITS: &[u8; 36] = b"0123456789abcdefghijklmnopqrstuvwxyz";
295 DIGITS[v as usize]
296}
297
298fn digits_of(s: &str) -> Vec<u8> {
300 s.bytes().filter_map(digit_value).collect()
301}
302
303fn add_frac(a: &[u8], b: &[u8], b_int: u8) -> (u8, Vec<u8>) {
307 let n = a.len().max(b.len());
308 let mut frac = vec![0u8; n];
309 let mut carry = 0u8;
310 for i in (0..n).rev() {
311 let da = a.get(i).copied().unwrap_or(0);
312 let db = b.get(i).copied().unwrap_or(0);
313 let sum = da + db + carry;
314 frac[i] = sum % BASE;
315 carry = sum / BASE;
316 }
317 (b_int + carry, frac)
318}
319
320fn half_frac(int: u8, frac: &[u8]) -> Vec<u8> {
325 debug_assert!(int / 2 == 0, "rank value out of expected range");
326 let mut out = Vec::with_capacity(frac.len() + 1);
327 let mut rem = int % 2;
328 for &d in frac {
329 let cur = rem * BASE + d;
330 out.push(cur / 2);
331 rem = cur % 2;
332 }
333 if rem != 0 {
335 out.push(BASE / 2);
336 }
337 out
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 use proptest::prelude::*;
344
345 proptest! {
346 #[test]
347 fn generated_ranks_preserve_canonical_order_and_uniqueness(steps in 1usize..200) {
348 let mut ranks = Vec::with_capacity(steps);
349 let mut previous = None;
350 for _ in 0..steps {
351 let next = Rank::after(previous.as_ref());
352 prop_assert!(Rank::parse(next.as_str()).is_ok());
353 if let Some(previous) = previous {
354 prop_assert!(previous < next);
355 }
356 ranks.push(next.clone());
357 previous = Some(next);
358 }
359 let unique = ranks.iter().collect::<std::collections::HashSet<_>>();
360 prop_assert_eq!(unique.len(), ranks.len());
361 }
362 }
363
364 fn r(s: &str) -> Rank {
365 Rank::parse(s).expect("valid rank")
366 }
367
368 #[test]
369 fn parse_accepts_base36_and_rejects_others() {
370 assert!(Rank::parse("i").is_ok());
371 assert!(Rank::parse("0z9a").is_ok());
372 assert!(matches!(Rank::parse(""), Err(Error::InvalidRank(_))));
373 assert!(matches!(Rank::parse("AB"), Err(Error::InvalidRank(_)))); assert!(matches!(Rank::parse("a-b"), Err(Error::InvalidRank(_))));
375 assert!(matches!(Rank::parse("0"), Err(Error::InvalidRank(_))));
377 assert!(matches!(Rank::parse("000"), Err(Error::InvalidRank(_))));
378 assert!(Rank::parse("01").is_ok()); }
380
381 #[test]
382 fn parse_rejects_trailing_zero_for_canonical_form() {
383 assert!(Rank::parse("1").is_ok());
387 assert!(matches!(Rank::parse("10"), Err(Error::InvalidRank(_))));
388 assert!(matches!(Rank::parse("100"), Err(Error::InvalidRank(_))));
389 assert!(matches!(Rank::parse("iz0"), Err(Error::InvalidRank(_))));
390 assert!(Rank::parse("101").is_ok());
392 }
393
394 #[test]
395 fn generated_ranks_are_always_canonical() {
396 let a = Rank::between(None, None).expect("open bounds produce a rank");
398 let b = Rank::after(Some(&a));
399 let c = Rank::before(Some(&a));
400 for g in [&a, &b, &c] {
401 assert!(
402 Rank::parse(g.as_str()).is_ok(),
403 "generated rank must be canonical: {g}"
404 );
405 }
406 }
407
408 #[test]
409 fn display_roundtrips() {
410 assert_eq!(r("1i").to_string(), "1i");
411 assert_eq!("1i".parse::<Rank>().unwrap(), r("1i"));
412 }
413
414 #[test]
415 fn first_rank_is_deterministic_middle() {
416 assert_eq!(
418 Rank::between(None, None).expect("open bounds produce a rank"),
419 r("i")
420 );
421 }
422
423 #[test]
424 fn after_produces_strictly_greater() {
425 let a = Rank::after(None);
426 let b = Rank::after(Some(&a));
427 let c = Rank::after(Some(&b));
428 assert!(a < b, "{a} < {b}");
429 assert!(b < c, "{b} < {c}");
430 }
431
432 #[test]
433 fn before_produces_strictly_smaller() {
434 let a = Rank::between(None, None).expect("open bounds produce a rank");
435 let b = Rank::between(None, Some(&a)).expect("valid ascending bounds");
436 assert!(b < a, "{b} < {a}");
437 }
438
439 #[test]
440 fn before_api_is_symmetric_to_after() {
441 assert_eq!(Rank::before(None), Rank::after(None));
443 assert_eq!(Rank::before(None), r("i"));
444 let mut next = Rank::before(None);
446 for _ in 0..50 {
447 let prev = Rank::before(Some(&next));
448 assert!(prev < next, "{prev} < {next}");
449 next = prev;
450 }
451 }
452
453 #[test]
454 fn between_is_strictly_ordered() {
455 let lo = r("1");
456 let hi = r("2");
457 let mid = Rank::between(Some(&lo), Some(&hi)).expect("valid ascending bounds");
458 assert!(lo < mid && mid < hi, "{lo} < {mid} < {hi}");
459 }
460
461 #[test]
462 fn between_rejects_violated_precondition() {
463 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");
466 assert!(matches!(error, Error::InvalidRank(message) if message.contains("lo < hi")));
467 }
468
469 #[test]
470 fn repeated_bisection_stays_between_and_unique() {
471 let lo = Rank::between(None, None).expect("open bounds produce a rank"); let hi = Rank::after(Some(&lo)); let mut left = lo.clone();
475 let mut seen = std::collections::HashSet::new();
476 seen.insert(lo.clone());
477 seen.insert(hi.clone());
478 for n in 0..100 {
479 let mid = Rank::between(Some(&left), Some(&hi)).expect("valid ascending bounds");
480 assert!(left < mid && mid < hi, "iter {n}: {left} < {mid} < {hi}");
481 assert!(seen.insert(mid.clone()), "iter {n}: duplicate {mid}");
482 left = mid;
483 }
484 }
485
486 #[test]
487 fn sequential_appends_sort_in_insertion_order() {
488 let mut ranks = Vec::new();
490 let mut prev: Option<Rank> = None;
491 for _ in 0..50 {
492 let next = Rank::after(prev.as_ref());
493 ranks.push(next.clone());
494 prev = Some(next);
495 }
496 let mut sorted = ranks.clone();
497 sorted.sort();
498 assert_eq!(ranks, sorted, "append order must equal sorted order");
499 let unique: std::collections::HashSet<_> = ranks.iter().collect();
501 assert_eq!(unique.len(), ranks.len());
502 }
503
504 #[test]
505 fn append_growth_is_bounded_over_many_appends() {
506 let mut prev: Option<Rank> = None;
510 let mut ranks: Vec<Rank> = Vec::with_capacity(1000);
511 let mut max_len = 0usize;
512 for _ in 0..1000 {
513 let next = Rank::after(prev.as_ref());
514 max_len = max_len.max(next.as_str().len());
515 ranks.push(next.clone());
516 prev = Some(next);
517 }
518 for w in ranks.windows(2) {
520 assert!(
521 w[0] < w[1],
522 "append must be strictly increasing: {} < {}",
523 w[0],
524 w[1]
525 );
526 }
527 let unique: std::collections::HashSet<_> = ranks.iter().collect();
529 assert_eq!(unique.len(), ranks.len(), "appended ranks must be unique");
530 assert!(
532 max_len <= 70,
533 "append growth not bounded: max_len = {max_len}"
534 );
535 }
536
537 #[test]
538 fn rebalance_preserves_order_and_shortens_ranks() {
539 let lo = Rank::between(None, None).expect("open bounds produce a rank");
541 let hi = Rank::after(Some(&lo));
542 let mut left = lo.clone();
543 let mut bloated = Vec::new();
544 for _ in 0..500 {
545 let mid = Rank::between(Some(&left), Some(&hi)).expect("valid ascending bounds");
546 bloated.push(mid.clone());
547 left = mid;
548 }
549 let bloated_max = bloated.iter().map(|r| r.as_str().len()).max().unwrap();
550
551 let balanced = Rank::rebalance(bloated.len());
553 assert_eq!(balanced.len(), bloated.len(), "same cardinality");
554 for w in balanced.windows(2) {
556 assert!(w[0] < w[1], "rebalanced ranks must be sorted");
557 }
558 let unique: std::collections::HashSet<_> = balanced.iter().collect();
559 assert_eq!(
560 unique.len(),
561 balanced.len(),
562 "rebalanced ranks must be unique"
563 );
564 let balanced_max = balanced.iter().map(|r| r.as_str().len()).max().unwrap();
566 assert!(
567 balanced_max < bloated_max,
568 "rebalance must shorten: {balanced_max} < {bloated_max}"
569 );
570 assert!(balanced_max <= 70, "rebalanced max_len = {balanced_max}");
571 }
572
573 #[test]
574 fn rebalance_zero_is_empty() {
575 assert!(Rank::rebalance(0).is_empty());
576 }
577
578 #[test]
579 fn rebalance_uses_minimal_fixed_width_even_spacing() {
580 for (count, expected_width) in [(0, 0), (1, 1), (36, 2), (1_000, 2)] {
581 let ranks = Rank::rebalance(count);
582 let stats = RankStats::collect(ranks.iter());
583
584 assert_eq!(stats.count, count);
585 assert_eq!(stats.max_len, expected_width);
586 assert_eq!(stats.total_len, count * expected_width);
587 assert_eq!(stats.average_len(), expected_width as f64);
588 assert!(
589 ranks
590 .iter()
591 .all(|rank| rank.as_str().len() == expected_width),
592 "all ranks for {count} items must use one fixed width"
593 );
594 assert!(
595 ranks.iter().all(|rank| Rank::parse(rank.as_str()).is_ok()),
596 "all rebalanced ranks must remain canonical"
597 );
598 assert!(
599 ranks.windows(2).all(|window| window[0] < window[1]),
600 "rebalanced ranks must be strictly increasing"
601 );
602 }
603 }
604
605 #[test]
606 fn rank_stats_reports_max_and_average() {
607 let ranks = vec![r("i"), r("zz"), r("1")]; let stats = RankStats::collect(&ranks);
610 assert_eq!(stats.count, 3);
611 assert_eq!(stats.max_len, 2);
612 assert_eq!(stats.total_len, 4);
613 assert!((stats.average_len() - 4.0 / 3.0).abs() < 1e-9);
614 assert!(stats.should_rebalance(1), "max_len 2 > 1");
616 assert!(!stats.should_rebalance(2), "max_len 2 not > 2");
617 let empty = RankStats::collect(std::iter::empty::<&Rank>());
619 assert_eq!(empty.count, 0);
620 assert_eq!(empty.average_len(), 0.0);
621 assert!(!empty.should_rebalance(0));
622 }
623
624 #[test]
625 fn generated_ranks_have_no_trailing_zero() {
626 for _ in 0..20 {
627 let a = Rank::between(None, None).expect("open bounds produce a rank");
628 let b = Rank::between(None, Some(&a)).expect("valid ascending bounds");
629 assert_ne!(b.as_str().as_bytes().last(), Some(&b'0'));
630 }
631 }
632
633 struct Lcg(u64);
636 impl Lcg {
637 fn next_usize(&mut self, bound: usize) -> usize {
638 self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1);
639 ((self.0 >> 33) as usize) % bound
640 }
641 }
642
643 #[test]
644 fn between_property_holds_for_random_insertions() {
645 let mut rng = Lcg(0x9E3779B97F4A7C15);
650 let mut ranks = vec![Rank::after(None)];
652 ranks.push(Rank::after(Some(&ranks[0])));
653 ranks.push(Rank::after(Some(&ranks[1])));
654
655 for _ in 0..2_000 {
656 let i = rng.next_usize(ranks.len() - 1);
658 let (lo, hi) = (ranks[i].clone(), ranks[i + 1].clone());
659 let mid = Rank::between(Some(&lo), Some(&hi)).expect("valid ascending bounds");
660
661 assert!(lo < mid && mid < hi, "expected {lo} < {mid} < {hi}");
662 assert_eq!(
663 Rank::parse(mid.as_str()),
664 Ok(mid.clone()),
665 "{mid} canonical"
666 );
667 ranks.insert(i + 1, mid);
668 }
669
670 for pair in ranks.windows(2) {
672 assert!(pair[0] < pair[1], "order broke: {} !< {}", pair[0], pair[1]);
673 }
674 let unique: std::collections::BTreeSet<&str> = ranks.iter().map(Rank::as_str).collect();
675 assert_eq!(unique.len(), ranks.len(), "ranks must stay unique");
676 }
677}