use serde::{Deserialize, Serialize};
use std::hash::{Hash, Hasher};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeterminismSpec {
pub seed: u64,
pub tie_break: TieBreakStrategy,
pub stable_sort: bool,
pub rules_version: String,
}
impl Default for DeterminismSpec {
fn default() -> Self {
Self {
seed: 0,
tie_break: TieBreakStrategy::LexicographicFirst,
stable_sort: true,
rules_version: "v1".to_string(),
}
}
}
impl DeterminismSpec {
pub fn with_seed(seed: u64) -> Self {
Self {
seed,
..Default::default()
}
}
pub fn new(seed: u64, tie_break: TieBreakStrategy) -> Self {
Self {
seed,
tie_break,
stable_sort: true,
rules_version: "v1".to_string(),
}
}
pub fn sub_seed(&self, phase: &str) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
self.seed.hash(&mut hasher);
phase.hash(&mut hasher);
hasher.finish()
}
pub fn iter_seed(&self, iteration: usize) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
self.seed.hash(&mut hasher);
iteration.hash(&mut hasher);
hasher.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TieBreakStrategy {
LexicographicFirst,
LexicographicLast,
SmallestIndex,
LargestIndex,
SeededRandom,
}
impl TieBreakStrategy {
pub fn select<T: Ord + Clone>(&self, candidates: &[T], seed: u64) -> Option<T> {
if candidates.is_empty() {
return None;
}
match self {
Self::LexicographicFirst | Self::SmallestIndex => candidates.iter().min().cloned(),
Self::LexicographicLast | Self::LargestIndex => candidates.iter().max().cloned(),
Self::SeededRandom => {
let idx = (seed as usize) % candidates.len();
Some(candidates[idx].clone())
}
}
}
pub fn select_by<'a, T, F>(&self, candidates: &'a [T], seed: u64, compare: F) -> Option<&'a T>
where
F: Fn(&T, &T) -> std::cmp::Ordering,
{
if candidates.is_empty() {
return None;
}
match self {
Self::LexicographicFirst | Self::SmallestIndex => {
candidates.iter().min_by(|a, b| compare(a, b))
}
Self::LexicographicLast | Self::LargestIndex => {
candidates.iter().max_by(|a, b| compare(a, b))
}
Self::SeededRandom => {
let idx = (seed as usize) % candidates.len();
Some(&candidates[idx])
}
}
}
pub fn sort<T: Ord>(&self, candidates: &mut [T]) {
match self {
Self::LexicographicFirst | Self::SmallestIndex => candidates.sort(),
Self::LexicographicLast | Self::LargestIndex => {
candidates.sort();
candidates.reverse();
}
Self::SeededRandom => {
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_determinism() {
let spec = DeterminismSpec::default();
assert_eq!(spec.seed, 0);
assert_eq!(spec.tie_break, TieBreakStrategy::LexicographicFirst);
assert!(spec.stable_sort);
}
#[test]
fn test_sub_seed_consistency() {
let spec = DeterminismSpec::with_seed(12345);
let seed1 = spec.sub_seed("phase1");
let seed2 = spec.sub_seed("phase1");
assert_eq!(seed1, seed2);
let seed3 = spec.sub_seed("phase2");
assert_ne!(seed1, seed3);
}
#[test]
fn test_tie_break_lexicographic() {
let candidates = vec!["charlie", "alice", "bob"];
let first = TieBreakStrategy::LexicographicFirst.select(&candidates, 0);
assert_eq!(first, Some("alice"));
let last = TieBreakStrategy::LexicographicLast.select(&candidates, 0);
assert_eq!(last, Some("charlie"));
}
#[test]
fn test_tie_break_seeded() {
let candidates = vec![1, 2, 3, 4, 5];
let sel1 = TieBreakStrategy::SeededRandom.select(&candidates, 42);
let sel2 = TieBreakStrategy::SeededRandom.select(&candidates, 42);
assert_eq!(sel1, sel2);
let sel3 = TieBreakStrategy::SeededRandom.select(&candidates, 43);
let _ = sel3; }
#[test]
fn test_tie_break_empty() {
let empty: Vec<i32> = vec![];
assert_eq!(TieBreakStrategy::LexicographicFirst.select(&empty, 0), None);
}
}