use crate::{Error, Result};
const EPS: f64 = 1e-9;
fn split_at_integer(x: f64) -> (u64, f64) {
let nearest = x.round();
if (x - nearest).abs() <= EPS {
return (nearest.max(0.0) as u64, 0.0);
}
let floor = x.floor();
(floor.max(0.0) as u64, x - floor)
}
pub(crate) fn largest_remainder(n: u64, shares: &[f64]) -> Vec<u64> {
let parts: Vec<(u64, f64)> = shares.iter().map(|r| split_at_integer(n as f64 * r)).collect();
let mut out: Vec<u64> = parts.iter().map(|(whole, _)| *whole).collect();
let given: u64 = out.iter().sum();
let mut order: Vec<usize> = (0..shares.len()).collect();
order.sort_by(|a, b| parts[*b].1.total_cmp(&parts[*a].1).then(a.cmp(b)));
for i in order.into_iter().take(n.saturating_sub(given) as usize) {
out[i] += 1;
}
out
}
pub(crate) fn controlled(rows: &[u64], shares: &[f64], totals: &[u64]) -> Result<Vec<Vec<u64>>> {
let (n_rows, n_cols) = (rows.len(), shares.len());
let mut floor = vec![vec![0u64; n_cols]; n_rows];
let mut frac = vec![vec![0f64; n_cols]; n_rows];
for (s, n) in rows.iter().enumerate() {
for (p, r) in shares.iter().enumerate() {
let (whole, part) = split_at_integer(*n as f64 * r);
floor[s][p] = whole;
frac[s][p] = part;
}
}
let row_need: Vec<i64> =
(0..n_rows).map(|s| rows[s] as i64 - floor[s].iter().map(|f| *f as i64).sum::<i64>()).collect();
let col_need: Vec<i64> =
(0..n_cols).map(|p| totals[p] as i64 - (0..n_rows).map(|s| floor[s][p] as i64).sum::<i64>()).collect();
let impossible = || {
Error::invalid(format!(
"no controlled rounding of strata {rows:?} into totals {totals:?}: the totals do not round the shares"
))
};
if row_need.iter().chain(&col_need).any(|n| *n < 0) || row_need.iter().sum::<i64>() != col_need.iter().sum::<i64>()
{
return Err(impossible());
}
let allowed = |s: usize, p: usize| frac[s][p] > 0.0;
let mut up = initial(n_rows, n_cols, &row_need, &col_need, &allowed).ok_or_else(impossible)?;
let mut fixed = vec![vec![None::<bool>; n_cols]; n_rows];
let mut cells: Vec<(usize, usize)> =
(0..n_rows).flat_map(|s| (0..n_cols).map(move |p| (s, p))).filter(|(s, p)| allowed(*s, *p)).collect();
cells.sort_by(|a, b| {
(floor[a.0][a.1] > 0)
.cmp(&(floor[b.0][b.1] > 0))
.then(rows[a.0].cmp(&rows[b.0]))
.then(b.1.cmp(&a.1))
.then(frac[b.0][b.1].total_cmp(&frac[a.0][a.1]))
.then(a.0.cmp(&b.0))
});
for (s, p) in cells {
if up[s][p] {
fixed[s][p] = Some(true);
continue;
}
match exchange(&up, &fixed, &allowed, s, p) {
Some(path) => {
up[s][p] = true;
for (r, c, value) in path {
up[r][c] = value;
}
fixed[s][p] = Some(true);
}
None => fixed[s][p] = Some(false),
}
}
Ok((0..n_rows).map(|s| (0..n_cols).map(|p| floor[s][p] + u64::from(up[s][p])).collect()).collect())
}
fn initial(
n_rows: usize,
n_cols: usize,
row_need: &[i64],
col_need: &[i64],
allowed: &dyn Fn(usize, usize) -> bool,
) -> Option<Vec<Vec<bool>>> {
let mut up = vec![vec![false; n_cols]; n_rows];
let mut row_left = row_need.to_vec();
let mut col_left = col_need.to_vec();
for s in 0..n_rows {
while row_left[s] > 0 {
let mut col_from: Vec<Option<usize>> = vec![None; n_cols];
let mut row_from: Vec<Option<usize>> = vec![None; n_rows];
let mut row_seen = vec![false; n_rows];
row_seen[s] = true;
let mut queue = std::collections::VecDeque::from([s]);
let mut end = None;
'search: while let Some(r) = queue.pop_front() {
for c in 0..n_cols {
if col_from[c].is_some() || up[r][c] || !allowed(r, c) {
continue;
}
col_from[c] = Some(r);
if col_left[c] > 0 {
end = Some(c);
break 'search;
}
for r2 in 0..n_rows {
if up[r2][c] && !row_seen[r2] {
row_seen[r2] = true;
row_from[r2] = Some(c);
queue.push_back(r2);
}
}
}
}
let mut c = end?;
col_left[c] -= 1;
row_left[s] -= 1;
loop {
let r = col_from[c].expect("on the path");
up[r][c] = true;
if r == s {
break;
}
let previous = row_from[r].expect("on the path");
up[r][previous] = false;
c = previous;
}
}
}
col_left.iter().all(|n| *n == 0).then_some(up)
}
fn exchange(
up: &[Vec<bool>],
fixed: &[Vec<Option<bool>>],
allowed: &dyn Fn(usize, usize) -> bool,
s: usize,
p: usize,
) -> Option<Vec<(usize, usize, bool)>> {
let (n_rows, n_cols) = (up.len(), up.first().map_or(0, Vec::len));
let mut row_from: Vec<Option<usize>> = vec![None; n_rows];
let mut col_from: Vec<Option<usize>> = vec![None; n_cols];
let mut col_seen = vec![false; n_cols];
col_seen[p] = true;
let mut queue = std::collections::VecDeque::from([p]);
while let Some(c) = queue.pop_front() {
for r in 0..n_rows {
if !up[r][c] || fixed[r][c] == Some(true) {
continue;
}
if r == s {
let mut path = vec![(s, c, false)];
let mut column = c;
while column != p {
let row = col_from[column].expect("on the path");
path.push((row, column, true));
column = row_from[row].expect("on the path");
path.push((row, column, false));
}
return Some(path);
}
if row_from[r].is_some() {
continue;
}
row_from[r] = Some(c);
for c2 in 0..n_cols {
if col_seen[c2] || up[r][c2] || !allowed(r, c2) || fixed[r][c2] == Some(false) {
continue;
}
col_seen[c2] = true;
col_from[c2] = Some(r);
queue.push_back(c2);
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn sums(cells: &[Vec<u64>]) -> (Vec<u64>, Vec<u64>) {
let rows = cells.iter().map(|r| r.iter().sum()).collect();
let cols = (0..cells[0].len()).map(|p| cells.iter().map(|r| r[p]).sum()).collect();
(rows, cols)
}
#[test]
fn largest_remainder_meets_the_total() {
assert_eq!(largest_remainder(115, &[0.7, 0.15, 0.15]), [81, 17, 17]);
assert_eq!(largest_remainder(10, &[0.7, 0.15, 0.15]), [7, 2, 1]);
assert_eq!(largest_remainder(7, &[0.2; 5]), [2, 2, 1, 1, 1]);
}
#[test]
fn s12_3_controlled_rounding_is_deterministic() {
let shares = [0.7, 0.15, 0.15];
let rows = [60, 30, 10, 3, 5, 7];
let totals = largest_remainder(rows.iter().sum(), &shares);
let cells = controlled(&rows, &shares, &totals).unwrap();
assert_eq!(cells, controlled(&rows, &shares, &totals).unwrap());
let (row_sums, col_sums) = sums(&cells);
assert_eq!(row_sums, rows);
assert_eq!(col_sums, totals);
for (s, n) in rows.iter().enumerate() {
for (p, r) in shares.iter().enumerate() {
let exact = *n as f64 * r;
assert!((cells[s][p] as f64 - exact).abs() < 1.0, "{s},{p}: {} for {exact}", cells[s][p]);
}
}
for rare in 3..6 {
assert!(cells[rare][2] >= 1, "stratum of {} has no test group: {:?}", rows[rare], cells[rare]);
}
}
#[test]
fn whole_products_are_whole() {
let cells = controlled(&[10, 20], &[0.7, 0.3], &[21, 9]).unwrap();
assert_eq!(cells, [[7, 3], [14, 6]]);
}
}