rovenue/placements/
bucketing.rs1use sha2::{Digest, Sha256};
2
3pub const BUCKET_COUNT: u32 = 10_000;
4
5pub fn assign_bucket(subscriber_id: &str, seed: &str) -> u32 {
11 let digest = Sha256::digest(format!("{subscriber_id}:{seed}").as_bytes());
12 let hash = u32::from_be_bytes([digest[0], digest[1], digest[2], digest[3]]);
13 hash % BUCKET_COUNT
14}
15
16pub fn select_variant_index(bucket: u32, weights: &[f64]) -> usize {
19 let mut cumulative = 0.0f64;
20 for (i, w) in weights.iter().enumerate() {
21 cumulative += w * BUCKET_COUNT as f64;
22 if (bucket as f64) < cumulative.round() {
23 return i;
24 }
25 }
26 weights.len() - 1
27}
28
29#[cfg(test)]
30mod tests {
31 use super::*;
32 use serde::Deserialize;
33
34 const VECTORS_JSON: &str =
38 include_str!("../../../shared/src/experiments/bucketing-vectors.json");
39
40 #[derive(Debug, Deserialize)]
41 struct VectorFile {
42 #[serde(rename = "bucketCount")]
43 bucket_count: u32,
44 cases: Vec<VectorCase>,
45 }
46
47 #[derive(Debug, Deserialize)]
48 struct VectorCase {
49 #[serde(rename = "subscriberId")]
50 subscriber_id: String,
51 seed: String,
52 #[serde(rename = "expectedBucket")]
53 expected_bucket: u32,
54 variants: Vec<VectorVariant>,
55 #[serde(rename = "expectedVariantId")]
56 expected_variant_id: String,
57 }
58
59 #[derive(Debug, Deserialize)]
60 struct VectorVariant {
61 id: String,
62 weight: f64,
63 }
64
65 #[test]
66 fn matches_cross_language_bucketing_vectors() {
67 let file: VectorFile = serde_json::from_str(VECTORS_JSON).expect("valid fixture json");
68 assert_eq!(file.bucket_count, BUCKET_COUNT);
69 assert!(!file.cases.is_empty());
70
71 for case in &file.cases {
72 let bucket = assign_bucket(&case.subscriber_id, &case.seed);
73 assert_eq!(
74 bucket, case.expected_bucket,
75 "bucket mismatch for subscriber={:?} seed={:?}",
76 case.subscriber_id, case.seed
77 );
78
79 let weights: Vec<f64> = case.variants.iter().map(|v| v.weight).collect();
80 let idx = select_variant_index(bucket, &weights);
81 assert_eq!(
82 case.variants[idx].id, case.expected_variant_id,
83 "variant mismatch for subscriber={:?} seed={:?}",
84 case.subscriber_id, case.seed
85 );
86 }
87 }
88}