From 2428fcb4ed9ac64997e46d41273f0fa9642d5e95 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sun, 30 May 2021 23:00:28 +1000 Subject: [PATCH] Implement rational numbers in WebAssembly using num-bigint --- Cargo.lock | 42 +++ Cargo.toml | 4 + pkg/test.html | 16 +- src/numbers/mod.rs | 10 +- src/numbers/rational_num.rs | 303 +++++++++++++++++++ src/numbers/{rational.rs => rational_rug.rs} | 0 src/stv/wasm.rs | 127 ++++---- 7 files changed, 436 insertions(+), 66 deletions(-) create mode 100644 src/numbers/rational_num.rs rename src/numbers/{rational.rs => rational_rug.rs} (100%) diff --git a/Cargo.lock b/Cargo.lock index 540f068..56fa273 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -239,6 +239,39 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num-bigint" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e0d047c1062aa51e256408c560894e5251f08925980e53cf1aa5bd00eec6512" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.14" @@ -257,7 +290,10 @@ dependencies = [ "csv", "flate2", "git-version", + "num-bigint", + "num-rational", "num-traits", + "paste", "rug", "wasm-bindgen", ] @@ -268,6 +304,12 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6acbef58a60fe69ab50510a55bc8cdd4d6cf2283d27ad338f54cb52747a9cf2d" +[[package]] +name = "paste" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58" + [[package]] name = "proc-macro-error" version = "1.0.4" diff --git a/Cargo.toml b/Cargo.toml index 4762b3e..510a917 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,11 @@ num-traits = "0.2" wasm-bindgen = "0.2.74" # Only for WebAssembly - include here for syntax highlighting +#[target.'cfg(target_arch = "wasm32")'.dependencies] console_error_panic_hook = "0.1.6" +num-bigint = "0.4.0" +num-rational = "0.4.0" +paste = "1.0.5" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] csv = "1.1.6" diff --git a/pkg/test.html b/pkg/test.html index 7131010..495d306 100644 --- a/pkg/test.html +++ b/pkg/test.html @@ -23,24 +23,24 @@ document.getElementById('output').append("\n"); }; - var { CountStateFloat64, STVOptions, count_init_float64, count_one_stage_float64, election_from_blt_float64, make_and_print_result_float64 } = wasm_bindgen; + var wasm = wasm_bindgen; async function run() { await wasm_bindgen("opentally_bg.wasm"); - let stv_opts = STVOptions.new(2, "one_round"); + let stv_opts = wasm.STVOptions.new(2, "one_round"); - let election = election_from_blt_float64(election_data); - let state = CountStateFloat64.new(election); + let election = wasm.election_from_blt_Rational(election_data); + let state = wasm.CountStateRational.new(election); - count_init_float64(state, stv_opts); - make_and_print_result_float64(1, state); + wasm.count_init_Rational(state, stv_opts); + wasm.make_and_print_result_Rational(1, state); for (let stage_num = 2;; stage_num++) { - let is_done = count_one_stage_float64(state, stv_opts); + let is_done = wasm.count_one_stage_Rational(state, stv_opts); if (is_done) { break; } - make_and_print_result_float64(stage_num, state); + wasm.make_and_print_result_Rational(stage_num, state); } } run(); diff --git a/src/numbers/mod.rs b/src/numbers/mod.rs index 5240f81..9c5e4b0 100644 --- a/src/numbers/mod.rs +++ b/src/numbers/mod.rs @@ -18,7 +18,10 @@ mod native; #[cfg(not(target_arch = "wasm32"))] -mod rational; +mod rational_rug; + +//#[cfg(target_arch = "wasm32")] +mod rational_num; use num_traits::{NumAssignRef, NumRef}; @@ -49,4 +52,7 @@ pub trait Number: NumRef + NumAssignRef + ops::Neg + Ord + Assign + pub use self::native::NativeFloat64; #[cfg(not(target_arch = "wasm32"))] -pub use self::rational::Rational; +pub use self::rational_rug::Rational; + +#[cfg(target_arch = "wasm32")] +pub use self::rational_num::Rational; diff --git a/src/numbers/rational_num.rs b/src/numbers/rational_num.rs new file mode 100644 index 0000000..13da822 --- /dev/null +++ b/src/numbers/rational_num.rs @@ -0,0 +1,303 @@ +/* OpenTally: Open-source election vote counting + * Copyright © 2021 Lee Yingtong Li (RunasSudo) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use super::{Assign, Number}; + +use num_bigint::{BigInt, ParseBigIntError}; +use num_rational::BigRational; // TODO: Can we do Ratio and combine with ibig? +use num_traits::{Num, One, Signed, Zero}; + +use std::cmp::{Ord, Ordering, PartialEq, PartialOrd}; +use std::fmt; +use std::ops; + +pub struct Rational(BigRational); + +impl Number for Rational { + fn new() -> Self { Self(BigRational::zero()) } + + fn from(n: usize) -> Self { Self(BigRational::from_integer(BigInt::from(n))) } + + fn floor_mut(&mut self, dps: usize) { + if dps == 0 { + self.0 = self.0.floor(); + } else { + let factor = BigRational::from_integer(BigInt::from(10)).pow(dps as i32); + self.0 *= &factor; + self.0 = self.0.floor(); + self.0 /= factor; + } + } +} + +impl Num for Rational { + //type FromStrRadixErr = ParseRatioError; + type FromStrRadixErr = ParseBigIntError; + + fn from_str_radix(str: &str, radix: u32) -> Result { + //match BigRational::from_str_radix(str, radix) { + match BigInt::from_str_radix(str, radix) { + Ok(value) => Ok(Self(BigRational::from_integer(value))), + Err(err) => Err(err) + } + } +} + +impl Assign for Rational { + fn assign(&mut self, src: Self) { self.0 = src.0 } +} + +impl Assign<&Rational> for Rational { + fn assign(&mut self, src: &Rational) { self.0 = src.0.clone() } +} + +impl Clone for Rational { + fn clone(&self) -> Self { Self(self.0.clone()) } +} + +impl fmt::Display for Rational { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(precision) = f.precision() { + if precision == 0 { + let result = self.0.round().to_integer().to_string(); + return f.write_str(&result); + } else { + let base = BigInt::from(10).pow(precision as u32); + let mut result = (&self.0 * base).abs().round().to_integer().to_string(); + + let should_add_minus = (self.0 < BigRational::zero()) && result != "0"; + + // Add leading 0s + result = format!("{0:0>1$}", result, precision + 1); + + // Add the decimal point + result.insert(result.len() - precision, '.'); + + // Add the sign + if should_add_minus { + result.insert(0, '-'); + } + + return f.write_str(&result); + } + } else { + return self.0.fmt(f); + } + } +} + +impl One for Rational { + fn one() -> Self { Self(BigRational::one()) } +} + +impl Zero for Rational { + fn zero() -> Self { Self::new() } + fn is_zero(&self) -> bool { self.0.is_zero() } +} + +impl Eq for Rational {} +impl PartialEq for Rational { + fn eq(&self, other: &Self) -> bool { self.0 == other.0 } +} + +impl Ord for Rational { + fn cmp(&self, other: &Self) -> Ordering { self.0.partial_cmp(&other.0).unwrap() } +} + +impl PartialOrd for Rational { + fn partial_cmp(&self, other: &Self) -> Option { self.0.partial_cmp(&other.0) } +} + +impl ops::Neg for Rational { + type Output = Rational; + fn neg(self) -> Self::Output { Self(-self.0) } +} + +impl ops::Add for Rational { + type Output = Rational; + fn add(self, _rhs: Self) -> Self::Output { + todo!() + } +} + +impl ops::Sub for Rational { + type Output = Rational; + fn sub(self, _rhs: Self) -> Self::Output { + todo!() + } +} + +impl ops::Mul for Rational { + type Output = Rational; + fn mul(self, _rhs: Self) -> Self::Output { + todo!() + } +} + +impl ops::Div for Rational { + type Output = Rational; + fn div(self, _rhs: Self) -> Self::Output { + todo!() + } +} + +impl ops::Rem for Rational { + type Output = Rational; + + fn rem(self, _rhs: Self) -> Self::Output { + todo!() + } +} + +impl ops::Add<&Rational> for Rational { + type Output = Rational; + fn add(self, rhs: &Rational) -> Self::Output { Self(self.0 + &rhs.0) } +} + +impl ops::Sub<&Rational> for Rational { + type Output = Rational; + fn sub(self, _rhs: &Rational) -> Self::Output { + todo!() + } +} + +impl ops::Mul<&Rational> for Rational { + type Output = Rational; + fn mul(self, rhs: &Rational) -> Self::Output { Rational(self.0 * &rhs.0) } +} + +impl ops::Div<&Rational> for Rational { + type Output = Rational; + fn div(self, rhs: &Rational) -> Self::Output { Rational(self.0 / &rhs.0) } +} + +impl ops::Rem<&Rational> for Rational { + type Output = Rational; + fn rem(self, _rhs: &Rational) -> Self::Output { + todo!() + } +} + +impl ops::AddAssign for Rational { + fn add_assign(&mut self, rhs: Self) { self.0 += rhs.0 } +} + +impl ops::SubAssign for Rational { + fn sub_assign(&mut self, rhs: Self) { self.0 -= rhs.0 } +} + +impl ops::MulAssign for Rational { + fn mul_assign(&mut self, _rhs: Self) { + todo!() + } +} + +impl ops::DivAssign for Rational { + fn div_assign(&mut self, rhs: Self) { + self.0 /= &rhs.0; + } +} + +impl ops::RemAssign for Rational { + fn rem_assign(&mut self, _rhs: Self) { + todo!() + } +} + +impl ops::AddAssign<&Rational> for Rational { + fn add_assign(&mut self, rhs: &Rational) { self.0 += &rhs.0 } +} + +impl ops::SubAssign<&Rational> for Rational { + fn sub_assign(&mut self, rhs: &Rational) { self.0 -= &rhs.0 } +} + +impl ops::MulAssign<&Rational> for Rational { + fn mul_assign(&mut self, _rhs: &Rational) { + todo!() + } +} + +impl ops::DivAssign<&Rational> for Rational { + fn div_assign(&mut self, _rhs: &Rational) { + todo!() + } +} + +impl ops::RemAssign<&Rational> for Rational { + fn rem_assign(&mut self, _rhs: &Rational) { + todo!() + } +} + +impl ops::Neg for &Rational { + type Output = Rational; + fn neg(self) -> Self::Output { Rational(-&self.0) } +} + +impl ops::Add<&Rational> for &Rational { + type Output = Rational; + fn add(self, _rhs: &Rational) -> Self::Output { + todo!() + } +} + +impl ops::Sub<&Rational> for &Rational { + type Output = Rational; + fn sub(self, rhs: &Rational) -> Self::Output { Rational(&self.0 - &rhs.0) } +} + +impl ops::Mul<&Rational> for &Rational { + type Output = Rational; + fn mul(self, _rhs: &Rational) -> Self::Output { + todo!() + } +} + +impl ops::Div<&Rational> for &Rational { + type Output = Rational; + fn div(self, rhs: &Rational) -> Self::Output { Rational(&self.0 / &rhs.0) } +} + +impl ops::Rem<&Rational> for &Rational { + type Output = Rational; + fn rem(self, _rhs: &Rational) -> Self::Output { + todo!() + } +} + +/* +impl ops::Add<&&NativeFloat> for &NativeFloat { + +} + +impl ops::Sub<&&NativeFloat> for &NativeFloat { + +} + +impl ops::Mul<&&NativeFloat> for &NativeFloat { + +} + +impl ops::Div<&&NativeFloat> for &NativeFloat { + +} + +impl ops::Rem<&&NativeFloat> for &NativeFloat { + +} +*/ diff --git a/src/numbers/rational.rs b/src/numbers/rational_rug.rs similarity index 100% rename from src/numbers/rational.rs rename to src/numbers/rational_rug.rs diff --git a/src/stv/wasm.rs b/src/stv/wasm.rs index e1d1908..50a5b7d 100644 --- a/src/stv/wasm.rs +++ b/src/stv/wasm.rs @@ -16,7 +16,7 @@ */ use crate::election::{Candidate, CandidateState, CountCard, CountState, CountStateOrRef, Election, StageResult}; -use crate::numbers::{NativeFloat64, Number}; +use crate::numbers::{NativeFloat64, Number, Rational}; use crate::stv; extern crate console_error_panic_hook; @@ -37,26 +37,79 @@ macro_rules! cprintln { ) } -// Exported functions +// Helper macros for making functions -#[wasm_bindgen] -pub fn election_from_blt_float64(text: String) -> ElectionFloat64 { - // Install panic! hook - console_error_panic_hook::set_once(); - - let election: Election = Election::from_blt(text.lines().map(|s| s.to_string()).into_iter()); - return ElectionFloat64(election); +macro_rules! impl_type { + ($type:ident) => { paste::item! { + // Exported functions + + #[wasm_bindgen] + #[allow(non_snake_case)] + pub fn [](text: String) -> [] { + // Install panic! hook + console_error_panic_hook::set_once(); + + let election: Election<$type> = Election::from_blt(text.lines().map(|s| s.to_string()).into_iter()); + return [](election); + } + + #[wasm_bindgen] + #[allow(non_snake_case)] + pub fn [](state: &mut [], opts: &STVOptions) { + stv::count_init(&mut state.0, &opts.0); + } + + #[wasm_bindgen] + #[allow(non_snake_case)] + pub fn [](state: &mut [], opts: &STVOptions) -> bool { + return stv::count_one_stage(&mut state.0, &opts.0); + } + + // Reporting + + #[wasm_bindgen] + #[allow(non_snake_case)] + pub fn [](stage_num: usize, state: &[]) { + let result = StageResult { + kind: state.0.kind, + title: &state.0.title, + logs: state.0.logger.render(), + state: CountStateOrRef::from(&state.0), + }; + print_stage(stage_num, &result); + } + + // Wrapper structs + // Required as we cannot specify &'static in wasm-bindgen: issue #1187 + + #[wasm_bindgen] + pub struct [](CountState<'static, $type>); + #[wasm_bindgen] + impl [] { + pub fn new(election: &[]) -> Self { + return [](CountState::new(election.as_static())); + } + } + + #[wasm_bindgen] + pub struct [](Election<$type>); + #[wasm_bindgen] + impl [] { + pub fn seats(&self) -> usize { self.0.seats } + + fn as_static(&self) -> &'static Election<$type> { + // Need to have this as we cannot specify &'static in wasm-bindgen: issue #1187 + unsafe { + let ptr = &self.0 as *const Election<$type>; + &*ptr + } + } + } + }} } -#[wasm_bindgen] -pub fn count_init_float64(state: &mut CountStateFloat64, opts: &STVOptions) { - stv::count_init(&mut state.0, &opts.0); -} - -#[wasm_bindgen] -pub fn count_one_stage_float64(state: &mut CountStateFloat64, opts: &STVOptions) -> bool { - return stv::count_one_stage(&mut state.0, &opts.0); -} +impl_type!(Rational); +impl_type!(NativeFloat64); // Reporting @@ -101,44 +154,6 @@ fn print_stage(stage_num: usize, result: &StageResult) { cprintln!(""); } -#[wasm_bindgen] -pub fn make_and_print_result_float64(stage_num: usize, state: &CountStateFloat64) { - let result = StageResult { - kind: state.0.kind, - title: &state.0.title, - logs: state.0.logger.render(), - state: CountStateOrRef::from(&state.0), - }; - print_stage(stage_num, &result); -} - -// Wrapper structs -// Required as we cannot specify &'static in wasm-bindgen: issue #1187 - -#[wasm_bindgen] -pub struct CountStateFloat64(CountState<'static, NativeFloat64>); -#[wasm_bindgen] -impl CountStateFloat64 { - pub fn new(election: &ElectionFloat64) -> Self { - return CountStateFloat64(CountState::new(election.as_static())); - } -} - -#[wasm_bindgen] -pub struct ElectionFloat64(Election); -#[wasm_bindgen] -impl ElectionFloat64 { - pub fn seats(&self) -> usize { self.0.seats } - - fn as_static(&self) -> &'static Election { - // Need to have this as we cannot specify &'static in wasm-bindgen: issue #1187 - unsafe { - let ptr = &self.0 as *const Election; - &*ptr - } - } -} - #[wasm_bindgen] pub struct STVOptions(stv::STVOptions<'static>); #[wasm_bindgen]