use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "DeclaredQuorum")]
pub struct Quorum {
need: u32,
lenses: Vec<String>,
}
#[derive(Deserialize)]
struct DeclaredQuorum {
need: u32,
lenses: Vec<String>,
}
impl TryFrom<DeclaredQuorum> for Quorum {
type Error = QuorumError;
fn try_from(d: DeclaredQuorum) -> Result<Self, Self::Error> {
Self::new(d.need, d.lenses)
}
}
impl Quorum {
pub fn new<I, S>(need: u32, lenses: I) -> Result<Self, QuorumError>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let lenses: Vec<String> = lenses.into_iter().map(Into::into).collect();
let of = u32::try_from(lenses.len()).unwrap_or(u32::MAX);
if need == 0 {
return Err(QuorumError::NeedsNobody);
}
if need > of {
return Err(QuorumError::Unreachable { need, of });
}
if of > 1 && need * 2 <= of {
return Err(QuorumError::NotAMajority { need, of });
}
let unique: BTreeSet<&String> = lenses.iter().collect();
if unique.len() != lenses.len() {
return Err(QuorumError::RepeatedLens);
}
if lenses.iter().any(|l| l.trim().is_empty()) {
return Err(QuorumError::UnnamedLens);
}
Ok(Self { need, lenses })
}
#[must_use]
pub const fn need(&self) -> u32 {
self.need
}
#[must_use]
pub fn of(&self) -> u32 {
u32::try_from(self.lenses.len()).unwrap_or(u32::MAX)
}
pub fn lenses(&self) -> impl Iterator<Item = &str> {
self.lenses.iter().map(String::as_str)
}
pub fn tally(&self, verdicts: &[(&str, Verdict)]) -> Result<PanelOutcome, QuorumError> {
let mut seen = BTreeSet::new();
for (lens, _) in verdicts {
if !self.lenses.iter().any(|l| l == lens) {
return Err(QuorumError::NotALens((*lens).to_owned()));
}
if !seen.insert(*lens) {
return Err(QuorumError::JudgedTwice((*lens).to_owned()));
}
}
let passed = verdicts.iter().filter(|(_, v)| *v == Verdict::Pass).count();
let failed = verdicts.iter().filter(|(_, v)| *v == Verdict::Fail).count();
let tally = Tally {
passed: u32::try_from(passed).unwrap_or(u32::MAX),
failed: u32::try_from(failed).unwrap_or(u32::MAX),
asked: self.of(),
};
if tally.passed >= self.need {
return Ok(PanelOutcome::Reached(Verdict::Pass, tally));
}
if tally.failed >= self.need {
return Ok(PanelOutcome::Reached(Verdict::Fail, tally));
}
Ok(PanelOutcome::NoQuorum(tally))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Verdict {
Pass,
Fail,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Tally {
pub passed: u32,
pub failed: u32,
pub asked: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PanelOutcome {
Reached(Verdict, Tally),
NoQuorum(Tally),
}
impl PanelOutcome {
#[must_use]
pub const fn decided(&self) -> Option<Verdict> {
match self {
Self::Reached(v, _) => Some(*v),
Self::NoQuorum(_) => None,
}
}
#[must_use]
pub const fn tally(&self) -> Tally {
match self {
Self::Reached(_, t) | Self::NoQuorum(t) => *t,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum QuorumError {
#[error("a quorum needing zero agreeing judgements decides nothing")]
NeedsNobody,
#[error("a quorum of {need} cannot be reached from {of} judgement(s)")]
Unreachable { need: u32, of: u32 },
#[error(
"{need} of {of} is not a majority: the panel could reach a quorum for \
'pass' and for 'fail' at once, and which one is reported would depend \
on tally order rather than on the judgements"
)]
NotAMajority { need: u32, of: u32 },
#[error(
"a lens is repeated; identical judges share their blind spots, so a \
repeated lens is repetition rather than the diversity a quorum is for"
)]
RepeatedLens,
#[error("a lens with no name cannot be a distinct angle")]
UnnamedLens,
#[error("a judgement from '{0}', which is not a lens of this panel")]
NotALens(String),
#[error("lens '{0}' was judged twice; one judge counted twice is not two judges")]
JudgedTwice(String),
}
#[cfg(test)]
mod tests {
use super::*;
fn q(need: u32, lenses: &[&str]) -> Quorum {
Quorum::new(need, lenses.iter().copied()).expect("valid")
}
#[test]
fn a_declared_panel_reports_its_shape() {
let quorum = q(2, &["correctness", "policy", "arithmetic"]);
assert_eq!(quorum.need(), 2);
assert_eq!(quorum.of(), 3);
assert_eq!(
quorum.lenses().collect::<Vec<_>>(),
vec!["correctness", "policy", "arithmetic"]
);
}
#[test]
fn repeating_a_lens_is_refused() {
let err = Quorum::new(2, ["correctness", "correctness", "policy"])
.expect_err("identical judges are not a panel");
assert_eq!(err, QuorumError::RepeatedLens);
}
#[test]
fn a_quorum_larger_than_the_panel_is_refused() {
assert_eq!(
Quorum::new(4, ["a", "b", "c"]).expect_err("unreachable"),
QuorumError::Unreachable { need: 4, of: 3 }
);
}
#[test]
fn a_quorum_of_zero_is_refused() {
assert_eq!(
Quorum::new(0, ["a"]).expect_err("decides nothing"),
QuorumError::NeedsNobody
);
}
#[test]
fn a_threshold_that_both_sides_could_meet_is_refused() {
assert_eq!(
Quorum::new(2, ["a", "b", "c", "d"]).expect_err("2 of 4 is not a majority"),
QuorumError::NotAMajority { need: 2, of: 4 }
);
assert_eq!(
Quorum::new(1, ["a", "b"]).expect_err("1 of 2 is any judge deciding alone"),
QuorumError::NotAMajority { need: 1, of: 2 }
);
}
#[test]
fn unanimity_is_allowed() {
assert!(Quorum::new(2, ["a", "b"]).is_ok());
assert!(Quorum::new(3, ["a", "b", "c"]).is_ok());
assert!(Quorum::new(1, ["a"]).is_ok(), "a panel of one is a panel");
}
#[test]
fn an_agreeing_panel_decides() {
let quorum = q(2, &["a", "b", "c"]);
let out = quorum
.tally(&[
("a", Verdict::Pass),
("b", Verdict::Pass),
("c", Verdict::Fail),
])
.unwrap();
assert_eq!(out.decided(), Some(Verdict::Pass));
assert_eq!(out.tally().passed, 2);
assert_eq!(out.tally().failed, 1);
}
#[test]
fn a_panel_agreeing_to_refuse_also_decides() {
let quorum = q(2, &["a", "b", "c"]);
let out = quorum
.tally(&[
("a", Verdict::Fail),
("b", Verdict::Fail),
("c", Verdict::Pass),
])
.unwrap();
assert_eq!(out.decided(), Some(Verdict::Fail));
}
#[test]
fn a_split_panel_decides_nothing() {
let quorum = q(3, &["a", "b", "c"]);
let out = quorum
.tally(&[
("a", Verdict::Pass),
("b", Verdict::Pass),
("c", Verdict::Fail),
])
.unwrap();
assert_eq!(
out.decided(),
None,
"2 of 3 where 3 was required is a disagreement, and reporting the \
majority converts 'we do not know' into 'approved'"
);
assert!(matches!(out, PanelOutcome::NoQuorum(_)));
assert_eq!(out.tally().passed, 2, "the tally is still reported");
}
#[test]
fn a_short_panel_does_not_reach_quorum() {
let quorum = q(2, &["a", "b", "c"]);
let out = quorum.tally(&[("a", Verdict::Pass)]).unwrap();
assert_eq!(out.decided(), None);
assert_eq!(out.tally().asked, 3, "the shortfall is visible");
}
#[test]
fn an_empty_panel_decides_nothing() {
let quorum = q(2, &["a", "b", "c"]);
assert_eq!(quorum.tally(&[]).unwrap().decided(), None);
}
#[test]
fn a_lens_judged_twice_is_refused() {
let quorum = q(2, &["a", "b", "c"]);
assert_eq!(
quorum.tally(&[("a", Verdict::Pass), ("a", Verdict::Pass)]),
Err(QuorumError::JudgedTwice("a".to_owned())),
"four passes over three lenses must not reach a quorum"
);
assert_eq!(
quorum.tally(&[("a", Verdict::Pass), ("z", Verdict::Pass)]),
Err(QuorumError::NotALens("z".to_owned()))
);
}
#[test]
fn an_unnamed_lens_is_refused() {
assert_eq!(
Quorum::new(1, [" "]).expect_err("unnamed"),
QuorumError::UnnamedLens
);
}
}