0.1.0
This commit is contained in:
parent
1a640d4056
commit
fc4063c13b
0
vote-counter/.gitignore → .gitignore
vendored
0
vote-counter/.gitignore → .gitignore
vendored
328
Cargo.lock
generated
Normal file
328
Cargo.lock
generated
Normal file
@ -0,0 +1,328 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d53da17d37dba964b9b3ecb5c5a1f193a2762c700e6829201e645b9381c99dc7"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"bitflags",
|
||||
"clap_derive",
|
||||
"clap_lex",
|
||||
"indexmap",
|
||||
"once_cell",
|
||||
"strsim",
|
||||
"termcolor",
|
||||
"textwrap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "3.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c11d40217d16aee8508cc8e5fde8b4ff24639758608e5374e731b53f85749fb9"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5538cd660450ebeb4234cfecf8f2284b844ffc4c50531e66d584ad5b91293613"
|
||||
dependencies = [
|
||||
"os_str_bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"lazy_static",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv"
|
||||
version = "1.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"csv-core",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv-core"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exitcode"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c6392766afd7964e2531940894cffe4bd8d7d17dbc3c1c4857040fd4b33bdb3"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.137"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "vote-counter"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"colored",
|
||||
"csv",
|
||||
"exitcode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "vote-counter"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "An opinionated single transferrable vote counter for the command line."
|
||||
license = "MIT OR Apache-2.0"
|
||||
authors = ["Aaron Manning"]
|
||||
repository = "https://github.com/aaron-jack-manning/vote-counter"
|
||||
|
||||
[dependencies]
|
||||
csv = "1.1.6"
|
||||
clap = { version = "3.2.5", features = ["derive"] }
|
||||
exitcode = "1.1.2"
|
||||
colored = "2"
|
93
README.md
93
README.md
@ -1,58 +1,77 @@
|
||||
# Vote Counter
|
||||
# Vote Counter
|
||||
|
||||
This is just a program that I wrote to count instant runoff votes, according to a certain specification.
|
||||
An opinionated single transferrable vote counter for the command line.
|
||||
|
||||
## Usage
|
||||
## Installation
|
||||
|
||||
It's usage is as follows:
|
||||
Installation can be done via cargo by running:
|
||||
|
||||
```
|
||||
vote-counter <CSV_PATH> <THRESHOLD>
|
||||
cargo install vote-counter
|
||||
```
|
||||
|
||||
The first command line arguments is the path to the `.csv` file containing the ballot papers, and the second is the threshold required to win, where 0.5 corresponds to a simple majority.
|
||||
## Demo
|
||||
|
||||
A sample `csv` file in the appropriate format is provided at the top level of this repository. Run:
|
||||
|
||||
```
|
||||
vote-counter sample.csv --report
|
||||
```
|
||||
|
||||
to count the votes from that file.
|
||||
|
||||
## Arguments
|
||||
|
||||
Running `vote-counter --help` will output the following:
|
||||
|
||||
```
|
||||
USAGE:
|
||||
vote-counter [OPTIONS] <PATH>
|
||||
|
||||
ARGS:
|
||||
<PATH> Path to the CSV containing the ballots
|
||||
|
||||
OPTIONS:
|
||||
-h, --help Print help information
|
||||
--report Generate report of counting
|
||||
-t, --threshold <THRESHOLD> Threshold to win [default: 0.5]
|
||||
-V, --version Print version information
|
||||
```
|
||||
|
||||
explaining each argument and how to use it.
|
||||
|
||||
## Ballot File
|
||||
|
||||
Here is a sample ballot file:
|
||||
The ballot file should be a `csv` formatted as below:
|
||||
|
||||
| Peter | Mia | Hannah | Lee |
|
||||
| ----- | --- | ------ | --- |
|
||||
| 1 | 2 | | 3 |
|
||||
| 2 | 4 | 3 | 1 |
|
||||
| | | 1 | |
|
||||
| Peter | Mia | Hannah | Lee | Fred | Julia |
|
||||
| ----- | --- | ------ | --- | ---- | ----- |
|
||||
| | 2 | 1 | | | 3 |
|
||||
| 1 | | 2 | 3 | 4 | |
|
||||
| 5 | 4 | 3 | 1 | 2 | 6 |
|
||||
|
||||
Each row represents a ballot paper, where preferenced are expressed starting at 1, and continuing until the voter no longer has a preference.
|
||||
|
||||
## Invalid Votes
|
||||
## Validity of Votes
|
||||
|
||||
The following votes are considered invalid:
|
||||
This program is generally permissive in the votes that are considered valid. If a ballot includes any number of non-negative preference numbers, none of which are repeating, the ballot is valid.
|
||||
|
||||
- Multiple occurances of the same preference, for example:
|
||||
An invalid ballot occurs when the same preference is expressed twice.
|
||||
|
||||
| Peter | Mia | Hannah | Lee |
|
||||
| ----- | --- | ------ | --- |
|
||||
| 1 | 1 | | 3 |
|
||||
For example, the following are not valid:
|
||||
|
||||
- A preference number which exceeds the number of candidates, for example:
|
||||
| Peter | Mia | Hannah | Lee | Fred | Julia |
|
||||
| ----- | --- | ------ | --- | ---- | ----- |
|
||||
| 1 | 1 | | 3 | | |
|
||||
| 0 | 1 | | 4 | | 4 |
|
||||
| 2 | | 2 | | 1 | |
|
||||
|
||||
| Peter | Mia | Hannah | Lee |
|
||||
| ----- | --- | ------ | --- |
|
||||
| 3 | 2 | 1 | 5 |
|
||||
However the following are valid:
|
||||
|
||||
To be warned about invalid ballots, run in debug mode.
|
||||
|
||||
The code has been internally documented reasonably thoroughly so if you want to fork the repo and change the logic surrounding invalid votes I hope I have made that reasonably easy.
|
||||
|
||||
## Permitted Votes
|
||||
|
||||
Aside from the obviously valid votes, which number candidates 1 to a given preference as far as the voter may chose, votes which skip a preference are also considered valid, for example:
|
||||
|
||||
| Peter | Mia | Hannah | Lee |
|
||||
| ----- | --- | ------ | --- |
|
||||
| | | 3 | 1 |
|
||||
|
||||
where preferences are shuffled down such that in the above example, Hannah is considered to be the second preference.
|
||||
|
||||
A sample ballots file called `sample.csv` is provided which includes only valid ballots.
|
||||
| Peter | Mia | Hannah | Lee | Fred | Julia |
|
||||
| ----- | --- | ------ | --- | ---- | ----- |
|
||||
| 0 | 1 | 2 | | | 3 |
|
||||
| | | 1 | 4 | | |
|
||||
| 2 | | 5 | | | 1 |
|
||||
|
||||
Negative numbers are simply ignored.
|
||||
|
28
sample.csv
28
sample.csv
@ -1,9 +1,19 @@
|
||||
Peter,Mia,Hannah,Lee
|
||||
3,,1,2
|
||||
2,1,,3
|
||||
,1,,
|
||||
,2,3,1
|
||||
4,3,1,2
|
||||
1,2,3,4
|
||||
1,2,,4
|
||||
,,,1
|
||||
Peter,Mia,Hannah,Lee,Fred,Julia
|
||||
1,2,3,4,5,6
|
||||
,4,3,,1,2
|
||||
,1,2,,5,
|
||||
3,6,,1,,
|
||||
,1,1,2,,
|
||||
,,1,,2,3
|
||||
,,,,,
|
||||
,3,2,1,,
|
||||
1,2,,,,
|
||||
,,1,,,
|
||||
,,3,1,2,
|
||||
,1,2,8,,
|
||||
,5,2,,0,
|
||||
1,2,,7,,
|
||||
,,3,4,,7
|
||||
,,,,1,
|
||||
,,,3,2,1
|
||||
,3,2,,,1
|
||||
|
|
72
src/ballot.rs
Normal file
72
src/ballot.rs
Normal file
@ -0,0 +1,72 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Represents a ballot paper.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Ballot(Vec<usize>);
|
||||
|
||||
impl Ballot {
|
||||
/// Creates a new ballot from the a `Vec<usize> where the values within the `Vec` are
|
||||
/// candidates and the order within the `Vec` expresses preference.
|
||||
pub fn new(ballot : Vec<usize>) -> Ballot {
|
||||
Ballot(ballot)
|
||||
}
|
||||
|
||||
/// Creates an iterator over the undelying `Vec<usize>` within the `Ballot`.
|
||||
pub fn iter(&self) -> std::slice::Iter<'_, usize> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
/// Removes the specified candidates from the ballot.
|
||||
pub fn remove_candidates(ballot : Ballot, to_remove : &[usize]) -> Option<Ballot> {
|
||||
let new_ballot: Vec<_> =
|
||||
ballot.0
|
||||
.into_iter()
|
||||
.filter(|c| !to_remove.contains(c))
|
||||
.collect();
|
||||
|
||||
match new_ballot.len() {
|
||||
0 => None,
|
||||
_ => Some(Ballot::new(new_ballot))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the highest preference candidate.
|
||||
pub fn first_pref(&self) -> usize {
|
||||
self.0[0]
|
||||
}
|
||||
|
||||
/// Creates a ballot from the representation read from the file.
|
||||
pub fn from_raw_ballot(raw_ballot : Vec<Option<usize>>) -> Result<Ballot, Vec<Option<usize>>> {
|
||||
let mut pref_pairs = Vec::with_capacity(raw_ballot.len());
|
||||
|
||||
let mut preference_set = HashSet::with_capacity(raw_ballot.len());
|
||||
|
||||
for (candidate, preference) in raw_ballot.iter().enumerate() {
|
||||
if let Some(preference) = preference {
|
||||
if !preference_set.insert(preference) {
|
||||
// Value already existed in set, which means preference was expressed twice.
|
||||
return Err(raw_ballot);
|
||||
}
|
||||
pref_pairs.push((preference, candidate));
|
||||
}
|
||||
}
|
||||
|
||||
match pref_pairs.len() {
|
||||
// No preference was expressed at all.
|
||||
0 => Err(raw_ballot),
|
||||
_ => {
|
||||
// Sort the ballot by order of preference.
|
||||
pref_pairs.sort_by(|(p1, _), (p2, _)| p1.cmp(p2));
|
||||
|
||||
// Resolve the preference-candidate pairs to just the candidate.
|
||||
let ballot =
|
||||
pref_pairs
|
||||
.into_iter()
|
||||
.map(|(_, c)| c)
|
||||
.collect();
|
||||
|
||||
Ok(Ballot(ballot))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
296
src/ballot_box.rs
Normal file
296
src/ballot_box.rs
Normal file
@ -0,0 +1,296 @@
|
||||
use std::mem;
|
||||
use std::path;
|
||||
|
||||
use crate::candidates::Candidates;
|
||||
use crate::reporting;
|
||||
use crate::ballot::Ballot;
|
||||
|
||||
/// Represents the current status of the count, and how to proceed counting.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum CountStatus {
|
||||
Winner(usize),
|
||||
Tie,
|
||||
Promotion(Vec<usize>),
|
||||
Runoff(Vec<usize>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// Node of trie like structure representing the votes. This stores ballots with common starting
|
||||
/// preference, using the endings value to count how many votes expressed the same preference from
|
||||
/// the top to that node. Each 'level' of the structure represents a preference, with each
|
||||
/// candidate appearing in the `children` field's vector in order.
|
||||
struct BallotBoxNode {
|
||||
total_beneath : u32,
|
||||
endings : u32,
|
||||
children : Vec<Option<BallotBoxNode>>,
|
||||
}
|
||||
|
||||
impl BallotBoxNode {
|
||||
/// Creates a new, empty ballot box node.
|
||||
fn new(children : usize) -> Self {
|
||||
BallotBoxNode {
|
||||
total_beneath : 0,
|
||||
endings : 0,
|
||||
children : vec![None; children],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores list of candidates, total number of votes, the candidates which have been eliminated and
|
||||
/// the votes themselves using a `BallotBoxNode`s.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BallotBox {
|
||||
eliminated : Vec<bool>,
|
||||
total_votes : u32,
|
||||
nodes : Vec<Option<BallotBoxNode>>,
|
||||
pub candidates : Candidates,
|
||||
}
|
||||
|
||||
impl BallotBox {
|
||||
/// Creates a new, empty ballot box.
|
||||
fn new(candidates : Candidates) -> Self {
|
||||
BallotBox {
|
||||
eliminated : vec![true; candidates.len()],
|
||||
total_votes : 0,
|
||||
nodes : vec![None; candidates.len()],
|
||||
candidates,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads and fills the ballot box from a file.
|
||||
pub fn from_file(path : &path::PathBuf, report : bool) -> Result<BallotBox, csv::Error> {
|
||||
|
||||
let mut reader =
|
||||
csv::ReaderBuilder::new()
|
||||
.has_headers(true)
|
||||
.from_path(path)?;
|
||||
|
||||
// Read the headers and create the candidates.
|
||||
let headers = reader.headers()?;
|
||||
|
||||
let candidates : Vec<String> =
|
||||
headers
|
||||
.into_iter()
|
||||
.map(|x| (*x).parse::<String>())
|
||||
.map(|x| x.unwrap())
|
||||
.collect();
|
||||
|
||||
let candidates = Candidates::new(candidates);
|
||||
|
||||
let mut ballot_box = BallotBox::new(candidates);
|
||||
|
||||
let mut counter = 1;
|
||||
for result in reader.records() {
|
||||
let mut raw_ballot = Vec::new();
|
||||
counter += 1;
|
||||
|
||||
for value in result?.iter() {
|
||||
raw_ballot.push(value.parse::<usize>().ok())
|
||||
}
|
||||
|
||||
match Ballot::from_raw_ballot(raw_ballot) {
|
||||
Ok(ballot) => ballot_box.push(ballot, 1),
|
||||
Err(raw_ballot) => reporting::invalid_ballot(counter, &raw_ballot, report),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ballot_box)
|
||||
}
|
||||
|
||||
/// Returns a collection of all eliminated candidates.
|
||||
fn eliminated(&self) -> Vec<usize> {
|
||||
let mut eliminated = Vec::new();
|
||||
|
||||
for i in 0..self.candidates.len() {
|
||||
if self.eliminated[i] {
|
||||
eliminated.push(i)
|
||||
}
|
||||
}
|
||||
|
||||
eliminated
|
||||
}
|
||||
|
||||
/// Returns the number of remaining candidates which have yet to be eliminated.
|
||||
fn remaining(&self) -> usize {
|
||||
self
|
||||
.eliminated
|
||||
.iter()
|
||||
.filter(|b| !*b)
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Adds the provided ballot to the `BallotBox` `quantity` times.
|
||||
fn push(&mut self, ballot : Ballot, quantity : u32) {
|
||||
|
||||
// All candidates are marked as eliminated at the start, so this may need to change as each
|
||||
// new ballot is added in.
|
||||
self.eliminated[ballot.first_pref()] = false;
|
||||
|
||||
// Update the total number of votes at the top level.
|
||||
self.total_votes += quantity;
|
||||
|
||||
let mut current_node : Option<&mut BallotBoxNode> = None;
|
||||
|
||||
for (_, &candidate) in ballot.iter().enumerate() {
|
||||
|
||||
// Traverse down the trie appropriately depending on if it is currently at the top
|
||||
// level or not.
|
||||
current_node = match current_node {
|
||||
None => {
|
||||
if self.nodes[candidate].is_none() {
|
||||
self.nodes[candidate] = Some(BallotBoxNode::new(self.candidates.len()));
|
||||
}
|
||||
|
||||
let children = &mut self.nodes;
|
||||
Some(children[candidate].as_mut().unwrap())
|
||||
},
|
||||
Some(current_node) => {
|
||||
if current_node.children[candidate].is_none() {
|
||||
current_node.children[candidate] = Some(BallotBoxNode::new(self.candidates.len()));
|
||||
}
|
||||
|
||||
let children = &mut current_node.children;
|
||||
Some(children[candidate].as_mut().unwrap())
|
||||
}
|
||||
};
|
||||
|
||||
// Update the total number of votes under the current node.
|
||||
current_node.as_mut().unwrap().total_beneath += quantity;
|
||||
}
|
||||
|
||||
// Update the endings count on the last node.
|
||||
current_node.unwrap().endings += quantity;
|
||||
}
|
||||
|
||||
|
||||
// Gives the current status of the count, and indicates who needs to be eliminated in a runoff
|
||||
// if necessary.
|
||||
pub fn status(&self, threshold : f64, report : bool) -> CountStatus {
|
||||
let totals : Vec<u32> =
|
||||
self
|
||||
.nodes
|
||||
.iter()
|
||||
.map(|n| match n {
|
||||
None => 0,
|
||||
Some(node) => node.total_beneath,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let max = *totals.iter().max().unwrap();
|
||||
let min = *totals.iter().filter(|x| x != &&0).min().unwrap();
|
||||
|
||||
let winners =
|
||||
totals
|
||||
.iter()
|
||||
.enumerate()
|
||||
.fold(Vec::new(), |mut winners, (candidate, total)| {
|
||||
if total == &max {
|
||||
winners.push(candidate);
|
||||
};
|
||||
|
||||
winners
|
||||
});
|
||||
|
||||
let losers =
|
||||
totals
|
||||
.iter()
|
||||
.enumerate()
|
||||
.fold(Vec::new(), |mut losers, (candidate, total)| {
|
||||
if total == &min {
|
||||
losers.push(candidate);
|
||||
};
|
||||
|
||||
losers
|
||||
});
|
||||
|
||||
reporting::current_count(totals.iter().enumerate().map(|(a, b)| (a, *b)).collect(), &self.candidates, report);
|
||||
|
||||
// All votes have been reduced to 0.
|
||||
let status = if max == 0 {
|
||||
CountStatus::Tie
|
||||
}
|
||||
// A unique winner has been determined.
|
||||
else if winners.len() == 1 && f64::try_from(max).unwrap() >= (threshold * f64::try_from(self.total_votes).unwrap()) {
|
||||
CountStatus::Winner(winners[0])
|
||||
}
|
||||
// All remaining candidates are on equal votes.
|
||||
else if winners.len() == self.remaining() {
|
||||
CountStatus::Promotion(winners)
|
||||
}
|
||||
// Distribute the votes of all losers.
|
||||
else {
|
||||
CountStatus::Runoff(losers)
|
||||
};
|
||||
|
||||
reporting::status(&status, &self.candidates, report);
|
||||
|
||||
status
|
||||
}
|
||||
|
||||
/// Promotes lower preference votes of the provided candidates.
|
||||
pub fn promote(&mut self, to_promote : Vec<usize>) {
|
||||
self.runoff_or_promote(to_promote, false);
|
||||
}
|
||||
|
||||
/// Eliminates the provided candidates and distributes their votes.
|
||||
pub fn runoff(&mut self, to_eliminate : Vec<usize>) {
|
||||
self.runoff_or_promote(to_eliminate, true);
|
||||
}
|
||||
|
||||
fn runoff_or_promote(&mut self, to_promote_or_eliminate : Vec<usize>, runoff : bool) {
|
||||
// Vector of ballots and the quantity to redistribute.
|
||||
let mut adjusted_votes : Vec<(Ballot, u32)> = Vec::new();
|
||||
|
||||
for candidate in to_promote_or_eliminate {
|
||||
// Swap the votes to distribute out.
|
||||
let mut to_distribute = None;
|
||||
mem::swap(&mut self.nodes[candidate], &mut to_distribute);
|
||||
let to_distribute = to_distribute.unwrap();
|
||||
|
||||
// Update the top level total.
|
||||
self.total_votes -= to_distribute.total_beneath;
|
||||
|
||||
BallotBox::distribute(&to_distribute, Vec::new(), &mut adjusted_votes);
|
||||
|
||||
// Update the array of eliminated candidates.
|
||||
if runoff {
|
||||
self.eliminated[candidate] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine all previously eliminated candidates (including in this round).
|
||||
let eliminated_candidates : Vec<usize> = self.eliminated();
|
||||
|
||||
for (vote, qty) in adjusted_votes {
|
||||
// Remove any preferences expressed for the candidates which have already been
|
||||
// eliminated, and add the remaining ballot if it is non-empty.
|
||||
if let Some(vote) = Ballot::remove_candidates(vote, &eliminated_candidates) {
|
||||
self.push(vote, qty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function for `runoff_or_promote` which handles the calculating of votes that need to
|
||||
/// be distributed.
|
||||
fn distribute(to_distribute : &BallotBoxNode, current_ballot : Vec<usize>, adjusted_votes : &mut Vec<(Ballot, u32)>) {
|
||||
for (candidate, child) in to_distribute.children.iter().enumerate() {
|
||||
if let Some(node) = child {
|
||||
// Clone the current ballot so that new values can be added as passed down.
|
||||
let mut next_ballot = current_ballot.clone();
|
||||
// Add the current candidate to the ballot.
|
||||
next_ballot.push(candidate);
|
||||
|
||||
BallotBox::distribute(node, next_ballot, adjusted_votes);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the current ballot to the collection with the corresponding count.
|
||||
// This will intentionally ignore ballots at the top level, which are being distributed
|
||||
// anyway.
|
||||
if to_distribute.endings > 0 {
|
||||
adjusted_votes.push((Ballot::new(current_ballot), to_distribute.endings));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
20
src/candidates.rs
Normal file
20
src/candidates.rs
Normal file
@ -0,0 +1,20 @@
|
||||
/// Collection of candidates, in the same order as the `csv`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Candidates(Vec<String>);
|
||||
|
||||
impl Candidates {
|
||||
/// Creates a new instance of `Candidates` from a `Vec<String>`.
|
||||
pub fn new(candidates : Vec<String>) -> Self {
|
||||
Candidates(candidates)
|
||||
}
|
||||
|
||||
/// Gets a candidate's name based on their index.
|
||||
pub fn get(&self, candidate : usize) -> Option<&String> {
|
||||
self.0.get(candidate)
|
||||
}
|
||||
|
||||
/// Returns the number of candidates.
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
}
|
61
src/main.rs
Normal file
61
src/main.rs
Normal file
@ -0,0 +1,61 @@
|
||||
mod ballot_box;
|
||||
mod reporting;
|
||||
mod candidates;
|
||||
mod ballot;
|
||||
|
||||
use ballot_box::BallotBox;
|
||||
use ballot_box::CountStatus::*;
|
||||
|
||||
use std::path;
|
||||
use std::process;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(author, version)]
|
||||
/// Stores the command line arguments.
|
||||
struct Args {
|
||||
/// Path to the CSV containing the ballots.
|
||||
#[clap()]
|
||||
path : path::PathBuf,
|
||||
|
||||
/// Threshold to win.
|
||||
#[clap(long, short, default_value = "0.5")]
|
||||
threshold : f64,
|
||||
|
||||
/// Generate report of counting.
|
||||
#[clap(long, takes_value = false)]
|
||||
report : bool,
|
||||
}
|
||||
|
||||
/// Primary entry point to vote counting algorithms.
|
||||
fn count(args : Args) -> Result<(), csv::Error> {
|
||||
|
||||
let mut ballot_box = BallotBox::from_file(&args.path, args.report)?;
|
||||
|
||||
let winner = loop {
|
||||
match ballot_box.status(args.threshold, args.report) {
|
||||
Winner(winner) => break Some(winner),
|
||||
Tie => break None,
|
||||
Runoff(to_eliminated) => ballot_box.runoff(to_eliminated),
|
||||
Promotion(to_promote) => ballot_box.promote(to_promote),
|
||||
}
|
||||
};
|
||||
|
||||
reporting::winner(winner, &ballot_box.candidates);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = Args::parse();
|
||||
match count(args) {
|
||||
Ok(_) => {
|
||||
process::exit(exitcode::OK);
|
||||
},
|
||||
Err(error) => {
|
||||
println!("An error occured reading the CSV data: {}", error);
|
||||
process::exit(exitcode::DATAERR);
|
||||
}
|
||||
}
|
||||
}
|
62
src/reporting.rs
Normal file
62
src/reporting.rs
Normal file
@ -0,0 +1,62 @@
|
||||
use colored::*;
|
||||
|
||||
use crate::ballot_box::{
|
||||
CountStatus,
|
||||
CountStatus::*
|
||||
};
|
||||
use crate::candidates::Candidates;
|
||||
|
||||
/// Displays the invalid ballot provided.
|
||||
pub fn invalid_ballot(number : u32, ballot : &[Option<usize>], report : bool) {
|
||||
if report {
|
||||
let segments : Vec<_> =
|
||||
ballot
|
||||
.iter()
|
||||
.map(|op| {
|
||||
match op {
|
||||
None => String::from("_"),
|
||||
Some(pref) => pref.to_string(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let formatted = segments.join(",");
|
||||
println!("{} {} (line: {})", "Invalid Ballot:".bright_green().bold(), formatted, number);
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays the current count of top preference votes.
|
||||
pub fn current_count(count : Vec<(usize, u32)>, candidates : &Candidates, report : bool) {
|
||||
if report {
|
||||
println!("{}", "Current Count:".bright_yellow().bold());
|
||||
|
||||
for (candidate, votes) in count {
|
||||
println!(" {} : {}", candidates.get(candidate).unwrap(), votes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays a `CountStatus` and associated data if it is a `Runoff` or `Promotion`.
|
||||
pub fn status(status : &CountStatus, candidates : &Candidates, report : bool) {
|
||||
if report {
|
||||
match status {
|
||||
Runoff(to_distribute) => {
|
||||
let candidates = to_distribute.iter().map(|c| candidates.get(*c).unwrap().clone()).collect::<Vec<String>>().join(", ");
|
||||
println!("{} {}", "Eliminating:".bright_magenta(), candidates);
|
||||
},
|
||||
Promotion(to_promote) => {
|
||||
let candidates = to_promote.iter().map(|c| candidates.get(*c).unwrap().clone()).collect::<Vec<String>>().join(", ");
|
||||
println!("Resolving tie between: {}", candidates.bright_cyan());
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays the winner.
|
||||
pub fn winner(winner : Option<usize>, candidates : &Candidates) {
|
||||
match winner {
|
||||
Some(winner) => println!("{} {}", "Winner:".bright_blue(), candidates.get(winner).unwrap()),
|
||||
None => println!("{}", "The election was a tie".bright_blue()),
|
||||
}
|
||||
}
|
80
vote-counter/Cargo.lock
generated
80
vote-counter/Cargo.lock
generated
@ -1,80 +0,0 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv"
|
||||
version = "1.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"csv-core",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv-core"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.137"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
|
||||
|
||||
[[package]]
|
||||
name = "vote-counter"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"csv",
|
||||
]
|
@ -1,7 +0,0 @@
|
||||
[package]
|
||||
name = "vote-counter"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
csv = "1.1.6"
|
@ -1,355 +0,0 @@
|
||||
use std::path;
|
||||
use std::mem;
|
||||
use std::env;
|
||||
|
||||
/// Represents the result of an election.
|
||||
enum ElecResult {
|
||||
Winner(usize),
|
||||
Tie(Vec<usize>),
|
||||
StillCounting,
|
||||
}
|
||||
|
||||
/// Collection of candidates, where the index is in the same order as the provided CSV, used
|
||||
/// throughout the program to identify a candidate.
|
||||
#[derive(Debug)]
|
||||
struct Candidates(Vec<String>);
|
||||
|
||||
impl Candidates {
|
||||
/// Returns the candidates read from the header of a CSV file.
|
||||
fn from(path : &path::PathBuf) -> Result<Candidates, csv::Error> {
|
||||
let mut reader = csv::Reader::from_path(path)?;
|
||||
let headers = reader.headers()?;
|
||||
|
||||
let result : Vec<String> =
|
||||
headers
|
||||
.into_iter()
|
||||
.map(|x| (*x).parse::<String>())
|
||||
.map(|x| x.unwrap())
|
||||
.collect();
|
||||
|
||||
Ok(Candidates(result))
|
||||
}
|
||||
|
||||
/// Gets a candidate's name based on their index.
|
||||
fn get(&self, candidate : usize) -> Option<&String> {
|
||||
self.0.get(candidate)
|
||||
}
|
||||
|
||||
/// Returns the number of candidates.
|
||||
fn qty(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// The index of the inner vector is the preference, with the value stored at that index
|
||||
/// representing the candidate. Preferences start at 0, and are adjusted at file read.
|
||||
#[derive(Debug)]
|
||||
struct Ballot(Vec<usize>);
|
||||
|
||||
impl Ballot {
|
||||
/// Creates a new ballot from the underlying vector.
|
||||
fn new(ballot : Vec<usize>) -> Ballot {
|
||||
Ballot(ballot)
|
||||
}
|
||||
|
||||
// This processes a ballot as it appears in the provided CSV (as a Vec<Option<usize>> where
|
||||
// None means expressing no preference).
|
||||
// As such, this function takes a vector indexed by candidate, which yields the preference, and
|
||||
// returns a trimmed vector indexed by preference which yields the candidate.
|
||||
fn from_csv_row(csv_row : Vec<Option<usize>>, num_candidates : usize) -> Option<Ballot> {
|
||||
// Populate the ballot paper with None before mutating, so that it can be done at arbitrary
|
||||
// index.
|
||||
let mut ballot = Vec::with_capacity(num_candidates);
|
||||
for _i in 0..csv_row.len() {
|
||||
ballot.push(None);
|
||||
}
|
||||
|
||||
for (candidate, preference) in csv_row.iter().enumerate() {
|
||||
// If the preference was expressed.
|
||||
if let Some(preference) = preference {
|
||||
if preference >= &num_candidates || ballot[*preference].is_some() {
|
||||
// Preference was specified which was greater than the number of candidates or
|
||||
// duplicate preferences were expressed, both of which lead to a discounted
|
||||
// vote.
|
||||
return None;
|
||||
}
|
||||
else {
|
||||
// No errors, so store the candidate at the index by preference.
|
||||
ballot[*preference] = Some(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ballot =
|
||||
ballot
|
||||
.iter()
|
||||
// Filter out any candidates which were not filled out.
|
||||
.filter_map(|x| *x)
|
||||
.collect();
|
||||
|
||||
Some(Ballot::new(ballot))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/// Trie like structure, which stores ballots with common starting preferences, using the endings
|
||||
/// value to represent how many votes expressed the preferences from that node to the top.
|
||||
/// Each 'level' of ballot box represents a preference, from 1 (or 0 internally) down, with each
|
||||
/// candidate appearing in the children.
|
||||
#[derive(Debug, Clone)]
|
||||
struct BallotBox {
|
||||
total : u32,
|
||||
endings : u32,
|
||||
children : Vec<Option<BallotBox>>,
|
||||
}
|
||||
|
||||
impl BallotBox {
|
||||
/// Creates a new ballot box.
|
||||
fn new(total : u32, endings : u32, children : Vec<Option<BallotBox>>) -> BallotBox {
|
||||
BallotBox {
|
||||
total,
|
||||
endings,
|
||||
children
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an empty ballot box.
|
||||
fn empty(children : usize) -> BallotBox {
|
||||
BallotBox::new(0, 0, vec![None; children])
|
||||
}
|
||||
|
||||
/// Fills the ballot box from a csv.
|
||||
fn fill(path : &path::PathBuf, num_candidates : usize) -> Result<BallotBox, csv::Error> {
|
||||
let mut ballot_box = BallotBox::empty(num_candidates);
|
||||
let mut reader =
|
||||
csv::ReaderBuilder::new()
|
||||
.has_headers(true)
|
||||
.from_path(path)?;
|
||||
|
||||
for result in reader.records() {
|
||||
let mut row = Vec::new();
|
||||
for value in result?.iter() {
|
||||
// The use of ok() and map() convert the result into an option, so that not
|
||||
// expressing preference is none, and then proceed to reduce all by 1 so that the
|
||||
// data structures can be 0 indexed even when the preferences start at 1 on the
|
||||
// papers themselves.
|
||||
row.push(value.parse::<usize>().ok().map(|x| x - 1))
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let raw_ballot = row.clone();
|
||||
|
||||
// If the ballot can be created from the csv row without error, it should be added to
|
||||
// the ballot box.
|
||||
if let Some(ballot) = Ballot::from_csv_row(row.clone(), num_candidates) {
|
||||
ballot_box.push(ballot, num_candidates);
|
||||
}
|
||||
else {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
println!("INVALID BALLOT: {:?}", raw_ballot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ballot_box)
|
||||
}
|
||||
|
||||
/// Wrapper around push many which adds a single ballot to the ballot_box.
|
||||
fn push(&mut self, ballot : Ballot, num_candidates : usize) {
|
||||
self.push_many(ballot, num_candidates, 1);
|
||||
}
|
||||
|
||||
/// Pushes quantity many instances of the provided ballot to the ballot box. The ballot is
|
||||
/// consumed and freed.
|
||||
fn push_many(&mut self, ballot : Ballot, num_candidates : usize, quantity : u32) {
|
||||
|
||||
self.total += quantity;
|
||||
|
||||
let mut curr = self;
|
||||
|
||||
for (preference, candidate) in ballot.0.iter().enumerate() {
|
||||
|
||||
// If the next candidate is none in the ballot box, that sub box needs to be created.
|
||||
if curr.children[*candidate].is_none() {
|
||||
curr.children[*candidate] = Some(BallotBox::empty(num_candidates));
|
||||
}
|
||||
|
||||
// Traverse the box downwards by entering the new ballot box.
|
||||
let children = &mut curr.children;
|
||||
curr = children[*candidate].as_mut().unwrap();
|
||||
|
||||
// Update the totals on the current node.
|
||||
curr.total += quantity;
|
||||
|
||||
// Reached the end of the ballot paper so need to mark the ending.
|
||||
if preference == ballot.0.len() - 1 {
|
||||
curr.endings += quantity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Assuming no winner exists, performs the runoff, redistributing votes of the least popular
|
||||
/// candidate.
|
||||
fn runoff(&mut self, num_candidates : usize) {
|
||||
|
||||
fn all_min(vec : &Vec<u32>) -> Vec<usize> {
|
||||
let mut min = std::u32::MAX;
|
||||
let mut minimums = Vec::new();
|
||||
for (i, element) in vec.iter().enumerate() {
|
||||
if element == &min {
|
||||
minimums.push(i);
|
||||
}
|
||||
else if element < &min {
|
||||
min = *element;
|
||||
minimums.clear();
|
||||
minimums.push(i);
|
||||
}
|
||||
}
|
||||
minimums
|
||||
}
|
||||
|
||||
// Total top preference votes for each candidate.
|
||||
let totals =
|
||||
self
|
||||
.children
|
||||
.iter()
|
||||
.map(|b| b.as_ref().map(|x| x.total).unwrap_or(std::u32::MAX))
|
||||
.collect();
|
||||
|
||||
let to_eliminate = all_min(&totals);
|
||||
|
||||
// Collection of all adjusted votes (with first preference removed) to be distributed.
|
||||
// These need to be collected across each candidate which is eliminated to prevent later
|
||||
// preferences of votes moved within this round from being counted as top level votes.
|
||||
let mut adjusted_votes = Vec::new();
|
||||
|
||||
println!("About to eliminate: {:?}", to_eliminate);
|
||||
// Loop over each candidate and eliminate accordingly.
|
||||
for candidate in to_eliminate {
|
||||
// Separate the ballot box which needs to be distributed from the original so that
|
||||
// the mutable and immutable ref can exist at the same time.
|
||||
let mut to_distribute = None;
|
||||
mem::swap(&mut self.children[candidate], &mut to_distribute);
|
||||
|
||||
// Remove the votes to distribute from the top level total as they will be re-added
|
||||
// later.
|
||||
self.total -= to_distribute.as_ref().unwrap().total;
|
||||
BallotBox::runoff_helper(self, &to_distribute.unwrap(), Vec::with_capacity(num_candidates), num_candidates, &mut adjusted_votes);
|
||||
}
|
||||
|
||||
for vote in adjusted_votes {
|
||||
let (ballot, quantity) = vote;
|
||||
self.push_many(ballot, num_candidates, quantity);
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively redistributes votes from to_distribute into original_box.
|
||||
fn runoff_helper(original_box : &mut BallotBox, to_distribute : &BallotBox, ballot : Vec<usize>, num_candidates : usize, to_add : &mut Vec<(Ballot, u32)>) {
|
||||
|
||||
for (candidate, child) in to_distribute.children.iter().enumerate() {
|
||||
// Only need to process valid ballot box children.
|
||||
if let Some(ballot_box) = child {
|
||||
// Clone the ballot to pass down so that the vote corresponding with the endings is
|
||||
// known.
|
||||
let mut new_ballot = ballot.clone();
|
||||
new_ballot.push(candidate);
|
||||
|
||||
// Redistribute all children.
|
||||
BallotBox::runoff_helper(original_box, ballot_box, new_ballot, num_candidates, to_add);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add the ballots accordingly.
|
||||
if to_distribute.endings != 0 {
|
||||
//original_box.push_many(Ballot::new(ballot), num_candidates, to_distribute.endings);
|
||||
to_add.push((Ballot::new(ballot), to_distribute.endings));
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if any candidates have reached the required threshold of first preference votes to
|
||||
/// win.
|
||||
fn winner(&self, threshold : f64) -> ElecResult {
|
||||
|
||||
fn all_max(vec : &Vec<u32>) -> Vec<(usize, u32)> {
|
||||
let mut max = std::u32::MIN;
|
||||
let mut maximums = Vec::new();
|
||||
for (i, element) in vec.iter().enumerate() {
|
||||
if element == &max {
|
||||
maximums.push((i, *element));
|
||||
}
|
||||
else if element > &max {
|
||||
max = *element;
|
||||
maximums.clear();
|
||||
maximums.push((i, *element));
|
||||
}
|
||||
}
|
||||
maximums
|
||||
}
|
||||
|
||||
// If there is a tie a winner cannot be cal
|
||||
let totals =
|
||||
self
|
||||
.children
|
||||
.iter()
|
||||
.map(|b| b.as_ref().map(|x| x.total).unwrap_or(std::u32::MIN))
|
||||
.collect();
|
||||
|
||||
let winners = all_max(&totals);
|
||||
|
||||
let (_, biggest_count) = winners[0];
|
||||
|
||||
let total_votes : u32 = totals.iter().sum();
|
||||
|
||||
if f64::from(biggest_count) / f64::from(total_votes) >= threshold {
|
||||
if winners.len() > 1 {
|
||||
ElecResult::Tie(winners.iter().map(|x| x.0).collect())
|
||||
}
|
||||
else {
|
||||
ElecResult::Winner(winners[0].0)
|
||||
}
|
||||
}
|
||||
else {
|
||||
ElecResult::StillCounting
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if env::args().len() == 3 {
|
||||
let path = path::PathBuf::from(env::args().nth(1).unwrap());
|
||||
let threshold = env::args().nth(2).unwrap().parse::<f64>().unwrap();
|
||||
|
||||
let candidates = Candidates::from(&path).unwrap();
|
||||
let mut ballots = BallotBox::fill(&path, candidates.qty()).unwrap();
|
||||
|
||||
let winners = loop {
|
||||
match ballots.winner(threshold) {
|
||||
ElecResult::Winner(winner) => break vec![winner],
|
||||
ElecResult::Tie(winners) => break winners,
|
||||
ElecResult::StillCounting => ballots.runoff(candidates.qty()),
|
||||
}
|
||||
};
|
||||
|
||||
if winners.len() == 1 {
|
||||
let winner = candidates.get(winners[0]).unwrap();
|
||||
println!("{} is the elected candidate.", winner);
|
||||
}
|
||||
else {
|
||||
let winners : Vec<&String> =
|
||||
winners
|
||||
.iter()
|
||||
.map(|x| candidates.get(*x).unwrap())
|
||||
.collect();
|
||||
|
||||
println!("{:?} are the elected candidates.", winners);
|
||||
}
|
||||
}
|
||||
else {
|
||||
println!("Usage: vote-counter <CSV_PATH> <THRESHOLD>");
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user