Simplify position module to single between() function
Assisted-by: Claude Opus 4.6 via pi
change ymttrvvtqlrklwtpxylqsopquottklus
commit 7da6b820ef4c3c4d1453dc61151c2001f9958a8d
author Alpha Chen <alpha@kejadlen.dev>
date
parent votnryyw
diff --git a/src/ops/task.rs b/src/ops/task.rs
index b3681e5..8c2bd2c 100644
--- a/src/ops/task.rs
+++ b/src/ops/task.rs
@@ -41,7 +41,7 @@ pub async fn create(
     .fetch_optional(&mut *conn)
     .await?;
 
-    let new_pos = position::midpoint(last_pos.as_deref(), None);
+    let new_pos = position::between(last_pos.as_deref().unwrap_or(""), "");
 
     sqlx::query("INSERT INTO backlog_tasks (backlog_id, task_id, position) VALUES (?, ?, ?)")
         .bind(params.backlog_id)
@@ -181,7 +181,7 @@ async fn resolve_position(
         .fetch_optional(&mut *conn)
         .await?;
 
-        return Ok(position::midpoint(last_pos.as_deref(), None));
+        return Ok(position::between(last_pos.as_deref().unwrap_or(""), ""));
     }
 
     // "before" task = the task we want to appear after us (upper bound)
@@ -244,7 +244,10 @@ async fn resolve_position(
         _ => (lower_bound.clone(), upper_bound.clone()),
     };
 
-    Ok(position::midpoint(lower.as_deref(), upper.as_deref()))
+    Ok(position::between(
+        lower.as_deref().unwrap_or(""),
+        upper.as_deref().unwrap_or(""),
+    ))
 }
 
 pub async fn move_task(
@@ -287,7 +290,7 @@ pub async fn add_to_backlog(
     .fetch_optional(&mut *conn)
     .await?;
 
-    let new_pos = position::midpoint(last_pos.as_deref(), None);
+    let new_pos = position::between(last_pos.as_deref().unwrap_or(""), "");
 
     sqlx::query("INSERT INTO backlog_tasks (backlog_id, task_id, position) VALUES (?, ?, ?)")
         .bind(backlog_id)
diff --git a/src/position.rs b/src/position.rs
index 519e022..7b8d46e 100644
--- a/src/position.rs
+++ b/src/position.rs
@@ -1,36 +1,9 @@
-/// 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")
-}
+/// Generate a position string lexicographically between `a` and `b`.
+/// Uses base-26 (a-z) characters. Empty `a` means "beginning" (digits
+/// default to 0), empty `b` means "end" (digits default to 25/'z').
+pub fn between(a: &str, b: &str) -> String {
+    debug_assert!(b.is_empty() || a < b, "a ({a}) must be less than b ({b})");
 
-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();
 
@@ -70,7 +43,7 @@ fn generate_between(a: &str, b: &str) -> String {
     // Structurally unreachable: the inner loop always terminates via the
     // mid2 > da check within 16 extra iterations (base-26 guarantees room
     // between any digit and 'z').
-    unreachable!("generate_between exhausted without finding a midpoint") // cov-excl-line
+    unreachable!("between exhausted without finding a midpoint") // cov-excl-line
 }
 
 #[cfg(test)]
@@ -78,43 +51,42 @@ mod tests {
     use super::*;
 
     #[test]
-    fn first_position() {
-        let pos = midpoint(None, None);
-        assert_eq!(pos, "m");
+    fn between_empty_bounds() {
+        assert_eq!(between("", ""), "m");
     }
 
     #[test]
     fn before_existing() {
-        let pos = midpoint(None, Some("m"));
+        let pos = between("", "m");
         assert!(*pos < *"m", "expected {pos} < m");
     }
 
     #[test]
     fn after_existing() {
-        let pos = midpoint(Some("m"), None);
+        let pos = between("m", "");
         assert!(*pos > *"m", "expected {pos} > m");
     }
 
     #[test]
     fn between_two() {
-        let pos = midpoint(Some("a"), Some("z"));
+        let pos = between("a", "z");
         assert!(*pos > *"a", "expected {pos} > a");
         assert!(*pos < *"z", "expected {pos} < z");
     }
 
     #[test]
     fn between_adjacent() {
-        let pos = midpoint(Some("a"), Some("b"));
+        let pos = between("a", "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)];
+        let mut positions = vec!["m".to_string()];
         for _ in 0..20 {
             let last = positions.last().unwrap().clone();
-            positions.push(midpoint(Some(&last), None));
+            positions.push(between(&last, ""));
         }
         for window in positions.windows(2) {
             assert!(window[0] < window[1]);
@@ -123,10 +95,10 @@ mod tests {
 
     #[test]
     fn ordering_is_stable_over_many_prepends() {
-        let mut positions = vec![midpoint(None, None)];
+        let mut positions = vec!["m".to_string()];
         for _ in 0..20 {
             let first = positions.first().unwrap().clone();
-            positions.insert(0, midpoint(None, Some(&first)));
+            positions.insert(0, between("", &first));
         }
         for window in positions.windows(2) {
             assert!(window[0] < window[1]);
@@ -135,16 +107,13 @@ mod tests {
 
     #[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
+        let mut positions = vec!["m".to_string()];
+        positions.push(between(&positions[0], ""));
 
         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));
+            let mid = between(a, b);
             assert!(mid > *a);
             assert!(mid < *b);
             positions.insert(positions.len() - 1, mid);