This commit is contained in:
Adrian Gunnar Lauterer 2024-03-03 06:11:13 +01:00
parent 1fe0b4e464
commit add30f3515
Signed by: adriangl
GPG Key ID: D33368A59745C2F0
6 changed files with 1992 additions and 2 deletions

1747
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

22
Cargo.toml Normal file
View File

@ -0,0 +1,22 @@
[package]
name = "vote-rs"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# for web server
rocket = { version = "0.5.0", features = ["json", "uuid"] }
# for json serialization
serde = "1.0.196"
# for random number generation
rand = "0.8.5"
# for json serialization
serde_json = "1.0.113"
# for uuid generation
uuid = { version = "1.7.0", features = ["serde", "v4"] }
# for command line arguments
structopt = "0.3.23"
# for csv reading
csv = "1.1.6"

View File

@ -1,3 +1,29 @@
# vote-rs # Vote-rs
This is a simple Rust program that reads a file of voting records and counts the votes for each nominee.
## goael
Implement a stv vote counting system in rust.
Could be based on rules and information found here: https://www.opavote.com/methods/single-transferable-vote
probably implement the scottish method https://www.legislation.gov.uk/ssi/2007/42/contents/made
because it is simpler and seems to be ok middle ground between complexity and fairness.
## Usage
To run the program, use the following command:
```bash
cargo run -- sample.csv --amount 3
```
Replace sample.csv with the path to your file of voting records.
amount is the number of nominees to select from the records.
## Functionality
The program reads the voting records from the specified file into a Vec<Record>. Each Record contains a Vec<Vote>, where each Vote has a name and a value.
The get_unique_names function is used to get a Vec<String> of the unique names in the records.
The count_votes function is used to count the votes for each name. It returns a Vec<Vote> where each Vote has a name and a value that is the total of the votes for that name. The Vec<Vote> is sorted in descending order by vote value.
stv voting system written in rust.

28
flake.nix Normal file
View File

@ -0,0 +1,28 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
devenv.url = "github:cachix/devenv";
fenix.url = "github:nix-community/fenix";
fenix.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, devenv, ... }@inputs:
let
pkgs = import nixpkgs {
system = "x86_64-linux";
};
in {
devShell.x86_64-linux = devenv.lib.mkShell {
inherit inputs pkgs;
modules = [
{
languages.rust = {
enable = true;
channel = "stable";
};
}
];
};
};
}

9
sample.json Normal file
View File

@ -0,0 +1,9 @@
[
["Alice", "Bob"],
[""],
["Alice"],
["Bob"],
["Charlie", "Bob", "Alice"],
["David"],
["David"]
]

158
src/main.rs Normal file
View File

@ -0,0 +1,158 @@
use std::path::PathBuf;
use std::process;
use structopt::StructOpt;
use std::error::Error;
use std::io;
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Debug, StructOpt)]
#[structopt(name = "example", about = "An example of StructOpt usage.")]
struct Opt {//options
/// File to process
#[structopt(parse(from_os_str))]
file: PathBuf,
/// Amount to select
#[structopt(short = "a", long = "amount")]
amount: usize,
}
#[derive(Deserialize, Debug, Clone)]
struct Vote {
name: String,
value: f64,
}
#[derive(Deserialize, Debug, Clone)]
struct Score {
name: String,
value: f64,
}
#[derive(Deserialize, Debug, Clone)]
struct Record {
votes: Vec<Vote>,
}
fn read_to_records(file: &PathBuf) -> Result<Vec<Record>, Box<dyn Error>> {
let file = std::fs::File::open(file)?;
let reader = io::BufReader::new(file);
//read the json file with a list of lists with names into a vector of records, where each record is a list of votes containing a name and a value. the first name in each list has value 1, the following names have value 0
let name_vec: Vec<Vec<String>> = serde_json::from_reader(reader)?;
let mut records: Vec<Record> = Vec::new();
for names in name_vec {
let mut votes: Vec<Vote> = Vec::new();
for (i, name) in names.iter().enumerate() {
let vote = Vote {
name: name.clone().to_string().to_lowercase().replace(" ", "_").replace("-","_").replace("__", "_"),
value: if i == 0 { 1.0 } else { 0.0 },
};
votes.push(vote);
}
let record = Record { votes };
records.push(record);
}
Ok(records)
}
//get all unique names from the records
fn get_unique_names(records: &Vec<Record>) -> Vec<String> {
let mut names: HashMap<String, bool> = HashMap::new();
for record in records {
for vote in &record.votes {
names.entry(vote.name.clone()).or_insert(true);
}
}
names.keys().cloned().collect()
}
fn count_votes(records: &Vec<Record>) -> Vec<Score> {
let mut counts: HashMap<String, f64> = HashMap::new();
for record in records {
for vote in &record.votes {
*counts.entry(vote.name.clone()).or_insert(0.0) += vote.value;
}
}
let mut scores: Vec<Score> = counts.into_iter()
.map(|(name, value)| Score { name, value })
.collect();
scores.sort_by(|a, b| {
let value_cmp = b.value.partial_cmp(&a.value).unwrap();
if value_cmp == std::cmp::Ordering::Equal {
a.name.cmp(&b.name)
} else {
value_cmp
}
});
scores
}
// (3) The vote on each ballot paper transferred under paragraph (2) shall have a value ("the transfer value") calculated as follows—
// A divided by B
// Where
// A = the value which is calculated by multiplying the surplus of the transferring candidate by the value of the ballot paper when received by that candidate; and
// B = the total number of votes credited to that candidate,
// the calculation being made to five decimal places (any remainder being ignored).
//TODO: implement the quota calculation
fn exceeds_quota(score: &Score, quota: f64) -> bool {
score.value >= quota
}
//TODO: implement the stv algorithm
fn stv(recs: &Vec<Record>, amount: usize) -> Vec<Score> {
let mut records: Vec<_> = recs.iter().map(|x| (*x).clone()).collect();
let mut winners: Vec<Score> = Vec::new();
let total_votes = records.len() as f64;
let mut round = 0;
while winners.len() < amount {
let quota = total_votes / (amount as f64 + 1.0);
round += 1;
let scores = count_votes(&records);
println!("Round {}: {:?}", round, scores);
winners.append(&mut scores.iter().filter(|x| exceeds_quota(x, quota)).cloned().collect());
if winners.len() >= amount {
break;
}
//do soemthing random for now until i actually implement the stv algorithm.
//remove the last candidate from the records
let names = get_unique_names(&records);
//remove the last name from the records
records.retain(|x| x.votes.iter().any(|y| y.name != names[names.len() - 1]));
//add the first candidate to the winners
winners.push(scores[0].clone());
}
winners
}
fn main() {
let opt = Opt::from_args();
let records = read_to_records(&opt.file).unwrap_or_else(|err| {
println!("Error reading file: {}", err);
process::exit(1);
});
println!("Records: {:?}", records);
let nominees = get_unique_names(&records);
println!("Nominees: {:?}", nominees);
let counts = count_votes(&records);
println!("Counts: {:?}", counts);
let winners = stv(&records, opt.amount);
println!("Winners: {:?}", winners);
}