tried to start on the web part
This commit is contained in:
parent
a88045226b
commit
376c49bc92
File diff suppressed because it is too large
Load Diff
|
@ -8,6 +8,7 @@ edition = "2021"
|
|||
[dependencies]
|
||||
# for web server
|
||||
rocket = { version = "0.5.0", features = ["json", "uuid"] }
|
||||
rocket_dyn_templates = { version = "0.1.0", features = ["handlebars", "tera"] }
|
||||
# for json serialization
|
||||
serde = "1.0.196"
|
||||
# for random number generation
|
||||
|
@ -20,4 +21,7 @@ uuid = { version = "1.7.0", features = ["serde", "v4"] }
|
|||
structopt = "0.3.23"
|
||||
# for csv reading
|
||||
csv = "1.1.6"
|
||||
clap = { version = "4.5.3", features = ["derive"] }
|
||||
clap = { version = "4.3.24", features = ["derive"] }
|
||||
|
||||
|
||||
stv-rs = "0.3.0"
|
221
src/main.rs
221
src/main.rs
|
@ -1,188 +1,61 @@
|
|||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::fs::OpenOptions;
|
||||
use std::path::Path;
|
||||
//import for taking application flags
|
||||
#[macro_use]
|
||||
extern crate rocket;
|
||||
|
||||
use rocket::form::Form;
|
||||
use rocket::response::Redirect;
|
||||
use rocket::State; // Add this line
|
||||
use rocket::fs::FileServer;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket_dyn_templates::Template;
|
||||
use std::sync::Mutex;
|
||||
use std::collections::HashMap;
|
||||
use csv::Writer;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Ballot {
|
||||
weight: i32,
|
||||
candidates: Vec<i32>,
|
||||
mod voting;
|
||||
|
||||
#[derive(rocket::FromForm)]
|
||||
struct Vote {
|
||||
option: String,
|
||||
}
|
||||
|
||||
struct Candidate {
|
||||
id: i32,
|
||||
name: String,
|
||||
type Votes = Mutex<Writer<std::fs::File>>;
|
||||
|
||||
#[post("/vote", data = "<vote>")]
|
||||
fn vote(vote: Form<Vote>, votes: &State<Votes>) -> Redirect {
|
||||
let mut writer = votes.lock().unwrap();
|
||||
writer.write_record(&[vote.option.clone()]).unwrap();
|
||||
writer.flush().unwrap();
|
||||
|
||||
Redirect::to("/")
|
||||
}
|
||||
|
||||
struct ElectionData {
|
||||
num_candidates: i32,
|
||||
num_seats: i32,
|
||||
ballots: Vec<Ballot>,
|
||||
candidates: Vec<Candidate>,
|
||||
title: String,
|
||||
#[post("/results")]
|
||||
fn results() -> Json<HashMap<String, String>> {
|
||||
let election_data = voting::csv_to_election_data("votes.csv", 1, "Election".to_string());
|
||||
let blt = voting::election_data_to_blt(election_data);
|
||||
let results = voting::run_blt(blt);
|
||||
Json(results)
|
||||
}
|
||||
|
||||
|
||||
fn election_data_to_blt(election_data: ElectionData) -> String {
|
||||
//convert the election data to a blt file
|
||||
//format
|
||||
// 7 4 # 7 candidates, 4 seats
|
||||
//1 1 3 0 # weigth, candidate ids, 0
|
||||
// ...
|
||||
//0
|
||||
//"a" # candidate name
|
||||
//...
|
||||
//"title" # election title
|
||||
|
||||
let mut blt = String::new();
|
||||
//push the number of candidates and seats
|
||||
blt.push_str(&format!("{} {}\n", election_data.num_candidates, election_data.num_seats));
|
||||
|
||||
|
||||
for ballot in &election_data.ballots {
|
||||
blt.push_str(&format!("{} ", ballot.weight));
|
||||
for candidate in &ballot.candidates {
|
||||
blt.push_str(&format!("{} ", candidate)); // Use candidate directly
|
||||
}
|
||||
blt.push_str("0\n");
|
||||
}
|
||||
blt.push_str("0\n");
|
||||
for candidate in &election_data.candidates {
|
||||
blt.push_str(&format!("\"{}\"\n", candidate.name));
|
||||
}
|
||||
blt.push_str(&format!("\"{}\"\n", election_data.title));
|
||||
|
||||
return blt;
|
||||
#[get("/")]
|
||||
fn index() -> Template {
|
||||
let context: HashMap<&str, &str> = HashMap::new();
|
||||
Template::render("index", &context)
|
||||
}
|
||||
|
||||
#[launch]
|
||||
fn rocket() -> _ {
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open("votes.csv")
|
||||
.unwrap();
|
||||
|
||||
fn csv_to_election_data(csv: &str, num_seats: i32, title: String) -> ElectionData {
|
||||
//read the csv file and convert it to a ballot
|
||||
let delimiter = ",";
|
||||
let writer = csv::Writer::from_writer(file);
|
||||
|
||||
//read the csv file
|
||||
let mut file = File::open(csv).expect("file not found");
|
||||
|
||||
//convert the csv file to a list of lists [ ['a', 'c', 'b'], ['b', 'a', 'd'] ] where each line is a array like ['a', 'c', 'b']
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents).expect("something went wrong reading the file");
|
||||
let lines: Vec<&str> = contents.split("\n").collect();
|
||||
|
||||
let mut votes: Vec<Vec<&str>> = Vec::new();
|
||||
for line in lines {
|
||||
let vote: Vec<&str> = line.split(delimiter).collect();
|
||||
votes.push(vote);
|
||||
}
|
||||
|
||||
//get all the unique candidates from all the votes and create a list of candidates
|
||||
let mut names: Vec<&str> = Vec::new();
|
||||
for vote in &votes {
|
||||
for name in vote {
|
||||
if !names.contains(name) {
|
||||
names.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
let num_candidates = names.len() as i32;
|
||||
|
||||
let mut candidates: Vec<Candidate> = Vec::new();
|
||||
for (i, name) in names.iter().enumerate() {
|
||||
let candidate = Candidate {
|
||||
id: (i + 1) as i32, // Add 1 to make it 1-indexed
|
||||
name: name.to_string(),
|
||||
};
|
||||
candidates.push(candidate);
|
||||
}
|
||||
|
||||
//generate the ballots
|
||||
let mut ballots: Vec<Ballot> = Vec::new();
|
||||
for vote in &votes {
|
||||
let mut ballot = Ballot {
|
||||
weight: 1,
|
||||
candidates: Vec::new(),
|
||||
};
|
||||
for name in vote {
|
||||
for candidate in &candidates {
|
||||
if candidate.name == *name {
|
||||
ballot.candidates.push(candidate.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
ballots.push(ballot);
|
||||
}
|
||||
|
||||
//deduplicate the ballots
|
||||
let mut deduped_ballots: Vec<Ballot> = Vec::new();
|
||||
for ballot in ballots {
|
||||
let mut found = false;
|
||||
for deduped_ballot in &deduped_ballots {
|
||||
if deduped_ballot.candidates == ballot.candidates {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
deduped_ballots.push(ballot);
|
||||
}
|
||||
}
|
||||
ballots = deduped_ballots;
|
||||
|
||||
let election_data = ElectionData {
|
||||
num_candidates: num_candidates,
|
||||
num_seats: num_seats,
|
||||
ballots: ballots,
|
||||
candidates: candidates,
|
||||
title: title,
|
||||
};
|
||||
return election_data;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
use clap::Parser;
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
struct Args {
|
||||
//number of seats to be elected
|
||||
#[clap(short, long, default_value = "1")]
|
||||
seats: i32,
|
||||
//title of the election
|
||||
#[clap(short, long)]
|
||||
title: String,
|
||||
//csv file containing the votes
|
||||
#[clap(short, long)]
|
||||
csv: String,
|
||||
//output file
|
||||
#[clap(short, long, default_value = "")]
|
||||
output: String,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Args = Args::parse();
|
||||
|
||||
let election_data = csv_to_election_data(&args.csv, args.seats, args.title);
|
||||
let blt = election_data_to_blt(election_data);
|
||||
//if output is not specified, print to stdout
|
||||
if args.output == "" {
|
||||
println!("{}", blt);
|
||||
return;
|
||||
}
|
||||
|
||||
let file_path = &args.output;
|
||||
if Path::new(file_path).exists() {
|
||||
panic!("File already exists");
|
||||
} else {
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(file_path)
|
||||
.expect("Failed to create file");
|
||||
file.write_all(blt.as_bytes()).expect("write failed");
|
||||
}
|
||||
rocket::build()
|
||||
.attach(Template::fairing())
|
||||
.manage(Mutex::new(writer))
|
||||
.mount("/", routes![index, vote])
|
||||
.mount("/static", FileServer::from("static"))
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Voting List</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.css">
|
||||
<style>
|
||||
#sortable { list-style-type: none; margin: 0; padding: 0; width: 60%; }
|
||||
#sortable li { margin: 5px; padding: 5px; font-size: 1.2em; height: 2em; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Vote List</h2>
|
||||
<ul id="sortable">
|
||||
<li class="ui-state-default"><input type="checkbox"> Item 1</li>
|
||||
<li class="ui-state-default"><input type="checkbox"> Item 2</li>
|
||||
</ul>
|
||||
<button id="add">Add Item</button>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
|
||||
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
|
||||
<script>
|
||||
$( function() {
|
||||
$( "#sortable" ).sortable();
|
||||
$( "#sortable" ).disableSelection();
|
||||
|
||||
let count = 3;
|
||||
$('#add').click(function() {
|
||||
if ($('#sortable li').length < 10) {
|
||||
$('#sortable').append('<li class="ui-state-default"><input type="checkbox"> Item ' + count++ + '</li>');
|
||||
} else {
|
||||
alert('Threshold reached');
|
||||
}
|
||||
});
|
||||
} );
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,210 @@
|
|||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::fs::OpenOptions;
|
||||
use std::path::Path;
|
||||
|
||||
|
||||
// https://docs.rs/stv-rs/0.3.0/stv_rs/all.html
|
||||
|
||||
|
||||
|
||||
|
||||
use stv_rs::parse::parse_election;
|
||||
use stv_rs::meek::stv_droop;
|
||||
use stv_rs::types::Election;
|
||||
use stv_rs::types::ElectionResult;
|
||||
|
||||
pub fn blt_to_string_results(blt: String) -> String {
|
||||
// Parse the blt string into an Election object
|
||||
let election: Election = parse_election(&blt).unwrap();
|
||||
|
||||
// Run the meek election
|
||||
let result: ElectionResult = stv_droop(&election).unwrap();
|
||||
|
||||
// Convert the ElectionResult object to a String
|
||||
let string_result = format!("{:?}", result);
|
||||
|
||||
string_result
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Ballot {
|
||||
weight: i32,
|
||||
candidates: Vec<i32>,
|
||||
}
|
||||
|
||||
struct Candidate {
|
||||
id: i32,
|
||||
name: String,
|
||||
|
||||
}
|
||||
|
||||
struct ElectionData {
|
||||
num_candidates: i32,
|
||||
num_seats: i32,
|
||||
ballots: Vec<Ballot>,
|
||||
candidates: Vec<Candidate>,
|
||||
title: String,
|
||||
}
|
||||
|
||||
|
||||
pub fn election_data_to_blt(election_data: ElectionData) -> String {
|
||||
//convert the election data to a blt file
|
||||
//format
|
||||
// 7 4 # 7 candidates, 4 seats
|
||||
//1 1 3 0 # weigth, candidate ids, 0
|
||||
// ...
|
||||
//0
|
||||
//"a" # candidate name
|
||||
//...
|
||||
//"title" # election title
|
||||
|
||||
let mut blt = String::new();
|
||||
//push the number of candidates and seats
|
||||
blt.push_str(&format!("{} {}\n", election_data.num_candidates, election_data.num_seats));
|
||||
|
||||
|
||||
for ballot in &election_data.ballots {
|
||||
blt.push_str(&format!("{} ", ballot.weight));
|
||||
for candidate in &ballot.candidates {
|
||||
blt.push_str(&format!("{} ", candidate)); // Use candidate directly
|
||||
}
|
||||
blt.push_str("0\n");
|
||||
}
|
||||
blt.push_str("0\n");
|
||||
for candidate in &election_data.candidates {
|
||||
blt.push_str(&format!("\"{}\"\n", candidate.name));
|
||||
}
|
||||
blt.push_str(&format!("\"{}\"\n", election_data.title));
|
||||
|
||||
return blt;
|
||||
}
|
||||
|
||||
pub fn csv_to_election_data(csv: &str, num_seats: i32, title: String) -> ElectionData {
|
||||
//read the csv file and convert it to a ballot
|
||||
let delimiter = ",";
|
||||
|
||||
//read the csv file
|
||||
let mut file = File::open(csv).expect("file not found");
|
||||
|
||||
//convert the csv file to a list of lists [ ['a', 'c', 'b'], ['b', 'a', 'd'] ] where each line is a array like ['a', 'c', 'b']
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents).expect("something went wrong reading the file");
|
||||
let lines: Vec<&str> = contents.split("\n").collect();
|
||||
|
||||
let mut votes: Vec<Vec<&str>> = Vec::new();
|
||||
for line in lines {
|
||||
let vote: Vec<&str> = line.split(delimiter).collect();
|
||||
votes.push(vote);
|
||||
}
|
||||
|
||||
//get all the unique candidates from all the votes and create a list of candidates
|
||||
let mut names: Vec<&str> = Vec::new();
|
||||
for vote in &votes {
|
||||
for name in vote {
|
||||
if !names.contains(name) {
|
||||
names.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
let num_candidates = names.len() as i32;
|
||||
|
||||
let mut candidates: Vec<Candidate> = Vec::new();
|
||||
for (i, name) in names.iter().enumerate() {
|
||||
let candidate = Candidate {
|
||||
id: (i + 1) as i32, // Add 1 to make it 1-indexed
|
||||
name: name.to_string(),
|
||||
};
|
||||
candidates.push(candidate);
|
||||
}
|
||||
|
||||
//generate the ballots
|
||||
let mut ballots: Vec<Ballot> = Vec::new();
|
||||
for vote in &votes {
|
||||
let mut ballot = Ballot {
|
||||
weight: 1,
|
||||
candidates: Vec::new(),
|
||||
};
|
||||
for name in vote {
|
||||
for candidate in &candidates {
|
||||
if candidate.name == *name {
|
||||
ballot.candidates.push(candidate.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
ballots.push(ballot);
|
||||
}
|
||||
|
||||
//deduplicate the ballots
|
||||
let mut deduped_ballots: Vec<Ballot> = Vec::new();
|
||||
for ballot in ballots {
|
||||
let mut found = false;
|
||||
for deduped_ballot in &deduped_ballots {
|
||||
if deduped_ballot.candidates == ballot.candidates {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
deduped_ballots.push(ballot);
|
||||
}
|
||||
}
|
||||
ballots = deduped_ballots;
|
||||
|
||||
let election_data = ElectionData {
|
||||
num_candidates: num_candidates,
|
||||
num_seats: num_seats,
|
||||
ballots: ballots,
|
||||
candidates: candidates,
|
||||
title: title,
|
||||
};
|
||||
return election_data;
|
||||
}
|
||||
|
||||
|
||||
use clap::Parser;
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
struct Args {
|
||||
//number of seats to be elected
|
||||
#[clap(short, long, default_value = "1")]
|
||||
seats: i32,
|
||||
//title of the election
|
||||
#[clap(short, long)]
|
||||
title: String,
|
||||
//csv file containing the votes
|
||||
#[clap(short, long)]
|
||||
csv: String,
|
||||
//output file
|
||||
#[clap(short, long, default_value = "")]
|
||||
output: String,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Args = Args::parse();
|
||||
|
||||
let election_data = csv_to_election_data(&args.csv, args.seats, args.title);
|
||||
let blt = election_data_to_blt(election_data);
|
||||
//if output is not specified, print to stdout
|
||||
if args.output == "" {
|
||||
println!("{}", blt);
|
||||
return;
|
||||
}
|
||||
|
||||
let file_path = &args.output;
|
||||
if Path::new(file_path).exists() {
|
||||
panic!("File already exists");
|
||||
} else {
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(file_path)
|
||||
.expect("Failed to create file");
|
||||
file.write_all(blt.as_bytes()).expect("write failed");
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue