Add lexicographic position generation
Assisted-by: Claude Opus 4.6 via pi
change xnkxvlptmxnvuwlnyrsrurwuqtowryyy
commit 9a8d7de551712cc0643c086aed08f6aeec47953e
author Alpha Chen <alpha@kejadlen.dev>
date
parent pynvnlon
diff --git a/crates/ranger-lib/src/lib.rs b/crates/ranger-lib/src/lib.rs
index 7915a9a..3da7711 100644
--- a/crates/ranger-lib/src/lib.rs
+++ b/crates/ranger-lib/src/lib.rs
@@ -1,2 +1,3 @@
 pub mod error;
 pub mod key;
+pub mod position;
diff --git a/crates/ranger-lib/src/position.rs b/crates/ranger-lib/src/position.rs
new file mode 100644
index 0000000..91a235d
--- /dev/null
+++ b/crates/ranger-lib/src/position.rs
@@ -0,0 +1,162 @@
+/// Generate a position string between `before` and `after`.
+/// Both are optional: None means "the boundary" (start or end).
+/// Uses base-26 (a-z) characters with lexicographic ordering.
+pub fn midpoint(before: Option<&str>, after: Option<&str>) -> String {
+    match (before, after) {
+        (None, None) => "m".to_string(),
+        (None, Some(b)) => generate_between("", b),
+        (Some(a), None) => generate_after(a),
+        (Some(a), Some(b)) => {
+            debug_assert!(a < b, "before ({a}) must be less than after ({b})");
+            generate_between(a, b)
+        }
+    }
+}
+
+fn generate_after(a: &str) -> String {
+    let mut digits: Vec<u8> = a.bytes().map(|c| c - b'a').collect();
+
+    // Find the rightmost character with room to increment toward 'z'
+    for i in (0..digits.len()).rev() {
+        let mid = (digits[i] as u16 + 25) / 2;
+        if mid as u8 > digits[i] {
+            digits[i] = mid as u8;
+            digits.truncate(i + 1);
+            return digits.iter().map(|&d| (d + b'a') as char).collect();
+        }
+    }
+
+    // All characters near 'z'; extend with 'm'
+    format!("{a}m")
+}
+
+fn generate_between(a: &str, b: &str) -> String {
+    let a_digits: Vec<u8> = a.bytes().map(|c| c - b'a').collect();
+    let b_digits: Vec<u8> = b.bytes().map(|c| c - b'a').collect();
+
+    let mut result: Vec<u8> = Vec::new();
+    let max_len = a_digits.len().max(b_digits.len());
+
+    for i in 0..=max_len {
+        let ca = a_digits.get(i).copied().unwrap_or(0);
+        let cb = b_digits.get(i).copied().unwrap_or(25);
+
+        if ca == cb {
+            result.push(ca);
+            continue;
+        }
+
+        // ca < cb
+        let mid = (ca as u16 + cb as u16) / 2;
+        if mid as u8 > ca {
+            result.push(mid as u8);
+            return result.iter().map(|&d| (d + b'a') as char).collect();
+        }
+
+        // Adjacent (differ by 1): take the lower, then find suffix between
+        // remaining digits of a and 'z' (implicit upper bound)
+        result.push(ca);
+        for j in (i + 1)..=(max_len + 16) {
+            let da = a_digits.get(j).copied().unwrap_or(0);
+            let mid2 = (da as u16 + 25) / 2;
+            if mid2 as u8 > da {
+                result.push(mid2 as u8);
+                return result.iter().map(|&d| (d + b'a') as char).collect();
+            }
+            result.push(da);
+        }
+    }
+
+    // Fallback
+    result.push(12);
+    result.iter().map(|&d| (d + b'a') as char).collect()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn first_position() {
+        let pos = midpoint(None, None);
+        assert_eq!(pos, "m");
+    }
+
+    #[test]
+    fn before_existing() {
+        let pos = midpoint(None, Some("m"));
+        assert!(*pos < *"m", "expected {pos} < m");
+    }
+
+    #[test]
+    fn after_existing() {
+        let pos = midpoint(Some("m"), None);
+        assert!(*pos > *"m", "expected {pos} > m");
+    }
+
+    #[test]
+    fn between_two() {
+        let pos = midpoint(Some("a"), Some("z"));
+        assert!(*pos > *"a", "expected {pos} > a");
+        assert!(*pos < *"z", "expected {pos} < z");
+    }
+
+    #[test]
+    fn between_adjacent() {
+        let pos = midpoint(Some("a"), Some("b"));
+        assert!(*pos > *"a", "expected {pos} > a");
+        assert!(*pos < *"b", "expected {pos} < b");
+    }
+
+    #[test]
+    fn ordering_is_stable_over_many_appends() {
+        let mut positions = vec![midpoint(None, None)];
+        for _ in 0..20 {
+            let last = positions.last().unwrap().clone();
+            positions.push(midpoint(Some(&last), None));
+        }
+        for window in positions.windows(2) {
+            assert!(
+                window[0] < window[1],
+                "{} should be < {}",
+                window[0],
+                window[1]
+            );
+        }
+    }
+
+    #[test]
+    fn ordering_is_stable_over_many_prepends() {
+        let mut positions = vec![midpoint(None, None)];
+        for _ in 0..20 {
+            let first = positions.first().unwrap().clone();
+            positions.insert(0, midpoint(None, Some(&first)));
+        }
+        for window in positions.windows(2) {
+            assert!(
+                window[0] < window[1],
+                "{} should be < {}",
+                window[0],
+                window[1]
+            );
+        }
+    }
+
+    #[test]
+    fn ordering_is_stable_over_many_interleaved_inserts() {
+        // Build a list by always inserting in the middle
+        let mut positions = vec![
+            midpoint(None, None), // first
+        ];
+        positions.push(midpoint(Some(&positions[0]), None)); // second
+
+        for _ in 0..20 {
+            let a = &positions[positions.len() - 2].clone();
+            let b = &positions[positions.len() - 1].clone();
+            let mid = midpoint(Some(a), Some(b));
+            assert!(mid > *a, "{mid} should be > {a}");
+            assert!(mid < *b, "{mid} should be < {b}");
+            positions.insert(positions.len() - 1, mid);
+        }
+    }
+}