diff --git a/src/stv/mod.rs b/src/stv/mod.rs index b99b894..f2916aa 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -212,24 +212,68 @@ impl STVOptions { /// Validate the combination of [STVOptions] and error if invalid pub fn validate(&self) -> Result<(), STVError> { if self.surplus == SurplusMethod::Meek { - if self.quota_mode != QuotaMode::DynamicByTotal { return Err(STVError::InvalidOptions("--surplus meek requires --quota-mode dynamic_by_total")); } - if self.transferable_only { return Err(STVError::InvalidOptions("--surplus meek is incompatible with --transferable-only")); } - if self.exclusion != ExclusionMethod::SingleStage { return Err(STVError::InvalidOptions("--surplus meek requires --exclusion single_stage")); } - if self.constraints_path.is_some() && self.constraint_mode == ConstraintMode::RepeatCount { return Err(STVError::InvalidOptions("--constraint-mode repeat_count requires a Gregory method for --surplus")); } // TODO: NYI? + if self.quota_mode == QuotaMode::ERS97 { + // Invalid because keep values cannot be calculated for a candidate elected with less than a surplus + return Err(STVError::InvalidOptions("--surplus meek is incompatible with --quota-mode ers97")); + } + if self.quota_mode == QuotaMode::ERS76 { + // Invalid because keep values cannot be calculated for a candidate elected with less than a surplus + return Err(STVError::InvalidOptions("--surplus meek is incompatible with --quota-mode ers76")); + } + if self.quota_mode == QuotaMode::DynamicByActive { + // Invalid because all votes are "active" in Meek STV + return Err(STVError::InvalidOptions("--surplus meek is incompatible with --quota-mode dynamic_by_active")); + } + if self.transferable_only { + // Invalid because this would imply a different keep value applies to nontransferable ballots (?) + // TODO: NYI? + return Err(STVError::InvalidOptions("--surplus meek is incompatible with --transferable-only")); + } + if self.exclusion != ExclusionMethod::SingleStage { + // Invalid because Meek STV is independent of order of exclusion, so segmented exclusion has no impact + return Err(STVError::InvalidOptions("--surplus meek requires --exclusion single_stage")); + } + if self.constraints_path.is_some() && self.constraint_mode == ConstraintMode::RepeatCount { + // TODO: NYI? + return Err(STVError::InvalidOptions("--constraint-mode repeat_count requires a Gregory method for --surplus")); + } } if self.surplus == SurplusMethod::IHare || self.surplus == SurplusMethod::Hare { - if self.round_quota != Some(0) { return Err(STVError::InvalidOptions("--surplus ihare and --surplus hare require --round-quota 0")); } - if self.sample == SampleMethod::StratifyLR && self.sample_per_ballot { return Err(STVError::InvalidOptions("--sample stratify is incompatible with --sample-per-ballot")); } - //if self.sample == SampleMethod::StratifyFloor && self.sample_per_ballot { return Err(STVError::InvalidOptions("--sample stratify_floor is incompatible with --sample-per-ballot")); } - if self.sample_per_ballot && !self.immediate_elect { return Err(STVError::InvalidOptions("--sample-per-ballot is incompatible with --no-immediate-elect")); } - if self.constraints_path.is_some() && self.constraint_mode == ConstraintMode::RepeatCount { return Err(STVError::InvalidOptions("--constraint-mode repeat_count requires a Gregory method for --surplus")); } // TODO: NYI? + if self.round_quota != Some(0) { + // Invalid because votes are counted only in whole numbers + return Err(STVError::InvalidOptions("--surplus ihare and --surplus hare require --round-quota 0")); + } + if self.sample == SampleMethod::StratifyLR && self.sample_per_ballot { + // Invalid because a stratification cannot be made until all relevant ballots are transferred + return Err(STVError::InvalidOptions("--sample stratify is incompatible with --sample-per-ballot")); + } + if self.sample_per_ballot && !self.immediate_elect { + // Invalid because otherwise --sample-per-ballot would be ineffectual + return Err(STVError::InvalidOptions("--sample-per-ballot is incompatible with --no-immediate-elect")); + } + if self.constraints_path.is_some() && self.constraint_mode == ConstraintMode::RepeatCount { + // TODO: NYI? + return Err(STVError::InvalidOptions("--constraint-mode repeat_count requires a Gregory method for --surplus")); + } } if self.subtract_nontransferable { - if self.surplus != SurplusMethod::WIG { return Err(STVError::InvalidOptions("--subtract-nontransferable requires --surplus wig")) } - if !self.transferable_only { return Err(STVError::InvalidOptions("--subtract-nontransferable requires --transferable-only")) } + if self.surplus != SurplusMethod::WIG { + // Invalid because other methods do not distinguish between ballots of different value during surplus transfer + return Err(STVError::InvalidOptions("--subtract-nontransferable requires --surplus wig")); + } + if !self.transferable_only { + // Invalid because nontransferables are only subtracted with --transferable-only + return Err(STVError::InvalidOptions("--subtract-nontransferable requires --transferable-only")); + } + } + if self.min_threshold != "0" && self.defer_surpluses { + // TODO: NYI + return Err(STVError::InvalidOptions("--min-threshold is incompatible with --defer-surpluses (not yet implemented)")); + } + if self.round_subtransfers == RoundSubtransfersMode::ByValueAndSource && self.bulk_exclude { + // TODO: NYI + return Err(STVError::InvalidOptions("--round-subtransfers by_value_and_source is incompatible with --bulk-exclude (not yet implemented)")); } - if self.min_threshold != "0" && self.defer_surpluses { return Err(STVError::InvalidOptions("--min-threshold is incompatible with --defer-surpluses (not yet implemented)")); } // TODO: NYI - if self.round_subtransfers == RoundSubtransfersMode::ByValueAndSource && self.bulk_exclude { return Err(STVError::InvalidOptions("--round-subtransfers by_value_and_source is incompatible with --bulk-exclude (not yet implemented)")); } // TODO: NYI return Ok(()); } } @@ -1792,7 +1836,8 @@ pub fn should_show_vre(opts: &STVOptions) -> bool { if opts.quota_mode == QuotaMode::ERS97 || opts.quota_mode == QuotaMode::ERS76 { return true; } - if opts.surplus == SurplusMethod::Meek { + if opts.surplus == SurplusMethod::Meek && opts.quota_mode == QuotaMode::DynamicByTotal { + // Meek method ensures that, if the quota is recalculated, every candidate will be elected with a quota return false; } if opts.early_bulk_elect {