#[derive(Debug, PartialEq, Eq)]
pub(super) enum BlobBucketIntersection {
FullyContained { blob_idx: usize },
RightHalf { blob_idx: usize },
LeftHalf { blob_idx: usize },
}
pub(super) fn classify_blobs_in_bucket(
bucket_start_slot: u64,
bucket_end_slot: u64,
way_slot_starts: &[u64],
total_slots: u64,
) -> std::result::Result<Vec<BlobBucketIntersection>, String> {
let bucket_size = bucket_end_slot - bucket_start_slot;
let n = way_slot_starts.len();
let j = way_slot_starts.partition_point(|x| *x <= bucket_start_slot);
let first_i = if j > 0 { j - 1 } else { 0 };
let mut result = Vec::new();
for i in first_i..n {
let blob_start = way_slot_starts[i];
let blob_end = if i + 1 < n {
way_slot_starts[i + 1]
} else {
total_slots
};
if blob_start >= bucket_end_slot {
break;
}
if blob_end == blob_start {
continue;
}
let blob_size = blob_end - blob_start;
if blob_size > bucket_size {
return Err(format!(
"blob {i} slot range [{blob_start}, {blob_end}) is wider ({blob_size}) than \
bucket [{bucket_start_slot}, {bucket_end_slot}) ({bucket_size}); \
structural assumption violated"
));
}
if blob_end <= bucket_start_slot {
continue;
}
let intersection = if blob_start >= bucket_start_slot && blob_end <= bucket_end_slot {
BlobBucketIntersection::FullyContained { blob_idx: i }
} else if blob_start < bucket_start_slot {
BlobBucketIntersection::RightHalf { blob_idx: i }
} else {
BlobBucketIntersection::LeftHalf { blob_idx: i }
};
result.push(intersection);
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_empty_inputs() {
let result = classify_blobs_in_bucket(0, 100, &[], 0).expect("classify");
assert!(result.is_empty());
}
#[test]
fn classify_single_blob_fully_contained() {
let result = classify_blobs_in_bucket(0, 100, &[20], 80).expect("classify");
assert_eq!(
result,
vec![BlobBucketIntersection::FullyContained { blob_idx: 0 }]
);
}
#[test]
fn classify_single_blob_left_half() {
let result = classify_blobs_in_bucket(0, 100, &[50], 150).expect("classify");
assert_eq!(
result,
vec![BlobBucketIntersection::LeftHalf { blob_idx: 0 }]
);
}
#[test]
fn classify_single_blob_right_half() {
let result = classify_blobs_in_bucket(25, 100, &[0], 50).expect("classify");
assert_eq!(
result,
vec![BlobBucketIntersection::RightHalf { blob_idx: 0 }]
);
}
#[test]
fn classify_multiple_blobs_in_bucket() {
let starts = [50, 150, 250, 350, 450];
let total_slots = 650;
let result = classify_blobs_in_bucket(100, 600, &starts, total_slots).expect("classify");
assert_eq!(result.len(), 5);
assert_eq!(result[0], BlobBucketIntersection::RightHalf { blob_idx: 0 });
assert_eq!(
result[1],
BlobBucketIntersection::FullyContained { blob_idx: 1 }
);
assert_eq!(
result[2],
BlobBucketIntersection::FullyContained { blob_idx: 2 }
);
assert_eq!(
result[3],
BlobBucketIntersection::FullyContained { blob_idx: 3 }
);
assert_eq!(result[4], BlobBucketIntersection::LeftHalf { blob_idx: 4 });
}
#[test]
fn classify_boundary_exact_match() {
let result = classify_blobs_in_bucket(0, 100, &[50], 100).expect("classify");
assert_eq!(
result,
vec![BlobBucketIntersection::FullyContained { blob_idx: 0 }]
);
let result = classify_blobs_in_bucket(0, 100, &[0], 80).expect("classify");
assert_eq!(
result,
vec![BlobBucketIntersection::FullyContained { blob_idx: 0 }]
);
}
#[test]
fn classify_empty_blob_omitted() {
let starts = [10, 10, 20];
let result = classify_blobs_in_bucket(0, 30, &starts, 20).expect("classify");
assert_eq!(
result,
vec![BlobBucketIntersection::FullyContained { blob_idx: 1 }]
);
}
#[test]
fn classify_last_blob_uses_total_slots() {
let starts = [0, 50];
let result = classify_blobs_in_bucket(0, 100, &starts, 80).expect("classify");
assert_eq!(
result,
vec![
BlobBucketIntersection::FullyContained { blob_idx: 0 },
BlobBucketIntersection::FullyContained { blob_idx: 1 },
]
);
}
#[test]
fn classify_blob_wider_than_bucket_errors() {
let result = classify_blobs_in_bucket(0, 10, &[0], 100);
assert!(result.is_err(), "expected Err for blob wider than bucket");
}
}