refactor!: replace emptiness-as-sentinel with Option for results and weights

Third of the five remaining items on #23.

`add_events_with_prior` and `TimeSlice::add_events` took `Vec<Vec<f64>>` and
`Vec<Vec<Vec<f64>>>` where an empty vec meant "not supplied" — so an empty
outer vec and a genuinely empty event list were the same value, and every
reader had to know which. Both are now `Option`, and the "not supplied"
branches read as `None` arms rather than `is_empty()` checks.

Two things fell out of the change that a sentinel would have hidden:

The tie pre-check iterated `results` directly. Under `Option` it needs
`.iter().flatten()`, which makes explicit that a `None` results list has no
ties to reject — previously an empty vec silently skipped the same loop and
looked identical to "checked, found nothing".

`MismatchedShape.got` could no longer be `results.len()`, because at the point
of the error there may be no vec to take a length from. It is now computed as
`map_or(0, Vec::len)` before the error is built.

MSRV note: my first draft used let-chains for the two validations. Those need
Rust 1.88 and this crate pins 1.85 — it compiled locally on 1.98 and would
have failed only in the MSRV CI job. Rewritten with `is_some_and`, and
verified by installing 1.85.0 and building against it rather than by assuming
the removal was complete.

Breaking: `TimeSlice::add_events` is public.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T5SYDExxL4vZgvunrcNSMc
This commit is contained in:
2026-08-27 17:48:50 +02:00
co-authored by Claude Opus 5
parent 06ed24b240
commit aff3fb948d
3 changed files with 55 additions and 44 deletions
+20 -21
View File
@@ -277,8 +277,8 @@ impl<T: Time> TimeSlice<T> {
pub fn add_events<D: Drift<T>>(
&mut self,
composition: Vec<Vec<Vec<Index>>>,
results: Vec<Vec<f64>>,
weights: Vec<Vec<Vec<f64>>>,
results: Option<Vec<Vec<f64>>>,
weights: Option<Vec<Vec<Vec<f64>>>>,
kinds: Vec<EventKind>,
agents: &CompetitorStore<T, D>,
) {
@@ -328,22 +328,21 @@ impl<T: Time> TimeSlice<T> {
Team {
items,
output: if results.is_empty() {
(event.len() - (t + 1)) as f64
} else {
results[e][t]
output: match &results {
Some(results) => results[e][t],
// No explicit result: rank by position, first team best.
None => (event.len() - (t + 1)) as f64,
},
}
})
.collect::<Vec<_>>();
let weights = if weights.is_empty() {
teams
let weights = match &weights {
Some(weights) => weights[e].clone(),
None => teams
.iter()
.map(|team| vec![1.0; team.items.len()])
.collect::<Vec<_>>()
} else {
weights[e].clone()
.collect::<Vec<_>>(),
};
Event {
@@ -828,8 +827,8 @@ mod tests {
vec![vec![c], vec![d]],
vec![vec![e], vec![f]],
],
vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0]],
vec![],
Some(vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0]]),
None,
vec![EventKind::Ranked; 3],
&agents,
);
@@ -905,8 +904,8 @@ mod tests {
vec![vec![a], vec![c]],
vec![vec![b], vec![c]],
],
vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0]],
vec![],
Some(vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0]]),
None,
vec![EventKind::Ranked; 3],
&agents,
);
@@ -985,8 +984,8 @@ mod tests {
vec![vec![a], vec![c]],
vec![vec![b], vec![c]],
],
vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0]],
vec![],
Some(vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0]]),
None,
vec![EventKind::Ranked; 3],
&agents,
);
@@ -1017,8 +1016,8 @@ mod tests {
vec![vec![a], vec![c]],
vec![vec![b], vec![c]],
],
vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0]],
vec![],
Some(vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0]]),
None,
vec![EventKind::Ranked; 3],
&agents,
);
@@ -1088,8 +1087,8 @@ mod tests {
vec![vec![c], vec![d]],
vec![vec![a], vec![c]],
],
vec![vec![1.0, 0.0], vec![1.0, 0.0], vec![1.0, 0.0]],
vec![],
Some(vec![vec![1.0, 0.0], vec![1.0, 0.0], vec![1.0, 0.0]]),
None,
vec![EventKind::Ranked; 3],
&agents,
);