Compare commits
1 Commits
Author | SHA1 | Date |
---|---|---|
RunasSudo | dde79520a1 |
|
@ -1,16 +1,3 @@
|
|||
/target
|
||||
/html/opentally.js
|
||||
/html/opentally_*.wasm
|
||||
|
||||
/homepage/_news.html
|
||||
|
||||
# Functional specifications build products
|
||||
/docs/FnSpecs.*
|
||||
!/docs/FnSpecs.tex
|
||||
|
||||
# Jekyll
|
||||
/homepage/_site
|
||||
/homepage/.sass-cache
|
||||
/homepage/.jekyll-cache
|
||||
/homepage/.jekyll-metadata
|
||||
/homepage/vendor
|
||||
/html/opentally_bg.wasm
|
||||
|
|
|
@ -1,24 +1,11 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "adler"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.18"
|
||||
|
@ -28,18 +15,6 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
|
||||
|
||||
[[package]]
|
||||
name = "assert_cmd"
|
||||
version = "1.0.5"
|
||||
|
@ -99,39 +74,12 @@ version = "3.7.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631"
|
||||
|
||||
[[package]]
|
||||
name = "bytecheck"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb738a1e65989ecdcd5bba16079641bd7209688fa546e1064832fd6e012fd32a"
|
||||
dependencies = [
|
||||
"bytecheck_derive",
|
||||
"ptr_meta",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytecheck_derive"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3b4dff26fdc9f847dab475c9fec16f2cba82d5aa1f09981b87c44520721e10a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "0.1.10"
|
||||
|
@ -146,23 +94,22 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d53da17d37dba964b9b3ecb5c5a1f193a2762c700e6829201e645b9381c99dc7"
|
||||
version = "3.0.0-beta.2"
|
||||
source = "git+https://github.com/clap-rs/clap?branch=master#65b3892ef6c1ddf0cf837c76d164b8182103fa5d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"clap_derive",
|
||||
"clap_lex",
|
||||
"indexmap",
|
||||
"once_cell",
|
||||
"lazy_static",
|
||||
"os_str_bytes",
|
||||
"textwrap",
|
||||
"vec_map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "3.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c11d40217d16aee8508cc8e5fde8b4ff24639758608e5374e731b53f85749fb9"
|
||||
version = "3.0.0-beta.2"
|
||||
source = "git+https://github.com/clap-rs/clap?branch=master#65b3892ef6c1ddf0cf837c76d164b8182103fa5d"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro-error",
|
||||
|
@ -171,15 +118,6 @@ dependencies = [
|
|||
"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 = "console_error_panic_hook"
|
||||
version = "0.1.6"
|
||||
|
@ -242,82 +180,15 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f2c43f534ea4b0b049015d00269734195e6d3f0f6635cb692251aca6f9f8b3c"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e91455b86830a1c21799d94524df0845183fa55bafd9aa137b01c7d1065fa36"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d13202debe11181040ae9063d739fa32cfcaaebe2275fe387703460ae2365b30"
|
||||
dependencies = [
|
||||
"derive_builder_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_core"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66e616858f6187ed828df7c64a6d71720d83767a7f19740b2d1b6fe6327b36e5"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_macro"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58a94ace95092c5acb1e97a7e846b310cfbd499652f72297da7493f618a98d73"
|
||||
dependencies = [
|
||||
"derive_builder_core",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.16"
|
||||
version = "0.99.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40eebddd2156ce1bb37b20bbe5151340a31828b1f2d22ba4141f3531710e38df"
|
||||
checksum = "5cc7b9cef1e351660e5443924e4f43ab25fbbed3e9a5f052df3677deb4d6b320"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"syn",
|
||||
]
|
||||
|
||||
|
@ -336,27 +207,6 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-next"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"dirs-sys-next",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys-next"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_users",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "doc-comment"
|
||||
version = "0.3.3"
|
||||
|
@ -369,33 +219,6 @@ version = "1.6.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
|
||||
dependencies = [
|
||||
"errno-dragonfly",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno-dragonfly"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.20"
|
||||
|
@ -417,12 +240,6 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.4"
|
||||
|
@ -433,17 +250,6 @@ dependencies = [
|
|||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "git-version"
|
||||
version = "0.3.4"
|
||||
|
@ -482,37 +288,13 @@ version = "0.9.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.0"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
|
||||
checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html-escape"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "816ea801a95538fc5f53c836697b3f8b64a9d664c4f0b91efe1fe7c92e4dbcb7"
|
||||
dependencies = [
|
||||
"utf8-width",
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -528,12 +310,6 @@ dependencies = [
|
|||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.6.2"
|
||||
|
@ -541,29 +317,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown 0.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-lifetimes"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"io-lifetimes",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -598,15 +352,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.139"
|
||||
version = "0.2.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
|
||||
checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
|
@ -655,12 +403,6 @@ dependencies = [
|
|||
"rawpointer",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nohash-hasher"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
|
||||
|
||||
[[package]]
|
||||
name = "normalize-line-endings"
|
||||
version = "0.3.0"
|
||||
|
@ -669,9 +411,9 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
|
|||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.2"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74e768dff5fb39a41b3bcd30bb25cf989706c90d028d1ad71971987aa309d535"
|
||||
checksum = "4e0d047c1062aa51e256408c560894e5251f08925980e53cf1aa5bd00eec6512"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
|
@ -718,12 +460,6 @@ dependencies = [
|
|||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.3.0"
|
||||
|
@ -734,40 +470,33 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
|||
name = "opentally"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
"clap",
|
||||
"console_error_panic_hook",
|
||||
"csv",
|
||||
"derive_builder",
|
||||
"derive_more",
|
||||
"flate2",
|
||||
"git-version",
|
||||
"html-escape",
|
||||
"ibig",
|
||||
"itertools",
|
||||
"js-sys",
|
||||
"ndarray",
|
||||
"nohash-hasher",
|
||||
"num-bigint",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
"paste",
|
||||
"predicates",
|
||||
"prettytable-rs",
|
||||
"rkyv",
|
||||
"rug",
|
||||
"sha2",
|
||||
"utf8-chars",
|
||||
"wasm-bindgen",
|
||||
"xmltree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.1.0"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa"
|
||||
checksum = "6acbef58a60fe69ab50510a55bc8cdd4d6cf2283d27ad338f54cb52747a9cf2d"
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
|
@ -775,15 +504,6 @@ version = "1.0.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58"
|
||||
|
||||
[[package]]
|
||||
name = "pest"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
|
||||
dependencies = [
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "predicates"
|
||||
version = "1.0.8"
|
||||
|
@ -813,20 +533,6 @@ dependencies = [
|
|||
"treeline",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettytable-rs"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a"
|
||||
dependencies = [
|
||||
"csv",
|
||||
"encode_unicode",
|
||||
"is-terminal",
|
||||
"lazy_static",
|
||||
"term",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
|
@ -859,31 +565,11 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.39"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f"
|
||||
checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ptr_meta"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1"
|
||||
dependencies = [
|
||||
"ptr_meta_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ptr_meta_derive"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -916,26 +602,6 @@ version = "0.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"redox_syscall",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.5.4"
|
||||
|
@ -962,40 +628,6 @@ version = "0.6.25"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
||||
|
||||
[[package]]
|
||||
name = "rend"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d0351a2e529ee30d571ef31faa5a4e0b9addaad087697b77efb20d2809e41c7"
|
||||
dependencies = [
|
||||
"bytecheck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rkyv"
|
||||
version = "0.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e804c561b577f5836dc8a1962b7f7a03eae36f716dcd5f779c5d52a0e9c09a7"
|
||||
dependencies = [
|
||||
"bytecheck",
|
||||
"hashbrown 0.11.2",
|
||||
"ptr_meta",
|
||||
"rend",
|
||||
"rkyv_derive",
|
||||
"seahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rkyv_derive"
|
||||
version = "0.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0afbc272334d4a4896e382508531f941a7d9505057d7424bcbed653682ce661e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rug"
|
||||
version = "1.12.0"
|
||||
|
@ -1007,65 +639,12 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.36.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"io-lifetimes",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
|
||||
|
||||
[[package]]
|
||||
name = "seahash"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
|
||||
dependencies = [
|
||||
"semver-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver-parser"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
|
||||
dependencies = [
|
||||
"pest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.126"
|
||||
|
@ -1091,59 +670,22 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[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.96"
|
||||
version = "1.0.72"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf"
|
||||
checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "term"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f"
|
||||
dependencies = [
|
||||
"dirs-next",
|
||||
"rustversion",
|
||||
"winapi",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.15.0"
|
||||
version = "0.13.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
checksum = "cd05616119e612a8041ef58f2b578906cc2531a6069047ae092cfb86a325d835"
|
||||
|
||||
[[package]]
|
||||
name = "treeline"
|
||||
|
@ -1158,37 +700,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
|
||||
|
||||
[[package]]
|
||||
name = "ucd-trie"
|
||||
version = "0.1.3"
|
||||
name = "unicode-segmentation"
|
||||
version = "1.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
|
||||
checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.1"
|
||||
name = "unicode-xid"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c"
|
||||
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.8"
|
||||
name = "vec_map"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
|
||||
|
||||
[[package]]
|
||||
name = "utf8-chars"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1348d8face79d019be7cbc0198e36bf93e160ddbfaa7bb54c9592627b9ec841"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8-width"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cf7d77f457ef8dfa11e4cd5933c5ddb5dc52a94664071951219a97710f0a32b"
|
||||
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
|
@ -1205,17 +732,11 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.2+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.81"
|
||||
version = "0.2.74"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994"
|
||||
checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"wasm-bindgen-macro",
|
||||
|
@ -1223,9 +744,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.81"
|
||||
version = "0.2.74"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a"
|
||||
checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"lazy_static",
|
||||
|
@ -1238,9 +759,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.81"
|
||||
version = "0.2.74"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa"
|
||||
checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
|
@ -1248,9 +769,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.81"
|
||||
version = "0.2.74"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048"
|
||||
checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -1261,9 +782,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.81"
|
||||
version = "0.2.74"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be"
|
||||
checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
|
@ -1287,63 +808,6 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
|
||||
|
||||
[[package]]
|
||||
name = "xml-rs"
|
||||
version = "0.8.3"
|
||||
|
|
35
Cargo.toml
35
Cargo.toml
|
@ -7,56 +7,43 @@ edition = "2018"
|
|||
[lib]
|
||||
crate-type = ["lib", "cdylib"]
|
||||
|
||||
[features]
|
||||
default = ["wasm"]
|
||||
wasm = [] # Build default wasm bindings
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.44"
|
||||
csv = "1.1.6"
|
||||
derive_builder = "0.10.2"
|
||||
derive_more = "0.99.14"
|
||||
git-version = "0.3.4"
|
||||
nohash-hasher = "0.2.0"
|
||||
ibig = "0.3.2"
|
||||
itertools = "0.10.1"
|
||||
ndarray = "0.15.3"
|
||||
num-traits = "0.2"
|
||||
predicates = "1.0.8"
|
||||
num-traits = "0.2"
|
||||
sha2 = "0.9.5"
|
||||
wasm-bindgen = "0.2.81"
|
||||
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"
|
||||
js-sys = "0.3.51"
|
||||
html-escape = "0.2.9"
|
||||
num-bigint = "0.4.0"
|
||||
num-rational = "0.4.0"
|
||||
paste = "1.0.5"
|
||||
|
||||
# For tests/CLI only
|
||||
# For tests
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
assert_cmd = "1.0.5"
|
||||
csv = "1.1.6"
|
||||
flate2 = "1.0"
|
||||
prettytable-rs = "0.10.0"
|
||||
rkyv = "0.7.15"
|
||||
utf8-chars = "1.0.2"
|
||||
xmltree = "0.10.3"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.clap]
|
||||
version = "3.2.5"
|
||||
default-features = false
|
||||
features = ["std", "derive"]
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.rug]
|
||||
version = "1.12"
|
||||
default-features = false
|
||||
features = ["integer", "rational", "float"]
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.clap]
|
||||
#version = "3.0.0-beta.2" # Bug 2279
|
||||
git = "https://github.com/clap-rs/clap"
|
||||
branch = "master"
|
||||
default-features = false
|
||||
features = ["std", "derive"]
|
||||
|
||||
[profile.test]
|
||||
opt-level = 3
|
||||
|
||||
[profile.perf]
|
||||
inherits = "release"
|
||||
debug = true
|
||||
|
|
|
@ -10,7 +10,7 @@ OpenTally may be used in a number of different ways:
|
|||
|
||||
## Features
|
||||
|
||||
OpenTally accepts data in the [BLT file format](https://yingtongli.me/git/OpenTally/about/docs/blt-fmt.md), and can count votes using:
|
||||
OpenTally accepts data in the [BLT file format](https://yingtongli.me/git/OpenTally/about/docs/blt.md), and can count votes using:
|
||||
|
||||
* weighted inclusive Gregory STV (e.g. [Scottish STV](https://www.legislation.gov.uk/ssi/2011/399/schedule/1/made))
|
||||
* unweighted inclusive Gregory STV (e.g. [Australian Senate STV](https://www.legislation.gov.au/Details/C2020C00400/Html/Text#_Toc59107700))
|
||||
|
@ -24,11 +24,14 @@ OpenTally is highly customisable, including options for:
|
|||
* calculations using fixed-point arithmetic, guarded fixed-point ([quasi-exact](http://www.votingmatters.org.uk/ISSUE24/I24P2.pdf)) or exact rational numbers
|
||||
* different tie breaking rules (backwards, random, manual) with auditable deterministic random number generation
|
||||
* multiple constraints (e.g. affirmative action rules)
|
||||
* equal rankings
|
||||
|
||||
## Online usage
|
||||
|
||||
See the [quick start guide](/opentally/docs/quick-start.html) for how to use OpenTally online.
|
||||
After preparing the [BLT file](https://yingtongli.me/git/OpenTally/about/docs/blt.md), open the web UI. Select the BLT file, and click *Count*. OpenTally will count the election and display the results in a count sheet.
|
||||
|
||||
By clicking *Show advanced options*, you can customise the options used for the count. A detailed explanation of the various options can be found [here](https://yingtongli.me/git/OpenTally/about/docs/options.md).
|
||||
|
||||
Once the count is complete, you can click *Print result* to generate a printable result report.
|
||||
|
||||
## Command line usage
|
||||
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
PROFILE=${1:-release}
|
||||
cargo build --lib --target wasm32-unknown-unknown --$PROFILE && /home/runassudo/.cargo/bin/wasm-bindgen --target no-modules target/wasm32-unknown-unknown/$PROFILE/opentally.wasm --out-dir html --no-typescript
|
|
@ -0,0 +1,20 @@
|
|||
#!/bin/bash
|
||||
mkdir -p target/coverage/prof
|
||||
rm target/coverage/prof/*.profraw
|
||||
|
||||
export RUSTC=./rustc_bs.sh
|
||||
export RUSTFLAGS="-Zinstrument-coverage -Copt-level=0 -Clink-dead-code"
|
||||
export LLVM_PROFILE_FILE="target/coverage/prof/opentally-%p-%m.profraw"
|
||||
export CARGO_TARGET_DIR=target/coverage
|
||||
cargo test
|
||||
|
||||
llvm-profdata merge -sparse target/coverage/prof/*.profraw -o target/coverage/opentally.profdata
|
||||
|
||||
# Need "eval" to correctly parse arguments
|
||||
eval llvm-cov show target/coverage/debug/opentally -instr-profile=target/coverage/opentally.profdata -Xdemangler="$HOME/.cargo/bin/rustfilt" \
|
||||
$(for file in $(cargo test --no-run --message-format=json 2>/dev/null | jq -r "select(.profile.test == true) | .filenames[]"); do echo -n --object '"'$file'" '; done) \
|
||||
-ignore-filename-regex="$HOME/." \
|
||||
-ignore-filename-regex=numbers/rational_num.rs \
|
||||
-ignore-filename-regex=stv/wasm.rs \
|
||||
-ignore-filename-regex=tests \
|
||||
-format=html --show-instantiations=false --output-dir=target/coverage/html
|
1018
docs/FnSpecs.tex
1018
docs/FnSpecs.tex
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
|||
# BLT file format
|
||||
|
||||
OpenTally accepts ballot data in the BLT file format, as described by [Hill, Wichmann & Woodall](https://www.dia.govt.nz/diawebsite.NSF/Files/meekm/%24file/meekm.pdf) for their implementation of Meek STV. The BLT file format is also used by [OpenSTV/OpaVote](https://www.opavote.com/help/overview#blt-file-format), Lundell's [Droop](https://github.com/jklundell/droop/wiki/BltFileFormat) and Otten's [eSTV](https://web.archive.org/web/20020606014623/http://estv.otten.co.uk/) (where it is known as a DAT data transfer file).
|
||||
OpenTally accepts ballot data in the BLT file format, as described by [Hill, Wichmann & Woodall](https://www.dia.govt.nz/diawebsite.NSF/Files/meekm/%24file/meekm.pdf) for their implementation of Meek STV. The BLT file format is also used by [OpenSTV/OpaVote](https://www.opavote.com/openstv), Lundell's [Droop](https://github.com/jklundell/droop/wiki/BltFileFormat) and Otten's [eSTV](https://web.archive.org/web/20020606014623/http://estv.otten.co.uk/) (where it is known as a DAT data transfer file).
|
||||
|
||||
The file format is as follows:
|
||||
|
||||
|
@ -9,9 +9,9 @@ The file format is as follows:
|
|||
-2
|
||||
3 1 3 4 0
|
||||
4 1 3 2 0
|
||||
2 4 1=3 0
|
||||
2 4 1 3 0
|
||||
1 2 0
|
||||
2 2=4=3 1 0
|
||||
2 2 4 3 1 0
|
||||
1 3 4 2 0
|
||||
0
|
||||
"Adam"
|
||||
|
@ -21,16 +21,14 @@ The file format is as follows:
|
|||
"Title"
|
||||
```
|
||||
|
||||
The first line (`4 2`) indicates that there are 4 candidates for 2 vacancies.
|
||||
The first line (`4 2`) indicates that there are 4 candidates for 2 vacancies. This must be on its own line.
|
||||
|
||||
The second line (`-2`), which is optional, indicates that the 2nd candidate (Basil) has withdrawn. Multiple withdrawn candidates may be specified on this line, e.g. `-2 -3 -4`.
|
||||
The second line (`-2`), which is optional, indicates that the 2nd candidate (Basil) has withdrawn. Multiple withdrawn candidates may be specified on this line, e.g. `-2 -3 -4`. This must, if present, be on its own line.
|
||||
|
||||
The third line (second, if there are no withdrawn candidates) begins the ballot data. `3 1 3 4 0` indicates that there were 3 ballots which voted, in order of preference, for the 1st candidate (Adam), then the 3rd candidate (Charlotte), then the 4th candidate (Donald). An `=` indicates that multiple candidates were ranked at the same preference. A `0` optionally indicates the end of the list of preferences.
|
||||
The third line (second, if there are no withdrawn candidates) begins the ballot data. `3 1 3 4 0` indicates that there were 3 ballots which voted, in order of preference, for the 1st candidate (Adam), then the 3rd candidate (Charlotte), then the 4th candidate (Donald). A `0` optionally indicates the end of the list of preferences. Each such set of ballots must be on its own line.
|
||||
|
||||
The end of the list of ballots must be indicated with a single `0`.
|
||||
The end of the list of ballots must be indicated with a single `0`, which must be on its own line.
|
||||
|
||||
The next lines give the names of the candidates, up to the number of candidates specified on the first line (in this case, 4). Each candidate's name, unless a single word, must be surrounded by quotation marks.
|
||||
The next lines give the names of the candidates, up to the number of candidates specified on the first line (in this case, 4). Each candidate's name must be surrounded by quotation marks, and must appear on its own line.
|
||||
|
||||
The final line gives the name of the election, which, unless a single word, must be surrounded by quotation marks.
|
||||
|
||||
Newlines are optional, but if not provided, the `0` at the end of each ballot's preferences is mandatory.
|
||||
The final line gives the name of the election, which must appear on its own line.
|
|
@ -1,6 +1,6 @@
|
|||
# CON file format
|
||||
|
||||
OpenTally accepts the specification of constraints in a nonstandard file format, referred to as a CON file. The CON format is inspired by the standard [BLT file format](blt-fmt.md) used for ballot data.
|
||||
OpenTally accepts the specification of constraints in a nonstandard file format, referred to as a CON file. The CON format is inspired by the standard [BLT file format](blt.md) used for ballot data.
|
||||
|
||||
Suppose there are 7 candidates in the election. An example CON file is as follows:
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
# Glossary of OpenTally terminology
|
||||
|
||||
Terminology relating to the single transferable vote is varied and not standardised. This page describes how terms are used in OpenTally.
|
||||
|
||||
## Candidates
|
||||
|
||||
A ***continuing candidate*** is a candidate who has been neither elected nor excluded (nor doomed, when constraints are in use). Other sources sometimes call this a *hopeful* candidate.
|
||||
|
||||
A candidate is ***elected*** upon meeting the quota. A candidate is ***excluded*** upon being eliminated from the count.
|
||||
|
||||
When constraints are in use, a candidate is ***guarded*** if they must be elected to obtain a result conforming to the constraints, and ***doomed*** if they must not be elected to obtain a result conforming to the constraints.
|
||||
|
||||
## Stages
|
||||
|
||||
In the single transferable vote, counting proceeds in ***stages***. In OpenTally, the defining feature of a stage is that it is at the end of each stage that candidates are declared elected or excluded. Other sources sometimes call these *rounds* or *counts*.
|
||||
|
||||
The first stage of an election count is the distribution of first preferences. Each subsequent stage involves the distribution of an elected candidate's surplus, or the exclusion of a candidate.
|
||||
|
||||
According to the particular rules in use, the exclusion of a candidate may take place over multiple stages. Other sources sometimes call these *substages*, but where candidates are declared elected or excluded at the end of each, OpenTally still calls these *stages*.
|
||||
|
||||
## Ballots and votes
|
||||
|
||||
A ***ballot*** represents a sequence of preferences, and when allocated to a particular candidate, has an associated ***value***. The unit for value is the ***vote***. The initial value associated with a ballot at the distribution of first preferences is the ballot's ***weight***.
|
||||
|
||||
A ***parcel*** is a set of ballots allocated to a particular candidate (or exhausted) all at the same value (relative to their weight). Other sources sometimes call this a *bundle* or *batch*.
|
||||
|
||||
In OpenTally, value is tracked at the level of parcels rather than individual ballots, so the value of an individual ballot is calculated when required as the ballot's weight multiplied by the value of the parcel containing it.
|
||||
|
||||
A candidate's ***progress total*** is the total value of all ballots allocated to that candidate.
|
||||
|
||||
## Transferring ballots
|
||||
|
||||
The ***next available preference*** on a ballot is the continuing candidate who appears highest on the ballot's preferences. During a surplus distribution or exclusion, a ballot is said to be ***transferable*** if there is a next available preference. If not, the ballot is said to be ***non-transferable*** and set aside as ***exhausted***.
|
||||
|
||||
Some sources differentiate between ballots which are *formal* (valid) and *informal* (blank or invalid for some reason or another). OpenTally assumes that all ballots are valid. The number of blank ballots is reported as the number of ballots exhausted in the first stage.
|
||||
|
||||
A candidate has a ***surplus*** when their progress total exceeds the quota. During a surplus distribution, the surplus is divided by the number of ballots or votes examined in the distribution (according to the particular rules in use), and the quotient is called the ***surplus fraction***. Other sources, particularly those using the exclusive Gregory or unweighted inclusive Gregory methods, often call this the *transfer value*.
|
||||
|
||||
If, as a result of rounding, the number of votes transferred during a surplus distribution is less than the surplus, the difference is called ***loss by fraction***. Other sources sometimes call this *lost fractions*, *vote fractions not transferred due to rounding*, or similar. Some other sources consider the exhausted votes and loss by fraction together, and call the total the *non-transferable difference*, *votes lost*, or similar.
|
249
docs/options.md
249
docs/options.md
|
@ -1,49 +1,19 @@
|
|||
# Options and advanced options
|
||||
|
||||
**Note:** OpenTally is in the process of transitioning to being defined by formal functional specifications. A draft of the functional specifications can be found [here]({{ site.baseurl }}/docs/FnSpecs.pdf), which describes many of these options in greater detail.
|
||||
|
||||
## Preset
|
||||
|
||||
The preset dropdown allows you to choose from a hardcoded list of preloaded STV counting rules. These are:
|
||||
|
||||
| Method | Description | Exceptions | Validated |
|
||||
|-|-|-|-|
|
||||
| [OpenTally WIGM](https://yingtongli.me/blog/2021/07/24/opentally-wigm.html) | Recommended set of simple STV rules designed for computer counting, using the weighted inclusive Gregory method, exact quotas and rational arithmetic. | | |
|
||||
| Scottish STV | Rules from the [*Scottish Local Government Elections Order 2011*](https://www.legislation.gov.uk/ssi/2011/399/schedule/1/made), using the weighted inclusive Gregory method. | | ✓ |
|
||||
| Meek STV | Advanced STV rules designed for computer counting, recognised by the Proportional Representation Society of Australia (Victoria–Tasmania) as the superior STV system. | | |
|
||||
| • OpenTally Meek | Recommended rules for Meek STV. Operates according to the original 1987 Hill–Wichmann–Woodall ‘Algorithm 123’ specification ([*The Computer Journal* 1987;30(3):277–81](https://www.dia.govt.nz/diawebsite.NSF/Files/meekm/%24file/meekm.pdf)), except that (a) ties are broken backwards then at random, (b) fixed-point arithmetic with 5 decimal places is used, and (c) candidates are elected on strictly exceeding the quota. | | ✓ |
|
||||
| • Meek STV (2006) | Operates according to Hill's 2006 revisions ([*Voting Matters* 2006;(22):7–10](http://www.votingmatters.org.uk/ISSUE22/I22P2.pdf)). This is the algorithm referred to in OpenSTV/OpaVote as ‘Meek STV’, and forms the basis of New Zealand's Meek STV rules. | [E1] | ✓ |
|
||||
| • Meek STV (New Zealand) | Rules from Schedule 1A of the [*Local Electoral Regulations 2001*](https://www.legislation.govt.nz/regulation/public/2001/0145/latest/DLM57125.html). | [E1] | ✓ |
|
||||
| Australian Senate STV | Rules from section 273 of the [*Commonwealth Electoral Act 1918*](https://www.legislation.gov.au/Details/C2020C00400/Html/Text#_Toc59107700), using the unweighted inclusive Gregory method. | [E2] [E3] [E4] | ✓ |
|
||||
| NSW Local Government STV | Rules from Schedule 5 of the [*Local Government (General) Regulation 2021*](https://legislation.nsw.gov.au/view/html/inforce/2022-12-16/sl-2021-0460#sch.5), using the weighted inclusive Gregory method. | [E2] [E5] | ✓ |
|
||||
| Victorian Legislative Council STV | Rules from section 114A of the [*Electoral Act 2002* (Vic)](https://content.legislation.vic.gov.au/sites/default/files/2022-06/02-23aa064%20authorised.pdf), using the unweighted inclusive Gregory method. | [E2] | ✓ |
|
||||
| Western Australia STV | Rules from Schedule 1 of the [*Electoral Act 1907* (WA)](https://www.legislation.wa.gov.au/legislation/prod/filestore.nsf/FileURL/mrdoc_29498.pdf/$FILE/Electoral%20Act%201907%20-%20[17-a0-06].pdf), using the weighted inclusive Gregory method. | [E2] [E3] [E5] | |
|
||||
| Australian Capital Territory STV | Rules from Schedule 4 of the [*Electoral Act 1992* (ACT)](https://www.legislation.act.gov.au/View/a/1992-71/current/PDF/1992-71.PDF), using the exclusive Gregory method. | | ✓ |
|
||||
| Minneapolis STV | Rules from chapter 167 of the [*Minneapolis Code of Ordinances*](https://library.municode.com/mn/minneapolis/codes/code_of_ordinances?nodeId=COOR_TIT8.5EL_CH167MUELRUCO), using the weighted inclusive Gregory method. | [E6] | ✓ |
|
||||
| Cambridge STV | Rules in force in Cambridge, Massachusetts, using random sample transfers. These rules are derived from the [former chapter 54A of the Massachusetts General Laws](https://www.cambridgema.gov/-/media/Files/electioncommission/massachusettsgenerallawschapter54a.pdf), but have by regulation been modified to incorporate the procedures set out in Article IX of the former [1938 Charter of the City of Cincinnati](https://catalog.hathitrust.org/Record/001754258). See also [here](https://web.archive.org/web/20081118104049/http://www.fairvote.org/media/1993countmanual.pdf). | | ✓ |
|
||||
| Dáil Éireann STV | Rules from the [*Electoral Act 1992* (Ireland)](http://www.irishstatutebook.ie/eli/1992/act/23/enacted/en/print), using stratified random sample transfers. | [E4] [E7] | ✓ |
|
||||
| [van der Craats (‘Wright’) STV](https://www.aph.gov.au/Parliamentary_Business/Committees/House_of_Representatives_Committees?url=em/elect07/subs/sub051.1.pdf) | Rules proposed by Anthony van der Craats designed for computer counting, involving reset and re-iteration of the count after each candidate exclusion. | | ✓ |
|
||||
| [PRSA 1977](https://www.prsa.org.au/rule1977.htm) | Simple rules designed for hand counting, using the exclusive Gregory method, with counting performed in thousandths of a vote. | | ✓ |
|
||||
| [ERS97](https://www.electoral-reform.org.uk/latest-news-and-research/publications/how-to-conduct-an-election-by-the-single-transferable-vote-3rd-edition/) | More complex rules designed for hand counting, using the exclusive Gregory method. | [E8] [E9] [E10] | ✓ |
|
||||
| • ERS76 | Former rules from the 1976 2nd edition. | [E8] [E9] [E10] [E11] | ✓ |
|
||||
| • ERS73 | Former rules from the 1973 1st edition. | [E8] [E9] [E10] [E11] | |
|
||||
| Church of England | Rules from the Church of England [*Single Transferable Vote Rules 2020*](https://www.churchofengland.org/sites/default/files/2020-02/STV%20Rules%202020%20-%20final.pdf), similar to ERS73. | [E8] | ✓ |
|
||||
|
||||
Exceptions:
|
||||
|
||||
* [E1] When generating random numbers, OpenTally uses a [deterministic random number generator based on SHA-256](https://yingtongli.me/git/OpenTally/about/docs/rng.md), rather than the Wichmann–Hill(-based) algorithm.
|
||||
* [E2] When breaking ties backwards, OpenTally applies a recursive method rather than the method described in the legislation. The OpenTally developers regard the method described in the legislation as a defect. For an independent discussion, see <a href="https://dl.acm.org/doi/10.1145/3014812.3014837">Conway et al.</a>
|
||||
* [E3] A tie between 2 candidates for the final vacancy will be broken backwards then at random, rather than the method described in the legislation.
|
||||
* [E4] Bulk exclusion is not performed, as the prescribed rules are more conservative than OpenTally's. See also the section on *Bulk exclusion* for further discussion.
|
||||
* [E5] The legislation is drafted such that a consistent interpretation is impossible – see <a href="https://github.com/AndrewConway/ConcreteSTV/blob/main/nsw/NSWLocalCouncilLegislation2021Commentary.md">[1]</a>, <a href="https://yingtongli.me/blog/2022/07/15/wigm-legislation.html">[2]</a> for a discussion. In practice, the New South Wales and Western Australia Electoral Commissions have applied the ‘by parcel’ method of segmented exclusion and rounding subtransfers, which OpenTally follows.
|
||||
* [E6] The ‘mathematically eliminated by the sum of all ranked-choice votes comparison’ is not implemented, and undeclared write-in candidates are not distinguished.
|
||||
* [E7] The ‘quarter of a quota’ provision is disregarded when determining whether to defer surplus distributions.
|
||||
* [E8] The distribution of a surplus is not deferred if it exactly equals the difference between the 2 trailing continuing candidates.
|
||||
* [E9] The distribution of a surplus is deferred if a bulk exclusion could be performed and it could not change the bulk exclusion, even if it could change which candidate is last.
|
||||
* [E10] No distinction is made between stages and substages (during exclusion). This affects only the numbering of stages and not the result.
|
||||
* [E11] By default, the quota is always calculated to 2 decimal places. For full ERS76 (ERS73) compliance, set *Round quota to 0 d.p.* when the quota is more than 100 (100 or more).
|
||||
|
||||
For details of validation, see [validation.md](https://yingtongli.me/git/OpenTally/about/docs/validation.md).
|
||||
* *Recommended WIGM*: A recommended set of simple STV rules designed for computer counting, using the weighted inclusive Gregory method and rational arithmetic.
|
||||
* *Scottish STV*: Rules from the [*Scottish Local Government Elections Order 2011*](https://www.legislation.gov.uk/ssi/2011/399/schedule/1/made), using the weighted inclusive Gregory method. Validated against the [2007 Scottish local government election result for Linn ward](https://web.archive.org/web/20121004213938/http://www.glasgow.gov.uk/en/YourCouncil/Elections_Voting/Election_Results/ElectionScotland2007/LGWardResults.htm?ward=1&wardname=1%20-%20Linn).
|
||||
* [*Meek STV*](http://www.dia.govt.nz/diawebsite.NSF/Files/meekm/%24file/meekm.pdf): Advanced STV rules designed for computer counting, recognised by the Proportional Representation Society of Australia (Victoria–Tasmania) as the superior STV system.
|
||||
* *Meek STV (1987)* operates according to the original [Hill–Wichmann–Woodall specification](https://www.dia.govt.nz/diawebsite.NSF/Files/meekm/%24file/meekm.pdf) of Meek STV, with the modifications, relevant only in exceptional cases, that (a) fixed-point arithmetic with 5 decimal places is used, and (b) candidates are elected on strictly exceeding the quota. Validated against the Hill–Wichmann–Woodall implementation for the [ERS97 model election](https://www.electoral-reform.org.uk/latest-news-and-research/publications/how-to-conduct-an-election-by-the-single-transferable-vote-3rd-edition/#sub-section-24).
|
||||
* *Meek STV (2006)* operates according to [Hill's 2006 revisions](http://www.votingmatters.org.uk/ISSUE22/I22P2.pdf). This is the algorithm referred to in OpenSTV/OpaVote as ‘Meek STV’, and forms the basis of New Zealand's Meek STV rules. Validated against OpenSTV 1.7 for the ERS97 model election.
|
||||
* *Meek STV (New Zealand)* operates according to Schedule 1A of the [*Local Electoral Regulations 2001*](https://www.legislation.govt.nz/regulation/public/2001/0145/latest/DLM57125.html). Validated against OpenSTV 1.7, and Hill's nzmeek version 6.7.7, for the ERS97 model election.
|
||||
* *Australian Senate STV*: Rules from the [*Commonwealth Electoral Act 1918*](https://www.legislation.gov.au/Details/C2020C00400/Html/Text#_Toc59107700), using the unweighted inclusive Gregory method. Validated against the [2019 Australian Senate election result for Tasmania](https://results.aec.gov.au/24310/Website/SenateDownloadsMenu-24310-Csv.htm).
|
||||
* [*Wright STV*](https://www.aph.gov.au/Parliamentary_Business/Committees/House_of_Representatives_Committees?url=em/elect07/subs/sub051.1.pdf): Rules proposed by Anthony van der Craats designed for computer counting, involving reset and re-iteration of the count after each candidate exclusion. Validated against the [EVE Online reference implementation](https://github.com/ccpgames/ccp-wright-stv) for the [CSM 15 election](https://www.eveonline.com/news/view/meet-the-new-council).
|
||||
* [*PRSA 1977*](https://www.prsa.org.au/rule1977.htm): Simple rules designed for hand counting, using the exclusive Gregory method, with counting automatically performed in thousandths of a vote. Validated against [example 1](https://www.prsa.org.au/example1.pdf) of the PRSA's [*Proportional Representation Manual*](https://www.prsa.org.au/publicat.htm#p2).
|
||||
* [*ERS97*](https://www.electoral-reform.org.uk/latest-news-and-research/publications/how-to-conduct-an-election-by-the-single-transferable-vote-3rd-edition/): More complex rules designed for hand counting, using the exclusive Gregory method. Validated against the ERS97 model election.
|
||||
|
||||
This functionality is not available on the command line.
|
||||
|
||||
|
@ -53,31 +23,29 @@ This functionality is not available on the command line.
|
|||
|
||||
The quota dropdowns allow you to define the quota used in the election, and the quota criterion used to elect candidates. The quota may be set to:
|
||||
|
||||
* *Droop* (default) and *Droop (exact)*: *V*/(*S*+1)
|
||||
* *Droop* and *Droop (exact)*: *V*/(*S*+1)
|
||||
* *Hare* and *Hare (exact)*: *V*/*S*
|
||||
|
||||
where *V* is the number of votes and *S* is the number of seats.
|
||||
|
||||
The ‘*(exact)*’ version of each quota has effect only if *Round quota to [n] d.p.* is enabled. When that setting is enabled, *Droop* and *Hare* will increment the quota up to the next available rounded unit (even if the quotient is exact already), while the ‘*(exact)*’ versions will round the quota up if and only if the quotient is not already exact.
|
||||
|
||||
When *Round quota to [n] d.p.* is not enabled, or when the exact form is used, the Droop quota is also known as the Newland–Britton or Hagenbach-Bischoff quota.
|
||||
When *Round quota to [n] d.p.* is not enabled, *Droop* (or *Droop (exact)*) is also known as the Newland–Britton or Hagenbach-Bischoff quota.
|
||||
|
||||
### Quota criterion (-c/--quota-criterion)
|
||||
|
||||
The quota criterion may be set to *>=* (candidates are elected if they meet or exceed the quota) or *>* (default; candidates are elected only if they strictly exceed the quota).
|
||||
The quota criterion may be set to *>=* (candidates are elected if they meet or exceed the quota) or *>* (candidates are elected only if they strictly exceed the quota).
|
||||
|
||||
Note that the combination ‘*>= Droop (exact)*’ (or, when *Round quota to [n] d.p.* is disabled, ‘*>= Droop*’) can result in more candidates meeting the quota than there are available vacancies, hence this particular combination is not recommended.
|
||||
Note that the combination ‘*>= Droop (exact)*’ (with *Round quota to [n] d.p.* enabled) can result in more candidates meeting the quota than there are available vacancies, hence this particular combination is not recommended.
|
||||
|
||||
### Quota mode (--quota-mode)
|
||||
|
||||
This option allows you to specify whether the votes required for election can change during the count. The options are:
|
||||
|
||||
* *Static quota* (default): The quota is calculated once after all first-preference votes are allocated, and remains constant throughout the count.
|
||||
* *Static with ERS97/ERS76 rules*: The quota is static, but candidates may be elected if their vote exceeds (or equals, according to the *Quota criterion*) the active vote, divided by one more than the number of remaining vacancies (with minor variations in details between ERS97 and ERS76). Additionally, under ERS97 rules, the quota is reduced if ballots exhaust before any candidate is elected.
|
||||
* *Dynamic by total vote*: The quota is recalculated at the end of each stage, according to the *Quota* option.
|
||||
* *Dynamic by active vote*: The quota is recalculated at the end of each stage, according to the *Quota* option, but where *V* is the active vote and *S* is the number of remaining vacancies.
|
||||
* *Static quota*: The quota is calculated once after all first-preference votes are allocated, and remains constant throughout the count.
|
||||
* *Static with ERS97 rules*: The quota is static, but candidates may be elected if their vote exceeds (or equals, according to the *Quota criterion*) the total active vote, divided by (*S* + 1) (or *S*, according to the *Quota* option).
|
||||
|
||||
When a dynamic quota is used, then unless *Surplus method* is set to *Meek*, the quota that applies to an elected candidate is the quota at the start of the stage when the candidate's surplus is distributed. Further distributions are not performed later, even if the quota is later reduced.
|
||||
When *Surplus method* is set to *Meek method*, this setting is ignored, and the progressively reducing quota of the Meek method is instead applied.
|
||||
|
||||
## STV variants
|
||||
|
||||
|
@ -94,38 +62,29 @@ Some STV counting rules provide, for example, that ‘no surplus shall be transf
|
|||
|
||||
This dropdown allows you to select how ballots are transferred during surplus transfers. The recommended methods are:
|
||||
|
||||
* *Weighted inclusive Gregory* (default): During surplus transfers, all applicable ballots of the elected candidate are examined. Transfers are weighted according to the values of the ballots.
|
||||
* *Weighted inclusive Gregory* (default): During surplus transfers, all applicable ballot papers of the transferring candidate are examined. Transfers are weighted according to the weights of the ballot papers.
|
||||
* *Meek method*: Transfers are computed as described at <http://www.dia.govt.nz/diawebsite.NSF/Files/meekm/%24file/meekm.pdf>.
|
||||
|
||||
Other Gregory methods are supported, but not recommended:
|
||||
Other methods are supported, but not recommended:
|
||||
|
||||
* *Unweighted inclusive Gregory*: During surplus transfers, all applicable ballots of the elected candidate are examined. Transfers are not weighted, and each ballot has equal value in the calculation.
|
||||
* *Exclusive Gregory (last bundle)*: During surplus transfers, only the ballots received in the last transfer (all of one value) are examined.
|
||||
* *Unweighted inclusive Gregory*: During surplus transfers, all applicable ballot papers of the transferring candidate are examined. Transfers are not weighted, and each ballot paper has equal value in the calculation.
|
||||
* *Exclusive Gregory (last bundle)*: During surplus transfers, only the ballot papers received in the last transfer are examined. Transfers are not weighted.
|
||||
|
||||
Random sample methods are also supported, but also not recommended:
|
||||
Other surplus transfer methods, such as non-fractional transfers (e.g. random sample) are not supported at this time.
|
||||
|
||||
* *Hare (exclusive sample)*: During surplus transfers, a subset of the ballots received in the last transfer, equal in size to the surplus, is examined.
|
||||
* *Inclusive Hare (sample)*: During surplus transfers, a subset of the elected candidate's ballots, equal in size to the surplus, is examined.
|
||||
### Papers to examine in surplus transfer (--transferable-only)
|
||||
|
||||
A random sample method will usually be used with a *Quota criterion* set to *>=*.
|
||||
* *Include non-transferable papers* (default): When this option is selected, all ballot papers of the transferring candidate are examined. Non-transferable papers are always exhausted at the relevant surplus fractions.
|
||||
* *Use transferable papers only* (CLI: --transferable-only): When this option is selected, only transferable papers of the transferring candidate are examined. Non-transferable papers are exhausted only if the value of the transferable papers is less than the surplus.
|
||||
|
||||
### Ballots to examine in surplus transfer (--transferable-only/--surplus-assume-total)
|
||||
### Exclusion method (--exclusion)
|
||||
|
||||
* *Include non-transferable ballots* (default): When this option is selected, all ballots of the transferring candidate are examined. The denominator of the surplus fraction is the total value of the ballots. Non-transferable ballots are always exhausted at the relevant surplus fractions. This is the method typically used with the weighted inclusive Gregory or Meek methods.
|
||||
* *Assume progress total* (--surplus-assume-total): Same as *Include non-transferable ballots*, but the denominator of the surplus fraction is the candidate's recorded progress total. This has effect only as far as concerns rounding, and only in the weighted inclusive Gregory method.
|
||||
* *Use transferable ballots only* (--transferable-only): When this option is selected, only transferable ballots of the transferring candidate are examined. The denominator of the surplus fraction is the total value of the transferable ballots. Non-transferable ballots are exhausted only if the value of the transferable ballots is less than the surplus. This is the method typically used with other surplus distribution methods.
|
||||
* *Subtract non-transferables* (--transferable-only --surplus-assume-total): Same as *Use transferable ballots only*, but the value of the transferable ballots is calculated by subtracting the value of non-transferable ballots from the progress total. This has effect only as far as concerns rounding, and only in the weighted inclusive Gregory method.
|
||||
* *Exclude in one round* (default): When excluding candidate(s), transfer all their ballot papers in one stage.
|
||||
* *Exclude by parcel (by order)*: When excluding a candidate, transfer their ballot papers one parcel at a time, in the order each was received. Each parcel forms a separate stage, i.e. if a transfer allows another candidate to meet the quota criterion, no further papers are transferred to that candidate. This option cannot be combined with bulk exclusion.
|
||||
* *Exclude by value*: When excluding candidate(s), transfer their ballot papers in descending order of accumulated transfer value. Each transfer of all ballots of a certain transfer value forms a separate stage.
|
||||
* *Wright method (re-iterate)*: When excluding candidate(s), reset the count from the distribution of first preferences, disregarding the excluded candidates.
|
||||
|
||||
### (Gregory) Exclusion method (--exclusion)
|
||||
|
||||
When *Surplus method* is set to a Gregory method, this option controls how candidates are excluded:
|
||||
|
||||
* *Single stage* (default): When excluding candidate(s), transfer all their ballots in one stage.
|
||||
* *By value*: When excluding candidate(s), transfer their ballots in descending order of accumulated transfer value. Each transfer of all ballots of a certain transfer value forms a separate stage, i.e. if a transfer allows another candidate to meet the quota, no further ballots are transferred to that candidate.
|
||||
* *FPV then by value*: When excluding candidate(s), transfer their first preference ballot papers in the first stage, then transfer ballot papers received on transfers as in *By value*.
|
||||
* *By source*: When excluding candidate(s), transfer their ballots according to the candidate from which those ballots were received, in the order the transferring candidates were elected or excluded. Each transfer of all ballots received from a certain candidate forms a separate stage.
|
||||
* *By parcel (by order)*: When excluding a candidate, transfer their ballot ballots one parcel at a time, in the order each was received. Each parcel forms a separate stage. This option cannot be combined with bulk exclusion.
|
||||
* *Reset and re-iterate*: When excluding candidate(s), reset the count from the distribution of first preferences, disregarding the excluded candidates.
|
||||
When *Surplus method* is set to *Meek method*, this setting is ignored, and the Meek method is instead applied.
|
||||
|
||||
### (Meek) NZ-style exclusion (--meek-nz-exclusion)
|
||||
|
||||
|
@ -134,28 +93,11 @@ When *Surplus method* is set to *Meek method*, this option controls how candidat
|
|||
* When NZ-style exclusion is disabled (default), the excluded candidate's keep value is immediately reduced to 0. This is the method specified in the 1987 and 2006 Meek rules.
|
||||
* When NZ-style exclusion is enabled, all elected candidates' keep values are first updated by one further iteration; only then is the excluded candidate's keep value reduced to 0. This is the method specified in the New Zealand *Local Electoral Regulations 2001*.
|
||||
|
||||
### (Sample) Sample method (--sample)
|
||||
|
||||
When *Surplus method* is set to a random sample method, this option controls which subset of ballots is selected for transfer during surplus distributions:
|
||||
|
||||
* *Stratify* (default): The candidate's ballots are first stratified into subparcels according to next available preference, and an equal proportion of each subparcel is transferred, with the subset transferred comprising the ballots in each subparcel most recently received by the candidate. In the calculation of proportions, the largest remainders are rounded up so there is no loss by fraction. This is the method specified by the [*Electoral Act 1992* (Ireland)](http://www.irishstatutebook.ie/eli/1992/act/23/section/121/enacted/en/html#sec121).
|
||||
* *By order*: The subset transferred comprises the ballots most recently received by the candidate.
|
||||
* *Cincinnati*: The subset is selected using the deterministic method used in [Cambridge, Massachusetts](https://web.archive.org/web/20081118104049/http://www.fairvote.org/media/1993countmanual.pdf) (derived from Article IX of the former 1938 Cincinnati *Code of Ordinances*).
|
||||
|
||||
In any case, the subset selected depends on the order of ballots in the BLT file, and is independent of the *Random seed* option.
|
||||
|
||||
### (Sample) Transfer ballot-by-ballot (--sample-per-ballot)
|
||||
|
||||
When *Surplus method* is set to a random sample method, this option controls when candidates are declared elected:
|
||||
|
||||
* When ballot-by-ballot transfer is disabled (default), candidates are declared elected only at the end of a stage, as usual.
|
||||
* When ballot-by-ballot transfer is enabled, candidates are declared elected immediately on meeting the quota after the transfer of any single ballot. Consequential surpluses therefore do not arise, and surpluses only occur during the count of first preferences.
|
||||
|
||||
### Ties (-t/--ties)
|
||||
|
||||
This dropdown allows you to select how ties (in surplus transfer or exclusion) are broken. The options are:
|
||||
|
||||
* *Backwards*: Ties are broken according to which tied candidate had the most/fewest votes at the end of the *previous* stage. If a tie for most/fewest votes exists in the previous stage also, that tie is broken based on the next previous stage, and so on. This is the method specified, for example, by the [*Electoral Act 1992* (ACT)](https://www.legislation.act.gov.au/View/a/1992-71/current/PDF/1992-71.PDF).
|
||||
* *Backwards*: Ties are broken according to which tied candidate had the most/fewest votes at the end of the *most recent* stage where one tied candidate had more/fewer votes than the others, if such a stage exists.
|
||||
* *Fowards*: Ties are broken according to which tied candidate had the most/fewest votes at the end of the *earliest* stage where one tied candidate had more/fewer votes than the others, if such a stage exists. This is also known as the ‘ahead at first difference’ method.
|
||||
* *Random*: Ties are broken at random (see *Random seed*).
|
||||
* *Prompt*: The user is prompted to break the tie.
|
||||
|
@ -168,52 +110,15 @@ This option allows you to input an arbitrary value to seed the deterministic ran
|
|||
|
||||
The default value is the current date, formatted YYYYMMDD.
|
||||
|
||||
The algorithm used by the random number generator is specified at [rng.md](https://yingtongli.me/git/OpenTally/about/docs/rng.md).
|
||||
The algorithm used by the random number generator is specified at [rng.md](rng.md).
|
||||
|
||||
## Constraints (--constraints)
|
||||
|
||||
This file selector allows you to load a [CON file](https://yingtongli.me/git/OpenTally/about/docs/con-fmt.md) specifying constraints on the election. For example, if a certain minimum or maximum number of candidates can be elected from a particular category.
|
||||
This file selector allows you to load a [CON file](con.md) specifying constraints on the election. For example, if a certain minimum or maximum number of candidates can be elected from a particular category.
|
||||
|
||||
### Constraint method (--constraint-method)
|
||||
OpenTally applies constraints using the Grey–Fitzgerald method. Whenever a candidate is declared elected or excluded, any candidate who must be elected to secure a conformant result is deemed *guarded*, and any candidate who must not be elected to secure a conformant result is deemed *doomed*. Any candidate who is doomed is excluded at the next opportunity. Any candidate who is guarded is prevented from being excluded.
|
||||
|
||||
This dropdown allows you to select how constraints are applied. The options are:
|
||||
|
||||
*Guard/doom* (default):
|
||||
|
||||
When this option is selected, OpenTally applies constraints using the Gray–Fitzgerald method. Whenever a candidate is declared elected or excluded, any candidate who must be elected to secure a conformant result is deemed *guarded*, and any candidate who must not be elected to secure a conformant result is deemed *doomed*. Any candidate who is doomed is excluded at the next opportunity. Any candidate who is guarded is prevented from being excluded.
|
||||
|
||||
Multiple constraints are supported using the method described by Hill ([*Voting Matters* 1998;(9):2–4](http://www.votingmatters.org.uk/ISSUE9/P1.HTM)) and Otten ([*Voting Matters* 2001;(13):4–7](http://www.votingmatters.org.uk/ISSUE13/P3.HTM)).
|
||||
|
||||
*Repeat count*:
|
||||
|
||||
When this option is selected, only constraints specifying a maximum number of candidates to be elected from a particular group are supported. Other constraint groups will be **silently ignored**. Note that each candidate must still be assigned to exactly one group within each constraint.
|
||||
|
||||
The count proceeds as normal, until the point that a candidate would be elected who would violate the constraint. At this point, that candidate and all other candidates from the constrained group are excluded, and all previously excluded candidates from the non-constrained group are reintroduced.
|
||||
|
||||
All ballots are removed from the count, and redistributed among the candidates in the following order:
|
||||
|
||||
* Any undistributed surpluses, each surplus comprising one stage
|
||||
* Any exhausted ballots, in one or more stages (according to *Exclusion method*)
|
||||
* The ballots of each continuing candidate from the non-constrained group, in one or more stages (according to *Exclusion method*), candidate-by-candidate in random order or an order specified by the user (according to *Ties*, with options other than *Random* and *Prompt* ignored)
|
||||
* The ballots of each continuing candidate from the constrained group, in like manner
|
||||
|
||||
Once all ballots have been so redistributed, the count resumes as usual.
|
||||
|
||||
This method is specified, for example, in Schedule 1.1 of the [Monash Student Association *Election Regulations* (2021)](https://msa.monash.edu/app/uploads/2021/07/MSA-Election-Regulations-2021.pdf).
|
||||
|
||||
## Report options
|
||||
|
||||
### Report style
|
||||
|
||||
* *Votes only*: The result sheet displays the number of votes held by each candidate at each stage of the count.
|
||||
* *Votes (transposed)*: Same as *Votes only*, but transfers are displayed to the left of, rather than above, progress totals.
|
||||
* *Ballots and votes*: The result sheet displays the number of votes *and ballots* held by each candidate at each stage of the count.
|
||||
|
||||
This functionality is not available on the command line.
|
||||
|
||||
### Display up to [n] d.p. (--pp-decimals)
|
||||
|
||||
This option allows you to specify to how many decimal places votes will be reported in the results report. It does not affect the internal precision of calculations.
|
||||
Multiple constraints are supported using the method described by Hill ([*Voting Matters* 1998;9(1):2–4](http://www.votingmatters.org.uk/ISSUE9/P1.HTM)) and Otten ([*Voting Matters* 2001;13(3):4–7](http://www.votingmatters.org.uk/ISSUE13/P3.HTM)).
|
||||
|
||||
## Numeric representation
|
||||
|
||||
|
@ -223,94 +128,84 @@ This dropdown allows you to select how numbers (vote totals, etc.) are represent
|
|||
|
||||
* *Fixed*: Numbers are represented as fixed-precision decimals, up to a certain number of decimal places (default: 5).
|
||||
* *Fixed (guarded)*: Numbers are represented as fixed-precision decimals with ‘guard digits’ – also known as [‘quasi-exact’ arithmetic](http://www.votingmatters.org.uk/ISSUE24/I24P2.pdf). If *n* decimal places are requested, numbers are represented up to 2*n* decimal places, and two values are considered equal if the absolute difference is less than (10<sup>−*n*</sup>)/2.
|
||||
* *Rational* (default): Numbers are represented exactly as fractions, resulting in the elimination of rounding error, but increasing computational complexity when the number of surplus transfers is very large.
|
||||
* *Rational*: Numbers are represented exactly as fractions, resulting in the elimination of rounding error, but increasing computational complexity when the number of surplus transfers is very large.
|
||||
* *Float (64-bit)*: Numbers are represented as native 64-bit floating-point numbers. This is fast, but not recommended as unexpectedly large rounding errors may be introduced in some circumstances.
|
||||
|
||||
### Display up to [n] d.p. (--pp-decimals)
|
||||
|
||||
This option allows you to specify to how many decimal places votes will be reported in the results report. It does not affect the internal precision of calculations.
|
||||
|
||||
### Normalise ballots (--normalise-ballots)
|
||||
|
||||
In the BLT file format, each set of preferences can have a specified weight – this is typically used to indicate multiple voters who had the same preferences.
|
||||
|
||||
When ballots are not normalised (default), a set of preferences with weight *n* > 1 is represented as a single ballot with value *n*. This is known as [list-packed ballots](http://www.votingmatters.org.uk/ISSUE21/I21P1.pdf).
|
||||
|
||||
When ballots are normalised, a set of preferences with weight *n* > 1 is instead converted to *n* ballots each with value 1. This is generally required only when the rules directly deal with individual ballot weights, such as when *Sum surplus transfers* is set to *Per ballot*.
|
||||
|
||||
## Count optimisations
|
||||
|
||||
### Early bulk election (--no-early-bulk-elect)
|
||||
|
||||
When early bulk election is enabled (default), the count terminates as soon as the set of winning candidates is known. Specifically:
|
||||
When early bulk election is enabled (default), all remaining candidates are declared elected in a single stage as soon as the number of not-excluded candidates exactly equals the number of vacancies to fill. Further surplus distributions are not performed, and outstanding exclusions, if any, are not completed. This is typical of most STV rules.
|
||||
|
||||
* At the beginning of each stage, if the number of continuing candidates exactly equals the number of remaining vacancies, all continuing candidates are declared elected in a single stage. This is typical of most STV rules.
|
||||
* If a proposed exclusion would cause the number of continuing candidates to exactly equal the number of remaining vacancies, all other continuing candidates are declared elected without transfers arising from the proposed exclusion being performed.
|
||||
* At the end of any stage, if *n* vacancies remain and the *n*-th top continuing candidate has more votes than all lower continuing candidates (plus votes awaiting transfer), the *n* top continuing candidates are immediately declared elected.
|
||||
When early bulk election is disabled, surpluses continue to be distributed, and outstanding exclusions continue to be completed, even once the number of not-excluded candidates exactly equals the number of vacancies to fill. Bulk election is performed only once there are no more surpluses to distribute, and no exclusions to complete.
|
||||
|
||||
If an early bulk election is performed, further surplus distributions are not performed, and outstanding exclusions are not completed, even if they could change the order of election.
|
||||
|
||||
When early bulk election is disabled, surpluses continue to be distributed, and outstanding exclusions continue to be completed, even once the number of continuing candidates exactly equals the number of remaining vacancies. Bulk election is performed only as a final measure once there are no more surpluses to distribute, and no exclusions to complete.
|
||||
|
||||
In either case, candidates are declared elected in descending order of votes. This ensures that only one candidate is ever elected at a time and the order of election is well-defined, which is required e.g. for affirmative action rules.
|
||||
|
||||
Note that the OpenTally rules for early bulk election are aggressive, and many STV rules do not implement all 3 (if any at all). It is not possible at this time to selectively apply only some of the rules. In order to reproduce the result of a count performed by others, where not all rules were implemented, consider disabling early bulk election and comparing the results at the time a bulk election would have been made.
|
||||
In either case, candidates are declared elected in descending order of votes. This ensures that only one candidate is ever elected at a time and the order of election is well-defined, which is required e.g. for some affirmative action rules.
|
||||
|
||||
### Bulk exclusion (--bulk-exclude)
|
||||
|
||||
When bulk exclusion is disabled (default), only one candidate is ever excluded per stage.
|
||||
|
||||
When bulk exclusion is enabled, as many candidates as possible are excluded together in each single stage, provided that sufficient candidates remain to fill the vacancies, and the bulk exclusion could not change the order of exclusion. If 2 or more candidates are tied, either all are bulk excluded or none are. The ballots of all excluded candidates are considered together, and transferred according to the *Exclusion method*.
|
||||
|
||||
Note that some rules (such as the Australian Senate rules) provide for more conservative ‘bulk exclusion’ which additionally requires that the bulk exclusion cannot cause a candidate to be elected. This form of bulk exclusion accordingly cannot change the result compared with no bulk exclusion (except as far as rounding or parcelling may be concerned), and is not currently supported.
|
||||
When bulk exclusion is enabled, as many candidates are excluded as possible at once per stage, provided that sufficient candidates remain to fill the vacancies, and the bulk exclusion could not change the order of exclusion. If 2 or more candidates are tied, either all are bulk excluded or none are. The ballot papers of all excluded candidates are considered together, and transferred according to the *Exclusion method*.
|
||||
|
||||
### Defer surpluses (--defer-surpluses)
|
||||
|
||||
When deferred surpluses is disabled (default), all surpluses must be transferred before candidates can be excluded.
|
||||
|
||||
When deferred surpluses is enabled, the transfer of all surpluses is deferred if doing so could not change the next exclusion (including a bulk exclusion, if that is enabled).
|
||||
When deferred surpluses is enabled, the transfer of all surpluses is deferred if doing so could not change the order of exclusion (including of a bulk exclusion, if that is enabled).
|
||||
|
||||
### Immediate election (--no-immediate-elect)
|
||||
### (Meek) Immediate election (--meek-immediate-elect)
|
||||
|
||||
When *Surplus method* is set to a Gregory or random sample method, this option controls when candidates are elected:
|
||||
When *Surplus method* is set to *Meek method*, this option controls when candidates are elected:
|
||||
|
||||
* When immediate election is enabled (default), a candidate is declared at the end of the stage once reaching the quota. This is typical of most STV rules.
|
||||
* When immediate election is disabled, a candidate is declared elected only once their surplus is transferred. This is the method specified by the Minneapolis rules.
|
||||
|
||||
Likewise, when *Surplus method* is set to *Meek method*:
|
||||
|
||||
* When immediate election is enabled (default), a candidate meeting the quota interrupts a surplus distribution. The candidate is immediately declared elected, before the distribution of all surpluses of all now-elected candidates continues. This is the method specified in the 2006 Meek rules.
|
||||
* When immediate election is disabled, all current surpluses are distributed and keep values finalised, before any candidates exceeding the quota are then declared elected. This is the method specified in the 1987 Meek rules.
|
||||
|
||||
### Minimum threshold (--min-threshold)
|
||||
|
||||
When candidates are first to be excluded, all candidates with votes less than or equal to this threshold are excluded at once. The default value is 0, i.e. all candidates with no votes are excluded at once.
|
||||
* When immediate election is disabled (default), all current surpluses are distributed and keep values finalised, before any candidates exceeding the quota are then declared elected. This is the method specified in the 1987 Meek rules.
|
||||
* When immediate election is enabled, a candidate meeting the quota interrupts a surplus distribution. The candidate is immediately declared elected, before the distribution of all surpluses of all now-elected candidates continues. This is the method specified in the 2006 Meek rules.
|
||||
|
||||
## Rounding
|
||||
|
||||
### Round quota/votes/surplus fractions/ballot values to [n] d.p. (--round-quota, --round-votes, --round-surplus-fractions, --round-values)
|
||||
### Round quota/votes/surplus fractions/ballot weights to [n] d.p. (--round-quota, --round-votes, --round-tvs, --round-weights)
|
||||
|
||||
When rounding is enabled, the specified values are rounded to the specified number of decimal places. This enables, for example, votes to be counted only in integers, while ballot values and surplus fractions are calculated to higher precision (according to the *Numbers* option).
|
||||
When rounding is enabled, the specified values are rounded to the specified number of decimal places. This enables, for example, votes to be counted only in integers, while ballot weights and surplus fractions are calculated to higher precision (according to the *Numbers* option).
|
||||
|
||||
When enabled, the quota is incremented or rounded up (according to the *Quota* option). When *Surplus method* is set to a Gregory method, votes, surplus fractions and ballot values are always rounded down.
|
||||
When enabled, the quota is incremented or rounded up (according to the *Quota* option), whereas votes, surplus fractions and weights are always rounded down.
|
||||
|
||||
In relation to *Round surplus fractions to [n] d.p.* – note that surplus fractions are used in STV in calculations of the form *A* × (*B*/*C*), where (*B*/*C*) is the surplus fraction. The order of operations depends on this setting:
|
||||
In relation to *Round surplus fractions to [n] d.p.* (--round-tvs) – note that surplus fractions are used in STV in calculations of the form *A* × (*B*/*C*), where (*B*/*C*) is the surplus fraction. The order of operations depends on this setting:
|
||||
|
||||
* When this option is disabled (default), (*A* × *B*) is calculated first, then divided by *C*. This minimises rounding errors.
|
||||
* When this option is enabled, (*B*/*C*) is calculated separately first and rounded to the specified precision, before being multiplied by *A*. Many STV rules designed for hand counting prescribe this method of manipulating surplus fractions.
|
||||
|
||||
Surplus fractions are often known as ‘transfer values’; however, the term ‘value’ is reserved in OpenTally for referring to the values of votes.
|
||||
In Australia, surplus fractions are often known as ‘transfer values’; however, the term ‘value’ is reserved in OpenTally for referring to the values of votes.
|
||||
|
||||
When *Surplus method* is set to *Meek method*:
|
||||
|
||||
* --round-values instead controls the rounding of candidate keep values
|
||||
* --round-surplus-fractions instead controls the rounding of each intermediate product when computing candidates' votes
|
||||
* --round-weights instead controls the rounding of candidate keep values
|
||||
* --round-tvs instead controls the rounding of each intermediate product when computing candidates' votes
|
||||
* --round-votes controls the rounding of the final number of votes credited to each candidate
|
||||
* Keep values, intermediate products and candidate votes are rounded *up*
|
||||
|
||||
### (Gregory) Round subtransfers (--round-subtransfers)
|
||||
### Sum surplus transfers (--sum-surplus-transfers)
|
||||
|
||||
When *Surplus method* is set to a Gregory method, this option allows you to specify how the numbers of votes credited to candidates in a surplus transfer/exclusion is calculated. In each case, votes are grouped according to the next available preference for a continuing candidate. Subsequently:
|
||||
This option allows you to specify how the numbers of votes credited to candidates in a surplus transfer is calculated. In each case, votes are grouped according to the next available preference for a continuing candidate. Subsequently:
|
||||
|
||||
* *Single step* (default): The total value of all votes expressing a next available preference for that candidate is multiplied by the surplus fraction. The product (rounded if requested) is credited to that candidate.
|
||||
* *By value*: The votes expressing a next available preference for that candidate are further divided according to value. For each group of votes at a particular value, the total value of all such votes is multiplied by the surplus fraction. The product (rounded if requested) is credited to that candidate.
|
||||
* *By value and source*: The votes are further divided according to value, and according to who they were received from by the elected/excluded candidate. Then as per *By value*.
|
||||
* *By parcel*: For each parcel of votes, the total value of the votes in the parcel expressing a next available preference for that candidate is multiplied by the surplus fraction. The product (rounded if requested) is credited to that candidate.
|
||||
* *Per ballot*: For each individual vote expressing a next available preference for that candidate, the value of the vote is multiplied by the surplus fraction. The product (rounded if requested) is credited to that candidate.
|
||||
* *Single step*: The total value of all votes expressing a next available preference for that candidate is multiplied by the surplus fraction. The product is credited to that candidate.
|
||||
* *By value*: The votes expressing a next available preference for that candidate are further divided according to value. For each group of votes at a particular value, the total value of all such votes is multiplied by the surplus fraction. The product is credited to that candidate.
|
||||
* *Per ballot*: For each individual vote expressing a next available preference for that candidate, the value of the vote is multiplied by the surplus fraction. The product is credited to that candidate.
|
||||
|
||||
This option affects the result only as far as rounding (due to use of fixed-precision/floating-point arithmetic, or an explicit rounding option) is concerned.
|
||||
This option affects the result only insofar as rounding (due to use of fixed-precision arithmetic, or due to an explicit rounding option) is concerned.
|
||||
|
||||
### (Meek) Surplus tolerance (--meek-surplus-tolerance)
|
||||
|
||||
When *Surplus method* is set to *Meek method*, this option allows you to specify when the distribution of surpluses will be considered complete. The tolerance may be specified either as a percentage (ends with a `%`) or absolute number of votes (no `%`):
|
||||
|
||||
* Percentage: Surplus distributions will be considered complete when every elected candidate's surplus exceeds the quota by no more than the specified percentage. This is the method specified in the 1987 Meek rules.
|
||||
* Absolute number of votes: Surplus distributions will be considered complete when the total surpluses of all elected candidates, when summed together, is no greater than the specified number of votes. This is the simpler method specified in the 2006 Meek rules.
|
||||
* Absolute number of votes: Surplus distributions will be considered complete when the total surpluses of all elected candidates is no greater than the specified number of votes. This is the simpler method specified in the 2006 Meek rules.
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
# Quick start guide
|
||||
|
||||
Prepare a [BLT file](https://yingtongli.me/git/OpenTally/about/docs/blt-fmt.md) containing the ballot papers in the election. If you would just like to see a demonstration election, you can download a sample BLT file <a href="https://yingtongli.me/git/OpenTally/plain/tests/data/prsa1.blt" target="_blank">here</a>.
|
||||
|
||||
Launch OpenTally at <a href="/opentally/stv/" target="_blank">https://yingtongli.me/opentally/stv/</a>.
|
||||
|
||||
In the top-right corner of the page, click *Browse* and select the BLT file you prepared or downloaded:
|
||||
|
||||
![Browse button](/opentally/assets/docs/005.png){: style="max-height:45px;display:block;margin:0 auto" }
|
||||
|
||||
Click the *Count* button to generate the result sheet for the election:
|
||||
|
||||
![Result sheet](/opentally/assets/docs/010.png){: style="max-height:500px;display:block;margin:0 auto" }
|
||||
|
||||
<!-- For more details on how to interpret the result sheet, see TODO. -->
|
||||
|
||||
To change the STV rules used to count the election, click the *Preset* dropdown at the top of the page, and choose a preset. Alternatively, click *Show advanced options*. A detailed explanation of the various presets and options can be found [here](/opentally/docs/options.html).
|
||||
|
||||
Once the count is complete, you can click *Print result* to generate a printable result report or PDF. To ensure the result report displays correctly, check that the paper size in the print window matches the paper size selected in OpenTally, the print orientation is set to landscape, and the scale is set to 100%.
|
|
@ -4,7 +4,7 @@ The deterministic random number generator used in OpenTally is based on an algor
|
|||
|
||||
The algorithm takes a *seed* value, which is an arbitrary character string. The algorithm has, in its internal state, a *counter*, whose value is initially 0.
|
||||
|
||||
In order to generate a value between 0 (inclusive) and *n* (exclusive), to the seed is appended a comma (",") followed by the value of the counter, and a SHA-256 hash *H* is computed of the resulting string encoded using UTF-8. The hash *H* is represented as an unsigned hexadecimal integer, *k*. The counter is incremented by 1.
|
||||
In order to generate a value between 0 (inclusive) and *n* (exclusive), to the state is appended a comma (",") followed by the value of the counter, and a SHA-256 hash *H* is computed of the resulting string encoded using UTF-8. The hash *H* is represented as an unsigned hexadecimal integer, *k*. The counter is incremented by 1.
|
||||
|
||||
In order to avoid modulo bias, if *k* ≥ ⌊*M*/*n*⌋ × *n* (where *M* = 2^256), *k* is discarded and the algorithm is repeated.
|
||||
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
# Validation
|
||||
|
||||
STV-counting software is frequently validated empirically by comparing the results of election counts to those generated by independent implementations. See, for example, [[1–5]](#references). The table describes the empirical validation performed on OpenTally to date.
|
||||
|
||||
| Method | Election | Comparator | Included test case |
|
||||
|-|-|-|-|
|
||||
| Scottish STV | [2007 Glasgow council Linn ward election](https://web.archive.org/web/20121004213938/http://www.glasgow.gov.uk/en/YourCouncil/Elections_Voting/Election_Results/ElectionScotland2007/LGWardResults.htm?ward=1&wardname=1%20-%20Linn) | eSTV 2.0.16 (official) | ✓ |
|
||||
| OpenTally Meek | [Reverse engineered ballots for the ERS97 model election](https://yingtongli.me/blog/2021/01/04/ers97.html) | [Algorithm 123](https://www.dia.govt.nz/diawebsite.NSF/Files/meekm/%24file/meekm.pdf) | ✓ |
|
||||
| Meek STV (2006) | Reverse engineered ballots for the ERS97 model election | [OpenSTV 1.7](https://github.com/Conservatory/openstv) | ✓ |
|
||||
| Meek STV (New Zealand) | Reverse engineered ballots for the ERS97 model election | OpenSTV 1.7, [Hill's nzmeek 6.7.7](https://yingtongli.me/blog/2021/07/08/nzmeek.html) | ✓ |
|
||||
| Australian Senate STV | [2019 Tasmanian Senate election](https://results.aec.gov.au/24310/Website/SenateDownloadsMenu-24310-Csv.htm) | EasyCount (official) | ✓ |
|
||||
| Australian Senate STV | [2019 NSW Senate election](https://results.aec.gov.au/24310/Website/SenateDownloadsMenu-24310-Csv.htm) | EasyCount (official) | |
|
||||
| Australian Capital Territory STV | [2020 Kurrajong Legislative Assembly election](https://www.elections.act.gov.au/elections_and_voting/2020_legislative_assembly_election/ballot-paper-preference-data-2020-election) | [eVACS 2020](https://www.elections.act.gov.au/elections_and_voting/electronic_voting_and_counting) (official) | ✓ |
|
||||
| NSW Local Government STV | [2021 City of Albury Council election](https://pastvtr.elections.nsw.gov.au/LG2101/albury/councillor) | PRCC Vote Count (official) | ✓ |
|
||||
| Victorian Legislative Council STV | [2022 Northern Metropolitan Region Legislative Council election](https://www.vec.vic.gov.au/results/state-election-results/2022-state-election-results/results-by-region/northern-metropolitan-region-results) | Results sheet (official) | ✓ |
|
||||
| Minneapolis STV | [2009 Minneapolis Board of Estimate & Taxation election](https://vote.minneapolismn.gov/results-data/election-results/2009/bet/) | Results sheet (official) | ✓ |
|
||||
| Minneapolis STV | [2013 Minneapolis Parks & Recreation Commissioner At Large election](https://vote.minneapolismn.gov/results-data/election-results/2013/park-board-at-large/) | Results sheet (official) | ✓ |
|
||||
| Minneapolis STV | [2021 Minneapolis Board of Estimate & Taxation election](https://vote.minneapolismn.gov/results-data/election-results/2021/bet/) | Results sheet (official) | ✓ |
|
||||
| Minneapolis STV | [2021 Minneapolis Parks & Recreation Commissioner At Large election](https://vote.minneapolismn.gov/results-data/election-results/2021/park-board-at-large/) | Results sheet (official) | ✓ |
|
||||
| Cambridge STV | [2003 Cambridge City Council election](https://web.archive.org/web/20070204083508/http://stv.sourceforge.net/) | OpenSTV 1.7, [ChoicePlus Pro 2.1](https://www.votingsolutions.com/cpdetail.htm) (official) | ✓ |
|
||||
| Dáil Éireann STV | [2002 Dublin North election](https://electionsireland.org/counts.cfm?election=2002&cons=96) | Results sheet (official) | ✓ |
|
||||
| van der Craats (‘Wright’) STV | [EVE Online CSM 15 election](https://www.eveonline.com/news/view/meet-the-new-council) | [ccp-wright-stv](https://github.com/ccpgames/ccp-wright-stv) (official) | ✓ |
|
||||
| PRSA 1977 | [*Proportional Representation Manual*](https://www.prsa.org.au/publicat.htm#p2) [example 1](https://www.prsa.org.au/utopiatc.pdf) | [Model result](https://www.prsa.org.au/example1.pdf) (official) | ✓ |
|
||||
| PRSA 1977 | 40 elections from [stvdb](https://gitlab.com/RunasSudo/stvdb) | [count.nl (RunasSudo version)](https://gitlab.com/RunasSudo/prsa_count) | ✓ |
|
||||
| ERS97 | [Reverse engineered ballots for the ERS97 model election](https://yingtongli.me/blog/2021/01/04/ers97.html) | [Model result](https://www.electoral-reform.org.uk/latest-news-and-research/publications/how-to-conduct-an-election-by-the-single-transferable-vote-3rd-edition/#sub-section-24) (official) | ✓ |
|
||||
| ERS97 | [Joe Otten/eSTV ballots for the ERS97 model election](https://web.archive.org/web/20020606014623/http://estv.otten.co.uk/) | [Model result](https://www.electoral-reform.org.uk/latest-news-and-research/publications/how-to-conduct-an-election-by-the-single-transferable-vote-3rd-edition/#sub-section-24) (official) | ✓ |
|
||||
| ERS76 | Ballots adapted from Joe Otten/eSTV ERS97 | Model result (official) | ✓ |
|
||||
| Church of England | Joe Otten/eSTV ballots for the ERS97 model election | [eSTV 1.47](https://web.archive.org/web/20040607021930/http://www.electoral-reform.org.uk/votingsystems/estv.htm) | ✓ |
|
||||
|
||||
# References
|
||||
|
||||
1. Wichmann BA. Checking two STV programs. *Voting Matters*. 2000 Apr; (11): 6–8. <http://www.votingmatters.org.uk/ISSUE11/P4.HTM>
|
||||
2. Wichmann BA. Validation of implementation of the Meek algorithm for STV. London: McDougall Trust; 2000 Apr 28. <http://www.votingmatters.org.uk/RES/MKVAL.pdf>
|
||||
3. Koopman P, Hubbers E, Pieters W, Poll E, de Vries R. Testing the eSTV program for the Scottish local government elections. Nijmegen (NL): Radboud University; 2007 Mar 30. <https://research.utwente.nl/en/publications/testing-the-estv-program-for-the-scottish-local-government-electi>
|
||||
4. Conway A, Blom M, Naish L, Teague V. An analysis of New South Wales electronic vote counting. *ACSW '17: Proceedings of the Australasian Computer Science Week multiconference*. New York: Association for Computing Machinery; 2017 Jan. [doi: 10.1145/3014812.3014837](http://doi.org/10.1145/3014812.3014837)
|
||||
5. Abate P, Dawson J, Goré R, Gray M, Norrish M, Slater A. *Formal methods applied to electronic voting systems*. Canberra: Australian National University; c2003. <https://users.cecs.anu.edu.au/~rpg/EVoting/>
|
|
@ -1,25 +0,0 @@
|
|||
---
|
||||
permalink: /404.html
|
||||
layout: default
|
||||
---
|
||||
|
||||
<style type="text/css" media="screen">
|
||||
.container {
|
||||
margin: 10px auto;
|
||||
max-width: 600px;
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
margin: 30px 0;
|
||||
font-size: 4em;
|
||||
line-height: 1;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container">
|
||||
<h1>404</h1>
|
||||
|
||||
<p><strong>Page not found :(</strong></p>
|
||||
<p>The requested page could not be found.</p>
|
||||
</div>
|
|
@ -1,15 +0,0 @@
|
|||
source "https://rubygems.org"
|
||||
gem "jekyll", "~> 4.2.0"
|
||||
|
||||
# Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem
|
||||
# and associated library.
|
||||
platforms :mingw, :x64_mingw, :mswin, :jruby do
|
||||
gem "tzinfo", "~> 1.2"
|
||||
gem "tzinfo-data"
|
||||
end
|
||||
|
||||
# Performance-booster for watching directories on Windows
|
||||
gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin]
|
||||
|
||||
# For Ruby 3.0
|
||||
gem "webrick", "~> 1.7"
|
|
@ -1,72 +0,0 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
colorator (1.1.0)
|
||||
concurrent-ruby (1.1.9)
|
||||
em-websocket (0.5.2)
|
||||
eventmachine (>= 0.12.9)
|
||||
http_parser.rb (~> 0.6.0)
|
||||
eventmachine (1.2.7)
|
||||
ffi (1.15.4)
|
||||
forwardable-extended (2.6.0)
|
||||
http_parser.rb (0.6.0)
|
||||
i18n (1.8.10)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jekyll (4.2.1)
|
||||
addressable (~> 2.4)
|
||||
colorator (~> 1.0)
|
||||
em-websocket (~> 0.5)
|
||||
i18n (~> 1.0)
|
||||
jekyll-sass-converter (~> 2.0)
|
||||
jekyll-watch (~> 2.0)
|
||||
kramdown (~> 2.3)
|
||||
kramdown-parser-gfm (~> 1.0)
|
||||
liquid (~> 4.0)
|
||||
mercenary (~> 0.4.0)
|
||||
pathutil (~> 0.9)
|
||||
rouge (~> 3.0)
|
||||
safe_yaml (~> 1.0)
|
||||
terminal-table (~> 2.0)
|
||||
jekyll-sass-converter (2.1.0)
|
||||
sassc (> 2.0.1, < 3.0)
|
||||
jekyll-watch (2.2.1)
|
||||
listen (~> 3.0)
|
||||
kramdown (2.3.1)
|
||||
rexml
|
||||
kramdown-parser-gfm (1.1.0)
|
||||
kramdown (~> 2.0)
|
||||
liquid (4.0.3)
|
||||
listen (3.7.0)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
mercenary (0.4.0)
|
||||
pathutil (0.16.2)
|
||||
forwardable-extended (~> 2.6)
|
||||
public_suffix (4.0.6)
|
||||
rb-fsevent (0.11.0)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
rexml (3.2.5)
|
||||
rouge (3.26.1)
|
||||
safe_yaml (1.0.5)
|
||||
sassc (2.4.0)
|
||||
ffi (~> 1.9)
|
||||
terminal-table (2.0.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
unicode-display_width (1.8.0)
|
||||
webrick (1.7.0)
|
||||
|
||||
PLATFORMS
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
jekyll (~> 4.2.0)
|
||||
tzinfo (~> 1.2)
|
||||
tzinfo-data
|
||||
wdm (~> 0.1.1)
|
||||
webrick (~> 1.7)
|
||||
|
||||
BUNDLED WITH
|
||||
2.2.26
|
|
@ -1,31 +0,0 @@
|
|||
# Site settings
|
||||
|
||||
title: OpenTally
|
||||
baseurl: "/opentally"
|
||||
url: "https://yingtongli.me"
|
||||
|
||||
git_url: "https://yingtongli.me/git/OpenTally"
|
||||
|
||||
# Build settings
|
||||
|
||||
plugins: []
|
||||
kramdown:
|
||||
smart_quotes: ["apos", "apos", "quot", "quot"]
|
||||
typographic_symbols: {"mdash": "---", "ndash": "--"}
|
||||
|
||||
# Exclude from processing.
|
||||
|
||||
# exclude:
|
||||
# - .sass-cache/
|
||||
# - .jekyll-cache/
|
||||
# - gemfiles/
|
||||
# - Gemfile
|
||||
# - Gemfile.lock
|
||||
# - node_modules/
|
||||
# - vendor/bundle/
|
||||
# - vendor/cache/
|
||||
# - vendor/gems/
|
||||
# - vendor/ruby/
|
||||
|
||||
keep_files:
|
||||
- stv
|
|
@ -1,50 +0,0 @@
|
|||
---
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
<title>{% if page.title %}{{ page.title }}{% else %}{{ site.title }}{% endif %}</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css" integrity="sha256-PDJQdTN7dolQWDASIoBVrjkuOEaI137FI15sqI3Oxu8=" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
||||
</head>
|
||||
<body class="d-flex flex-column h-100">
|
||||
<main class="flex-shrink-0">
|
||||
<!-- Navigation-->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container px-5">
|
||||
<a class="navbar-brand" href="{{ site.baseurl }}/">{{ site.title }}</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item"><a class="nav-link" href="{{ site.baseurl }}/">Home</a></li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" id="navbarDropdownBlog" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">Documentation</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdownBlog">
|
||||
<li><a class="dropdown-item" href="{{ site.baseurl }}/docs/about.html">About OpenTally</a></li>
|
||||
<li><a class="dropdown-item" href="{{ site.baseurl }}/docs/quick-start.html">Quick start guide</a></li>
|
||||
<li><a class="dropdown-item" href="{{ site.baseurl }}/docs/options.html">Advanced options</a></li>
|
||||
<li><a class="dropdown-item" href="{{ site.baseurl }}/docs/glossary.html">Glossary</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ site.git_url }}/tree/">Source Code</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{{ content }}
|
||||
</main>
|
||||
<!-- Footer-->
|
||||
<footer class="bg-dark py-4 mt-auto">
|
||||
<div class="container px-5">
|
||||
<div class="row align-items-center justify-content-between flex-column flex-sm-row">
|
||||
<div class="col-auto"><div class="small m-0 text-white">Copyright © <a href="{{ site.url }}" style="color:inherit;">Lee Yingtong Li</a> (RunasSudo) 2021–2022</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- Bootstrap core JS-->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js" integrity="sha256-XDbijJp72GS2c+Ij234ZNJIyJ1Nv+9+HH1i28JuayMk=" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,38 +0,0 @@
|
|||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
<section class="py-5">
|
||||
<div class="container px-5">
|
||||
<div class="row">
|
||||
<div class="col-lg-10 order-lg-2">
|
||||
<h1 class="mb-4">{{ page.title }}</h1>
|
||||
|
||||
<div class="post-content">
|
||||
{{ content }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-2 order-lg-1">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item"><a class="nav-link px-0" href="{{ site.baseurl }}/docs/about.html">About OpenTally</a></li>
|
||||
<li class="nav-item"><a class="nav-link px-0" href="{{ site.baseurl }}/docs/quick-start.html">Quick start guide</a></li>
|
||||
<li class="nav-item"><a class="nav-link px-0" href="{{ site.baseurl }}/docs/options.html">Advanced options</a></li>
|
||||
<li class="nav-item"><a class="nav-link px-0" href="{{ site.baseurl }}/docs/glossary.html">Glossary</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style type="text/css">
|
||||
.md-content h1 {
|
||||
display: none;
|
||||
}
|
||||
.post-content h2 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.post-content h3 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
|
@ -1,26 +0,0 @@
|
|||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
<section class="py-5">
|
||||
<div class="container px-5">
|
||||
<h1 class="mb-4">{{ page.title }}</h1>
|
||||
|
||||
<div class="post-content">
|
||||
{{ content }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style type="text/css">
|
||||
.md-content h1 {
|
||||
display: none;
|
||||
}
|
||||
.post-content h2 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.post-content h3 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
|
@ -1,168 +0,0 @@
|
|||
module Jekyll
|
||||
module Tags
|
||||
class IncludeAbsoluteTagError < StandardError
|
||||
attr_accessor :path
|
||||
|
||||
def initialize(msg, path)
|
||||
super(msg)
|
||||
@path = path
|
||||
end
|
||||
end
|
||||
|
||||
class IncludeAbsoluteTag < Liquid::Tag
|
||||
VALID_SYNTAX = %r!
|
||||
([\w-]+)\s*=\s*
|
||||
(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|([\w\.-]+))
|
||||
!x
|
||||
VARIABLE_SYNTAX = %r!
|
||||
(?<variable>[^{]*(\{\{\s*[\w\-\.]+\s*(\|.*)?\}\}[^\s{}]*)+)
|
||||
(?<params>.*)
|
||||
!mx
|
||||
|
||||
FULL_VALID_SYNTAX = %r!\A\s*(?:#{VALID_SYNTAX}(?=\s|\z)\s*)*\z!
|
||||
VALID_FILENAME_CHARS = %r!^[\w/\.-]+$!
|
||||
|
||||
def initialize(tag_name, markup, tokens)
|
||||
super
|
||||
matched = markup.strip.match(VARIABLE_SYNTAX)
|
||||
if matched
|
||||
@file = matched["variable"].strip
|
||||
@params = matched["params"].strip
|
||||
else
|
||||
@file, @params = markup.strip.split(%r!\s+!, 2)
|
||||
end
|
||||
validate_params if @params
|
||||
@tag_name = tag_name
|
||||
end
|
||||
|
||||
def syntax_example
|
||||
"{% #{@tag_name} 'file.ext' param='value' param2='value' %}"
|
||||
end
|
||||
|
||||
def parse_params(context)
|
||||
params = {}
|
||||
markup = @params
|
||||
|
||||
while (match = VALID_SYNTAX.match(markup))
|
||||
markup = markup[match.end(0)..-1]
|
||||
|
||||
value = if match[2]
|
||||
match[2].gsub(%r!\\"!, '"')
|
||||
elsif match[3]
|
||||
match[3].gsub(%r!\\'!, "'")
|
||||
elsif match[4]
|
||||
context[match[4]]
|
||||
end
|
||||
|
||||
params[match[1]] = value
|
||||
end
|
||||
params
|
||||
end
|
||||
|
||||
def validate_file_name(file)
|
||||
if file !~ VALID_FILENAME_CHARS
|
||||
raise ArgumentError, <<-MSG
|
||||
Invalid syntax for include tag. File contains invalid characters or sequences:
|
||||
|
||||
#{file}
|
||||
|
||||
Valid syntax:
|
||||
|
||||
#{syntax_example}
|
||||
|
||||
MSG
|
||||
end
|
||||
end
|
||||
|
||||
def validate_params
|
||||
unless @params =~ FULL_VALID_SYNTAX
|
||||
raise ArgumentError, <<-MSG
|
||||
Invalid syntax for include tag:
|
||||
|
||||
#{@params}
|
||||
|
||||
Valid syntax:
|
||||
|
||||
#{syntax_example}
|
||||
|
||||
MSG
|
||||
end
|
||||
end
|
||||
|
||||
# Grab file read opts in the context
|
||||
def file_read_opts(context)
|
||||
context.registers[:site].file_read_opts
|
||||
end
|
||||
|
||||
# Render the variable if required
|
||||
def render_variable(context)
|
||||
if @file =~ VARIABLE_SYNTAX
|
||||
partial = context.registers[:site]
|
||||
.liquid_renderer
|
||||
.file("(variable)")
|
||||
.parse(@file)
|
||||
partial.render!(context)
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
site = context.registers[:site]
|
||||
|
||||
file = render_variable(context) || @file
|
||||
# strip leading and trailing quote's
|
||||
file = file.gsub!(/\A'|'\Z/, '')
|
||||
validate_file_name(file)
|
||||
|
||||
source = File.expand_path(context.registers[:site].config['source']).freeze
|
||||
path = File.join(source, file)
|
||||
return unless path
|
||||
|
||||
partial = Liquid::Template.parse(read_file(path, context))
|
||||
|
||||
context.stack do
|
||||
context["include"] = parse_params(context) if @params
|
||||
begin
|
||||
partial.render!(context)
|
||||
rescue Liquid::Error => e
|
||||
e.template_name = path
|
||||
e.markup_context = "included " if e.markup_context.nil?
|
||||
raise e
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def valid_include_file?(path, dir, safe)
|
||||
!outside_site_source?(path, dir, safe) && File.file?(path)
|
||||
end
|
||||
|
||||
def outside_site_source?(path, dir, safe)
|
||||
safe && !realpath_prefixed_with?(path, dir)
|
||||
end
|
||||
|
||||
def realpath_prefixed_with?(path, dir)
|
||||
File.exist?(path) && File.realpath(path).start_with?(dir)
|
||||
rescue StandardError
|
||||
false
|
||||
end
|
||||
|
||||
# This method allows to modify the file content by inheriting from the class.
|
||||
def read_file(file, context)
|
||||
File.read(file, **file_read_opts(context))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def could_not_locate_message(file, includes_dirs, safe)
|
||||
message = "Could not locate the included file '#{file}' in any of "\
|
||||
"#{includes_dirs}. Ensure it exists in one of those directories and"
|
||||
message + if safe
|
||||
" is not a symlink as those are not allowed in safe mode."
|
||||
else
|
||||
", if it is a symlink, does not point outside your site source."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Liquid::Template.register_tag("include_absolute", Jekyll::Tags::IncludeAbsoluteTag)
|
Binary file not shown.
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
Before Width: | Height: | Size: 219 KiB |
Binary file not shown.
Before Width: | Height: | Size: 92 KiB |
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
layout: docs
|
||||
title: "About OpenTally"
|
||||
---
|
||||
|
||||
<div class="md-content" markdown="1">{% include_absolute '../README.md' %}</div>
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
layout: docs
|
||||
title: "Glossary"
|
||||
---
|
||||
|
||||
<div class="md-content" markdown="1">{% include_absolute '../docs/glossary.md' %}</div>
|
|
@ -1,10 +0,0 @@
|
|||
---
|
||||
layout: docs
|
||||
title: "Advanced options"
|
||||
---
|
||||
|
||||
<div class="md-content" markdown="1">{% include_absolute '../docs/options.md' %}</div>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('.md-content table').forEach(el => el.classList.add('table'));
|
||||
</script>
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
layout: docs
|
||||
title: "Quick start guide"
|
||||
---
|
||||
|
||||
<div class="md-content" markdown="1">{% include_absolute '../docs/quick-start.md' %}</div>
|
|
@ -1,74 +0,0 @@
|
|||
---
|
||||
layout: default
|
||||
title: "OpenTally: Advanced online election counting"
|
||||
---
|
||||
|
||||
<!-- Header-->
|
||||
<header class="bg-dark py-5">
|
||||
<div class="container px-5">
|
||||
<div class="row gx-5 align-items-center justify-content-center">
|
||||
<div class="col-lg-8 col-xl-7 col-xxl-6">
|
||||
<div class="my-5 text-center text-xl-start">
|
||||
<h1 class="display-5 fw-bolder text-white mb-2">Advanced online election counting</h1>
|
||||
<p class="lead fw-normal text-white-50 mb-4">Count instant runoff and single transferable vote elections for free, no downloads or sign-up required</p>
|
||||
<div class="d-grid gap-3 d-sm-flex justify-content-sm-center justify-content-xl-start">
|
||||
<a class="btn btn-primary btn-lg px-4 me-sm-3" href="{{ site.baseurl }}/stv/">Launch OpenTally</a>
|
||||
<a class="btn btn-outline-light btn-lg px-4" href="{{ site.baseurl }}/docs/quick-start.html">Quick Start Guide</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-8 col-xl-5 col-xxl-6 d-block text-center"><img class="img-fluid rounded-3 my-5" src="{{ site.baseurl }}/assets/headerimg.png" alt="Screenshot of OpenTally" /></div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Features section-->
|
||||
<section class="py-5 bg-light" id="features">
|
||||
<div class="container px-5 my-5">
|
||||
<div class="row gx-5">
|
||||
<div class="col-lg-4 mb-5 mb-lg-0"><h2 class="fw-bolder mb-0">Key features</h2></div>
|
||||
<div class="col-lg-8">
|
||||
<div class="row gx-5 row-cols-1 row-cols-md-2">
|
||||
<div class="col mb-5 h-100">
|
||||
<h2 class="h5">Runs in your browser</h2>
|
||||
<p class="mb-0">No downloads or sign-ups are required. OpenTally counts are computed entirely inside your browser, and no data ever leaves your computer.</p>
|
||||
</div>
|
||||
<div class="col mb-5 h-100">
|
||||
<h2 class="h5">Wide range of STV systems</h2>
|
||||
<p class="mb-0">OpenTally supports Gregory (inclusive and exclusive, weighted and unweighted), Meek and Wright variants of the single transferable vote.</p>
|
||||
</div>
|
||||
<div class="col mb-5 mb-md-0 h-100">
|
||||
<h2 class="h5">Support for arbitrary constraints</h2>
|
||||
<p class="mb-0">OpenTally is the only publicly available election counting software to support arbitrary combinations of constraints, such as gender quotas and other affirmative action requirements.</p>
|
||||
</div>
|
||||
<div class="col h-100">
|
||||
<h2 class="h5">Free and open source</h2>
|
||||
<p class="mb-0">Source code for OpenTally is publicly available under the <a href="{{ site.git_url }}/tree/COPYING">GNU AGPLv3</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Blog preview section-->
|
||||
<section class="py-5">
|
||||
<div class="container px-5 my-5">
|
||||
<div class="row gx-5 justify-content-center">
|
||||
<div class="col-lg-8 col-xl-6">
|
||||
<div class="text-center">
|
||||
<h2 class="fw-bolder">From our blog</h2>
|
||||
<p class="lead fw-normal text-muted mb-5">Latest news and posts from the OpenTally blog</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row gx-5">
|
||||
{% include_relative _news.html %}
|
||||
</div>
|
||||
<div class="row gx-5 justify-content-center">
|
||||
<div class="col-8 text-center">
|
||||
<a class="btn btn-outline-primary btn-lg px-4" href="{{ site.url }}/blog/tag/opentally/">Read More</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
143
html/index.html
143
html/index.html
|
@ -1,6 +1,6 @@
|
|||
<!--
|
||||
* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021–2023 Lee Yingtong Li (RunasSudo)
|
||||
* 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
|
||||
|
@ -35,38 +35,21 @@
|
|||
<label>
|
||||
Preset:
|
||||
<select id="selPreset" onchange="changePreset()">
|
||||
<optgroup label="Recommended">
|
||||
<option value="wigm" selected>OpenTally WIGM</option>
|
||||
<option value="scottish">Scottish STV</option>
|
||||
<option value="meek87">OpenTally Meek</option>
|
||||
</optgroup>
|
||||
<optgroup label="Legislative">
|
||||
<option value="senate">Australian Senate STV</option>
|
||||
<option value="act">Australian Capital Territory STV</option>
|
||||
<option value="nswlg">NSW Local Government STV</option>
|
||||
<option value="viclc">Victorian Legislative Council STV</option>
|
||||
<option value="wa">Western Australia STV</option>
|
||||
<option value="meeknz">Meek STV (New Zealand)</option>
|
||||
<option value="minneapolis">Minneapolis STV</option>
|
||||
<option value="cambridge">Cambridge STV</option>
|
||||
<option value="dail">Dáil Éireann STV</option>
|
||||
</optgroup>
|
||||
<optgroup label="Hand-count">
|
||||
<option value="prsa77">PRSA 1977</option>
|
||||
<option value="ers97">ERS97</option>
|
||||
<option value="ers76">ERS76</option>
|
||||
<option value="ers73">ERS73</option>
|
||||
<option value="cofe">Church of England</option>
|
||||
</optgroup>
|
||||
<optgroup label="Computer-count">
|
||||
<option value="meek06">Meek STV (2006)</option>
|
||||
<option value="vdc">van der Craats (‘Wright’) STV</option>
|
||||
</optgroup>
|
||||
<option value="wigm" selected>Recommended WIGM</option>
|
||||
<option value="scottish">Scottish STV</option>
|
||||
<option value="meek87">Meek STV (1987)</option>
|
||||
<option value="meek06">Meek STV (2006)</option>
|
||||
<option value="meeknz">Meek STV (New Zealand)</option>
|
||||
<option value="senate">Australian Senate STV</option>
|
||||
<option value="wright">Wright STV</option>
|
||||
<option value="prsa77">PRSA 1977</option>
|
||||
<option value="ers97">ERS97</option>
|
||||
</select>
|
||||
</label>
|
||||
<button id="btnAdvancedOptions" onclick="clickAdvancedOptions()">Show advanced options</button>
|
||||
OpenTally (revision <span id="spanRevNum"></span>)
|
||||
· <a href="https://yingtongli.me/opentally/">Information and instructions</a>
|
||||
<!--· <a href="https://yingtongli.me/blog/2020/12/24/pyrcv2.html">Information and instructions</a> ·
|
||||
<a href="blt/">Ballot input/editor</a>-->
|
||||
</div>
|
||||
|
||||
<div id="divAdvancedOptions" class="menudiv cols-12 cols-sm-6" style="display: none;">
|
||||
|
@ -84,8 +67,8 @@
|
|||
</label>
|
||||
<label>
|
||||
<select id="selQuota">
|
||||
<option value="droop" selected>Droop</option>
|
||||
<option value="droop_exact">Droop (exact)</option>
|
||||
<option value="droop">Droop</option>
|
||||
<option value="droop_exact" selected>Droop (exact)</option>
|
||||
<option value="hare">Hare</option>
|
||||
<option value="hare_exact">Hare (exact)</option>
|
||||
</select>
|
||||
|
@ -95,9 +78,6 @@
|
|||
<option value="static" selected>Static quota</option>
|
||||
<!--<option value="progressive">Progressive quota</option>-->
|
||||
<option value="ers97">Static with ERS97 rules</option>
|
||||
<option value="ers76">Static with ERS76 rules</option>
|
||||
<option value="dynamic_by_total">Dynamic by total vote</option>
|
||||
<option value="dynamic_by_active">Dynamic by active vote</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
@ -111,35 +91,28 @@
|
|||
</label>
|
||||
<label>
|
||||
Method:
|
||||
<select id="selMethod">
|
||||
<select id="selTransfers">
|
||||
<option value="wig" selected>Weighted inclusive Gregory</option>
|
||||
<option value="uig">Unweighted inclusive Gregory</option>
|
||||
<option value="eg">Exclusive Gregory (last bundle)</option>
|
||||
<option value="meek">Meek method</option>
|
||||
<option value="hare">Hare (exclusive sample)</option>
|
||||
<option value="ihare">Inclusive Hare (sample)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<select id="selPapers">
|
||||
<option value="both" selected>Include non-transferable ballots</option>
|
||||
<option value="assume_progress_total">Assume progress total</option>
|
||||
<option value="transferable">Use transferable ballots only</option>
|
||||
<option value="subtract_nontransferable">Subtract non-transferables</option>
|
||||
<option value="both" selected>Include non-transferable papers</option>
|
||||
<option value="transferable">Use transferable papers only</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label style="margin-right:1em;">
|
||||
<span class="pill-grey" title="This option has effect only if “Method” is set to a Gregory method">Gregory</span>
|
||||
Exclusion:
|
||||
<select id="selExclusion">
|
||||
<option value="single_stage" selected>Single stage</option>
|
||||
<option value="by_value">By value</option>
|
||||
<option value="first_prefs_then_by_value">FPV then by value</option>
|
||||
<option value="by_source">By source</option>
|
||||
<option value="parcels_by_order">By parcel (by order)</option>
|
||||
<option value="reset_and_reiterate">Reset and re-iterate</option>
|
||||
<option value="wright">Wright method (re-iterate)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
|
@ -148,22 +121,6 @@
|
|||
NZ-style exclusion
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label style="margin-right:1em;">
|
||||
<span class="pill-grey" title="This option has effect only if “Method” is set to a Hare method">Hare</span>
|
||||
Sample method:
|
||||
<select id="selSample">
|
||||
<option value="stratify" selected>Stratify</option>
|
||||
<!--<option value="stratify_floor" selected>Stratify (floor)</option>-->
|
||||
<option value="by_order">By order</option>
|
||||
<option value="cincinnati">Cincinnati</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" id="chkSamplePerBallot">
|
||||
Per-ballot transfers
|
||||
</label>
|
||||
</div>
|
||||
<div class="subheading">
|
||||
Tie-breaking:
|
||||
</div>
|
||||
|
@ -186,35 +143,8 @@
|
|||
Constraints:
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<input type="file" id="conFile">
|
||||
</label>
|
||||
<label>
|
||||
Method:
|
||||
<select id="selConstraintMethod">
|
||||
<option value="guard_doom" selected>Guard/doom</option>
|
||||
<option value="repeat_count">Repeat count</option>
|
||||
</select>
|
||||
</label>
|
||||
<input type="file" id="conFile">
|
||||
</div>
|
||||
<div class="subheading">
|
||||
Report options:
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label style="margin-right:1em;">
|
||||
Report style:
|
||||
<select id="selReport">
|
||||
<option value="votes">Votes only</option>
|
||||
<option value="votes_transposed" selected>Votes (transposed)</option>
|
||||
<option value="ballots_votes">Ballots and votes</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<label class="col-12">
|
||||
Display up to
|
||||
<input type="number" id="txtPPDP" value="2" min="0" style="width: 3em;">
|
||||
d.p.
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-6 cols-12" style="align-self: start;">
|
||||
<div class="col-12 subheading">
|
||||
|
@ -236,6 +166,15 @@
|
|||
<input type="number" id="txtDP" value="5" min="0" style="width: 3em;">
|
||||
</label>
|
||||
</div>
|
||||
<label class="col-12">
|
||||
Display up to
|
||||
<input type="number" id="txtPPDP" value="2" min="0" style="width: 3em;">
|
||||
d.p.
|
||||
</label>
|
||||
<label class="col-12">
|
||||
<input type="checkbox" id="chkNormaliseBallots">
|
||||
Normalise ballots
|
||||
</label>
|
||||
<div class="col-12 subheading">
|
||||
Count optimisations:
|
||||
</div>
|
||||
|
@ -252,13 +191,10 @@
|
|||
Defer surpluses
|
||||
</label>
|
||||
<label class="col-6">
|
||||
<input type="checkbox" id="chkImmediateElect" checked>
|
||||
<input type="checkbox" id="chkMeekImmediateElect">
|
||||
<span class="pill-grey" title="This option has effect only if “Method” is set to “Meek method”">Meek</span>
|
||||
Immediate election
|
||||
</label>
|
||||
<label class="col-12">
|
||||
Minimum threshold:
|
||||
<input type="number" id="txtMinThreshold" value="0" min="0" style="width: 3em;">
|
||||
</label>
|
||||
<div class="col-12 subheading">
|
||||
Rounding:
|
||||
</div>
|
||||
|
@ -284,32 +220,29 @@
|
|||
</div>
|
||||
<div class="col-6">
|
||||
<label>
|
||||
<input type="checkbox" id="chkRoundSFs">
|
||||
<input type="checkbox" id="chkRoundTVs">
|
||||
Surplus fractions:
|
||||
</label>
|
||||
<label>
|
||||
<input type="number" id="txtRoundSFs" value="0" min="0" style="width: 3em;">
|
||||
<input type="number" id="txtRoundTVs" value="0" min="0" style="width: 3em;">
|
||||
d.p.
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label>
|
||||
<input type="checkbox" id="chkRoundValues">
|
||||
Ballot values:
|
||||
<input type="checkbox" id="chkRoundWeights">
|
||||
Ballot weights:
|
||||
</label>
|
||||
<label>
|
||||
<input type="number" id="txtRoundValues" value="0" min="0" style="width: 3em;">
|
||||
<input type="number" id="txtRoundWeights" value="0" min="0" style="width: 3em;">
|
||||
d.p.
|
||||
</label>
|
||||
</div>
|
||||
<label class="col-12">
|
||||
<span class="pill-grey" title="This option has effect only if “Method” is a Gregory method">Gregory</span>
|
||||
Round subtransfers:
|
||||
Sum surplus transfers:
|
||||
<select id="selSumTransfers">
|
||||
<option value="single_step" selected>Single step</option>
|
||||
<option value="by_value">By value</option>
|
||||
<option value="by_value_and_source">By value and source</option>
|
||||
<option value="by_parcel">By parcel</option>
|
||||
<option value="per_ballot">Per ballot</option>
|
||||
</select>
|
||||
</label>
|
||||
|
@ -345,9 +278,7 @@
|
|||
|
||||
<div id="printWarning">Printing directly from this page is not supported. Use the ‘Print result’ button to generate a printer-friendly report.</div>
|
||||
|
||||
<script src="vendor/vanilla-js-dropdown.min.js"></script>
|
||||
<script src="opentally.js?v=GITVERSION"></script>
|
||||
<script src="index.js?v=GITVERSION"></script>
|
||||
<script src="presets.js?v=GITVERSION"></script>
|
||||
<script src="print.js?v=GITVERSION"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
493
html/index.js
493
html/index.js
|
@ -1,5 +1,5 @@
|
|||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021–2022 Lee Yingtong Li (RunasSudo)
|
||||
* 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
|
||||
|
@ -29,9 +29,7 @@ var tblResult = document.getElementById('result');
|
|||
var divLogs2 = document.getElementById('resultLogs2');
|
||||
var olStageComments;
|
||||
|
||||
var detailedTransfers = {};
|
||||
|
||||
var worker = new Worker('worker.js?v=GITVERSION');
|
||||
var worker = new Worker('worker.js');
|
||||
|
||||
worker.onmessage = function(evt) {
|
||||
if (evt.data.type === 'init') {
|
||||
|
@ -39,41 +37,26 @@ worker.onmessage = function(evt) {
|
|||
document.getElementById('divLoading').style.display = 'none';
|
||||
document.getElementById('divUI').style.display = 'block';
|
||||
|
||||
// Init dropdowns
|
||||
// Can't compute correct width until #divUI, etc. is display: block
|
||||
//document.getElementById('divAdvancedOptions').style.display = 'grid';
|
||||
//for (let elSel of document.querySelectorAll('select')) {
|
||||
let elSel = document.getElementById('selPreset'); {
|
||||
var sel = new CustomSelect({elem: elSel});
|
||||
sel.open();
|
||||
document.getElementById('custom-' + elSel.id).style.width = (document.getElementById('custom-' + elSel.id).querySelector('.js-Dropdown-list').clientWidth + 32) + 'px';
|
||||
sel.close();
|
||||
}
|
||||
//document.getElementById('divAdvancedOptions').style.display = 'none';
|
||||
} else if (evt.data.type === 'initResultsTable') {
|
||||
tblResult.innerHTML = evt.data.content;
|
||||
divLogs2.innerHTML = '<p>Stage comments:</p>';
|
||||
olStageComments = document.createElement('ol');
|
||||
olStageComments.id = 'olStageComments';
|
||||
divLogs2.append(olStageComments);
|
||||
|
||||
} else if (evt.data.type === 'describeCount') {
|
||||
document.getElementById('resultLogs1').innerHTML = evt.data.content;
|
||||
|
||||
} else if (evt.data.type === 'updateResultsTable') {
|
||||
for (let row = 0; row < evt.data.result.length; row++) {
|
||||
if (evt.data.result[row]) {
|
||||
tblResult.rows[row].insertAdjacentHTML('beforeend', evt.data.result[row]);
|
||||
for (let i = 0; i < evt.data.result.length; i++) {
|
||||
if (evt.data.result[i]) {
|
||||
tblResult.rows[i].insertAdjacentHTML('beforeend', evt.data.result[i]);
|
||||
|
||||
// Update candidate status
|
||||
if (
|
||||
(document.getElementById('selReport').value == 'votes' && row >= 3 && row % 2 == 1) ||
|
||||
(document.getElementById('selReport').value == 'ballots_votes' && row >= 4 && row % 2 == 0)
|
||||
) {
|
||||
if (tblResult.rows[row].lastElementChild.classList.contains('elected')) {
|
||||
tblResult.rows[row].cells[0].classList.add('elected');
|
||||
if (i >= 3 && i % 2 == 1) {
|
||||
if (tblResult.rows[i].lastElementChild.classList.contains('elected')) {
|
||||
tblResult.rows[i].cells[0].classList.add('elected');
|
||||
} else {
|
||||
tblResult.rows[row].cells[0].classList.remove('elected');
|
||||
tblResult.rows[i].cells[0].classList.remove('elected');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -81,34 +64,19 @@ worker.onmessage = function(evt) {
|
|||
|
||||
} else if (evt.data.type === 'updateStageComments') {
|
||||
let elLi = document.createElement('li');
|
||||
elLi.id = 'stage' + evt.data.stageNum;
|
||||
elLi.innerHTML = evt.data.comment;
|
||||
olStageComments.append(elLi);
|
||||
|
||||
} else if (evt.data.type === 'updateDetailedTransfers') {
|
||||
detailedTransfers[evt.data.stageNum] = evt.data.table;
|
||||
|
||||
} else if (evt.data.type === 'finalResultSummary') {
|
||||
divLogs2.insertAdjacentHTML('beforeend', evt.data.summary);
|
||||
document.getElementById('printPane').style.display = 'block';
|
||||
|
||||
// Linkify stage numbers
|
||||
document.querySelectorAll('tr.stage-no a').forEach(function(elA) {
|
||||
elA.onclick = function() {
|
||||
olStageComments.childNodes.forEach(function(elLi) { elLi.classList.remove('highlight'); });
|
||||
document.getElementById(elA.href.substring(elA.href.indexOf('#') + 1)).classList.add('highlight');
|
||||
};
|
||||
});
|
||||
|
||||
} else if (evt.data.type === 'requireInput') {
|
||||
let response = window.prompt(evt.data.message);
|
||||
while (response === null) {
|
||||
response = window.prompt(evt.data.message);
|
||||
}
|
||||
worker.postMessage({'type': 'userInput', 'response': response});
|
||||
|
||||
} else if (evt.data.type === 'errorMessage') {
|
||||
divLogs2.insertAdjacentHTML('beforeend', evt.data.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -141,81 +109,444 @@ async function clickCount() {
|
|||
|
||||
// Init STV options
|
||||
let optsStr = [
|
||||
document.getElementById('chkRoundSFs').checked ? parseInt(document.getElementById('txtRoundSFs').value) : null,
|
||||
document.getElementById('chkRoundValues').checked ? parseInt(document.getElementById('txtRoundValues').value) : null,
|
||||
document.getElementById('chkRoundTVs').checked ? parseInt(document.getElementById('txtRoundTVs').value) : null,
|
||||
document.getElementById('chkRoundWeights').checked ? parseInt(document.getElementById('txtRoundWeights').value) : null,
|
||||
document.getElementById('chkRoundVotes').checked ? parseInt(document.getElementById('txtRoundVotes').value) : null,
|
||||
document.getElementById('chkRoundQuota').checked ? parseInt(document.getElementById('txtRoundQuota').value) : null,
|
||||
document.getElementById('selSumTransfers').value,
|
||||
document.getElementById('txtMeekSurplusTolerance').value,
|
||||
document.getElementById('chkNormaliseBallots').checked,
|
||||
document.getElementById('selQuota').value,
|
||||
document.getElementById('selQuotaCriterion').value,
|
||||
document.getElementById('selQuotaMode').value,
|
||||
document.getElementById('selTies').value.split(','),
|
||||
document.getElementById('txtSeed').value,
|
||||
document.getElementById('selMethod').value,
|
||||
document.getElementById('selTransfers').value,
|
||||
document.getElementById('selSurplus').value,
|
||||
document.getElementById('selPapers').value,
|
||||
document.getElementById('selPapers').value == 'transferable',
|
||||
document.getElementById('selExclusion').value,
|
||||
document.getElementById('chkMeekNZExclusion').checked,
|
||||
document.getElementById('selSample').value,
|
||||
document.getElementById('chkSamplePerBallot').checked,
|
||||
document.getElementById('chkBulkElection').checked,
|
||||
document.getElementById('chkBulkExclusion').checked,
|
||||
document.getElementById('chkDeferSurpluses').checked,
|
||||
document.getElementById('chkImmediateElect').checked,
|
||||
document.getElementById('txtMinThreshold').value,
|
||||
document.getElementById('chkMeekImmediateElect').checked,
|
||||
conPath,
|
||||
document.getElementById('selConstraintMethod').value,
|
||||
"guard_doom",
|
||||
parseInt(document.getElementById('txtPPDP').value),
|
||||
];
|
||||
|
||||
// Reset UI
|
||||
document.getElementById('printPane').style.display = 'none';
|
||||
document.getElementById('resultLogs1').innerHTML = '';
|
||||
tblResult.innerHTML = '';
|
||||
divLogs2.innerHTML = '';
|
||||
|
||||
detailedTransfers = {};
|
||||
|
||||
// Dispatch to worker
|
||||
worker.postMessage({
|
||||
'type': 'countElection',
|
||||
// Data
|
||||
'bltData': bltData,
|
||||
'conData': conData,
|
||||
'optsStr': optsStr,
|
||||
'bltPath': bltPath,
|
||||
'conPath': conPath,
|
||||
// Options
|
||||
'optsStr': optsStr,
|
||||
'numbers': document.getElementById('selNumbers').value,
|
||||
'decimals': document.getElementById('txtDP').value,
|
||||
'reportStyle': document.getElementById('selReport').value,
|
||||
'normaliseBallots': document.getElementById('chkNormaliseBallots').checked,
|
||||
});
|
||||
}
|
||||
|
||||
function viewDetailedTransfers(stageNum) {
|
||||
let wtransfers = window.open('', '', 'location=0,width=800,height=600');
|
||||
wtransfers.document.title = 'OpenTally Detailed Transfers: Stage ' + stageNum;
|
||||
|
||||
// Add stylesheets
|
||||
for (let elCSSBase of document.querySelectorAll('head link')) {
|
||||
let elCSS = wtransfers.document.createElement('link');
|
||||
elCSS.rel = elCSSBase.rel;
|
||||
elCSS.type = elCSSBase.type;
|
||||
if (elCSSBase.href.endsWith('?v=GITVERSION')) {
|
||||
elCSS.href = elCSSBase.href.replace('?v=GITVERSION', '?v=' + Math.random());
|
||||
} else {
|
||||
elCSS.href = elCSSBase.href;
|
||||
}
|
||||
wtransfers.document.head.appendChild(elCSS);
|
||||
}
|
||||
|
||||
wtransfers.document.body.innerHTML = detailedTransfers[stageNum];
|
||||
}
|
||||
|
||||
// Provide a default seed
|
||||
if (document.getElementById('txtSeed').value === '') {
|
||||
function pad(x) { if (x < 10) { return '0' + x; } return '' + x; }
|
||||
let d = new Date();
|
||||
document.getElementById('txtSeed').value = d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate());
|
||||
}
|
||||
|
||||
// Print logic
|
||||
|
||||
async function printResult() {
|
||||
let printableWidth; // Printable width in CSS pixels
|
||||
let paperSize = document.getElementById('selPaperSize').value;
|
||||
if (paperSize === 'A4') {
|
||||
printableWidth = (29.7 - 2) * 96 / 2.54;
|
||||
} else if (paperSize === 'A3') {
|
||||
printableWidth = (42.0 - 2) * 96 / 2.54;
|
||||
} else if (paperSize === 'letter') {
|
||||
printableWidth = (27.9 - 2) * 96 / 2.54;
|
||||
}
|
||||
printableWidth = Math.round(printableWidth);
|
||||
|
||||
let wprint = window.open('');
|
||||
wprint.document.title = 'OpenTally Report';
|
||||
|
||||
// Add stylesheets
|
||||
let numToLoad = 0;
|
||||
let numLoaded = -1;
|
||||
|
||||
function onLoadStylesheet() {
|
||||
numLoaded++;
|
||||
if (numLoaded == numToLoad) {
|
||||
wprint.print();
|
||||
}
|
||||
}
|
||||
|
||||
for (let elCSSBase of document.querySelectorAll('head link')) {
|
||||
numToLoad++;
|
||||
let elCSS = wprint.document.createElement('link');
|
||||
elCSS.rel = elCSSBase.rel;
|
||||
elCSS.type = elCSSBase.type;
|
||||
if (elCSSBase.href.endsWith('?v=GITVERSION')) {
|
||||
elCSS.href = elCSSBase.href.replace('?v=GITVERSION', '?v=' + Math.random());
|
||||
} else {
|
||||
elCSS.href = elCSSBase.href;
|
||||
}
|
||||
|
||||
elCSS.onload = onLoadStylesheet;
|
||||
wprint.document.head.appendChild(elCSS);
|
||||
}
|
||||
|
||||
// Configure printing
|
||||
let elStyle = wprint.document.createElement('style');
|
||||
elStyle.innerHTML = '@page { size: ' + paperSize + ' landscape; margin: 1cm; } @media print { body { padding: 0; } }';
|
||||
wprint.document.head.appendChild(elStyle);
|
||||
|
||||
let elContainer = wprint.document.createElement('div');
|
||||
elContainer.id = 'printContainer';
|
||||
elContainer.style.width = printableWidth + 'px';
|
||||
wprint.document.body.appendChild(elContainer);
|
||||
|
||||
// Copy result logs 1
|
||||
let divResultLogs1 = document.getElementById('resultLogs1');
|
||||
let divResultLogs2 = wprint.document.createElement('div');
|
||||
divResultLogs2.innerHTML = divResultLogs1.innerHTML;
|
||||
elContainer.appendChild(divResultLogs2);
|
||||
|
||||
// Parse table, accounting for rowspan
|
||||
let elTrs1 = document.querySelector('#result').rows;
|
||||
let rows = [];
|
||||
for (let elTr1 of elTrs1) {
|
||||
rows.push([]);
|
||||
}
|
||||
for (let r = 0; r < elTrs1.length; r++) {
|
||||
for (let c = 0; c < elTrs1[r].cells.length; c++) {
|
||||
let elTd1 = elTrs1[r].cells[c];
|
||||
rows[r].push(elTd1);
|
||||
|
||||
let rowspan = elTd1.getAttribute('rowspan');
|
||||
// NB: Only works for rowspan in first column
|
||||
if (rowspan !== null && c == 0) {
|
||||
rowspan = parseInt(rowspan);
|
||||
// Add ghost cells
|
||||
for (let i = 1; i < rowspan; i++) {
|
||||
rows[r + i].push(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function copyColumn(c, elTrs2) {
|
||||
let tdsAdded = [];
|
||||
for (let r = 0; r < rows.length; r++) {
|
||||
if (c < rows[r].length) {
|
||||
let elTd1 = rows[r][c];
|
||||
if (elTd1 !== null) {
|
||||
let elTd2 = wprint.document.createElement('td');
|
||||
elTd2.innerHTML = elTd1.innerHTML;
|
||||
elTd2.className = elTd1.className;
|
||||
elTd2.setAttribute('rowspan', elTd1.getAttribute('rowspan'));
|
||||
elTd2.setAttribute('style', elTd1.getAttribute('style'));
|
||||
elTrs2[r].appendChild(elTd2);
|
||||
tdsAdded.push(elTd2);
|
||||
}
|
||||
}
|
||||
}
|
||||
return tdsAdded;
|
||||
}
|
||||
|
||||
async function copyTableColumns(startCol) {
|
||||
// Add table
|
||||
let elTable2 = wprint.document.createElement('table');
|
||||
elTable2.className = 'result';
|
||||
if (startCol > 1) {
|
||||
elTable2.style.pageBreakBefore = 'always';
|
||||
}
|
||||
elContainer.appendChild(elTable2);
|
||||
|
||||
// Add rows
|
||||
let elTrs2 = [];
|
||||
for (let elTr1 of elTrs1) {
|
||||
let elTr2 = wprint.document.createElement('tr');
|
||||
elTr2.className = elTr1.className;
|
||||
elTrs2.push(elTr2);
|
||||
elTable2.appendChild(elTr2);
|
||||
}
|
||||
|
||||
// Copy first column
|
||||
copyColumn(0, elTrs2);
|
||||
|
||||
// How many columns to copy?
|
||||
let totalWidth = rows[0][0].clientWidth;
|
||||
let endCol;
|
||||
for (endCol = startCol; endCol < rows[0].length; endCol++) {
|
||||
if (totalWidth + rows[0][endCol].clientWidth > printableWidth) {
|
||||
break;
|
||||
}
|
||||
totalWidth += rows[0][endCol].clientWidth;
|
||||
}
|
||||
|
||||
// Copy columns
|
||||
for (let c = startCol; c < endCol; c++) {
|
||||
copyColumn(c, elTrs2);
|
||||
}
|
||||
|
||||
// Copy stage comments
|
||||
elContainer.insertAdjacentHTML('beforeend', '<p>Stage comments:</p>');
|
||||
let olStageComments2 = wprint.document.createElement('ol');
|
||||
olStageComments2.start = startCol;
|
||||
elContainer.append(olStageComments2);
|
||||
for (let c = startCol; c < endCol && c < rows[0].length - 1; c++) {
|
||||
olStageComments2.insertAdjacentHTML('beforeend', olStageComments.children[c-1].outerHTML);
|
||||
}
|
||||
|
||||
if (endCol < rows[0].length) {
|
||||
// Start new table if columns remain
|
||||
copyTableColumns(endCol);
|
||||
} else {
|
||||
// Copy winning candidates
|
||||
elContainer.insertAdjacentHTML('beforeend', '<p>Count complete. The winning candidates are, in order of election:</p>');
|
||||
elContainer.insertAdjacentHTML('beforeend', divLogs2.lastElementChild.outerHTML);
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust results table to width
|
||||
document.getElementById('resultsDiv').style.width = printableWidth + 'px';
|
||||
await new Promise(window.requestAnimationFrame); // Allow DOM to update
|
||||
|
||||
// Copy table
|
||||
await copyTableColumns(1);
|
||||
|
||||
// Restore original view
|
||||
document.getElementById('resultsDiv').style.width = 'auto';
|
||||
|
||||
// Trigger print when ready
|
||||
onLoadStylesheet();
|
||||
}
|
||||
|
||||
// Presets
|
||||
|
||||
function changePreset() {
|
||||
if (document.getElementById('selPreset').value === 'wigm') {
|
||||
document.getElementById('selQuotaCriterion').value = 'gt';
|
||||
document.getElementById('selQuota').value = 'droop_exact';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = false;
|
||||
document.getElementById('selNumbers').value = 'rational';
|
||||
document.getElementById('txtPPDP').value = '2';
|
||||
document.getElementById('chkNormaliseBallots').checked = false;
|
||||
document.getElementById('chkRoundQuota').checked = false;
|
||||
document.getElementById('chkRoundVotes').checked = false;
|
||||
document.getElementById('chkRoundTVs').checked = false;
|
||||
document.getElementById('chkRoundWeights').checked = false;
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selTransfers').value = 'wig';
|
||||
document.getElementById('selPapers').value = 'both';
|
||||
document.getElementById('selExclusion').value = 'single_stage';
|
||||
document.getElementById('selTies').value = 'backwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'scottish') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = false;
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '5';
|
||||
document.getElementById('txtPPDP').value = '5';
|
||||
document.getElementById('chkNormaliseBallots').checked = true;
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '0';
|
||||
document.getElementById('chkRoundVotes').checked = false;
|
||||
document.getElementById('chkRoundTVs').checked = true;
|
||||
document.getElementById('txtRoundTVs').value = '5';
|
||||
document.getElementById('chkRoundWeights').checked = false;
|
||||
document.getElementById('selSumTransfers').value = 'per_ballot';
|
||||
document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selTransfers').value = 'wig';
|
||||
document.getElementById('selPapers').value = 'both';
|
||||
document.getElementById('selExclusion').value = 'single_stage';
|
||||
document.getElementById('selTies').value = 'backwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'meek87') {
|
||||
document.getElementById('selQuotaCriterion').value = 'gt';
|
||||
document.getElementById('selQuota').value = 'droop_exact';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = false;
|
||||
document.getElementById('chkMeekImmediateElect').checked = false;
|
||||
document.getElementById('chkMeekNZExclusion').checked = false;
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '5';
|
||||
document.getElementById('txtPPDP').value = '2';
|
||||
document.getElementById('chkNormaliseBallots').checked = false;
|
||||
document.getElementById('chkRoundQuota').checked = false;
|
||||
document.getElementById('chkRoundVotes').checked = false;
|
||||
document.getElementById('chkRoundTVs').checked = false;
|
||||
document.getElementById('chkRoundWeights').checked = false;
|
||||
//document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('txtMeekSurplusTolerance').value = '0.001%';
|
||||
//document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selTransfers').value = 'meek';
|
||||
document.getElementById('selPapers').value = 'both';
|
||||
document.getElementById('selExclusion').value = 'single_stage';
|
||||
document.getElementById('selTies').value = 'backwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'meek06') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = true;
|
||||
document.getElementById('chkMeekImmediateElect').checked = true;
|
||||
document.getElementById('chkMeekNZExclusion').checked = false;
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '12';
|
||||
document.getElementById('txtPPDP').value = '2';
|
||||
document.getElementById('chkNormaliseBallots').checked = false;
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '9';
|
||||
document.getElementById('chkRoundVotes').checked = true;
|
||||
document.getElementById('txtRoundVotes').value = '9';
|
||||
document.getElementById('chkRoundTVs').checked = true;
|
||||
document.getElementById('txtRoundTVs').value = '9';
|
||||
document.getElementById('chkRoundWeights').checked = true;
|
||||
document.getElementById('txtRoundWeights').value = '9';
|
||||
//document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('txtMeekSurplusTolerance').value = '0.0001';
|
||||
//document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selTransfers').value = 'meek';
|
||||
document.getElementById('selPapers').value = 'both';
|
||||
document.getElementById('selExclusion').value = 'single_stage';
|
||||
document.getElementById('selTies').value = 'backwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'meeknz') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = true;
|
||||
document.getElementById('chkMeekImmediateElect').checked = true;
|
||||
document.getElementById('chkMeekNZExclusion').checked = true;
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '12';
|
||||
document.getElementById('txtPPDP').value = '2';
|
||||
document.getElementById('chkNormaliseBallots').checked = false;
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '9';
|
||||
document.getElementById('chkRoundVotes').checked = true;
|
||||
document.getElementById('txtRoundVotes').value = '9';
|
||||
document.getElementById('chkRoundTVs').checked = true;
|
||||
document.getElementById('txtRoundTVs').value = '9';
|
||||
document.getElementById('chkRoundWeights').checked = true;
|
||||
document.getElementById('txtRoundWeights').value = '9';
|
||||
//document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('txtMeekSurplusTolerance').value = '0.0001';
|
||||
//document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selTransfers').value = 'meek';
|
||||
document.getElementById('selPapers').value = 'both';
|
||||
document.getElementById('selExclusion').value = 'single_stage';
|
||||
document.getElementById('selTies').value = 'backwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'senate') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = true;
|
||||
document.getElementById('chkDeferSurpluses').checked = false;
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '5';
|
||||
document.getElementById('txtPPDP').value = '0';
|
||||
document.getElementById('chkNormaliseBallots').checked = false;
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '0';
|
||||
document.getElementById('chkRoundVotes').checked = true;
|
||||
document.getElementById('txtRoundVotes').value = '0';
|
||||
document.getElementById('chkRoundTVs').checked = false;
|
||||
document.getElementById('chkRoundWeights').checked = false;
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_order';
|
||||
document.getElementById('selTransfers').value = 'uig';
|
||||
document.getElementById('selPapers').value = 'both';
|
||||
document.getElementById('selExclusion').value = 'by_value';
|
||||
document.getElementById('selTies').value = 'backwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'wright') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = true;
|
||||
document.getElementById('chkDeferSurpluses').checked = false;
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '5';
|
||||
document.getElementById('txtPPDP').value = '2';
|
||||
document.getElementById('chkNormaliseBallots').checked = false;
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '0';
|
||||
document.getElementById('chkRoundVotes').checked = false;
|
||||
document.getElementById('chkRoundTVs').checked = false;
|
||||
document.getElementById('chkRoundWeights').checked = false;
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selTransfers').value = 'wig';
|
||||
document.getElementById('selPapers').value = 'both';
|
||||
document.getElementById('selExclusion').value = 'wright';
|
||||
document.getElementById('selTies').value = 'random';
|
||||
} else if (document.getElementById('selPreset').value === 'prsa77') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = true;
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '5';
|
||||
document.getElementById('txtPPDP').value = '3';
|
||||
document.getElementById('chkNormaliseBallots').checked = false;
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '3';
|
||||
document.getElementById('chkRoundVotes').checked = true;
|
||||
document.getElementById('txtRoundVotes').value = '3';
|
||||
document.getElementById('chkRoundTVs').checked = true;
|
||||
document.getElementById('txtRoundTVs').value = '3';
|
||||
document.getElementById('chkRoundWeights').checked = true;
|
||||
document.getElementById('txtRoundWeights').value = '3';
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_order';
|
||||
document.getElementById('selTransfers').value = 'eg';
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
document.getElementById('selExclusion').value = 'parcels_by_order';
|
||||
document.getElementById('selTies').value = 'backwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'ers97') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop_exact';
|
||||
document.getElementById('selQuotaMode').value = 'ers97';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = true;
|
||||
document.getElementById('chkDeferSurpluses').checked = true;
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '5';
|
||||
document.getElementById('txtPPDP').value = '2';
|
||||
document.getElementById('chkNormaliseBallots').checked = false;
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '2';
|
||||
document.getElementById('chkRoundVotes').checked = true;
|
||||
document.getElementById('txtRoundVotes').value = '2';
|
||||
document.getElementById('chkRoundTVs').checked = true;
|
||||
document.getElementById('txtRoundTVs').value = '2';
|
||||
document.getElementById('chkRoundWeights').checked = true;
|
||||
document.getElementById('txtRoundWeights').value = '2';
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selTransfers').value = 'eg';
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
document.getElementById('selExclusion').value = 'by_value';
|
||||
document.getElementById('selTies').value = 'forwards,random';
|
||||
}
|
||||
}
|
||||
|
|
170
html/main.css
170
html/main.css
|
@ -1,19 +1,20 @@
|
|||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021–2022 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/*
|
||||
pyRCV2: Preferential vote counting
|
||||
Copyright © 2020–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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600&display=swap');
|
||||
|
||||
|
@ -33,13 +34,6 @@ a:hover {
|
|||
color: #1d3da2;
|
||||
text-decoration: underline;
|
||||
}
|
||||
tr.stage-no a {
|
||||
color: initial !important;
|
||||
}
|
||||
|
||||
li.highlight {
|
||||
background-color: #fffedd;
|
||||
}
|
||||
|
||||
/* Menu styling */
|
||||
|
||||
|
@ -53,10 +47,6 @@ li.highlight {
|
|||
.menudiv .subheading {
|
||||
font-size: 0.8em;
|
||||
font-weight: 600;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.menudiv > div > .subheading:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.pill-grey {
|
||||
|
@ -93,7 +83,7 @@ table {
|
|||
color-adjust: exact;
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
table.result td, table.transfers td {
|
||||
.result td {
|
||||
padding: 0px 8px;
|
||||
height: 1em;
|
||||
}
|
||||
|
@ -104,22 +94,15 @@ td.count sup {
|
|||
font-size: 0.6rem;
|
||||
top: 0;
|
||||
}
|
||||
tr.stage-no td, tr.stage-kind td, tr.stage-comment td, tr.hint-papers-votes td {
|
||||
tr.stage-no td, tr.stage-kind td, tr.stage-comment td {
|
||||
text-align: center;
|
||||
}
|
||||
td.candidate-name, td.elected, td.excluded {
|
||||
white-space: nowrap;
|
||||
}
|
||||
tr.stage-kind td {
|
||||
font-size: 0.75em;
|
||||
min-width: 5rem;
|
||||
color: #1b2839;
|
||||
background-color: #f0f5fb;
|
||||
}
|
||||
tr.hint-papers-votes td {
|
||||
font-size: 0.75em;
|
||||
font-style: italic;
|
||||
}
|
||||
td.excluded {
|
||||
background-color: #fde2e2;
|
||||
}
|
||||
|
@ -129,34 +112,23 @@ td.elected {
|
|||
tr.info td {
|
||||
background-color: #f0f5fb;
|
||||
}
|
||||
tr.stage-no td:not(:empty), tr.hint-papers-votes td:not(:empty), tr.transfers td,
|
||||
table.transfers tr:first-child td, table.transfers tr:nth-last-child(2) td, table.transfers tr:last-child td {
|
||||
tr.stage-no td:not(:empty), tr.transfers td {
|
||||
border-top: 1px solid #76858c;
|
||||
}
|
||||
tr.info:last-child td, .bb,
|
||||
table.transfers tr:first-child td, table.transfers tr:nth-last-child(2) td, table.transfers tr:last-child td {
|
||||
tr.info:last-child td, .bb {
|
||||
border-bottom: 1px solid #76858c;
|
||||
}
|
||||
.blw {
|
||||
/* Used to separate counts in van der Craats (‘Wright’) STV */
|
||||
/* Used to separate counts in Wright STV */
|
||||
border-left: 2px solid #76858c;
|
||||
}
|
||||
|
||||
table.transfers tr:first-child td {
|
||||
font-weight: 600;
|
||||
}
|
||||
table.transfers tr:first-child td, table.transfers tr:nth-last-child(2) td, .transfers tr:last-child td {
|
||||
background-color: #f0f5fb;
|
||||
}
|
||||
|
||||
/* Table stripes */
|
||||
|
||||
tr.stage-no td:nth-child(even):not([rowspan]),
|
||||
tr.stage-comment td:nth-child(odd),
|
||||
tr.hint-papers-votes td:nth-child(even),
|
||||
tr.candidate.transfers td:nth-child(even):not(.elected):not(.excluded),
|
||||
tr.candidate.votes td:nth-child(odd):not(.elected):not(.excluded),
|
||||
table.transfers td:nth-child(even) {
|
||||
tr.candidate.votes td:nth-child(odd):not(.elected):not(.excluded) {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
tr.candidate.transfers td.elected:nth-child(even),
|
||||
|
@ -169,13 +141,37 @@ tr.candidate.votes td.excluded:nth-child(odd) {
|
|||
}
|
||||
tr.info.stage-kind td:nth-child(odd),
|
||||
tr.info.transfers td:nth-child(even),
|
||||
tr.info.votes td:nth-child(odd),
|
||||
table.transfers tr:first-child td:nth-child(even), table.transfers tr:nth-last-child(2) td:nth-child(even), .transfers tr:last-child td:nth-child(even) {
|
||||
tr.info.votes td:nth-child(odd) {
|
||||
background-color: #e8eef7;
|
||||
}
|
||||
|
||||
a.detailedTransfersLink {
|
||||
color: #aaa;
|
||||
/* BLT input tool */
|
||||
|
||||
#selBallots {
|
||||
min-width: 10em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
#bltMain {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#tblBallot {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
#tblBallot input {
|
||||
margin-right: 0.5ex;
|
||||
}
|
||||
|
||||
#divEditCandidates div {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
#txtCandidates {
|
||||
min-width: 20em;
|
||||
min-height: 10em;
|
||||
}
|
||||
|
||||
/* Print stylesheet */
|
||||
|
@ -195,9 +191,6 @@ a.detailedTransfersLink {
|
|||
#printWarning {
|
||||
display: block;
|
||||
}
|
||||
a.detailedTransfersLink {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Form styling */
|
||||
|
@ -207,9 +200,9 @@ select, input, button {
|
|||
line-height: 1.15;
|
||||
}
|
||||
|
||||
select, input[type="text"], input[type="number"], textarea, .js-Dropdown-title {
|
||||
select, input[type="text"], input[type="number"], textarea {
|
||||
appearance: none;
|
||||
background-color: #fff !important;
|
||||
background-color: #fff;
|
||||
border: 1px solid;
|
||||
border-color: #999 #bbb #ddd;
|
||||
border-radius: 0;
|
||||
|
@ -218,7 +211,7 @@ select, input[type="text"], input[type="number"], textarea, .js-Dropdown-title {
|
|||
padding: 2px 3px;
|
||||
}
|
||||
|
||||
select, .js-Dropdown-title {
|
||||
select {
|
||||
/* Dropdown arrow */
|
||||
background-image: url(data:image/png;base64,R0lGODlhDQAEAIAAAAAAAP8A/yH5BAEHAAEALAAAAAANAAQAAAILhA+hG5jMDpxvhgIAOw==);
|
||||
background-position: right center;
|
||||
|
@ -284,63 +277,10 @@ input[type="checkbox"]:checked {
|
|||
button:focus, select:focus, input:focus, textarea:focus {
|
||||
outline: 0;
|
||||
}
|
||||
select:focus, .js-Dropdown-title:focus, input:focus, textarea:focus {
|
||||
select:focus, input:focus, textarea:focus {
|
||||
border-color: #3daee9;
|
||||
}
|
||||
|
||||
label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Custom dropdown */
|
||||
/* Adapted from https://github.com/zoltantothcom/vanilla-js-dropdown (Unlicense) */
|
||||
|
||||
.js-Dropdown {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
.js-Dropdown-title {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
z-index: 999;
|
||||
padding: 2px 6px; /* Needs additional padding to match <select> */
|
||||
}
|
||||
.js-Dropdown-list {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #ddd;
|
||||
border-left: 1px solid #bbb;
|
||||
border-right: 1px solid #bbb;
|
||||
box-sizing: border-box;
|
||||
display: none;
|
||||
list-style: none;
|
||||
margin: -5px 0 0 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
min-width: 100%;
|
||||
z-index: 998;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.js-Dropdown-list.is-open {
|
||||
display: block;
|
||||
}
|
||||
.js-Dropdown-list li {
|
||||
cursor: pointer;
|
||||
padding: 2px 3px 2px 11px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.js-Dropdown-list li:hover {
|
||||
background-color: #e9f7ff;
|
||||
}
|
||||
.js-Dropdown-list li.is-selected {
|
||||
background-color: #c0e8fd;
|
||||
}
|
||||
.js-Dropdown-optgroup {
|
||||
font-weight: bold;
|
||||
padding: 2px 3px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.js-Dropdown-optgroup:first-child {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
|
478
html/presets.js
478
html/presets.js
|
@ -1,478 +0,0 @@
|
|||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021–2022 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
function changePreset() {
|
||||
if (document.getElementById('selPreset').value === 'wigm') {
|
||||
document.getElementById('selQuotaCriterion').value = 'gt';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = false;
|
||||
document.getElementById('chkImmediateElect').checked = true;
|
||||
document.getElementById('txtMinThreshold').value = '0';
|
||||
document.getElementById('selNumbers').value = 'rational';
|
||||
document.getElementById('txtPPDP').value = '2';
|
||||
document.getElementById('chkRoundQuota').checked = false;
|
||||
document.getElementById('chkRoundVotes').checked = false;
|
||||
document.getElementById('chkRoundSFs').checked = false;
|
||||
document.getElementById('chkRoundValues').checked = false;
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selMethod').value = 'wig';
|
||||
document.getElementById('selPapers').value = 'both';
|
||||
document.getElementById('selExclusion').value = 'single_stage';
|
||||
document.getElementById('selTies').value = 'backwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'scottish') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = false;
|
||||
document.getElementById('chkImmediateElect').checked = true;
|
||||
document.getElementById('txtMinThreshold').value = '0';
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '5';
|
||||
document.getElementById('txtPPDP').value = '5';
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '0';
|
||||
document.getElementById('chkRoundVotes').checked = false;
|
||||
document.getElementById('chkRoundSFs').checked = true;
|
||||
document.getElementById('txtRoundSFs').value = '5';
|
||||
document.getElementById('chkRoundValues').checked = false;
|
||||
document.getElementById('selSumTransfers').value = 'per_ballot';
|
||||
document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selMethod').value = 'wig';
|
||||
document.getElementById('selPapers').value = 'both';
|
||||
document.getElementById('selExclusion').value = 'single_stage';
|
||||
document.getElementById('selTies').value = 'backwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'meek87') {
|
||||
document.getElementById('selQuotaCriterion').value = 'gt';
|
||||
document.getElementById('selQuota').value = 'droop_exact';
|
||||
document.getElementById('selQuotaMode').value = 'dynamic_by_total';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = false;
|
||||
document.getElementById('chkImmediateElect').checked = false;
|
||||
document.getElementById('chkMeekNZExclusion').checked = false;
|
||||
document.getElementById('txtMinThreshold').value = '0';
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '5';
|
||||
document.getElementById('txtPPDP').value = '2';
|
||||
document.getElementById('chkRoundQuota').checked = false;
|
||||
document.getElementById('chkRoundVotes').checked = false;
|
||||
document.getElementById('chkRoundSFs').checked = false;
|
||||
document.getElementById('chkRoundValues').checked = false;
|
||||
//document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('txtMeekSurplusTolerance').value = '0.001%';
|
||||
//document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selMethod').value = 'meek';
|
||||
document.getElementById('selPapers').value = 'both';
|
||||
document.getElementById('selExclusion').value = 'single_stage';
|
||||
document.getElementById('selTies').value = 'backwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'meek06') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('selQuotaMode').value = 'dynamic_by_total';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = true;
|
||||
document.getElementById('chkImmediateElect').checked = true;
|
||||
document.getElementById('chkMeekNZExclusion').checked = false;
|
||||
document.getElementById('txtMinThreshold').value = '0';
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '12';
|
||||
document.getElementById('txtPPDP').value = '2';
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '9';
|
||||
document.getElementById('chkRoundVotes').checked = true;
|
||||
document.getElementById('txtRoundVotes').value = '9';
|
||||
document.getElementById('chkRoundSFs').checked = true;
|
||||
document.getElementById('txtRoundSFs').value = '9';
|
||||
document.getElementById('chkRoundValues').checked = true;
|
||||
document.getElementById('txtRoundValues').value = '9';
|
||||
//document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('txtMeekSurplusTolerance').value = '0.0001';
|
||||
//document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selMethod').value = 'meek';
|
||||
document.getElementById('selPapers').value = 'both';
|
||||
document.getElementById('selExclusion').value = 'single_stage';
|
||||
document.getElementById('selTies').value = 'forwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'meeknz') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('selQuotaMode').value = 'dynamic_by_total';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = true;
|
||||
document.getElementById('chkImmediateElect').checked = true;
|
||||
document.getElementById('chkMeekNZExclusion').checked = true;
|
||||
document.getElementById('txtMinThreshold').value = '0';
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '12';
|
||||
document.getElementById('txtPPDP').value = '2';
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '9';
|
||||
document.getElementById('chkRoundVotes').checked = true;
|
||||
document.getElementById('txtRoundVotes').value = '9';
|
||||
document.getElementById('chkRoundSFs').checked = true;
|
||||
document.getElementById('txtRoundSFs').value = '9';
|
||||
document.getElementById('chkRoundValues').checked = true;
|
||||
document.getElementById('txtRoundValues').value = '9';
|
||||
//document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('txtMeekSurplusTolerance').value = '0.0001';
|
||||
//document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selMethod').value = 'meek';
|
||||
document.getElementById('selPapers').value = 'both';
|
||||
document.getElementById('selExclusion').value = 'single_stage';
|
||||
document.getElementById('selTies').value = 'forwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'senate') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false; // Senate "bulk exclusion" does not permit quota to be exceeded
|
||||
document.getElementById('chkDeferSurpluses').checked = false;
|
||||
document.getElementById('chkImmediateElect').checked = true;
|
||||
document.getElementById('txtMinThreshold').value = '0';
|
||||
document.getElementById('selNumbers').value = 'rational';
|
||||
document.getElementById('txtPPDP').value = '0';
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '0';
|
||||
document.getElementById('chkRoundVotes').checked = true;
|
||||
document.getElementById('txtRoundVotes').value = '0';
|
||||
document.getElementById('chkRoundSFs').checked = false;
|
||||
document.getElementById('chkRoundValues').checked = false;
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_order';
|
||||
document.getElementById('selMethod').value = 'uig';
|
||||
document.getElementById('selPapers').value = 'both';
|
||||
document.getElementById('selExclusion').value = 'by_value';
|
||||
document.getElementById('selTies').value = 'backwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'viclc') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = false;
|
||||
document.getElementById('chkImmediateElect').checked = true;
|
||||
document.getElementById('txtMinThreshold').value = '0';
|
||||
document.getElementById('selNumbers').value = 'rational';
|
||||
document.getElementById('txtPPDP').value = '0';
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '0';
|
||||
document.getElementById('chkRoundVotes').checked = true;
|
||||
document.getElementById('txtRoundVotes').value = '0';
|
||||
document.getElementById('chkRoundSFs').checked = false;
|
||||
document.getElementById('chkRoundValues').checked = false;
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_order';
|
||||
document.getElementById('selMethod').value = 'uig';
|
||||
document.getElementById('selPapers').value = 'both';
|
||||
document.getElementById('selExclusion').value = 'first_prefs_then_by_value';
|
||||
document.getElementById('selTies').value = 'backwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'wa') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = false;
|
||||
document.getElementById('chkImmediateElect').checked = true;
|
||||
document.getElementById('txtMinThreshold').value = '0';
|
||||
document.getElementById('selNumbers').value = 'rational';
|
||||
document.getElementById('txtPPDP').value = '0';
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '0';
|
||||
document.getElementById('chkRoundVotes').checked = true;
|
||||
document.getElementById('txtRoundVotes').value = '0';
|
||||
document.getElementById('chkRoundSFs').checked = false;
|
||||
document.getElementById('chkRoundValues').checked = false;
|
||||
document.getElementById('selSumTransfers').value = 'by_parcel';
|
||||
document.getElementById('selSurplus').value = 'by_order';
|
||||
document.getElementById('selMethod').value = 'wig';
|
||||
document.getElementById('selPapers').value = 'assume_progress_total';
|
||||
document.getElementById('selExclusion').value = 'parcels_by_order';
|
||||
document.getElementById('selTies').value = 'backwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'act') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = false;
|
||||
document.getElementById('chkImmediateElect').checked = true;
|
||||
document.getElementById('txtMinThreshold').value = '0';
|
||||
document.getElementById('selNumbers').value = 'rational';
|
||||
document.getElementById('txtPPDP').value = '2';
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '0';
|
||||
document.getElementById('chkRoundVotes').checked = true;
|
||||
document.getElementById('txtRoundVotes').value = '6';
|
||||
document.getElementById('chkRoundSFs').checked = false;
|
||||
document.getElementById('chkRoundValues').checked = false;
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_order';
|
||||
document.getElementById('selMethod').value = 'eg';
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
document.getElementById('selExclusion').value = 'by_value';
|
||||
document.getElementById('selTies').value = 'backwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'nswlg') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = false;
|
||||
document.getElementById('chkImmediateElect').checked = true;
|
||||
document.getElementById('txtMinThreshold').value = '0';
|
||||
document.getElementById('selNumbers').value = 'rational';
|
||||
document.getElementById('txtPPDP').value = '0';
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '0';
|
||||
document.getElementById('chkRoundVotes').checked = true;
|
||||
document.getElementById('txtRoundVotes').value = '0';
|
||||
document.getElementById('chkRoundSFs').checked = false;
|
||||
document.getElementById('chkRoundValues').checked = false;
|
||||
document.getElementById('selSumTransfers').value = 'by_parcel';
|
||||
document.getElementById('selSurplus').value = 'by_order';
|
||||
document.getElementById('selMethod').value = 'wig';
|
||||
document.getElementById('selPapers').value = 'subtract_nontransferable';
|
||||
document.getElementById('selExclusion').value = 'single_stage';
|
||||
document.getElementById('selTies').value = 'backwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'minneapolis') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = true;
|
||||
document.getElementById('chkDeferSurpluses').checked = true;
|
||||
document.getElementById('chkImmediateElect').checked = false;
|
||||
document.getElementById('txtMinThreshold').value = '0';
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '4';
|
||||
document.getElementById('txtPPDP').value = '4';
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '0';
|
||||
document.getElementById('chkRoundVotes').checked = false;
|
||||
document.getElementById('chkRoundSFs').checked = true;
|
||||
document.getElementById('txtRoundSFs').value = '4';
|
||||
document.getElementById('chkRoundValues').checked = false;
|
||||
document.getElementById('selSumTransfers').value = 'per_ballot';
|
||||
document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selMethod').value = 'wig';
|
||||
document.getElementById('selPapers').value = 'both';
|
||||
document.getElementById('selExclusion').value = 'single_stage';
|
||||
document.getElementById('selTies').value = 'random';
|
||||
} else if (document.getElementById('selPreset').value === 'cambridge') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = false;
|
||||
document.getElementById('chkImmediateElect').checked = true;
|
||||
document.getElementById('selSample').value = 'cincinnati';
|
||||
document.getElementById('chkSamplePerBallot').checked = true;
|
||||
document.getElementById('txtMinThreshold').value = '49';
|
||||
document.getElementById('selNumbers').value = 'rational';
|
||||
document.getElementById('txtPPDP').value = '0';
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '0';
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selMethod').value = 'hare';
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
document.getElementById('selExclusion').value = 'single_stage';
|
||||
document.getElementById('selTies').value = 'backwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'dail') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = true;
|
||||
document.getElementById('chkImmediateElect').checked = true;
|
||||
document.getElementById('selSample').value = 'stratify';
|
||||
document.getElementById('chkSamplePerBallot').checked = false;
|
||||
document.getElementById('txtMinThreshold').value = '0';
|
||||
document.getElementById('selNumbers').value = 'rational';
|
||||
document.getElementById('txtPPDP').value = '0';
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '0';
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_order';
|
||||
document.getElementById('selMethod').value = 'hare';
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
document.getElementById('selExclusion').value = 'single_stage';
|
||||
document.getElementById('selTies').value = 'forwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'vdc') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = true;
|
||||
document.getElementById('chkDeferSurpluses').checked = false;
|
||||
document.getElementById('chkImmediateElect').checked = true;
|
||||
document.getElementById('txtMinThreshold').value = '0';
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '5';
|
||||
document.getElementById('txtPPDP').value = '2';
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '0';
|
||||
document.getElementById('chkRoundVotes').checked = false;
|
||||
document.getElementById('chkRoundSFs').checked = false;
|
||||
document.getElementById('chkRoundValues').checked = false;
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selMethod').value = 'wig';
|
||||
document.getElementById('selPapers').value = 'both';
|
||||
document.getElementById('selExclusion').value = 'reset_and_reiterate';
|
||||
document.getElementById('selTies').value = 'random';
|
||||
} else if (document.getElementById('selPreset').value === 'prsa77') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = true;
|
||||
document.getElementById('chkImmediateElect').checked = true;
|
||||
document.getElementById('txtMinThreshold').value = '0';
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '6';
|
||||
document.getElementById('txtPPDP').value = '3';
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '3';
|
||||
document.getElementById('chkRoundVotes').checked = true;
|
||||
document.getElementById('txtRoundVotes').value = '3';
|
||||
document.getElementById('chkRoundSFs').checked = true;
|
||||
document.getElementById('txtRoundSFs').value = '3';
|
||||
document.getElementById('chkRoundValues').checked = true;
|
||||
document.getElementById('txtRoundValues').value = '3';
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_order';
|
||||
document.getElementById('selMethod').value = 'eg';
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
document.getElementById('selExclusion').value = 'parcels_by_order';
|
||||
document.getElementById('selTies').value = 'backwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'ers97') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop_exact';
|
||||
document.getElementById('selQuotaMode').value = 'ers97';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = true;
|
||||
document.getElementById('chkDeferSurpluses').checked = true;
|
||||
document.getElementById('chkImmediateElect').checked = true;
|
||||
document.getElementById('txtMinThreshold').value = '0';
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '5';
|
||||
document.getElementById('txtPPDP').value = '2';
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '2';
|
||||
document.getElementById('chkRoundVotes').checked = true;
|
||||
document.getElementById('txtRoundVotes').value = '2';
|
||||
document.getElementById('chkRoundSFs').checked = true;
|
||||
document.getElementById('txtRoundSFs').value = '2';
|
||||
document.getElementById('chkRoundValues').checked = true;
|
||||
document.getElementById('txtRoundValues').value = '2';
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selMethod').value = 'eg';
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
document.getElementById('selExclusion').value = 'by_value';
|
||||
document.getElementById('selTies').value = 'forwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'ers76') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop_exact';
|
||||
document.getElementById('selQuotaMode').value = 'ers76';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = true;
|
||||
document.getElementById('chkDeferSurpluses').checked = true;
|
||||
document.getElementById('chkImmediateElect').checked = true;
|
||||
document.getElementById('txtMinThreshold').value = '0';
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '5';
|
||||
document.getElementById('txtPPDP').value = '2';
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '2';
|
||||
document.getElementById('chkRoundVotes').checked = true;
|
||||
document.getElementById('txtRoundVotes').value = '2';
|
||||
document.getElementById('chkRoundSFs').checked = true;
|
||||
document.getElementById('txtRoundSFs').value = '2';
|
||||
document.getElementById('chkRoundValues').checked = true;
|
||||
document.getElementById('txtRoundValues').value = '2';
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selMethod').value = 'eg';
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
document.getElementById('selExclusion').value = 'by_value';
|
||||
document.getElementById('selTies').value = 'forwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'ers73') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop_exact';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = true;
|
||||
document.getElementById('chkDeferSurpluses').checked = true;
|
||||
document.getElementById('chkImmediateElect').checked = true;
|
||||
document.getElementById('txtMinThreshold').value = '0';
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '5';
|
||||
document.getElementById('txtPPDP').value = '2';
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '2';
|
||||
document.getElementById('chkRoundVotes').checked = true;
|
||||
document.getElementById('txtRoundVotes').value = '2';
|
||||
document.getElementById('chkRoundSFs').checked = true;
|
||||
document.getElementById('txtRoundSFs').value = '2';
|
||||
document.getElementById('chkRoundValues').checked = true;
|
||||
document.getElementById('txtRoundValues').value = '2';
|
||||
document.getElementById('selSumTransfers').value = 'single_step';
|
||||
document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selMethod').value = 'eg';
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
document.getElementById('selExclusion').value = 'by_value';
|
||||
document.getElementById('selTies').value = 'forwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'cofe') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = true;
|
||||
document.getElementById('chkImmediateElect').checked = true;
|
||||
document.getElementById('txtMinThreshold').value = '0';
|
||||
document.getElementById('selNumbers').value = 'fixed';
|
||||
document.getElementById('txtDP').value = '5';
|
||||
document.getElementById('txtPPDP').value = '2';
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '2';
|
||||
document.getElementById('chkRoundVotes').checked = true;
|
||||
document.getElementById('txtRoundVotes').value = '2';
|
||||
document.getElementById('chkRoundSFs').checked = true;
|
||||
document.getElementById('txtRoundSFs').value = '2';
|
||||
document.getElementById('chkRoundValues').checked = true;
|
||||
document.getElementById('txtRoundValues').value = '2';
|
||||
document.getElementById('selSumTransfers').value = 'per_ballot';
|
||||
document.getElementById('selSurplus').value = 'by_size';
|
||||
document.getElementById('selMethod').value = 'eg';
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
document.getElementById('selExclusion').value = 'by_value';
|
||||
document.getElementById('selTies').value = 'forwards,random';
|
||||
}
|
||||
}
|
229
html/print.js
229
html/print.js
|
@ -1,229 +0,0 @@
|
|||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021–2022 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
async function printResult() {
|
||||
// Remove highlighted stage comment if any
|
||||
for (let elLi of document.getElementById('olStageComments').children) {
|
||||
elLi.classList.remove('highlight');
|
||||
}
|
||||
|
||||
let printableWidth; // Printable width in CSS pixels
|
||||
let paperSize = document.getElementById('selPaperSize').value;
|
||||
if (paperSize === 'A4') {
|
||||
printableWidth = (29.7 - 2) * 96 / 2.54;
|
||||
} else if (paperSize === 'A3') {
|
||||
printableWidth = (42.0 - 2) * 96 / 2.54;
|
||||
} else if (paperSize === 'letter') {
|
||||
printableWidth = (27.9 - 2) * 96 / 2.54;
|
||||
}
|
||||
printableWidth = Math.round(printableWidth);
|
||||
|
||||
let wprint = window.open('');
|
||||
wprint.document.title = 'OpenTally Report';
|
||||
|
||||
// Add stylesheets
|
||||
let numToLoad = 0;
|
||||
let numLoaded = -1;
|
||||
|
||||
function onLoadStylesheet() {
|
||||
numLoaded++;
|
||||
if (numLoaded == numToLoad) {
|
||||
wprint.print();
|
||||
}
|
||||
}
|
||||
|
||||
for (let elCSSBase of document.querySelectorAll('head link')) {
|
||||
numToLoad++;
|
||||
let elCSS = wprint.document.createElement('link');
|
||||
elCSS.rel = elCSSBase.rel;
|
||||
elCSS.type = elCSSBase.type;
|
||||
if (elCSSBase.href.endsWith('?v=GITVERSION')) {
|
||||
elCSS.href = elCSSBase.href.replace('?v=GITVERSION', '?v=' + Math.random());
|
||||
} else {
|
||||
elCSS.href = elCSSBase.href;
|
||||
}
|
||||
|
||||
elCSS.onload = onLoadStylesheet;
|
||||
wprint.document.head.appendChild(elCSS);
|
||||
}
|
||||
|
||||
// Configure printing
|
||||
let elStyle = wprint.document.createElement('style');
|
||||
elStyle.innerHTML = '@page { size: ' + paperSize + ' landscape; margin: 1cm; } @media print { body { padding: 0; } }';
|
||||
wprint.document.head.appendChild(elStyle);
|
||||
|
||||
let elContainer = wprint.document.createElement('div');
|
||||
elContainer.id = 'printContainer';
|
||||
elContainer.style.width = printableWidth + 'px';
|
||||
wprint.document.body.appendChild(elContainer);
|
||||
|
||||
// Copy result logs 1
|
||||
let divResultLogs1 = document.getElementById('resultLogs1');
|
||||
let divResultLogs2 = wprint.document.createElement('div');
|
||||
divResultLogs2.innerHTML = divResultLogs1.innerHTML;
|
||||
elContainer.appendChild(divResultLogs2);
|
||||
|
||||
// Parse table, accounting for colspan/rowspan
|
||||
let elTrs1 = document.getElementById('result').rows;
|
||||
let rows = [];
|
||||
for (let elTr1 of elTrs1) {
|
||||
rows.push([]);
|
||||
}
|
||||
for (let r = 0; r < elTrs1.length; r++) {
|
||||
for (let c = 0; c < elTrs1[r].cells.length; c++) {
|
||||
let elTd1 = elTrs1[r].cells[c];
|
||||
rows[r].push(elTd1);
|
||||
|
||||
let colspan = elTd1.getAttribute('colspan');
|
||||
if (colspan !== null) {
|
||||
colspan = parseInt(colspan);
|
||||
// Add ghost cells
|
||||
for (let i = 1; i < colspan; i++) {
|
||||
rows[r].push(null);
|
||||
}
|
||||
}
|
||||
|
||||
let rowspan = elTd1.getAttribute('rowspan');
|
||||
// NB: Only works for rowspan in first column
|
||||
if (rowspan !== null && c == 0) {
|
||||
rowspan = parseInt(rowspan);
|
||||
// Add ghost cells
|
||||
for (let i = 1; i < rowspan; i++) {
|
||||
rows[r + i].push(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function copyColumn(c, elTrs2) {
|
||||
let tdsAdded = [];
|
||||
for (let r = 0; r < rows.length; r++) {
|
||||
if (c < rows[r].length) {
|
||||
let elTd1 = rows[r][c];
|
||||
if (elTd1 !== null) {
|
||||
let elTd2 = wprint.document.createElement('td');
|
||||
elTd2.innerHTML = elTd1.innerHTML;
|
||||
elTd2.className = elTd1.className;
|
||||
if (elTd1.getAttribute('rowspan') !== null) { elTd2.setAttribute('rowspan', elTd1.getAttribute('rowspan')); }
|
||||
if (elTd1.getAttribute('colspan') !== null) { elTd2.setAttribute('colspan', elTd1.getAttribute('colspan')); }
|
||||
if (elTd1.getAttribute('style') !== null) { elTd2.setAttribute('style', elTd1.getAttribute('style')); }
|
||||
elTrs2[r].appendChild(elTd2);
|
||||
tdsAdded.push(elTd2);
|
||||
}
|
||||
}
|
||||
}
|
||||
return tdsAdded;
|
||||
}
|
||||
|
||||
async function copyTableColumns(startCol) {
|
||||
let modelRow = document.getElementById('selReport').value === 'ballots_votes' ? rows[4] : rows[3];
|
||||
|
||||
// Add table
|
||||
let elTable2 = wprint.document.createElement('table');
|
||||
elTable2.className = 'result';
|
||||
if (startCol > 1) {
|
||||
elTable2.style.pageBreakBefore = 'always';
|
||||
}
|
||||
elContainer.appendChild(elTable2);
|
||||
|
||||
// Add rows
|
||||
let elTrs2 = [];
|
||||
for (let elTr1 of elTrs1) {
|
||||
let elTr2 = wprint.document.createElement('tr');
|
||||
elTr2.className = elTr1.className;
|
||||
elTrs2.push(elTr2);
|
||||
elTable2.appendChild(elTr2);
|
||||
}
|
||||
|
||||
// Copy first column
|
||||
copyColumn(0, elTrs2);
|
||||
|
||||
// How many columns to copy?
|
||||
let totalWidth = modelRow[0].clientWidth;
|
||||
let endCol;
|
||||
for (endCol = startCol; endCol < modelRow.length; ) {
|
||||
// Check first column
|
||||
if (totalWidth + modelRow[endCol].clientWidth > printableWidth) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
(document.getElementById('selReport').value === 'ballots_votes' && endCol + 1 < modelRow.length) ||
|
||||
(document.getElementById('selReport').value === 'votes_transposed' && endCol != 1 && endCol + 1 < modelRow.length)
|
||||
) {
|
||||
// Check second column
|
||||
if (totalWidth + modelRow[endCol].clientWidth + modelRow[endCol + 1].clientWidth > printableWidth) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Ok!
|
||||
totalWidth += modelRow[endCol].clientWidth;
|
||||
endCol++;
|
||||
|
||||
if (
|
||||
(document.getElementById('selReport').value === 'ballots_votes' && endCol < modelRow.length) ||
|
||||
(document.getElementById('selReport').value === 'votes_transposed' && endCol != 2 && endCol + 1 < modelRow.length)
|
||||
) {
|
||||
// Second column
|
||||
totalWidth += modelRow[endCol].clientWidth;
|
||||
endCol++;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy columns
|
||||
let stages = [];
|
||||
for (let c = startCol; c < endCol; c++) {
|
||||
if (rows[0][c] !== null && rows[0][c].querySelector('a')) {
|
||||
// Track stage headings copied
|
||||
stages.push(parseInt(rows[0][c].querySelector('a').innerHTML));
|
||||
}
|
||||
copyColumn(c, elTrs2);
|
||||
}
|
||||
|
||||
// Copy stage comments
|
||||
elContainer.insertAdjacentHTML('beforeend', '<p>Stage comments:</p>');
|
||||
let olStageComments2 = wprint.document.createElement('ol');
|
||||
olStageComments2.start = stages[0];
|
||||
elContainer.append(olStageComments2);
|
||||
for (let stage of stages) {
|
||||
olStageComments2.insertAdjacentHTML('beforeend', olStageComments.children[stage-1].outerHTML);
|
||||
}
|
||||
|
||||
if (endCol < modelRow.length) {
|
||||
// Start new table if columns remain
|
||||
copyTableColumns(endCol);
|
||||
} else {
|
||||
// Copy winning candidates
|
||||
elContainer.insertAdjacentHTML('beforeend', '<p>Count complete. The winning candidates are, in order of election:</p>');
|
||||
elContainer.insertAdjacentHTML('beforeend', document.getElementById('resultLogs2').lastElementChild.outerHTML);
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust results table to width
|
||||
document.getElementById('resultsDiv').style.width = printableWidth + 'px';
|
||||
await new Promise(window.requestAnimationFrame); // Allow DOM to update
|
||||
|
||||
// Copy table
|
||||
await copyTableColumns(1);
|
||||
|
||||
// Restore original view
|
||||
document.getElementById('resultsDiv').style.width = 'auto';
|
||||
|
||||
// Trigger print when ready
|
||||
onLoadStylesheet();
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
/**
|
||||
* Vanilla JavaScript Dropdown v2.2.0
|
||||
* https://zoltantothcom.github.io/vanilla-js-dropdown
|
||||
*/
|
||||
var CustomSelect=function(e){var o="string"==typeof e.elem?document.getElementById(e.elem):e.elem,s="boolean"==typeof e.bubbles,l="js-Dropdown-title",i="is-selected",t="is-open",n=o.getElementsByTagName("optgroup"),a=o.options,d=a.length,r=0,c=document.createElement("div");c.className="js-Dropdown",o.id&&(c.id="custom-"+o.id);var u=document.createElement("button");u.className=l,u.textContent=a[0].textContent;var m=document.createElement("ul");if(m.className="js-Dropdown-list",n.length)for(var p=0;p<n.length;p++){var v=document.createElement("div");v.innerText=n[p].label,v.classList.add("js-Dropdown-optgroup"),m.appendChild(v),g(n[p].getElementsByTagName("option"))}else g(a);function g(e){for(var t=0;t<e.length;t++){var n=document.createElement("li");n.innerText=e[t].textContent,n.setAttribute("data-value",e[t].value),n.setAttribute("data-index",r++),a[o.selectedIndex].textContent===e[t].textContent&&(n.classList.add(i),u.textContent=e[t].textContent),m.appendChild(n)}}function f(){m.classList.toggle(t)}function x(){m.classList.remove(t)}return c.appendChild(u),c.appendChild(m),c.addEventListener("click",function(e){e.preventDefault();var t=e.target;t.className===l&&f();if("LI"===t.tagName){c.querySelector("."+l).innerText=t.innerText,o.options.selectedIndex=t.getAttribute("data-index");var n=s?new CustomEvent("change",{bubbles:!0}):new CustomEvent("change");o.dispatchEvent(n);for(var a=0;a<d;a++)m.querySelectorAll("li")[a].classList.remove(i);t.classList.add(i),x()}}),o.parentNode.insertBefore(c,o),o.style.display="none",document.addEventListener("click",function(e){c.contains(e.target)||x()}),{toggle:f,close:x,open:function(){m.classList.add(t)}}};
|
208
html/worker.js
208
html/worker.js
|
@ -1,162 +1,106 @@
|
|||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021–2022 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
importScripts('opentally.js?v=GITVERSION');
|
||||
importScripts('opentally.js');
|
||||
|
||||
var wasm = wasm_bindgen;
|
||||
var wasmRaw;
|
||||
|
||||
// For asyncify
|
||||
const DATA_ADDR = 16;
|
||||
const DATA_START = DATA_ADDR + 8;
|
||||
const DATA_END = 50 * 1024; // Needs to be increased compared with Asyncify default
|
||||
|
||||
async function initWasm() {
|
||||
wasmRaw = await wasm_bindgen('opentally_async.wasm?v=GITVERSION');
|
||||
|
||||
new Int32Array(wasmRaw.memory.buffer, DATA_ADDR).set([DATA_START, DATA_END]);
|
||||
|
||||
await wasm_bindgen('opentally_bg.wasm');
|
||||
postMessage({'type': 'init', 'version': wasm.version()});
|
||||
}
|
||||
initWasm();
|
||||
|
||||
var reportStyle;
|
||||
var numbers, election, opts, state, stageNum;
|
||||
|
||||
onmessage = function(evt) {
|
||||
try {
|
||||
if (evt.data.type === 'countElection') {
|
||||
errored = false;
|
||||
|
||||
if (evt.data.numbers === 'fixed') {
|
||||
numbers = 'Fixed';
|
||||
wasm.fixed_set_dps(evt.data.decimals);
|
||||
} else if (evt.data.numbers === 'gfixed') {
|
||||
numbers = 'GuardedFixed';
|
||||
wasm.gfixed_set_dps(evt.data.decimals);
|
||||
} else if (evt.data.numbers === 'float64') {
|
||||
numbers = 'NativeFloat64';
|
||||
} else if (evt.data.numbers === 'rational') {
|
||||
numbers = 'Rational';
|
||||
} else {
|
||||
throw 'Unknown --numbers';
|
||||
}
|
||||
|
||||
reportStyle = evt.data.reportStyle;
|
||||
|
||||
// Init STV options
|
||||
opts = wasm.STVOptions.new.apply(null, evt.data.optsStr);
|
||||
|
||||
// Validate options
|
||||
opts.validate();
|
||||
|
||||
// Init election
|
||||
election = wasm['election_from_blt_' + numbers](evt.data.bltData);
|
||||
wasm['preprocess_election_' + numbers](election, opts);
|
||||
|
||||
// Init constraints if applicable
|
||||
if (evt.data.conData) {
|
||||
wasm['election_load_constraints_' + numbers](election, evt.data.conData, opts);
|
||||
}
|
||||
|
||||
// Describe count
|
||||
postMessage({'type': 'describeCount', 'content': wasm['describe_count_' + numbers](evt.data.bltPath, election, opts)});
|
||||
|
||||
// Init results table
|
||||
postMessage({'type': 'initResultsTable', 'content': wasm['init_results_table_' + numbers](election, opts, reportStyle)});
|
||||
|
||||
// Step election
|
||||
state = wasm['CountState' + numbers].new(election);
|
||||
stageNum = 1;
|
||||
resumeCount();
|
||||
|
||||
} else if (evt.data.type == 'userInput') {
|
||||
userInputBuffer = evt.data.response;
|
||||
|
||||
// Rewind the stack
|
||||
// Asyncify will retrace the function calls in the stack until again reaching get_user_input
|
||||
wasmRaw.asyncify_start_rewind(DATA_ADDR);
|
||||
resumeCount();
|
||||
}
|
||||
} catch (ex) {
|
||||
if (errored) {
|
||||
// Panic already logged and sent to UI
|
||||
if (evt.data.type === 'countElection') {
|
||||
numbers = 'DynNum';
|
||||
|
||||
if (evt.data.numbers === 'fixed') {
|
||||
wasm.dynnum_set_kind(wasm.NumKind.Fixed);
|
||||
wasm.fixed_set_dps(evt.data.decimals);
|
||||
} else if (evt.data.numbers === 'gfixed') {
|
||||
wasm.dynnum_set_kind(wasm.NumKind.GuardedFixed);
|
||||
wasm.gfixed_set_dps(evt.data.decimals);
|
||||
} else if (evt.data.numbers === 'float64') {
|
||||
wasm.dynnum_set_kind(wasm.NumKind.NativeFloat64);
|
||||
} else if (evt.data.numbers === 'rational') {
|
||||
wasm.dynnum_set_kind(wasm.NumKind.Rational);
|
||||
} else {
|
||||
throw ex;
|
||||
throw 'Unknown --numbers';
|
||||
}
|
||||
|
||||
// Init election
|
||||
election = wasm['election_from_blt_' + numbers](evt.data.bltData);
|
||||
|
||||
if (evt.data.normaliseBallots) {
|
||||
wasm['election_normalise_ballots_' + numbers](election);
|
||||
}
|
||||
|
||||
// Init constraints if applicable
|
||||
if (evt.data.conData) {
|
||||
wasm['election_load_constraints_' + numbers](election, evt.data.conData);
|
||||
}
|
||||
|
||||
// Init STV options
|
||||
opts = wasm.STVOptions.new.apply(null, evt.data.optsStr);
|
||||
|
||||
// Validate options
|
||||
opts.validate();
|
||||
|
||||
// Describe count
|
||||
postMessage({'type': 'describeCount', 'content': wasm['describe_count_' + numbers](evt.data.bltPath, election, opts)});
|
||||
|
||||
// Init results table
|
||||
postMessage({'type': 'initResultsTable', 'content': wasm['init_results_table_' + numbers](election, opts)});
|
||||
|
||||
// Step election
|
||||
state = wasm['CountState' + numbers].new(election);
|
||||
wasm['count_init_' + numbers](state, opts);
|
||||
|
||||
postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](1, state, opts)});
|
||||
postMessage({'type': 'updateStageComments', 'comment': wasm['update_stage_comments_' + numbers](state)});
|
||||
|
||||
stageNum = 2;
|
||||
|
||||
resume_count();
|
||||
|
||||
} else if (evt.data.type == 'userInput') {
|
||||
user_input_buffer = evt.data.response;
|
||||
resume_count();
|
||||
}
|
||||
}
|
||||
|
||||
function resumeCount() {
|
||||
function resume_count() {
|
||||
for (;; stageNum++) {
|
||||
let isDone;
|
||||
if (stageNum <= 1) {
|
||||
isDone = wasm['count_init_' + numbers](state, opts);
|
||||
} else {
|
||||
isDone = wasm['count_one_stage_' + numbers](state, opts);
|
||||
try {
|
||||
let isDone = wasm['count_one_stage_' + numbers](state, opts);
|
||||
if (isDone) {
|
||||
break;
|
||||
}
|
||||
} catch (ex) {
|
||||
if (ex === "RequireInput") {
|
||||
return;
|
||||
} else {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
if (wasmRaw.asyncify_get_state() !== 0) {
|
||||
// This stage caused a stack unwind in get_user_input so ignore the result
|
||||
// We will resume execution when a userInput message is received
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDone) {
|
||||
break;
|
||||
}
|
||||
|
||||
postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](stageNum, state, opts, reportStyle)});
|
||||
postMessage({'type': 'updateStageComments', 'comment': wasm['update_stage_comments_' + numbers](state, stageNum), 'stageNum': stageNum});
|
||||
|
||||
let transfers_table = state.transfer_table_render_html(opts);
|
||||
if (transfers_table) {
|
||||
postMessage({'type': 'updateDetailedTransfers', 'table': transfers_table, 'stageNum': stageNum});
|
||||
}
|
||||
postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](stageNum, state, opts)});
|
||||
postMessage({'type': 'updateStageComments', 'comment': wasm['update_stage_comments_' + numbers](state)});
|
||||
}
|
||||
|
||||
postMessage({'type': 'updateResultsTable', 'result': wasm['finalise_results_table_' + numbers](state, reportStyle)});
|
||||
postMessage({'type': 'updateResultsTable', 'result': wasm['finalise_results_table_' + numbers](state)});
|
||||
postMessage({'type': 'finalResultSummary', 'summary': wasm['final_result_summary_' + numbers](state, opts)});
|
||||
}
|
||||
|
||||
var errored = false;
|
||||
function wasm_error(message) {
|
||||
postMessage({'type': 'errorMessage', 'message': message});
|
||||
errored = true;
|
||||
}
|
||||
var user_input_buffer = null;
|
||||
|
||||
var userInputBuffer = null;
|
||||
|
||||
function get_user_input(message) {
|
||||
if (userInputBuffer === null) {
|
||||
function read_user_input_buffer(message) {
|
||||
if (user_input_buffer === null) {
|
||||
postMessage({'type': 'requireInput', 'message': message});
|
||||
|
||||
// Record the current state of the stack
|
||||
wasmRaw.asyncify_start_unwind(DATA_ADDR);
|
||||
|
||||
// No further WebAssembly will be executed and control will return to resumeCount
|
||||
return null;
|
||||
} else {
|
||||
// We have reached the point the stack was originally unwound, so resume normal execution
|
||||
wasmRaw.asyncify_stop_rewind();
|
||||
|
||||
// Return the correct result to WebAssembly
|
||||
let userInput = userInputBuffer;
|
||||
userInputBuffer = null;
|
||||
return userInput;
|
||||
let user_input = user_input_buffer;
|
||||
user_input_buffer = null;
|
||||
return user_input;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
RUSTC_BOOTSTRAP=1 rustc $@
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
#!/bin/bash
|
||||
cargo build --release || exit
|
||||
|
||||
perf stat -r 5 --table -o target/benchmark.log ./target/release/opentally stv tests/data/raw/VIC2022.bin --bin --round-votes 0 --round-quota 0 --quota droop --quota-criterion geq --ties backwards random --random-seed 20210727 --surplus uig --surplus-order by_order --exclusion by_value --pp-decimals 0 $@
|
||||
|
||||
cat target/benchmark.log
|
||||
git describe --always --dirty=-dev | tee -a target/benchmark.log
|
|
@ -1,8 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
DESTDIR='/home/runassudo/Documents/Work/School Cloud Data/unenc/public/www/opentally'
|
||||
|
||||
cd homepage
|
||||
bundle exec jekyll build -d "$DESTDIR"
|
||||
cd ..
|
||||
cp docs/FnSpecs.pdf "$DESTDIR/docs"
|
|
@ -1,21 +0,0 @@
|
|||
#!/bin/sh
|
||||
PATH=$PATH:$HOME/.cargo/bin
|
||||
|
||||
# Build cargo
|
||||
PROFILE=${1:-release}
|
||||
if [ $PROFILE == 'debug' ]; then
|
||||
cargo build --lib --target wasm32-unknown-unknown || exit 1
|
||||
else
|
||||
cargo build --lib --target wasm32-unknown-unknown --$PROFILE || exit 1
|
||||
fi
|
||||
|
||||
if [ target/wasm32-unknown-unknown/$PROFILE/opentally.wasm -nt html/opentally_async.wasm ]; then
|
||||
# Apply wasm-bindgen
|
||||
wasm-bindgen --target no-modules target/wasm32-unknown-unknown/$PROFILE/opentally.wasm --out-dir html --no-typescript
|
||||
|
||||
# Apply Asyncify
|
||||
MANGLED=$(wasm-dis html/opentally_bg.wasm | grep '(import "wbg" "__wbg_getuserinput_' | awk '{print $3;}' | tr -d '"')
|
||||
wasm-opt -O2 --asyncify --pass-arg asyncify-imports@wbg.$MANGLED html/opentally_bg.wasm -o html/opentally_async.wasm
|
||||
|
||||
rm html/opentally_bg.wasm
|
||||
fi
|
|
@ -1,24 +0,0 @@
|
|||
#!/bin/bash
|
||||
PATH=$PATH:$HOME/.cargo/bin
|
||||
|
||||
mkdir -p target/coverage/prof
|
||||
rm target/coverage/prof/*.profraw
|
||||
|
||||
export RUSTFLAGS="-Cinstrument-coverage -Copt-level=0 -Clink-dead-code"
|
||||
export LLVM_PROFILE_FILE="target/coverage/prof/opentally-%p-%m.profraw"
|
||||
export CARGO_TARGET_DIR=target/coverage
|
||||
cargo test
|
||||
|
||||
llvm-profdata merge -sparse target/coverage/prof/*.profraw -o target/coverage/opentally.profdata
|
||||
for file in $(cargo test --no-run --message-format=json 2>/dev/null | jq -r "select(.profile.test == true) | .filenames[]"); do echo -n --object '"'$file'" '; done > target/coverage/objects
|
||||
|
||||
# Need "eval" to correctly parse arguments
|
||||
eval llvm-cov show target/coverage/debug/opentally -instr-profile=target/coverage/opentally.profdata -Xdemangler=rustfilt \
|
||||
$(cat target/coverage/objects) \
|
||||
-ignore-filename-regex="/\\\\." \
|
||||
-ignore-filename-regex="^/rustc" \
|
||||
-ignore-filename-regex="src/numbers/rational_num.rs" \
|
||||
-ignore-filename-regex="src/stv/gregory/prettytable_html.rs" \
|
||||
-ignore-filename-regex="src/stv/wasm.rs" \
|
||||
-ignore-filename-regex="tests/" \
|
||||
-format=html --show-instantiations=false --output-dir=target/coverage/html
|
|
@ -1,31 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
DESTDIR='/home/runassudo/Documents/Work/School Cloud Data/unenc/public/www/opentally'
|
||||
|
||||
# Prevent deploy with unstaged changes
|
||||
|
||||
git update-index --refresh > /dev/null
|
||||
if git diff-index --quiet HEAD -- ; then true; else
|
||||
echo Cannot deploy with unstaged changes
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Rebuild WASM
|
||||
|
||||
./scripts/build_wasm.sh
|
||||
|
||||
# Build homepage
|
||||
|
||||
./scripts/build_homepage.sh
|
||||
|
||||
# Copy files
|
||||
|
||||
#mkdir "$DESTDIR/stv/"
|
||||
cp -r html/* "$DESTDIR/stv/"
|
||||
|
||||
# Replace GITVERSION, etc.
|
||||
|
||||
GITVERSION=$(git rev-parse --short HEAD)
|
||||
sed -i "s#GITVERSION#$GITVERSION#g" "$DESTDIR/stv/index.html"
|
||||
sed -i "s#GITVERSION#$GITVERSION#g" "$DESTDIR/stv/index.js"
|
||||
sed -i "s#GITVERSION#$GITVERSION#g" "$DESTDIR/stv/worker.js"
|
|
@ -1,3 +0,0 @@
|
|||
#!/bin/bash
|
||||
mv target/benchmark.log target/benchmark.baseline.log
|
||||
mv target/perf.data target/perf.baseline.data
|
|
@ -1,9 +0,0 @@
|
|||
#!/bin/bash
|
||||
cargo build --profile perf || exit
|
||||
rm target/perf.data
|
||||
|
||||
# Burn in
|
||||
./target/perf/opentally stv tests/data/raw/VIC2022.bin --bin --round-votes 0 --round-quota 0 --quota droop --quota-criterion geq --ties backwards random --random-seed 20210727 --surplus uig --surplus-order by_order --exclusion by_value --pp-decimals 0 $@
|
||||
|
||||
# Profile
|
||||
perf record -g -o target/perf.data --call-graph=dwarf ./target/perf/opentally stv tests/data/raw/VIC2022.bin --bin --round-votes 0 --round-quota 0 --quota droop --quota-criterion geq --ties backwards random --random-seed 20210727 --surplus uig --surplus-order by_order --exclusion by_value --pp-decimals 0 $@
|
269
src/candmap.rs
269
src/candmap.rs
|
@ -1,269 +0,0 @@
|
|||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021–2022 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::election::Candidate;
|
||||
|
||||
use std::ops::Index;
|
||||
|
||||
/// Mimics a [HashMap](std::collections::HashMap) on [Candidate]s, but internally is a [Vec] based on [Candidate::index]
|
||||
#[derive(Clone)]
|
||||
pub struct CandidateMap<'e, V> {
|
||||
entries: Vec<Option<(&'e Candidate, V)>>
|
||||
}
|
||||
|
||||
impl<'e, V> CandidateMap<'e, V> {
|
||||
/// See [HashMap::new](std::collections::HashMap::new)
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
entries: Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// See [HashMap::with_capacity](std::collections::HashMap::with_capacity)
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
let mut ret = Self {
|
||||
entries: Vec::with_capacity(capacity)
|
||||
};
|
||||
ret.maybe_resize(capacity);
|
||||
return ret;
|
||||
}
|
||||
|
||||
fn maybe_resize(&mut self, len: usize) {
|
||||
if len < self.entries.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.entries.resize_with(len, || None);
|
||||
}
|
||||
|
||||
/// See [HashMap::len](std::collections::HashMap::len)
|
||||
#[inline]
|
||||
pub fn len(&self) -> usize {
|
||||
return self.entries.iter().filter(|e| e.is_some()).count();
|
||||
}
|
||||
|
||||
/// See [HashMap::insert](std::collections::HashMap::insert)
|
||||
#[inline]
|
||||
pub fn insert(&mut self, candidate: &'e Candidate, value: V) {
|
||||
self.maybe_resize(candidate.index + 1);
|
||||
self.entries[candidate.index] = Some((candidate, value));
|
||||
}
|
||||
|
||||
/// See [HashMap::get](std::collections::HashMap::get)
|
||||
#[inline]
|
||||
pub fn get(&self, candidate: &'e Candidate) -> Option<&V> {
|
||||
return self.entries.get(candidate.index).unwrap_or(&None).as_ref().map(|(_, v)| v);
|
||||
}
|
||||
|
||||
/// See [HashMap::get_mut](std::collections::HashMap::get_mut)
|
||||
#[inline]
|
||||
pub fn get_mut(&mut self, candidate: &'e Candidate) -> Option<&mut V> {
|
||||
match self.entries.get_mut(candidate.index) {
|
||||
Some(v) => {
|
||||
return v.as_mut().map(|(_, v)| v);
|
||||
}
|
||||
None => {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// See [HashMap::iter](std::collections::HashMap::iter)
|
||||
#[inline]
|
||||
pub fn iter<'a>(&'a self) -> impl Iterator<Item=(&'e Candidate, &'a V)> {
|
||||
return Iter { map: &self, index: 0 };
|
||||
}
|
||||
|
||||
/// See [HashMap::iter_mut](std::collections::HashMap::iter_mut)
|
||||
#[inline]
|
||||
pub fn iter_mut<'a>(&'a mut self) -> impl Iterator<Item=(&'e Candidate, &'a mut V)> {
|
||||
return IterMut { map: self, index: 0 };
|
||||
}
|
||||
|
||||
/// See [HashMap::values](std::collections::HashMap::values)
|
||||
#[inline]
|
||||
pub fn values<'a>(&'a self) -> impl Iterator<Item=&'a V> {
|
||||
return Values { map: &self, index: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
impl<'e, V> Index<&Candidate> for CandidateMap<'e, V> {
|
||||
type Output = V;
|
||||
|
||||
fn index(&self, candidate: &Candidate) -> &Self::Output {
|
||||
return self.entries.get(candidate.index).unwrap_or(&None).as_ref().map(|(_, v)| v).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// See [CandidateMap::iter]
|
||||
struct Iter<'m, 'e, V> {
|
||||
map: &'m CandidateMap<'e, V>,
|
||||
index: usize
|
||||
}
|
||||
|
||||
impl<'m, 'e, V> Iterator for Iter<'m, 'e, V> {
|
||||
type Item = (&'e Candidate, &'m V);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
match self.map.entries.get(self.index) {
|
||||
Some(e) => {
|
||||
// Key within range
|
||||
match e {
|
||||
Some((k, v)) => {
|
||||
// Key is set
|
||||
self.index += 1;
|
||||
return Some((k, v));
|
||||
}
|
||||
None => {
|
||||
// Key is unset
|
||||
self.index += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Key outside range
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// See [CandidateMap::iter_mut]
|
||||
struct IterMut<'m, 'e, V> {
|
||||
map: &'m mut CandidateMap<'e, V>,
|
||||
index: usize
|
||||
}
|
||||
|
||||
impl<'m, 'e, V> Iterator for IterMut<'m, 'e, V> {
|
||||
type Item = (&'e Candidate, &'m mut V);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
match self.map.entries.get_mut(self.index) {
|
||||
Some(e) => {
|
||||
// Key within range
|
||||
match e {
|
||||
Some((k, v)) => {
|
||||
// Key is set
|
||||
let v_ptr = v as *mut V;
|
||||
|
||||
// SAFETY: Need unsafe pointer magic for IterMut
|
||||
let vv = unsafe { &mut *v_ptr };
|
||||
|
||||
self.index += 1;
|
||||
return Some((k, vv));
|
||||
}
|
||||
None => {
|
||||
// Key is unset
|
||||
self.index += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Key outside range
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// See [CandidateMap::values]
|
||||
struct Values<'m, 'e, V> {
|
||||
map: &'m CandidateMap<'e, V>,
|
||||
index: usize
|
||||
}
|
||||
|
||||
impl<'m, 'e, V> Iterator for Values<'m, 'e, V> {
|
||||
type Item = &'m V;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
match self.map.entries.get(self.index) {
|
||||
Some(e) => {
|
||||
// Key within range
|
||||
match e {
|
||||
Some((_, v)) => {
|
||||
// Key is set
|
||||
self.index += 1;
|
||||
return Some(v);
|
||||
}
|
||||
None => {
|
||||
// Key is unset
|
||||
self.index += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Key outside range
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// See [CandidateMap::into_iter]
|
||||
pub struct IntoIter<'e, V> {
|
||||
map: CandidateMap<'e, V>,
|
||||
index: usize
|
||||
}
|
||||
|
||||
impl<'e, V> Iterator for IntoIter<'e, V> {
|
||||
type Item = (&'e Candidate, V);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
match self.map.entries.get_mut(self.index) {
|
||||
Some(e) => {
|
||||
// Key within range
|
||||
match e.take() {
|
||||
Some((k, v)) => {
|
||||
// Key is set
|
||||
self.index += 1;
|
||||
return Some((k, v));
|
||||
}
|
||||
None => {
|
||||
// Key is unset
|
||||
self.index += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Key outside range
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'e, V> IntoIterator for CandidateMap<'e, V> {
|
||||
type Item = (&'e Candidate, V);
|
||||
|
||||
type IntoIter = IntoIter<'e, V>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
return IntoIter { map: self, index: 0 };
|
||||
}
|
||||
}
|
|
@ -1,155 +0,0 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::election::Election;
|
||||
use crate::numbers::Rational;
|
||||
use crate::parser;
|
||||
use crate::writer;
|
||||
|
||||
use clap::{AppSettings, Parser};
|
||||
|
||||
use std::fs::File;
|
||||
|
||||
/// Convert between different ballot data formats
|
||||
#[derive(Parser)]
|
||||
#[clap(setting=AppSettings::DeriveDisplayOrder)]
|
||||
pub struct SubcmdOptions {
|
||||
/// Path to the input data file
|
||||
#[clap(help_heading=Some("INPUT"))]
|
||||
infile: String,
|
||||
|
||||
/// Format of input file
|
||||
#[clap(help_heading=Some("INPUT"), short, long, possible_values=&["bin", "blt", "csp"], value_name="format")]
|
||||
r#in: Option<String>,
|
||||
|
||||
/// Path to the output data file
|
||||
#[clap(help_heading=Some("OUTPUT"))]
|
||||
outfile: String,
|
||||
|
||||
/// Format of output file
|
||||
#[clap(help_heading=Some("OUTPUT"), short, long, possible_values=&["bin", "blt", "csp"], value_name="format")]
|
||||
out: Option<String>,
|
||||
|
||||
/// Number of seats
|
||||
#[clap(help_heading=Some("ELECTION SPECIFICATION"), long)]
|
||||
seats: Option<usize>,
|
||||
|
||||
/// Require 1st preference
|
||||
#[clap(help_heading=Some("PREFERENCE VALIDATION"), long)]
|
||||
require_1: bool,
|
||||
|
||||
/// Require sequential preferences
|
||||
#[clap(help_heading=Some("PREFERENCE VALIDATION"), long)]
|
||||
require_sequential: bool,
|
||||
|
||||
/// Require strict ordering of preferences (disallow equal rankings)
|
||||
#[clap(help_heading=Some("PREFERENCE VALIDATION"), long)]
|
||||
require_strict_order: bool,
|
||||
|
||||
/// Do not output wholly informal ballots
|
||||
#[clap(help_heading=Some("PREFERENCE VALIDATION"), long)]
|
||||
omit_informal: bool,
|
||||
}
|
||||
|
||||
/// Entrypoint for subcommand
|
||||
pub fn main(mut cmd_opts: SubcmdOptions) -> Result<(), i32> {
|
||||
// Auto-detect input/output formats
|
||||
if cmd_opts.r#in == None {
|
||||
if cmd_opts.infile.ends_with(".bin") {
|
||||
cmd_opts.r#in = Some("bin".to_string());
|
||||
} else if cmd_opts.infile.ends_with(".blt") {
|
||||
cmd_opts.r#in = Some("blt".to_string());
|
||||
} else if cmd_opts.infile.ends_with(".csp") {
|
||||
cmd_opts.r#in = Some("csp".to_string());
|
||||
} else {
|
||||
println!("Error: --in not specified and format cannot be determined from input filename");
|
||||
return Err(1);
|
||||
}
|
||||
}
|
||||
if cmd_opts.out == None {
|
||||
if cmd_opts.outfile.ends_with(".bin") {
|
||||
cmd_opts.out = Some("bin".to_string());
|
||||
} else if cmd_opts.outfile.ends_with(".blt") {
|
||||
cmd_opts.out = Some("blt".to_string());
|
||||
} else if cmd_opts.outfile.ends_with(".csp") {
|
||||
cmd_opts.out = Some("csp".to_string());
|
||||
} else {
|
||||
println!("Error: --out not specified and format cannot be determined from output filename");
|
||||
return Err(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Read input file
|
||||
let mut election: Election<Rational>;
|
||||
|
||||
match cmd_opts.r#in.as_deref().unwrap() {
|
||||
"bin" => {
|
||||
election = parser::bin::parse_path(cmd_opts.infile);
|
||||
}
|
||||
"blt" => {
|
||||
match parser::blt::parse_path(cmd_opts.infile) {
|
||||
Ok(e) => {
|
||||
election = e;
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Syntax Error: {}", err);
|
||||
return Err(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
"csp" => {
|
||||
let file = File::open(cmd_opts.infile).expect("IO Error");
|
||||
election = parser::csp::parse_reader(file, cmd_opts.require_1, cmd_opts.require_sequential, cmd_opts.require_strict_order).expect("Syntax Error");
|
||||
}
|
||||
_ => unreachable!()
|
||||
};
|
||||
|
||||
match cmd_opts.seats {
|
||||
Some(seats) => {
|
||||
election.seats = seats;
|
||||
}
|
||||
None => {
|
||||
if election.seats == 0 {
|
||||
println!("Error: --seats must be specified with CSP input");
|
||||
return Err(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cmd_opts.omit_informal {
|
||||
// Remove wholly informal ballots from output
|
||||
election.ballots.retain(|b| !b.preferences.is_empty());
|
||||
}
|
||||
|
||||
// Write output file
|
||||
let output = File::create(cmd_opts.outfile).expect("IO Error");
|
||||
|
||||
match cmd_opts.out.as_deref().unwrap() {
|
||||
"bin" => {
|
||||
writer::bin::write(election, output);
|
||||
}
|
||||
"blt" => {
|
||||
writer::blt::write(election, output);
|
||||
}
|
||||
"csp" => {
|
||||
writer::csp::write(election, output);
|
||||
}
|
||||
_ => unreachable!()
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/// Convert between different ballot data formats
|
||||
pub mod convert;
|
||||
/// Count a single transferable vote (STV) election
|
||||
pub mod stv;
|
789
src/cli/stv.rs
789
src/cli/stv.rs
|
@ -1,789 +0,0 @@
|
|||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021–2023 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::constraints::{self, Constraints};
|
||||
use crate::election::{CandidateState, CountState, Election, StageKind};
|
||||
use crate::numbers::{Fixed, GuardedFixed, NativeFloat64, Number, Rational};
|
||||
use crate::parser::{bin, blt};
|
||||
use crate::stv::{self, STVOptions};
|
||||
use crate::ties;
|
||||
|
||||
use clap::{AppSettings, Parser};
|
||||
use itertools::Itertools;
|
||||
|
||||
use std::cmp::max;
|
||||
use std::fs::File;
|
||||
use std::io::{self, BufRead};
|
||||
use std::ops;
|
||||
|
||||
/// Count a single transferable vote (STV) election
|
||||
#[derive(Parser)]
|
||||
#[clap(setting=AppSettings::DeriveDisplayOrder)]
|
||||
pub struct SubcmdOptions {
|
||||
// ----------------
|
||||
// -- File input --
|
||||
|
||||
/// Path to the BLT file to be counted
|
||||
#[clap(help_heading=Some("INPUT"))]
|
||||
filename: String,
|
||||
|
||||
/// Input is in serialised binary format from "opentally convert"
|
||||
#[clap(help_heading=Some("INPUT"), long)]
|
||||
bin: bool,
|
||||
|
||||
// ----------------------
|
||||
// -- Numbers settings --
|
||||
|
||||
/// Numbers mode
|
||||
#[clap(help_heading=Some("NUMBERS"), short, long, possible_values=&["rational", "fixed", "gfixed", "float64"], default_value="rational", value_name="mode")]
|
||||
numbers: String,
|
||||
|
||||
/// Decimal places if --numbers fixed
|
||||
#[clap(help_heading=Some("NUMBERS"), long, default_value="5", value_name="dps")]
|
||||
decimals: usize,
|
||||
|
||||
// -----------------------
|
||||
// -- Rounding settings --
|
||||
|
||||
/// Round surplus fractions to specified decimal places
|
||||
#[clap(help_heading=Some("ROUNDING"), long, alias="round-tvs", value_name="dps")]
|
||||
round_surplus_fractions: Option<usize>,
|
||||
|
||||
/// Round ballot values to specified decimal places
|
||||
#[clap(help_heading=Some("ROUNDING"), long, alias="round-weights", value_name="dps")]
|
||||
round_values: Option<usize>,
|
||||
|
||||
/// Round votes to specified decimal places
|
||||
#[clap(help_heading=Some("ROUNDING"), long, value_name="dps")]
|
||||
round_votes: Option<usize>,
|
||||
|
||||
/// Round quota to specified decimal places
|
||||
#[clap(help_heading=Some("ROUNDING"), long, value_name="dps")]
|
||||
round_quota: Option<usize>,
|
||||
|
||||
/// (Gregory STV) How to round subtransfers during surpluses/exclusions
|
||||
#[clap(help_heading=Some("ROUNDING"), long, possible_values=&["single_step", "by_value", "by_value_and_source", "by_parcel", "per_ballot"], default_value="single_step", value_name="mode")]
|
||||
round_subtransfers: String,
|
||||
|
||||
/// (Meek STV) Limit for stopping iteration of surplus distribution
|
||||
#[clap(help_heading=Some("ROUNDING"), long, default_value="0.001%", value_name="tolerance")]
|
||||
meek_surplus_tolerance: String,
|
||||
|
||||
// -----------
|
||||
// -- Quota --
|
||||
|
||||
/// Quota type
|
||||
#[clap(help_heading=Some("QUOTA"), short, long, possible_values=&["droop", "hare", "droop_exact", "hare_exact"], default_value="droop")]
|
||||
quota: String,
|
||||
|
||||
/// Whether to elect candidates on meeting (geq) or strictly exceeding (gt) the quota
|
||||
#[clap(help_heading=Some("QUOTA"), short='c', long, possible_values=&["geq", "gt"], default_value="gt", value_name="criterion")]
|
||||
quota_criterion: String,
|
||||
|
||||
/// Whether to apply a form of progressive quota
|
||||
#[clap(help_heading=Some("QUOTA"), long, possible_values=&["static", "ers97", "ers76", "dynamic_by_total", "dynamic_by_active"], default_value="static", value_name="mode")]
|
||||
quota_mode: String,
|
||||
|
||||
// ------------------
|
||||
// -- STV variants --
|
||||
|
||||
/// Tie-breaking method
|
||||
#[clap(help_heading=Some("STV VARIANTS"), short='t', long, multiple_values=true, possible_values=&["forwards", "backwards", "random", "prompt"], default_value="prompt", value_name="methods")]
|
||||
ties: Vec<String>,
|
||||
|
||||
/// Random seed to use with --ties random
|
||||
#[clap(help_heading=Some("STV VARIANTS"), long, value_name="seed")]
|
||||
random_seed: Option<String>,
|
||||
|
||||
/// Method of surplus distributions [default: wig] [possible values: wig, uig, eg, meek, ihare, hare]
|
||||
#[clap(help_heading=Some("STV VARIANTS"), short='s', long, possible_values=&["wig", "uig", "eg", "meek", "ihare", "hare", "eh"], default_value="wig", value_name="method", hide_possible_values=true, hide_default_value=true)]
|
||||
surplus: String,
|
||||
|
||||
/// (Gregory STV) Order to distribute surpluses
|
||||
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["by_size", "by_order"], default_value="by_size", value_name="order")]
|
||||
surplus_order: String,
|
||||
|
||||
/// (Gregory STV) Examine only transferable papers during surplus distributions
|
||||
#[clap(help_heading=Some("STV VARIANTS"), long)]
|
||||
transferable_only: bool,
|
||||
|
||||
/// (Gregory STV) When calculating surplus fractions, assume the progress total is the total value of all the candidate's papers
|
||||
#[clap(help_heading=Some("STV VARIANTS"), long)]
|
||||
surplus_assume_total: bool,
|
||||
|
||||
/// (Gregory STV) Method of exclusions [default: single_stage] [possible values: single_stage, by_value, by_source, parcels_by_order, reset_and_reiterate]
|
||||
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["single_stage", "by_value", "first_prefs_then_by_value", "by_source", "parcels_by_order", "wright", "reset_and_reiterate"], default_value="single_stage", value_name="method", hide_possible_values=true, hide_default_value=true)]
|
||||
exclusion: String,
|
||||
|
||||
/// (Meek STV) NZ Meek STV behaviour: Iterate keep values one round before candidate exclusion
|
||||
#[clap(help_heading=Some("STV VARIANTS"), long)]
|
||||
meek_nz_exclusion: bool,
|
||||
|
||||
/// (Hare) Method of drawing a sample [default: stratify] [possible values: stratify, by_order, cincinnati]
|
||||
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["stratify", "stratify_lr", "by_order", "cincinnati", "nth_ballot"], default_value="stratify", value_name="method", hide_possible_values=true, hide_default_value=true)]
|
||||
sample: String,
|
||||
|
||||
/// (Hare) Sample-based methods: Check for candidate election after each individual ballot paper transfer
|
||||
#[clap(help_heading=Some("STV VARIANTS"), long)]
|
||||
sample_per_ballot: bool,
|
||||
|
||||
// -------------------------
|
||||
// -- Count optimisations --
|
||||
|
||||
/// Continue count even if continuing candidates fill all remaining vacancies
|
||||
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)]
|
||||
no_early_bulk_elect: bool,
|
||||
|
||||
/// Use bulk exclusion
|
||||
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)]
|
||||
bulk_exclude: bool,
|
||||
|
||||
/// Defer surplus distributions if possible
|
||||
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)]
|
||||
defer_surpluses: bool,
|
||||
|
||||
/// Elect candidates only when their surpluses are distributed; (Meek STV) Wait for keep values to converge before electing candidates
|
||||
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)]
|
||||
no_immediate_elect: bool,
|
||||
|
||||
/// On exclusion, exclude any candidate with fewer than this many votes
|
||||
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long, default_value="0", value_name="votes")]
|
||||
min_threshold: String,
|
||||
|
||||
// -----------------
|
||||
// -- Constraints --
|
||||
|
||||
/// Path to a CON file specifying constraints
|
||||
#[clap(help_heading=Some("CONSTRAINTS"), long)]
|
||||
constraints: Option<String>,
|
||||
|
||||
/// Mode of handling constraints
|
||||
#[clap(help_heading=Some("CONSTRAINTS"), long, possible_values=&["guard_doom", "repeat_count"], default_value="guard_doom")]
|
||||
constraint_mode: String,
|
||||
|
||||
// ---------------------
|
||||
// -- Output settings --
|
||||
|
||||
/// Output format
|
||||
#[clap(help_heading=Some("OUTPUT"), short, long, possible_values=&["text", "csv", "html"], default_value="text")]
|
||||
output: String,
|
||||
|
||||
/// Hide excluded candidates from results report
|
||||
#[clap(help_heading=Some("OUTPUT"), long)]
|
||||
hide_excluded: bool,
|
||||
|
||||
/// Sort candidates by votes in results report
|
||||
#[clap(help_heading=Some("OUTPUT"), long)]
|
||||
sort_votes: bool,
|
||||
|
||||
/// Show details of transfers to candidates during surplus distributions/candidate exclusions
|
||||
#[clap(help_heading=Some("OUTPUT"), long)]
|
||||
transfers_detail: bool,
|
||||
|
||||
/// Print votes to specified decimal places in results report
|
||||
#[clap(help_heading=Some("OUTPUT"), long, default_value="2", value_name="dps")]
|
||||
pp_decimals: usize,
|
||||
|
||||
/// (HTML) Report style
|
||||
#[clap(help_heading=Some("OUTPUT"), long, possible_values=&["votes", "votes_transposed", "ballots_votes"], default_value="votes_transposed")]
|
||||
report_style: String,
|
||||
}
|
||||
|
||||
/// Entrypoint for subcommand
|
||||
pub fn main(cmd_opts: SubcmdOptions) -> Result<(), i32> {
|
||||
// Read and count election according to --numbers
|
||||
if cmd_opts.numbers == "rational" {
|
||||
let mut election = election_from_file(&cmd_opts.filename, cmd_opts.bin)?;
|
||||
maybe_load_constraints(&mut election, &cmd_opts.constraints, &cmd_opts.constraint_mode)?;
|
||||
|
||||
// Must specify ::<N> here and in a few other places because ndarray causes E0275 otherwise
|
||||
count_election::<Rational>(election, cmd_opts)?;
|
||||
} else if cmd_opts.numbers == "float64" {
|
||||
let mut election = election_from_file(&cmd_opts.filename, cmd_opts.bin)?;
|
||||
maybe_load_constraints(&mut election, &cmd_opts.constraints, &cmd_opts.constraint_mode)?;
|
||||
count_election::<NativeFloat64>(election, cmd_opts)?;
|
||||
} else if cmd_opts.numbers == "fixed" {
|
||||
Fixed::set_dps(cmd_opts.decimals);
|
||||
|
||||
let mut election = election_from_file(&cmd_opts.filename, cmd_opts.bin)?;
|
||||
maybe_load_constraints(&mut election, &cmd_opts.constraints, &cmd_opts.constraint_mode)?;
|
||||
count_election::<Fixed>(election, cmd_opts)?;
|
||||
} else if cmd_opts.numbers == "gfixed" {
|
||||
GuardedFixed::set_dps(cmd_opts.decimals);
|
||||
|
||||
let mut election = election_from_file(&cmd_opts.filename, cmd_opts.bin)?;
|
||||
maybe_load_constraints(&mut election, &cmd_opts.constraints, &cmd_opts.constraint_mode)?;
|
||||
count_election::<GuardedFixed>(election, cmd_opts)?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
fn election_from_file<N: Number>(path: &str, bin: bool) -> Result<Election<N>, i32> {
|
||||
if bin {
|
||||
// BIN format
|
||||
return Ok(bin::parse_path(path));
|
||||
} else {
|
||||
// BLT format
|
||||
match blt::parse_path(path) {
|
||||
Ok(e) => return Ok(e),
|
||||
Err(err) => {
|
||||
println!("Syntax Error: {}", err);
|
||||
return Err(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_load_constraints<N: Number>(election: &mut Election<N>, constraints: &Option<String>, constraint_mode: &str) -> Result<(), i32> {
|
||||
if let Some(c) = constraints {
|
||||
let file = File::open(c).expect("IO Error");
|
||||
let lines = io::BufReader::new(file).lines();
|
||||
let lines: Vec<_> = lines.map(|r| r.expect("IO Error")).collect();
|
||||
|
||||
match Constraints::from_con(lines.into_iter()) {
|
||||
Ok(c) => {
|
||||
election.constraints = Some(c);
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Constraint Syntax Error: {}", err);
|
||||
return Err(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate constraints
|
||||
if let Err(err) = election.constraints.as_ref().unwrap().validate_constraints(election.candidates.len(), constraint_mode.into()) {
|
||||
println!("Constraint Validation Error: {}", err);
|
||||
return Err(1);
|
||||
}
|
||||
|
||||
if constraint_mode == "repeat_count" {
|
||||
constraints::init_repeat_count(election);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn count_election<N: Number>(election: Election<N>, cmd_opts: SubcmdOptions) -> Result<(), i32>
|
||||
where
|
||||
for<'r> &'r N: ops::Add<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Neg<Output=N>
|
||||
{
|
||||
// Copy applicable options
|
||||
let opts = STVOptions::new(
|
||||
cmd_opts.round_surplus_fractions,
|
||||
cmd_opts.round_values,
|
||||
cmd_opts.round_votes,
|
||||
cmd_opts.round_quota,
|
||||
cmd_opts.round_subtransfers.into(),
|
||||
cmd_opts.meek_surplus_tolerance,
|
||||
cmd_opts.quota.into(),
|
||||
cmd_opts.quota_criterion.into(),
|
||||
cmd_opts.quota_mode.into(),
|
||||
ties::from_strs(cmd_opts.ties, cmd_opts.random_seed),
|
||||
cmd_opts.surplus.into(),
|
||||
cmd_opts.surplus_order.into(),
|
||||
cmd_opts.transferable_only,
|
||||
cmd_opts.surplus_assume_total,
|
||||
cmd_opts.exclusion.into(),
|
||||
cmd_opts.meek_nz_exclusion,
|
||||
cmd_opts.sample.into(),
|
||||
cmd_opts.sample_per_ballot,
|
||||
!cmd_opts.no_early_bulk_elect,
|
||||
cmd_opts.bulk_exclude,
|
||||
cmd_opts.defer_surpluses,
|
||||
!cmd_opts.no_immediate_elect,
|
||||
cmd_opts.min_threshold,
|
||||
cmd_opts.constraints,
|
||||
cmd_opts.constraint_mode.into(),
|
||||
cmd_opts.hide_excluded,
|
||||
cmd_opts.sort_votes,
|
||||
cmd_opts.transfers_detail,
|
||||
cmd_opts.pp_decimals,
|
||||
);
|
||||
|
||||
// Validate options
|
||||
match opts.validate() {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
println!("Error: {}", err.describe());
|
||||
return Err(1);
|
||||
}
|
||||
}
|
||||
|
||||
match cmd_opts.output.as_str() {
|
||||
"text" => { return count_election_text(election, &cmd_opts.filename, opts); }
|
||||
"csv" => { return count_election_csv(election, opts); }
|
||||
"html" => { return count_election_html(election, &cmd_opts.filename, opts, &cmd_opts.report_style); }
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------
|
||||
// CLI text report
|
||||
|
||||
fn count_election_text<N: Number>(mut election: Election<N>, filename: &str, opts: STVOptions) -> Result<(), i32>
|
||||
where
|
||||
for<'r> &'r N: ops::Add<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Neg<Output=N>
|
||||
{
|
||||
// Describe count
|
||||
// TODO: Can we precompute total_ballots?
|
||||
let total_ballots = election.ballots.iter().fold(N::new(), |mut acc, b| { acc += &b.orig_value; acc });
|
||||
print!("Count computed by OpenTally (revision {}). Read {:.0} ballots from \"{}\" for election \"{}\". There are {} candidates for {} vacancies. ", crate::VERSION, total_ballots, filename, election.name, election.candidates.iter().filter(|c| !c.is_dummy).count(), election.seats);
|
||||
let opts_str = opts.describe::<N>();
|
||||
if !opts_str.is_empty() {
|
||||
println!("Counting using options \"{}\".", opts_str);
|
||||
} else {
|
||||
println!("Counting using default options.");
|
||||
}
|
||||
println!();
|
||||
|
||||
stv::preprocess_election(&mut election, &opts);
|
||||
|
||||
// Initialise count state
|
||||
let mut state = CountState::new(&election);
|
||||
|
||||
// Distribute first preferences
|
||||
match stv::count_init(&mut state, &opts) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
println!("Error: {}", err.describe());
|
||||
return Err(1);
|
||||
}
|
||||
}
|
||||
|
||||
let mut stage_num = 1;
|
||||
print_stage(stage_num, &state, &opts);
|
||||
|
||||
loop {
|
||||
match stv::count_one_stage(&mut state, &opts) {
|
||||
Ok(is_done) => {
|
||||
if is_done {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error: {}", err.describe());
|
||||
return Err(1);
|
||||
}
|
||||
}
|
||||
|
||||
stage_num += 1;
|
||||
print_stage(stage_num, &state, &opts);
|
||||
}
|
||||
|
||||
println!("Count complete. The winning candidates are, in order of election:");
|
||||
|
||||
let mut winners = Vec::new();
|
||||
for (candidate, count_card) in state.candidates.iter() {
|
||||
if count_card.state == CandidateState::Elected {
|
||||
winners.push((candidate, count_card));
|
||||
}
|
||||
}
|
||||
winners.sort_unstable_by(|a, b| a.1.order_elected.cmp(&b.1.order_elected));
|
||||
|
||||
for (i, (winner, count_card)) in winners.into_iter().enumerate() {
|
||||
if let Some(kv) = &count_card.keep_value {
|
||||
println!("{}. {} (kv = {:.dps2$})", i + 1, winner.name, kv, dps2=max(opts.pp_decimals, 2));
|
||||
} else {
|
||||
println!("{}. {}", i + 1, winner.name);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
fn print_stage<N: Number>(stage_num: u32, state: &CountState<N>, opts: &STVOptions) {
|
||||
// Print stage details
|
||||
println!("{}. {}", stage_num, state.title);
|
||||
println!("{}", state.logger.render().join(" "));
|
||||
|
||||
if opts.transfers_detail {
|
||||
if let Some(tt) = &state.transfer_table {
|
||||
println!();
|
||||
println!("{}", tt.render_text(opts));
|
||||
}
|
||||
}
|
||||
|
||||
// Print candidates
|
||||
print!("{}", state.describe_candidates(opts));
|
||||
|
||||
// Print summary rows
|
||||
print!("{}", state.describe_summary(opts));
|
||||
|
||||
println!();
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
// Wichmann/eSTV/ERS-style CSV report
|
||||
|
||||
fn count_election_csv<N: Number>(mut election: Election<N>, opts: STVOptions) -> Result<(), i32>
|
||||
where
|
||||
for<'r> &'r N: ops::Add<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Neg<Output=N>
|
||||
{
|
||||
// Header rows
|
||||
let total_ballots = election.ballots.iter().fold(N::new(), |mut acc, b| { acc += &b.orig_value; acc });
|
||||
|
||||
// eSTV does not consistently quote records, so we won't use a CSV library here
|
||||
println!(r#""Election for","{}""#, election.name);
|
||||
println!(r#""Date"," / / ""#);
|
||||
println!(r#""Number to be elected",{}"#, election.seats);
|
||||
|
||||
stv::preprocess_election(&mut election, &opts);
|
||||
|
||||
// Initialise count state
|
||||
let mut state = CountState::new(&election);
|
||||
|
||||
let mut stage_results = vec![Vec::new(); election.candidates.len() + 5];
|
||||
|
||||
// -----------
|
||||
// First stage
|
||||
|
||||
// Distribute first preferences
|
||||
match stv::count_init(&mut state, &opts) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
println!("Error: {}", err.describe());
|
||||
return Err(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Subtract this from progressive NTs
|
||||
// TODO: May fail to round correctly with minivoters
|
||||
let invalid_votes = state.exhausted.votes.clone();
|
||||
let valid_votes = total_ballots - &invalid_votes;
|
||||
|
||||
// Stage number row
|
||||
stage_results[0].push(String::new());
|
||||
stage_results[0].push(String::new());
|
||||
|
||||
// Stage kind row
|
||||
stage_results[1].push(String::new());
|
||||
stage_results[1].push(String::from(r#""First""#));
|
||||
|
||||
// Stage title row
|
||||
stage_results[2].push(String::from(r#""Candidates""#));
|
||||
stage_results[2].push(String::from(r#""Preferences""#));
|
||||
|
||||
for (i, candidate) in election.candidates.iter().enumerate() {
|
||||
let count_card = &state.candidates[candidate];
|
||||
stage_results[3 + i].push(format!(r#""{}""#, candidate.name));
|
||||
stage_results[3 + i].push(format!(r#"{:.0}"#, count_card.votes)); // TODO: May fail to round correctly with minivoters
|
||||
}
|
||||
|
||||
stage_results[3 + election.candidates.len()].push(String::from(r#""Non-transferable""#));
|
||||
stage_results[3 + election.candidates.len()].push(String::new()); // TODO: May fail to round correctly with minivoters
|
||||
|
||||
stage_results[4 + election.candidates.len()].push(String::from(r#""Totals""#));
|
||||
stage_results[4 + election.candidates.len()].push(format!(r#"{:.0}"#, valid_votes));
|
||||
|
||||
//let mut orig_states = HashMap::new();
|
||||
//for (candidate, count_card) in state.candidates.iter() {
|
||||
// orig_states.insert(*candidate, count_card.state);
|
||||
//}
|
||||
|
||||
// -----------------
|
||||
// Subsequent stages
|
||||
|
||||
let mut stage_num: u32 = 1;
|
||||
loop {
|
||||
match stv::count_one_stage(&mut state, &opts) {
|
||||
Ok(is_done) => {
|
||||
if is_done {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error: {}", err.describe());
|
||||
return Err(1);
|
||||
}
|
||||
}
|
||||
|
||||
stage_num += 1;
|
||||
|
||||
// Stage number row
|
||||
stage_results[0].push(String::from(r#""Stage""#));
|
||||
stage_results[0].push(format!(r#"{}"#, stage_num));
|
||||
|
||||
// Stage kind row
|
||||
stage_results[1].push(format!(r#""{}""#, state.title.kind_as_string()));
|
||||
stage_results[1].push(String::new());
|
||||
|
||||
// Stage title row
|
||||
match &state.title {
|
||||
StageKind::FirstPreferences => unreachable!(),
|
||||
StageKind::SurplusOf(candidate) => {
|
||||
stage_results[2].push(format!(r#""{}""#, candidate.name));
|
||||
}
|
||||
StageKind::ExclusionOf(candidates) => {
|
||||
stage_results[2].push(format!(r#""{}""#, candidates.iter().map(|c| &c.name).sorted().join("+")));
|
||||
}
|
||||
StageKind::Rollback => todo!(),
|
||||
StageKind::RollbackExhausted => todo!(),
|
||||
StageKind::BallotsOf(candidate) => {
|
||||
stage_results[2].push(format!(r#""{}""#, candidate.name));
|
||||
}
|
||||
StageKind::SurplusesDistributed => todo!(),
|
||||
StageKind::BulkElection => {
|
||||
//let mut elected_candidates = Vec::new();
|
||||
//for candidate in election.candidates.iter() {
|
||||
// if state.candidates[candidate].state == CandidateState::Hopeful && orig_states[candidate].state != CandidateState::Hopeful {
|
||||
// elected_candidates.push(candidate);
|
||||
// }
|
||||
//}
|
||||
stage_results[2].push(String::from(r#""Bulk election""#));
|
||||
}
|
||||
}
|
||||
stage_results[2].push(String::from(r#""#));
|
||||
|
||||
for (i, candidate) in election.candidates.iter().enumerate() {
|
||||
let count_card = &state.candidates[candidate];
|
||||
|
||||
if count_card.transfers.is_zero() {
|
||||
stage_results[3 + i].push(String::new());
|
||||
} else if count_card.transfers > N::zero() {
|
||||
stage_results[3 + i].push(format!(r#"+{:.dps$}"#, count_card.transfers, dps=opts.pp_decimals));
|
||||
} else {
|
||||
stage_results[3 + i].push(format!(r#"{:.dps$}"#, count_card.transfers, dps=opts.pp_decimals));
|
||||
}
|
||||
|
||||
if count_card.votes.is_zero() {
|
||||
stage_results[3 + i].push(String::from(r#""-""#));
|
||||
} else {
|
||||
stage_results[3 + i].push(format!(r#"{:.dps$}"#, count_card.votes, dps=opts.pp_decimals));
|
||||
}
|
||||
}
|
||||
|
||||
// Nontransferable
|
||||
let nt_transfers = state.exhausted.transfers.clone() + &state.loss_fraction.transfers;
|
||||
if nt_transfers.is_zero() {
|
||||
stage_results[3 + election.candidates.len()].push(String::new());
|
||||
} else if nt_transfers > N::zero() {
|
||||
stage_results[3 + election.candidates.len()].push(format!(r#"+{:.dps$}"#, nt_transfers, dps=opts.pp_decimals));
|
||||
} else {
|
||||
stage_results[3 + election.candidates.len()].push(format!(r#"{:.dps$}"#, nt_transfers, dps=opts.pp_decimals));
|
||||
}
|
||||
stage_results[3 + election.candidates.len()].push(format!(r#"{:.dps$}"#, &state.exhausted.votes + &state.loss_fraction.votes - &invalid_votes, dps=opts.pp_decimals));
|
||||
|
||||
// Totals
|
||||
stage_results[4 + election.candidates.len()].push(String::new());
|
||||
stage_results[4 + election.candidates.len()].push(format!(r#"{:.dps$}"#, valid_votes, dps=opts.pp_decimals));
|
||||
|
||||
//for (candidate, count_card) in state.candidates.iter() {
|
||||
// orig_states.insert(*candidate, count_card.state);
|
||||
//}
|
||||
}
|
||||
|
||||
// ----------------
|
||||
// Candidate states
|
||||
|
||||
stage_results[3 + election.candidates.len()].push(String::new()); // Nontransferable row
|
||||
|
||||
for (i, candidate) in election.candidates.iter().enumerate() {
|
||||
let count_card = &state.candidates[candidate];
|
||||
if count_card.state == CandidateState::Elected {
|
||||
stage_results[3 + i].push(String::from(r#""Elected""#));
|
||||
} else {
|
||||
stage_results[3 + i].push(String::new());
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------
|
||||
// Output stages to CSV
|
||||
|
||||
println!(r#""Valid votes",{:.0}"#, valid_votes);
|
||||
println!(r#""Invalid votes",{:.0}"#, invalid_votes);
|
||||
println!(r#""Quota",{:.dps$}"#, state.quota.as_ref().unwrap(), dps=opts.pp_decimals);
|
||||
println!(r#""OpenTally","{}""#, crate::VERSION);
|
||||
println!(r#""Election rules","{}""#, opts.describe::<N>());
|
||||
|
||||
for row in stage_results {
|
||||
println!("{}", row.join(","));
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// HTML report in the style of wasm UI
|
||||
|
||||
fn count_election_html<N: Number>(mut election: Election<N>, filename: &str, opts: STVOptions, report_style: &str) -> Result<(), i32>
|
||||
where
|
||||
for<'r> &'r N: ops::Add<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Neg<Output=N>
|
||||
{
|
||||
// HTML preamble, etc.
|
||||
// TODO: Make this/URLs not hardcoded
|
||||
print!(r#"<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>OpenTally Report</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css" integrity="sha512-NhSC1YmyruXifcj/KFRWoC561YpHpc5Jtzgvbuzx5VozKpWvQ+4nXhPdFgmx8xqexRcpAglTj9sIBWINXa8x5w==" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" type="text/css" href="https://yingtongli.me/opentally/stv/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="divUI">
|
||||
<div id="resultsDiv">
|
||||
<div id="resultLogs1" style="white-space: pre-wrap;">"#);
|
||||
|
||||
// Describe count
|
||||
println!(r#"{}</div>"#, stv::html::describe_count(filename, &election, &opts));
|
||||
|
||||
stv::preprocess_election(&mut election, &opts);
|
||||
|
||||
// Initialise count state
|
||||
let mut state = CountState::new(&election);
|
||||
|
||||
// TODO: Enable report_style to be customised
|
||||
let mut result_rows = stv::html::init_results_table(&election, &opts, report_style);
|
||||
|
||||
let mut stage_comments = Vec::new();
|
||||
|
||||
// -----------
|
||||
// First stage
|
||||
|
||||
// Distribute first preferences
|
||||
match stv::count_init(&mut state, &opts) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
println!("Error: {}", err.describe());
|
||||
return Err(1);
|
||||
}
|
||||
}
|
||||
|
||||
let stage_result = stv::html::update_results_table(1, &state, &opts, report_style);
|
||||
for (row, cell) in stage_result.into_iter().enumerate() {
|
||||
// 5 characters from end to insert before "</tr>"
|
||||
let idx = result_rows[row].len() - 5;
|
||||
result_rows[row].insert_str(idx, &cell);
|
||||
}
|
||||
|
||||
stage_comments.push(state.logger.render().join(" "));
|
||||
|
||||
// -----------------
|
||||
// Subsequent stages
|
||||
|
||||
let mut stage_num = 1;
|
||||
loop {
|
||||
match stv::count_one_stage(&mut state, &opts) {
|
||||
Ok(is_done) => {
|
||||
if is_done {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error: {}", err.describe());
|
||||
return Err(1);
|
||||
}
|
||||
}
|
||||
|
||||
stage_num += 1;
|
||||
|
||||
let stage_result = stv::html::update_results_table(stage_num, &state, &opts, report_style);
|
||||
for (row, cell) in stage_result.into_iter().enumerate() {
|
||||
// 5 characters from end to insert before "</tr>"
|
||||
let idx = result_rows[row].len() - 5;
|
||||
result_rows[row].insert_str(idx, &cell);
|
||||
}
|
||||
|
||||
stage_comments.push(state.logger.render().join(" "));
|
||||
}
|
||||
|
||||
// ----------------
|
||||
// Candidate states
|
||||
|
||||
for (row, cell) in stv::html::finalise_results_table(&state, report_style).into_iter().enumerate() {
|
||||
// 5 characters from end to insert before "</tr>"
|
||||
let idx = result_rows[row].len() - 5;
|
||||
result_rows[row].insert_str(idx, &cell);
|
||||
}
|
||||
|
||||
// --------------------
|
||||
// Output table to HTML
|
||||
|
||||
println!(r#"<table id="result" class="result">"#);
|
||||
for row in result_rows {
|
||||
println!("{}", row);
|
||||
}
|
||||
println!("</table>");
|
||||
|
||||
// --------------------
|
||||
// Print stage comments
|
||||
|
||||
println!(r#"<div id="resultLogs2"><p>Stage comments:</p><ol id="olStageComments">"#);
|
||||
for comment in stage_comments {
|
||||
println!("<li>{}</li>", comment);
|
||||
}
|
||||
println!("</ol>");
|
||||
|
||||
// -------------
|
||||
// Print summary
|
||||
|
||||
println!("<p>Count complete. The winning candidates are, in order of election:</p><ol>");
|
||||
|
||||
let mut winners = Vec::new();
|
||||
for (candidate, count_card) in state.candidates.iter() {
|
||||
if count_card.state == CandidateState::Elected {
|
||||
winners.push((candidate, count_card));
|
||||
}
|
||||
}
|
||||
winners.sort_unstable_by(|a, b| a.1.order_elected.cmp(&b.1.order_elected));
|
||||
|
||||
for (_i, (winner, count_card)) in winners.into_iter().enumerate() {
|
||||
if let Some(kv) = &count_card.keep_value {
|
||||
println!("<li>{} (kv = {:.dps2$})</li>", winner.name, kv, dps2=max(opts.pp_decimals, 2));
|
||||
} else {
|
||||
println!("<li>{}</li>", winner.name);
|
||||
}
|
||||
}
|
||||
|
||||
println!(r#"</ol></div></div>
|
||||
<div id="printPane">
|
||||
<button onclick="printResult()">Print result</button>
|
||||
<label>
|
||||
Paper size:
|
||||
<select id="selPaperSize">
|
||||
<option value="A4" selected>A4</option>
|
||||
<option value="A3">A3</option>
|
||||
<option value="letter">US Letter</option>
|
||||
</select>
|
||||
(Landscape)
|
||||
</label>
|
||||
</div></div>
|
||||
<div id="printWarning">Printing directly from this page is not supported. Use the ‘Print result’ button to generate a printer-friendly report.</div>
|
||||
<script src="https://yingtongli.me/opentally/stv/print.js"></script>
|
||||
<input type="hidden" id="selReport" value="{}">
|
||||
</body></html>"#, report_style);
|
||||
|
||||
return Ok(());
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021–2022 Lee Yingtong Li (RunasSudo)
|
||||
* 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
|
||||
|
@ -15,55 +15,81 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::candmap::CandidateMap;
|
||||
use crate::election::{Candidate, CandidateState, CountCard, CountState, Election, StageKind, RollbackState};
|
||||
use crate::election::{Candidate, CandidateState, CountCard, CountState, Election};
|
||||
use crate::numbers::Number;
|
||||
use crate::stv::{self, gregory, sample, ConstraintMode, STVError, STVOptions, SurplusMethod, SurplusOrder};
|
||||
use crate::ties::{self, TieStrategy};
|
||||
use crate::stv::{ConstraintMode, STVOptions};
|
||||
|
||||
use itertools::Itertools;
|
||||
use ndarray::{Array, Dimension, IxDyn};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use rkyv::{Archive, Deserialize, Serialize};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::num::ParseIntError;
|
||||
use std::ops;
|
||||
|
||||
/// Constraints for an [crate::election::Election]
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))]
|
||||
#[derive(Debug)]
|
||||
pub struct Constraints(pub Vec<Constraint>);
|
||||
|
||||
impl Constraints {
|
||||
/// Parse the given CON file and return a [Constraints]
|
||||
pub fn from_con<S: AsRef<str>, I: Iterator<Item=S>>(lines: I) -> Result<Self, ParseError> {
|
||||
pub fn from_con<I: Iterator<Item=String>>(lines: I) -> Self {
|
||||
let mut constraints = Constraints(Vec::new());
|
||||
|
||||
for (line_no, line) in lines.enumerate() {
|
||||
let mut bits = line.as_ref().split(' ').peekable();
|
||||
for line in lines {
|
||||
let mut bits = line.split(" ").peekable();
|
||||
|
||||
// Read constraint category and group
|
||||
let constraint_name = read_quoted_string(line_no, &mut bits)?;
|
||||
let group_name = read_quoted_string(line_no, &mut bits)?;
|
||||
// Read constraint category
|
||||
let mut constraint_name = String::new();
|
||||
let x = bits.next().expect("Syntax Error");
|
||||
if x.starts_with('"') {
|
||||
if x.ends_with('"') {
|
||||
constraint_name.push_str(&x[1..x.len()-1]);
|
||||
} else {
|
||||
constraint_name.push_str(&x[1..]);
|
||||
while !bits.peek().expect("Syntax Error").ends_with('"') {
|
||||
constraint_name.push_str(" ");
|
||||
constraint_name.push_str(bits.next().unwrap());
|
||||
}
|
||||
let x = bits.next().unwrap();
|
||||
constraint_name.push_str(" ");
|
||||
constraint_name.push_str(&x[..x.len()-1]);
|
||||
}
|
||||
} else {
|
||||
constraint_name.push_str(x);
|
||||
}
|
||||
|
||||
// Read constraint group
|
||||
let mut group_name = String::new();
|
||||
let x = bits.next().expect("Syntax Error");
|
||||
if x.starts_with('"') {
|
||||
if x.ends_with('"') {
|
||||
group_name.push_str(&x[1..x.len()-1]);
|
||||
} else {
|
||||
group_name.push_str(&x[1..]);
|
||||
while !bits.peek().expect("Syntax Error").ends_with('"') {
|
||||
group_name.push_str(" ");
|
||||
group_name.push_str(bits.next().unwrap());
|
||||
}
|
||||
let x = bits.next().unwrap();
|
||||
group_name.push_str(" ");
|
||||
group_name.push_str(&x[..x.len()-1]);
|
||||
}
|
||||
} else {
|
||||
group_name.push_str(x);
|
||||
}
|
||||
|
||||
// Read min, max
|
||||
let min: usize = bits
|
||||
.next().ok_or(ParseError::UnexpectedEOL(line_no, "minimum number"))?
|
||||
.parse().map_err(|e| ParseError::InvalidNumber(line_no, e))?;
|
||||
let max: usize = bits
|
||||
.next().ok_or(ParseError::UnexpectedEOL(line_no, "maximum number"))?
|
||||
.parse().map_err(|e| ParseError::InvalidNumber(line_no, e))?;
|
||||
let min: usize = bits.next().expect("Syntax Error").parse().expect("Syntax Error");
|
||||
let max: usize = bits.next().expect("Syntax Error").parse().expect("Syntax Error");
|
||||
|
||||
// Read candidates
|
||||
let mut candidates: Vec<usize> = Vec::new();
|
||||
for x in bits {
|
||||
candidates.push(x.parse::<usize>().map_err(|e| ParseError::InvalidNumber(line_no, e))? - 1);
|
||||
candidates.push(x.parse::<usize>().expect("Syntax Error") - 1);
|
||||
}
|
||||
|
||||
// Insert constraint/group
|
||||
let constraint = match constraints.0.iter_mut().find(|c| c.name == constraint_name) {
|
||||
let constraint = match constraints.0.iter_mut().filter(|c| c.name == constraint_name).next() {
|
||||
Some(c) => { c }
|
||||
None => {
|
||||
let c = Constraint {
|
||||
|
@ -76,201 +102,25 @@ impl Constraints {
|
|||
};
|
||||
|
||||
if constraint.groups.iter().any(|g| g.name == group_name) {
|
||||
return Err(ParseError::DuplicateGroup(line_no, group_name, constraint.name.clone()));
|
||||
panic!("Duplicate group \"{}\" in constraint \"{}\"", group_name, constraint.name);
|
||||
}
|
||||
|
||||
constraint.groups.push(ConstrainedGroup {
|
||||
name: group_name,
|
||||
candidates,
|
||||
min,
|
||||
max,
|
||||
candidates: candidates,
|
||||
min: min,
|
||||
max: max,
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(constraints);
|
||||
}
|
||||
|
||||
/// Validate that each candidate is specified exactly once in each constraint, and (if applicable) limitations of the constraint mode are applied
|
||||
pub fn validate_constraints(&self, num_candidates: usize, constraint_mode: ConstraintMode) -> Result<(), ValidationError> {
|
||||
for constraint in &self.0 {
|
||||
let mut remaining_candidates: Vec<usize> = (0..num_candidates).collect();
|
||||
|
||||
for group in &constraint.groups {
|
||||
for candidate in &group.candidates {
|
||||
match remaining_candidates.iter().position(|c| c == candidate) {
|
||||
Some(idx) => {
|
||||
remaining_candidates.remove(idx);
|
||||
}
|
||||
None => {
|
||||
return Err(ValidationError::DuplicateCandidate(*candidate, constraint.name.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if constraint_mode == ConstraintMode::RepeatCount {
|
||||
// Each group must be either a maximum constraint, or the remaining group
|
||||
if group.min == 0 {
|
||||
// Maximum constraint: OK
|
||||
} else if group.max >= group.candidates.len() {
|
||||
// Remaining group: OK
|
||||
} else {
|
||||
return Err(ValidationError::InvalidTwoStage(constraint.name.clone(), group.name.clone()));
|
||||
}
|
||||
|
||||
// FIXME: Is other validation required?
|
||||
}
|
||||
}
|
||||
|
||||
if !remaining_candidates.is_empty() {
|
||||
return Err(ValidationError::UnassignedCandidate(*remaining_candidates.first().unwrap(), constraint.name.clone()));
|
||||
}
|
||||
}
|
||||
// TODO: Validate constraints
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if any elected candidates exceed constrained maximums
|
||||
pub fn exceeds_maximum<'a, N: Number>(&self, election: &Election<N>, candidates: CandidateMap<CountCard<'a, N>>) -> Option<(&Constraint, &ConstrainedGroup)> {
|
||||
for constraint in &self.0 {
|
||||
for group in &constraint.groups {
|
||||
let mut num_elected = 0;
|
||||
for candidate in &group.candidates {
|
||||
if candidates[&election.candidates[*candidate]].state == CandidateState::Elected {
|
||||
num_elected += 1;
|
||||
}
|
||||
}
|
||||
if num_elected > group.max {
|
||||
return Some((&constraint, &group));
|
||||
}
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Error parsing constraints
|
||||
pub enum ParseError {
|
||||
/// Duplicate group in a constraint
|
||||
DuplicateGroup(usize, String, String),
|
||||
/// Unexpected EOL, expected ...
|
||||
UnexpectedEOL(usize, &'static str),
|
||||
/// Invalid number
|
||||
InvalidNumber(usize, ParseIntError),
|
||||
}
|
||||
|
||||
impl fmt::Display for ParseError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ParseError::DuplicateGroup(line_no, group_name, constraint_name) => {
|
||||
f.write_fmt(format_args!(r#"Line {}, duplicate group "{}" in constraint "{}""#, line_no, group_name, constraint_name))
|
||||
}
|
||||
ParseError::UnexpectedEOL(line_no, expected) => {
|
||||
f.write_fmt(format_args!(r#"Line {}, unexpected end-of-line, expected {}"#, line_no, expected))
|
||||
}
|
||||
ParseError::InvalidNumber(line_no, err) => {
|
||||
f.write_fmt(format_args!(r#"Line {}, invalid number: {}"#, line_no, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ParseError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
return fmt::Display::fmt(self, f);
|
||||
}
|
||||
}
|
||||
|
||||
/// Error validating constraints
|
||||
pub enum ValidationError {
|
||||
/// Duplicate candidate in a constraint
|
||||
DuplicateCandidate(usize, String),
|
||||
/// Unassigned candidate in a constraint
|
||||
UnassignedCandidate(usize, String),
|
||||
/// Constraint is incompatible with ConstraintMode::TwoStage
|
||||
InvalidTwoStage(String, String),
|
||||
}
|
||||
|
||||
impl fmt::Display for ValidationError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ValidationError::DuplicateCandidate(candidate, constraint_name) => {
|
||||
f.write_fmt(format_args!(r#"Duplicate candidate {} in constraint "{}""#, candidate + 1, constraint_name))
|
||||
}
|
||||
ValidationError::UnassignedCandidate(candidate, constraint_name) => {
|
||||
f.write_fmt(format_args!(r#"Unassigned candidate {} in constraint "{}""#, candidate + 1, constraint_name))
|
||||
}
|
||||
ValidationError::InvalidTwoStage(constraint_name, group_name) => {
|
||||
f.write_fmt(format_args!(r#"Constraint "{}" group "{}" is incompatible with --constraint-mode repeat_count"#, constraint_name, group_name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ValidationError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
return fmt::Display::fmt(self, f);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_contraint_group() {
|
||||
let input = r#""Constraint 1" "Group 1" 0 3 1 2 3
|
||||
"Constraint 1" "Group 1" 0 3 4 5 6"#;
|
||||
Constraints::from_con(input.lines()).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_candidate() {
|
||||
let input = r#""Constraint 1" "Group 1" 0 3 1 2 3 4
|
||||
"Constraint 1" "Group 2" 0 3 4 5 6"#;
|
||||
let constraints = Constraints::from_con(input.lines()).unwrap();
|
||||
constraints.validate_constraints(6, ConstraintMode::GuardDoom).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unassigned_candidate() {
|
||||
let input = r#""Constraint 1" "Group 1" 0 3 1 2 3
|
||||
"Constraint 1" "Group 2" 0 3 4 5 6"#;
|
||||
let constraints = Constraints::from_con(input.lines()).unwrap();
|
||||
constraints.validate_constraints(7, ConstraintMode::GuardDoom).unwrap_err();
|
||||
}
|
||||
|
||||
/// Read an optionally quoted string, returning the string without quotes
|
||||
fn read_quoted_string<'a, I: Iterator<Item=&'a str>>(line_no: usize, bits: &mut I) -> Result<String, ParseError> {
|
||||
let x = bits.next().ok_or(ParseError::UnexpectedEOL(line_no, "string continuation"))?;
|
||||
if let Some(x1) = x.strip_prefix('"') {
|
||||
if let Some(x2) = x.strip_suffix('"') {
|
||||
// Complete string
|
||||
return Ok(String::from(x2));
|
||||
} else {
|
||||
// Incomplete string
|
||||
let mut result = String::from(x1);
|
||||
|
||||
// Read until matching "
|
||||
loop {
|
||||
let x = bits.next().ok_or(ParseError::UnexpectedEOL(line_no, "string continuation"))?;
|
||||
result.push(' ');
|
||||
if let Some(x1) = x.strip_suffix('"') {
|
||||
// End of string
|
||||
result.push_str(x1);
|
||||
break;
|
||||
} else {
|
||||
// Middle of string
|
||||
result.push_str(x);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
} else {
|
||||
// Unquoted string
|
||||
return Ok(String::from(x));
|
||||
return constraints;
|
||||
}
|
||||
}
|
||||
|
||||
/// A single dimension of constraint
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))]
|
||||
#[derive(Debug)]
|
||||
pub struct Constraint {
|
||||
/// Name of this constraint
|
||||
pub name: String,
|
||||
|
@ -279,8 +129,7 @@ pub struct Constraint {
|
|||
}
|
||||
|
||||
/// A group of candidates, of which a certain minimum and maximum must be elected
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))]
|
||||
#[derive(Debug)]
|
||||
pub struct ConstrainedGroup {
|
||||
/// Name of this group
|
||||
pub name: String,
|
||||
|
@ -299,10 +148,6 @@ pub enum ConstraintError {
|
|||
NoConformantResult,
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// GUARD/DOOM CONSTRAINTS
|
||||
// ----------------------
|
||||
|
||||
/// Cell in a [ConstraintMatrix]
|
||||
#[derive(Clone)]
|
||||
pub struct ConstraintMatrixCell {
|
||||
|
@ -316,8 +161,7 @@ pub struct ConstraintMatrixCell {
|
|||
pub cands: usize,
|
||||
}
|
||||
|
||||
/// N-dimensional cube of [ConstraintMatrixCell]s representing the conformant combinations of elected candidates
|
||||
#[derive(Clone)]
|
||||
/// Hypercube/tensor of [ConstraintMatrixCell]s representing the conformant combinations of elected candidates
|
||||
pub struct ConstraintMatrix(pub Array<ConstraintMatrixCell, IxDyn>);
|
||||
|
||||
impl ConstraintMatrix {
|
||||
|
@ -360,7 +204,7 @@ impl ConstraintMatrix {
|
|||
}
|
||||
|
||||
/// Update cands/elected in innermost cells based on the provided [CountState::candidates](crate::election::CountState::candidates)
|
||||
pub fn update_from_state<N: Number>(&mut self, election: &Election<N>, candidates: &CandidateMap<CountCard<N>>) {
|
||||
pub fn update_from_state<N: Number>(&mut self, election: &Election<N>, candidates: &HashMap<&Candidate, CountCard<N>>) {
|
||||
let constraints = election.constraints.as_ref().unwrap();
|
||||
|
||||
// Reset innermost cells
|
||||
|
@ -373,18 +217,13 @@ impl ConstraintMatrix {
|
|||
}
|
||||
|
||||
for (i, candidate) in election.candidates.iter().enumerate() {
|
||||
if candidate.is_dummy {
|
||||
continue;
|
||||
}
|
||||
|
||||
let idx: Vec<usize> = constraints.0.iter().map(|c| {
|
||||
for (j, group) in c.groups.iter().enumerate() {
|
||||
if group.candidates.contains(&i) {
|
||||
return j + 1;
|
||||
}
|
||||
}
|
||||
// Should be caught by validate_constraints
|
||||
unreachable!("Candidate \"{}\" not represented in constraint \"{}\"", candidate.name, c.name);
|
||||
panic!("Candidate \"{}\" not represented in constraint \"{}\"", candidate.name, c.name);
|
||||
}).collect();
|
||||
let cell = &mut self[&idx[..]];
|
||||
|
||||
|
@ -418,7 +257,7 @@ impl ConstraintMatrix {
|
|||
self.0[&idx].elected = 0;
|
||||
|
||||
// The axis along which to sum - if multiple, just pick the first, as these should agree
|
||||
let zero_axis = (0..idx.ndim()).find(|d| idx[*d] == 0).unwrap();
|
||||
let zero_axis = (0..idx.ndim()).filter(|d| idx[*d] == 0).next().unwrap();
|
||||
|
||||
// Traverse along the axis and sum the candidates
|
||||
let mut idx2 = idx.clone();
|
||||
|
@ -528,87 +367,87 @@ impl fmt::Display for ConstraintMatrix {
|
|||
|
||||
// TODO: >2 dimensions
|
||||
if shape.len() == 1 {
|
||||
result.push('+');
|
||||
result.push_str("+");
|
||||
for _ in 0..shape[0] {
|
||||
result.push_str("-------------+");
|
||||
}
|
||||
result.push('\n');
|
||||
result.push_str("\n");
|
||||
|
||||
result.push('|');
|
||||
result.push_str("|");
|
||||
for x in 0..shape[0] {
|
||||
result.push_str(&format!(" Elected: {:2}", self[&[x]].elected));
|
||||
result.push_str(if x == 0 { " ‖" } else { " |" });
|
||||
}
|
||||
result.push('\n');
|
||||
result.push_str("\n");
|
||||
|
||||
result.push('|');
|
||||
result.push_str("|");
|
||||
for x in 0..shape[0] {
|
||||
result.push_str(&format!(" Min: {:2}", self[&[x]].min));
|
||||
result.push_str(if x == 0 { " ‖" } else { " |" });
|
||||
}
|
||||
result.push('\n');
|
||||
result.push_str("\n");
|
||||
|
||||
result.push('|');
|
||||
result.push_str("|");
|
||||
for x in 0..shape[0] {
|
||||
result.push_str(&format!(" Max: {:2}", self[&[x]].max));
|
||||
result.push_str(if x == 0 { " ‖" } else { " |" });
|
||||
}
|
||||
result.push('\n');
|
||||
result.push_str("\n");
|
||||
|
||||
result.push('|');
|
||||
result.push_str("|");
|
||||
for x in 0..shape[0] {
|
||||
result.push_str(&format!(" Cands: {:2}", self[&[x]].cands));
|
||||
result.push_str(if x == 0 { " ‖" } else { " |" });
|
||||
}
|
||||
result.push('\n');
|
||||
result.push_str("\n");
|
||||
|
||||
result.push('+');
|
||||
result.push_str("+");
|
||||
for _ in 0..shape[0] {
|
||||
result.push_str("-------------+");
|
||||
}
|
||||
result.push('\n');
|
||||
result.push_str("\n");
|
||||
} else if shape.len() == 2 {
|
||||
for y in 0..shape[1] {
|
||||
result.push('+');
|
||||
result.push_str("+");
|
||||
for _ in 0..shape[0] {
|
||||
result.push_str(if y == 1 { "=============+" } else { "-------------+" });
|
||||
}
|
||||
result.push('\n');
|
||||
result.push_str("\n");
|
||||
|
||||
result.push('|');
|
||||
result.push_str("|");
|
||||
for x in 0..shape[0] {
|
||||
result.push_str(&format!(" Elected: {:2}", self[&[x, y]].elected));
|
||||
result.push_str(if x == 0 { " ‖" } else { " |" });
|
||||
}
|
||||
result.push('\n');
|
||||
result.push_str("\n");
|
||||
|
||||
result.push('|');
|
||||
result.push_str("|");
|
||||
for x in 0..shape[0] {
|
||||
result.push_str(&format!(" Min: {:2}", self[&[x, y]].min));
|
||||
result.push_str(if x == 0 { " ‖" } else { " |" });
|
||||
}
|
||||
result.push('\n');
|
||||
result.push_str("\n");
|
||||
|
||||
result.push('|');
|
||||
result.push_str("|");
|
||||
for x in 0..shape[0] {
|
||||
result.push_str(&format!(" Max: {:2}", self[&[x, y]].max));
|
||||
result.push_str(if x == 0 { " ‖" } else { " |" });
|
||||
}
|
||||
result.push('\n');
|
||||
result.push_str("\n");
|
||||
|
||||
result.push('|');
|
||||
result.push_str("|");
|
||||
for x in 0..shape[0] {
|
||||
result.push_str(&format!(" Cands: {:2}", self[&[x, y]].cands));
|
||||
result.push_str(if x == 0 { " ‖" } else { " |" });
|
||||
}
|
||||
result.push('\n');
|
||||
result.push_str("\n");
|
||||
}
|
||||
|
||||
result.push('+');
|
||||
result.push_str("+");
|
||||
for _ in 0..shape[0] {
|
||||
result.push_str("-------------+");
|
||||
}
|
||||
result.push('\n');
|
||||
result.push_str("\n");
|
||||
} else {
|
||||
todo!();
|
||||
}
|
||||
|
@ -626,7 +465,7 @@ impl ops::IndexMut<&[usize]> for ConstraintMatrix {
|
|||
}
|
||||
|
||||
/// Return the [Candidate]s referred to in the given [ConstraintMatrixCell] at location `idx`
|
||||
fn candidates_in_constraint_cell<'a, N: Number>(election: &'a Election<N>, candidates: &CandidateMap<CountCard<N>>, idx: &[usize]) -> Vec<&'a Candidate> {
|
||||
fn candidates_in_constraint_cell<'a, N: Number>(election: &'a Election<N>, candidates: &HashMap<&Candidate, CountCard<N>>, idx: &[usize]) -> Vec<&'a Candidate> {
|
||||
let mut result: Vec<&Candidate> = Vec::new();
|
||||
for (i, candidate) in election.candidates.iter().enumerate() {
|
||||
let cc = &candidates[candidate];
|
||||
|
@ -652,28 +491,6 @@ fn candidates_in_constraint_cell<'a, N: Number>(election: &'a Election<N>, candi
|
|||
return result;
|
||||
}
|
||||
|
||||
/// Clone and update the constraints matrix, with the state of the given candidates set to candidate_state – check if a conformant result is possible
|
||||
pub fn test_constraints_any_time<N: Number>(state: &CountState<N>, candidates: &[&Candidate], candidate_state: CandidateState) -> Result<(), ConstraintError> {
|
||||
if state.constraint_matrix.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut cm = state.constraint_matrix.as_ref().unwrap().clone();
|
||||
|
||||
let mut trial_candidates = state.candidates.clone(); // TODO: Can probably be optimised by not cloning CountCard::parcels
|
||||
for candidate in candidates {
|
||||
trial_candidates.get_mut(candidate).unwrap().state = candidate_state;
|
||||
}
|
||||
|
||||
// Update cands/elected
|
||||
cm.update_from_state(state.election, &trial_candidates);
|
||||
cm.recount_cands();
|
||||
|
||||
// Iterate for stable state
|
||||
while !cm.step()? {}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
/// Update the constraints matrix, and perform the necessary actions given by [STVOptions::constraint_mode]
|
||||
pub fn update_constraints<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> bool {
|
||||
if state.constraint_matrix.is_none() {
|
||||
|
@ -682,16 +499,15 @@ pub fn update_constraints<N: Number>(state: &mut CountState<N>, opts: &STVOption
|
|||
let cm = state.constraint_matrix.as_mut().unwrap();
|
||||
|
||||
// Update cands/elected
|
||||
cm.update_from_state(state.election, &state.candidates);
|
||||
cm.update_from_state(&state.election, &state.candidates);
|
||||
cm.recount_cands();
|
||||
|
||||
// Iterate for stable state
|
||||
while !cm.step().expect("No conformant result is possible") {}
|
||||
|
||||
if state.num_elected == state.election.seats {
|
||||
// Election is complete, so skip guarding/dooming candidates
|
||||
return false;
|
||||
//println!("{}", cm);
|
||||
while !cm.step().expect("No conformant result is possible") {
|
||||
//println!("{}", cm);
|
||||
}
|
||||
//println!("{}", cm);
|
||||
|
||||
match opts.constraint_mode {
|
||||
ConstraintMode::GuardDoom => {
|
||||
|
@ -742,383 +558,10 @@ pub fn update_constraints<N: Number>(state: &mut CountState<N>, opts: &STVOption
|
|||
|
||||
return guarded_or_doomed;
|
||||
}
|
||||
ConstraintMode::RepeatCount => { return false; } // No action needed here: elect_hopefuls checks test_constraints_immediate
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
// FOR --constraint-mode repeat_count
|
||||
// ----------------------------------
|
||||
|
||||
/// Check constraints, with the state of the given candidates set to candidate_state – check if this immediately violates constraints
|
||||
pub fn test_constraints_immediate<'a, N: Number>(state: &CountState<'a, N>, candidates: &[&Candidate], candidate_state: CandidateState) -> Result<(), (&'a Constraint, &'a ConstrainedGroup)> {
|
||||
if state.election.constraints.is_none() {
|
||||
return Ok(());
|
||||
_ => { todo!() }
|
||||
}
|
||||
|
||||
let mut trial_candidates = state.candidates.clone(); // TODO: Can probably be optimised by not cloning CountCard::parcels
|
||||
for candidate in candidates {
|
||||
trial_candidates.get_mut(candidate).unwrap().state = candidate_state;
|
||||
}
|
||||
|
||||
if let Some((a, b)) = state.election.constraints.as_ref().unwrap().exceeds_maximum(state.election, trial_candidates) {
|
||||
return Err((a, b));
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
/// Initialise the [Election] as required for --constraint-mode repeat_count
|
||||
pub fn init_repeat_count<N: Number>(election: &mut Election<N>) {
|
||||
// Add dummy candidates
|
||||
let mut new_candidates = Vec::new();
|
||||
for candidate in &election.candidates {
|
||||
let mut new_candidate = candidate.clone();
|
||||
new_candidate.index += election.candidates.len(); // Ensure unique index
|
||||
new_candidate.is_dummy = true;
|
||||
new_candidates.push(new_candidate);
|
||||
}
|
||||
election.candidates.append(&mut new_candidates);
|
||||
}
|
||||
|
||||
/// Initialise the rollback for [ConstraintMode::RepeatCount]
|
||||
pub fn init_repeat_count_rollback<'a, N: Number>(state: &mut CountState<'a, N>, constraint: &'a Constraint, group: &'a ConstrainedGroup) {
|
||||
let mut rollback_candidates = CandidateMap::with_capacity(state.candidates.len());
|
||||
let rollback_exhausted = state.exhausted.clone();
|
||||
|
||||
// Copy ballot papers to rollback state
|
||||
for (candidate, count_card) in state.candidates.iter_mut() {
|
||||
rollback_candidates.insert(candidate, count_card.clone());
|
||||
}
|
||||
|
||||
state.rollback_state = RollbackState::NeedsRollback { candidates: Some(rollback_candidates), exhausted: Some(rollback_exhausted), constraint, group };
|
||||
}
|
||||
|
||||
/// Process one stage of rollback for [ConstraintMode::RepeatCount]
|
||||
pub fn rollback_one_stage<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<(), STVError>
|
||||
where
|
||||
for<'r> &'r N: ops::Add<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Neg<Output=N>
|
||||
{
|
||||
if let RollbackState::NeedsRollback { candidates, exhausted, constraint, group } = &mut state.rollback_state {
|
||||
let mut candidates = candidates.take().unwrap();
|
||||
|
||||
// Exclude candidates who cannot be elected due to constraint violations
|
||||
let order_excluded = state.num_excluded + 1;
|
||||
let mut excluded_candidates = Vec::new();
|
||||
for candidate_idx in &group.candidates {
|
||||
let count_card = state.candidates.get_mut(&state.election.candidates[*candidate_idx]).unwrap();
|
||||
if count_card.state == CandidateState::Hopeful {
|
||||
count_card.state = CandidateState::Excluded;
|
||||
count_card.finalised = true;
|
||||
state.num_excluded += 1;
|
||||
count_card.order_elected = -(order_excluded as isize);
|
||||
excluded_candidates.push(state.election.candidates[*candidate_idx].name.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare dummy candidates, etc.
|
||||
for candidate in &state.election.candidates {
|
||||
if candidate.is_dummy {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Move ballot papers to dummy candidate
|
||||
let dummy_candidate = state.election.candidates.iter().find(|c| c.name == candidate.name && c.is_dummy).unwrap();
|
||||
let dummy_count_card = state.candidates.get_mut(dummy_candidate).unwrap();
|
||||
dummy_count_card.parcels.append(&mut candidates.get_mut(candidate).unwrap().parcels);
|
||||
dummy_count_card.votes = candidates[candidate].votes.clone();
|
||||
|
||||
// Reset count
|
||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||
count_card.parcels.clear();
|
||||
count_card.votes = N::new();
|
||||
count_card.transfers = N::new();
|
||||
|
||||
if candidates[candidate].state == CandidateState::Elected {
|
||||
if &candidates[candidate].votes > state.quota.as_ref().unwrap() {
|
||||
count_card.votes = state.quota.as_ref().unwrap().clone();
|
||||
} else {
|
||||
count_card.votes = candidates[candidate].votes.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.title = StageKind::Rollback;
|
||||
state.logger.log_smart(
|
||||
"Rolled back to apply constraints. {} is excluded.",
|
||||
"Rolled back to apply constraints. {} are excluded.",
|
||||
excluded_candidates.into_iter().sorted().collect()
|
||||
);
|
||||
|
||||
state.rollback_state = RollbackState::RollingBack { candidates: Some(candidates), exhausted: exhausted.take(), candidate_distributing: None, constraint: Some(constraint), group: Some(group) };
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let RollbackState::RollingBack { candidates, exhausted, candidate_distributing, constraint, group } = &mut state.rollback_state {
|
||||
let candidates = candidates.take().unwrap();
|
||||
let mut exhausted = exhausted.take().unwrap();
|
||||
let mut candidate_distributing = candidate_distributing.take();
|
||||
let constraint = constraint.take().unwrap();
|
||||
let group = group.take().unwrap();
|
||||
|
||||
// --------------------
|
||||
// Distribute surpluses
|
||||
|
||||
let has_surplus: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of tie
|
||||
.filter(|c| {
|
||||
let cc = &candidates[c];
|
||||
if !c.is_dummy && cc.state == CandidateState::Elected && !cc.finalised {
|
||||
let dummy_candidate = state.election.candidates.iter().find(|x| x.name == c.name && x.is_dummy).unwrap();
|
||||
!state.candidates[dummy_candidate].finalised
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !has_surplus.is_empty() {
|
||||
// Distribute top candidate's surplus
|
||||
let max_cands = match opts.surplus_order {
|
||||
SurplusOrder::BySize => {
|
||||
ties::multiple_max_by(&has_surplus, |c| &candidates[c].votes)
|
||||
}
|
||||
SurplusOrder::ByOrder => {
|
||||
ties::multiple_min_by(&has_surplus, |c| candidates[c].order_elected)
|
||||
}
|
||||
};
|
||||
let elected_candidate = if max_cands.len() > 1 {
|
||||
stv::choose_highest(state, opts, &max_cands, "Which candidate's surplus to distribute?")?
|
||||
} else {
|
||||
max_cands[0]
|
||||
};
|
||||
|
||||
let dummy_candidate = state.election.candidates.iter().find(|c| c.name == elected_candidate.name && c.is_dummy).unwrap();
|
||||
|
||||
match opts.surplus {
|
||||
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => { gregory::distribute_surplus(state, opts, dummy_candidate); }
|
||||
SurplusMethod::IHare | SurplusMethod::Hare => { sample::distribute_surplus(state, opts, dummy_candidate)?; }
|
||||
_ => unreachable!()
|
||||
}
|
||||
|
||||
state.rollback_state = RollbackState::RollingBack { candidates: Some(candidates), exhausted: Some(exhausted), candidate_distributing, constraint: Some(constraint), group: Some(group) };
|
||||
rollback_check_complete(state);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
// Distribute exhausted ballot papers
|
||||
// FIXME: Untested!
|
||||
|
||||
if exhausted.parcels.iter().any(|p| !p.votes.is_empty()) {
|
||||
// Use arbitrary dummy candidate
|
||||
let dummy_candidate = state.election.candidates.iter().find(|c| c.is_dummy).unwrap();
|
||||
|
||||
let dummy_count_card = state.candidates.get_mut(dummy_candidate).unwrap();
|
||||
dummy_count_card.parcels.append(&mut exhausted.parcels);
|
||||
|
||||
// Nasty hack to check if continuing distribution!!
|
||||
if let StageKind::RollbackExhausted = state.title {
|
||||
// Continuing
|
||||
state.logger.log_literal(String::from("Continuing distribution of exhausted ballots."));
|
||||
} else {
|
||||
state.title = StageKind::RollbackExhausted;
|
||||
state.logger.log_literal(String::from("Distributing exhausted ballots."));
|
||||
}
|
||||
|
||||
stv::exclude_candidates(state, opts, vec![dummy_candidate], "Distribution")?;
|
||||
|
||||
let dummy_count_card = state.candidates.get_mut(dummy_candidate).unwrap();
|
||||
exhausted.parcels.append(&mut dummy_count_card.parcels);
|
||||
|
||||
state.rollback_state = RollbackState::RollingBack { candidates: Some(candidates), exhausted: Some(exhausted), candidate_distributing: None, constraint: Some(constraint), group: Some(group) };
|
||||
rollback_check_complete(state);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// ------------------------------------------
|
||||
// Distribute ballots of electable candidates
|
||||
|
||||
let electable_candidates_old: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of multiple
|
||||
.filter(|c| {
|
||||
let cc = &candidates[c];
|
||||
let cand_idx = state.election.candidates.iter().position(|x| x == *c).unwrap();
|
||||
if !c.is_dummy && !group.candidates.contains(&cand_idx) && !cc.finalised {
|
||||
let dummy_candidate = state.election.candidates.iter().find(|x| x.name == c.name && x.is_dummy).unwrap();
|
||||
!state.candidates[dummy_candidate].finalised
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !electable_candidates_old.is_empty() {
|
||||
if candidate_distributing.is_none() || !electable_candidates_old.contains(candidate_distributing.as_ref().unwrap()) {
|
||||
if electable_candidates_old.len() > 1 {
|
||||
// Determine or prompt for which candidate to distribute
|
||||
for strategy in opts.ties.iter() {
|
||||
match strategy {
|
||||
TieStrategy::Random(_) | TieStrategy::Prompt => {
|
||||
candidate_distributing = Some(strategy.choose_lowest(state, opts, &electable_candidates_old, "Which candidate's ballots to distribute?").unwrap());
|
||||
break;
|
||||
}
|
||||
TieStrategy::Forwards | TieStrategy::Backwards => {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
candidate_distributing = Some(electable_candidates_old[0]);
|
||||
}
|
||||
|
||||
state.logger.log_smart(
|
||||
"Distributing ballots of {}.",
|
||||
"Distributing ballots of {}.",
|
||||
vec![candidate_distributing.unwrap().name.as_str()]
|
||||
);
|
||||
} else {
|
||||
state.logger.log_smart(
|
||||
"Continuing distribution of ballots of {}.",
|
||||
"Continuing distribution of ballots of {}.",
|
||||
vec![candidate_distributing.unwrap().name.as_str()]
|
||||
);
|
||||
}
|
||||
|
||||
let candidate_distributing = candidate_distributing.unwrap();
|
||||
let dummy_candidate = state.election.candidates.iter().find(|c| c.name == candidate_distributing.name && c.is_dummy).unwrap();
|
||||
|
||||
state.title = StageKind::BallotsOf(candidate_distributing);
|
||||
stv::exclude_candidates(state, opts, vec![dummy_candidate], "Distribution")?;
|
||||
|
||||
state.rollback_state = RollbackState::RollingBack { candidates: Some(candidates), exhausted: Some(exhausted), candidate_distributing: Some(candidate_distributing), constraint: Some(constraint), group: Some(group) };
|
||||
rollback_check_complete(state);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
// Distribute ballots of unelectable candidates
|
||||
|
||||
let unelectable_candidates_old: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of multiple
|
||||
.filter(|c| {
|
||||
let cc = &candidates[c];
|
||||
let cand_idx = state.election.candidates.iter().position(|x| x == *c).unwrap();
|
||||
if !c.is_dummy && group.candidates.contains(&cand_idx) && !cc.finalised {
|
||||
let dummy_candidate = state.election.candidates.iter().find(|x| x.name == c.name && x.is_dummy).unwrap();
|
||||
!state.candidates[dummy_candidate].finalised
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !unelectable_candidates_old.is_empty() {
|
||||
if candidate_distributing.is_none() || !unelectable_candidates_old.contains(candidate_distributing.as_ref().unwrap()) {
|
||||
if unelectable_candidates_old.len() > 1 {
|
||||
// Determine or prompt for which candidate to distribute
|
||||
for strategy in opts.ties.iter() {
|
||||
match strategy {
|
||||
TieStrategy::Random(_) | TieStrategy::Prompt => {
|
||||
candidate_distributing = Some(strategy.choose_lowest(state, opts, &unelectable_candidates_old, "Which candidate's ballots to distribute?").unwrap());
|
||||
break;
|
||||
}
|
||||
TieStrategy::Forwards | TieStrategy::Backwards => {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
candidate_distributing = Some(unelectable_candidates_old[0]);
|
||||
}
|
||||
|
||||
state.logger.log_smart(
|
||||
"Distributing ballots of {}.",
|
||||
"Distributing ballots of {}.",
|
||||
vec![candidate_distributing.unwrap().name.as_str()]
|
||||
);
|
||||
} else {
|
||||
state.logger.log_smart(
|
||||
"Continuing distribution of ballots of {}.",
|
||||
"Continuing distribution of ballots of {}.",
|
||||
vec![candidate_distributing.unwrap().name.as_str()]
|
||||
);
|
||||
}
|
||||
|
||||
let candidate_distributing = candidate_distributing.unwrap();
|
||||
let dummy_candidate = state.election.candidates.iter().find(|c| c.name == candidate_distributing.name && c.is_dummy).unwrap();
|
||||
|
||||
state.title = StageKind::BallotsOf(candidate_distributing);
|
||||
stv::exclude_candidates(state, opts, vec![dummy_candidate], "Distribution")?;
|
||||
|
||||
state.rollback_state = RollbackState::RollingBack { candidates: Some(candidates), exhausted: Some(exhausted), candidate_distributing: Some(candidate_distributing), constraint: Some(constraint), group: Some(group) };
|
||||
rollback_check_complete(state);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
fn rollback_check_complete<N: Number>(state: &mut CountState<N>) {
|
||||
if let RollbackState::RollingBack { candidates, exhausted, candidate_distributing: _, constraint: _, group: _ } = &state.rollback_state {
|
||||
let candidates = candidates.as_ref().unwrap();
|
||||
let exhausted = exhausted.as_ref().unwrap();
|
||||
|
||||
let has_surplus: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of tie
|
||||
.filter(|c| {
|
||||
let cc = &candidates[c];
|
||||
if !c.is_dummy && cc.state == CandidateState::Elected && !cc.finalised {
|
||||
let dummy_candidate = state.election.candidates.iter().find(|x| x.name == c.name && x.is_dummy).unwrap();
|
||||
!state.candidates[dummy_candidate].finalised
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !has_surplus.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if exhausted.parcels.iter().any(|p| !p.votes.is_empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let continuing_candidates: Vec<&Candidate> = state.election.candidates.iter()
|
||||
.filter(|c| {
|
||||
let cc = &candidates[c];
|
||||
if !c.is_dummy && !cc.finalised {
|
||||
let dummy_candidate = state.election.candidates.iter().find(|x| x.name == c.name && x.is_dummy).unwrap();
|
||||
!state.candidates[dummy_candidate].finalised
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !continuing_candidates.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// Rollback complete: finalise
|
||||
|
||||
// Delete dummy candidates
|
||||
for (candidate, count_card) in state.candidates.iter_mut() {
|
||||
if candidate.is_dummy {
|
||||
count_card.state = CandidateState::Withdrawn;
|
||||
count_card.parcels.clear();
|
||||
count_card.votes = N::new();
|
||||
count_card.transfers = N::new();
|
||||
}
|
||||
}
|
||||
|
||||
state.logger.log_literal(String::from("Rollback complete."));
|
||||
state.rollback_state = RollbackState::Normal;
|
||||
state.num_excluded = state.candidates.values().filter(|cc| cc.state == CandidateState::Excluded).count();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
unreachable!();
|
||||
//return false;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
550
src/election.rs
550
src/election.rs
|
@ -1,5 +1,5 @@
|
|||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021–2023 Lee Yingtong Li (RunasSudo)
|
||||
* 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
|
||||
|
@ -15,29 +15,14 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::candmap::CandidateMap;
|
||||
use crate::constraints::{Constraint, Constraints, ConstrainedGroup, ConstraintMatrix};
|
||||
use crate::constraints::{Constraints, ConstraintMatrix};
|
||||
use crate::logger::Logger;
|
||||
use crate::numbers::Number;
|
||||
use crate::sharandom::SHARandom;
|
||||
use crate::stv::{self, STVOptions};
|
||||
use crate::stv::gregory::TransferTable;
|
||||
use crate::stv::meek::BallotTree;
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use rkyv::{Archive, Deserialize, Serialize};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::numbers::{SerializedNumber, SerializedOptionNumber};
|
||||
|
||||
use std::cmp::max;
|
||||
use std::fmt;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// An election to be counted
|
||||
#[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))]
|
||||
#[derive(Clone)]
|
||||
pub struct Election<N> {
|
||||
/// Name of the election
|
||||
pub name: String,
|
||||
|
@ -49,19 +34,85 @@ pub struct Election<N> {
|
|||
pub withdrawn_candidates: Vec<usize>,
|
||||
/// [Vec] of [Ballot]s cast in the election
|
||||
pub ballots: Vec<Ballot<N>>,
|
||||
/// Total value of [Ballot]s cast in the election
|
||||
///
|
||||
/// Used for [Election::realise_equal_rankings].
|
||||
#[cfg_attr(not(target_arch = "wasm32"), with(SerializedOptionNumber))]
|
||||
pub total_votes: Option<N>,
|
||||
/// Constraints on candidates
|
||||
pub constraints: Option<Constraints>,
|
||||
}
|
||||
|
||||
impl<N: Number> Election<N> {
|
||||
/// Parse the given BLT file and return an [Election]
|
||||
pub fn from_blt<I: Iterator<Item=String>>(mut lines: I) -> Self {
|
||||
// Read first line
|
||||
let line = lines.next().expect("Unexpected EOF");
|
||||
let mut bits = line.split(" ");
|
||||
let num_candidates = bits.next().expect("Syntax Error").parse().expect("Syntax Error");
|
||||
let seats: usize = bits.next().expect("Syntax Error").parse().expect("Syntax Error");
|
||||
|
||||
// Initialise the Election object
|
||||
let mut election = Election {
|
||||
name: String::new(),
|
||||
seats: seats,
|
||||
candidates: Vec::with_capacity(num_candidates),
|
||||
withdrawn_candidates: Vec::new(),
|
||||
ballots: Vec::new(),
|
||||
constraints: None,
|
||||
};
|
||||
|
||||
// Read ballots
|
||||
for line in &mut lines {
|
||||
if line == "0" {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut bits = line.split(" ");
|
||||
|
||||
if line.starts_with("-") {
|
||||
// Withdrawn candidates
|
||||
for bit in bits.into_iter() {
|
||||
let val = bit[1..bit.len()].parse::<usize>().expect("Syntax Error");
|
||||
election.withdrawn_candidates.push(val - 1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let value = N::parse(bits.next().expect("Syntax Error"));
|
||||
|
||||
let mut ballot = Ballot {
|
||||
orig_value: value,
|
||||
preferences: Vec::new(),
|
||||
};
|
||||
|
||||
for preference in bits {
|
||||
if preference != "0" {
|
||||
let preference = preference.parse::<usize>().expect("Syntax Error");
|
||||
ballot.preferences.push(preference - 1);
|
||||
}
|
||||
}
|
||||
|
||||
election.ballots.push(ballot);
|
||||
}
|
||||
|
||||
// Read candidates
|
||||
for line in lines.by_ref().take(num_candidates) {
|
||||
let mut line = &line[..];
|
||||
if line.starts_with("\"") && line.ends_with("\"") {
|
||||
line = &line[1..line.len()-1];
|
||||
}
|
||||
|
||||
election.candidates.push(Candidate { name: line.to_string() });
|
||||
}
|
||||
|
||||
// Read name
|
||||
let line = lines.next().expect("Syntax Error");
|
||||
let mut line = &line[..];
|
||||
if line.starts_with("\"") && line.ends_with("\"") {
|
||||
line = &line[1..line.len()-1];
|
||||
}
|
||||
election.name.push_str(line);
|
||||
|
||||
return election;
|
||||
}
|
||||
|
||||
/// Convert ballots with weight >1 to multiple ballots of weight 1
|
||||
///
|
||||
/// Assumes ballots have integer weight.
|
||||
pub fn normalise_ballots(&mut self) {
|
||||
let mut normalised_ballots = Vec::new();
|
||||
for ballot in self.ballots.iter() {
|
||||
|
@ -71,7 +122,6 @@ impl<N: Number> Election<N> {
|
|||
let new_ballot = Ballot {
|
||||
orig_value: N::one(),
|
||||
preferences: ballot.preferences.clone(),
|
||||
has_equal_rankings: ballot.has_equal_rankings,
|
||||
};
|
||||
normalised_ballots.push(new_ballot);
|
||||
n += &one;
|
||||
|
@ -79,75 +129,35 @@ impl<N: Number> Election<N> {
|
|||
}
|
||||
self.ballots = normalised_ballots;
|
||||
}
|
||||
|
||||
/// Convert ballots with equal rankings to strict-preference "minivoters"
|
||||
pub fn realise_equal_rankings(&mut self) {
|
||||
if !self.ballots.iter().any(|b| b.has_equal_rankings) {
|
||||
// No equal rankings
|
||||
return;
|
||||
}
|
||||
|
||||
// Record total_votes so loss by fraction can be calculated
|
||||
// See crate::stv::gregory::distribute_first_preferences, etc.
|
||||
self.total_votes = Some(self.ballots.iter().fold(N::new(), |mut acc, b| { acc += &b.orig_value; acc }));
|
||||
|
||||
let mut realised_ballots = Vec::with_capacity(self.ballots.len());
|
||||
for ballot in self.ballots.drain(..) {
|
||||
ballot.realise_equal_rankings_into(&mut realised_ballots);
|
||||
}
|
||||
self.ballots = realised_ballots;
|
||||
}
|
||||
}
|
||||
|
||||
/// A candidate in an [Election]
|
||||
#[derive(Clone, Eq)]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))]
|
||||
#[derive(PartialEq, Eq, Hash)]
|
||||
pub struct Candidate {
|
||||
/// Index of the candidate
|
||||
pub index: usize,
|
||||
/// Name of the candidate
|
||||
pub name: String,
|
||||
/// If this candidate is a dummy candidate (e.g. for --constraint-mode repeat_count)
|
||||
pub is_dummy: bool,
|
||||
}
|
||||
|
||||
impl PartialEq for Candidate {
|
||||
// Custom implementation of eq for HashMap purposes, to improve performance
|
||||
//
|
||||
// SAFETY: Results in undefined behaviour if multiple Candidates are allowed to have the same index
|
||||
fn eq(&self, other: &Candidate) -> bool {
|
||||
return self.index == other.index;
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Candidate {
|
||||
fn hash<H: Hasher>(&self, hasher: &mut H) {
|
||||
// Custom implementation of hash for use with NoHashHasher, to improve performance
|
||||
hasher.write_usize(self.index);
|
||||
}
|
||||
}
|
||||
impl nohash_hasher::IsEnabled for Candidate {}
|
||||
|
||||
/// The current state of counting an [Election]
|
||||
#[derive(Clone)]
|
||||
//#[derive(Clone)]
|
||||
pub struct CountState<'a, N: Number> {
|
||||
/// Pointer to the [Election] being counted
|
||||
pub election: &'a Election<N>,
|
||||
|
||||
/// [CandidateMap] of [CountCard]s for each [Candidate] in the election
|
||||
pub candidates: CandidateMap<'a, CountCard<'a, N>>,
|
||||
/// [HashMap] of [CountCard]s for each [Candidate] in the election
|
||||
pub candidates: HashMap<&'a Candidate, CountCard<'a, N>>,
|
||||
/// [CountCard] representing the exhausted pile
|
||||
pub exhausted: CountCard<'a, N>,
|
||||
/// [CountCard] representing loss by fraction
|
||||
pub loss_fraction: CountCard<'a, N>,
|
||||
|
||||
/// [BallotTree] for Meek STV
|
||||
pub ballot_tree: Option<BallotTree<'a, N>>,
|
||||
/// [crate::stv::meek::BallotTree] for Meek STV
|
||||
pub ballot_tree: Option<crate::stv::meek::BallotTree<'a, N>>,
|
||||
|
||||
/// Values used to break ties, based on forwards tie-breaking
|
||||
pub forwards_tiebreak: Option<CandidateMap<'a, usize>>,
|
||||
pub forwards_tiebreak: Option<HashMap<&'a Candidate, usize>>,
|
||||
/// Values used to break ties, based on backwards tie-breaking
|
||||
pub backwards_tiebreak: Option<CandidateMap<'a, usize>>,
|
||||
pub backwards_tiebreak: Option<HashMap<&'a Candidate, usize>>,
|
||||
/// [SHARandom] for random tie-breaking
|
||||
pub random: Option<SHARandom<'a>>,
|
||||
|
||||
|
@ -155,7 +165,7 @@ pub struct CountState<'a, N: Number> {
|
|||
pub quota: Option<N>,
|
||||
/// Vote required for election
|
||||
///
|
||||
/// Only used in ERS97/ERS76.
|
||||
/// With a static quota, this is equal to the quota. With ERS97 rules, this may vary from the quota.
|
||||
pub vote_required_election: Option<N>,
|
||||
|
||||
/// Number of candidates who have been declared elected
|
||||
|
@ -165,14 +175,13 @@ pub struct CountState<'a, N: Number> {
|
|||
|
||||
/// [ConstraintMatrix] for constrained elections
|
||||
pub constraint_matrix: Option<ConstraintMatrix>,
|
||||
/// [RollbackState] when using [ConstraintMode::RepeatCount](crate::stv::ConstraintMode::RepeatCount)
|
||||
pub rollback_state: RollbackState<'a, N>,
|
||||
|
||||
/// Transfer table for this surplus/exclusion
|
||||
pub transfer_table: Option<TransferTable<'a, N>>,
|
||||
|
||||
/// The type of stage being counted, etc.
|
||||
pub title: StageKind<'a>,
|
||||
/// The type of stage being counted
|
||||
///
|
||||
/// For example, "Surplus of", "Exclusion of"
|
||||
pub kind: Option<&'a str>,
|
||||
/// The description of the stage being counted, excluding [CountState::kind]
|
||||
pub title: String,
|
||||
/// [Logger] for this stage of the count
|
||||
pub logger: Logger<'a>,
|
||||
}
|
||||
|
@ -181,8 +190,8 @@ impl<'a, N: Number> CountState<'a, N> {
|
|||
/// Construct a new blank [CountState] for the given [Election]
|
||||
pub fn new(election: &'a Election<N>) -> Self {
|
||||
let mut state = CountState {
|
||||
election,
|
||||
candidates: CandidateMap::with_capacity(election.candidates.len()),
|
||||
election: &election,
|
||||
candidates: HashMap::new(),
|
||||
exhausted: CountCard::new(),
|
||||
loss_fraction: CountCard::new(),
|
||||
ballot_tree: None,
|
||||
|
@ -194,29 +203,20 @@ impl<'a, N: Number> CountState<'a, N> {
|
|||
num_elected: 0,
|
||||
num_excluded: 0,
|
||||
constraint_matrix: None,
|
||||
rollback_state: RollbackState::Normal,
|
||||
transfer_table: None,
|
||||
title: StageKind::FirstPreferences,
|
||||
kind: None,
|
||||
title: String::new(),
|
||||
logger: Logger { entries: Vec::new() },
|
||||
};
|
||||
|
||||
// Init candidate count cards
|
||||
for candidate in election.candidates.iter() {
|
||||
let mut count_card = CountCard::new();
|
||||
if candidate.is_dummy {
|
||||
count_card.state = CandidateState::Withdrawn;
|
||||
}
|
||||
state.candidates.insert(candidate, count_card);
|
||||
state.candidates.insert(candidate, CountCard::new());
|
||||
}
|
||||
|
||||
// Set withdrawn candidates state
|
||||
for withdrawn_idx in election.withdrawn_candidates.iter() {
|
||||
state.candidates.get_mut(&election.candidates[*withdrawn_idx]).unwrap().state = CandidateState::Withdrawn;
|
||||
}
|
||||
|
||||
// Init constraints
|
||||
if let Some(constraints) = &election.constraints {
|
||||
// Init constraint matrix
|
||||
let mut num_groups: Vec<usize> = constraints.0.iter().map(|c| c.groups.len()).collect();
|
||||
let mut cm = ConstraintMatrix::new(&mut num_groups[..]);
|
||||
|
||||
|
@ -232,7 +232,7 @@ impl<'a, N: Number> CountState<'a, N> {
|
|||
}
|
||||
|
||||
// Fill in grand total, etc.
|
||||
cm.update_from_state(state.election, &state.candidates);
|
||||
cm.update_from_state(&state.election, &state.candidates);
|
||||
cm.init();
|
||||
//println!("{}", cm);
|
||||
|
||||
|
@ -255,210 +255,6 @@ impl<'a, N: Number> CountState<'a, N> {
|
|||
self.exhausted.step();
|
||||
self.loss_fraction.step();
|
||||
}
|
||||
|
||||
/// List the candidates, and their current state, votes and transfers
|
||||
pub fn describe_candidates(&self, opts: &STVOptions) -> String {
|
||||
let mut candidates: Vec<(&Candidate, &CountCard<N>)>;
|
||||
|
||||
if opts.sort_votes {
|
||||
// Sort by votes if requested
|
||||
candidates = self.candidates.iter().collect();
|
||||
// First sort by order of election (as a tie-breaker, if votes are equal)
|
||||
candidates.sort_unstable_by(|a, b| b.1.order_elected.cmp(&a.1.order_elected));
|
||||
// Then sort by votes
|
||||
candidates.sort_by(|a, b| a.1.votes.cmp(&b.1.votes));
|
||||
candidates.reverse();
|
||||
} else {
|
||||
candidates = self.election.candidates.iter()
|
||||
.map(|c| (c, &self.candidates[c]))
|
||||
.collect();
|
||||
}
|
||||
|
||||
let mut result = String::new();
|
||||
|
||||
for (candidate, count_card) in candidates {
|
||||
if candidate.is_dummy {
|
||||
continue;
|
||||
}
|
||||
|
||||
match count_card.state {
|
||||
CandidateState::Hopeful => {
|
||||
result.push_str(&format!("- {}: {:.dps$} ({:.dps$})\n", candidate.name, count_card.votes, count_card.transfers, dps=opts.pp_decimals));
|
||||
}
|
||||
CandidateState::Guarded => {
|
||||
result.push_str(&format!("- {}: {:.dps$} ({:.dps$}) - Guarded\n", candidate.name, count_card.votes, count_card.transfers, dps=opts.pp_decimals));
|
||||
}
|
||||
CandidateState::Elected => {
|
||||
if let Some(kv) = &count_card.keep_value {
|
||||
result.push_str(&format!("- {}: {:.dps$} ({:.dps$}) - ELECTED {} (kv = {:.dps2$})\n", candidate.name, count_card.votes, count_card.transfers, count_card.order_elected, kv, dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
||||
} else {
|
||||
result.push_str(&format!("- {}: {:.dps$} ({:.dps$}) - ELECTED {}\n", candidate.name, count_card.votes, count_card.transfers, count_card.order_elected, dps=opts.pp_decimals));
|
||||
}
|
||||
}
|
||||
CandidateState::Doomed => {
|
||||
result.push_str(&format!("- {}: {:.dps$} ({:.dps$}) - Doomed\n", candidate.name, count_card.votes, count_card.transfers, dps=opts.pp_decimals));
|
||||
}
|
||||
CandidateState::Withdrawn => {
|
||||
if !opts.hide_excluded || !count_card.votes.is_zero() || !count_card.transfers.is_zero() {
|
||||
result.push_str(&format!("- {}: {:.dps$} ({:.dps$}) - Withdrawn\n", candidate.name, count_card.votes, count_card.transfers, dps=opts.pp_decimals));
|
||||
}
|
||||
}
|
||||
CandidateState::Excluded => {
|
||||
// If --hide-excluded, hide unless nonzero votes or nonzero transfers
|
||||
if !opts.hide_excluded || !count_card.votes.is_zero() || !count_card.transfers.is_zero() {
|
||||
result.push_str(&format!("- {}: {:.dps$} ({:.dps$}) - Excluded {}\n", candidate.name, count_card.votes, count_card.transfers, -count_card.order_elected, dps=opts.pp_decimals));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Produce summary rows for the current stage
|
||||
pub fn describe_summary(&self, opts: &STVOptions) -> String {
|
||||
let mut result = String::new();
|
||||
|
||||
result.push_str(&format!("Exhausted: {:.dps$} ({:.dps$})\n", self.exhausted.votes, self.exhausted.transfers, dps=opts.pp_decimals));
|
||||
result.push_str(&format!("Loss by fraction: {:.dps$} ({:.dps$})\n", self.loss_fraction.votes, self.loss_fraction.transfers, dps=opts.pp_decimals));
|
||||
|
||||
let mut total_vote = self.candidates.iter().filter_map(|(c, cc)| if c.is_dummy { None } else { Some(cc) }).fold(N::new(), |mut acc, cc| { acc += &cc.votes; acc });
|
||||
total_vote += &self.exhausted.votes;
|
||||
total_vote += &self.loss_fraction.votes;
|
||||
result.push_str(&format!("Total votes: {:.dps$}\n", total_vote, dps=opts.pp_decimals));
|
||||
|
||||
if self.election.seats == 1 {
|
||||
result.push_str(&format!("Majority: {:.dps$}\n", self.quota.as_ref().unwrap(), dps=opts.pp_decimals));
|
||||
} else {
|
||||
result.push_str(&format!("Quota: {:.dps$}\n", self.quota.as_ref().unwrap(), dps=opts.pp_decimals));
|
||||
}
|
||||
if stv::should_show_vre(opts) {
|
||||
if let Some(vre) = &self.vote_required_election {
|
||||
result.push_str(&format!("Vote required for election: {:.dps$}\n", vre, dps=opts.pp_decimals));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// -------------------
|
||||
// HELPER CALCULATIONS
|
||||
|
||||
/// Get the total vote, viz. the sum votes of all candidates
|
||||
pub fn total_vote(&self) -> N {
|
||||
return self.total_votes_of(self.candidates.values());
|
||||
}
|
||||
|
||||
/// Get the total votes of the given candidates
|
||||
pub fn total_votes_of<I: Iterator<Item=&'a CountCard<'a, N>>>(&self, count_cards: I) -> N {
|
||||
return count_cards.fold(N::new(), |mut acc, cc| { acc += &cc.votes; acc });
|
||||
}
|
||||
|
||||
/// Get the total active vote, viz. the votes of all continuing candidates plus all votes awaiting transfer
|
||||
pub fn active_vote(&self) -> N {
|
||||
return self.candidates.values().fold(N::new(), |mut acc, cc| {
|
||||
match cc.state {
|
||||
CandidateState::Elected => {
|
||||
if !cc.finalised && &cc.votes > self.quota.as_ref().unwrap() {
|
||||
acc += &cc.votes;
|
||||
acc -= self.quota.as_ref().unwrap();
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
acc += &cc.votes;
|
||||
}
|
||||
}
|
||||
acc
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the total surplus
|
||||
///
|
||||
/// A candidate has a surplus if the candidate's votes are not finalised, and the votes exceed the quota.
|
||||
/// The votes of all other candidates are ignored.
|
||||
pub fn total_surplus(&self) -> N {
|
||||
return self.total_surplus_of(self.candidates.values());
|
||||
}
|
||||
|
||||
/// Get the total surpluses of the given candidates
|
||||
///
|
||||
/// See [CountState::total_surplus].
|
||||
pub fn total_surplus_of<I: Iterator<Item=&'a CountCard<'a, N>>>(&self, count_cards: I) -> N {
|
||||
return count_cards.fold(N::new(), |mut acc, cc| {
|
||||
if !cc.finalised && &cc.votes > self.quota.as_ref().unwrap() {
|
||||
acc += &cc.votes;
|
||||
acc -= self.quota.as_ref().unwrap();
|
||||
}
|
||||
acc
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind, title, etc. of the stage being counted
|
||||
#[derive(Clone)]
|
||||
pub enum StageKind<'a> {
|
||||
/// First preferences
|
||||
FirstPreferences,
|
||||
/// Surplus of ...
|
||||
SurplusOf(&'a Candidate),
|
||||
/// Exclusion of ...
|
||||
ExclusionOf(Vec<&'a Candidate>),
|
||||
/// Rolled back (--constraint-mode repeat_count)
|
||||
Rollback,
|
||||
/// Exhausted ballots (--constraint-mode repeat_count)
|
||||
RollbackExhausted,
|
||||
/// Ballots of ... (--constraint-mode repeat_count)
|
||||
BallotsOf(&'a Candidate),
|
||||
/// Surpluses distributed (Meek)
|
||||
SurplusesDistributed,
|
||||
/// Bulk election
|
||||
BulkElection,
|
||||
}
|
||||
|
||||
impl<'a> StageKind<'a> {
|
||||
/// Return the "kind" portion of the title
|
||||
pub fn kind_as_string(&self) -> &'static str {
|
||||
return match self {
|
||||
StageKind::FirstPreferences => "",
|
||||
StageKind::SurplusOf(_) => "Surplus of",
|
||||
StageKind::ExclusionOf(_) => "Exclusion of",
|
||||
StageKind::Rollback => "",
|
||||
StageKind::RollbackExhausted => "",
|
||||
StageKind::BallotsOf(_) => "Ballots of",
|
||||
StageKind::SurplusesDistributed => "",
|
||||
StageKind::BulkElection => "",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for StageKind<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
StageKind::FirstPreferences => {
|
||||
return f.write_str("First preferences");
|
||||
}
|
||||
StageKind::SurplusOf(candidate) => {
|
||||
return f.write_fmt(format_args!("{} {}", self.kind_as_string(), candidate.name));
|
||||
}
|
||||
StageKind::ExclusionOf(candidates) => {
|
||||
return f.write_fmt(format_args!("{} {}", self.kind_as_string(), candidates.iter().map(|c| &c.name).sorted().join(", ")));
|
||||
}
|
||||
StageKind::Rollback => {
|
||||
return f.write_str("Constraints applied");
|
||||
}
|
||||
StageKind::RollbackExhausted => {
|
||||
return f.write_str("Exhausted ballots");
|
||||
}
|
||||
StageKind::BallotsOf(candidate) => {
|
||||
return f.write_fmt(format_args!("{} {}", self.kind_as_string(), candidate.name));
|
||||
}
|
||||
StageKind::SurplusesDistributed => {
|
||||
return f.write_str("Surpluses distributed");
|
||||
}
|
||||
StageKind::BulkElection => {
|
||||
return f.write_str("Bulk election");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Current state of a [Candidate] during an election count
|
||||
|
@ -468,19 +264,15 @@ pub struct CountCard<'a, N> {
|
|||
pub state: CandidateState,
|
||||
/// Order of election or exclusion
|
||||
///
|
||||
/// Positive integers represent order of election; negative integers represent order of exclusion.
|
||||
/// Positive integers represent order of election; negative integers represent order of exclusion
|
||||
pub order_elected: isize,
|
||||
/// Whether distribution of this candidate's surpluses/transfer of excluded candidate's votes is complete
|
||||
pub finalised: bool,
|
||||
|
||||
//pub orig_votes: N,
|
||||
/// Net votes transferred to this candidate in this stage
|
||||
pub transfers: N,
|
||||
/// Votes of the candidate at the end of this stage
|
||||
pub votes: N,
|
||||
|
||||
/// Net ballots transferred to this candidate in this stage
|
||||
pub ballot_transfers: N,
|
||||
|
||||
/// Parcels of ballots assigned to this candidate
|
||||
pub parcels: Vec<Parcel<'a, N>>,
|
||||
|
||||
|
@ -494,10 +286,9 @@ impl<'a, N: Number> CountCard<'a, N> {
|
|||
return CountCard {
|
||||
state: CandidateState::Hopeful,
|
||||
order_elected: 0,
|
||||
finalised: false,
|
||||
//orig_votes: N::new(),
|
||||
transfers: N::new(),
|
||||
votes: N::new(),
|
||||
ballot_transfers: N::new(),
|
||||
parcels: Vec::new(),
|
||||
keep_value: None,
|
||||
};
|
||||
|
@ -511,137 +302,37 @@ impl<'a, N: Number> CountCard<'a, N> {
|
|||
|
||||
/// Set [transfers](CountCard::transfers) to 0
|
||||
pub fn step(&mut self) {
|
||||
//self.orig_votes = self.votes.clone();
|
||||
self.transfers = N::new();
|
||||
self.ballot_transfers = N::new();
|
||||
}
|
||||
|
||||
/// Concatenate all parcels into a single parcel, leaving [parcels](CountCard::parcels) empty
|
||||
pub fn concat_parcels(&mut self) -> Vec<Vote<'a, N>> {
|
||||
let mut result = Vec::new();
|
||||
for parcel in self.parcels.iter_mut() {
|
||||
result.append(&mut parcel.votes);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Return the number of ballots across all parcels
|
||||
pub fn num_ballots(&self) -> N {
|
||||
return self.parcels.iter().fold(N::new(), |mut acc, p| { acc += p.num_ballots(); acc });
|
||||
}
|
||||
}
|
||||
|
||||
/// Parcel of [Vote]s during a count
|
||||
#[derive(Clone)]
|
||||
pub struct Parcel<'a, N> {
|
||||
/// [Vote]s in this parcel
|
||||
pub votes: Vec<Vote<'a, N>>,
|
||||
/// Accumulated relative value of each [Vote] in this parcel
|
||||
pub value_fraction: N,
|
||||
/// Order for sorting with [crate::stv::ExclusionMethod::BySource]
|
||||
pub source_order: usize,
|
||||
}
|
||||
|
||||
impl<'a, N: Number> Parcel<'a, N> {
|
||||
/// Return the number of ballots in this parcel
|
||||
pub fn num_ballots(&self) -> N {
|
||||
return self.votes.iter().fold(N::new(), |mut acc, v| { acc += &v.ballot.orig_value; acc });
|
||||
}
|
||||
|
||||
/// Return the value of the votes in this parcel
|
||||
pub fn num_votes(&self) -> N {
|
||||
return self.num_ballots() * &self.value_fraction;
|
||||
}
|
||||
}
|
||||
pub type Parcel<'a, N> = Vec<Vote<'a, N>>;
|
||||
|
||||
/// Represents a [Ballot] with an associated value
|
||||
#[derive(Clone)]
|
||||
pub struct Vote<'a, N> {
|
||||
/// Ballot from which the vote is derived
|
||||
pub ballot: &'a Ballot<N>,
|
||||
/// Current value of the ballot
|
||||
pub value: N,
|
||||
/// Index of the next preference to examine
|
||||
pub up_to_pref: usize,
|
||||
}
|
||||
|
||||
impl<'a, N> Vote<'a, N> {
|
||||
/// Get the next preference and increment `up_to_pref`
|
||||
///
|
||||
/// Assumes that each preference level contains only one preference.
|
||||
pub fn next_preference(&mut self) -> Option<usize> {
|
||||
if self.up_to_pref >= self.ballot.preferences.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let preference = &self.ballot.preferences[self.up_to_pref];
|
||||
self.up_to_pref += 1;
|
||||
|
||||
return Some(*preference.first().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
/// A record of a voter's preferences
|
||||
#[derive(Clone)]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))]
|
||||
pub struct Ballot<N> {
|
||||
/// Original value/weight of the ballot
|
||||
#[cfg_attr(not(target_arch = "wasm32"), with(SerializedNumber))]
|
||||
pub orig_value: N,
|
||||
/// Indexes of candidates preferenced at each level on the ballot
|
||||
pub preferences: Vec<Vec<usize>>,
|
||||
/// Whether multiple candidates are preferenced at the same level
|
||||
pub has_equal_rankings: bool,
|
||||
}
|
||||
|
||||
impl<N: Number> Ballot<N> {
|
||||
/// Convert ballot with equal rankings to strict-preference "minivoters"
|
||||
pub fn realise_equal_rankings_into(self, dest: &mut Vec<Ballot<N>>) {
|
||||
if !self.has_equal_rankings {
|
||||
dest.push(self);
|
||||
return;
|
||||
}
|
||||
|
||||
// Preferences for each minivoter
|
||||
let mut minivoters = vec![Vec::new()];
|
||||
|
||||
for preference in self.preferences.iter() {
|
||||
if preference.len() == 1 {
|
||||
// Single preference so just add to the end of existing preferences
|
||||
for minivoter in minivoters.iter_mut() {
|
||||
minivoter.push(preference.clone());
|
||||
}
|
||||
} else {
|
||||
// Equal ranking
|
||||
// Get all possible permutations
|
||||
let permutations: Vec<Vec<usize>> = preference.iter().copied().permutations(preference.len()).collect();
|
||||
|
||||
// Split into new "minivoters" for each possible permutation
|
||||
let mut new_minivoters = Vec::with_capacity(minivoters.len() * permutations.len());
|
||||
for permutation in permutations {
|
||||
for minivoter in minivoters.iter() {
|
||||
let mut new_minivoter = minivoter.clone();
|
||||
for p in permutation.iter() {
|
||||
new_minivoter.push(vec![*p]);
|
||||
}
|
||||
new_minivoters.push(new_minivoter);
|
||||
}
|
||||
}
|
||||
minivoters = new_minivoters;
|
||||
}
|
||||
}
|
||||
|
||||
let weight_each = self.orig_value.clone() / N::from(minivoters.len());
|
||||
|
||||
for minivoter in minivoters {
|
||||
dest.push(Ballot {
|
||||
orig_value: weight_each.clone(),
|
||||
preferences: minivoter,
|
||||
has_equal_rankings: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
/// Indexes of candidates preferenced on the ballot
|
||||
pub preferences: Vec<usize>,
|
||||
}
|
||||
|
||||
/// State of a [Candidate] during a count
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
#[derive(PartialEq)]
|
||||
#[derive(Clone)]
|
||||
pub enum CandidateState {
|
||||
/// Hopeful (continuing candidate)
|
||||
Hopeful,
|
||||
|
@ -656,26 +347,3 @@ pub enum CandidateState {
|
|||
/// Declared excluded
|
||||
Excluded,
|
||||
}
|
||||
|
||||
/// If --constraint-mode repeat_count and redistribution is required, tracks the ballot papers being redistributed
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Clone)]
|
||||
pub enum RollbackState<'a, N> {
|
||||
/// Not rolling back
|
||||
Normal,
|
||||
/// Start rolling back next stage
|
||||
NeedsRollback {
|
||||
candidates: Option<CandidateMap<'a, CountCard<'a, N>>>,
|
||||
exhausted: Option<CountCard<'a, N>>,
|
||||
constraint: &'a Constraint,
|
||||
group: &'a ConstrainedGroup
|
||||
},
|
||||
/// Rolling back
|
||||
RollingBack {
|
||||
candidates: Option<CandidateMap<'a, CountCard<'a, N>>>,
|
||||
exhausted: Option<CountCard<'a, N>>,
|
||||
candidate_distributing: Option<&'a Candidate>,
|
||||
constraint: Option<&'a Constraint>,
|
||||
group: Option<&'a ConstrainedGroup>
|
||||
},
|
||||
}
|
||||
|
|
15
src/lib.rs
15
src/lib.rs
|
@ -16,12 +16,9 @@
|
|||
*/
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![allow(clippy::collapsible_else_if, clippy::collapsible_if, clippy::comparison_chain, clippy::derive_ord_xor_partial_ord, clippy::needless_bool, clippy::needless_return, clippy::new_without_default, clippy::too_many_arguments)]
|
||||
|
||||
//! Open source counting software for various preferential voting election systems
|
||||
|
||||
/// Helper newtype for HashMap on [election::Candidate]s
|
||||
pub mod candmap;
|
||||
/// Data types and logic for constraints on elections
|
||||
pub mod constraints;
|
||||
/// Data types for representing abstract elections
|
||||
|
@ -30,29 +27,19 @@ pub mod election;
|
|||
pub mod logger;
|
||||
/// Implementations of different numeric representations
|
||||
pub mod numbers;
|
||||
/// File parsers
|
||||
pub mod parser;
|
||||
/// Deterministic random number generation using SHA256
|
||||
pub mod sharandom;
|
||||
/// STV counting logic
|
||||
pub mod stv;
|
||||
/// Tie-breaking methods
|
||||
pub mod ties;
|
||||
/// File writers
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod writer;
|
||||
|
||||
/// CLI implementations
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod cli;
|
||||
|
||||
use git_version::git_version;
|
||||
#[allow(unused_imports)]
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
/// The git revision of this OpenTally build
|
||||
pub const VERSION: &str = git_version!(args=["--always", "--dirty=-dev"], fallback="unknown");
|
||||
|
||||
/// Get [VERSION] as a String (for WebAssembly)
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[wasm_bindgen]
|
||||
pub fn version() -> String { VERSION.to_string() }
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021–2022 Lee Yingtong Li (RunasSudo)
|
||||
* 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
|
||||
|
@ -28,10 +28,10 @@ impl<'a> Logger<'a> {
|
|||
/// If consecutive smart log entries have the same templates, they will be merged
|
||||
pub fn log(&mut self, entry: LogEntry<'a>) {
|
||||
if let LogEntry::Smart(mut smart) = entry {
|
||||
if !self.entries.is_empty() {
|
||||
if self.entries.len() > 0 {
|
||||
if let LogEntry::Smart(last_smart) = self.entries.last_mut().unwrap() {
|
||||
if last_smart.template1 == smart.template1 && last_smart.template2 == smart.template2 {
|
||||
last_smart.data.append(&mut smart.data);
|
||||
&last_smart.data.append(&mut smart.data);
|
||||
} else {
|
||||
self.entries.push(LogEntry::Smart(smart));
|
||||
}
|
||||
|
@ -56,9 +56,9 @@ impl<'a> Logger<'a> {
|
|||
/// If consecutive smart log entries have the same templates, they will be merged
|
||||
pub fn log_smart(&mut self, template1: &'a str, template2: &'a str, data: Vec<&'a str>) {
|
||||
self.log(LogEntry::Smart(SmartLogEntry {
|
||||
template1,
|
||||
template2,
|
||||
data,
|
||||
template1: template1,
|
||||
template2: template2,
|
||||
data: data,
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -91,7 +91,7 @@ pub struct SmartLogEntry<'a> {
|
|||
impl<'a> SmartLogEntry<'a> {
|
||||
/// Render the [SmartLogEntry] to a [String]
|
||||
pub fn render(&self) -> String {
|
||||
if self.data.is_empty() {
|
||||
if self.data.len() == 0 {
|
||||
panic!("Attempted to format smart log entry with no data");
|
||||
} else if self.data.len() == 1 {
|
||||
return String::from(self.template1).replace("{}", self.data.first().unwrap());
|
||||
|
@ -102,7 +102,6 @@ impl<'a> SmartLogEntry<'a> {
|
|||
}
|
||||
|
||||
/// Join the given strings, with commas and terminal "and"
|
||||
#[allow(clippy::ptr_arg)]
|
||||
pub fn smart_join(data: &Vec<&str>) -> String {
|
||||
return format!("{} and {}", data[0..data.len()-1].join(", "), data.last().unwrap());
|
||||
}
|
||||
|
|
376
src/main.rs
376
src/main.rs
|
@ -15,42 +15,378 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#![allow(clippy::needless_return)]
|
||||
use opentally::constraints::Constraints;
|
||||
use opentally::election::{Candidate, CandidateState, CountCard, CountState, Election};
|
||||
use opentally::numbers::{DynNum, Fixed, GuardedFixed, NativeFloat64, Number, NumKind, Rational};
|
||||
use opentally::stv;
|
||||
|
||||
use opentally::cli;
|
||||
use clap::{AppSettings, Clap};
|
||||
|
||||
use clap::Parser;
|
||||
use std::cmp::max;
|
||||
use std::fs::File;
|
||||
use std::io::{self, BufRead};
|
||||
use std::ops;
|
||||
|
||||
/// Open-source election vote counting
|
||||
#[derive(Parser)]
|
||||
#[derive(Clap)]
|
||||
#[clap(name="OpenTally", version=opentally::VERSION)]
|
||||
struct Opts {
|
||||
#[clap(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Parser)]
|
||||
#[derive(Clap)]
|
||||
enum Command {
|
||||
Convert(cli::convert::SubcmdOptions),
|
||||
Stv(cli::stv::SubcmdOptions),
|
||||
STV(STV),
|
||||
}
|
||||
|
||||
/// Count a single transferable vote (STV) election
|
||||
#[derive(Clap)]
|
||||
#[clap(setting=AppSettings::DeriveDisplayOrder)]
|
||||
struct STV {
|
||||
// ----------------
|
||||
// -- File input --
|
||||
|
||||
/// Path to the BLT file to be counted
|
||||
filename: String,
|
||||
|
||||
// ----------------------
|
||||
// -- Numbers settings --
|
||||
|
||||
/// Numbers mode
|
||||
#[clap(help_heading=Some("NUMBERS"), short, long, possible_values=&["rational", "fixed", "gfixed", "float64"], default_value="rational", value_name="mode")]
|
||||
numbers: String,
|
||||
|
||||
/// Decimal places if --numbers fixed
|
||||
#[clap(help_heading=Some("NUMBERS"), long, default_value="5", value_name="dps")]
|
||||
decimals: usize,
|
||||
|
||||
/// Use dynamic dispatch for numbers
|
||||
//#[clap(help_heading=Some("NUMBERS"), long)]
|
||||
//dynnum: bool,
|
||||
|
||||
/// Convert ballots with value >1 to multiple ballots of value 1
|
||||
#[clap(help_heading=Some("NUMBERS"), long)]
|
||||
normalise_ballots: bool,
|
||||
|
||||
// -----------------------
|
||||
// -- Rounding settings --
|
||||
|
||||
/// Round transfer values to specified decimal places
|
||||
#[clap(help_heading=Some("ROUNDING"), long, value_name="dps")]
|
||||
round_tvs: Option<usize>,
|
||||
|
||||
/// Round ballot weights to specified decimal places
|
||||
#[clap(help_heading=Some("ROUNDING"), long, value_name="dps")]
|
||||
round_weights: Option<usize>,
|
||||
|
||||
/// Round votes to specified decimal places
|
||||
#[clap(help_heading=Some("ROUNDING"), long, value_name="dps")]
|
||||
round_votes: Option<usize>,
|
||||
|
||||
/// Round quota to specified decimal places
|
||||
#[clap(help_heading=Some("ROUNDING"), long, value_name="dps")]
|
||||
round_quota: Option<usize>,
|
||||
|
||||
/// How to calculate votes to credit to candidates in surplus transfers
|
||||
#[clap(help_heading=Some("ROUNDING"), long, possible_values=&["single_step", "by_value", "per_ballot"], default_value="single_step", value_name="mode")]
|
||||
sum_surplus_transfers: String,
|
||||
|
||||
/// (Meek STV) Limit for stopping iteration of surplus distribution
|
||||
#[clap(help_heading=Some("ROUNDING"), long, default_value="0.001%", value_name="tolerance")]
|
||||
meek_surplus_tolerance: String,
|
||||
|
||||
// -----------
|
||||
// -- Quota --
|
||||
|
||||
/// Quota type
|
||||
#[clap(help_heading=Some("QUOTA"), short, long, possible_values=&["droop", "hare", "droop_exact", "hare_exact"], default_value="droop_exact")]
|
||||
quota: String,
|
||||
|
||||
/// Whether to elect candidates on meeting (geq) or strictly exceeding (gt) the quota
|
||||
#[clap(help_heading=Some("QUOTA"), short='c', long, possible_values=&["geq", "gt"], default_value="gt", value_name="criterion")]
|
||||
quota_criterion: String,
|
||||
|
||||
/// Whether to apply a form of progressive quota
|
||||
#[clap(help_heading=Some("QUOTA"), long, possible_values=&["static", "ers97"], default_value="static", value_name="mode")]
|
||||
quota_mode: String,
|
||||
|
||||
// ------------------
|
||||
// -- STV variants --
|
||||
|
||||
/// Tie-breaking method
|
||||
#[clap(help_heading=Some("STV VARIANTS"), short='t', long, possible_values=&["forwards", "backwards", "random", "prompt"], default_value="prompt", value_name="methods")]
|
||||
ties: Vec<String>,
|
||||
|
||||
/// Random seed to use with --ties random
|
||||
#[clap(help_heading=Some("STV VARIANTS"), long, value_name="seed")]
|
||||
random_seed: Option<String>,
|
||||
|
||||
/// Method of surplus distributions
|
||||
#[clap(help_heading=Some("STV VARIANTS"), short='s', long, possible_values=&["wig", "uig", "eg", "meek"], default_value="wig", value_name="method")]
|
||||
surplus: String,
|
||||
|
||||
/// Order to distribute surpluses
|
||||
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["by_size", "by_order"], default_value="by_size", value_name="order")]
|
||||
surplus_order: String,
|
||||
|
||||
/// Examine only transferable papers during surplus distributions
|
||||
#[clap(help_heading=Some("STV VARIANTS"), long)]
|
||||
transferable_only: bool,
|
||||
|
||||
/// Method of exclusions
|
||||
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["single_stage", "by_value", "parcels_by_order", "wright"], default_value="single_stage", value_name="method")]
|
||||
exclusion: String,
|
||||
|
||||
/// (Meek STV) NZ Meek STV behaviour: Iterate keep values one round before candidate exclusion
|
||||
#[clap(help_heading=Some("STV VARIANTS"), long)]
|
||||
meek_nz_exclusion: bool,
|
||||
|
||||
// -------------------------
|
||||
// -- Count optimisations --
|
||||
|
||||
/// Continue count even if continuing candidates fill all remaining vacancies
|
||||
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)]
|
||||
no_early_bulk_elect: bool,
|
||||
|
||||
/// Use bulk exclusion
|
||||
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)]
|
||||
bulk_exclude: bool,
|
||||
|
||||
/// Defer surplus distributions if possible
|
||||
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)]
|
||||
defer_surpluses: bool,
|
||||
|
||||
/// (Meek STV) Immediately elect candidates even if keep values have not converged
|
||||
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)]
|
||||
meek_immediate_elect: bool,
|
||||
|
||||
// -----------------
|
||||
// -- Constraints --
|
||||
|
||||
/// Path to a CON file specifying constraints
|
||||
#[clap(help_heading=Some("CONSTRAINTS"), long)]
|
||||
constraints: Option<String>,
|
||||
|
||||
/// Mode of handling constraints
|
||||
#[clap(help_heading=Some("CONSTRAINTS"), long, possible_values=&["guard_doom"], default_value="guard_doom")]
|
||||
constraint_mode: String,
|
||||
|
||||
// ----------------------
|
||||
// -- Display settings --
|
||||
|
||||
/// Hide excluded candidates from results report
|
||||
#[clap(help_heading=Some("DISPLAY"), long)]
|
||||
hide_excluded: bool,
|
||||
|
||||
/// Sort candidates by votes in results report
|
||||
#[clap(help_heading=Some("DISPLAY"), long)]
|
||||
sort_votes: bool,
|
||||
|
||||
/// Print votes to specified decimal places in results report
|
||||
#[clap(help_heading=Some("DISPLAY"), long, default_value="2", value_name="dps")]
|
||||
pp_decimals: usize,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
match main_() {
|
||||
Ok(_) => {}
|
||||
Err(code) => {
|
||||
std::process::exit(code);
|
||||
// Read arguments
|
||||
let opts: Opts = Opts::parse();
|
||||
let Command::STV(cmd_opts) = opts.command;
|
||||
|
||||
// Read BLT file
|
||||
let file = File::open(&cmd_opts.filename).expect("IO Error");
|
||||
let lines = io::BufReader::new(file).lines();
|
||||
|
||||
// Create and count election according to --numbers
|
||||
if cmd_opts.numbers == "rational" {
|
||||
DynNum::set_kind(NumKind::Rational);
|
||||
} else if cmd_opts.numbers == "float64" {
|
||||
DynNum::set_kind(NumKind::NativeFloat64);
|
||||
} else if cmd_opts.numbers == "fixed" {
|
||||
Fixed::set_dps(cmd_opts.decimals);
|
||||
DynNum::set_kind(NumKind::Fixed);
|
||||
} else if cmd_opts.numbers == "gfixed" {
|
||||
GuardedFixed::set_dps(cmd_opts.decimals);
|
||||
DynNum::set_kind(NumKind::GuardedFixed);
|
||||
}
|
||||
|
||||
let mut election: Election<DynNum> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
|
||||
maybe_load_constraints(&mut election, &cmd_opts.constraints);
|
||||
|
||||
count_election::<DynNum>(election, cmd_opts);
|
||||
}
|
||||
|
||||
fn maybe_load_constraints<N: Number>(election: &mut Election<N>, constraints: &Option<String>) {
|
||||
if let Some(c) = constraints {
|
||||
let file = File::open(c).expect("IO Error");
|
||||
let lines = io::BufReader::new(file).lines();
|
||||
election.constraints = Some(Constraints::from_con(lines.map(|r| r.expect("IO Error").to_string()).into_iter()));
|
||||
}
|
||||
}
|
||||
|
||||
fn count_election<N: Number>(mut election: Election<N>, cmd_opts: STV)
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Neg<Output=N>
|
||||
{
|
||||
// Copy applicable options
|
||||
let stv_opts = stv::STVOptions::new(
|
||||
cmd_opts.round_tvs,
|
||||
cmd_opts.round_weights,
|
||||
cmd_opts.round_votes,
|
||||
cmd_opts.round_quota,
|
||||
&cmd_opts.sum_surplus_transfers,
|
||||
&cmd_opts.meek_surplus_tolerance,
|
||||
cmd_opts.normalise_ballots,
|
||||
&cmd_opts.quota,
|
||||
&cmd_opts.quota_criterion,
|
||||
&cmd_opts.quota_mode,
|
||||
&cmd_opts.ties,
|
||||
&cmd_opts.random_seed,
|
||||
&cmd_opts.surplus,
|
||||
&cmd_opts.surplus_order,
|
||||
cmd_opts.transferable_only,
|
||||
&cmd_opts.exclusion,
|
||||
cmd_opts.meek_nz_exclusion,
|
||||
!cmd_opts.no_early_bulk_elect,
|
||||
cmd_opts.bulk_exclude,
|
||||
cmd_opts.defer_surpluses,
|
||||
cmd_opts.meek_immediate_elect,
|
||||
cmd_opts.constraints.as_deref(),
|
||||
&cmd_opts.constraint_mode,
|
||||
cmd_opts.pp_decimals,
|
||||
);
|
||||
|
||||
// Validate options
|
||||
stv_opts.validate();
|
||||
|
||||
// Describe count
|
||||
let total_ballots = election.ballots.iter().fold(N::zero(), |acc, b| { acc + &b.orig_value });
|
||||
print!("Count computed by OpenTally (revision {}). Read {:.0} ballots from \"{}\" for election \"{}\". There are {} candidates for {} vacancies. ", opentally::VERSION, total_ballots, cmd_opts.filename, election.name, election.candidates.len(), election.seats);
|
||||
let opts_str = stv_opts.describe::<N>();
|
||||
if opts_str.len() > 0 {
|
||||
println!("Counting using options \"{}\".", opts_str);
|
||||
} else {
|
||||
println!("Counting using default options.");
|
||||
}
|
||||
println!();
|
||||
|
||||
// Normalise ballots if requested
|
||||
if cmd_opts.normalise_ballots {
|
||||
election.normalise_ballots();
|
||||
}
|
||||
|
||||
// Initialise count state
|
||||
let mut state = CountState::new(&election);
|
||||
|
||||
// Distribute first preferences
|
||||
stv::count_init(&mut state, &stv_opts).unwrap();
|
||||
let mut stage_num = 1;
|
||||
print_stage(stage_num, &state, &cmd_opts);
|
||||
|
||||
loop {
|
||||
let is_done = stv::count_one_stage(&mut state, &stv_opts);
|
||||
if is_done.unwrap() {
|
||||
break;
|
||||
}
|
||||
stage_num += 1;
|
||||
print_stage(stage_num, &state, &cmd_opts);
|
||||
}
|
||||
|
||||
println!("Count complete. The winning candidates are, in order of election:");
|
||||
|
||||
let mut winners = Vec::new();
|
||||
for (candidate, count_card) in state.candidates.iter() {
|
||||
if count_card.state == CandidateState::Elected {
|
||||
winners.push((candidate, count_card));
|
||||
}
|
||||
}
|
||||
winners.sort_unstable_by(|a, b| a.1.order_elected.cmp(&b.1.order_elected));
|
||||
|
||||
for (i, (winner, count_card)) in winners.into_iter().enumerate() {
|
||||
if let Some(kv) = &count_card.keep_value {
|
||||
println!("{}. {} (kv = {:.dps2$})", i + 1, winner.name, kv, dps2=max(stv_opts.pp_decimals, 2));
|
||||
} else {
|
||||
println!("{}. {}", i + 1, winner.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main_() -> Result<(), i32> {
|
||||
// Read arguments
|
||||
let opts: Opts = Opts::parse();
|
||||
|
||||
return match opts.command {
|
||||
Command::Convert(cmd_opts) => cli::convert::main(cmd_opts),
|
||||
Command::Stv(cmd_opts) => cli::stv::main(cmd_opts),
|
||||
};
|
||||
fn print_candidates<'a, N: 'a + Number, I: Iterator<Item=(&'a Candidate, &'a CountCard<'a, N>)>>(candidates: I, cmd_opts: &STV) {
|
||||
for (candidate, count_card) in candidates {
|
||||
match count_card.state {
|
||||
CandidateState::Hopeful => {
|
||||
println!("- {}: {:.dps$} ({:.dps$})", candidate.name, count_card.votes, count_card.transfers, dps=cmd_opts.pp_decimals);
|
||||
}
|
||||
CandidateState::Guarded => {
|
||||
println!("- {}: {:.dps$} ({:.dps$}) - Guarded", candidate.name, count_card.votes, count_card.transfers, dps=cmd_opts.pp_decimals);
|
||||
}
|
||||
CandidateState::Elected => {
|
||||
if let Some(kv) = &count_card.keep_value {
|
||||
println!("- {}: {:.dps$} ({:.dps$}) - ELECTED {} (kv = {:.dps2$})", candidate.name, count_card.votes, count_card.transfers, count_card.order_elected, kv, dps=cmd_opts.pp_decimals, dps2=max(cmd_opts.pp_decimals, 2));
|
||||
} else {
|
||||
println!("- {}: {:.dps$} ({:.dps$}) - ELECTED {}", candidate.name, count_card.votes, count_card.transfers, count_card.order_elected, dps=cmd_opts.pp_decimals);
|
||||
}
|
||||
}
|
||||
CandidateState::Doomed => {
|
||||
println!("- {}: {:.dps$} ({:.dps$}) - Doomed", candidate.name, count_card.votes, count_card.transfers, dps=cmd_opts.pp_decimals);
|
||||
}
|
||||
CandidateState::Withdrawn => {
|
||||
if !cmd_opts.hide_excluded || !count_card.votes.is_zero() || !count_card.transfers.is_zero() {
|
||||
println!("- {}: {:.dps$} ({:.dps$}) - Withdrawn", candidate.name, count_card.votes, count_card.transfers, dps=cmd_opts.pp_decimals);
|
||||
}
|
||||
}
|
||||
CandidateState::Excluded => {
|
||||
// If --hide-excluded, hide unless nonzero votes or nonzero transfers
|
||||
if !cmd_opts.hide_excluded || !count_card.votes.is_zero() || !count_card.transfers.is_zero() {
|
||||
println!("- {}: {:.dps$} ({:.dps$}) - Excluded {}", candidate.name, count_card.votes, count_card.transfers, -count_card.order_elected, dps=cmd_opts.pp_decimals);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_stage<N: Number>(stage_num: usize, state: &CountState<N>, cmd_opts: &STV) {
|
||||
let logs = state.logger.render();
|
||||
|
||||
// Print stage details
|
||||
match state.kind {
|
||||
None => { println!("{}. {}", stage_num, state.title); }
|
||||
Some(kind) => { println!("{}. {} {}", stage_num, kind, state.title); }
|
||||
};
|
||||
println!("{}", logs.join(" "));
|
||||
|
||||
// Print candidates
|
||||
if cmd_opts.sort_votes {
|
||||
// Sort by votes if requested
|
||||
let mut candidates: Vec<(&Candidate, &CountCard<N>)> = state.candidates.iter()
|
||||
.map(|(c, cc)| (*c, cc)).collect();
|
||||
// First sort by order of election (as a tie-breaker, if votes are equal)
|
||||
candidates.sort_unstable_by(|a, b| b.1.order_elected.cmp(&a.1.order_elected));
|
||||
// Then sort by votes
|
||||
candidates.sort_by(|a, b| a.1.votes.cmp(&b.1.votes));
|
||||
print_candidates(candidates.into_iter().rev(), cmd_opts);
|
||||
} else {
|
||||
let candidates = state.election.candidates.iter()
|
||||
.map(|c| (c, &state.candidates[c]));
|
||||
print_candidates(candidates, cmd_opts);
|
||||
}
|
||||
|
||||
// Print summary rows
|
||||
println!("Exhausted: {:.dps$} ({:.dps$})", state.exhausted.votes, state.exhausted.transfers, dps=cmd_opts.pp_decimals);
|
||||
println!("Loss by fraction: {:.dps$} ({:.dps$})", state.loss_fraction.votes, state.loss_fraction.transfers, dps=cmd_opts.pp_decimals);
|
||||
|
||||
let mut total_vote = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes });
|
||||
total_vote += &state.exhausted.votes;
|
||||
total_vote += &state.loss_fraction.votes;
|
||||
println!("Total votes: {:.dps$}", total_vote, dps=cmd_opts.pp_decimals);
|
||||
|
||||
println!("Quota: {:.dps$}", state.quota.as_ref().unwrap(), dps=cmd_opts.pp_decimals);
|
||||
if cmd_opts.quota_mode == "ers97" {
|
||||
println!("Vote required for election: {:.dps$}", state.vote_required_election.as_ref().unwrap(), dps=cmd_opts.pp_decimals);
|
||||
}
|
||||
|
||||
println!("");
|
||||
}
|
||||
|
|
|
@ -18,40 +18,30 @@
|
|||
use super::{Assign, Fixed, GuardedFixed, NativeFloat64, Number, Rational};
|
||||
|
||||
use num_traits::{Num, One, Zero};
|
||||
//use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
use std::cell::Cell;
|
||||
use std::cmp::{Ord, Ordering};
|
||||
use std::fmt;
|
||||
use std::mem::ManuallyDrop;
|
||||
use std::ops::{self, Deref, DerefMut};
|
||||
|
||||
/// Represents the underlying implementation for [DynNum]s
|
||||
//#[wasm_bindgen]
|
||||
#[derive(Copy, Clone)]
|
||||
#[wasm_bindgen]
|
||||
pub enum NumKind {
|
||||
/// See [crate::numbers::Fixed]
|
||||
Fixed,
|
||||
/// See [crate::numbers::GuardedFixed]
|
||||
GuardedFixed,
|
||||
/// See [crate::numbers::NativeFloat64]
|
||||
NativeFloat64,
|
||||
/// See [crate::numbers::Rational]
|
||||
Rational,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// Determines which underlying implementation to use
|
||||
static KIND: Cell<NumKind> = Cell::new(NumKind::Fixed);
|
||||
}
|
||||
static mut KIND: NumKind = NumKind::Fixed;
|
||||
|
||||
/// Returns which underlying implementation is in use
|
||||
#[inline]
|
||||
fn get_kind() -> NumKind {
|
||||
return KIND.with(|kind_cell| kind_cell.get());
|
||||
fn get_kind() -> &'static NumKind {
|
||||
unsafe {
|
||||
return &KIND;
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper for different numeric types using dynamic dispatch
|
||||
pub union DynNum {
|
||||
fixed: ManuallyDrop<Fixed>,
|
||||
gfixed: ManuallyDrop<GuardedFixed>,
|
||||
|
@ -60,11 +50,10 @@ pub union DynNum {
|
|||
}
|
||||
|
||||
impl DynNum {
|
||||
/// Set which underlying implementation to use
|
||||
pub fn set_kind(kind: NumKind) {
|
||||
KIND.with(|kind_cell| {
|
||||
kind_cell.set(kind);
|
||||
});
|
||||
unsafe {
|
||||
KIND = kind;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,60 +116,10 @@ impl Number for DynNum {
|
|||
}
|
||||
}
|
||||
|
||||
fn parse(str: &str) -> Self {
|
||||
// Separate implementation required as e.g. Fixed from_str_radix does not support decimals
|
||||
match get_kind() {
|
||||
NumKind::Fixed => {
|
||||
DynNum { fixed: ManuallyDrop::new(Fixed::parse(str)) }
|
||||
}
|
||||
NumKind::GuardedFixed => {
|
||||
DynNum { gfixed: ManuallyDrop::new(GuardedFixed::parse(str)) }
|
||||
}
|
||||
NumKind::NativeFloat64 => {
|
||||
DynNum { float64: NativeFloat64::parse(str) }
|
||||
}
|
||||
NumKind::Rational => {
|
||||
DynNum { rational: ManuallyDrop::new(Rational::parse(str)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn describe() -> String { impl_assoc_nowrap!(describe) }
|
||||
fn pow_assign(&mut self, exponent: i32) { impl_1arg_nowrap!(self, exponent, pow_assign) }
|
||||
fn floor_mut(&mut self, dps: usize) { impl_1arg_nowrap!(self, dps, floor_mut) }
|
||||
fn ceil_mut(&mut self, dps: usize) { impl_1arg_nowrap!(self, dps, ceil_mut) }
|
||||
fn round_mut(&mut self, dps: usize) { impl_1arg_nowrap!(self, dps, round_mut) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rounding() {
|
||||
// Must specify scope so references are dropped at the correct time, before KIND is changed
|
||||
{
|
||||
DynNum::set_kind(NumKind::Fixed);
|
||||
Fixed::set_dps(5);
|
||||
let mut x = DynNum::parse("55.557"); x.floor_mut(2); assert_eq!(x, DynNum::parse("55.55"));
|
||||
let mut x = DynNum::parse("55.557"); x.ceil_mut(2); assert_eq!(x, DynNum::parse("55.56"));
|
||||
let mut x = DynNum::parse("55.557"); x.round_mut(2); assert_eq!(x, DynNum::parse("55.56"));
|
||||
}
|
||||
{
|
||||
DynNum::set_kind(NumKind::GuardedFixed);
|
||||
GuardedFixed::set_dps(5);
|
||||
let mut x = DynNum::parse("55.557"); x.floor_mut(2); assert_eq!(x, DynNum::parse("55.55"));
|
||||
let mut x = DynNum::parse("55.557"); x.ceil_mut(2); assert_eq!(x, DynNum::parse("55.56"));
|
||||
let mut x = DynNum::parse("55.557"); x.round_mut(2); assert_eq!(x, DynNum::parse("55.56"));
|
||||
}
|
||||
{
|
||||
DynNum::set_kind(NumKind::NativeFloat64);
|
||||
let mut x = DynNum::parse("55.557"); x.floor_mut(2); assert_eq!(x, DynNum::parse("55.55"));
|
||||
let mut x = DynNum::parse("55.557"); x.ceil_mut(2); assert_eq!(x, DynNum::parse("55.56"));
|
||||
let mut x = DynNum::parse("55.557"); x.round_mut(2); assert_eq!(x, DynNum::parse("55.56"));
|
||||
}
|
||||
{
|
||||
DynNum::set_kind(NumKind::Rational);
|
||||
let mut x = DynNum::parse("55.557"); x.floor_mut(2); assert_eq!(x, DynNum::parse("55.55"));
|
||||
let mut x = DynNum::parse("55.557"); x.ceil_mut(2); assert_eq!(x, DynNum::parse("55.56"));
|
||||
let mut x = DynNum::parse("55.557"); x.round_mut(2); assert_eq!(x, DynNum::parse("55.56"));
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DynNum {
|
||||
|
@ -302,40 +241,6 @@ impl Assign<&Self> for DynNum {
|
|||
fn assign(&mut self, src: &Self) { impl_1other_nowrap_mut!(self, src, assign) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assign() {
|
||||
{
|
||||
DynNum::set_kind(NumKind::Fixed);
|
||||
Fixed::set_dps(2);
|
||||
let a = DynNum::parse("123.45");
|
||||
let b = DynNum::parse("678.90");
|
||||
let mut x = a.clone(); x.assign(b.clone()); assert_eq!(x, b);
|
||||
let mut x = a.clone(); x.assign(&b); assert_eq!(x, b);
|
||||
}
|
||||
{
|
||||
DynNum::set_kind(NumKind::GuardedFixed);
|
||||
GuardedFixed::set_dps(2);
|
||||
let a = DynNum::parse("123.45");
|
||||
let b = DynNum::parse("678.90");
|
||||
let mut x = a.clone(); x.assign(b.clone()); assert_eq!(x, b);
|
||||
let mut x = a.clone(); x.assign(&b); assert_eq!(x, b);
|
||||
}
|
||||
{
|
||||
DynNum::set_kind(NumKind::NativeFloat64);
|
||||
let a = DynNum::parse("123.45");
|
||||
let b = DynNum::parse("678.90");
|
||||
let mut x = a.clone(); x.assign(b.clone()); assert_eq!(x, b);
|
||||
let mut x = a.clone(); x.assign(&b); assert_eq!(x, b);
|
||||
}
|
||||
{
|
||||
DynNum::set_kind(NumKind::Rational);
|
||||
let a = DynNum::parse("123.45");
|
||||
let b = DynNum::parse("678.90");
|
||||
let mut x = a.clone(); x.assign(b.clone()); assert_eq!(x, b);
|
||||
let mut x = a.clone(); x.assign(&b); assert_eq!(x, b);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<usize> for DynNum {
|
||||
fn from(n: usize) -> Self {
|
||||
match get_kind() {
|
||||
|
@ -360,52 +265,7 @@ impl fmt::Display for DynNum {
|
|||
}
|
||||
|
||||
impl fmt::Debug for DynNum {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// Safety: Access only correct union field
|
||||
unsafe {
|
||||
match get_kind() {
|
||||
NumKind::Fixed => {
|
||||
self.fixed.deref().fmt(f)
|
||||
}
|
||||
NumKind::GuardedFixed => {
|
||||
self.gfixed.deref().fmt(f)
|
||||
}
|
||||
NumKind::NativeFloat64 => {
|
||||
self.float64.fmt(f)
|
||||
}
|
||||
NumKind::Rational => {
|
||||
self.rational.deref().fmt(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_debug() {
|
||||
{
|
||||
DynNum::set_kind(NumKind::Fixed);
|
||||
Fixed::set_dps(2);
|
||||
let x = DynNum::parse("123.4"); assert_eq!(format!("{}", x), "123.40");
|
||||
let x = DynNum::parse("123.4"); assert_eq!(format!("{:?}", x), "Fixed(12340)");
|
||||
}
|
||||
{
|
||||
DynNum::set_kind(NumKind::GuardedFixed);
|
||||
GuardedFixed::set_dps(2);
|
||||
let x = DynNum::parse("123.4"); assert_eq!(format!("{}", x), "123.40");
|
||||
let x = DynNum::parse("123.4"); assert_eq!(format!("{:?}", x), "GuardedFixed(1234000)");
|
||||
}
|
||||
{
|
||||
DynNum::set_kind(NumKind::NativeFloat64);
|
||||
let x = DynNum::parse("123.4"); assert_eq!(format!("{}", x), format!("{}", 123.40_f64));
|
||||
let x = DynNum::parse("123.4"); assert_eq!(format!("{:?}", x), format!("NativeFloat64({})", 123.40_f64));
|
||||
}
|
||||
{
|
||||
DynNum::set_kind(NumKind::Rational);
|
||||
let x = DynNum::parse("123.4"); assert_eq!(format!("{}", x), "617/5");
|
||||
let x = DynNum::parse("123.4"); assert_eq!(format!("{:.2}", x), "123.40");
|
||||
let x = DynNum::parse("123.4"); assert_eq!(format!("{:?}", x), "Rational(617/5)");
|
||||
}
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { impl_1arg_nowrap!(self, f, fmt) }
|
||||
}
|
||||
|
||||
impl PartialEq for DynNum {
|
||||
|
@ -525,52 +385,6 @@ impl ops::Rem for DynNum {
|
|||
fn rem(self, rhs: Self) -> Self::Output { impl_1other_wrap!(self, rhs, rem) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arith_owned_owned() {
|
||||
{
|
||||
DynNum::set_kind(NumKind::Fixed);
|
||||
Fixed::set_dps(2);
|
||||
let a = DynNum::parse("123.45");
|
||||
let b = DynNum::parse("678.90");
|
||||
assert_eq!(a.clone() + b.clone(), DynNum::parse("802.35"));
|
||||
assert_eq!(a.clone() - b.clone(), DynNum::parse("-555.45"));
|
||||
assert_eq!(a.clone() * b.clone(), DynNum::parse("83810.20")); // = 83810.205 rounds to 83810.20
|
||||
assert_eq!(a.clone() / b.clone(), DynNum::parse("0.18"));
|
||||
assert_eq!(b.clone() % a.clone(), DynNum::parse("61.65"));
|
||||
}
|
||||
{
|
||||
DynNum::set_kind(NumKind::GuardedFixed);
|
||||
GuardedFixed::set_dps(2);
|
||||
let a = DynNum::parse("123.45");
|
||||
let b = DynNum::parse("678.90");
|
||||
assert_eq!(a.clone() + b.clone(), DynNum::parse("802.35"));
|
||||
assert_eq!(a.clone() - b.clone(), DynNum::parse("-555.45"));
|
||||
assert_eq!(a.clone() * b.clone(), DynNum::parse("83810.205")); // Must compare to 3 d.p.s as doesn't meet FACTOR_CMP
|
||||
assert_eq!(a.clone() / b.clone(), DynNum::parse("0.18")); // Meets FACTOR_CMP so compare only 2 d.p.s
|
||||
assert_eq!(b.clone() % a.clone(), DynNum::parse("61.65"));
|
||||
}
|
||||
{
|
||||
DynNum::set_kind(NumKind::NativeFloat64);
|
||||
let a = DynNum::parse("123.45");
|
||||
let b = DynNum::parse("678.90");
|
||||
assert_eq!(a.clone() + b.clone(), DynNum { float64: NativeFloat64::from(123.45_f64 + 678.90_f64) });
|
||||
assert_eq!(a.clone() - b.clone(), DynNum { float64: NativeFloat64::from(123.45_f64 - 678.90_f64) });
|
||||
assert_eq!(a.clone() * b.clone(), DynNum { float64: NativeFloat64::from(123.45_f64 * 678.90_f64) });
|
||||
assert_eq!(a.clone() / b.clone(), DynNum { float64: NativeFloat64::from(123.45_f64 / 678.90_f64) });
|
||||
assert_eq!(b.clone() % a.clone(), DynNum { float64: NativeFloat64::from(678.90_f64 % 123.45_f64) });
|
||||
}
|
||||
{
|
||||
DynNum::set_kind(NumKind::Rational);
|
||||
let a = DynNum::parse("123.45");
|
||||
let b = DynNum::parse("678.90");
|
||||
assert_eq!(a.clone() + b.clone(), DynNum::parse("802.35"));
|
||||
assert_eq!(a.clone() - b.clone(), DynNum::parse("-555.45"));
|
||||
assert_eq!(a.clone() * b.clone(), DynNum::parse("83810.205"));
|
||||
assert_eq!((a.clone() / b.clone()) * b.clone(), a);
|
||||
assert_eq!(b.clone() % a.clone(), DynNum::parse("61.65"));
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Add<&Self> for DynNum {
|
||||
type Output = Self;
|
||||
fn add(self, rhs: &Self) -> Self::Output { impl_1other_wrap!(self, rhs, add) }
|
||||
|
@ -596,52 +410,6 @@ impl ops::Rem<&Self> for DynNum {
|
|||
fn rem(self, rhs: &Self) -> Self::Output { impl_1other_wrap!(self, rhs, rem) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arith_owned_ref() {
|
||||
{
|
||||
DynNum::set_kind(NumKind::Fixed);
|
||||
Fixed::set_dps(2);
|
||||
let a = DynNum::parse("123.45");
|
||||
let b = DynNum::parse("678.90");
|
||||
assert_eq!(a.clone() + &b, DynNum::parse("802.35"));
|
||||
assert_eq!(a.clone() - &b, DynNum::parse("-555.45"));
|
||||
assert_eq!(a.clone() * &b, DynNum::parse("83810.20"));
|
||||
assert_eq!(a.clone() / &b, DynNum::parse("0.18"));
|
||||
assert_eq!(b.clone() % &a, DynNum::parse("61.65"));
|
||||
}
|
||||
{
|
||||
DynNum::set_kind(NumKind::GuardedFixed);
|
||||
GuardedFixed::set_dps(2);
|
||||
let a = DynNum::parse("123.45");
|
||||
let b = DynNum::parse("678.90");
|
||||
assert_eq!(a.clone() + &b, DynNum::parse("802.35"));
|
||||
assert_eq!(a.clone() - &b, DynNum::parse("-555.45"));
|
||||
assert_eq!(a.clone() * &b, DynNum::parse("83810.205"));
|
||||
assert_eq!(a.clone() / &b, DynNum::parse("0.18"));
|
||||
assert_eq!(b.clone() % &a, DynNum::parse("61.65"));
|
||||
}
|
||||
{
|
||||
DynNum::set_kind(NumKind::NativeFloat64);
|
||||
let a = DynNum::parse("123.45");
|
||||
let b = DynNum::parse("678.90");
|
||||
assert_eq!(a.clone() + &b, DynNum { float64: NativeFloat64::from(123.45_f64 + 678.90_f64) });
|
||||
assert_eq!(a.clone() - &b, DynNum { float64: NativeFloat64::from(123.45_f64 - 678.90_f64) });
|
||||
assert_eq!(a.clone() * &b, DynNum { float64: NativeFloat64::from(123.45_f64 * 678.90_f64) });
|
||||
assert_eq!(a.clone() / &b, DynNum { float64: NativeFloat64::from(123.45_f64 / 678.90_f64) });
|
||||
assert_eq!(b.clone() % &a, DynNum { float64: NativeFloat64::from(678.90_f64 % 123.45_f64) });
|
||||
}
|
||||
{
|
||||
DynNum::set_kind(NumKind::Rational);
|
||||
let a = DynNum::parse("123.45");
|
||||
let b = DynNum::parse("678.90");
|
||||
assert_eq!(a.clone() + &b, DynNum::parse("802.35"));
|
||||
assert_eq!(a.clone() - &b, DynNum::parse("-555.45"));
|
||||
assert_eq!(a.clone() * &b, DynNum::parse("83810.205"));
|
||||
assert_eq!((a.clone() / &b) * &b, a);
|
||||
assert_eq!(b.clone() % &a, DynNum::parse("61.65"));
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::AddAssign for DynNum {
|
||||
fn add_assign(&mut self, rhs: Self) { impl_1other_nowrap_mut!(self, rhs, add_assign) }
|
||||
}
|
||||
|
@ -662,52 +430,6 @@ impl ops::RemAssign for DynNum {
|
|||
fn rem_assign(&mut self, rhs: Self) { impl_1other_nowrap_mut!(self, rhs, rem_assign) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arithassign_owned() {
|
||||
{
|
||||
DynNum::set_kind(NumKind::Fixed);
|
||||
Fixed::set_dps(2);
|
||||
let a = DynNum::parse("123.45");
|
||||
let b = DynNum::parse("678.90");
|
||||
let mut x = a.clone(); x += b.clone(); assert_eq!(x, DynNum::parse("802.35"));
|
||||
let mut x = a.clone(); x -= b.clone(); assert_eq!(x, DynNum::parse("-555.45"));
|
||||
let mut x = a.clone(); x *= b.clone(); assert_eq!(x, DynNum::parse("83810.20"));
|
||||
let mut x = a.clone(); x /= b.clone(); assert_eq!(x, DynNum::parse("0.18"));
|
||||
let mut x = b.clone(); x %= a.clone(); assert_eq!(x, DynNum::parse("61.65"));
|
||||
}
|
||||
{
|
||||
DynNum::set_kind(NumKind::GuardedFixed);
|
||||
GuardedFixed::set_dps(2);
|
||||
let a = DynNum::parse("123.45");
|
||||
let b = DynNum::parse("678.90");
|
||||
let mut x = a.clone(); x += b.clone(); assert_eq!(x, DynNum::parse("802.35"));
|
||||
let mut x = a.clone(); x -= b.clone(); assert_eq!(x, DynNum::parse("-555.45"));
|
||||
let mut x = a.clone(); x *= b.clone(); assert_eq!(x, DynNum::parse("83810.205"));
|
||||
let mut x = a.clone(); x /= b.clone(); assert_eq!(x, DynNum::parse("0.18"));
|
||||
let mut x = b.clone(); x %= a.clone(); assert_eq!(x, DynNum::parse("61.65"));
|
||||
}
|
||||
{
|
||||
DynNum::set_kind(NumKind::NativeFloat64);
|
||||
let a = DynNum::parse("123.45");
|
||||
let b = DynNum::parse("678.90");
|
||||
let mut x = a.clone(); x += b.clone(); assert_eq!(x, DynNum { float64: NativeFloat64::from(123.45_f64 + 678.90_f64) });
|
||||
let mut x = a.clone(); x -= b.clone(); assert_eq!(x, DynNum { float64: NativeFloat64::from(123.45_f64 - 678.90_f64) });
|
||||
let mut x = a.clone(); x *= b.clone(); assert_eq!(x, DynNum { float64: NativeFloat64::from(123.45_f64 * 678.90_f64) });
|
||||
let mut x = a.clone(); x /= b.clone(); assert_eq!(x, DynNum { float64: NativeFloat64::from(123.45_f64 / 678.90_f64) });
|
||||
let mut x = b.clone(); x %= a.clone(); assert_eq!(x, DynNum { float64: NativeFloat64::from(678.90_f64 % 123.45_f64) });
|
||||
}
|
||||
{
|
||||
DynNum::set_kind(NumKind::Rational);
|
||||
let a = DynNum::parse("123.45");
|
||||
let b = DynNum::parse("678.90");
|
||||
let mut x = a.clone(); x += b.clone(); assert_eq!(x, DynNum::parse("802.35"));
|
||||
let mut x = a.clone(); x -= b.clone(); assert_eq!(x, DynNum::parse("-555.45"));
|
||||
let mut x = a.clone(); x *= b.clone(); assert_eq!(x, DynNum::parse("83810.205"));
|
||||
let mut x = a.clone(); x /= b.clone(); x *= &b; assert_eq!(x, a);
|
||||
let mut x = b.clone(); x %= a.clone(); assert_eq!(x, DynNum::parse("61.65"));
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::AddAssign<&Self> for DynNum {
|
||||
fn add_assign(&mut self, rhs: &Self) { impl_1other_nowrap_mut!(self, rhs, add_assign) }
|
||||
}
|
||||
|
@ -728,52 +450,6 @@ impl ops::RemAssign<&Self> for DynNum {
|
|||
fn rem_assign(&mut self, rhs: &Self) { impl_1other_nowrap_mut!(self, rhs, rem_assign) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arithassign_ref() {
|
||||
{
|
||||
DynNum::set_kind(NumKind::Fixed);
|
||||
Fixed::set_dps(2);
|
||||
let a = DynNum::parse("123.45");
|
||||
let b = DynNum::parse("678.90");
|
||||
let mut x = a.clone(); x += &b; assert_eq!(x, DynNum::parse("802.35"));
|
||||
let mut x = a.clone(); x -= &b; assert_eq!(x, DynNum::parse("-555.45"));
|
||||
let mut x = a.clone(); x *= &b; assert_eq!(x, DynNum::parse("83810.20"));
|
||||
let mut x = a.clone(); x /= &b; assert_eq!(x, DynNum::parse("0.18"));
|
||||
let mut x = b.clone(); x %= &a; assert_eq!(x, DynNum::parse("61.65"));
|
||||
}
|
||||
{
|
||||
DynNum::set_kind(NumKind::GuardedFixed);
|
||||
GuardedFixed::set_dps(2);
|
||||
let a = DynNum::parse("123.45");
|
||||
let b = DynNum::parse("678.90");
|
||||
let mut x = a.clone(); x += &b; assert_eq!(x, DynNum::parse("802.35"));
|
||||
let mut x = a.clone(); x -= &b; assert_eq!(x, DynNum::parse("-555.45"));
|
||||
let mut x = a.clone(); x *= &b; assert_eq!(x, DynNum::parse("83810.205"));
|
||||
let mut x = a.clone(); x /= &b; assert_eq!(x, DynNum::parse("0.18"));
|
||||
let mut x = b.clone(); x %= &a; assert_eq!(x, DynNum::parse("61.65"));
|
||||
}
|
||||
{
|
||||
DynNum::set_kind(NumKind::NativeFloat64);
|
||||
let a = DynNum::parse("123.45");
|
||||
let b = DynNum::parse("678.90");
|
||||
let mut x = a.clone(); x += &b; assert_eq!(x, DynNum { float64: NativeFloat64::from(123.45_f64 + 678.90_f64) });
|
||||
let mut x = a.clone(); x -= &b; assert_eq!(x, DynNum { float64: NativeFloat64::from(123.45_f64 - 678.90_f64) });
|
||||
let mut x = a.clone(); x *= &b; assert_eq!(x, DynNum { float64: NativeFloat64::from(123.45_f64 * 678.90_f64) });
|
||||
let mut x = a.clone(); x /= &b; assert_eq!(x, DynNum { float64: NativeFloat64::from(123.45_f64 / 678.90_f64) });
|
||||
let mut x = b.clone(); x %= &a; assert_eq!(x, DynNum { float64: NativeFloat64::from(678.90_f64 % 123.45_f64) });
|
||||
}
|
||||
{
|
||||
DynNum::set_kind(NumKind::Rational);
|
||||
let a = DynNum::parse("123.45");
|
||||
let b = DynNum::parse("678.90");
|
||||
let mut x = a.clone(); x += &b; assert_eq!(x, DynNum::parse("802.35"));
|
||||
let mut x = a.clone(); x -= &b; assert_eq!(x, DynNum::parse("-555.45"));
|
||||
let mut x = a.clone(); x *= &b; assert_eq!(x, DynNum::parse("83810.205"));
|
||||
let mut x = a.clone(); x /= &b; x *= &b; assert_eq!(x, a);
|
||||
let mut x = b.clone(); x %= &a; assert_eq!(x, DynNum::parse("61.65"));
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Neg for &DynNum {
|
||||
type Output = DynNum;
|
||||
fn neg(self) -> Self::Output { impl_0arg_wrap!(self, neg) }
|
||||
|
@ -804,52 +480,6 @@ impl ops::Rem<Self> for &DynNum {
|
|||
fn rem(self, rhs: Self) -> Self::Output { impl_1other_wrap!(self, rhs, rem) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arith_ref_ref() {
|
||||
{
|
||||
DynNum::set_kind(NumKind::Fixed);
|
||||
Fixed::set_dps(2);
|
||||
let a = DynNum::parse("123.45");
|
||||
let b = DynNum::parse("678.90");
|
||||
assert_eq!(&a + &b, DynNum::parse("802.35"));
|
||||
assert_eq!(&a - &b, DynNum::parse("-555.45"));
|
||||
assert_eq!(&a * &b, DynNum::parse("83810.20"));
|
||||
assert_eq!(&a / &b, DynNum::parse("0.18"));
|
||||
assert_eq!(&b % &a, DynNum::parse("61.65"));
|
||||
}
|
||||
{
|
||||
DynNum::set_kind(NumKind::GuardedFixed);
|
||||
GuardedFixed::set_dps(2);
|
||||
let a = DynNum::parse("123.45");
|
||||
let b = DynNum::parse("678.90");
|
||||
assert_eq!(&a + &b, DynNum::parse("802.35"));
|
||||
assert_eq!(&a - &b, DynNum::parse("-555.45"));
|
||||
assert_eq!(&a * &b, DynNum::parse("83810.205"));
|
||||
assert_eq!(&a / &b, DynNum::parse("0.18"));
|
||||
assert_eq!(&b % &a, DynNum::parse("61.65"));
|
||||
}
|
||||
{
|
||||
DynNum::set_kind(NumKind::NativeFloat64);
|
||||
let a = DynNum::parse("123.45");
|
||||
let b = DynNum::parse("678.90");
|
||||
assert_eq!(&a + &b, DynNum { float64: NativeFloat64::from(123.45_f64 + 678.90_f64) });
|
||||
assert_eq!(&a - &b, DynNum { float64: NativeFloat64::from(123.45_f64 - 678.90_f64) });
|
||||
assert_eq!(&a * &b, DynNum { float64: NativeFloat64::from(123.45_f64 * 678.90_f64) });
|
||||
assert_eq!(&a / &b, DynNum { float64: NativeFloat64::from(123.45_f64 / 678.90_f64) });
|
||||
assert_eq!(&b % &a, DynNum { float64: NativeFloat64::from(678.90_f64 % 123.45_f64) });
|
||||
}
|
||||
{
|
||||
DynNum::set_kind(NumKind::Rational);
|
||||
let a = DynNum::parse("123.45");
|
||||
let b = DynNum::parse("678.90");
|
||||
assert_eq!(&a + &b, DynNum::parse("802.35"));
|
||||
assert_eq!(&a - &b, DynNum::parse("-555.45"));
|
||||
assert_eq!(&a * &b, DynNum::parse("83810.205"));
|
||||
assert_eq!((&a / &b) * &b, a);
|
||||
assert_eq!(&b % &a, DynNum::parse("61.65"));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
impl ops::Add<&&Rational> for &Rational {
|
||||
|
||||
|
|
|
@ -18,29 +18,23 @@
|
|||
use super::{Assign, Number};
|
||||
|
||||
use ibig::{IBig, ops::Abs};
|
||||
use num_traits::{Num, One, Signed, Zero};
|
||||
use num_traits::{Num, One, Zero};
|
||||
|
||||
use std::cell::{Cell, UnsafeCell};
|
||||
use std::cmp::{Ord, PartialEq, PartialOrd};
|
||||
use std::ops;
|
||||
use std::fmt;
|
||||
|
||||
thread_local! {
|
||||
static DPS: Cell<usize> = Cell::new(5);
|
||||
static FACTOR: UnsafeCell<IBig> = UnsafeCell::new(IBig::from(10).pow(5));
|
||||
}
|
||||
static mut DPS: Option<usize> = None;
|
||||
static mut FACTOR: Option<IBig> = None;
|
||||
|
||||
#[inline]
|
||||
pub fn get_dps() -> usize {
|
||||
return DPS.with(|dps_cell| dps_cell.get());
|
||||
unsafe { DPS.unwrap() }
|
||||
}
|
||||
|
||||
fn get_factor<'a>() -> &'a IBig {
|
||||
FACTOR.with(|factor_cell| {
|
||||
let factor_ptr = factor_cell.get();
|
||||
// SAFETY: Safe if requirements of Fixed::set_dps met
|
||||
let factor_ref = unsafe { &*factor_ptr };
|
||||
return factor_ref;
|
||||
})
|
||||
#[inline]
|
||||
fn get_factor() -> &'static IBig {
|
||||
unsafe { FACTOR.as_ref().unwrap() }
|
||||
}
|
||||
|
||||
/// Fixed-point number
|
||||
|
@ -49,20 +43,11 @@ pub struct Fixed(IBig);
|
|||
|
||||
impl Fixed {
|
||||
/// Set the number of decimal places to compute results to
|
||||
///
|
||||
/// SAFETY: This must be called before, and never after, any operations on [Fixed].
|
||||
pub fn set_dps(dps: usize) {
|
||||
DPS.with(|dps_cell| {
|
||||
dps_cell.set(dps);
|
||||
});
|
||||
FACTOR.with(|factor_cell| {
|
||||
let factor = IBig::from(10).pow(dps);
|
||||
let factor_ptr = factor_cell.get();
|
||||
// SAFETY: Safe if requirements above met
|
||||
unsafe {
|
||||
*factor_ptr = factor;
|
||||
}
|
||||
});
|
||||
unsafe {
|
||||
DPS = Some(dps);
|
||||
FACTOR = Some(IBig::from(10).pow(dps));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,9 +57,6 @@ impl Number for Fixed {
|
|||
fn describe() -> String { format!("--numbers fixed --decimals {}", get_dps()) }
|
||||
|
||||
fn pow_assign(&mut self, exponent: i32) {
|
||||
if exponent < 0 {
|
||||
todo!();
|
||||
}
|
||||
self.0 = self.0.pow(exponent as usize) * get_factor() / get_factor().pow(exponent as usize);
|
||||
}
|
||||
|
||||
|
@ -98,17 +80,6 @@ impl Number for Fixed {
|
|||
}
|
||||
}
|
||||
|
||||
fn round_mut(&mut self, dps: usize) {
|
||||
// Only do something if truncating
|
||||
if dps < get_dps() {
|
||||
let mut factor = IBig::from(10).pow(get_dps() - dps);
|
||||
factor /= IBig::from(2);
|
||||
|
||||
self.0 += factor;
|
||||
self.floor_mut(dps);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Self {
|
||||
// Parse decimal
|
||||
if s.contains('.') {
|
||||
|
@ -121,12 +92,7 @@ impl Number for Fixed {
|
|||
Ok(value) => value,
|
||||
Err(_) => panic!("Syntax Error"),
|
||||
} * get_factor() / IBig::from(10).pow(decimal.len());
|
||||
|
||||
if whole.is_negative() {
|
||||
return Self(whole - decimal);
|
||||
} else {
|
||||
return Self(whole + decimal);
|
||||
}
|
||||
return Self(whole + decimal);
|
||||
}
|
||||
|
||||
// Parse integer
|
||||
|
@ -138,26 +104,6 @@ impl Number for Fixed {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rounding() {
|
||||
Fixed::set_dps(5);
|
||||
|
||||
let mut x = Fixed::parse("55.550"); x.floor_mut(2); assert_eq!(x, Fixed::parse("55.55"));
|
||||
let mut x = Fixed::parse("55.552"); x.floor_mut(2); assert_eq!(x, Fixed::parse("55.55"));
|
||||
let mut x = Fixed::parse("55.555"); x.floor_mut(2); assert_eq!(x, Fixed::parse("55.55"));
|
||||
let mut x = Fixed::parse("55.557"); x.floor_mut(2); assert_eq!(x, Fixed::parse("55.55"));
|
||||
|
||||
let mut x = Fixed::parse("55.550"); x.ceil_mut(2); assert_eq!(x, Fixed::parse("55.55"));
|
||||
let mut x = Fixed::parse("55.552"); x.ceil_mut(2); assert_eq!(x, Fixed::parse("55.56"));
|
||||
let mut x = Fixed::parse("55.555"); x.ceil_mut(2); assert_eq!(x, Fixed::parse("55.56"));
|
||||
let mut x = Fixed::parse("55.557"); x.ceil_mut(2); assert_eq!(x, Fixed::parse("55.56"));
|
||||
|
||||
let mut x = Fixed::parse("55.550"); x.round_mut(2); assert_eq!(x, Fixed::parse("55.55"));
|
||||
let mut x = Fixed::parse("55.552"); x.round_mut(2); assert_eq!(x, Fixed::parse("55.55"));
|
||||
let mut x = Fixed::parse("55.555"); x.round_mut(2); assert_eq!(x, Fixed::parse("55.56"));
|
||||
let mut x = Fixed::parse("55.557"); x.round_mut(2); assert_eq!(x, Fixed::parse("55.56"));
|
||||
}
|
||||
|
||||
impl Num for Fixed {
|
||||
type FromStrRadixErr = ibig::error::ParseError;
|
||||
fn from_str_radix(str: &str, radix: u32) -> Result<Self, Self::FromStrRadixErr> {
|
||||
|
@ -176,16 +122,6 @@ impl Assign<&Self> for Fixed {
|
|||
fn assign(&mut self, src: &Self) { self.0 = src.0.clone() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assign() {
|
||||
Fixed::set_dps(2);
|
||||
let a = Fixed::parse("123.45");
|
||||
let b = Fixed::parse("678.90");
|
||||
|
||||
let mut x = a.clone(); x.assign(b.clone()); assert_eq!(x, b);
|
||||
let mut x = a.clone(); x.assign(&b); assert_eq!(x, b);
|
||||
}
|
||||
|
||||
impl From<usize> for Fixed {
|
||||
fn from(n: usize) -> Self { Self(IBig::from(n) * get_factor()) }
|
||||
}
|
||||
|
@ -229,13 +165,6 @@ impl fmt::Display for Fixed {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_debug() {
|
||||
Fixed::set_dps(2);
|
||||
let x = Fixed::parse("123.4"); assert_eq!(format!("{}", x), "123.40");
|
||||
let x = Fixed::parse("123.4"); assert_eq!(format!("{:?}", x), "Fixed(12340)");
|
||||
}
|
||||
|
||||
impl One for Fixed {
|
||||
fn one() -> Self { Self(get_factor().clone()) }
|
||||
}
|
||||
|
@ -257,12 +186,16 @@ impl ops::Add for Fixed {
|
|||
|
||||
impl ops::Sub for Fixed {
|
||||
type Output = Self;
|
||||
fn sub(self, rhs: Self) -> Self::Output { Self(self.0 - rhs.0) }
|
||||
fn sub(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul for Fixed {
|
||||
type Output = Self;
|
||||
fn mul(self, rhs: Self) -> Self::Output { Self(self.0 * rhs.0 / get_factor()) }
|
||||
fn mul(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Div for Fixed {
|
||||
|
@ -272,20 +205,9 @@ impl ops::Div for Fixed {
|
|||
|
||||
impl ops::Rem for Fixed {
|
||||
type Output = Self;
|
||||
fn rem(self, rhs: Self) -> Self::Output { Self(self.0 % rhs.0) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arith_owned_owned() {
|
||||
Fixed::set_dps(2);
|
||||
let a = Fixed::parse("123.45");
|
||||
let b = Fixed::parse("678.90");
|
||||
|
||||
assert_eq!(a.clone() + b.clone(), Fixed::parse("802.35"));
|
||||
assert_eq!(a.clone() - b.clone(), Fixed::parse("-555.45"));
|
||||
assert_eq!(a.clone() * b.clone(), Fixed::parse("83810.20")); // = 83810.205 rounds to 83810.20
|
||||
assert_eq!(a.clone() / b.clone(), Fixed::parse("0.18"));
|
||||
assert_eq!(b.clone() % a.clone(), Fixed::parse("61.65"));
|
||||
fn rem(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Add<&Self> for Fixed {
|
||||
|
@ -310,20 +232,9 @@ impl ops::Div<&Self> for Fixed {
|
|||
|
||||
impl ops::Rem<&Self> for Fixed {
|
||||
type Output = Self;
|
||||
fn rem(self, rhs: &Self) -> Self::Output { Self(self.0 % &rhs.0) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arith_owned_ref() {
|
||||
Fixed::set_dps(2);
|
||||
let a = Fixed::parse("123.45");
|
||||
let b = Fixed::parse("678.90");
|
||||
|
||||
assert_eq!(a.clone() + &b, Fixed::parse("802.35"));
|
||||
assert_eq!(a.clone() - &b, Fixed::parse("-555.45"));
|
||||
assert_eq!(a.clone() * &b, Fixed::parse("83810.20"));
|
||||
assert_eq!(a.clone() / &b, Fixed::parse("0.18"));
|
||||
assert_eq!(b.clone() % &a, Fixed::parse("61.65"));
|
||||
fn rem(self, _rhs: &Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::AddAssign for Fixed {
|
||||
|
@ -349,20 +260,9 @@ impl ops::DivAssign for Fixed {
|
|||
}
|
||||
|
||||
impl ops::RemAssign for Fixed {
|
||||
fn rem_assign(&mut self, rhs: Self) { self.0 %= rhs.0; }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arithassign_owned() {
|
||||
Fixed::set_dps(2);
|
||||
let a = Fixed::parse("123.45");
|
||||
let b = Fixed::parse("678.90");
|
||||
|
||||
let mut x = a.clone(); x += b.clone(); assert_eq!(x, Fixed::parse("802.35"));
|
||||
let mut x = a.clone(); x -= b.clone(); assert_eq!(x, Fixed::parse("-555.45"));
|
||||
let mut x = a.clone(); x *= b.clone(); assert_eq!(x, Fixed::parse("83810.20"));
|
||||
let mut x = a.clone(); x /= b.clone(); assert_eq!(x, Fixed::parse("0.18"));
|
||||
let mut x = b.clone(); x %= a.clone(); assert_eq!(x, Fixed::parse("61.65"));
|
||||
fn rem_assign(&mut self, _rhs: Self) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::AddAssign<&Self> for Fixed {
|
||||
|
@ -388,20 +288,9 @@ impl ops::DivAssign<&Self> for Fixed {
|
|||
}
|
||||
|
||||
impl ops::RemAssign<&Self> for Fixed {
|
||||
fn rem_assign(&mut self, rhs: &Self) { self.0 %= &rhs.0; }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arithassign_ref() {
|
||||
Fixed::set_dps(2);
|
||||
let a = Fixed::parse("123.45");
|
||||
let b = Fixed::parse("678.90");
|
||||
|
||||
let mut x = a.clone(); x += &b; assert_eq!(x, Fixed::parse("802.35"));
|
||||
let mut x = a.clone(); x -= &b; assert_eq!(x, Fixed::parse("-555.45"));
|
||||
let mut x = a.clone(); x *= &b; assert_eq!(x, Fixed::parse("83810.20"));
|
||||
let mut x = a.clone(); x /= &b; assert_eq!(x, Fixed::parse("0.18"));
|
||||
let mut x = b.clone(); x %= &a; assert_eq!(x, Fixed::parse("61.65"));
|
||||
fn rem_assign(&mut self, _rhs: &Self) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Neg for &Fixed {
|
||||
|
@ -431,20 +320,9 @@ impl ops::Div<Self> for &Fixed {
|
|||
|
||||
impl ops::Rem<Self> for &Fixed {
|
||||
type Output = Fixed;
|
||||
fn rem(self, rhs: Self) -> Self::Output { Fixed(&self.0 % &rhs.0) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arith_ref_ref() {
|
||||
Fixed::set_dps(2);
|
||||
let a = Fixed::parse("123.45");
|
||||
let b = Fixed::parse("678.90");
|
||||
|
||||
assert_eq!(&a + &b, Fixed::parse("802.35"));
|
||||
assert_eq!(&a - &b, Fixed::parse("-555.45"));
|
||||
assert_eq!(&a * &b, Fixed::parse("83810.20"));
|
||||
assert_eq!(&a / &b, Fixed::parse("0.18"));
|
||||
assert_eq!(&b % &a, Fixed::parse("61.65"));
|
||||
fn rem(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -18,39 +18,29 @@
|
|||
use super::{Assign, Number};
|
||||
|
||||
use ibig::{IBig, ops::Abs};
|
||||
use num_traits::{Num, One, Signed, Zero};
|
||||
use num_traits::{Num, One, Zero};
|
||||
|
||||
use std::cell::{Cell, UnsafeCell};
|
||||
use std::cmp::{Ord, Ordering, PartialEq, PartialOrd};
|
||||
use std::ops;
|
||||
use std::fmt;
|
||||
|
||||
thread_local! {
|
||||
static DPS: Cell<usize> = Cell::new(5);
|
||||
static FACTOR: UnsafeCell<IBig> = UnsafeCell::new(IBig::from(10).pow(10));
|
||||
static FACTOR_CMP: UnsafeCell<IBig> = UnsafeCell::new(IBig::from(10).pow(5) / IBig::from(2));
|
||||
}
|
||||
static mut DPS: Option<usize> = None;
|
||||
static mut FACTOR: Option<IBig> = None;
|
||||
static mut FACTOR_CMP: Option<IBig> = None;
|
||||
|
||||
#[inline]
|
||||
pub fn get_dps() -> usize {
|
||||
return DPS.with(|dps_cell| dps_cell.get());
|
||||
unsafe { DPS.unwrap() }
|
||||
}
|
||||
|
||||
fn get_factor<'a>() -> &'a IBig {
|
||||
FACTOR.with(|factor_cell| {
|
||||
let factor_ptr = factor_cell.get();
|
||||
// SAFETY: Safe if requirements of GuardedFixed::set_dps met
|
||||
let factor_ref = unsafe { &*factor_ptr };
|
||||
return factor_ref;
|
||||
})
|
||||
#[inline]
|
||||
fn get_factor() -> &'static IBig {
|
||||
unsafe { FACTOR.as_ref().unwrap() }
|
||||
}
|
||||
|
||||
fn get_factor_cmp<'a>() -> &'a IBig {
|
||||
FACTOR_CMP.with(|factor_cmp_cell| {
|
||||
let factor_cmp_ptr = factor_cmp_cell.get();
|
||||
// SAFETY: Safe if requirements of Fixed::set_dps met
|
||||
let factor_cmp_ref = unsafe { &*factor_cmp_ptr };
|
||||
return factor_cmp_ref;
|
||||
})
|
||||
#[inline]
|
||||
fn get_factor_cmp() -> &'static IBig {
|
||||
unsafe { FACTOR_CMP.as_ref().unwrap() }
|
||||
}
|
||||
|
||||
/// Guarded fixed-point number
|
||||
|
@ -59,28 +49,12 @@ pub struct GuardedFixed(IBig);
|
|||
|
||||
impl GuardedFixed {
|
||||
/// Set the number of decimal places to compute results to
|
||||
///
|
||||
/// SAFETY: This must be called before, and never after, any operations on [GuardedFixed].
|
||||
pub fn set_dps(dps: usize) {
|
||||
DPS.with(|dps_cell| {
|
||||
dps_cell.set(dps);
|
||||
});
|
||||
FACTOR.with(|factor_cell| {
|
||||
let factor = IBig::from(10).pow(dps * 2);
|
||||
let factor_ptr = factor_cell.get();
|
||||
// SAFETY: Safe if requirements above met
|
||||
unsafe {
|
||||
*factor_ptr = factor;
|
||||
}
|
||||
});
|
||||
FACTOR_CMP.with(|factor_cmp_cell| {
|
||||
let factor_cmp = IBig::from(10).pow(dps) / IBig::from(2);
|
||||
let factor_cmp_ptr = factor_cmp_cell.get();
|
||||
// SAFETY: Safe if requirements above met
|
||||
unsafe {
|
||||
*factor_cmp_ptr = factor_cmp;
|
||||
}
|
||||
});
|
||||
unsafe {
|
||||
DPS = Some(dps);
|
||||
FACTOR = Some(IBig::from(10).pow(dps * 2));
|
||||
FACTOR_CMP = Some(IBig::from(10).pow(dps) / IBig::from(2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,9 +64,6 @@ impl Number for GuardedFixed {
|
|||
fn describe() -> String { format!("--numbers gfixed --decimals {}", get_dps()) }
|
||||
|
||||
fn pow_assign(&mut self, exponent: i32) {
|
||||
if exponent < 0 {
|
||||
todo!();
|
||||
}
|
||||
self.0 = self.0.pow(exponent as usize) * get_factor() / get_factor().pow(exponent as usize);
|
||||
}
|
||||
|
||||
|
@ -116,17 +87,6 @@ impl Number for GuardedFixed {
|
|||
}
|
||||
}
|
||||
|
||||
fn round_mut(&mut self, dps: usize) {
|
||||
// Only do something if truncating
|
||||
if dps < get_dps() * 2 {
|
||||
let mut factor = IBig::from(10).pow(get_dps() * 2 - dps);
|
||||
factor /= IBig::from(2);
|
||||
|
||||
self.0 += factor;
|
||||
self.floor_mut(dps);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Self {
|
||||
// Parse decimal
|
||||
if s.contains('.') {
|
||||
|
@ -139,12 +99,7 @@ impl Number for GuardedFixed {
|
|||
Ok(value) => value,
|
||||
Err(_) => panic!("Syntax Error"),
|
||||
} * get_factor() / IBig::from(10).pow(decimal.len());
|
||||
|
||||
if whole.is_negative() {
|
||||
return Self(whole - decimal);
|
||||
} else {
|
||||
return Self(whole + decimal);
|
||||
}
|
||||
return Self(whole + decimal);
|
||||
}
|
||||
|
||||
// Parse integer
|
||||
|
@ -156,26 +111,6 @@ impl Number for GuardedFixed {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rounding() {
|
||||
GuardedFixed::set_dps(5);
|
||||
|
||||
let mut x = GuardedFixed::parse("55.550"); x.floor_mut(2); assert_eq!(x, GuardedFixed::parse("55.55"));
|
||||
let mut x = GuardedFixed::parse("55.552"); x.floor_mut(2); assert_eq!(x, GuardedFixed::parse("55.55"));
|
||||
let mut x = GuardedFixed::parse("55.555"); x.floor_mut(2); assert_eq!(x, GuardedFixed::parse("55.55"));
|
||||
let mut x = GuardedFixed::parse("55.557"); x.floor_mut(2); assert_eq!(x, GuardedFixed::parse("55.55"));
|
||||
|
||||
let mut x = GuardedFixed::parse("55.550"); x.ceil_mut(2); assert_eq!(x, GuardedFixed::parse("55.55"));
|
||||
let mut x = GuardedFixed::parse("55.552"); x.ceil_mut(2); assert_eq!(x, GuardedFixed::parse("55.56"));
|
||||
let mut x = GuardedFixed::parse("55.555"); x.ceil_mut(2); assert_eq!(x, GuardedFixed::parse("55.56"));
|
||||
let mut x = GuardedFixed::parse("55.557"); x.ceil_mut(2); assert_eq!(x, GuardedFixed::parse("55.56"));
|
||||
|
||||
let mut x = GuardedFixed::parse("55.550"); x.round_mut(2); assert_eq!(x, GuardedFixed::parse("55.55"));
|
||||
let mut x = GuardedFixed::parse("55.552"); x.round_mut(2); assert_eq!(x, GuardedFixed::parse("55.55"));
|
||||
let mut x = GuardedFixed::parse("55.555"); x.round_mut(2); assert_eq!(x, GuardedFixed::parse("55.56"));
|
||||
let mut x = GuardedFixed::parse("55.557"); x.round_mut(2); assert_eq!(x, GuardedFixed::parse("55.56"));
|
||||
}
|
||||
|
||||
impl Num for GuardedFixed {
|
||||
type FromStrRadixErr = ibig::error::ParseError;
|
||||
fn from_str_radix(str: &str, radix: u32) -> Result<Self, Self::FromStrRadixErr> {
|
||||
|
@ -220,16 +155,6 @@ impl Assign<&Self> for GuardedFixed {
|
|||
fn assign(&mut self, src: &Self) { self.0 = src.0.clone() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assign() {
|
||||
GuardedFixed::set_dps(2);
|
||||
let a = GuardedFixed::parse("123.45");
|
||||
let b = GuardedFixed::parse("678.90");
|
||||
|
||||
let mut x = a.clone(); x.assign(b.clone()); assert_eq!(x, b);
|
||||
let mut x = a.clone(); x.assign(&b); assert_eq!(x, b);
|
||||
}
|
||||
|
||||
impl From<usize> for GuardedFixed {
|
||||
fn from(n: usize) -> Self { Self(IBig::from(n) * get_factor()) }
|
||||
}
|
||||
|
@ -272,13 +197,6 @@ impl fmt::Display for GuardedFixed {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_debug() {
|
||||
GuardedFixed::set_dps(2);
|
||||
let x = GuardedFixed::parse("123.4"); assert_eq!(format!("{}", x), "123.40");
|
||||
let x = GuardedFixed::parse("123.4"); assert_eq!(format!("{:?}", x), "GuardedFixed(1234000)");
|
||||
}
|
||||
|
||||
impl One for GuardedFixed {
|
||||
fn one() -> Self { Self(get_factor().clone()) }
|
||||
}
|
||||
|
@ -300,12 +218,16 @@ impl ops::Add for GuardedFixed {
|
|||
|
||||
impl ops::Sub for GuardedFixed {
|
||||
type Output = Self;
|
||||
fn sub(self, rhs: Self) -> Self::Output { Self(self.0 - rhs.0) }
|
||||
fn sub(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul for GuardedFixed {
|
||||
type Output = Self;
|
||||
fn mul(self, rhs: Self) -> Self::Output { Self(self.0 * rhs.0 / get_factor())}
|
||||
fn mul(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Div for GuardedFixed {
|
||||
|
@ -315,20 +237,9 @@ impl ops::Div for GuardedFixed {
|
|||
|
||||
impl ops::Rem for GuardedFixed {
|
||||
type Output = Self;
|
||||
fn rem(self, rhs: Self) -> Self::Output { Self(self.0 % rhs.0) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arith_owned_owned() {
|
||||
GuardedFixed::set_dps(2);
|
||||
let a = GuardedFixed::parse("123.45");
|
||||
let b = GuardedFixed::parse("678.90");
|
||||
|
||||
assert_eq!(a.clone() + b.clone(), GuardedFixed::parse("802.35"));
|
||||
assert_eq!(a.clone() - b.clone(), GuardedFixed::parse("-555.45"));
|
||||
assert_eq!(a.clone() * b.clone(), GuardedFixed::parse("83810.205")); // Must compare to 3 d.p.s as doesn't meet FACTOR_CMP
|
||||
assert_eq!(a.clone() / b.clone(), GuardedFixed::parse("0.18")); // Meets FACTOR_CMP so compare only 2 d.p.s
|
||||
assert_eq!(b.clone() % a.clone(), GuardedFixed::parse("61.65"));
|
||||
fn rem(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Add<&Self> for GuardedFixed {
|
||||
|
@ -353,20 +264,9 @@ impl ops::Div<&Self> for GuardedFixed {
|
|||
|
||||
impl ops::Rem<&Self> for GuardedFixed {
|
||||
type Output = Self;
|
||||
fn rem(self, rhs: &Self) -> Self::Output { Self(self.0 % &rhs.0) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arith_owned_ref() {
|
||||
GuardedFixed::set_dps(2);
|
||||
let a = GuardedFixed::parse("123.45");
|
||||
let b = GuardedFixed::parse("678.90");
|
||||
|
||||
assert_eq!(a.clone() + &b, GuardedFixed::parse("802.35"));
|
||||
assert_eq!(a.clone() - &b, GuardedFixed::parse("-555.45"));
|
||||
assert_eq!(a.clone() * &b, GuardedFixed::parse("83810.205"));
|
||||
assert_eq!(a.clone() / &b, GuardedFixed::parse("0.18"));
|
||||
assert_eq!(b.clone() % &a, GuardedFixed::parse("61.65"));
|
||||
fn rem(self, _rhs: &Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::AddAssign for GuardedFixed {
|
||||
|
@ -392,20 +292,9 @@ impl ops::DivAssign for GuardedFixed {
|
|||
}
|
||||
|
||||
impl ops::RemAssign for GuardedFixed {
|
||||
fn rem_assign(&mut self, rhs: Self) { self.0 %= rhs.0; }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arithassign_owned() {
|
||||
GuardedFixed::set_dps(2);
|
||||
let a = GuardedFixed::parse("123.45");
|
||||
let b = GuardedFixed::parse("678.90");
|
||||
|
||||
let mut x = a.clone(); x += b.clone(); assert_eq!(x, GuardedFixed::parse("802.35"));
|
||||
let mut x = a.clone(); x -= b.clone(); assert_eq!(x, GuardedFixed::parse("-555.45"));
|
||||
let mut x = a.clone(); x *= b.clone(); assert_eq!(x, GuardedFixed::parse("83810.205"));
|
||||
let mut x = a.clone(); x /= b.clone(); assert_eq!(x, GuardedFixed::parse("0.18"));
|
||||
let mut x = b.clone(); x %= a.clone(); assert_eq!(x, GuardedFixed::parse("61.65"));
|
||||
fn rem_assign(&mut self, _rhs: Self) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::AddAssign<&Self> for GuardedFixed {
|
||||
|
@ -431,20 +320,9 @@ impl ops::DivAssign<&Self> for GuardedFixed {
|
|||
}
|
||||
|
||||
impl ops::RemAssign<&Self> for GuardedFixed {
|
||||
fn rem_assign(&mut self, rhs: &Self) { self.0 %= &rhs.0; }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arithassign_ref() {
|
||||
GuardedFixed::set_dps(2);
|
||||
let a = GuardedFixed::parse("123.45");
|
||||
let b = GuardedFixed::parse("678.90");
|
||||
|
||||
let mut x = a.clone(); x += &b; assert_eq!(x, GuardedFixed::parse("802.35"));
|
||||
let mut x = a.clone(); x -= &b; assert_eq!(x, GuardedFixed::parse("-555.45"));
|
||||
let mut x = a.clone(); x *= &b; assert_eq!(x, GuardedFixed::parse("83810.205"));
|
||||
let mut x = a.clone(); x /= &b; assert_eq!(x, GuardedFixed::parse("0.18"));
|
||||
let mut x = b.clone(); x %= &a; assert_eq!(x, GuardedFixed::parse("61.65"));
|
||||
fn rem_assign(&mut self, _rhs: &Self) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Neg for &GuardedFixed {
|
||||
|
@ -474,20 +352,9 @@ impl ops::Div<Self> for &GuardedFixed {
|
|||
|
||||
impl ops::Rem<Self> for &GuardedFixed {
|
||||
type Output = GuardedFixed;
|
||||
fn rem(self, rhs: Self) -> Self::Output { GuardedFixed(&self.0 % &rhs.0) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arith_ref_ref() {
|
||||
GuardedFixed::set_dps(2);
|
||||
let a = GuardedFixed::parse("123.45");
|
||||
let b = GuardedFixed::parse("678.90");
|
||||
|
||||
assert_eq!(&a + &b, GuardedFixed::parse("802.35"));
|
||||
assert_eq!(&a - &b, GuardedFixed::parse("-555.45"));
|
||||
assert_eq!(&a * &b, GuardedFixed::parse("83810.205"));
|
||||
assert_eq!(&a / &b, GuardedFixed::parse("0.18"));
|
||||
assert_eq!(&b % &a, GuardedFixed::parse("61.65"));
|
||||
fn rem(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -35,9 +35,6 @@ mod dynnum;
|
|||
|
||||
use num_traits::{NumAssignRef, NumRef};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use rkyv::{Archive, Archived, Deserialize, Fallible, Resolver, Serialize, with::{ArchiveWith, DeserializeWith, SerializeWith}};
|
||||
|
||||
use std::cmp::Ord;
|
||||
use std::fmt;
|
||||
use std::ops;
|
||||
|
@ -53,7 +50,7 @@ pub trait Assign<Src=Self> {
|
|||
pub trait Number:
|
||||
NumRef + NumAssignRef + ops::Neg<Output=Self> + Ord + Assign + From<usize> + Clone + fmt::Debug + fmt::Display
|
||||
where
|
||||
for<'a> Self: Assign<&'a Self>,
|
||||
for<'a> Self: Assign<&'a Self>
|
||||
{
|
||||
/// Return a new [Number]
|
||||
fn new() -> Self;
|
||||
|
@ -69,8 +66,6 @@ where
|
|||
fn floor_mut(&mut self, dps: usize);
|
||||
/// Round `self` up if necessary to `dps` decimal places
|
||||
fn ceil_mut(&mut self, dps: usize);
|
||||
/// Round `self` half-up to the nearest `dps` decimal places
|
||||
fn round_mut(&mut self, dps: usize);
|
||||
|
||||
/// Parse the given string into a [Number]
|
||||
fn parse(s: &str) -> Self {
|
||||
|
@ -82,80 +77,6 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// rkyv-serialized representation of [Number]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub struct SerializedNumber;
|
||||
|
||||
/// rkyv-serialized representation of [Option<Number>]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub struct SerializedOptionNumber;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl<N: Number> ArchiveWith<N> for SerializedNumber {
|
||||
type Archived = Archived<String>;
|
||||
type Resolver = Resolver<String>;
|
||||
|
||||
unsafe fn resolve_with(field: &N, pos: usize, resolver: Self::Resolver, out: *mut Self::Archived) {
|
||||
field.to_string().resolve(pos, resolver, out);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl<N: Number> ArchiveWith<Option<N>> for SerializedOptionNumber {
|
||||
type Archived = Archived<String>;
|
||||
type Resolver = Resolver<String>;
|
||||
|
||||
unsafe fn resolve_with(field: &Option<N>, pos: usize, resolver: Self::Resolver, out: *mut Self::Archived) {
|
||||
match field {
|
||||
Some(n) => {
|
||||
n.to_string().resolve(pos, resolver, out);
|
||||
}
|
||||
None => {
|
||||
String::new().resolve(pos, resolver, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl<N: Number, S: Fallible + ?Sized> SerializeWith<N, S> for SerializedNumber where String: Serialize<S> {
|
||||
fn serialize_with(field: &N, serializer: &mut S) -> Result<Self::Resolver, S::Error> {
|
||||
return field.to_string().serialize(serializer);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl<N: Number, S: Fallible + ?Sized> SerializeWith<Option<N>, S> for SerializedOptionNumber where String: Serialize<S> {
|
||||
fn serialize_with(field: &Option<N>, serializer: &mut S) -> Result<Self::Resolver, S::Error> {
|
||||
match field {
|
||||
Some(n) => {
|
||||
return n.to_string().serialize(serializer);
|
||||
}
|
||||
None => {
|
||||
return String::new().serialize(serializer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl<N: Number, D: Fallible + ?Sized> DeserializeWith<Archived<String>, N, D> for SerializedNumber where Archived<String>: Deserialize<String, D> {
|
||||
fn deserialize_with(field: &Archived<String>, deserializer: &mut D) -> Result<N, D::Error> {
|
||||
return Ok(N::parse(&field.deserialize(deserializer)?));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl<N: Number, D: Fallible + ?Sized> DeserializeWith<Archived<String>, Option<N>, D> for SerializedOptionNumber where Archived<String>: Deserialize<String, D> {
|
||||
fn deserialize_with(field: &Archived<String>, deserializer: &mut D) -> Result<Option<N>, D::Error> {
|
||||
let s = field.deserialize(deserializer)?;
|
||||
if s.is_empty() {
|
||||
return Ok(None);
|
||||
} else {
|
||||
return Ok(Some(N::parse(&s)));
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use self::dynnum::NumKind;
|
||||
pub use self::dynnum::DynNum;
|
||||
|
||||
|
|
|
@ -47,35 +47,6 @@ impl Number for NativeFloat64 {
|
|||
let factor = 10.0_f64.powi(dps as i32);
|
||||
self.0 = (self.0 * factor).ceil() / factor;
|
||||
}
|
||||
|
||||
fn round_mut(&mut self, dps: usize) {
|
||||
let factor = 10.0_f64.powi(dps as i32);
|
||||
self.0 = (self.0 * factor).round() / factor;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_debug() {
|
||||
let x = NativeFloat64::parse("123.4"); assert_eq!(format!("{}", x), format!("{}", 123.40_f64));
|
||||
let x = NativeFloat64::parse("123.4"); assert_eq!(format!("{:?}", x), format!("NativeFloat64({})", 123.40_f64));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rounding() {
|
||||
let mut x = NativeFloat64::parse("55.550"); x.floor_mut(2); assert_eq!(x, NativeFloat64::parse("55.55"));
|
||||
let mut x = NativeFloat64::parse("55.552"); x.floor_mut(2); assert_eq!(x, NativeFloat64::parse("55.55"));
|
||||
let mut x = NativeFloat64::parse("55.555"); x.floor_mut(2); assert_eq!(x, NativeFloat64::parse("55.55"));
|
||||
let mut x = NativeFloat64::parse("55.557"); x.floor_mut(2); assert_eq!(x, NativeFloat64::parse("55.55"));
|
||||
|
||||
let mut x = NativeFloat64::parse("55.550"); x.ceil_mut(2); assert_eq!(x, NativeFloat64::parse("55.55"));
|
||||
let mut x = NativeFloat64::parse("55.552"); x.ceil_mut(2); assert_eq!(x, NativeFloat64::parse("55.56"));
|
||||
let mut x = NativeFloat64::parse("55.555"); x.ceil_mut(2); assert_eq!(x, NativeFloat64::parse("55.56"));
|
||||
let mut x = NativeFloat64::parse("55.557"); x.ceil_mut(2); assert_eq!(x, NativeFloat64::parse("55.56"));
|
||||
|
||||
let mut x = NativeFloat64::parse("55.550"); x.round_mut(2); assert_eq!(x, NativeFloat64::parse("55.55"));
|
||||
let mut x = NativeFloat64::parse("55.552"); x.round_mut(2); assert_eq!(x, NativeFloat64::parse("55.55"));
|
||||
let mut x = NativeFloat64::parse("55.555"); x.round_mut(2); assert_eq!(x, NativeFloat64::parse("55.56"));
|
||||
let mut x = NativeFloat64::parse("55.557"); x.round_mut(2); assert_eq!(x, NativeFloat64::parse("55.56"));
|
||||
}
|
||||
|
||||
impl Num for NativeFloat64 {
|
||||
|
@ -96,23 +67,10 @@ impl Assign<&NativeFloat64> for NativeFloat64 {
|
|||
fn assign(&mut self, src: &NativeFloat64) { self.0 = src.0; }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assign() {
|
||||
let a = NativeFloat64::parse("123.45");
|
||||
let b = NativeFloat64::parse("678.90");
|
||||
|
||||
let mut x = a.clone(); x.assign(b.clone()); assert_eq!(x, b);
|
||||
let mut x = a.clone(); x.assign(&b); assert_eq!(x, b);
|
||||
}
|
||||
|
||||
impl From<usize> for NativeFloat64 {
|
||||
fn from(n: usize) -> Self { Self(n as ImplType) }
|
||||
}
|
||||
|
||||
impl From<f64> for NativeFloat64 {
|
||||
fn from(n: f64) -> Self { Self(n as ImplType) }
|
||||
}
|
||||
|
||||
impl One for NativeFloat64 {
|
||||
fn one() -> Self { Self(1.0) }
|
||||
}
|
||||
|
@ -139,12 +97,16 @@ impl ops::Add for NativeFloat64 {
|
|||
|
||||
impl ops::Sub for NativeFloat64 {
|
||||
type Output = NativeFloat64;
|
||||
fn sub(self, rhs: Self) -> Self::Output { Self(self.0 - rhs.0) }
|
||||
fn sub(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul for NativeFloat64 {
|
||||
type Output = NativeFloat64;
|
||||
fn mul(self, rhs: Self) -> Self::Output { Self(self.0 * rhs.0) }
|
||||
fn mul(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Div for NativeFloat64 {
|
||||
|
@ -154,56 +116,36 @@ impl ops::Div for NativeFloat64 {
|
|||
|
||||
impl ops::Rem for NativeFloat64 {
|
||||
type Output = NativeFloat64;
|
||||
fn rem(self, rhs: Self) -> Self::Output { Self(self.0 % rhs.0) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arith_owned_owned() {
|
||||
let a = NativeFloat64::parse("123.45");
|
||||
let b = NativeFloat64::parse("678.90");
|
||||
|
||||
assert_eq!(a.clone() + b.clone(), NativeFloat64::from(123.45_f64 + 678.90_f64));
|
||||
assert_eq!(a.clone() - b.clone(), NativeFloat64::from(123.45_f64 - 678.90_f64));
|
||||
assert_eq!(a.clone() * b.clone(), NativeFloat64::from(123.45_f64 * 678.90_f64));
|
||||
assert_eq!(a.clone() / b.clone(), NativeFloat64::from(123.45_f64 / 678.90_f64));
|
||||
assert_eq!(b.clone() % a.clone(), NativeFloat64::from(678.90_f64 % 123.45_f64));
|
||||
fn rem(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Add<&NativeFloat64> for NativeFloat64 {
|
||||
type Output = NativeFloat64;
|
||||
fn add(self, rhs: &NativeFloat64) -> Self::Output { Self(self.0 + rhs.0) }
|
||||
fn add(self, rhs: &NativeFloat64) -> Self::Output { Self(self.0 + &rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Sub<&NativeFloat64> for NativeFloat64 {
|
||||
type Output = NativeFloat64;
|
||||
fn sub(self, rhs: &NativeFloat64) -> Self::Output { Self(self.0 - rhs.0) }
|
||||
fn sub(self, rhs: &NativeFloat64) -> Self::Output { Self(self.0 - &rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Mul<&NativeFloat64> for NativeFloat64 {
|
||||
type Output = NativeFloat64;
|
||||
fn mul(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(self.0 * rhs.0) }
|
||||
fn mul(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(self.0 * &rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Div<&NativeFloat64> for NativeFloat64 {
|
||||
type Output = NativeFloat64;
|
||||
fn div(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(self.0 / rhs.0) }
|
||||
fn div(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(self.0 / &rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Rem<&NativeFloat64> for NativeFloat64 {
|
||||
type Output = NativeFloat64;
|
||||
fn rem(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(self.0 % rhs.0) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arith_owned_ref() {
|
||||
let a = NativeFloat64::parse("123.45");
|
||||
let b = NativeFloat64::parse("678.90");
|
||||
|
||||
assert_eq!(a.clone() + &b, NativeFloat64::from(123.45_f64 + 678.90_f64));
|
||||
assert_eq!(a.clone() - &b, NativeFloat64::from(123.45_f64 - 678.90_f64));
|
||||
assert_eq!(a.clone() * &b, NativeFloat64::from(123.45_f64 * 678.90_f64));
|
||||
assert_eq!(a.clone() / &b, NativeFloat64::from(123.45_f64 / 678.90_f64));
|
||||
assert_eq!(b.clone() % &a, NativeFloat64::from(678.90_f64 % 123.45_f64));
|
||||
fn rem(self, _rhs: &NativeFloat64) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::AddAssign for NativeFloat64 {
|
||||
|
@ -219,97 +161,67 @@ impl ops::MulAssign for NativeFloat64 {
|
|||
}
|
||||
|
||||
impl ops::DivAssign for NativeFloat64 {
|
||||
fn div_assign(&mut self, rhs: Self) { self.0 /= rhs.0; }
|
||||
fn div_assign(&mut self, rhs: Self) { self.0 /= &rhs.0; }
|
||||
}
|
||||
|
||||
impl ops::RemAssign for NativeFloat64 {
|
||||
fn rem_assign(&mut self, rhs: Self) { self.0 %= rhs.0; }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arithassign_owned() {
|
||||
let a = NativeFloat64::parse("123.45");
|
||||
let b = NativeFloat64::parse("678.90");
|
||||
|
||||
let mut x = a.clone(); x += b.clone(); assert_eq!(x, NativeFloat64::from(123.45_f64 + 678.90_f64));
|
||||
let mut x = a.clone(); x -= b.clone(); assert_eq!(x, NativeFloat64::from(123.45_f64 - 678.90_f64));
|
||||
let mut x = a.clone(); x *= b.clone(); assert_eq!(x, NativeFloat64::from(123.45_f64 * 678.90_f64));
|
||||
let mut x = a.clone(); x /= b.clone(); assert_eq!(x, NativeFloat64::from(123.45_f64 / 678.90_f64));
|
||||
let mut x = b.clone(); x %= a.clone(); assert_eq!(x, NativeFloat64::from(678.90_f64 % 123.45_f64));
|
||||
fn rem_assign(&mut self, _rhs: Self) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::AddAssign<&NativeFloat64> for NativeFloat64 {
|
||||
fn add_assign(&mut self, rhs: &NativeFloat64) { self.0 += rhs.0; }
|
||||
fn add_assign(&mut self, rhs: &NativeFloat64) { self.0 += &rhs.0; }
|
||||
}
|
||||
|
||||
impl ops::SubAssign<&NativeFloat64> for NativeFloat64 {
|
||||
fn sub_assign(&mut self, rhs: &NativeFloat64) { self.0 -= rhs.0; }
|
||||
fn sub_assign(&mut self, rhs: &NativeFloat64) { self.0 -= &rhs.0; }
|
||||
}
|
||||
|
||||
impl ops::MulAssign<&NativeFloat64> for NativeFloat64 {
|
||||
fn mul_assign(&mut self, rhs: &NativeFloat64) { self.0 *= rhs.0; }
|
||||
fn mul_assign(&mut self, rhs: &NativeFloat64) { self.0 *= &rhs.0; }
|
||||
}
|
||||
|
||||
impl ops::DivAssign<&NativeFloat64> for NativeFloat64 {
|
||||
fn div_assign(&mut self, rhs: &NativeFloat64) { self.0 /= rhs.0; }
|
||||
fn div_assign(&mut self, rhs: &NativeFloat64) { self.0 /= &rhs.0; }
|
||||
}
|
||||
|
||||
impl ops::RemAssign<&NativeFloat64> for NativeFloat64 {
|
||||
fn rem_assign(&mut self, rhs: &NativeFloat64) { self.0 %= rhs.0; }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arithassign_ref() {
|
||||
let a = NativeFloat64::parse("123.45");
|
||||
let b = NativeFloat64::parse("678.90");
|
||||
|
||||
let mut x = a.clone(); x += &b; assert_eq!(x, NativeFloat64::from(123.45_f64 + 678.90_f64));
|
||||
let mut x = a.clone(); x -= &b; assert_eq!(x, NativeFloat64::from(123.45_f64 - 678.90_f64));
|
||||
let mut x = a.clone(); x *= &b; assert_eq!(x, NativeFloat64::from(123.45_f64 * 678.90_f64));
|
||||
let mut x = a.clone(); x /= &b; assert_eq!(x, NativeFloat64::from(123.45_f64 / 678.90_f64));
|
||||
let mut x = b.clone(); x %= &a; assert_eq!(x, NativeFloat64::from(678.90_f64 % 123.45_f64));
|
||||
fn rem_assign(&mut self, _rhs: &NativeFloat64) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Neg for &NativeFloat64 {
|
||||
type Output = NativeFloat64;
|
||||
fn neg(self) -> Self::Output { NativeFloat64(-self.0) }
|
||||
fn neg(self) -> Self::Output { NativeFloat64(-&self.0) }
|
||||
}
|
||||
|
||||
impl ops::Add<Self> for &NativeFloat64 {
|
||||
type Output = NativeFloat64;
|
||||
fn add(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(self.0 + rhs.0) }
|
||||
fn add(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(&self.0 + &rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Sub<Self> for &NativeFloat64 {
|
||||
type Output = NativeFloat64;
|
||||
fn sub(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(self.0 - rhs.0) }
|
||||
fn sub(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(&self.0 - &rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Mul<Self> for &NativeFloat64 {
|
||||
type Output = NativeFloat64;
|
||||
fn mul(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(self.0 * rhs.0) }
|
||||
fn mul(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(&self.0 * &rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Div<Self> for &NativeFloat64 {
|
||||
type Output = NativeFloat64;
|
||||
fn div(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(self.0 / rhs.0) }
|
||||
fn div(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(&self.0 / &rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Rem<Self> for &NativeFloat64 {
|
||||
type Output = NativeFloat64;
|
||||
fn rem(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(self.0 % rhs.0) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arith_ref_ref() {
|
||||
let a = NativeFloat64::parse("123.45");
|
||||
let b = NativeFloat64::parse("678.90");
|
||||
|
||||
assert_eq!(&a + &b, NativeFloat64::from(123.45_f64 + 678.90_f64));
|
||||
assert_eq!(&a - &b, NativeFloat64::from(123.45_f64 - 678.90_f64));
|
||||
assert_eq!(&a * &b, NativeFloat64::from(123.45_f64 * 678.90_f64));
|
||||
assert_eq!(&a / &b, NativeFloat64::from(123.45_f64 / 678.90_f64));
|
||||
assert_eq!(&b % &a, NativeFloat64::from(678.90_f64 % 123.45_f64));
|
||||
fn rem(self, _rhs: &NativeFloat64) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -62,20 +62,6 @@ impl Number for Rational {
|
|||
}
|
||||
}
|
||||
|
||||
fn round_mut(&mut self, dps: usize) {
|
||||
if dps == 0 {
|
||||
self.0 = self.0.round();
|
||||
} else {
|
||||
// TODO: Streamline
|
||||
let mut factor = Self::from(10);
|
||||
factor.pow_assign(-(dps as i32));
|
||||
factor /= Self::from(2);
|
||||
|
||||
*self = self.clone() + factor;
|
||||
self.floor_mut(dps);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Self {
|
||||
// Parse decimal
|
||||
if s.contains('.') {
|
||||
|
@ -179,12 +165,16 @@ impl ops::Add for Rational {
|
|||
|
||||
impl ops::Sub for Rational {
|
||||
type Output = Rational;
|
||||
fn sub(self, rhs: Self) -> Self::Output { Self(self.0 - rhs.0) }
|
||||
fn sub(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul for Rational {
|
||||
type Output = Rational;
|
||||
fn mul(self, rhs: Self) -> Self::Output { Self(self.0 * rhs.0) }
|
||||
fn mul(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Div for Rational {
|
||||
|
@ -194,7 +184,9 @@ impl ops::Div for Rational {
|
|||
|
||||
impl ops::Rem for Rational {
|
||||
type Output = Rational;
|
||||
fn rem(self, rhs: Self) -> Self::Output { Self(self.0 % rhs.0) }
|
||||
fn rem(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Add<&Rational> for Rational {
|
||||
|
@ -219,7 +211,9 @@ impl ops::Div<&Rational> for Rational {
|
|||
|
||||
impl ops::Rem<&Rational> for Rational {
|
||||
type Output = Rational;
|
||||
fn rem(self, rhs: &Rational) -> Self::Output { Rational(self.0 % &rhs.0) }
|
||||
fn rem(self, _rhs: &Rational) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::AddAssign for Rational {
|
||||
|
@ -239,7 +233,9 @@ impl ops::DivAssign for Rational {
|
|||
}
|
||||
|
||||
impl ops::RemAssign for Rational {
|
||||
fn rem_assign(&mut self, rhs: Self) { self.0 %= &rhs.0; }
|
||||
fn rem_assign(&mut self, _rhs: Self) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::AddAssign<&Rational> for Rational {
|
||||
|
@ -259,7 +255,9 @@ impl ops::DivAssign<&Rational> for Rational {
|
|||
}
|
||||
|
||||
impl ops::RemAssign<&Rational> for Rational {
|
||||
fn rem_assign(&mut self, rhs: &Rational) { self.0 %= &rhs.0; }
|
||||
fn rem_assign(&mut self, _rhs: &Rational) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Neg for &Rational {
|
||||
|
@ -289,7 +287,9 @@ impl ops::Div<Self> for &Rational {
|
|||
|
||||
impl ops::Rem<Self> for &Rational {
|
||||
type Output = Rational;
|
||||
fn rem(self, rhs: &Rational) -> Self::Output { Rational(&self.0 % &rhs.0) }
|
||||
fn rem(self, _rhs: &Rational) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -61,20 +61,6 @@ impl Number for Rational {
|
|||
}
|
||||
}
|
||||
|
||||
fn round_mut(&mut self, dps: usize) {
|
||||
if dps == 0 {
|
||||
self.0.round_mut();
|
||||
} else {
|
||||
// TODO: Streamline
|
||||
let mut factor = Self::from(10);
|
||||
factor.pow_assign(-(dps as i32));
|
||||
factor /= Self::from(2);
|
||||
|
||||
*self = self.clone() + factor;
|
||||
self.floor_mut(dps);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Self {
|
||||
// Parse decimal
|
||||
if s.contains('.') {
|
||||
|
@ -87,12 +73,7 @@ impl Number for Rational {
|
|||
Ok(value) => rug::Rational::from(value),
|
||||
Err(_) => panic!("Syntax Error"),
|
||||
} / rug::Rational::from(10).pow(decimal.len() as u32);
|
||||
|
||||
if whole < rug::Rational::new() {
|
||||
return Self(whole - decimal);
|
||||
} else {
|
||||
return Self(whole + decimal);
|
||||
}
|
||||
return Self(whole + decimal);
|
||||
}
|
||||
|
||||
// Parse integer
|
||||
|
@ -104,30 +85,6 @@ impl Number for Rational {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn describe() {
|
||||
// Never executed - just for the sake of code coverage
|
||||
assert_eq!(Rational::describe(), "--numbers rational");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rounding() {
|
||||
let mut x = Rational::parse("55.550"); x.floor_mut(2); assert_eq!(x, Rational::parse("55.55"));
|
||||
let mut x = Rational::parse("55.552"); x.floor_mut(2); assert_eq!(x, Rational::parse("55.55"));
|
||||
let mut x = Rational::parse("55.555"); x.floor_mut(2); assert_eq!(x, Rational::parse("55.55"));
|
||||
let mut x = Rational::parse("55.557"); x.floor_mut(2); assert_eq!(x, Rational::parse("55.55"));
|
||||
|
||||
let mut x = Rational::parse("55.550"); x.ceil_mut(2); assert_eq!(x, Rational::parse("55.55"));
|
||||
let mut x = Rational::parse("55.552"); x.ceil_mut(2); assert_eq!(x, Rational::parse("55.56"));
|
||||
let mut x = Rational::parse("55.555"); x.ceil_mut(2); assert_eq!(x, Rational::parse("55.56"));
|
||||
let mut x = Rational::parse("55.557"); x.ceil_mut(2); assert_eq!(x, Rational::parse("55.56"));
|
||||
|
||||
let mut x = Rational::parse("55.550"); x.round_mut(2); assert_eq!(x, Rational::parse("55.55"));
|
||||
let mut x = Rational::parse("55.552"); x.round_mut(2); assert_eq!(x, Rational::parse("55.55"));
|
||||
let mut x = Rational::parse("55.555"); x.round_mut(2); assert_eq!(x, Rational::parse("55.56"));
|
||||
let mut x = Rational::parse("55.557"); x.round_mut(2); assert_eq!(x, Rational::parse("55.56"));
|
||||
}
|
||||
|
||||
impl Num for Rational {
|
||||
type FromStrRadixErr = ParseRationalError;
|
||||
fn from_str_radix(str: &str, radix: u32) -> Result<Self, Self::FromStrRadixErr> {
|
||||
|
@ -146,15 +103,6 @@ impl Assign<&Self> for Rational {
|
|||
fn assign(&mut self, src: &Self) { self.0.assign(&src.0) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assign() {
|
||||
let a = Rational::parse("123.45");
|
||||
let b = Rational::parse("678.90");
|
||||
|
||||
let mut x = a.clone(); x.assign(b.clone()); assert_eq!(x, b);
|
||||
let mut x = a.clone(); x.assign(&b); assert_eq!(x, b);
|
||||
}
|
||||
|
||||
impl From<usize> for Rational {
|
||||
fn from(n: usize) -> Self { Self(rug::Rational::from(n)) }
|
||||
}
|
||||
|
@ -190,13 +138,6 @@ impl fmt::Display for Rational {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_debug() {
|
||||
let x = Rational::parse("123.4"); assert_eq!(format!("{}", x), "617/5");
|
||||
let x = Rational::parse("123.4"); assert_eq!(format!("{:.2}", x), "123.40");
|
||||
let x = Rational::parse("123.4"); assert_eq!(format!("{:?}", x), "Rational(617/5)");
|
||||
}
|
||||
|
||||
impl One for Rational {
|
||||
fn one() -> Self { Self(rug::Rational::from(1)) }
|
||||
}
|
||||
|
@ -223,51 +164,30 @@ impl ops::Add for Rational {
|
|||
|
||||
impl ops::Sub for Rational {
|
||||
type Output = Self;
|
||||
fn sub(self, rhs: Self) -> Self::Output { Self(self.0 - rhs.0) }
|
||||
fn sub(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul for Rational {
|
||||
type Output = Self;
|
||||
fn mul(self, rhs: Self) -> Self::Output { Self(self.0 * rhs.0) }
|
||||
fn mul(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Div for Rational {
|
||||
type Output = Self;
|
||||
fn div(self, rhs: Self) -> Self::Output {
|
||||
if rhs.0.cmp0() == Ordering::Equal {
|
||||
panic!("Divide by zero");
|
||||
}
|
||||
return Self(self.0 / rhs.0);
|
||||
}
|
||||
fn div(self, rhs: Self) -> Self::Output { Self(self.0 / rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Rem for Rational {
|
||||
type Output = Self;
|
||||
fn rem(self, rhs: Self) -> Self::Output {
|
||||
if rhs.0.cmp0() == Ordering::Equal {
|
||||
panic!("Divide by zero");
|
||||
}
|
||||
|
||||
// TODO: Is there a cleaner way of implementing this?
|
||||
let mut quotient = self.0 / &rhs.0;
|
||||
quotient.rem_trunc_mut();
|
||||
quotient *= rhs.0;
|
||||
return Self(quotient);
|
||||
fn rem(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arith_owned_owned() {
|
||||
let a = Rational::parse("123.45");
|
||||
let b = Rational::parse("678.90");
|
||||
|
||||
assert_eq!(a.clone() + b.clone(), Rational::parse("802.35"));
|
||||
assert_eq!(a.clone() - b.clone(), Rational::parse("-555.45"));
|
||||
assert_eq!(a.clone() * b.clone(), Rational::parse("83810.205"));
|
||||
assert_eq!((a.clone() / b.clone()) * b.clone(), a);
|
||||
assert_eq!(b.clone() % a.clone(), Rational::parse("61.65"));
|
||||
}
|
||||
|
||||
impl ops::Add<&Self> for Rational {
|
||||
type Output = Self;
|
||||
fn add(self, rhs: &Self) -> Self::Output { Self(self.0 + &rhs.0) }
|
||||
|
@ -285,39 +205,16 @@ impl ops::Mul<&Self> for Rational {
|
|||
|
||||
impl ops::Div<&Self> for Rational {
|
||||
type Output = Self;
|
||||
fn div(self, rhs: &Self) -> Self::Output {
|
||||
if rhs.0.cmp0() == Ordering::Equal {
|
||||
panic!("Divide by zero");
|
||||
}
|
||||
return Self(self.0 / &rhs.0);
|
||||
}
|
||||
fn div(self, rhs: &Self) -> Self::Output { Self(self.0 / &rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Rem<&Self> for Rational {
|
||||
type Output = Self;
|
||||
fn rem(self, rhs: &Self) -> Self::Output {
|
||||
if rhs.0.cmp0() == Ordering::Equal {
|
||||
panic!("Divide by zero");
|
||||
}
|
||||
let mut quotient = self.0 / &rhs.0;
|
||||
quotient.rem_trunc_mut();
|
||||
quotient *= &rhs.0;
|
||||
return Self(quotient);
|
||||
fn rem(self, _rhs: &Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arith_owned_ref() {
|
||||
let a = Rational::parse("123.45");
|
||||
let b = Rational::parse("678.90");
|
||||
|
||||
assert_eq!(a.clone() + &b, Rational::parse("802.35"));
|
||||
assert_eq!(a.clone() - &b, Rational::parse("-555.45"));
|
||||
assert_eq!(a.clone() * &b, Rational::parse("83810.205"));
|
||||
assert_eq!((a.clone() / &b) * &b, a);
|
||||
assert_eq!(b.clone() % &a, Rational::parse("61.65"));
|
||||
}
|
||||
|
||||
impl ops::AddAssign for Rational {
|
||||
fn add_assign(&mut self, rhs: Self) { self.0 += rhs.0; }
|
||||
}
|
||||
|
@ -331,37 +228,15 @@ impl ops::MulAssign for Rational {
|
|||
}
|
||||
|
||||
impl ops::DivAssign for Rational {
|
||||
fn div_assign(&mut self, rhs: Self) {
|
||||
if rhs.0.cmp0() == Ordering::Equal {
|
||||
panic!("Divide by zero");
|
||||
}
|
||||
self.0 /= rhs.0;
|
||||
}
|
||||
fn div_assign(&mut self, rhs: Self) { self.0 /= rhs.0; }
|
||||
}
|
||||
|
||||
impl ops::RemAssign for Rational {
|
||||
fn rem_assign(&mut self, rhs: Self) {
|
||||
if rhs.0.cmp0() == Ordering::Equal {
|
||||
panic!("Divide by zero");
|
||||
}
|
||||
self.0 /= &rhs.0;
|
||||
self.0.rem_trunc_mut();
|
||||
self.0 *= rhs.0;
|
||||
fn rem_assign(&mut self, _rhs: Self) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arithassign_owned() {
|
||||
let a = Rational::parse("123.45");
|
||||
let b = Rational::parse("678.90");
|
||||
|
||||
let mut x = a.clone(); x += b.clone(); assert_eq!(x, Rational::parse("802.35"));
|
||||
let mut x = a.clone(); x -= b.clone(); assert_eq!(x, Rational::parse("-555.45"));
|
||||
let mut x = a.clone(); x *= b.clone(); assert_eq!(x, Rational::parse("83810.205"));
|
||||
let mut x = a.clone(); x /= b.clone(); x *= &b; assert_eq!(x, a);
|
||||
let mut x = b.clone(); x %= a.clone(); assert_eq!(x, Rational::parse("61.65"));
|
||||
}
|
||||
|
||||
impl ops::AddAssign<&Self> for Rational {
|
||||
fn add_assign(&mut self, rhs: &Self) { self.0 += &rhs.0; }
|
||||
}
|
||||
|
@ -375,37 +250,15 @@ impl ops::MulAssign<&Self> for Rational {
|
|||
}
|
||||
|
||||
impl ops::DivAssign<&Self> for Rational {
|
||||
fn div_assign(&mut self, rhs: &Self) {
|
||||
if rhs.0.cmp0() == Ordering::Equal {
|
||||
panic!("Divide by zero");
|
||||
}
|
||||
self.0 /= &rhs.0;
|
||||
}
|
||||
fn div_assign(&mut self, rhs: &Self) { self.0 /= &rhs.0; }
|
||||
}
|
||||
|
||||
impl ops::RemAssign<&Self> for Rational {
|
||||
fn rem_assign(&mut self, rhs: &Self) {
|
||||
if rhs.0.cmp0() == Ordering::Equal {
|
||||
panic!("Divide by zero");
|
||||
}
|
||||
self.0 /= &rhs.0;
|
||||
self.0.rem_trunc_mut();
|
||||
self.0 *= &rhs.0;
|
||||
fn rem_assign(&mut self, _rhs: &Self) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arithassign_ref() {
|
||||
let a = Rational::parse("123.45");
|
||||
let b = Rational::parse("678.90");
|
||||
|
||||
let mut x = a.clone(); x += &b; assert_eq!(x, Rational::parse("802.35"));
|
||||
let mut x = a.clone(); x -= &b; assert_eq!(x, Rational::parse("-555.45"));
|
||||
let mut x = a.clone(); x *= &b; assert_eq!(x, Rational::parse("83810.205"));
|
||||
let mut x = a.clone(); x /= &b; x *= &b; assert_eq!(x, a);
|
||||
let mut x = b.clone(); x %= &a; assert_eq!(x, Rational::parse("61.65"));
|
||||
}
|
||||
|
||||
impl ops::Neg for &Rational {
|
||||
type Output = Rational;
|
||||
fn neg(self) -> Self::Output { Rational(rug::Rational::from(-&self.0)) }
|
||||
|
@ -428,39 +281,16 @@ impl ops::Mul<Self> for &Rational {
|
|||
|
||||
impl ops::Div<Self> for &Rational {
|
||||
type Output = Rational;
|
||||
fn div(self, rhs: Self) -> Self::Output {
|
||||
if rhs.0.cmp0() == Ordering::Equal {
|
||||
panic!("Divide by zero");
|
||||
}
|
||||
return Rational(rug::Rational::from(&self.0 / &rhs.0));
|
||||
}
|
||||
fn div(self, rhs: Self) -> Self::Output { Rational(rug::Rational::from(&self.0 / &rhs.0)) }
|
||||
}
|
||||
|
||||
impl ops::Rem<Self> for &Rational {
|
||||
type Output = Rational;
|
||||
fn rem(self, rhs: Self) -> Self::Output {
|
||||
if rhs.0.cmp0() == Ordering::Equal {
|
||||
panic!("Divide by zero");
|
||||
}
|
||||
let mut quotient = rug::Rational::from(&self.0 / &rhs.0);
|
||||
quotient.rem_trunc_mut();
|
||||
quotient *= &rhs.0;
|
||||
return Rational(quotient);
|
||||
fn rem(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arith_ref_ref() {
|
||||
let a = Rational::parse("123.45");
|
||||
let b = Rational::parse("678.90");
|
||||
|
||||
assert_eq!(&a + &b, Rational::parse("802.35"));
|
||||
assert_eq!(&a - &b, Rational::parse("-555.45"));
|
||||
assert_eq!(&a * &b, Rational::parse("83810.205"));
|
||||
assert_eq!((&a / &b) * &b, a);
|
||||
assert_eq!(&b % &a, Rational::parse("61.65"));
|
||||
}
|
||||
|
||||
/*
|
||||
impl ops::Add<&&Rational> for &Rational {
|
||||
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::election::Election;
|
||||
use crate::numbers::Number;
|
||||
|
||||
use rkyv::{Deserialize, Infallible, archived_root};
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
/// Parse the given BIN file
|
||||
pub fn parse_path<P: AsRef<Path>, N: Number>(path: P) -> Election<N> {
|
||||
let content = fs::read(path).expect("IO Error");
|
||||
return parse_bytes(&content);
|
||||
}
|
||||
|
||||
/// Parse the given BIN file
|
||||
pub fn parse_bytes<N: Number>(content: &[u8]) -> Election<N> {
|
||||
let archived = unsafe {
|
||||
archived_root::<Election<N>>(content)
|
||||
};
|
||||
return archived.deserialize(&mut Infallible).unwrap();
|
||||
}
|
|
@ -1,440 +0,0 @@
|
|||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021–2022 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::election::{Ballot, Candidate, Election};
|
||||
use crate::numbers::Number;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use utf8_chars::BufReadCharsExt;
|
||||
|
||||
use std::fmt;
|
||||
use std::iter::Peekable;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::fs::File;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::io::BufReader;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::path::Path;
|
||||
|
||||
/// Utility for parsing a BLT file
|
||||
pub struct BLTParser<N: Number, I: Iterator<Item=char>> {
|
||||
/// The peekable iterator of chars representing the BLT file
|
||||
chars: Peekable<I>,
|
||||
|
||||
/// Temporary buffer for parsing ballot values
|
||||
ballot_value_buf: String,
|
||||
/// Whether the current ballot has equal preferences
|
||||
ballot_has_equal_rankings: bool,
|
||||
|
||||
/// Current line number
|
||||
line_no: u32,
|
||||
/// Current column number
|
||||
col_no: u32,
|
||||
|
||||
/// Number of candidates
|
||||
num_candidates: usize,
|
||||
/// Parsed [Election]
|
||||
election: Election<N>,
|
||||
}
|
||||
|
||||
/// An error when parsing a BLT file
|
||||
pub enum ParseError {
|
||||
/// Unexpected character
|
||||
Unexpected(u32, u32, char),
|
||||
/// Unexpected character, expected ...
|
||||
Expected(u32, u32, char, &'static str),
|
||||
}
|
||||
|
||||
impl fmt::Display for ParseError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ParseError::Unexpected(line_no, col_no, char) => {
|
||||
f.write_fmt(format_args!("Line {} col {}, unexpected '{}'", line_no, col_no, char))?;
|
||||
}
|
||||
ParseError::Expected(line_no, col_no, char, expected) => {
|
||||
f.write_fmt(format_args!("Line {} col {}, unexpected '{}', expected {}", line_no, col_no, char, expected))?;
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ParseError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
return fmt::Display::fmt(self, f);
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: Number, I: Iterator<Item=char>> BLTParser<N, I> {
|
||||
// NON-TERMINALS - HIGHER LEVEL
|
||||
|
||||
/// Parse the BLT file
|
||||
pub fn parse_blt(&mut self) -> Result<(), ParseError> {
|
||||
self.delimiter();
|
||||
|
||||
self.header()?;
|
||||
self.withdrawn_candidates()?;
|
||||
self.ballots()?;
|
||||
self.strings()?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
/// Parse the header
|
||||
fn header(&mut self) -> Result<(), ParseError> {
|
||||
self.num_candidates = self.usize()?;
|
||||
self.delimiter();
|
||||
self.election.seats = self.usize()?;
|
||||
self.delimiter();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
/// Parse the withdrawn candidates (if any)
|
||||
fn withdrawn_candidates(&mut self) -> Result<(), ParseError> {
|
||||
while self.lookahead() == '-' {
|
||||
self.accept(); // Minus sign
|
||||
let index = self.usize()? - 1;
|
||||
self.election.withdrawn_candidates.push(index);
|
||||
self.delimiter();
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
/// Parse the list of ballots
|
||||
fn ballots(&mut self) -> Result<(), ParseError> {
|
||||
loop {
|
||||
if self.lookahead() == '0' {
|
||||
// End of ballots, or start of decimal?
|
||||
self.accept();
|
||||
|
||||
if self.lookahead() == '.' {
|
||||
// Decimal
|
||||
self.ballot_value_buf.clear();
|
||||
self.ballot_value_buf.push('0');
|
||||
self.ballot()?;
|
||||
} else {
|
||||
// End of ballots
|
||||
self.delimiter();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
self.ballot_value_buf.clear();
|
||||
self.ballot()?;
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
/// Parse a ballot
|
||||
fn ballot(&mut self) -> Result<(), ParseError> {
|
||||
self.ballot_has_equal_rankings = false;
|
||||
|
||||
self.ballot_value()?;
|
||||
|
||||
self.delimiter_not_nl();
|
||||
|
||||
// Read preferences
|
||||
let mut preferences: Vec<Vec<usize>> = Vec::new();
|
||||
loop {
|
||||
if self.lookahead() == '0' || self.lookahead() == '\n' {
|
||||
// End of preferences
|
||||
self.accept();
|
||||
break;
|
||||
} else if self.lookahead() == '=' {
|
||||
// Equal preference
|
||||
self.ballot_has_equal_rankings = true;
|
||||
|
||||
self.accept();
|
||||
preferences.last_mut().unwrap().push(self.usize()? - 1);
|
||||
self.delimiter_not_nl();
|
||||
} else {
|
||||
// No equal preference
|
||||
preferences.push(vec![self.usize()? - 1]);
|
||||
self.delimiter_not_nl();
|
||||
}
|
||||
}
|
||||
|
||||
self.delimiter();
|
||||
|
||||
let ballot = Ballot {
|
||||
orig_value: N::parse(&self.ballot_value_buf),
|
||||
preferences,
|
||||
has_equal_rankings: self.ballot_has_equal_rankings,
|
||||
};
|
||||
self.election.ballots.push(ballot);
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
/// Parse the list of strings at the end of the BLT file
|
||||
fn strings(&mut self) -> Result<(), ParseError> {
|
||||
for index in 0..self.num_candidates {
|
||||
let name = self.string()?;
|
||||
self.election.candidates.push(Candidate {
|
||||
index,
|
||||
name,
|
||||
is_dummy: false,
|
||||
});
|
||||
}
|
||||
let name = self.string()?;
|
||||
self.election.name = name;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// NON-TERMINALS - LOWER LEVEL
|
||||
|
||||
/// Parse an integer into a [usize]
|
||||
fn usize(&mut self) -> Result<usize, ParseError> {
|
||||
// return Ok(self.integer()?.parse().expect("Invalid usize"));
|
||||
// Use a separate implementation to avoid allocating String
|
||||
|
||||
let mut result = self.digit_nonzero()?.to_digit(10).unwrap() as usize;
|
||||
loop {
|
||||
match self.digit() {
|
||||
Err(_) => { break; }
|
||||
Ok(d) => {
|
||||
result = result.checked_mul(10).expect("Integer overflows usize");
|
||||
result = result.checked_add(d.to_digit(10).unwrap() as usize).expect("Integer overflows usize");
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/*/// Parse an integer as a [String]
|
||||
fn integer(&mut self) -> Result<String, ParseError> {
|
||||
let mut result = String::from(self.digit_nonzero()?);
|
||||
loop {
|
||||
match self.digit() {
|
||||
Err(_) => { break; }
|
||||
Ok(d) => { result.push(d); }
|
||||
}
|
||||
}
|
||||
return Ok(result);
|
||||
}*/
|
||||
|
||||
/// Parse a number as an instance of N
|
||||
fn ballot_value(&mut self) -> Result<(), ParseError> {
|
||||
loop {
|
||||
match self.ballot_value_element() {
|
||||
Err(_) => { break; }
|
||||
Ok(d) => { self.ballot_value_buf.push(d); }
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
/// Parse a quoted or raw string
|
||||
fn string(&mut self) -> Result<String, ParseError> {
|
||||
if let Ok(s) = self.quoted_string() {
|
||||
return Ok(s);
|
||||
}
|
||||
if let Ok(s) = self.raw_string() {
|
||||
return Ok(s);
|
||||
}
|
||||
return Err(ParseError::Expected(self.line_no, self.col_no, self.lookahead(), "string"));
|
||||
}
|
||||
|
||||
/// Parse a quoted string
|
||||
fn quoted_string(&mut self) -> Result<String, ParseError> {
|
||||
if self.lookahead() == '"' {
|
||||
self.accept(); // Opening quotation mark
|
||||
let mut result = String::new();
|
||||
|
||||
loop {
|
||||
// Read string contents
|
||||
if self.lookahead() == '"' {
|
||||
break;
|
||||
} else if self.lookahead() == '\\' {
|
||||
// Escape sequence
|
||||
self.accept();
|
||||
if self.lookahead() == '"' || self.lookahead() == '\\' {
|
||||
result.push(self.accept());
|
||||
} else {
|
||||
return Err(ParseError::Unexpected(self.line_no, self.col_no, self.lookahead()));
|
||||
}
|
||||
} else {
|
||||
result.push(self.accept());
|
||||
}
|
||||
}
|
||||
|
||||
while self.lookahead() != '"' {
|
||||
// TODO: BufRead::read_until ?
|
||||
|
||||
}
|
||||
self.accept(); // Closing quotation mark
|
||||
if !self.eof() {
|
||||
self.delimiter();
|
||||
}
|
||||
return Ok(result);
|
||||
} else {
|
||||
return Err(ParseError::Expected(self.line_no, self.col_no, self.lookahead(), "'\"'"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a raw (unquoted) string
|
||||
fn raw_string(&mut self) -> Result<String, ParseError> {
|
||||
let mut result = String::new();
|
||||
while !self.lookahead().is_whitespace() && !self.eof() {
|
||||
result.push(self.accept());
|
||||
}
|
||||
if !self.eof() {
|
||||
self.delimiter();
|
||||
}
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// Skip any sequence of whitespace or comments
|
||||
fn delimiter(&mut self) {
|
||||
loop {
|
||||
if self.eof() {
|
||||
break;
|
||||
} else if self.lookahead() == '#' {
|
||||
self.dnl();
|
||||
if !self.eof() {
|
||||
self.accept(); // Trailing newline
|
||||
}
|
||||
} else if self.lookahead().is_whitespace() {
|
||||
self.accept();
|
||||
while !self.eof() && self.lookahead().is_whitespace() { self.accept(); }
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Skip any sequence of whitespace or comments, but do not accept any newline and leave it trailing
|
||||
fn delimiter_not_nl(&mut self) {
|
||||
loop {
|
||||
if self.eof() {
|
||||
break;
|
||||
} else if self.lookahead() == '#' {
|
||||
self.dnl();
|
||||
} else if self.lookahead().is_whitespace() && self.lookahead() != '\n' {
|
||||
self.accept();
|
||||
while !self.eof() && self.lookahead().is_whitespace() && self.lookahead() != '\n' { self.accept(); }
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Skip to the next newline
|
||||
fn dnl(&mut self) {
|
||||
while !self.eof() && self.lookahead() != '\n' {
|
||||
// TODO: BufRead::read_until ?
|
||||
self.accept();
|
||||
}
|
||||
}
|
||||
|
||||
// TERMINALS
|
||||
|
||||
/// Read a nonzero digit
|
||||
fn digit_nonzero(&mut self) -> Result<char, ParseError> {
|
||||
if self.lookahead() >= '1' && self.lookahead() <= '9' {
|
||||
return Ok(self.accept());
|
||||
} else {
|
||||
return Err(ParseError::Expected(self.line_no, self.col_no, self.lookahead(), "nonzero digit"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Read any digit
|
||||
fn digit(&mut self) -> Result<char, ParseError> {
|
||||
if self.lookahead() >= '0' && self.lookahead() <= '9' {
|
||||
return Ok(self.accept());
|
||||
} else {
|
||||
return Err(ParseError::Expected(self.line_no, self.col_no, self.lookahead(), "digit"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Read any element of a valid number, i.e. a digit, decimal point or slash
|
||||
fn ballot_value_element(&mut self) -> Result<char, ParseError> {
|
||||
if (self.lookahead() >= '0' && self.lookahead() <= '9') || self.lookahead() == '.' || self.lookahead() == '/' {
|
||||
return Ok(self.accept());
|
||||
} else {
|
||||
return Err(ParseError::Expected(self.line_no, self.col_no, self.lookahead(), "number"));
|
||||
}
|
||||
}
|
||||
|
||||
// UTILITIES
|
||||
|
||||
/// Return if this is the end of the file
|
||||
fn eof(&mut self) -> bool {
|
||||
return self.chars.peek().is_none();
|
||||
}
|
||||
|
||||
/// Peek at the next character in the stream
|
||||
fn lookahead(&mut self) -> char {
|
||||
return *self.chars.peek().expect("Unexpected EOF");
|
||||
}
|
||||
|
||||
/// Read and return one character from the stream
|
||||
fn accept(&mut self) -> char {
|
||||
let result = self.chars.next().expect("Unexpected EOF");
|
||||
if result == '\n' {
|
||||
self.line_no += 1;
|
||||
self.col_no = 1;
|
||||
} else {
|
||||
self.col_no += 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// PUBLIC API
|
||||
|
||||
/// Return a new [BLTParser]
|
||||
pub fn new(chars: Peekable<I>) -> Self {
|
||||
Self {
|
||||
chars,
|
||||
ballot_value_buf: String::new(),
|
||||
ballot_has_equal_rankings: false,
|
||||
line_no: 1,
|
||||
col_no: 1,
|
||||
num_candidates: 0,
|
||||
election: Election {
|
||||
name: String::new(),
|
||||
seats: 0,
|
||||
candidates: Vec::new(),
|
||||
withdrawn_candidates: Vec::new(),
|
||||
ballots: Vec::new(),
|
||||
total_votes: None,
|
||||
constraints: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the parsed [Election]
|
||||
pub fn as_election(self) -> Election<N> {
|
||||
return self.election;
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the given BLT file and return an [Election]
|
||||
pub fn parse_iterator<I: Iterator<Item=char>, N: Number>(input: Peekable<I>) -> Result<Election<N>, ParseError> {
|
||||
let mut parser = BLTParser::new(input);
|
||||
parser.parse_blt()?;
|
||||
return Ok(parser.as_election());
|
||||
}
|
||||
|
||||
/// Parse the BLT file at the given path and return an [Election]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn parse_path<P: AsRef<Path>, N: Number>(path: P) -> Result<Election<N>, ParseError> {
|
||||
let mut reader = BufReader::new(File::open(path).expect("IO Error"));
|
||||
let chars = reader.chars().map(|r| r.expect("IO Error")).peekable();
|
||||
return parse_iterator(chars);
|
||||
}
|
|
@ -1,183 +0,0 @@
|
|||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021–2022 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::election::{Ballot, Candidate, Election};
|
||||
use crate::numbers::Number;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use csv::ReaderBuilder;
|
||||
use itertools::Itertools;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read;
|
||||
|
||||
/// Parse the given CSP file
|
||||
pub fn parse_reader<R: Read, N: Number>(reader: R, require_1: bool, require_sequential: bool, require_strict_order: bool) -> Result<Election<N>> {
|
||||
// Read CSV file
|
||||
let mut reader = ReaderBuilder::new()
|
||||
.has_headers(true)
|
||||
.from_reader(reader);
|
||||
|
||||
// Read candidates
|
||||
let mut candidates = Vec::new();
|
||||
let mut col_map = HashMap::new(); // Map csp column -> candidates index
|
||||
|
||||
for (index, cand_name) in reader.headers()?.into_iter().enumerate() {
|
||||
if cand_name == "$mult" {
|
||||
continue;
|
||||
}
|
||||
|
||||
col_map.insert(index, candidates.len());
|
||||
candidates.push(Candidate {
|
||||
index,
|
||||
name: cand_name.to_string(),
|
||||
is_dummy: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Read ballots
|
||||
let mut ballots = Vec::new();
|
||||
|
||||
for (csv_row, record) in reader.into_records().enumerate() {
|
||||
let record = record?;
|
||||
|
||||
let mut value = N::one();
|
||||
|
||||
// Record preferences
|
||||
let mut preferences = Vec::new(); // Vec of (ranking, candidate index)
|
||||
for (csv_col, preference) in record.into_iter().enumerate() {
|
||||
match col_map.get(&csv_col) {
|
||||
Some(cand_index) => {
|
||||
// Preference
|
||||
if preference.is_empty() || preference == "-" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let preference: usize = preference.parse().context(format!("Invalid number \"{}\" at row {}, column {}", preference, csv_row + 2, csv_col + 1))?;
|
||||
if preference == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
preferences.push((preference, cand_index));
|
||||
}
|
||||
None => {
|
||||
// $mult column
|
||||
let mult: usize = preference.parse().context(format!("Invalid number \"{}\" at row {}, column {}", preference, csv_row + 2, csv_col + 1))?;
|
||||
if mult == 1 {
|
||||
continue;
|
||||
}
|
||||
value = N::from(mult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by ranking
|
||||
let mut unique_rankings: Vec<usize> = preferences.iter().map(|(r, _)| *r).unique().collect();
|
||||
unique_rankings.sort_unstable();
|
||||
|
||||
if require_1 {
|
||||
if !unique_rankings.first().map(|r| *r == 1).unwrap_or(false) {
|
||||
// No #1 preference
|
||||
ballots.push(Ballot {
|
||||
orig_value: value,
|
||||
preferences: vec![],
|
||||
has_equal_rankings: false,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let mut sorted_preferences = Vec::with_capacity(preferences.len());
|
||||
let mut last_ranking = None;
|
||||
let mut has_equal_rankings = false;
|
||||
|
||||
for ranking in unique_rankings {
|
||||
// Filter for preferences at this ranking
|
||||
let prefs_this_ranking: Vec<usize> = preferences.iter()
|
||||
.filter_map(|(r, i)| if *r == ranking { Some(**i) } else { None })
|
||||
.collect();
|
||||
|
||||
if prefs_this_ranking.len() != 1 {
|
||||
if require_strict_order {
|
||||
// Duplicate rankings
|
||||
break;
|
||||
}
|
||||
has_equal_rankings = true;
|
||||
}
|
||||
|
||||
if require_sequential {
|
||||
if let Some(r) = last_ranking {
|
||||
if ranking != r + 1 {
|
||||
// Not sequential
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sorted_preferences.push(prefs_this_ranking);
|
||||
last_ranking = Some(ranking);
|
||||
}
|
||||
|
||||
ballots.push(Ballot {
|
||||
orig_value: value,
|
||||
preferences: sorted_preferences,
|
||||
has_equal_rankings,
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(Election {
|
||||
name: String::new(),
|
||||
seats: 0,
|
||||
candidates,
|
||||
withdrawn_candidates: Vec::new(),
|
||||
ballots,
|
||||
total_votes: None,
|
||||
constraints: None,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn csp_formal() {
|
||||
let csp_data = "A,B,C\n1,2,3";
|
||||
let election = parse_reader::<_, crate::numbers::Rational>(csp_data.as_bytes(), false, false, false).unwrap();
|
||||
assert_eq!(election.ballots.first().unwrap().preferences, vec![vec![0], vec![1], vec![2]]);
|
||||
|
||||
let csp_data = "A,B,C\n2,3,4";
|
||||
let election = parse_reader::<_, crate::numbers::Rational>(csp_data.as_bytes(), false, false, false).unwrap();
|
||||
assert_eq!(election.ballots.first().unwrap().preferences, vec![vec![0], vec![1], vec![2]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn csp_no1() {
|
||||
let csp_data = "A,B,C\n2,3,4";
|
||||
let election = parse_reader::<_, crate::numbers::Rational>(csp_data.as_bytes(), true, false, false).unwrap();
|
||||
assert_eq!(election.ballots.first().unwrap().preferences.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn csp_skipped_preference() {
|
||||
let csp_data = "A,B,C\n1,3,4";
|
||||
let election = parse_reader::<_, crate::numbers::Rational>(csp_data.as_bytes(), false, true, false).unwrap();
|
||||
assert_eq!(election.ballots.first().unwrap().preferences, vec![vec![0]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn csp_duplicate_preference() {
|
||||
let csp_data = "A,B,C\n1,2,2";
|
||||
let election = parse_reader::<_, crate::numbers::Rational>(csp_data.as_bytes(), false, false, true).unwrap();
|
||||
assert_eq!(election.ballots.first().unwrap().preferences, vec![vec![0]]);
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/// BIN file parser
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod bin;
|
||||
|
||||
/// BLT file parser
|
||||
pub mod blt;
|
||||
/// CSP file parser
|
||||
pub mod csp;
|
|
@ -1,5 +1,5 @@
|
|||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021–2022 Lee Yingtong Li (RunasSudo)
|
||||
* 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
|
||||
|
@ -29,7 +29,7 @@ impl<'r> SHARandom<'r> {
|
|||
/// Return a new [SHARandom] with the given seed
|
||||
pub fn new(seed: &'r str) -> Self {
|
||||
Self {
|
||||
seed,
|
||||
seed: seed,
|
||||
counter: 0,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,572 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use super::{ExclusionMethod, NextPreferencesEntry, NextPreferencesResult, STVError, STVOptions, SumSurplusTransfersMode, SurplusMethod, SurplusOrder};
|
||||
|
||||
use crate::constraints;
|
||||
use crate::election::{Candidate, CandidateState, CountState, Parcel, Vote};
|
||||
use crate::numbers::Number;
|
||||
use crate::ties;
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
use std::cmp::max;
|
||||
use std::ops;
|
||||
|
||||
/// Distribute first preference votes according to the Gregory method
|
||||
pub fn distribute_first_preferences<N: Number>(state: &mut CountState<N>) {
|
||||
let votes = state.election.ballots.iter().map(|b| Vote {
|
||||
ballot: b,
|
||||
value: b.orig_value.clone(),
|
||||
up_to_pref: 0,
|
||||
}).collect();
|
||||
|
||||
let result = super::next_preferences(state, votes);
|
||||
|
||||
// Transfer candidate votes
|
||||
for (candidate, entry) in result.candidates.into_iter() {
|
||||
let parcel = entry.votes as Parcel<N>;
|
||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||
count_card.parcels.push(parcel);
|
||||
count_card.transfer(&entry.num_votes);
|
||||
}
|
||||
|
||||
// Transfer exhausted votes
|
||||
let parcel = result.exhausted.votes as Parcel<N>;
|
||||
state.exhausted.parcels.push(parcel);
|
||||
state.exhausted.transfer(&result.exhausted.num_votes);
|
||||
|
||||
state.kind = None;
|
||||
state.title = "First preferences".to_string();
|
||||
state.logger.log_literal("First preferences distributed.".to_string());
|
||||
}
|
||||
|
||||
/// Distribute the largest surplus according to the Gregory method, based on [STVOptions::surplus]
|
||||
pub fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError>
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Neg<Output=N>
|
||||
{
|
||||
let quota = state.quota.as_ref().unwrap();
|
||||
let has_surplus: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of tie
|
||||
.filter(|c| {
|
||||
let cc = &state.candidates[c];
|
||||
&cc.votes > quota && cc.parcels.iter().any(|p| !p.is_empty())
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !has_surplus.is_empty() {
|
||||
let total_surpluses = has_surplus.iter()
|
||||
.fold(N::new(), |acc, c| acc + &state.candidates[c].votes - quota);
|
||||
|
||||
// Determine if surplues can be deferred
|
||||
if opts.defer_surpluses {
|
||||
if super::can_defer_surpluses(state, opts, &total_surpluses) {
|
||||
state.logger.log_literal(format!("Distribution of surpluses totalling {:.dps$} votes will be deferred.", total_surpluses, dps=opts.pp_decimals));
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Distribute top candidate's surplus
|
||||
let max_cands = match opts.surplus_order {
|
||||
SurplusOrder::BySize => {
|
||||
ties::multiple_max_by(&has_surplus, |c| &state.candidates[c].votes)
|
||||
}
|
||||
SurplusOrder::ByOrder => {
|
||||
ties::multiple_min_by(&has_surplus, |c| state.candidates[c].order_elected)
|
||||
}
|
||||
};
|
||||
let elected_candidate = if max_cands.len() > 1 {
|
||||
super::choose_highest(state, opts, max_cands)?
|
||||
} else {
|
||||
max_cands[0]
|
||||
};
|
||||
|
||||
distribute_surplus(state, &opts, elected_candidate);
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
/// Return the denominator of the transfer value
|
||||
fn calculate_surplus_denom<N: Number>(surplus: &N, result: &NextPreferencesResult<N>, transferable_votes: &N, weighted: bool, transferable_only: bool) -> Option<N>
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>
|
||||
{
|
||||
if transferable_only {
|
||||
let total_units = if weighted { &result.total_votes } else { &result.total_ballots };
|
||||
let exhausted_units = if weighted { &result.exhausted.num_votes } else { &result.exhausted.num_ballots };
|
||||
let transferable_units = total_units - exhausted_units;
|
||||
|
||||
if transferable_votes > surplus {
|
||||
return Some(transferable_units);
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
if weighted {
|
||||
return Some(result.total_votes.clone());
|
||||
} else {
|
||||
return Some(result.total_ballots.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the reweighted value of the vote after being transferred
|
||||
fn reweight_vote<N: Number>(
|
||||
num_votes: &N,
|
||||
num_ballots: &N,
|
||||
surplus: &N,
|
||||
weighted: bool,
|
||||
surplus_fraction: &Option<N>,
|
||||
surplus_denom: &Option<N>,
|
||||
round_tvs: Option<usize>,
|
||||
rounding: Option<usize>) -> N
|
||||
{
|
||||
let mut result;
|
||||
|
||||
match surplus_denom {
|
||||
Some(v) => {
|
||||
if let Some(_) = round_tvs {
|
||||
// Rounding requested: use the rounded transfer value
|
||||
if weighted {
|
||||
result = num_votes.clone() * surplus_fraction.as_ref().unwrap();
|
||||
} else {
|
||||
result = num_ballots.clone() * surplus_fraction.as_ref().unwrap();
|
||||
}
|
||||
} else {
|
||||
// Avoid unnecessary rounding error by first multiplying by the surplus
|
||||
if weighted {
|
||||
result = num_votes.clone() * surplus / v;
|
||||
} else {
|
||||
result = num_ballots.clone() * surplus / v;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
result = num_votes.clone();
|
||||
}
|
||||
}
|
||||
|
||||
// Round down if requested
|
||||
if let Some(dps) = rounding {
|
||||
result.floor_mut(dps);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Compute the number of votes to credit to a continuing candidate during a surplus transfer, based on [STVOptions::sum_surplus_transfers]
|
||||
fn sum_surplus_transfers<N: Number>(entry: &NextPreferencesEntry<N>, surplus: &N, is_weighted: bool, surplus_fraction: &Option<N>, surplus_denom: &Option<N>, _state: &mut CountState<N>, opts: &STVOptions) -> N
|
||||
where
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
{
|
||||
match opts.sum_surplus_transfers {
|
||||
SumSurplusTransfersMode::SingleStep => {
|
||||
// Calculate transfer across all votes
|
||||
//state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes.", entry.num_ballots, entry.num_votes, dps=opts.pp_decimals));
|
||||
return reweight_vote(&entry.num_votes, &entry.num_ballots, surplus, is_weighted, surplus_fraction, surplus_denom, opts.round_tvs, opts.round_votes);
|
||||
}
|
||||
SumSurplusTransfersMode::ByValue => {
|
||||
// Sum transfers by value
|
||||
let mut result = N::new();
|
||||
|
||||
// Sort into parcels by value
|
||||
let mut votes: Vec<&Vote<N>> = entry.votes.iter().collect();
|
||||
votes.sort_unstable_by(|a, b| (&a.value / &a.ballot.orig_value).cmp(&(&b.value / &b.ballot.orig_value)));
|
||||
for (_value, parcel) in &votes.into_iter().group_by(|v| &v.value / &v.ballot.orig_value) {
|
||||
let mut num_votes = N::new();
|
||||
let mut num_ballots = N::new();
|
||||
for vote in parcel {
|
||||
num_votes += &vote.value;
|
||||
num_ballots += &vote.ballot.orig_value;
|
||||
}
|
||||
//state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes, received at value {:.dps2$}.", num_ballots, num_votes, value, dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
||||
result += reweight_vote(&num_votes, &num_ballots, surplus, is_weighted, surplus_fraction, surplus_denom, opts.round_tvs, opts.round_votes);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
SumSurplusTransfersMode::PerBallot => {
|
||||
// Sum transfer per each individual ballot
|
||||
// TODO: This could be moved to distribute_surplus to avoid looping over the votes and calculating transfer values twice
|
||||
let mut result = N::new();
|
||||
for vote in entry.votes.iter() {
|
||||
result += reweight_vote(&vote.value, &vote.ballot.orig_value, surplus, is_weighted, surplus_fraction, surplus_denom, opts.round_tvs, opts.round_votes);
|
||||
}
|
||||
//state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes.", entry.num_ballots, entry.num_votes, dps=opts.pp_decimals));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Distribute the surplus of a given candidate according to the Gregory method, based on [STVOptions::surplus]
|
||||
fn distribute_surplus<N: Number>(state: &mut CountState<N>, opts: &STVOptions, elected_candidate: &Candidate)
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Neg<Output=N>
|
||||
{
|
||||
state.logger.log_literal(format!("Surplus of {} distributed.", elected_candidate.name));
|
||||
|
||||
let count_card = &state.candidates[elected_candidate];
|
||||
let surplus = &count_card.votes - state.quota.as_ref().unwrap();
|
||||
|
||||
let votes;
|
||||
match opts.surplus {
|
||||
SurplusMethod::WIG | SurplusMethod::UIG => {
|
||||
// Inclusive Gregory
|
||||
votes = count_card.parcels.concat();
|
||||
}
|
||||
SurplusMethod::EG => {
|
||||
// Exclusive Gregory
|
||||
// Should be safe to unwrap() - or else how did we get a quota!
|
||||
votes = state.candidates.get_mut(elected_candidate).unwrap().parcels.pop().unwrap();
|
||||
}
|
||||
_ => { panic!("Invalid --surplus for Gregory method"); }
|
||||
}
|
||||
|
||||
// Count next preferences
|
||||
let result = super::next_preferences(state, votes);
|
||||
|
||||
state.kind = Some("Surplus of");
|
||||
state.title = String::from(&elected_candidate.name);
|
||||
|
||||
// Transfer candidate votes
|
||||
// TODO: Refactor??
|
||||
let is_weighted = match opts.surplus {
|
||||
SurplusMethod::WIG => { true }
|
||||
SurplusMethod::UIG | SurplusMethod::EG => { false }
|
||||
SurplusMethod::Meek => { todo!() }
|
||||
};
|
||||
|
||||
let transferable_votes = &result.total_votes - &result.exhausted.num_votes;
|
||||
let surplus_denom = calculate_surplus_denom(&surplus, &result, &transferable_votes, is_weighted, opts.transferable_only);
|
||||
let mut surplus_fraction;
|
||||
match surplus_denom {
|
||||
Some(ref v) => {
|
||||
surplus_fraction = Some(surplus.clone() / v);
|
||||
|
||||
// Round down if requested
|
||||
if let Some(dps) = opts.round_tvs {
|
||||
surplus_fraction.as_mut().unwrap().floor_mut(dps);
|
||||
}
|
||||
|
||||
if opts.transferable_only {
|
||||
if &result.total_ballots - &result.exhausted.num_ballots == N::one() {
|
||||
state.logger.log_literal(format!("Transferring 1 transferable ballot, totalling {:.dps$} transferable votes, with surplus fraction {:.dps2$}.", transferable_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
||||
} else {
|
||||
state.logger.log_literal(format!("Transferring {:.0} transferable ballots, totalling {:.dps$} transferable votes, with surplus fraction {:.dps2$}.", &result.total_ballots - &result.exhausted.num_ballots, transferable_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
||||
}
|
||||
} else {
|
||||
if result.total_ballots == N::one() {
|
||||
state.logger.log_literal(format!("Transferring 1 ballot, totalling {:.dps$} votes, with surplus fraction {:.dps2$}.", result.total_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
||||
} else {
|
||||
state.logger.log_literal(format!("Transferring {:.0} ballots, totalling {:.dps$} votes, with surplus fraction {:.dps2$}.", result.total_ballots, result.total_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
surplus_fraction = None;
|
||||
|
||||
if opts.transferable_only {
|
||||
state.logger.log_literal(format!("Transferring {:.0} transferable ballots, totalling {:.dps$} transferable votes, at values received.", &result.total_ballots - &result.exhausted.num_ballots, transferable_votes, dps=opts.pp_decimals));
|
||||
} else {
|
||||
state.logger.log_literal(format!("Transferring {:.0} ballots, totalling {:.dps$} votes, at values received.", result.total_ballots, result.total_votes, dps=opts.pp_decimals));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut checksum = N::new();
|
||||
|
||||
for (candidate, entry) in result.candidates.into_iter() {
|
||||
// Credit transferred votes
|
||||
let candidate_transfers = sum_surplus_transfers(&entry, &surplus, is_weighted, &surplus_fraction, &surplus_denom, state, opts);
|
||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||
count_card.transfer(&candidate_transfers);
|
||||
checksum += candidate_transfers;
|
||||
|
||||
let mut parcel = entry.votes as Parcel<N>;
|
||||
|
||||
// Reweight votes
|
||||
for vote in parcel.iter_mut() {
|
||||
vote.value = reweight_vote(&vote.value, &vote.ballot.orig_value, &surplus, is_weighted, &surplus_fraction, &surplus_denom, opts.round_tvs, opts.round_weights);
|
||||
}
|
||||
|
||||
count_card.parcels.push(parcel);
|
||||
}
|
||||
|
||||
// Credit exhausted votes
|
||||
let mut exhausted_transfers;
|
||||
if opts.transferable_only {
|
||||
if transferable_votes > surplus {
|
||||
// No ballots exhaust
|
||||
exhausted_transfers = N::new();
|
||||
} else {
|
||||
exhausted_transfers = &surplus - &transferable_votes;
|
||||
|
||||
if let Some(dps) = opts.round_votes {
|
||||
exhausted_transfers.floor_mut(dps);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
exhausted_transfers = sum_surplus_transfers(&result.exhausted, &surplus, is_weighted, &surplus_fraction, &surplus_denom, state, opts);
|
||||
}
|
||||
|
||||
state.exhausted.transfer(&exhausted_transfers);
|
||||
checksum += exhausted_transfers;
|
||||
|
||||
// Transfer exhausted votes
|
||||
let parcel = result.exhausted.votes as Parcel<N>;
|
||||
state.exhausted.parcels.push(parcel);
|
||||
|
||||
// Finalise candidate votes
|
||||
let count_card = state.candidates.get_mut(elected_candidate).unwrap();
|
||||
count_card.transfers = -&surplus;
|
||||
count_card.votes.assign(state.quota.as_ref().unwrap());
|
||||
checksum -= surplus;
|
||||
|
||||
count_card.parcels.clear(); // Mark surpluses as done
|
||||
|
||||
// Update loss by fraction
|
||||
state.loss_fraction.transfer(&-checksum);
|
||||
}
|
||||
|
||||
/// Perform one stage of a candidate exclusion according to the Gregory method, based on [STVOptions::exclusion]
|
||||
pub fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>)
|
||||
where
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
{
|
||||
// Used to give bulk excluded candidate the same order_elected
|
||||
let order_excluded = state.num_excluded + 1;
|
||||
|
||||
for excluded_candidate in excluded_candidates.iter() {
|
||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||
|
||||
// Rust borrow checker is unhappy if we try to put this in exclude_hopefuls ??!
|
||||
if count_card.state != CandidateState::Excluded {
|
||||
count_card.state = CandidateState::Excluded;
|
||||
state.num_excluded += 1;
|
||||
count_card.order_elected = -(order_excluded as isize);
|
||||
|
||||
constraints::update_constraints(state, opts);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine votes to transfer in this stage
|
||||
let mut votes = Vec::new();
|
||||
let mut votes_remain;
|
||||
let mut checksum = N::new();
|
||||
|
||||
match opts.exclusion {
|
||||
ExclusionMethod::SingleStage => {
|
||||
// Exclude in one round
|
||||
for excluded_candidate in excluded_candidates.iter() {
|
||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||
votes.append(&mut count_card.parcels.concat());
|
||||
count_card.parcels.clear();
|
||||
|
||||
// Update votes
|
||||
let votes_transferred = votes.iter().fold(N::new(), |acc, v| acc + &v.value);
|
||||
checksum -= &votes_transferred;
|
||||
count_card.transfer(&-votes_transferred);
|
||||
}
|
||||
votes_remain = false;
|
||||
}
|
||||
ExclusionMethod::ByValue => {
|
||||
// Exclude by value
|
||||
let max_value = excluded_candidates.iter()
|
||||
.map(|c| state.candidates[c].parcels.iter()
|
||||
.map(|p| p.iter().map(|v| &v.value / &v.ballot.orig_value).max().unwrap())
|
||||
.max().unwrap())
|
||||
.max().unwrap();
|
||||
|
||||
votes_remain = false;
|
||||
|
||||
for excluded_candidate in excluded_candidates.iter() {
|
||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||
|
||||
// Filter out just those votes with max_value
|
||||
let mut remaining_votes = Vec::new();
|
||||
|
||||
let cand_votes = count_card.parcels.concat();
|
||||
|
||||
let mut votes_transferred = N::new();
|
||||
for vote in cand_votes.into_iter() {
|
||||
if &vote.value / &vote.ballot.orig_value == max_value {
|
||||
votes_transferred += &vote.value;
|
||||
votes.push(vote);
|
||||
} else {
|
||||
remaining_votes.push(vote);
|
||||
}
|
||||
}
|
||||
|
||||
if !remaining_votes.is_empty() {
|
||||
votes_remain = true;
|
||||
}
|
||||
|
||||
// Leave remaining votes with candidate (as one parcel)
|
||||
count_card.parcels = vec![remaining_votes];
|
||||
|
||||
// Update votes
|
||||
checksum -= &votes_transferred;
|
||||
count_card.transfer(&-votes_transferred);
|
||||
}
|
||||
}
|
||||
ExclusionMethod::ParcelsByOrder => {
|
||||
// Exclude by parcel by order
|
||||
if excluded_candidates.len() > 1 {
|
||||
panic!("--exclusion parcels_by_order is incompatible with --bulk-exclude");
|
||||
}
|
||||
|
||||
let count_card = state.candidates.get_mut(excluded_candidates[0]).unwrap();
|
||||
votes = count_card.parcels.remove(0);
|
||||
votes_remain = !count_card.parcels.is_empty();
|
||||
|
||||
// Update votes
|
||||
let votes_transferred = votes.iter().fold(N::new(), |acc, v| acc + &v.value);
|
||||
checksum -= &votes_transferred;
|
||||
count_card.transfer(&-votes_transferred);
|
||||
}
|
||||
_ => panic!()
|
||||
}
|
||||
|
||||
if !votes.is_empty() {
|
||||
let value = &votes[0].value / &votes[0].ballot.orig_value;
|
||||
|
||||
// Count next preferences
|
||||
let result = super::next_preferences(state, votes);
|
||||
|
||||
if let ExclusionMethod::SingleStage = opts.exclusion {
|
||||
if result.total_ballots == N::one() {
|
||||
state.logger.log_literal(format!("Transferring 1 ballot, totalling {:.dps$} votes.", result.total_votes, dps=opts.pp_decimals));
|
||||
} else {
|
||||
state.logger.log_literal(format!("Transferring {:.0} ballots, totalling {:.dps$} votes.", result.total_ballots, result.total_votes, dps=opts.pp_decimals));
|
||||
}
|
||||
} else {
|
||||
if result.total_ballots == N::one() {
|
||||
state.logger.log_literal(format!("Transferring 1 ballot, totalling {:.dps$} votes, received at value {:.dps2$}.", result.total_votes, value, dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
||||
} else {
|
||||
state.logger.log_literal(format!("Transferring {:.0} ballots, totalling {:.dps$} votes, received at value {:.dps2$}.", result.total_ballots, result.total_votes, value, dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
||||
}
|
||||
}
|
||||
|
||||
// Transfer candidate votes
|
||||
for (candidate, entry) in result.candidates.into_iter() {
|
||||
let parcel = entry.votes as Parcel<N>;
|
||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||
count_card.parcels.push(parcel);
|
||||
|
||||
// Round transfers
|
||||
let mut candidate_transfers = entry.num_votes;
|
||||
if let Some(dps) = opts.round_votes {
|
||||
candidate_transfers.floor_mut(dps);
|
||||
}
|
||||
count_card.transfer(&candidate_transfers);
|
||||
checksum += candidate_transfers;
|
||||
}
|
||||
|
||||
// Transfer exhausted votes
|
||||
let parcel = result.exhausted.votes as Parcel<N>;
|
||||
state.exhausted.parcels.push(parcel);
|
||||
|
||||
let mut exhausted_transfers = result.exhausted.num_votes;
|
||||
if let Some(dps) = opts.round_votes {
|
||||
exhausted_transfers.floor_mut(dps);
|
||||
}
|
||||
state.exhausted.transfer(&exhausted_transfers);
|
||||
checksum += exhausted_transfers;
|
||||
}
|
||||
|
||||
if !votes_remain {
|
||||
// Finalise candidate votes
|
||||
for excluded_candidate in excluded_candidates.into_iter() {
|
||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||
checksum -= &count_card.votes;
|
||||
count_card.transfers -= &count_card.votes;
|
||||
count_card.votes = N::new();
|
||||
}
|
||||
|
||||
if let ExclusionMethod::SingleStage = opts.exclusion {
|
||||
} else {
|
||||
state.logger.log_literal("Exclusion complete.".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Update loss by fraction
|
||||
state.loss_fraction.transfer(&-checksum);
|
||||
}
|
||||
|
||||
/// Perform one stage of a candidate exclusion according to the Wright method
|
||||
pub fn wright_exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>)
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
{
|
||||
// Used to give bulk excluded candidate the same order_elected
|
||||
let order_excluded = state.num_excluded + 1;
|
||||
|
||||
for excluded_candidate in excluded_candidates.iter() {
|
||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||
|
||||
// Rust borrow checker is unhappy if we try to put this in exclude_hopefuls ??!
|
||||
if count_card.state != CandidateState::Excluded {
|
||||
count_card.state = CandidateState::Excluded;
|
||||
state.num_excluded += 1;
|
||||
count_card.order_elected = -(order_excluded as isize);
|
||||
}
|
||||
|
||||
constraints::update_constraints(state, opts);
|
||||
}
|
||||
|
||||
// Reset count
|
||||
for (_, count_card) in state.candidates.iter_mut() {
|
||||
if count_card.order_elected > 0 {
|
||||
count_card.order_elected = 0;
|
||||
}
|
||||
count_card.parcels.clear();
|
||||
count_card.votes = N::new();
|
||||
count_card.transfers = N::new();
|
||||
count_card.state = match count_card.state {
|
||||
CandidateState::Withdrawn => CandidateState::Withdrawn,
|
||||
CandidateState::Excluded => CandidateState::Excluded,
|
||||
_ => CandidateState::Hopeful,
|
||||
};
|
||||
}
|
||||
|
||||
state.exhausted.votes = N::new();
|
||||
state.exhausted.transfers = N::new();
|
||||
state.loss_fraction.votes = N::new();
|
||||
state.loss_fraction.transfers = N::new();
|
||||
|
||||
state.num_elected = 0;
|
||||
|
||||
let orig_title = state.title.clone();
|
||||
|
||||
// Redistribute first preferences
|
||||
super::distribute_first_preferences(state, opts);
|
||||
|
||||
state.kind = Some("Exclusion of");
|
||||
state.title = orig_title;
|
||||
|
||||
// Trigger recalculation of quota within stv::count_one_stage
|
||||
state.quota = None;
|
||||
state.vote_required_election = None;
|
||||
}
|
|
@ -1,799 +0,0 @@
|
|||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021–2023 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// --------------
|
||||
// Child packages
|
||||
|
||||
/// Transfer tables
|
||||
mod transfers;
|
||||
pub use transfers::{TransferTable, TransferTableCell, TransferTableColumn};
|
||||
|
||||
/// prettytable-compatible API for HTML table output in WebAssembly
|
||||
pub mod prettytable_html;
|
||||
|
||||
// --------
|
||||
// STV code
|
||||
|
||||
use super::{ExclusionMethod, RoundSubtransfersMode, STVError, STVOptions, SurplusMethod, SurplusOrder};
|
||||
use super::sample;
|
||||
|
||||
use crate::constraints;
|
||||
use crate::election::{Candidate, CandidateState, CountState, Parcel, StageKind, Vote};
|
||||
use crate::numbers::Number;
|
||||
use crate::ties;
|
||||
|
||||
use std::cmp::max;
|
||||
use std::ops;
|
||||
|
||||
/// Distribute first preference votes according to the Gregory method
|
||||
pub fn distribute_first_preferences<N: Number>(state: &mut CountState<N>, opts: &STVOptions)
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>
|
||||
{
|
||||
let votes = state.election.ballots.iter().map(|b| Vote {
|
||||
ballot: b,
|
||||
up_to_pref: 0,
|
||||
}).collect();
|
||||
|
||||
let result = super::next_preferences(state, votes);
|
||||
|
||||
// Transfer candidate votes
|
||||
for (candidate, entry) in result.candidates.into_iter() {
|
||||
let parcel = Parcel {
|
||||
votes: entry.votes,
|
||||
value_fraction: N::one(),
|
||||
source_order: 0,
|
||||
};
|
||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||
count_card.parcels.push(parcel);
|
||||
|
||||
let mut vote_transfers = entry.num_ballots.clone();
|
||||
if let Some(dps) = opts.round_votes {
|
||||
vote_transfers.floor_mut(dps);
|
||||
}
|
||||
count_card.transfer(&vote_transfers);
|
||||
|
||||
count_card.ballot_transfers += entry.num_ballots;
|
||||
}
|
||||
|
||||
// Transfer exhausted votes
|
||||
let parcel = Parcel {
|
||||
votes: result.exhausted.votes,
|
||||
value_fraction: N::one(),
|
||||
source_order: 0,
|
||||
};
|
||||
state.exhausted.parcels.push(parcel);
|
||||
state.exhausted.transfer(&result.exhausted.num_ballots);
|
||||
state.exhausted.ballot_transfers += result.exhausted.num_ballots;
|
||||
|
||||
// Calculate loss by fraction - if minivoters used
|
||||
if let Some(orig_total) = &state.election.total_votes {
|
||||
let mut total_votes = state.total_vote();
|
||||
total_votes += &state.exhausted.votes;
|
||||
let lbf = orig_total - &total_votes;
|
||||
|
||||
state.loss_fraction.votes = lbf.clone();
|
||||
state.loss_fraction.transfers = lbf;
|
||||
}
|
||||
|
||||
state.title = StageKind::FirstPreferences;
|
||||
state.logger.log_literal("First preferences distributed.".to_string());
|
||||
}
|
||||
|
||||
/// Distribute the largest surplus according to the Gregory or random subset method, based on [STVOptions::surplus]
|
||||
///
|
||||
/// Returns `true` if any surpluses were distributed.
|
||||
pub fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError>
|
||||
where
|
||||
for<'r> &'r N: ops::Add<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Neg<Output=N>
|
||||
{
|
||||
let quota = state.quota.as_ref().unwrap();
|
||||
let has_surplus: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of tie
|
||||
.filter(|c| {
|
||||
let cc = &state.candidates[c];
|
||||
&cc.votes > quota && !cc.finalised
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !has_surplus.is_empty() {
|
||||
let total_surpluses = state.total_surplus();
|
||||
|
||||
// Determine if surplues can be deferred
|
||||
if opts.defer_surpluses {
|
||||
if super::can_defer_surpluses(state, opts, &total_surpluses) {
|
||||
state.logger.log_literal(format!("Distribution of surpluses totalling {:.dps$} votes will be deferred.", total_surpluses, dps=opts.pp_decimals));
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Distribute top candidate's surplus
|
||||
let max_cands = match opts.surplus_order {
|
||||
SurplusOrder::BySize => {
|
||||
ties::multiple_max_by(&has_surplus, |c| &state.candidates[c].votes)
|
||||
}
|
||||
SurplusOrder::ByOrder => {
|
||||
ties::multiple_min_by(&has_surplus, |c| state.candidates[c].order_elected)
|
||||
}
|
||||
};
|
||||
let elected_candidate = if max_cands.len() > 1 {
|
||||
super::choose_highest(state, opts, &max_cands, "Which candidate's surplus to distribute?")?
|
||||
} else {
|
||||
max_cands[0]
|
||||
};
|
||||
|
||||
// If --no-immediate-elect, declare elected the candidate with the highest surplus
|
||||
if !opts.immediate_elect {
|
||||
let count_card = state.candidates.get_mut(elected_candidate).unwrap();
|
||||
count_card.state = CandidateState::Elected;
|
||||
state.num_elected += 1;
|
||||
count_card.order_elected = state.num_elected as isize;
|
||||
|
||||
state.logger.log_smart(
|
||||
"{} meets the quota and is elected.",
|
||||
"{} meet the quota and are elected.",
|
||||
vec![elected_candidate.name.as_str()]
|
||||
);
|
||||
|
||||
constraints::update_constraints(state, opts);
|
||||
}
|
||||
|
||||
match opts.surplus {
|
||||
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => { distribute_surplus(state, opts, elected_candidate); }
|
||||
SurplusMethod::IHare | SurplusMethod::Hare => { sample::distribute_surplus(state, opts, elected_candidate)?; }
|
||||
_ => unreachable!()
|
||||
}
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// If --no-immediate-elect, check for candidates with exactly a quota to elect
|
||||
// However, if --defer-surpluses, zero surplus is necessarily deferred so skip
|
||||
if !opts.immediate_elect && !opts.defer_surpluses {
|
||||
if super::elect_hopefuls(state, opts, false)? {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
/// Return the denominator of the surplus fraction
|
||||
///
|
||||
/// Returns `None` if the value of transferable votes <= surplus (i.e. all transferable votes are transferred at values received).
|
||||
fn calculate_surplus_denom<'n, N: Number>(surplus: &N, transferable_ballots: &'n N, transferable_votes: &'n N, total_ballots: &'n N, total_votes: &'n N, opts: &STVOptions) -> Option<N>
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>
|
||||
{
|
||||
if opts.transferable_only {
|
||||
let transferable_units = if opts.surplus.is_weighted() { transferable_votes } else { transferable_ballots };
|
||||
|
||||
if transferable_votes > surplus {
|
||||
return Some(transferable_units.clone());
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
if opts.surplus.is_weighted() {
|
||||
return Some(total_votes.clone());
|
||||
} else {
|
||||
return Some(total_ballots.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Distribute the surplus of a given candidate according to the Gregory method, based on [STVOptions::surplus]
|
||||
pub fn distribute_surplus<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, elected_candidate: &'a Candidate)
|
||||
where
|
||||
for<'r> &'r N: ops::Add<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Neg<Output=N>
|
||||
{
|
||||
state.title = StageKind::SurplusOf(elected_candidate);
|
||||
state.logger.log_literal(format!("Surplus of {} distributed.", elected_candidate.name));
|
||||
|
||||
let count_card = &state.candidates[elected_candidate];
|
||||
let surplus = &count_card.votes - state.quota.as_ref().unwrap();
|
||||
|
||||
// Determine which votes to examine
|
||||
|
||||
let mut parcels;
|
||||
match opts.surplus {
|
||||
SurplusMethod::WIG | SurplusMethod::UIG => {
|
||||
// Inclusive Gregory
|
||||
parcels = Vec::new();
|
||||
parcels.append(&mut state.candidates.get_mut(elected_candidate).unwrap().parcels);
|
||||
}
|
||||
SurplusMethod::EG => {
|
||||
// Exclusive Gregory
|
||||
// Should be safe to unwrap() - or else how did we get a quota!
|
||||
parcels = vec![state.candidates.get_mut(elected_candidate).unwrap().parcels.pop().unwrap()];
|
||||
}
|
||||
_ => unreachable!()
|
||||
}
|
||||
|
||||
// Count votes
|
||||
|
||||
let mut parcels_next_prefs = Vec::new();
|
||||
|
||||
let mut transferable_ballots = N::new();
|
||||
let mut transferable_votes = N::new();
|
||||
|
||||
let mut exhausted_ballots = N::new();
|
||||
let mut exhausted_votes = N::new();
|
||||
|
||||
for (parcel_num, parcel) in parcels.into_iter().enumerate() {
|
||||
// Count next preferences
|
||||
let result = super::next_preferences(state, parcel.votes);
|
||||
|
||||
for (_, entry) in result.candidates.iter() {
|
||||
transferable_ballots += &entry.num_ballots;
|
||||
transferable_votes += &entry.num_ballots * &parcel.value_fraction;
|
||||
}
|
||||
|
||||
exhausted_ballots += &result.exhausted.num_ballots;
|
||||
exhausted_votes += &result.exhausted.num_ballots * &parcel.value_fraction;
|
||||
|
||||
// Determine which column of the transfer table to use
|
||||
let table_column_num = match opts.round_subtransfers {
|
||||
RoundSubtransfersMode::ByValueAndSource => Some(parcel.source_order),
|
||||
RoundSubtransfersMode::ByParcel => Some(parcel_num),
|
||||
_ => Some(0)
|
||||
};
|
||||
|
||||
parcels_next_prefs.push((parcel.value_fraction, table_column_num, result));
|
||||
}
|
||||
|
||||
// Calculate and print surplus fraction
|
||||
|
||||
let total_ballots = &transferable_ballots + &exhausted_ballots;
|
||||
let mut total_votes = &transferable_votes + &exhausted_votes;
|
||||
|
||||
let count_card = state.candidates.get_mut(elected_candidate).unwrap();
|
||||
count_card.ballot_transfers = -&total_ballots;
|
||||
|
||||
if opts.surplus_assume_total {
|
||||
// Override total_votes
|
||||
total_votes = count_card.votes.clone();
|
||||
|
||||
if opts.transferable_only {
|
||||
// Override transferable_votes
|
||||
transferable_votes = count_card.votes.clone() - exhausted_votes;
|
||||
}
|
||||
}
|
||||
|
||||
let mut surplus_denom = calculate_surplus_denom(&surplus, &transferable_ballots, &transferable_votes, &total_ballots, &total_votes, opts);
|
||||
let surplus_numer;
|
||||
let mut surplus_fraction;
|
||||
match &surplus_denom {
|
||||
Some(v) => {
|
||||
surplus_fraction = Some(surplus.clone() / v);
|
||||
|
||||
// Round down if requested
|
||||
if let Some(dps) = opts.round_surplus_fractions {
|
||||
surplus_fraction.as_mut().unwrap().floor_mut(dps);
|
||||
surplus_numer = surplus_fraction.clone();
|
||||
surplus_denom = None;
|
||||
} else {
|
||||
surplus_numer = Some(surplus.clone());
|
||||
}
|
||||
|
||||
if opts.transferable_only {
|
||||
if transferable_ballots == N::one() {
|
||||
state.logger.log_literal(format!("Transferring 1 transferable ballot, totalling {:.dps$} transferable votes, with surplus fraction {:.dps2$}.", transferable_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
||||
} else {
|
||||
state.logger.log_literal(format!("Transferring {:.0} transferable ballots, totalling {:.dps$} transferable votes, with surplus fraction {:.dps2$}.", transferable_ballots, transferable_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
||||
}
|
||||
} else {
|
||||
if total_ballots == N::one() {
|
||||
state.logger.log_literal(format!("Transferring 1 ballot, totalling {:.dps$} votes, with surplus fraction {:.dps2$}.", total_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
||||
} else {
|
||||
state.logger.log_literal(format!("Transferring {:.0} ballots, totalling {:.dps$} votes, with surplus fraction {:.dps2$}.", total_ballots, total_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
surplus_fraction = None;
|
||||
surplus_numer = None;
|
||||
surplus_denom = None;
|
||||
|
||||
// This can only happen if --transferable-only
|
||||
if transferable_ballots == N::one() {
|
||||
state.logger.log_literal(format!("Transferring 1 transferable ballot, totalling {:.dps$} transferable votes, at values received.", transferable_votes, dps=opts.pp_decimals));
|
||||
} else {
|
||||
state.logger.log_literal(format!("Transferring {:.0} transferable ballots, totalling {:.dps$} transferable votes, at values received.", transferable_ballots, transferable_votes, dps=opts.pp_decimals));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reweight and transfer parcels
|
||||
|
||||
let mut transfer_table = TransferTable::new_surplus(
|
||||
state.election.candidates.iter().filter(|c| state.candidates[c].state == CandidateState::Hopeful || state.candidates[c].state == CandidateState::Guarded).collect(),
|
||||
surplus.clone(), surplus_fraction.clone(), surplus_numer.clone(), surplus_denom.clone()
|
||||
);
|
||||
|
||||
for (value_fraction, table_column_num, result) in parcels_next_prefs {
|
||||
for (candidate, entry) in result.candidates.into_iter() {
|
||||
// Record transfers
|
||||
transfer_table.add_transfers(
|
||||
&value_fraction,
|
||||
table_column_num,
|
||||
candidate,
|
||||
&entry.num_ballots
|
||||
);
|
||||
|
||||
let mut new_value_fraction;
|
||||
if opts.surplus.is_weighted() {
|
||||
new_value_fraction = value_fraction.clone();
|
||||
new_value_fraction *= surplus_numer.as_ref().unwrap(); // Guaranteed to be Some in WIGM
|
||||
if let Some(n) = &surplus_denom {
|
||||
new_value_fraction /= n;
|
||||
}
|
||||
} else {
|
||||
if let Some(sf) = &surplus_fraction {
|
||||
new_value_fraction = sf.clone();
|
||||
} else {
|
||||
new_value_fraction = value_fraction.clone();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(dps) = opts.round_values {
|
||||
new_value_fraction.floor_mut(dps);
|
||||
}
|
||||
|
||||
// Transfer candidate votes
|
||||
let parcel = Parcel {
|
||||
votes: entry.votes,
|
||||
value_fraction: new_value_fraction,
|
||||
source_order: state.num_elected + state.num_excluded,
|
||||
};
|
||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||
count_card.parcels.push(parcel);
|
||||
}
|
||||
|
||||
// Record exhausted votes
|
||||
transfer_table.add_exhausted(
|
||||
&value_fraction,
|
||||
table_column_num,
|
||||
&result.exhausted.num_ballots
|
||||
);
|
||||
|
||||
// Transfer exhausted votes
|
||||
let parcel = Parcel {
|
||||
votes: result.exhausted.votes,
|
||||
value_fraction, // TODO: Reweight exhausted votes
|
||||
source_order: state.num_elected + state.num_excluded,
|
||||
};
|
||||
state.exhausted.parcels.push(parcel);
|
||||
}
|
||||
|
||||
let mut checksum = N::new();
|
||||
|
||||
// Credit transferred votes
|
||||
transfer_table.calculate(opts);
|
||||
checksum += transfer_table.apply_to(state, opts);
|
||||
state.transfer_table = Some(transfer_table);
|
||||
|
||||
// Finalise candidate votes
|
||||
let count_card = state.candidates.get_mut(elected_candidate).unwrap();
|
||||
count_card.transfers = -&surplus;
|
||||
count_card.votes.assign(state.quota.as_ref().unwrap());
|
||||
checksum -= surplus;
|
||||
|
||||
count_card.finalised = true; // Mark surpluses as done
|
||||
|
||||
// Update loss by fraction
|
||||
state.loss_fraction.transfer(&-checksum);
|
||||
}
|
||||
|
||||
/// Perform one stage of a candidate exclusion according to the Gregory method, based on [STVOptions::exclusion]
|
||||
#[allow(clippy::branches_sharing_code)]
|
||||
pub fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>, complete_type: &'static str)
|
||||
where
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
{
|
||||
// Used to give bulk excluded candidate the same order_elected
|
||||
let order_excluded = state.num_excluded + 1;
|
||||
|
||||
for excluded_candidate in excluded_candidates.iter() {
|
||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||
|
||||
// Rust borrow checker is unhappy if we try to put this in exclude_hopefuls ??!
|
||||
if count_card.state != CandidateState::Excluded {
|
||||
count_card.state = CandidateState::Excluded;
|
||||
state.num_excluded += 1;
|
||||
count_card.order_elected = -(order_excluded as isize);
|
||||
|
||||
constraints::update_constraints(state, opts);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine votes to transfer in this stage
|
||||
let mut parcels = Vec::new();
|
||||
let mut votes_remain;
|
||||
let mut checksum = N::new();
|
||||
|
||||
match opts.exclusion {
|
||||
ExclusionMethod::SingleStage => {
|
||||
// Exclude in one round
|
||||
for excluded_candidate in excluded_candidates.iter() {
|
||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||
count_card.ballot_transfers = -count_card.num_ballots();
|
||||
count_card.finalised = true;
|
||||
|
||||
parcels.append(&mut count_card.parcels);
|
||||
|
||||
// Update votes
|
||||
checksum -= &count_card.votes;
|
||||
count_card.transfers = -count_card.votes.clone();
|
||||
count_card.votes = N::new();
|
||||
}
|
||||
votes_remain = false;
|
||||
}
|
||||
ExclusionMethod::ByValue | ExclusionMethod::FirstPreferencesThenByValue => {
|
||||
// Exclude by value
|
||||
let excluded_with_votes: Vec<&&Candidate> = excluded_candidates.iter()
|
||||
.filter(|c| { let cc = &state.candidates[*c]; !cc.finalised && !cc.parcels.is_empty() })
|
||||
.collect();
|
||||
|
||||
if excluded_with_votes.is_empty() {
|
||||
votes_remain = false;
|
||||
} else {
|
||||
votes_remain = false;
|
||||
let mut votes = Vec::new();
|
||||
|
||||
if opts.exclusion == ExclusionMethod::FirstPreferencesThenByValue
|
||||
&& excluded_with_votes.iter().any(|c| state.candidates[*c].parcels.iter().any(|p| p.source_order == 0))
|
||||
{
|
||||
// If candidates to exclude still having votes, select only those with first preferences
|
||||
for excluded_candidate in excluded_with_votes.iter() {
|
||||
let count_card = state.candidates.get_mut(*excluded_candidate).unwrap();
|
||||
let mut cc_parcels = Vec::new();
|
||||
cc_parcels.append(&mut count_card.parcels);
|
||||
|
||||
// Filter out just those first preferences
|
||||
let mut remaining_parcels = Vec::new();
|
||||
|
||||
for mut parcel in cc_parcels {
|
||||
if parcel.source_order == 0 {
|
||||
count_card.ballot_transfers -= parcel.num_ballots();
|
||||
|
||||
let votes_transferred = parcel.num_votes();
|
||||
votes.append(&mut parcel.votes);
|
||||
|
||||
// Update votes
|
||||
checksum -= &votes_transferred;
|
||||
count_card.transfer(&-votes_transferred);
|
||||
} else {
|
||||
remaining_parcels.push(parcel);
|
||||
}
|
||||
}
|
||||
|
||||
if !remaining_parcels.is_empty() {
|
||||
votes_remain = true;
|
||||
}
|
||||
|
||||
// Leave remaining votes with candidate
|
||||
count_card.parcels = remaining_parcels;
|
||||
}
|
||||
|
||||
// Group all votes of one value in single parcel
|
||||
parcels.push(Parcel {
|
||||
votes,
|
||||
value_fraction: N::one(), // By definition, first preferences have value of 1
|
||||
source_order: 0, // Set this later
|
||||
});
|
||||
} else {
|
||||
// If candidates to exclude still having votes, select only those with the greatest value
|
||||
let max_value = excluded_with_votes.iter()
|
||||
.map(|c| state.candidates[*c].parcels.iter()
|
||||
.map(|p| &p.value_fraction)
|
||||
.max().unwrap())
|
||||
.max().unwrap()
|
||||
.clone();
|
||||
|
||||
for excluded_candidate in excluded_with_votes.iter() {
|
||||
let count_card = state.candidates.get_mut(*excluded_candidate).unwrap();
|
||||
let mut cc_parcels = Vec::new();
|
||||
cc_parcels.append(&mut count_card.parcels);
|
||||
|
||||
// Filter out just those votes with max_value
|
||||
let mut remaining_parcels = Vec::new();
|
||||
|
||||
for mut parcel in cc_parcels {
|
||||
if parcel.value_fraction == max_value {
|
||||
count_card.ballot_transfers -= parcel.num_ballots();
|
||||
|
||||
let votes_transferred = parcel.num_votes();
|
||||
votes.append(&mut parcel.votes);
|
||||
|
||||
// Update votes
|
||||
checksum -= &votes_transferred;
|
||||
count_card.transfer(&-votes_transferred);
|
||||
} else {
|
||||
remaining_parcels.push(parcel);
|
||||
}
|
||||
}
|
||||
|
||||
if !remaining_parcels.is_empty() {
|
||||
votes_remain = true;
|
||||
}
|
||||
|
||||
// Leave remaining votes with candidate
|
||||
count_card.parcels = remaining_parcels;
|
||||
}
|
||||
|
||||
// Group all votes of one value in single parcel
|
||||
parcels.push(Parcel {
|
||||
votes,
|
||||
value_fraction: max_value,
|
||||
source_order: 0, // Set this later
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
ExclusionMethod::BySource => {
|
||||
// Exclude by source candidate
|
||||
let excluded_with_votes: Vec<&&Candidate> = excluded_candidates.iter()
|
||||
.filter(|c| { let cc = &state.candidates[*c]; !cc.finalised && !cc.parcels.is_empty() })
|
||||
.collect();
|
||||
|
||||
if excluded_with_votes.is_empty() {
|
||||
votes_remain = false;
|
||||
} else {
|
||||
// If candidates to exclude still having votes, select only those from the earliest elected/excluded source candidate
|
||||
let min_order = excluded_with_votes.iter()
|
||||
.map(|c| state.candidates[*c].parcels.iter()
|
||||
.map(|p| p.source_order)
|
||||
.min().unwrap())
|
||||
.min().unwrap();
|
||||
|
||||
votes_remain = false;
|
||||
|
||||
for excluded_candidate in excluded_with_votes.iter() {
|
||||
let count_card = state.candidates.get_mut(*excluded_candidate).unwrap();
|
||||
let mut cc_parcels = Vec::new();
|
||||
cc_parcels.append(&mut count_card.parcels);
|
||||
|
||||
// Filter out just those votes with min_order
|
||||
let mut remaining_parcels = Vec::new();
|
||||
|
||||
for parcel in cc_parcels {
|
||||
if parcel.source_order == min_order {
|
||||
count_card.ballot_transfers -= parcel.num_ballots();
|
||||
|
||||
let votes_transferred = parcel.num_votes();
|
||||
parcels.push(parcel);
|
||||
|
||||
// Update votes
|
||||
checksum -= &votes_transferred;
|
||||
count_card.transfer(&-votes_transferred);
|
||||
} else {
|
||||
remaining_parcels.push(parcel);
|
||||
}
|
||||
}
|
||||
|
||||
if !remaining_parcels.is_empty() {
|
||||
votes_remain = true;
|
||||
}
|
||||
|
||||
// Leave remaining votes with candidate
|
||||
count_card.parcels = remaining_parcels;
|
||||
}
|
||||
}
|
||||
}
|
||||
ExclusionMethod::ParcelsByOrder => {
|
||||
// Exclude by parcel by order
|
||||
if excluded_candidates.len() > 1 && excluded_candidates.iter().any(|c| !state.candidates[c].parcels.is_empty()) {
|
||||
// TODO: We can probably support this actually
|
||||
panic!("--exclusion parcels_by_order is incompatible with multiple exclusions");
|
||||
}
|
||||
|
||||
let count_card = state.candidates.get_mut(excluded_candidates[0]).unwrap();
|
||||
|
||||
if count_card.parcels.is_empty() {
|
||||
votes_remain = false;
|
||||
} else {
|
||||
parcels.push(count_card.parcels.remove(0));
|
||||
votes_remain = !count_card.parcels.is_empty();
|
||||
|
||||
count_card.ballot_transfers -= parcels.first().unwrap().num_ballots();
|
||||
|
||||
// Update votes
|
||||
let votes_transferred = parcels.first().unwrap().num_votes();
|
||||
checksum -= &votes_transferred;
|
||||
count_card.transfer(&-votes_transferred);
|
||||
}
|
||||
}
|
||||
_ => panic!()
|
||||
}
|
||||
|
||||
let mut total_ballots = N::new();
|
||||
let mut total_votes = N::new();
|
||||
|
||||
let value = parcels.first().map(|p| p.value_fraction.clone());
|
||||
|
||||
let mut transfer_table = TransferTable::new_exclusion(
|
||||
state.election.candidates.iter().filter(|c| state.candidates[c].state == CandidateState::Hopeful || state.candidates[c].state == CandidateState::Guarded).collect(),
|
||||
);
|
||||
|
||||
for src_parcel in parcels {
|
||||
// Count next preferences
|
||||
let result = super::next_preferences(state, src_parcel.votes);
|
||||
|
||||
total_ballots += &result.total_ballots;
|
||||
total_votes += &result.total_ballots * &src_parcel.value_fraction;
|
||||
|
||||
// Transfer candidate votes
|
||||
for (candidate, entry) in result.candidates.into_iter() {
|
||||
let parcel = Parcel {
|
||||
votes: entry.votes,
|
||||
value_fraction: src_parcel.value_fraction.clone(),
|
||||
source_order: state.num_elected + state.num_excluded,
|
||||
};
|
||||
|
||||
// Record transfers
|
||||
transfer_table.add_transfers(
|
||||
&parcel.value_fraction,
|
||||
match opts.round_subtransfers {
|
||||
RoundSubtransfersMode::ByValueAndSource => Some(src_parcel.source_order),
|
||||
RoundSubtransfersMode::ByParcel => None, // Force new column per parcel
|
||||
_ => Some(0)
|
||||
},
|
||||
candidate,
|
||||
&entry.num_ballots
|
||||
);
|
||||
|
||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||
count_card.parcels.push(parcel);
|
||||
}
|
||||
|
||||
// Transfer exhausted votes
|
||||
let parcel = Parcel {
|
||||
votes: result.exhausted.votes,
|
||||
value_fraction: src_parcel.value_fraction,
|
||||
source_order: state.num_elected + state.num_excluded,
|
||||
};
|
||||
|
||||
// Record transfers
|
||||
transfer_table.add_exhausted(
|
||||
&parcel.value_fraction,
|
||||
match opts.round_subtransfers {
|
||||
RoundSubtransfersMode::ByValueAndSource => Some(src_parcel.source_order),
|
||||
RoundSubtransfersMode::ByParcel => None, // Force new column per parcel
|
||||
_ => Some(0)
|
||||
},
|
||||
&result.exhausted.num_ballots
|
||||
);
|
||||
|
||||
state.exhausted.parcels.push(parcel);
|
||||
}
|
||||
|
||||
if let ExclusionMethod::SingleStage = opts.exclusion {
|
||||
if state.election.seats == 1 {
|
||||
// Ballots can never have nonzero value with a single winner
|
||||
state.logger.log_literal(format!("Transferring {:.dps$} votes.", total_votes, dps=opts.pp_decimals));
|
||||
} else if total_ballots == N::one() {
|
||||
state.logger.log_literal(format!("Transferring 1 ballot, totalling {:.dps$} votes.", total_votes, dps=opts.pp_decimals));
|
||||
} else {
|
||||
state.logger.log_literal(format!("Transferring {:.0} ballots, totalling {:.dps$} votes.", total_ballots, total_votes, dps=opts.pp_decimals));
|
||||
}
|
||||
} else {
|
||||
if state.election.seats == 1 {
|
||||
state.logger.log_literal(format!("Transferring {:.dps$} votes, received at value {:.dps2$}.", total_votes, value.unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
||||
} else if total_ballots.is_zero() {
|
||||
state.logger.log_literal(format!("Transferring 0 ballots, totalling {:.dps$} votes.", 0, dps=opts.pp_decimals));
|
||||
} else if total_ballots == N::one() {
|
||||
state.logger.log_literal(format!("Transferring 1 ballot, totalling {:.dps$} votes, received at value {:.dps2$}.", total_votes, value.unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
||||
} else {
|
||||
state.logger.log_literal(format!("Transferring {:.0} ballots, totalling {:.dps$} votes, received at value {:.dps2$}.", total_ballots, total_votes, value.unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
||||
}
|
||||
}
|
||||
|
||||
// Credit transferred votes
|
||||
transfer_table.calculate(opts);
|
||||
checksum += transfer_table.apply_to(state, opts);
|
||||
state.transfer_table = Some(transfer_table);
|
||||
|
||||
if !votes_remain {
|
||||
// Finalise candidate votes
|
||||
for excluded_candidate in excluded_candidates.into_iter() {
|
||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||
checksum -= &count_card.votes;
|
||||
count_card.transfers -= &count_card.votes;
|
||||
count_card.votes = N::new();
|
||||
count_card.finalised = true;
|
||||
}
|
||||
|
||||
if opts.exclusion != ExclusionMethod::SingleStage {
|
||||
state.logger.log_literal(format!("{} complete.", complete_type));
|
||||
}
|
||||
}
|
||||
|
||||
// Update loss by fraction
|
||||
state.loss_fraction.transfer(&-checksum);
|
||||
}
|
||||
|
||||
/// Exclude a candidate and reset the count from first preferences
|
||||
pub fn exclude_candidates_and_reset<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>)
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
{
|
||||
// Used to give bulk excluded candidate the same order_elected
|
||||
let order_excluded = state.num_excluded + 1;
|
||||
|
||||
for excluded_candidate in excluded_candidates.iter() {
|
||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||
|
||||
// Rust borrow checker is unhappy if we try to put this in exclude_hopefuls ??!
|
||||
if count_card.state != CandidateState::Excluded {
|
||||
count_card.state = CandidateState::Excluded;
|
||||
state.num_excluded += 1;
|
||||
count_card.order_elected = -(order_excluded as isize);
|
||||
}
|
||||
|
||||
constraints::update_constraints(state, opts);
|
||||
}
|
||||
|
||||
// Reset count
|
||||
for (_, count_card) in state.candidates.iter_mut() {
|
||||
if count_card.order_elected > 0 {
|
||||
count_card.order_elected = 0;
|
||||
}
|
||||
count_card.parcels.clear();
|
||||
count_card.votes = N::new();
|
||||
count_card.transfers = N::new();
|
||||
count_card.state = match count_card.state {
|
||||
CandidateState::Withdrawn => CandidateState::Withdrawn,
|
||||
CandidateState::Excluded => CandidateState::Excluded,
|
||||
_ => CandidateState::Hopeful,
|
||||
};
|
||||
|
||||
if count_card.state == CandidateState::Excluded {
|
||||
count_card.finalised = true;
|
||||
} else {
|
||||
count_card.finalised = false;
|
||||
}
|
||||
}
|
||||
|
||||
state.exhausted.votes = N::new();
|
||||
state.exhausted.transfers = N::new();
|
||||
state.loss_fraction.votes = N::new();
|
||||
state.loss_fraction.transfers = N::new();
|
||||
|
||||
state.num_elected = 0;
|
||||
|
||||
let orig_title = state.title.clone();
|
||||
|
||||
// Redistribute first preferences
|
||||
super::distribute_first_preferences(state, opts);
|
||||
|
||||
state.title = orig_title;
|
||||
|
||||
// Trigger recalculation of quota within stv::count_one_stage
|
||||
state.quota = None;
|
||||
state.vote_required_election = None;
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
/// Table
|
||||
pub struct Table {
|
||||
/// Rows in the table
|
||||
rows: Vec<Row>,
|
||||
}
|
||||
|
||||
impl Table {
|
||||
/// Return a new [Table]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
rows: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a [Row] to the table
|
||||
pub fn add_row(&mut self, row: Row) {
|
||||
self.rows.push(row);
|
||||
}
|
||||
|
||||
/// Alias for [Table::add_row]
|
||||
pub fn set_titles(&mut self, row: Row) {
|
||||
self.add_row(row);
|
||||
}
|
||||
|
||||
/// Render the table as HTML
|
||||
pub fn to_html(&self) -> String {
|
||||
return format!(r#"<table class="transfers">{}</table>"#, self.rows.iter().map(|r| r.to_html()).join(""));
|
||||
}
|
||||
}
|
||||
|
||||
/// Row in a [Table]
|
||||
pub struct Row {
|
||||
/// Cells in the row
|
||||
cells: Vec<Cell>,
|
||||
}
|
||||
|
||||
impl Row {
|
||||
/// Return a new [Row]
|
||||
pub fn new(cells: Vec<Cell>) -> Self {
|
||||
Self {
|
||||
cells
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the row as HTML
|
||||
fn to_html(&self) -> String {
|
||||
return format!(r#"<tr>{}</tr>"#, self.cells.iter().map(|c| c.to_html()).join(""));
|
||||
}
|
||||
}
|
||||
|
||||
/// Cell in a [Row]
|
||||
pub struct Cell {
|
||||
/// Content of the cell
|
||||
content: String,
|
||||
/// HTML tag/attributes
|
||||
attrs: Vec<&'static str>,
|
||||
}
|
||||
|
||||
impl Cell {
|
||||
/// Return a new [Cell]
|
||||
pub fn new(content: &str) -> Self {
|
||||
Self {
|
||||
content: String::from(content),
|
||||
attrs: vec!["td"],
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a style to the cell
|
||||
#[allow(unused_mut)]
|
||||
pub fn style_spec(mut self, spec: &str) -> Self {
|
||||
if spec.contains("H2") {
|
||||
self.attrs.push(r#"colspan="2""#);
|
||||
}
|
||||
if spec.contains('c') {
|
||||
self.attrs.push(r#"style="text-align:center""#);
|
||||
}
|
||||
if spec.contains('r') {
|
||||
self.attrs.push(r#"style="text-align:right""#);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Render the cell as HTML
|
||||
fn to_html(&self) -> String {
|
||||
return format!(r#"<{}>{}</td>"#, self.attrs.join(" "), html_escape::encode_text(&self.content));
|
||||
}
|
||||
}
|
|
@ -1,636 +0,0 @@
|
|||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021–2022 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::candmap::CandidateMap;
|
||||
use crate::election::{Candidate, CountState};
|
||||
use crate::numbers::Number;
|
||||
use crate::stv::{STVOptions, RoundSubtransfersMode};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use prettytable::{Cell, Row, Table};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use super::prettytable_html::{Cell, Row, Table};
|
||||
|
||||
use std::cmp::max;
|
||||
|
||||
/// Table describing vote transfers during a surplus distribution or exclusion
|
||||
#[derive(Clone)]
|
||||
pub struct TransferTable<'e, N: Number> {
|
||||
/// Continuing candidates
|
||||
pub hopefuls: Vec<&'e Candidate>,
|
||||
|
||||
/// Columns in the table
|
||||
pub columns: Vec<TransferTableColumn<'e, N>>,
|
||||
|
||||
/// Total column
|
||||
pub total: TransferTableColumn<'e, N>,
|
||||
|
||||
/// Size of surplus, or `None` if an exclusion
|
||||
pub surplus: Option<N>,
|
||||
/// Surplus fraction, or `None` if votes not reweighted/an exclusion (for display/optimisation only)
|
||||
pub surpfrac: Option<N>,
|
||||
/// Numerator of surplus fraction, or `None` if votes not reweighted/an exclusion
|
||||
pub surpfrac_numer: Option<N>,
|
||||
/// Denominator of surplus fraction, or `None`
|
||||
pub surpfrac_denom: Option<N>,
|
||||
}
|
||||
|
||||
impl<'e, N: Number> TransferTable<'e, N> {
|
||||
/// Return a new [TransferTable] for an exclusion
|
||||
pub fn new_exclusion(hopefuls: Vec<&'e Candidate>) -> Self {
|
||||
let num_hopefuls = hopefuls.len();
|
||||
|
||||
return TransferTable {
|
||||
hopefuls,
|
||||
columns: Vec::new(),
|
||||
total: TransferTableColumn {
|
||||
value_fraction: N::new(),
|
||||
order: 0,
|
||||
cells: CandidateMap::with_capacity(num_hopefuls),
|
||||
exhausted: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
|
||||
total: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
|
||||
},
|
||||
surplus: None,
|
||||
surpfrac: None,
|
||||
surpfrac_numer: None,
|
||||
surpfrac_denom: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a new [TransferTable] for a surplus distribution
|
||||
pub fn new_surplus(hopefuls: Vec<&'e Candidate>, surplus: N, surpfrac: Option<N>, surpfrac_numer: Option<N>, surpfrac_denom: Option<N>) -> Self {
|
||||
let num_hopefuls = hopefuls.len();
|
||||
|
||||
return TransferTable {
|
||||
hopefuls,
|
||||
columns: Vec::new(),
|
||||
total: TransferTableColumn {
|
||||
value_fraction: N::new(),
|
||||
order: 0,
|
||||
cells: CandidateMap::with_capacity(num_hopefuls),
|
||||
exhausted: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
|
||||
total: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
|
||||
},
|
||||
surplus: Some(surplus),
|
||||
surpfrac,
|
||||
surpfrac_numer,
|
||||
surpfrac_denom,
|
||||
}
|
||||
}
|
||||
|
||||
/// Record the specified transfer
|
||||
///
|
||||
/// order: Pass `None` to force a new column
|
||||
pub fn add_transfers(&mut self, value_fraction: &N, order: Option<usize>, candidate: &'e Candidate, ballots: &N) {
|
||||
for col in self.columns.iter_mut() {
|
||||
if &col.value_fraction == value_fraction && order.map(|o| col.order == o).unwrap_or(false) {
|
||||
col.add_transfers(candidate, ballots);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let mut col = TransferTableColumn {
|
||||
value_fraction: value_fraction.clone(),
|
||||
order: order.unwrap_or(0),
|
||||
cells: CandidateMap::new(),
|
||||
exhausted: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
|
||||
total: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
|
||||
};
|
||||
col.add_transfers(candidate, ballots);
|
||||
self.columns.push(col);
|
||||
}
|
||||
|
||||
/// Record the specified exhaustion
|
||||
///
|
||||
/// order: Pass `None` to force a new column
|
||||
pub fn add_exhausted(&mut self, value_fraction: &N, order: Option<usize>, ballots: &N) {
|
||||
for col in self.columns.iter_mut() {
|
||||
if &col.value_fraction == value_fraction && order.map(|o| col.order == o).unwrap_or(false) {
|
||||
col.exhausted.ballots += ballots;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let col = TransferTableColumn {
|
||||
value_fraction: value_fraction.clone(),
|
||||
order: order.unwrap_or(0),
|
||||
cells: CandidateMap::new(),
|
||||
exhausted: TransferTableCell { ballots: ballots.clone(), votes_in: N::new(), votes_out: N::new() },
|
||||
total: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
|
||||
};
|
||||
self.columns.push(col);
|
||||
}
|
||||
|
||||
/// Calculate the votes to be transferred according to this table
|
||||
pub fn calculate(&mut self, opts: &STVOptions) {
|
||||
// Use weighted rules if exclusion or WIGM
|
||||
let is_weighted = self.surplus.is_none() || opts.surplus.is_weighted();
|
||||
|
||||
// Iterate through columns
|
||||
// Sum votes_in, etc.
|
||||
for column in self.columns.iter_mut() {
|
||||
// Candidate votes
|
||||
for (candidate, cell) in column.cells.iter_mut() {
|
||||
column.total.ballots += &cell.ballots;
|
||||
self.total.add_transfers(candidate, &cell.ballots);
|
||||
self.total.total.ballots += &cell.ballots;
|
||||
|
||||
let votes_in = cell.ballots.clone() * &column.value_fraction;
|
||||
cell.votes_in += &votes_in;
|
||||
column.total.votes_in += &votes_in;
|
||||
self.total.cells.get_mut(candidate).unwrap().votes_in += &votes_in;
|
||||
self.total.total.votes_in += votes_in;
|
||||
}
|
||||
|
||||
// Exhausted votes
|
||||
column.total.ballots += &column.exhausted.ballots;
|
||||
self.total.exhausted.ballots += &column.exhausted.ballots;
|
||||
self.total.total.ballots += &column.exhausted.ballots;
|
||||
|
||||
let votes_in = column.exhausted.ballots.clone() * &column.value_fraction;
|
||||
column.exhausted.votes_in += &votes_in;
|
||||
column.total.votes_in += &votes_in;
|
||||
self.total.exhausted.votes_in += &votes_in;
|
||||
self.total.total.votes_in += votes_in;
|
||||
}
|
||||
|
||||
match opts.round_subtransfers {
|
||||
RoundSubtransfersMode::SingleStep => {
|
||||
// No need to calculate votes_out for each column
|
||||
|
||||
// Calculate total votes_out per candidate
|
||||
for (_candidate, cell) in self.total.cells.iter_mut() {
|
||||
if is_weighted {
|
||||
// Weighted rules
|
||||
// Multiply votes in by surplus fraction
|
||||
cell.votes_out = multiply_surpfrac(cell.votes_in.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||
} else if self.surpfrac.is_none() {
|
||||
// Unweighted rules but transfer at values received
|
||||
cell.votes_out = cell.votes_in.clone();
|
||||
} else {
|
||||
// Unweighted rules
|
||||
// Multiply ballots in by surplus fraction
|
||||
cell.votes_out = multiply_surpfrac(cell.ballots.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||
}
|
||||
|
||||
// Round if required
|
||||
if let Some(dps) = opts.round_votes {
|
||||
cell.votes_out.floor_mut(dps);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate total exhausted votes
|
||||
if is_weighted {
|
||||
// Weighted rules
|
||||
// Multiply votes in by surplus fraction
|
||||
self.total.exhausted.votes_out = multiply_surpfrac(self.total.exhausted.votes_in.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||
} else if self.surpfrac.is_none() {
|
||||
// Unweighted rules but transfer at values received
|
||||
// This can only happen with --transferable-only, so this will be calculated in apply_to
|
||||
} else {
|
||||
// Unweighted rules
|
||||
// Multiply ballots in by surplus fraction
|
||||
self.total.exhausted.votes_out = multiply_surpfrac(self.total.exhausted.ballots.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||
}
|
||||
|
||||
// Round if required
|
||||
if let Some(dps) = opts.round_votes {
|
||||
self.total.exhausted.votes_out.floor_mut(dps);
|
||||
}
|
||||
}
|
||||
RoundSubtransfersMode::ByValue | RoundSubtransfersMode::ByValueAndSource | RoundSubtransfersMode::ByParcel => {
|
||||
// Calculate votes_out for each column
|
||||
for column in self.columns.iter_mut() {
|
||||
// Calculate votes_out per candidate in the column
|
||||
for (_candidate, cell) in column.cells.iter_mut() {
|
||||
if is_weighted {
|
||||
// Weighted rules
|
||||
// Multiply votes in by surplus fraction
|
||||
cell.votes_out = multiply_surpfrac(cell.votes_in.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||
} else if self.surpfrac.is_none() {
|
||||
// Unweighted rules but transfer at values received
|
||||
cell.votes_out = cell.votes_in.clone();
|
||||
} else {
|
||||
// Unweighted rules
|
||||
// Multiply ballots in by surplus fraction
|
||||
cell.votes_out = multiply_surpfrac(cell.ballots.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||
}
|
||||
|
||||
// Round if required
|
||||
if let Some(dps) = opts.round_votes {
|
||||
cell.votes_out.floor_mut(dps);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate exhausted votes in the column
|
||||
if is_weighted {
|
||||
// Weighted rules
|
||||
// Multiply votes in by surplus fraction
|
||||
column.exhausted.votes_out = multiply_surpfrac(column.exhausted.votes_in.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||
} else if self.surpfrac.is_none() {
|
||||
// Unweighted rules but transfer at values received
|
||||
// This can only happen with --transferable-only, so this will be calculated in apply_to
|
||||
} else {
|
||||
// Unweighted rules
|
||||
// Multiply ballots in by surplus fraction
|
||||
column.exhausted.votes_out = multiply_surpfrac(column.exhausted.ballots.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||
}
|
||||
|
||||
// Round if required
|
||||
if let Some(dps) = opts.round_votes {
|
||||
column.exhausted.votes_out.floor_mut(dps);
|
||||
}
|
||||
}
|
||||
|
||||
// Sum total votes_out per candidate
|
||||
for (candidate, cell) in self.total.cells.iter_mut() {
|
||||
cell.votes_out = self.columns.iter().fold(N::new(), |mut acc, col| {
|
||||
if let Some(cell) = col.cells.get(candidate) {
|
||||
acc += &cell.votes_out
|
||||
}
|
||||
acc
|
||||
});
|
||||
}
|
||||
|
||||
// Sum total exhausted votes
|
||||
self.total.exhausted.votes_out = self.columns.iter().fold(N::new(), |mut acc, col| { acc += &col.exhausted.votes_out; acc });
|
||||
}
|
||||
RoundSubtransfersMode::PerBallot => {
|
||||
// Calculate votes_out for each column
|
||||
for column in self.columns.iter_mut() {
|
||||
// Calculate votes_out per candidate in the column
|
||||
for (_candidate, cell) in column.cells.iter_mut() {
|
||||
if is_weighted {
|
||||
// Weighted rules
|
||||
// Multiply ballots in by new value fraction
|
||||
let mut new_value_fraction = multiply_surpfrac(column.value_fraction.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||
if let Some(dps) = opts.round_values {
|
||||
new_value_fraction.floor_mut(dps);
|
||||
}
|
||||
|
||||
cell.votes_out = cell.ballots.clone() * new_value_fraction;
|
||||
} else if self.surpfrac.is_none() {
|
||||
// Unweighted rules but transfer at values received
|
||||
cell.votes_out = cell.votes_in.clone();
|
||||
} else {
|
||||
// Unweighted rules
|
||||
// Multiply ballots in by surplus fraction
|
||||
cell.votes_out = multiply_surpfrac(cell.ballots.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||
}
|
||||
|
||||
// Round if required
|
||||
if let Some(dps) = opts.round_votes {
|
||||
cell.votes_out.floor_mut(dps);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate exhausted votes in the column
|
||||
if is_weighted {
|
||||
// Weighted rules
|
||||
// Multiply ballots in by new value fraction
|
||||
let mut new_value_fraction = multiply_surpfrac(column.value_fraction.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||
if let Some(dps) = opts.round_values {
|
||||
new_value_fraction.floor_mut(dps);
|
||||
}
|
||||
|
||||
column.exhausted.votes_out = column.exhausted.ballots.clone() * new_value_fraction;
|
||||
} else if self.surpfrac.is_none() {
|
||||
// Unweighted rules but transfer at values received
|
||||
// This can only happen with --transferable-only, so this will be calculated in apply_to
|
||||
} else {
|
||||
// Unweighted rules
|
||||
// Multiply ballots in by surplus fraction
|
||||
column.exhausted.votes_out = multiply_surpfrac(column.exhausted.ballots.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||
}
|
||||
|
||||
// Round if required
|
||||
if let Some(dps) = opts.round_votes {
|
||||
column.exhausted.votes_out.floor_mut(dps);
|
||||
}
|
||||
}
|
||||
|
||||
// Sum total votes_out per candidate
|
||||
for (candidate, cell) in self.total.cells.iter_mut() {
|
||||
cell.votes_out = self.columns.iter().fold(N::new(), |mut acc, col| {
|
||||
if let Some(cell) = col.cells.get(candidate) {
|
||||
acc += &cell.votes_out;
|
||||
}
|
||||
acc
|
||||
});
|
||||
}
|
||||
|
||||
// Sum total exhausted votes
|
||||
self.total.exhausted.votes_out = self.columns.iter().fold(N::new(), |mut acc, col| { acc += &col.exhausted.votes_out; acc });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the transfers described in the table to the count sheet
|
||||
///
|
||||
/// Credit continuing candidates and exhausted pile with the appropriate number of ballot papers and votes.
|
||||
pub fn apply_to(&self, state: &mut CountState<N>, opts: &STVOptions) -> N {
|
||||
let mut checksum = N::new();
|
||||
|
||||
// Credit transferred votes
|
||||
for (candidate, count_card) in state.candidates.iter_mut() {
|
||||
if let Some(cell) = self.total.cells.get(candidate) {
|
||||
count_card.transfer(&cell.votes_out);
|
||||
count_card.ballot_transfers += &cell.ballots;
|
||||
checksum += &cell.votes_out;
|
||||
}
|
||||
}
|
||||
|
||||
// Credit exhausted votes
|
||||
// If exclusion or not --transferable-only
|
||||
if self.surplus.is_none() || !opts.transferable_only {
|
||||
// Standard rules
|
||||
state.exhausted.transfer(&self.total.exhausted.votes_out);
|
||||
state.exhausted.ballot_transfers += &self.total.exhausted.ballots;
|
||||
checksum += &self.total.exhausted.votes_out;
|
||||
} else {
|
||||
// Credit only nontransferable difference
|
||||
if self.surpfrac_numer.is_none() {
|
||||
// TODO: Is there a purer way of calculating this?
|
||||
let difference = self.surplus.as_ref().unwrap().clone() - &checksum;
|
||||
state.exhausted.transfer(&difference);
|
||||
checksum += difference;
|
||||
|
||||
for column in self.columns.iter() {
|
||||
state.exhausted.ballot_transfers += &column.exhausted.ballots;
|
||||
}
|
||||
} else {
|
||||
// No ballots exhaust
|
||||
}
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
/// Render table as [Table]
|
||||
fn render(&self, opts: &STVOptions) -> Table {
|
||||
let mut table = Table::new();
|
||||
set_table_format(&mut table);
|
||||
|
||||
let show_transfers_per_column = opts.round_subtransfers != RoundSubtransfersMode::SingleStep;
|
||||
|
||||
let num_cols;
|
||||
if show_transfers_per_column {
|
||||
num_cols = self.columns.len() * 3 + 4;
|
||||
} else {
|
||||
if self.surpfrac.is_none() {
|
||||
num_cols = self.columns.len() * 2 + 3;
|
||||
} else {
|
||||
num_cols = self.columns.len() * 2 + 4;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------
|
||||
// Header row
|
||||
|
||||
let mut row = Vec::with_capacity(num_cols);
|
||||
row.push(Cell::new("Preference"));
|
||||
for column in self.columns.iter() {
|
||||
row.push(Cell::new(&format!("Ballots @ {:.dps2$}", column.value_fraction, dps2=max(opts.pp_decimals, 2))).style_spec("cH2"));
|
||||
|
||||
if show_transfers_per_column {
|
||||
if self.surplus.is_some() {
|
||||
row.push(Cell::new(&format!("× {:.dps2$}", self.surpfrac.as_ref().unwrap(), dps2=max(opts.pp_decimals, 2))).style_spec("r"));
|
||||
} else {
|
||||
row.push(Cell::new("=").style_spec("c"));
|
||||
}
|
||||
}
|
||||
}
|
||||
row.push(Cell::new("Total").style_spec("cH2"));
|
||||
if self.surpfrac.is_some() {
|
||||
row.push(Cell::new(&format!("× {:.dps2$}", self.surpfrac.as_ref().unwrap(), dps2=max(opts.pp_decimals, 2))).style_spec("r"));
|
||||
} else if show_transfers_per_column {
|
||||
row.push(Cell::new("=").style_spec("c"));
|
||||
}
|
||||
table.set_titles(Row::new(row));
|
||||
|
||||
// --------------
|
||||
// Candidate rows
|
||||
|
||||
for candidate in self.hopefuls.iter() {
|
||||
let mut row = Vec::with_capacity(num_cols);
|
||||
row.push(Cell::new(&candidate.name));
|
||||
for column in self.columns.iter() {
|
||||
if let Some(cell) = column.cells.get(candidate) {
|
||||
row.push(Cell::new(&format!("{:.0}", cell.ballots)).style_spec("r"));
|
||||
row.push(Cell::new(&format!("{:.dps$}", cell.votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
||||
if show_transfers_per_column {
|
||||
row.push(Cell::new(&format!("{:.dps$}", cell.votes_out, dps=opts.pp_decimals)).style_spec("r"));
|
||||
}
|
||||
} else {
|
||||
row.push(Cell::new(""));
|
||||
row.push(Cell::new(""));
|
||||
if show_transfers_per_column {
|
||||
row.push(Cell::new(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Totals
|
||||
if let Some(cell) = self.total.cells.get(candidate) {
|
||||
row.push(Cell::new(&format!("{:.0}", cell.ballots)).style_spec("r"));
|
||||
row.push(Cell::new(&format!("{:.dps$}", cell.votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
||||
if self.surpfrac.is_some() || show_transfers_per_column {
|
||||
row.push(Cell::new(&format!("{:.dps$}", cell.votes_out, dps=opts.pp_decimals)).style_spec("r"));
|
||||
}
|
||||
} else {
|
||||
row.push(Cell::new(""));
|
||||
row.push(Cell::new(""));
|
||||
if self.surpfrac.is_some() || show_transfers_per_column {
|
||||
row.push(Cell::new(""));
|
||||
}
|
||||
}
|
||||
|
||||
table.add_row(Row::new(row));
|
||||
}
|
||||
|
||||
// -------------
|
||||
// Exhausted row
|
||||
|
||||
let mut row = Vec::with_capacity(num_cols);
|
||||
row.push(Cell::new("Exhausted"));
|
||||
for column in self.columns.iter() {
|
||||
if !column.exhausted.ballots.is_zero() {
|
||||
row.push(Cell::new(&format!("{:.0}", column.exhausted.ballots)).style_spec("r"));
|
||||
row.push(Cell::new(&format!("{:.dps$}", column.exhausted.votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
||||
if show_transfers_per_column {
|
||||
if column.exhausted.votes_out.is_zero() {
|
||||
row.push(Cell::new("-").style_spec("c"));
|
||||
} else {
|
||||
row.push(Cell::new(&format!("{:.dps$}", column.exhausted.votes_out, dps=opts.pp_decimals)).style_spec("r"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
row.push(Cell::new(""));
|
||||
row.push(Cell::new(""));
|
||||
if show_transfers_per_column {
|
||||
row.push(Cell::new(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Totals
|
||||
if !self.total.exhausted.ballots.is_zero() {
|
||||
row.push(Cell::new(&format!("{:.0}", self.total.exhausted.ballots)).style_spec("r"));
|
||||
row.push(Cell::new(&format!("{:.dps$}", self.total.exhausted.votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
||||
if self.surpfrac.is_some() || show_transfers_per_column {
|
||||
if self.total.exhausted.votes_out.is_zero() {
|
||||
row.push(Cell::new("-").style_spec("c"));
|
||||
} else {
|
||||
row.push(Cell::new(&format!("{:.dps$}", self.total.exhausted.votes_out, dps=opts.pp_decimals)).style_spec("r"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
row.push(Cell::new(""));
|
||||
row.push(Cell::new(""));
|
||||
if self.surpfrac.is_some() || show_transfers_per_column {
|
||||
row.push(Cell::new(""));
|
||||
}
|
||||
}
|
||||
|
||||
table.add_row(Row::new(row));
|
||||
|
||||
// ----------
|
||||
// Totals row
|
||||
|
||||
let mut row = Vec::with_capacity(num_cols);
|
||||
row.push(Cell::new("Total"));
|
||||
|
||||
for column in self.columns.iter() {
|
||||
row.push(Cell::new(&format!("{:.0}", column.total.ballots)).style_spec("r"));
|
||||
row.push(Cell::new(&format!("{:.dps$}", column.total.votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
||||
if show_transfers_per_column {
|
||||
row.push(Cell::new(&format!("{:.dps$}", column.total.votes_out, dps=opts.pp_decimals)).style_spec("r"));
|
||||
}
|
||||
}
|
||||
|
||||
// Grand total cell
|
||||
|
||||
let mut gt_ballots = N::new();
|
||||
let mut gt_votes_in = N::new();
|
||||
let mut gt_votes_out = N::new();
|
||||
|
||||
for candidate in self.hopefuls.iter() {
|
||||
if let Some(cell) = self.total.cells.get(candidate) {
|
||||
gt_ballots += &cell.ballots;
|
||||
gt_votes_in += &cell.votes_in;
|
||||
gt_votes_out += &cell.votes_out;
|
||||
}
|
||||
}
|
||||
gt_ballots += &self.total.exhausted.ballots;
|
||||
gt_votes_in += &self.total.exhausted.votes_in;
|
||||
gt_votes_out += &self.total.exhausted.votes_out;
|
||||
|
||||
row.push(Cell::new(&format!("{:.0}", gt_ballots)).style_spec("r"));
|
||||
row.push(Cell::new(&format!("{:.dps$}", gt_votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
||||
if self.surpfrac.is_some() || show_transfers_per_column {
|
||||
row.push(Cell::new(&format!("{:.dps$}", gt_votes_out, dps=opts.pp_decimals)).style_spec("r"));
|
||||
}
|
||||
|
||||
table.add_row(Row::new(row));
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
/// Render table as plain text
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn render_text(&self, opts: &STVOptions) -> String {
|
||||
return self.render(opts).to_string();
|
||||
}
|
||||
|
||||
/// Render table as HTML
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn render_text(&self, opts: &STVOptions) -> String {
|
||||
return self.render(opts).to_html();
|
||||
}
|
||||
|
||||
// Render table as HTML
|
||||
//pub fn render_html(&self, opts: &STVOptions) -> String {
|
||||
// return self.render(opts).to_string();
|
||||
//}
|
||||
}
|
||||
|
||||
/// Multiply the specified number by the surplus fraction (if applicable)
|
||||
fn multiply_surpfrac<N: Number>(mut number: N, surpfrac_numer: &Option<N>, surpfrac_denom: &Option<N>) -> N {
|
||||
if let Some(n) = surpfrac_numer {
|
||||
number *= n;
|
||||
}
|
||||
if let Some(n) = surpfrac_denom {
|
||||
number /= n;
|
||||
}
|
||||
return number;
|
||||
}
|
||||
|
||||
/// Column in a [TransferTable]
|
||||
#[derive(Clone)]
|
||||
pub struct TransferTableColumn<'e, N: Number> {
|
||||
/// Value fraction of ballots counted in this column
|
||||
pub value_fraction: N,
|
||||
|
||||
/// Number to separate parcels in modes where subtransfers by parcel, etc.
|
||||
pub order: usize,
|
||||
|
||||
/// Cells in this column
|
||||
pub cells: CandidateMap<'e, TransferTableCell<N>>,
|
||||
|
||||
/// Exhausted cell
|
||||
pub exhausted: TransferTableCell<N>,
|
||||
|
||||
/// Totals cell
|
||||
pub total: TransferTableCell<N>,
|
||||
}
|
||||
|
||||
impl<'e, N: Number> TransferTableColumn<'e, N> {
|
||||
/// Record the specified transfer
|
||||
pub fn add_transfers(&mut self, candidate: &'e Candidate, ballots: &N) {
|
||||
if let Some(cell) = self.cells.get_mut(candidate) {
|
||||
cell.ballots += ballots;
|
||||
} else {
|
||||
let cell = TransferTableCell {
|
||||
ballots: ballots.clone(),
|
||||
votes_in: N::new(),
|
||||
votes_out: N::new(),
|
||||
};
|
||||
self.cells.insert(candidate, cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cell in a [TransferTable], representing transfers to one candidate at a particular value
|
||||
#[derive(Clone)]
|
||||
pub struct TransferTableCell<N: Number> {
|
||||
/// Ballots expressing a next preference for the continuing candidate
|
||||
pub ballots: N,
|
||||
/// Value of votes when received by the transferring candidate
|
||||
pub votes_in: N,
|
||||
/// Votes transferred to the continuing candidate
|
||||
pub votes_out: N,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn set_table_format(table: &mut Table) {
|
||||
table.set_format(*prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn set_table_format(_table: &mut Table) {
|
||||
// No op
|
||||
}
|
457
src/stv/html.rs
457
src/stv/html.rs
|
@ -1,457 +0,0 @@
|
|||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021–2023 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::election::{CandidateState, Election, StageKind};
|
||||
use crate::numbers::Number;
|
||||
use crate::stv::{self, CountState};
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
/// Generate the lead-in description of the count in HTML
|
||||
pub fn describe_count<N: Number>(filename: &str, election: &Election<N>, opts: &stv::STVOptions) -> String {
|
||||
let mut result = String::from("<p>Count computed by OpenTally (revision ");
|
||||
result.push_str(crate::VERSION);
|
||||
let total_ballots = election.ballots.iter().fold(N::new(), |mut acc, b| { acc += &b.orig_value; acc });
|
||||
result.push_str(&format!(r#"). Read {:.0} ballots from ‘{}’ for election ‘{}’. There are {} candidates for {} vacancies. "#, total_ballots, filename, election.name, election.candidates.iter().filter(|c| !c.is_dummy).count(), election.seats));
|
||||
|
||||
let opts_str = opts.describe::<N>();
|
||||
if !opts_str.is_empty() {
|
||||
result.push_str(&format!(r#"Counting using options <span style="font-family: monospace;">{}</span>.</p>"#, opts_str))
|
||||
} else {
|
||||
result.push_str(r#"Counting using default options.</p>"#);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Generate the first column of the HTML results table
|
||||
pub fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions, report_style: &str) -> Vec<String> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
result.push(String::from(r#"<tr class="stage-no"><td rowspan="3"></td></tr>"#));
|
||||
result.push(String::from(r#"<tr class="stage-kind"></tr>"#));
|
||||
result.push(String::from(r#"<tr class="stage-comment"></tr>"#));
|
||||
|
||||
if report_style == "ballots_votes" {
|
||||
result.push(String::from(r#"<tr class="hint-papers-votes"><td></td></tr>"#));
|
||||
}
|
||||
|
||||
for candidate in election.candidates.iter() {
|
||||
if candidate.is_dummy {
|
||||
continue;
|
||||
}
|
||||
|
||||
if report_style == "votes_transposed" {
|
||||
result.push(format!(r#"<tr class="candidate transfers"><td class="candidate-name">{}</td></tr>"#, candidate.name));
|
||||
} else {
|
||||
result.push(format!(r#"<tr class="candidate transfers"><td rowspan="2" class="candidate-name">{}</td></tr>"#, candidate.name));
|
||||
result.push(String::from(r#"<tr class="candidate votes"></tr>"#));
|
||||
}
|
||||
}
|
||||
|
||||
if report_style == "votes_transposed" {
|
||||
result.push(String::from(r#"<tr class="info transfers"><td>Exhausted</td></tr>"#));
|
||||
} else {
|
||||
result.push(String::from(r#"<tr class="info transfers"><td rowspan="2">Exhausted</td></tr>"#));
|
||||
result.push(String::from(r#"<tr class="info votes"></tr>"#));
|
||||
}
|
||||
|
||||
if report_style == "votes_transposed" {
|
||||
result.push(String::from(r#"<tr class="info transfers"><td>Loss by fraction</td></tr>"#));
|
||||
result.push(String::from(r#"<tr class="info transfers"><td>Total</td></tr>"#));
|
||||
if election.seats == 1 {
|
||||
result.push(String::from(r#"<tr class="info transfers"><td>Majority</td></tr>"#));
|
||||
} else {
|
||||
result.push(String::from(r#"<tr class="info transfers"><td>Quota</td></tr>"#));
|
||||
}
|
||||
} else {
|
||||
result.push(String::from(r#"<tr class="info transfers"><td rowspan="2">Loss by fraction</td></tr>"#));
|
||||
result.push(String::from(r#"<tr class="info votes"></tr>"#));
|
||||
result.push(String::from(r#"<tr class="info transfers"><td>Total</td></tr>"#));
|
||||
if election.seats == 1 {
|
||||
result.push(String::from(r#"<tr class="info transfers"><td>Majority</td></tr>"#));
|
||||
} else {
|
||||
result.push(String::from(r#"<tr class="info transfers"><td>Quota</td></tr>"#));
|
||||
}
|
||||
}
|
||||
|
||||
if stv::should_show_vre(opts) {
|
||||
result.push(String::from(r#"<tr class="info transfers"><td>Vote required for election</td></tr>"#));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Generate subsequent columns of the HTML results table
|
||||
pub fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts: &stv::STVOptions, report_style: &str) -> Vec<String> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
// Insert borders to left of new exclusions if reset-and-reiterate method applied
|
||||
let classes_o; // Outer version
|
||||
let classes_i; // Inner version
|
||||
if (opts.exclusion == stv::ExclusionMethod::ResetAndReiterate && matches!(state.title, StageKind::ExclusionOf(_))) || matches!(state.title, StageKind::Rollback) {
|
||||
classes_o = r#" class="blw""#;
|
||||
classes_i = r#"blw "#;
|
||||
} else {
|
||||
classes_o = "";
|
||||
classes_i = "";
|
||||
}
|
||||
|
||||
// Hide transfers column for first preferences if transposed
|
||||
let hide_xfers_trsp;
|
||||
if let StageKind::FirstPreferences = state.title {
|
||||
hide_xfers_trsp = true;
|
||||
} else if opts.exclusion == stv::ExclusionMethod::ResetAndReiterate && matches!(state.title, StageKind::ExclusionOf(_)) {
|
||||
hide_xfers_trsp = true;
|
||||
} else if let StageKind::Rollback = state.title {
|
||||
hide_xfers_trsp = true;
|
||||
} else if let StageKind::BulkElection = state.title {
|
||||
hide_xfers_trsp = true;
|
||||
} else if state.candidates.values().all(|cc| cc.transfers.is_zero()) && state.exhausted.transfers.is_zero() && state.loss_fraction.transfers.is_zero() {
|
||||
hide_xfers_trsp = true;
|
||||
} else {
|
||||
hide_xfers_trsp = false;
|
||||
}
|
||||
|
||||
// Header rows
|
||||
let kind_str = state.title.kind_as_string();
|
||||
let title_str;
|
||||
match &state.title {
|
||||
StageKind::FirstPreferences | StageKind::Rollback | StageKind::RollbackExhausted | StageKind::SurplusesDistributed | StageKind::BulkElection => {
|
||||
title_str = format!("{}", state.title);
|
||||
}
|
||||
StageKind::SurplusOf(candidate) => {
|
||||
title_str = candidate.name.clone();
|
||||
}
|
||||
StageKind::ExclusionOf(candidates) => {
|
||||
if candidates.len() > 5 {
|
||||
let first_4_cands = candidates.iter().map(|c| &c.name).sorted().take(4).join(",<br>");
|
||||
title_str = format!("{},<br>and {} others", first_4_cands, candidates.len() - 4);
|
||||
} else {
|
||||
title_str = candidates.iter().map(|c| &c.name).join(",<br>");
|
||||
}
|
||||
}
|
||||
StageKind::BallotsOf(candidate) => {
|
||||
title_str = candidate.name.clone();
|
||||
}
|
||||
};
|
||||
|
||||
match report_style {
|
||||
"votes" => {
|
||||
result.push(format!(r##"<td{0}><a href="#stage{1}">{1}</a></td>"##, classes_o, stage_num));
|
||||
result.push(format!(r#"<td{}>{}</td>"#, classes_o, kind_str));
|
||||
result.push(format!(r#"<td{}>{}</td>"#, classes_o, title_str));
|
||||
}
|
||||
"votes_transposed" => {
|
||||
if hide_xfers_trsp {
|
||||
result.push(format!(r##"<td{0}><a href="#stage{1}">{1}</a></td>"##, classes_o, stage_num));
|
||||
result.push(format!(r#"<td{}>{}</td>"#, classes_o, kind_str));
|
||||
result.push(format!(r#"<td{}>{}</td>"#, classes_o, title_str));
|
||||
} else {
|
||||
result.push(format!(r##"<td{0} colspan="2"><a href="#stage{1}">{1}</a></td>"##, classes_o, stage_num));
|
||||
result.push(format!(r#"<td{} colspan="2">{}</td>"#, classes_o, kind_str));
|
||||
result.push(format!(r#"<td{} colspan="2">{}</td>"#, classes_o, title_str));
|
||||
//result.push(format!(r#"<td{}>X'fers</td><td>Total</td>"#, tdclasses1));
|
||||
}
|
||||
}
|
||||
"ballots_votes" => {
|
||||
result.push(format!(r##"<td{0} colspan="2"><a href="#stage{1}">{1}</a></td>"##, classes_o, stage_num));
|
||||
result.push(format!(r#"<td{} colspan="2">{}</td>"#, classes_o, kind_str));
|
||||
result.push(format!(r#"<td{} colspan="2">{}</td>"#, classes_o, title_str));
|
||||
result.push(format!(r#"<td{}>Ballots</td><td>Votes</td>"#, classes_o));
|
||||
}
|
||||
_ => unreachable!("Invalid report_style")
|
||||
}
|
||||
|
||||
for candidate in state.election.candidates.iter() {
|
||||
if candidate.is_dummy {
|
||||
continue;
|
||||
}
|
||||
|
||||
let count_card = &state.candidates[candidate];
|
||||
|
||||
// TODO: REFACTOR THIS!!
|
||||
|
||||
match report_style {
|
||||
"votes" => {
|
||||
match count_card.state {
|
||||
CandidateState::Hopeful | CandidateState::Guarded => {
|
||||
result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
|
||||
}
|
||||
CandidateState::Elected => {
|
||||
result.push(format!(r#"<td class="{}count elected">{}</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count elected">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
|
||||
}
|
||||
CandidateState::Doomed => {
|
||||
result.push(format!(r#"<td class="{}count excluded">{}</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count excluded">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
|
||||
}
|
||||
CandidateState::Withdrawn => {
|
||||
result.push(format!(r#"<td class="{}count excluded"></td>"#, classes_i));
|
||||
result.push(format!(r#"<td class="{}count excluded">WD</td>"#, classes_i));
|
||||
}
|
||||
CandidateState::Excluded => {
|
||||
result.push(format!(r#"<td class="{}count excluded">{}</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)));
|
||||
if count_card.votes.is_zero() && count_card.parcels.iter().all(|p| p.votes.is_empty()) {
|
||||
result.push(format!(r#"<td class="{}count excluded">Ex</td>"#, classes_i));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count excluded">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"votes_transposed" => {
|
||||
match count_card.state {
|
||||
CandidateState::Hopeful | CandidateState::Guarded => {
|
||||
if hide_xfers_trsp {
|
||||
result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals), pp(&count_card.votes, opts.pp_decimals)));
|
||||
}
|
||||
}
|
||||
CandidateState::Elected => {
|
||||
if hide_xfers_trsp {
|
||||
result.push(format!(r#"<td class="{}count elected">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count elected">{}</td><td class="count elected">{}</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals), pp(&count_card.votes, opts.pp_decimals)));
|
||||
}
|
||||
}
|
||||
CandidateState::Doomed => {
|
||||
if hide_xfers_trsp {
|
||||
result.push(format!(r#"<td class="{}count excluded">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals), pp(&count_card.votes, opts.pp_decimals)));
|
||||
}
|
||||
}
|
||||
CandidateState::Withdrawn => {
|
||||
if hide_xfers_trsp {
|
||||
result.push(format!(r#"<td class="{}count excluded">WD</td>"#, classes_i));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count excluded"></td><td class="count excluded">WD</td>"#, classes_i));
|
||||
}
|
||||
}
|
||||
CandidateState::Excluded => {
|
||||
if count_card.votes.is_zero() && count_card.parcels.iter().all(|p| p.votes.is_empty()) {
|
||||
if hide_xfers_trsp {
|
||||
result.push(format!(r#"<td class="{}count excluded">Ex</td>"#, classes_i));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">Ex</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)));
|
||||
}
|
||||
} else {
|
||||
if hide_xfers_trsp {
|
||||
result.push(format!(r#"<td class="{}count excluded">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals), pp(&count_card.votes, opts.pp_decimals)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"ballots_votes" => {
|
||||
match count_card.state {
|
||||
CandidateState::Hopeful | CandidateState::Guarded => {
|
||||
result.push(format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pps(&count_card.ballot_transfers, 0), pps(&count_card.transfers, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)));
|
||||
}
|
||||
CandidateState::Elected => {
|
||||
result.push(format!(r#"<td class="{}count elected">{}</td><td class="count elected">{}</td>"#, classes_i, pps(&count_card.ballot_transfers, 0), pps(&count_card.transfers, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count elected">{}</td><td class="count elected">{}</td>"#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)));
|
||||
}
|
||||
CandidateState::Doomed => {
|
||||
result.push(format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, classes_i, pps(&count_card.ballot_transfers, 0), pps(&count_card.transfers, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)));
|
||||
}
|
||||
CandidateState::Withdrawn => {
|
||||
result.push(format!(r#"<td class="{}count excluded"></td><td class="count excluded"></td>"#, classes_i));
|
||||
result.push(format!(r#"<td class="{}count excluded"></td><td class="count excluded">WD</td>"#, classes_i));
|
||||
}
|
||||
CandidateState::Excluded => {
|
||||
result.push(format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, classes_i, pps(&count_card.ballot_transfers, 0), pps(&count_card.transfers, opts.pp_decimals)));
|
||||
if count_card.votes.is_zero() && count_card.parcels.iter().all(|p| p.votes.is_empty()) {
|
||||
result.push(format!(r#"<td class="{}count excluded"></td><td class="count excluded">Ex</td>"#, classes_i));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => unreachable!("Invalid report_style")
|
||||
}
|
||||
}
|
||||
|
||||
match report_style {
|
||||
"votes" => {
|
||||
result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pps(&state.exhausted.transfers, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&state.exhausted.votes, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pps(&state.loss_fraction.transfers, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&state.loss_fraction.votes, opts.pp_decimals)));
|
||||
}
|
||||
"votes_transposed" => {
|
||||
if hide_xfers_trsp {
|
||||
result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&state.exhausted.votes, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&state.loss_fraction.votes, opts.pp_decimals)));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pps(&state.exhausted.transfers, opts.pp_decimals), pp(&state.exhausted.votes, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pps(&state.loss_fraction.transfers, opts.pp_decimals), pp(&state.loss_fraction.votes, opts.pp_decimals)));
|
||||
}
|
||||
}
|
||||
"ballots_votes" => {
|
||||
result.push(format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pps(&state.exhausted.ballot_transfers, 0), pps(&state.exhausted.transfers, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pp(&state.exhausted.num_ballots(), 0), pp(&state.exhausted.votes, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, classes_i, pps(&state.loss_fraction.transfers, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, classes_i, pp(&state.loss_fraction.votes, opts.pp_decimals)));
|
||||
}
|
||||
_ => unreachable!("Invalid report_style")
|
||||
}
|
||||
|
||||
// Calculate total votes
|
||||
let mut total_vote = state.candidates.iter().filter_map(|(c, cc)| if c.is_dummy { None } else { Some(cc) }).fold(N::new(), |mut acc, cc| { acc += &cc.votes; acc });
|
||||
total_vote += &state.exhausted.votes;
|
||||
total_vote += &state.loss_fraction.votes;
|
||||
|
||||
match report_style {
|
||||
"votes" => {
|
||||
result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&total_vote, opts.pp_decimals)));
|
||||
}
|
||||
"votes_transposed" => {
|
||||
if hide_xfers_trsp {
|
||||
result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&total_vote, opts.pp_decimals)));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, classes_i, pp(&total_vote, opts.pp_decimals)));
|
||||
}
|
||||
}
|
||||
"ballots_votes" => {
|
||||
// Calculate total ballots
|
||||
let mut total_ballots = state.candidates.values().fold(N::new(), |mut acc, cc| { acc += cc.num_ballots(); acc });
|
||||
total_ballots += state.exhausted.num_ballots();
|
||||
result.push(format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pp(&total_ballots, 0), pp(&total_vote, opts.pp_decimals)));
|
||||
}
|
||||
_ => unreachable!("Invalid report_style")
|
||||
}
|
||||
|
||||
if report_style == "votes" || (report_style == "votes_transposed" && hide_xfers_trsp) {
|
||||
result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, classes_i, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)));
|
||||
}
|
||||
|
||||
if stv::should_show_vre(opts) {
|
||||
if let Some(vre) = &state.vote_required_election {
|
||||
if report_style == "votes" || (report_style == "votes_transposed" && hide_xfers_trsp) {
|
||||
result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(vre, opts.pp_decimals)));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, classes_i, pp(vre, opts.pp_decimals)));
|
||||
}
|
||||
} else {
|
||||
if report_style == "votes" || (report_style == "votes_transposed" && hide_xfers_trsp) {
|
||||
result.push(format!(r#"<td class="{}count"></td>"#, classes_i));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count"></td><td class="count"></td>"#, classes_i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Generate the final column of the HTML results table
|
||||
pub fn finalise_results_table<N: Number>(state: &CountState<N>, report_style: &str) -> Vec<String> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
// Header rows
|
||||
match report_style {
|
||||
"votes" | "votes_transposed" => {
|
||||
result.push(String::from(r#"<td rowspan="3"></td>"#));
|
||||
result.push(String::from(""));
|
||||
result.push(String::from(""));
|
||||
}
|
||||
"ballots_votes" => {
|
||||
result.push(String::from(r#"<td rowspan="4"></td>"#));
|
||||
result.push(String::from(""));
|
||||
result.push(String::from(""));
|
||||
result.push(String::from(""));
|
||||
}
|
||||
_ => unreachable!("Invalid report_style")
|
||||
}
|
||||
|
||||
let rowspan = if report_style == "votes_transposed" { "" } else { r#" rowspan="2""# };
|
||||
|
||||
// Candidate states
|
||||
for candidate in state.election.candidates.iter() {
|
||||
if candidate.is_dummy {
|
||||
continue;
|
||||
}
|
||||
|
||||
let count_card = &state.candidates[candidate];
|
||||
if count_card.state == stv::CandidateState::Elected {
|
||||
result.push(format!(r#"<td{} class="bb elected">ELECTED {}</td>"#, rowspan, count_card.order_elected));
|
||||
} else if count_card.state == stv::CandidateState::Excluded {
|
||||
result.push(format!(r#"<td{} class="bb excluded">Excluded {}</td>"#, rowspan, -count_card.order_elected));
|
||||
} else if count_card.state == stv::CandidateState::Withdrawn {
|
||||
result.push(format!(r#"<td{} class="bb excluded">Withdrawn</td>"#, rowspan));
|
||||
} else {
|
||||
result.push(format!(r#"<td{} class="bb"></td>"#, rowspan));
|
||||
}
|
||||
|
||||
if report_style != "votes_transposed" {
|
||||
result.push(String::from(""));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// HTML pretty-print the number to the specified decimal places
|
||||
fn pp<N: Number>(n: &N, dps: usize) -> String {
|
||||
if n.is_zero() {
|
||||
return "".to_string();
|
||||
}
|
||||
|
||||
let mut raw = format!("{:.dps$}", n, dps=dps);
|
||||
if raw.contains('.') {
|
||||
raw = raw.replacen(".", ".<sup>", 1);
|
||||
raw.push_str("</sup>");
|
||||
}
|
||||
|
||||
if raw.starts_with('-') {
|
||||
raw = raw.replacen("-", "−", 1);
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
/// Signed version of [pp]
|
||||
fn pps<N: Number>(n: &N, dps: usize) -> String {
|
||||
if n.is_zero() {
|
||||
return "".to_string();
|
||||
}
|
||||
|
||||
let mut raw = format!("{:.dps$}", n, dps=dps);
|
||||
if raw.contains('.') {
|
||||
raw = raw.replacen(".", ".<sup>", 1);
|
||||
raw.push_str("</sup>");
|
||||
}
|
||||
|
||||
if raw.starts_with('-') {
|
||||
raw = raw.replacen("-", "−", 1);
|
||||
} else {
|
||||
raw.insert(0, '+');
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
103
src/stv/meek.rs
103
src/stv/meek.rs
|
@ -1,5 +1,5 @@
|
|||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021–2022 Lee Yingtong Li (RunasSudo)
|
||||
* 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
|
||||
|
@ -17,12 +17,10 @@
|
|||
|
||||
use super::{STVError, STVOptions};
|
||||
|
||||
use crate::candmap::CandidateMap;
|
||||
use crate::election::{Ballot, Candidate, CandidateState, CountCard, CountState, Election, StageKind};
|
||||
use crate::election::{Ballot, Candidate, CandidateState, CountCard, CountState, Election};
|
||||
use crate::numbers::Number;
|
||||
|
||||
use itertools::Itertools;
|
||||
use nohash_hasher::BuildNoHashHasher;
|
||||
|
||||
use std::cmp::max;
|
||||
use std::collections::HashMap;
|
||||
|
@ -37,11 +35,10 @@ struct BallotInTree<'b, N: Number> {
|
|||
}
|
||||
|
||||
/// Tree-packed ballot representation
|
||||
#[derive(Clone)]
|
||||
pub struct BallotTree<'t, N: Number> {
|
||||
num_ballots: N,
|
||||
ballots: Vec<BallotInTree<'t, N>>,
|
||||
next_preferences: Option<Box<HashMap<&'t Candidate, BallotTree<'t, N>, BuildNoHashHasher<Candidate>>>>,
|
||||
next_preferences: Option<Box<HashMap<&'t Candidate, BallotTree<'t, N>>>>,
|
||||
next_exhausted: Option<Box<BallotTree<'t, N>>>,
|
||||
}
|
||||
|
||||
|
@ -57,37 +54,29 @@ impl<'t, N: Number> BallotTree<'t, N> {
|
|||
}
|
||||
|
||||
/// Descend one level of the [BallotTree]
|
||||
fn descend_tree(&mut self, candidates: &'t [Candidate]) {
|
||||
let mut next_preferences: HashMap<&Candidate, BallotTree<N>, BuildNoHashHasher<Candidate>> = HashMap::with_capacity_and_hasher(candidates.len(), BuildNoHashHasher::default());
|
||||
fn descend_tree(&mut self, candidates: &'t Vec<Candidate>) {
|
||||
let mut next_preferences: HashMap<&Candidate, BallotTree<N>> = HashMap::new();
|
||||
let mut next_exhausted = BallotTree::new();
|
||||
|
||||
for bit in self.ballots.iter() {
|
||||
if bit.up_to_pref < bit.ballot.preferences.len() {
|
||||
let preference = &bit.ballot.preferences[bit.up_to_pref];
|
||||
let candidate = &candidates[bit.ballot.preferences[bit.up_to_pref]];
|
||||
|
||||
if preference.len() != 1 {
|
||||
todo!();
|
||||
}
|
||||
|
||||
let candidate = &candidates[*preference.first().unwrap()];
|
||||
|
||||
match next_preferences.get_mut(candidate) {
|
||||
Some(np_bt) => {
|
||||
np_bt.num_ballots += &bit.ballot.orig_value;
|
||||
np_bt.ballots.push(BallotInTree {
|
||||
ballot: bit.ballot,
|
||||
up_to_pref: bit.up_to_pref + 1,
|
||||
});
|
||||
}
|
||||
None => {
|
||||
let mut np_bt = BallotTree::new();
|
||||
np_bt.num_ballots += &bit.ballot.orig_value;
|
||||
np_bt.ballots.push(BallotInTree {
|
||||
ballot: bit.ballot,
|
||||
up_to_pref: bit.up_to_pref + 1,
|
||||
});
|
||||
next_preferences.insert(candidate, np_bt);
|
||||
}
|
||||
if next_preferences.contains_key(candidate) {
|
||||
let np_bt = next_preferences.get_mut(candidate).unwrap();
|
||||
np_bt.num_ballots += &bit.ballot.orig_value;
|
||||
np_bt.ballots.push(BallotInTree {
|
||||
ballot: bit.ballot,
|
||||
up_to_pref: bit.up_to_pref + 1,
|
||||
});
|
||||
} else {
|
||||
let mut np_bt = BallotTree::new();
|
||||
np_bt.num_ballots += &bit.ballot.orig_value;
|
||||
np_bt.ballots.push(BallotInTree {
|
||||
ballot: bit.ballot,
|
||||
up_to_pref: bit.up_to_pref + 1,
|
||||
});
|
||||
next_preferences.insert(candidate, np_bt);
|
||||
}
|
||||
} else {
|
||||
// Exhausted
|
||||
|
@ -116,7 +105,7 @@ where
|
|||
let mut ballot_tree = BallotTree::new();
|
||||
for ballot in state.election.ballots.iter() {
|
||||
ballot_tree.ballots.push(BallotInTree {
|
||||
ballot,
|
||||
ballot: ballot,
|
||||
up_to_pref: 0,
|
||||
});
|
||||
ballot_tree.num_ballots += &ballot.orig_value;
|
||||
|
@ -132,17 +121,8 @@ where
|
|||
}
|
||||
state.exhausted.transfers.assign(&state.exhausted.votes);
|
||||
|
||||
// Calculate loss by fraction - if minivoters used
|
||||
if let Some(orig_total) = &state.election.total_votes {
|
||||
let mut total_votes = state.total_vote();
|
||||
total_votes += &state.exhausted.votes;
|
||||
let lbf = orig_total - &total_votes;
|
||||
|
||||
state.loss_fraction.votes = lbf.clone();
|
||||
state.loss_fraction.transfers = lbf;
|
||||
}
|
||||
|
||||
state.title = StageKind::FirstPreferences;
|
||||
state.kind = None;
|
||||
state.title = "First preferences".to_string();
|
||||
state.logger.log_literal("First preferences distributed.".to_string());
|
||||
}
|
||||
|
||||
|
@ -160,21 +140,23 @@ where
|
|||
}
|
||||
state.exhausted.votes = N::new();
|
||||
|
||||
distribute_recursively(&mut state.candidates, &mut state.exhausted, state.ballot_tree.as_mut().unwrap(), N::one(), state.election, opts);
|
||||
distribute_recursively(&mut state.candidates, &mut state.exhausted, state.ballot_tree.as_mut().unwrap(), N::one(), &state.election, opts);
|
||||
}
|
||||
|
||||
/// Distribute preferences recursively
|
||||
///
|
||||
/// Called by [distribute_preferences]
|
||||
fn distribute_recursively<'t, N: Number>(candidates: &mut CandidateMap<'t, CountCard<N>>, exhausted: &mut CountCard<N>, tree: &mut BallotTree<'t, N>, remaining_multiplier: N, election: &'t Election<N>, opts: &STVOptions)
|
||||
fn distribute_recursively<'t, N: Number>(candidates: &mut HashMap<&'t Candidate, CountCard<N>>, exhausted: &mut CountCard<N>, tree: &mut BallotTree<'t, N>, remaining_multiplier: N, election: &'t Election<N>, opts: &STVOptions)
|
||||
where
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
{
|
||||
// Descend tree if required
|
||||
if tree.next_exhausted.is_none() {
|
||||
if let None = tree.next_exhausted {
|
||||
tree.descend_tree(&election.candidates);
|
||||
}
|
||||
|
||||
// FIXME: Possibility of infinite loop if malformed inputs?
|
||||
|
||||
// Credit votes at this level
|
||||
for (candidate, cand_tree) in tree.next_preferences.as_mut().unwrap().as_mut().iter_mut() {
|
||||
let count_card = candidates.get_mut(candidate).unwrap();
|
||||
|
@ -197,7 +179,7 @@ where
|
|||
count_card.votes += to_transfer;
|
||||
|
||||
let mut new_remaining_multiplier = &remaining_multiplier * &(N::one() - count_card.keep_value.as_ref().unwrap());
|
||||
if let Some(dps) = opts.round_surplus_fractions {
|
||||
if let Some(dps) = opts.round_tvs {
|
||||
new_remaining_multiplier.ceil_mut(dps);
|
||||
}
|
||||
|
||||
|
@ -216,12 +198,12 @@ where
|
|||
exhausted.votes += &remaining_multiplier * &tree.next_exhausted.as_ref().unwrap().as_ref().num_ballots;
|
||||
}
|
||||
|
||||
fn recompute_keep_values<'s, N: Number>(state: &mut CountState<'s, N>, opts: &STVOptions, has_surplus: &[&'s Candidate]) {
|
||||
for candidate in has_surplus {
|
||||
fn recompute_keep_values<'s, N: Number>(state: &mut CountState<'s, N>, opts: &STVOptions, has_surplus: &Vec<&'s Candidate>) {
|
||||
for candidate in has_surplus.into_iter() {
|
||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||
count_card.keep_value = Some(count_card.keep_value.take().unwrap() * state.quota.as_ref().unwrap() / &count_card.votes);
|
||||
|
||||
if let Some(dps) = opts.round_values {
|
||||
if let Some(dps) = opts.round_weights {
|
||||
// NZ Meek STV rounds *up*!
|
||||
count_card.keep_value.as_mut().unwrap().ceil_mut(dps);
|
||||
}
|
||||
|
@ -229,7 +211,7 @@ fn recompute_keep_values<'s, N: Number>(state: &mut CountState<'s, N>, opts: &ST
|
|||
}
|
||||
|
||||
/// Determine if the specified surpluses should be distributed, according to [STVOptions::meek_surplus_tolerance]
|
||||
fn should_distribute_surpluses<'a, N: Number>(state: &CountState<'a, N>, has_surplus: &[&'a Candidate], opts: &STVOptions) -> bool
|
||||
fn should_distribute_surpluses<'a, N: Number>(state: &CountState<'a, N>, has_surplus: &Vec<&'a Candidate>, opts: &STVOptions) -> bool
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
|
@ -244,7 +226,8 @@ where
|
|||
} else {
|
||||
// Distribute if the total surplus exceeds the tolerance
|
||||
let quota_tolerance = N::parse(&opts.meek_surplus_tolerance);
|
||||
let total_surpluses = state.total_surplus();
|
||||
let total_surpluses = has_surplus.iter()
|
||||
.fold(N::new(), |acc, c| acc + &state.candidates[c].votes - state.quota.as_ref().unwrap());
|
||||
return total_surpluses > quota_tolerance;
|
||||
}
|
||||
}
|
||||
|
@ -268,7 +251,8 @@ where
|
|||
if should_distribute {
|
||||
// Determine if surplues can be deferred
|
||||
if opts.defer_surpluses {
|
||||
let total_surpluses = state.total_surplus();
|
||||
let total_surpluses = has_surplus.iter()
|
||||
.fold(N::new(), |acc, c| acc + &state.candidates[c].votes - quota);
|
||||
if super::can_defer_surpluses(state, opts, &total_surpluses) {
|
||||
state.logger.log_literal(format!("Distribution of surpluses totalling {:.dps$} votes will be deferred.", total_surpluses, dps=opts.pp_decimals));
|
||||
return Ok(false);
|
||||
|
@ -295,9 +279,9 @@ where
|
|||
// Recompute quota if more ballots have become exhausted
|
||||
super::calculate_quota(state, opts);
|
||||
|
||||
if opts.immediate_elect {
|
||||
if opts.meek_immediate_elect {
|
||||
// Try to elect candidates
|
||||
if super::elect_hopefuls(state, opts, true)? {
|
||||
if super::elect_meeting_quota(state, opts)? {
|
||||
candidates_elected = Some(state.logger.entries.pop().unwrap());
|
||||
break;
|
||||
}
|
||||
|
@ -315,7 +299,8 @@ where
|
|||
|
||||
// Determine if surplues can be deferred
|
||||
if should_distribute && opts.defer_surpluses {
|
||||
let total_surpluses = state.total_surplus();
|
||||
let total_surpluses = has_surplus.iter()
|
||||
.fold(N::new(), |acc, c| acc + &state.candidates[c].votes - quota);
|
||||
if super::can_defer_surpluses(state, opts, &total_surpluses) {
|
||||
surpluses_deferred = Some(total_surpluses);
|
||||
break;
|
||||
|
@ -336,7 +321,8 @@ where
|
|||
// Remove intermediate logs on quota calculation
|
||||
state.logger.entries.clear();
|
||||
|
||||
state.title = StageKind::SurplusesDistributed;
|
||||
state.kind = None;
|
||||
state.title = "Surpluses distributed".to_string();
|
||||
if num_iterations == 1 {
|
||||
state.logger.log_literal("Surpluses distributed, requiring 1 iteration.".to_string());
|
||||
} else {
|
||||
|
@ -403,7 +389,6 @@ where
|
|||
count_card.state = CandidateState::Excluded;
|
||||
state.num_excluded += 1;
|
||||
count_card.order_elected = -(order_excluded as isize);
|
||||
count_card.finalised = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
1267
src/stv/mod.rs
1267
src/stv/mod.rs
File diff suppressed because it is too large
Load Diff
|
@ -1,631 +0,0 @@
|
|||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021–2023 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use super::STVError;
|
||||
|
||||
use crate::numbers::Number;
|
||||
use crate::ties::TieStrategy;
|
||||
|
||||
use derive_builder::Builder;
|
||||
use derive_more::Constructor;
|
||||
use itertools::Itertools;
|
||||
#[allow(unused_imports)]
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
/// Options for conducting an STV count
|
||||
#[derive(Builder, Constructor)]
|
||||
pub struct STVOptions {
|
||||
/// Round surplus fractions to specified decimal places
|
||||
#[builder(default="None")]
|
||||
pub round_surplus_fractions: Option<usize>,
|
||||
|
||||
/// Round ballot values to specified decimal places
|
||||
#[builder(default="None")]
|
||||
pub round_values: Option<usize>,
|
||||
|
||||
/// Round votes to specified decimal places
|
||||
#[builder(default="None")]
|
||||
pub round_votes: Option<usize>,
|
||||
|
||||
/// Round quota to specified decimal places
|
||||
#[builder(default="None")]
|
||||
pub round_quota: Option<usize>,
|
||||
|
||||
/// How to round votes in transfer table
|
||||
#[builder(default="RoundSubtransfersMode::SingleStep")]
|
||||
pub round_subtransfers: RoundSubtransfersMode,
|
||||
|
||||
/// (Meek STV) Limit for stopping iteration of surplus distribution
|
||||
#[builder(default=r#"String::from("0.001%")"#)]
|
||||
pub meek_surplus_tolerance: String,
|
||||
|
||||
/// Quota type
|
||||
#[builder(default="QuotaType::Droop")]
|
||||
pub quota: QuotaType,
|
||||
|
||||
/// Whether to elect candidates on meeting (geq) or strictly exceeding (gt) the quota
|
||||
#[builder(default="QuotaCriterion::Greater")]
|
||||
pub quota_criterion: QuotaCriterion,
|
||||
|
||||
/// Whether to apply a form of progressive quota
|
||||
#[builder(default="QuotaMode::Static")]
|
||||
pub quota_mode: QuotaMode,
|
||||
|
||||
/// Tie-breaking method
|
||||
#[builder(default="vec![TieStrategy::Prompt]")]
|
||||
pub ties: Vec<TieStrategy>,
|
||||
|
||||
/// Method of surplus distributions
|
||||
#[builder(default="SurplusMethod::WIG")]
|
||||
pub surplus: SurplusMethod,
|
||||
|
||||
/// (Gregory STV) Order to distribute surpluses
|
||||
#[builder(default="SurplusOrder::BySize")]
|
||||
pub surplus_order: SurplusOrder,
|
||||
|
||||
/// (Gregory STV) Examine only transferable papers during surplus distributions
|
||||
#[builder(default="false")]
|
||||
pub transferable_only: bool,
|
||||
|
||||
/// (Gregory STV) When calculating surplus fractions, assume the progress total is the total value of all the candidate's papers
|
||||
#[builder(default="false")]
|
||||
pub surplus_assume_total: bool,
|
||||
|
||||
/// (Gregory STV) Method of exclusions
|
||||
#[builder(default="ExclusionMethod::SingleStage")]
|
||||
pub exclusion: ExclusionMethod,
|
||||
|
||||
/// (Meek STV) NZ Meek STV behaviour: Iterate keep values one round before candidate exclusion
|
||||
#[builder(default="false")]
|
||||
pub meek_nz_exclusion: bool,
|
||||
|
||||
/// (Hare) Method of drawing a sample
|
||||
#[builder(default="SampleMethod::StratifyLR")]
|
||||
pub sample: SampleMethod,
|
||||
|
||||
/// (Hare) Sample-based methods: Check for candidate election after each individual ballot paper transfer
|
||||
#[builder(default="false")]
|
||||
pub sample_per_ballot: bool,
|
||||
|
||||
/// Bulk elect as soon as continuing candidates fill all remaining vacancies
|
||||
#[builder(default="true")]
|
||||
pub early_bulk_elect: bool,
|
||||
|
||||
/// Use bulk exclusion
|
||||
#[builder(default="false")]
|
||||
pub bulk_exclude: bool,
|
||||
|
||||
/// Defer surplus distributions if possible
|
||||
#[builder(default="false")]
|
||||
pub defer_surpluses: bool,
|
||||
|
||||
/// Elect candidates on meeting the quota, rather than on surpluses being distributed; (Meek STV) Immediately elect candidates even if keep values have not converged
|
||||
#[builder(default="true")]
|
||||
pub immediate_elect: bool,
|
||||
|
||||
/// On exclusion, exclude any candidate with this many votes or fewer
|
||||
#[builder(default="\"0\".to_string()")]
|
||||
pub min_threshold: String,
|
||||
|
||||
/// Path to constraints file (used only for [STVOptions::describe])
|
||||
#[builder(default="None")]
|
||||
pub constraints_path: Option<String>,
|
||||
|
||||
/// Mode of handling constraints
|
||||
#[builder(default="ConstraintMode::GuardDoom")]
|
||||
pub constraint_mode: ConstraintMode,
|
||||
|
||||
/// (CLI) Hide excluded candidates from results report
|
||||
#[builder(default="false")]
|
||||
pub hide_excluded: bool,
|
||||
|
||||
/// (CLI) Sort candidates by votes in results report
|
||||
#[builder(default="false")]
|
||||
pub sort_votes: bool,
|
||||
|
||||
/// (CLI) Show details of transfers to candidates during surplus distributions/candidate exclusions
|
||||
#[builder(default="false")]
|
||||
pub transfers_detail: bool,
|
||||
|
||||
/// Print votes to specified decimal places in results report
|
||||
#[builder(default="2")]
|
||||
pub pp_decimals: usize,
|
||||
}
|
||||
|
||||
impl STVOptions {
|
||||
/// Converts the [STVOptions] into CLI argument representation
|
||||
pub fn describe<N: Number>(&self) -> String {
|
||||
let mut flags = Vec::new();
|
||||
let n_str = N::describe_opt(); if !n_str.is_empty() { flags.push(N::describe_opt()) };
|
||||
if self.surplus != SurplusMethod::IHare && self.surplus != SurplusMethod::Hare {
|
||||
if let Some(dps) = self.round_surplus_fractions { flags.push(format!("--round-surplus-fractions {}", dps)); }
|
||||
if let Some(dps) = self.round_values { flags.push(format!("--round-values {}", dps)); }
|
||||
if let Some(dps) = self.round_votes { flags.push(format!("--round-votes {}", dps)); }
|
||||
}
|
||||
if let Some(dps) = self.round_quota { flags.push(format!("--round-quota {}", dps)); }
|
||||
if self.surplus != SurplusMethod::Meek && self.round_subtransfers != RoundSubtransfersMode::SingleStep { flags.push(self.round_subtransfers.describe()); }
|
||||
if self.surplus == SurplusMethod::Meek && self.meek_surplus_tolerance != "0.001%" { flags.push(format!("--meek-surplus-tolerance {}", self.meek_surplus_tolerance)); }
|
||||
if self.quota != QuotaType::Droop { flags.push(self.quota.describe()); }
|
||||
if self.quota_criterion != QuotaCriterion::Greater { flags.push(self.quota_criterion.describe()); }
|
||||
if self.quota_mode != QuotaMode::Static { flags.push(self.quota_mode.describe()); }
|
||||
let ties_str = self.ties.iter().map(|t| t.describe()).join(" ");
|
||||
if ties_str != "prompt" { flags.push(format!("--ties {}", ties_str)); }
|
||||
for t in self.ties.iter() { if let TieStrategy::Random(seed) = t { flags.push(format!("--random-seed {}", seed)); } }
|
||||
if self.surplus != SurplusMethod::WIG { flags.push(self.surplus.describe()); }
|
||||
if self.surplus != SurplusMethod::Meek {
|
||||
if self.surplus_order != SurplusOrder::BySize { flags.push(self.surplus_order.describe()); }
|
||||
if self.transferable_only { flags.push("--transferable-only".to_string()); }
|
||||
if self.surplus_assume_total { flags.push("--surplus-assume-total".to_string()); }
|
||||
if self.exclusion != ExclusionMethod::SingleStage { flags.push(self.exclusion.describe()); }
|
||||
}
|
||||
if self.surplus == SurplusMethod::Meek && self.meek_nz_exclusion { flags.push("--meek-nz-exclusion".to_string()); }
|
||||
if (self.surplus == SurplusMethod::IHare || self.surplus == SurplusMethod::Hare) && self.sample != SampleMethod::StratifyLR { flags.push(self.sample.describe()); }
|
||||
if (self.surplus == SurplusMethod::IHare || self.surplus == SurplusMethod::Hare) && self.sample_per_ballot { flags.push("--sample-per-ballot".to_string()); }
|
||||
if !self.early_bulk_elect { flags.push("--no-early-bulk-elect".to_string()); }
|
||||
if self.bulk_exclude { flags.push("--bulk-exclude".to_string()); }
|
||||
if self.defer_surpluses { flags.push("--defer-surpluses".to_string()); }
|
||||
if !self.immediate_elect { flags.push("--no-immediate-elect".to_string()); }
|
||||
if self.min_threshold != "0" { flags.push(format!("--min-threshold {}", self.min_threshold)); }
|
||||
if let Some(path) = &self.constraints_path {
|
||||
flags.push(format!("--constraints {}", path));
|
||||
if self.constraint_mode != ConstraintMode::GuardDoom { flags.push(self.constraint_mode.describe()); }
|
||||
}
|
||||
if self.hide_excluded { flags.push("--hide-excluded".to_string()); }
|
||||
if self.sort_votes { flags.push("--sort-votes".to_string()); }
|
||||
if self.transfers_detail { flags.push("--transfers-detail".to_string()); }
|
||||
if self.pp_decimals != 2 { flags.push(format!("--pp-decimals {}", self.pp_decimals)); }
|
||||
return flags.join(" ");
|
||||
}
|
||||
|
||||
/// 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::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) {
|
||||
// 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.surplus_assume_total {
|
||||
if self.surplus != SurplusMethod::WIG {
|
||||
// Invalid because other methods do not distinguish between ballots of different value during surplus transfer
|
||||
return Err(STVError::InvalidOptions("--surplus-assume-total requires --surplus wig"));
|
||||
}
|
||||
}
|
||||
if !self.immediate_elect {
|
||||
if self.surplus_order != SurplusOrder::BySize {
|
||||
// Invalid because there is no other metric to determine which surplus to distribute
|
||||
return Err(STVError::InvalidOptions("--no-immediate-elect requires --surplus-order by_size"));
|
||||
}
|
||||
if self.quota_mode == QuotaMode::ERS97 {
|
||||
// Invalid because candidates meeting the VRE never have surpluses distributed
|
||||
return Err(STVError::InvalidOptions("--no-immediate-elect is incompatible with --quota-mode ers97"));
|
||||
}
|
||||
if self.quota_mode == QuotaMode::ERS76 {
|
||||
// Invalid because candidates meeting the VRE never have surpluses distributed
|
||||
return Err(STVError::InvalidOptions("--no-immediate-elect is incompatible with --quota-mode ers76"));
|
||||
}
|
||||
}
|
||||
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)"));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum of options for [STVOptions::round_subtransfers]
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(PartialEq)]
|
||||
pub enum RoundSubtransfersMode {
|
||||
/// Do not round subtransfers (only round final number of votes credited)
|
||||
SingleStep,
|
||||
/// Round in subtransfers according to the value when received
|
||||
ByValue,
|
||||
/// Round in subtransfers according to the candidate from who each vote was received, and the value when received
|
||||
ByValueAndSource,
|
||||
/// Round in subtransfers according to parcel
|
||||
ByParcel,
|
||||
/// Sum and round transfers individually for each ballot paper
|
||||
PerBallot,
|
||||
}
|
||||
|
||||
impl RoundSubtransfersMode {
|
||||
/// Convert to CLI argument representation
|
||||
fn describe(self) -> String {
|
||||
match self {
|
||||
RoundSubtransfersMode::SingleStep => "--round-subtransfers single_step",
|
||||
RoundSubtransfersMode::ByValue => "--round-subtransfers by_value",
|
||||
RoundSubtransfersMode::ByValueAndSource => "--round-subtransfers by_value_and_source",
|
||||
RoundSubtransfersMode::ByParcel => "--round-subtransfers by_parcel",
|
||||
RoundSubtransfersMode::PerBallot => "--round-subtransfers per_ballot",
|
||||
}.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<str>> From<S> for RoundSubtransfersMode {
|
||||
fn from(s: S) -> Self {
|
||||
match s.as_ref() {
|
||||
"single_step" => RoundSubtransfersMode::SingleStep,
|
||||
"by_value" => RoundSubtransfersMode::ByValue,
|
||||
"by_value_and_source" => RoundSubtransfersMode::ByValueAndSource,
|
||||
"by_parcel" => RoundSubtransfersMode::ByParcel,
|
||||
"per_ballot" => RoundSubtransfersMode::PerBallot,
|
||||
_ => panic!("Invalid --round-subtransfers"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum of options for [STVOptions::quota]
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(PartialEq)]
|
||||
pub enum QuotaType {
|
||||
/// Droop quota
|
||||
Droop,
|
||||
/// Hare quota
|
||||
Hare,
|
||||
/// Exact Droop quota (Newland–Britton/Hagenbach-Bischoff quota)
|
||||
DroopExact,
|
||||
/// Exact Hare quota
|
||||
HareExact,
|
||||
}
|
||||
|
||||
impl QuotaType {
|
||||
/// Convert to CLI argument representation
|
||||
fn describe(self) -> String {
|
||||
match self {
|
||||
QuotaType::Droop => "--quota droop",
|
||||
QuotaType::Hare => "--quota hare",
|
||||
QuotaType::DroopExact => "--quota droop_exact",
|
||||
QuotaType::HareExact => "--quota hare_exact",
|
||||
}.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<str>> From<S> for QuotaType {
|
||||
fn from(s: S) -> Self {
|
||||
match s.as_ref() {
|
||||
"droop" => QuotaType::Droop,
|
||||
"hare" => QuotaType::Hare,
|
||||
"droop_exact" => QuotaType::DroopExact,
|
||||
"hare_exact" => QuotaType::HareExact,
|
||||
_ => panic!("Invalid --quota"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum of options for [STVOptions::quota_criterion]
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(PartialEq)]
|
||||
pub enum QuotaCriterion {
|
||||
/// Elect candidates on equalling or exceeding the quota
|
||||
GreaterOrEqual,
|
||||
/// Elect candidates on strictly exceeding the quota
|
||||
Greater,
|
||||
}
|
||||
|
||||
impl QuotaCriterion {
|
||||
/// Convert to CLI argument representation
|
||||
fn describe(self) -> String {
|
||||
match self {
|
||||
QuotaCriterion::GreaterOrEqual => "--quota-criterion geq",
|
||||
QuotaCriterion::Greater => "--quota-criterion gt",
|
||||
}.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<str>> From<S> for QuotaCriterion {
|
||||
fn from(s: S) -> Self {
|
||||
match s.as_ref() {
|
||||
"geq" => QuotaCriterion::GreaterOrEqual,
|
||||
"gt" => QuotaCriterion::Greater,
|
||||
_ => panic!("Invalid --quota-criterion"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum of options for [STVOptions::quota_mode]
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(PartialEq)]
|
||||
pub enum QuotaMode {
|
||||
/// Static quota
|
||||
Static,
|
||||
/// Static quota with ERS97 rules
|
||||
ERS97,
|
||||
/// Static quota with ERS76 rules
|
||||
ERS76,
|
||||
/// Dynamic quota by total vote
|
||||
DynamicByTotal,
|
||||
/// Dynamic quota by active vote
|
||||
DynamicByActive,
|
||||
}
|
||||
|
||||
impl QuotaMode {
|
||||
/// Convert to CLI argument representation
|
||||
fn describe(self) -> String {
|
||||
match self {
|
||||
QuotaMode::Static => "--quota-mode static",
|
||||
QuotaMode::ERS97 => "--quota-mode ers97",
|
||||
QuotaMode::ERS76 => "--quota-mode ers76",
|
||||
QuotaMode::DynamicByTotal => "--quota-mode dynamic_by_total",
|
||||
QuotaMode::DynamicByActive => "--quota-mode dynamic_by_active",
|
||||
}.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<str>> From<S> for QuotaMode {
|
||||
fn from(s: S) -> Self {
|
||||
match s.as_ref() {
|
||||
"static" => QuotaMode::Static,
|
||||
"ers97" => QuotaMode::ERS97,
|
||||
"ers76" => QuotaMode::ERS76,
|
||||
"dynamic_by_total" => QuotaMode::DynamicByTotal,
|
||||
"dynamic_by_active" => QuotaMode::DynamicByActive,
|
||||
_ => panic!("Invalid --quota-mode"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum of options for [STVOptions::surplus]
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(PartialEq)]
|
||||
pub enum SurplusMethod {
|
||||
/// Weighted inclusive Gregory method
|
||||
WIG,
|
||||
/// Unweighted inclusive Gregory method
|
||||
UIG,
|
||||
/// Exclusive Gregory method (last bundle)
|
||||
EG,
|
||||
/// Meek method
|
||||
Meek,
|
||||
/// Inclusive Hare method (random subset)
|
||||
IHare,
|
||||
/// (Exclusive) Hare method (random subset)
|
||||
Hare,
|
||||
}
|
||||
|
||||
impl SurplusMethod {
|
||||
/// Convert to CLI argument representation
|
||||
fn describe(self) -> String {
|
||||
match self {
|
||||
SurplusMethod::WIG => "--surplus wig",
|
||||
SurplusMethod::UIG => "--surplus uig",
|
||||
SurplusMethod::EG => "--surplus eg",
|
||||
SurplusMethod::Meek => "--surplus meek",
|
||||
SurplusMethod::IHare => "--surplus ihare",
|
||||
SurplusMethod::Hare => "--surplus hare",
|
||||
}.to_string()
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a weighted method
|
||||
pub fn is_weighted(&self) -> bool {
|
||||
return match self {
|
||||
SurplusMethod::WIG => { true }
|
||||
SurplusMethod::UIG | SurplusMethod::EG => { false }
|
||||
_ => unreachable!()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<str>> From<S> for SurplusMethod {
|
||||
fn from(s: S) -> Self {
|
||||
match s.as_ref() {
|
||||
"wig" => SurplusMethod::WIG,
|
||||
"uig" => SurplusMethod::UIG,
|
||||
"eg" => SurplusMethod::EG,
|
||||
"meek" => SurplusMethod::Meek,
|
||||
"ihare" | "ih" | "cincinnati" => SurplusMethod::IHare, // Inclusive Hare method used to be erroneously referred to as "Cincinnati" method - accept for backwards compatibility
|
||||
"hare" | "eh" => SurplusMethod::Hare,
|
||||
_ => panic!("Invalid --surplus"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum of options for [STVOptions::surplus_order]
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(PartialEq)]
|
||||
pub enum SurplusOrder {
|
||||
/// Transfer the largest surplus first, even if it arose at a later stage of the count
|
||||
BySize,
|
||||
/// Transfer the surplus of the candidate elected first, even if it is smaller than another
|
||||
ByOrder,
|
||||
}
|
||||
|
||||
impl SurplusOrder {
|
||||
/// Convert to CLI argument representation
|
||||
fn describe(self) -> String {
|
||||
match self {
|
||||
SurplusOrder::BySize => "--surplus-order by_size",
|
||||
SurplusOrder::ByOrder => "--surplus-order by_order",
|
||||
}.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<str>> From<S> for SurplusOrder {
|
||||
fn from(s: S) -> Self {
|
||||
match s.as_ref() {
|
||||
"by_size" => SurplusOrder::BySize,
|
||||
"by_order" => SurplusOrder::ByOrder,
|
||||
_ => panic!("Invalid --surplus-order"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum of options for [STVOptions::exclusion]
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(PartialEq)]
|
||||
pub enum ExclusionMethod {
|
||||
/// Transfer all ballot papers of an excluded candidate in one stage
|
||||
SingleStage,
|
||||
/// Transfer the ballot papers of an excluded candidate in descending order of accumulated transfer value
|
||||
ByValue,
|
||||
/// Transfer the first preferences for an excluded candidate, then ballot papers received on transfers in descending order of accumulated transfer value
|
||||
FirstPreferencesThenByValue,
|
||||
/// Transfer the ballot papers of an excluded candidate according to the candidate who transferred the papers to the excluded candidate, in the order the transferring candidates were elected or excluded
|
||||
BySource,
|
||||
/// Transfer the ballot papers of an excluded candidate parcel by parcel in the order received
|
||||
ParcelsByOrder,
|
||||
/// Reset count and re-iterate from count of first preferences
|
||||
ResetAndReiterate,
|
||||
}
|
||||
|
||||
impl ExclusionMethod {
|
||||
/// Convert to CLI argument representation
|
||||
fn describe(self) -> String {
|
||||
match self {
|
||||
ExclusionMethod::SingleStage => "--exclusion single_stage",
|
||||
ExclusionMethod::ByValue => "--exclusion by_value",
|
||||
ExclusionMethod::FirstPreferencesThenByValue => "--exclusion first_prefs_then_by_value",
|
||||
ExclusionMethod::BySource => "--exclusion by_source",
|
||||
ExclusionMethod::ParcelsByOrder => "--exclusion parcels_by_order",
|
||||
ExclusionMethod::ResetAndReiterate => "--exclusion reset_and_reiterate",
|
||||
}.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<str>> From<S> for ExclusionMethod {
|
||||
fn from(s: S) -> Self {
|
||||
match s.as_ref() {
|
||||
"single_stage" => ExclusionMethod::SingleStage,
|
||||
"by_value" => ExclusionMethod::ByValue,
|
||||
"first_prefs_then_by_value" => ExclusionMethod::FirstPreferencesThenByValue,
|
||||
"by_source" => ExclusionMethod::BySource,
|
||||
"parcels_by_order" => ExclusionMethod::ParcelsByOrder,
|
||||
"reset_and_reiterate" => ExclusionMethod::ResetAndReiterate,
|
||||
"wright" => ExclusionMethod::ResetAndReiterate,
|
||||
_ => panic!("Invalid --exclusion"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum of options for [STVOptions::sample]
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(PartialEq)]
|
||||
pub enum SampleMethod {
|
||||
/// Stratify the ballots into parcels according to next available preference and transfer the last ballots from each parcel; round fractions according to largest remainders
|
||||
StratifyLR,
|
||||
// Stratify the ballots into parcels according to next available preference and transfer the last ballots from each parcel; disregard fractions
|
||||
//StratifyFloor,
|
||||
/// Transfer the last ballots
|
||||
ByOrder,
|
||||
/// Transfer every n-th ballot, Cincinnati style
|
||||
Cincinnati,
|
||||
}
|
||||
|
||||
impl SampleMethod {
|
||||
/// Convert to CLI argument representation
|
||||
fn describe(self) -> String {
|
||||
match self {
|
||||
SampleMethod::StratifyLR => "--sample stratify",
|
||||
//SampleMethod::StratifyFloor => "--sample stratify_floor",
|
||||
SampleMethod::ByOrder => "--sample by_order",
|
||||
SampleMethod::Cincinnati => "--sample cincinnati",
|
||||
}.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<str>> From<S> for SampleMethod {
|
||||
fn from(s: S) -> Self {
|
||||
match s.as_ref() {
|
||||
"stratify" | "stratify_lr" => SampleMethod::StratifyLR,
|
||||
//"stratify_floor" => SampleMethod::StratifyFloor,
|
||||
"by_order" => SampleMethod::ByOrder,
|
||||
"cincinnati" | "nth_ballot" => SampleMethod::Cincinnati,
|
||||
_ => panic!("Invalid --sample-method"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum of options for [STVOptions::constraint_mode]
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(PartialEq)]
|
||||
pub enum ConstraintMode {
|
||||
/// Guard or doom candidates as soon as required to secure a conformant result
|
||||
GuardDoom,
|
||||
/// If constraints violated, exclude/reintroduce candidates as required and redistribute ballot papers
|
||||
RepeatCount,
|
||||
}
|
||||
|
||||
impl ConstraintMode {
|
||||
/// Convert to CLI argument representation
|
||||
fn describe(self) -> String {
|
||||
match self {
|
||||
ConstraintMode::GuardDoom => "--constraint-mode guard_doom",
|
||||
ConstraintMode::RepeatCount => "--constraint-mode repeat_count",
|
||||
}.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<str>> From<S> for ConstraintMode {
|
||||
fn from(s: S) -> Self {
|
||||
match s.as_ref() {
|
||||
"guard_doom" => ConstraintMode::GuardDoom,
|
||||
"repeat_count" => ConstraintMode::RepeatCount,
|
||||
_ => panic!("Invalid --constraint-mode"),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,510 +0,0 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::constraints;
|
||||
use crate::election::{Candidate, CandidateState, CountState, Parcel, StageKind, Vote};
|
||||
use crate::numbers::Number;
|
||||
use crate::stv::{STVOptions, SampleMethod, SurplusMethod};
|
||||
|
||||
use std::cmp::max;
|
||||
use std::collections::HashMap;
|
||||
use std::ops;
|
||||
|
||||
use super::{NextPreferencesResult, STVError};
|
||||
|
||||
/// Return the denominator of the surplus fraction
|
||||
///
|
||||
/// Returns `None` if transferable ballots <= surplus (i.e. all transferable ballots are transferred at full value).
|
||||
fn calculate_surplus_denom<N: Number>(surplus: &N, result: &NextPreferencesResult<N>, transferable_ballots: &N, transferable_only: bool) -> Option<N>
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>
|
||||
{
|
||||
if transferable_only {
|
||||
if transferable_ballots > surplus {
|
||||
return Some(transferable_ballots.clone());
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
return Some(result.total_ballots.clone());
|
||||
}
|
||||
}
|
||||
|
||||
/// Distribute the surplus of a given candidate according to the random subset method, based on [STVOptions::surplus]
|
||||
pub fn distribute_surplus<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, elected_candidate: &'a Candidate) -> Result<(), STVError>
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Neg<Output=N>
|
||||
{
|
||||
state.title = StageKind::SurplusOf(elected_candidate);
|
||||
state.logger.log_literal(format!("Surplus of {} distributed.", elected_candidate.name));
|
||||
|
||||
let count_card = state.candidates.get_mut(elected_candidate).unwrap();
|
||||
let surplus = &count_card.votes - state.quota.as_ref().unwrap();
|
||||
|
||||
let mut votes;
|
||||
match opts.surplus {
|
||||
SurplusMethod::IHare => {
|
||||
// Inclusive
|
||||
votes = count_card.concat_parcels();
|
||||
}
|
||||
SurplusMethod::Hare => {
|
||||
// Exclusive
|
||||
// Should be safe to unwrap() - or else how did we get a quota!
|
||||
votes = count_card.parcels.pop().unwrap().votes;
|
||||
}
|
||||
_ => unreachable!()
|
||||
}
|
||||
|
||||
match opts.sample {
|
||||
SampleMethod::StratifyLR /*| SampleMethod::StratifyFloor*/ => {
|
||||
// Stratified by next available preference (round fractions according to largest remainders)
|
||||
let result = super::next_preferences(state, votes);
|
||||
|
||||
let transferable_ballots = &result.total_ballots - &result.exhausted.num_ballots;
|
||||
let surplus_denom = calculate_surplus_denom(&surplus, &result, &transferable_ballots, opts.transferable_only);
|
||||
let mut surplus_fraction;
|
||||
match &surplus_denom {
|
||||
Some(v) => {
|
||||
surplus_fraction = Some(surplus.clone() / v);
|
||||
|
||||
// Round down if requested
|
||||
if let Some(dps) = opts.round_surplus_fractions {
|
||||
surplus_fraction.as_mut().unwrap().floor_mut(dps);
|
||||
}
|
||||
|
||||
if opts.transferable_only {
|
||||
if &result.total_ballots - &result.exhausted.num_ballots == N::one() {
|
||||
state.logger.log_literal(format!("Examining 1 transferable ballot, with surplus fraction {:.dps2$}.", surplus_fraction.as_ref().unwrap(), dps2=max(opts.pp_decimals, 2)));
|
||||
} else {
|
||||
state.logger.log_literal(format!("Examining {:.0} transferable ballots, with surplus fraction {:.dps2$}.", &result.total_ballots - &result.exhausted.num_ballots, surplus_fraction.as_ref().unwrap(), dps2=max(opts.pp_decimals, 2)));
|
||||
}
|
||||
} else {
|
||||
if result.total_ballots == N::one() {
|
||||
state.logger.log_literal(format!("Examining 1 ballot, with surplus fraction {:.dps2$}.", surplus_fraction.as_ref().unwrap(), dps2=max(opts.pp_decimals, 2)));
|
||||
} else {
|
||||
state.logger.log_literal(format!("Examining {:.0} ballots, with surplus fraction {:.dps2$}.", result.total_ballots, surplus_fraction.as_ref().unwrap(), dps2=max(opts.pp_decimals, 2)));
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
surplus_fraction = None;
|
||||
|
||||
// This can only happen if --transferable-only
|
||||
if result.total_ballots == N::one() {
|
||||
state.logger.log_literal("Transferring 1 ballot at full value.".to_string());
|
||||
} else {
|
||||
state.logger.log_literal(format!("Transferring {:.0} ballots at full value.", result.total_ballots));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut candidate_transfers_remainders: HashMap<Option<&Candidate>, (N, N)> = HashMap::new(); // None -> exhausted pile
|
||||
|
||||
for (candidate, entry) in result.candidates.iter() {
|
||||
// Calculate votes to transfer
|
||||
let mut candidate_transfers;
|
||||
let remainder;
|
||||
match surplus_fraction {
|
||||
Some(_) => {
|
||||
match opts.sample {
|
||||
SampleMethod::StratifyLR => {
|
||||
// Incompatible with --round-surplus-fractions
|
||||
candidate_transfers = &entry.num_ballots * &surplus / surplus_denom.as_ref().unwrap();
|
||||
candidate_transfers.floor_mut(0);
|
||||
remainder = (&entry.num_ballots * &surplus) % surplus_denom.as_ref().unwrap();
|
||||
}
|
||||
/*SampleMethod::StratifyFloor => {
|
||||
match opts.round_surplus_fractions {
|
||||
Some(_) => {
|
||||
candidate_transfers = &entry.num_ballots * f;
|
||||
}
|
||||
None => {
|
||||
candidate_transfers = &entry.num_ballots * &surplus / surplus_denom.as_ref().unwrap();
|
||||
}
|
||||
}
|
||||
candidate_transfers.floor_mut(0);
|
||||
remainder = N::new();
|
||||
}*/
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// All ballots transferred
|
||||
candidate_transfers = entry.num_ballots.clone();
|
||||
remainder = N::new();
|
||||
}
|
||||
}
|
||||
|
||||
candidate_transfers_remainders.insert(Some(candidate), (candidate_transfers, remainder));
|
||||
}
|
||||
|
||||
// Calculate exhausted votes to transfer
|
||||
if !opts.transferable_only {
|
||||
let mut exhausted_transfers;
|
||||
let remainder;
|
||||
match opts.sample {
|
||||
SampleMethod::StratifyLR => {
|
||||
// Incompatible with --round-surplus-fractions
|
||||
exhausted_transfers = &result.exhausted.num_ballots * &surplus / surplus_denom.as_ref().unwrap();
|
||||
exhausted_transfers.floor_mut(0);
|
||||
remainder = (&result.exhausted.num_ballots * &surplus) % surplus_denom.as_ref().unwrap();
|
||||
}
|
||||
/*SampleMethod::StratifyFloor => {
|
||||
match opts.round_surplus_fractions {
|
||||
Some(_) => {
|
||||
exhausted_transfers = &result.exhausted.num_ballots * surplus_fraction.as_ref().unwrap();
|
||||
}
|
||||
None => {
|
||||
exhausted_transfers = &result.exhausted.num_ballots * &surplus / surplus_denom.as_ref().unwrap();
|
||||
}
|
||||
}
|
||||
exhausted_transfers.floor_mut(0);
|
||||
remainder = N::new();
|
||||
}*/
|
||||
_ => unreachable!()
|
||||
}
|
||||
|
||||
candidate_transfers_remainders.insert(None, (exhausted_transfers, remainder));
|
||||
}
|
||||
|
||||
if opts.sample == SampleMethod::StratifyLR {
|
||||
// Round remainders to remove loss by fraction
|
||||
let transferred = candidate_transfers_remainders.values().fold(N::new(), |mut acc, (t, _)| { acc += t; acc });
|
||||
let loss_fraction = &surplus - &transferred;
|
||||
if !loss_fraction.is_zero() && surplus_fraction.is_some() {
|
||||
let n_to_round: usize = format!("{:.0}", loss_fraction).parse().expect("Loss by fraction overflows usize");
|
||||
|
||||
let mut cands_by_remainder = candidate_transfers_remainders.keys().cloned().collect::<Vec<_>>();
|
||||
|
||||
// Sort by whole parts
|
||||
// Compare b to a to sort high-low
|
||||
cands_by_remainder.sort_unstable_by(|a, b| candidate_transfers_remainders[b].0.cmp(&candidate_transfers_remainders[a].0));
|
||||
|
||||
// Then sort by remainders
|
||||
cands_by_remainder.sort_by(|a, b| candidate_transfers_remainders[b].1.cmp(&candidate_transfers_remainders[a].1));
|
||||
|
||||
// Select top remainders
|
||||
let mut top_remainders = cands_by_remainder.iter().take(n_to_round).collect::<Vec<_>>();
|
||||
|
||||
// Check for tied remainders
|
||||
if candidate_transfers_remainders[top_remainders.last().unwrap()] == candidate_transfers_remainders[&cands_by_remainder[n_to_round]] {
|
||||
// Get the top entry
|
||||
let top_entry = &candidate_transfers_remainders[top_remainders.last().unwrap()];
|
||||
|
||||
// Separate out tied entries
|
||||
top_remainders = top_remainders.into_iter().filter(|c| &candidate_transfers_remainders[c] != top_entry).collect();
|
||||
let mut tied_top = cands_by_remainder.iter()
|
||||
.filter_map(|c| if let Some(c2) = c { if &candidate_transfers_remainders[c] == top_entry { Some(*c2) } else { None } } else { None })
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Get top entries by tie-breaking method
|
||||
for _ in 0..(n_to_round-top_remainders.len()) {
|
||||
let cand = super::choose_highest(state, opts, &tied_top, "Which fraction to round up?")?;
|
||||
tied_top.remove(tied_top.iter().position(|c| *c == cand).unwrap());
|
||||
top_remainders.push(cands_by_remainder.iter().find(|c| **c == Some(cand)).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
// Round up top remainders
|
||||
for candidate in top_remainders {
|
||||
candidate_transfers_remainders.get_mut(candidate).unwrap().0 += N::one();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut checksum = N::new();
|
||||
|
||||
for (candidate, entry) in result.candidates.into_iter() {
|
||||
// Credit transferred votes
|
||||
let candidate_transfers = &candidate_transfers_remainders[&Some(candidate)].0;
|
||||
let candidate_transfers_usize: usize = format!("{:.0}", candidate_transfers).parse().expect("Transfer overflows usize");
|
||||
|
||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||
count_card.transfer(candidate_transfers);
|
||||
checksum += candidate_transfers;
|
||||
|
||||
let parcel = Parcel {
|
||||
votes: entry.votes.into_iter().rev().take(candidate_transfers_usize).rev().collect(),
|
||||
value_fraction: N::one(),
|
||||
source_order: state.num_elected + state.num_excluded,
|
||||
};
|
||||
|
||||
count_card.parcels.push(parcel);
|
||||
}
|
||||
|
||||
// Credit exhausted votes
|
||||
let mut exhausted_transfers;
|
||||
if opts.transferable_only {
|
||||
if transferable_ballots > surplus {
|
||||
// No ballots exhaust
|
||||
exhausted_transfers = N::new();
|
||||
} else {
|
||||
exhausted_transfers = &surplus - &transferable_ballots;
|
||||
if let Some(dps) = opts.round_votes {
|
||||
exhausted_transfers.floor_mut(dps);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
exhausted_transfers = candidate_transfers_remainders.remove(&None).unwrap().0;
|
||||
}
|
||||
let exhausted_transfers_usize: usize = format!("{:.0}", exhausted_transfers).parse().expect("Transfer overflows usize");
|
||||
|
||||
state.exhausted.transfer(&exhausted_transfers);
|
||||
checksum += exhausted_transfers;
|
||||
|
||||
// Transfer exhausted votes
|
||||
let parcel = Parcel {
|
||||
votes: result.exhausted.votes.into_iter().rev().take(exhausted_transfers_usize).rev().collect(),
|
||||
value_fraction: N::one(),
|
||||
source_order: state.num_elected + state.num_excluded,
|
||||
};
|
||||
state.exhausted.parcels.push(parcel);
|
||||
|
||||
// Finalise candidate votes
|
||||
let count_card = state.candidates.get_mut(elected_candidate).unwrap();
|
||||
count_card.transfers = -&surplus;
|
||||
count_card.votes.assign(state.quota.as_ref().unwrap());
|
||||
checksum -= surplus;
|
||||
|
||||
// Update loss by fraction
|
||||
state.loss_fraction.transfer(&-checksum);
|
||||
}
|
||||
SampleMethod::ByOrder => {
|
||||
// Ballots by order
|
||||
// FIXME: This is untested
|
||||
|
||||
state.logger.log_literal(format!("Examining {:.0} ballots.", votes.len())); // votes.len() is total ballots as --normalise-ballots is required
|
||||
|
||||
// Transfer candidate votes
|
||||
while &state.candidates[elected_candidate].votes > state.quota.as_ref().unwrap() {
|
||||
match votes.pop() {
|
||||
Some(vote) => {
|
||||
// Transfer to next preference
|
||||
transfer_ballot(state, opts, elected_candidate, vote, opts.transferable_only)?;
|
||||
if state.num_elected == state.election.seats {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// We have run out of ballot papers
|
||||
// Remaining ballot papers exhaust
|
||||
|
||||
let surplus = &state.candidates[elected_candidate].votes - state.quota.as_ref().unwrap();
|
||||
state.exhausted.transfer(&surplus);
|
||||
state.candidates.get_mut(elected_candidate).unwrap().transfer(&-surplus);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SampleMethod::Cincinnati => {
|
||||
// Every nth-ballot (Cincinnati-style)
|
||||
|
||||
// Calculate skip value
|
||||
let total_ballots = votes.len();
|
||||
let mut skip_fraction = N::from(total_ballots) / &surplus;
|
||||
skip_fraction.round_mut(0);
|
||||
|
||||
state.logger.log_literal(format!("Examining {:.0} ballots, with skip value {:.0}.", total_ballots, skip_fraction));
|
||||
|
||||
// Number the votes
|
||||
let mut numbered_votes: HashMap<usize, Vote<N>> = HashMap::new();
|
||||
for (i, vote) in votes.into_iter().enumerate() {
|
||||
numbered_votes.insert(i, vote);
|
||||
}
|
||||
|
||||
// Transfer candidate votes
|
||||
let skip_value: usize = format!("{:.0}", skip_fraction).parse().expect("Skip value overflows usize");
|
||||
let mut iteration = 0;
|
||||
let mut index = skip_value - 1; // Subtract 1 as votes are 0-indexed
|
||||
|
||||
while &state.candidates[elected_candidate].votes > state.quota.as_ref().unwrap() {
|
||||
// Transfer one vote to next available preference
|
||||
match numbered_votes.remove(&index) {
|
||||
Some(vote) => {
|
||||
transfer_ballot(state, opts, elected_candidate, vote, opts.transferable_only)?;
|
||||
if state.num_elected == state.election.seats {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// We have run out of ballot papers
|
||||
// Remaining ballot papers exhaust
|
||||
let surplus = &state.candidates[elected_candidate].votes - state.quota.as_ref().unwrap();
|
||||
state.exhausted.transfer(&surplus);
|
||||
state.candidates.get_mut(elected_candidate).unwrap().transfer(&-surplus);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
index += skip_value;
|
||||
if index >= total_ballots {
|
||||
iteration += 1;
|
||||
index = iteration + skip_value - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let count_card = state.candidates.get_mut(elected_candidate).unwrap();
|
||||
count_card.finalised = true; // Mark surpluses as done
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
/// Transfer the given ballot paper to its next available preference, and check for candidates meeting the quota if --sample-per-ballot
|
||||
///
|
||||
/// If `ignore_nontransferable`, does nothing if --transferable-only and the ballot is nontransferable.
|
||||
fn transfer_ballot<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, source_candidate: &'a Candidate, mut vote: Vote<'a, N>, ignore_nontransferable: bool) -> Result<(), STVError> {
|
||||
// Get next preference
|
||||
let mut next_candidate = None;
|
||||
while let Some(preference) = vote.next_preference() {
|
||||
let candidate = &state.election.candidates[preference];
|
||||
let count_card = &state.candidates[candidate];
|
||||
|
||||
if let CandidateState::Hopeful | CandidateState::Guarded = count_card.state {
|
||||
next_candidate = Some(candidate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Have to structure like this to satisfy Rust's borrow checker
|
||||
if let Some(candidate) = next_candidate {
|
||||
// Available preference
|
||||
state.candidates.get_mut(source_candidate).unwrap().transfer(&-vote.ballot.orig_value.clone());
|
||||
|
||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||
count_card.transfer(&vote.ballot.orig_value);
|
||||
|
||||
match count_card.parcels.last_mut() {
|
||||
Some(parcel) => {
|
||||
if parcel.source_order == state.num_elected + state.num_excluded {
|
||||
parcel.votes.push(vote);
|
||||
} else {
|
||||
let parcel = Parcel {
|
||||
votes: vec![vote],
|
||||
value_fraction: N::one(),
|
||||
source_order: state.num_elected + state.num_excluded,
|
||||
};
|
||||
count_card.parcels.push(parcel);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let parcel = Parcel {
|
||||
votes: vec![vote],
|
||||
value_fraction: N::one(),
|
||||
source_order: state.num_elected + state.num_excluded,
|
||||
};
|
||||
count_card.parcels.push(parcel);
|
||||
}
|
||||
}
|
||||
|
||||
if opts.sample_per_ballot {
|
||||
super::elect_hopefuls(state, opts, true)?;
|
||||
}
|
||||
} else {
|
||||
// Exhausted
|
||||
if opts.transferable_only && ignore_nontransferable {
|
||||
// Another ballot paper required
|
||||
} else {
|
||||
state.candidates.get_mut(source_candidate).unwrap().transfer(&-vote.ballot.orig_value.clone());
|
||||
state.exhausted.transfer(&vote.ballot.orig_value);
|
||||
|
||||
match state.exhausted.parcels.last_mut() {
|
||||
Some(parcel) => {
|
||||
if parcel.source_order == state.num_elected + state.num_excluded {
|
||||
parcel.votes.push(vote);
|
||||
} else {
|
||||
let parcel = Parcel {
|
||||
votes: vec![vote],
|
||||
value_fraction: N::one(),
|
||||
source_order: state.num_elected + state.num_excluded,
|
||||
};
|
||||
state.exhausted.parcels.push(parcel);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let parcel = Parcel {
|
||||
votes: vec![vote],
|
||||
value_fraction: N::one(),
|
||||
source_order: state.num_elected + state.num_excluded,
|
||||
};
|
||||
state.exhausted.parcels.push(parcel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
/// Perform one stage of a candidate exclusion according to the random subset method
|
||||
pub fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>) -> Result<(), STVError>
|
||||
where
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
{
|
||||
// Used to give bulk excluded candidate the same order_elected
|
||||
let order_excluded = state.num_excluded + 1;
|
||||
|
||||
for excluded_candidate in excluded_candidates.iter() {
|
||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||
|
||||
// Rust borrow checker is unhappy if we try to put this in exclude_hopefuls ??!
|
||||
if count_card.state != CandidateState::Excluded {
|
||||
count_card.state = CandidateState::Excluded;
|
||||
state.num_excluded += 1;
|
||||
count_card.order_elected = -(order_excluded as isize);
|
||||
|
||||
constraints::update_constraints(state, opts);
|
||||
}
|
||||
}
|
||||
|
||||
// Count votes
|
||||
let mut total_ballots: usize = 0;
|
||||
for excluded_candidate in excluded_candidates.iter() {
|
||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||
total_ballots = count_card.parcels.iter()
|
||||
.fold(total_ballots, |acc, p| acc + p.votes.len());
|
||||
}
|
||||
|
||||
if total_ballots == 1 {
|
||||
state.logger.log_literal("Transferring 1 ballot.".to_string());
|
||||
} else {
|
||||
state.logger.log_literal(format!("Transferring {:.0} ballots.", total_ballots));
|
||||
}
|
||||
|
||||
// Transfer votes
|
||||
for excluded_candidate in excluded_candidates.iter() {
|
||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||
let votes = count_card.concat_parcels();
|
||||
|
||||
for vote in votes {
|
||||
transfer_ballot(state, opts, excluded_candidate, vote, false)?;
|
||||
if state.num_elected == state.election.seats {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||
count_card.finalised = true;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
360
src/stv/wasm.rs
360
src/stv/wasm.rs
|
@ -1,5 +1,5 @@
|
|||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021–2023 Lee Yingtong Li (RunasSudo)
|
||||
* 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
|
||||
|
@ -16,15 +16,11 @@
|
|||
*/
|
||||
|
||||
#![allow(rustdoc::private_intra_doc_links)]
|
||||
#![allow(unused_unsafe)] // Confuses cargo check
|
||||
|
||||
use crate::constraints::{self, Constraints};
|
||||
use crate::constraints::Constraints;
|
||||
use crate::election::{CandidateState, CountState, Election};
|
||||
//use crate::numbers::{DynNum, Fixed, GuardedFixed, NativeFloat64, Number, NumKind, Rational};
|
||||
use crate::numbers::{Fixed, GuardedFixed, NativeFloat64, Number, Rational};
|
||||
use crate::parser::blt;
|
||||
use crate::numbers::{DynNum, Fixed, GuardedFixed, NumKind, Number};
|
||||
use crate::stv;
|
||||
use crate::ties;
|
||||
|
||||
extern crate console_error_panic_hook;
|
||||
|
||||
|
@ -33,36 +29,22 @@ use wasm_bindgen::{JsValue, prelude::wasm_bindgen};
|
|||
|
||||
use std::cmp::max;
|
||||
|
||||
// Error handling
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
fn wasm_error(message: String);
|
||||
}
|
||||
|
||||
macro_rules! wasm_error {
|
||||
($type:expr, $err:expr) => { {
|
||||
unsafe { wasm_error(format!("{}: {}", $type, $err)); }
|
||||
panic!("{}: {}", $type, $err);
|
||||
} }
|
||||
}
|
||||
|
||||
// Init
|
||||
|
||||
// Wrapper for [DynNum::set_kind]
|
||||
//#[wasm_bindgen]
|
||||
//pub fn dynnum_set_kind(kind: NumKind) {
|
||||
// DynNum::set_kind(kind);
|
||||
//}
|
||||
/// Wrapper for [DynNum::set_kind]
|
||||
#[wasm_bindgen]
|
||||
pub fn dynnum_set_kind(kind: NumKind) {
|
||||
DynNum::set_kind(kind);
|
||||
}
|
||||
|
||||
/// Wrapper for [Fixed::set_dps]
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[wasm_bindgen]
|
||||
pub fn fixed_set_dps(dps: usize) {
|
||||
Fixed::set_dps(dps);
|
||||
}
|
||||
|
||||
/// Wrapper for [GuardedFixed::set_dps]
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[wasm_bindgen]
|
||||
pub fn gfixed_set_dps(dps: usize) {
|
||||
GuardedFixed::set_dps(dps);
|
||||
}
|
||||
|
@ -73,106 +55,90 @@ macro_rules! impl_type {
|
|||
($type:ident) => { paste::item! {
|
||||
// Counting
|
||||
|
||||
/// Wrapper for [blt::parse_iterator]
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
/// Wrapper for [Election::from_blt]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<election_from_blt_$type>](text: String) -> [<Election$type>] {
|
||||
// Install panic! hook
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let election: Election<$type> = match blt::parse_iterator(text.chars().peekable()) {
|
||||
Ok(e) => e,
|
||||
Err(err) => wasm_error!("Syntax Error", err),
|
||||
};
|
||||
let election: Election<$type> = Election::from_blt(text.lines().map(|s| s.to_string()).into_iter());
|
||||
return [<Election$type>](election);
|
||||
}
|
||||
|
||||
/// Call [Constraints::from_con] and set [Election::constraints]
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
/// Wrapper for [Election::normalise_ballots]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<election_load_constraints_$type>](election: &mut [<Election$type>], text: String, opts: &STVOptions) {
|
||||
election.0.constraints = match Constraints::from_con(text.lines()) {
|
||||
Ok(c) => Some(c),
|
||||
Err(err) => wasm_error!("Constraint Syntax Error", err),
|
||||
};
|
||||
|
||||
// Validate constraints
|
||||
if let Err(err) = election.0.constraints.as_ref().unwrap().validate_constraints(election.0.candidates.len(), opts.0.constraint_mode) {
|
||||
wasm_error!("Constraint Validation Error", err);
|
||||
}
|
||||
|
||||
// Add dummy candidates if required
|
||||
if opts.0.constraint_mode == stv::ConstraintMode::RepeatCount {
|
||||
constraints::init_repeat_count(&mut election.0);
|
||||
}
|
||||
pub fn [<election_normalise_ballots_$type>](election: &mut [<Election$type>]) {
|
||||
election.0.normalise_ballots();
|
||||
}
|
||||
|
||||
/// Wrapper for [stv::preprocess_election]
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
/// Call [Constraints::from_con] and set [Election::constraints]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<preprocess_election_$type>](election: &mut [<Election$type>], opts: &STVOptions) {
|
||||
stv::preprocess_election(&mut election.0, &opts.0);
|
||||
pub fn [<election_load_constraints_$type>](election: &mut [<Election$type>], text: String) {
|
||||
election.0.constraints = Some(Constraints::from_con(text.lines().map(|s| s.to_string()).into_iter()));
|
||||
}
|
||||
|
||||
/// Wrapper for [stv::count_init]
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<count_init_$type>](state: &mut [<CountState$type>], opts: &STVOptions) {
|
||||
pub fn [<count_init_$type>](state: &mut [<CountState$type>], opts: &STVOptions) -> Result<bool, JsValue> {
|
||||
match stv::count_init(&mut state.0, opts.as_static()) {
|
||||
Ok(_) => (),
|
||||
Err(err) => wasm_error!("Error", err),
|
||||
Ok(v) => Ok(v),
|
||||
Err(e) => Err(e.name().into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper for [stv::count_one_stage]
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<count_one_stage_$type>](state: &mut [<CountState$type>], opts: &STVOptions) -> bool {
|
||||
pub fn [<count_one_stage_$type>](state: &mut [<CountState$type>], opts: &STVOptions) -> Result<bool, JsValue> {
|
||||
match stv::count_one_stage::<[<$type>]>(&mut state.0, &opts.0) {
|
||||
Ok(v) => v,
|
||||
Err(err) => wasm_error!("Error", err),
|
||||
Ok(v) => Ok(v),
|
||||
Err(e) => Err(e.name().into()),
|
||||
}
|
||||
}
|
||||
|
||||
// Reporting
|
||||
|
||||
/// Wrapper for [init_results_table]
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<init_results_table_$type>](election: &[<Election$type>], opts: &STVOptions, report_style: &str) -> String {
|
||||
return init_results_table(&election.0, &opts.0, report_style);
|
||||
pub fn [<init_results_table_$type>](election: &[<Election$type>], opts: &STVOptions) -> String {
|
||||
return init_results_table(&election.0, &opts.0);
|
||||
}
|
||||
|
||||
/// Wrapper for [describe_count]
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<describe_count_$type>](filename: String, election: &[<Election$type>], opts: &STVOptions) -> String {
|
||||
return stv::html::describe_count(&filename, &election.0, &opts.0);
|
||||
return describe_count(filename, &election.0, &opts.0);
|
||||
}
|
||||
|
||||
/// Wrapper for [update_results_table]
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<update_results_table_$type>](stage_num: usize, state: &[<CountState$type>], opts: &STVOptions, report_style: &str) -> Array {
|
||||
return update_results_table(stage_num, &state.0, &opts.0, report_style);
|
||||
pub fn [<update_results_table_$type>](stage_num: usize, state: &[<CountState$type>], opts: &STVOptions) -> Array {
|
||||
return update_results_table(stage_num, &state.0, &opts.0);
|
||||
}
|
||||
|
||||
/// Wrapper for [update_stage_comments]
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<update_stage_comments_$type>](state: &[<CountState$type>], stage_num: usize) -> String {
|
||||
return update_stage_comments(&state.0, stage_num);
|
||||
pub fn [<update_stage_comments_$type>](state: &[<CountState$type>]) -> String {
|
||||
return update_stage_comments(&state.0);
|
||||
}
|
||||
|
||||
/// Wrapper for [finalise_results_table]
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<finalise_results_table_$type>](state: &[<CountState$type>], report_style: &str) -> Array {
|
||||
return finalise_results_table(&state.0, report_style);
|
||||
pub fn [<finalise_results_table_$type>](state: &[<CountState$type>]) -> Array {
|
||||
return finalise_results_table(&state.0);
|
||||
}
|
||||
|
||||
/// Wrapper for [final_result_summary]
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<final_result_summary_$type>](state: &[<CountState$type>], opts: &STVOptions) -> String {
|
||||
return final_result_summary(&state.0, &opts.0);
|
||||
|
@ -184,28 +150,23 @@ macro_rules! impl_type {
|
|||
///
|
||||
/// This is required as `&'static` cannot be specified in wasm-bindgen: see [issue 1187](https://github.com/rustwasm/wasm-bindgen/issues/1187).
|
||||
///
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[wasm_bindgen]
|
||||
pub struct [<CountState$type>](CountState<'static, $type>);
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[wasm_bindgen]
|
||||
impl [<CountState$type>] {
|
||||
/// Create a new [CountState] wrapper
|
||||
pub fn new(election: &[<Election$type>]) -> Self {
|
||||
return [<CountState$type>](CountState::new(election.as_static()));
|
||||
}
|
||||
|
||||
/// Call [render_text](crate::stv::gregory::TransferTable::render_text) (as HTML) on [CountState::transfer_table]
|
||||
pub fn transfer_table_render_html(&self, opts: &STVOptions) -> Option<String> {
|
||||
return self.0.transfer_table.as_ref().map(|tt| tt.render_text(&opts.0));
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper for [Election]
|
||||
///
|
||||
/// This is required as `&'static` cannot be specified in wasm-bindgen: see [issue 1187](https://github.com/rustwasm/wasm-bindgen/issues/1187).
|
||||
///
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[wasm_bindgen]
|
||||
pub struct [<Election$type>](Election<$type>);
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[wasm_bindgen]
|
||||
impl [<Election$type>] {
|
||||
/// Return [Election::seats]
|
||||
pub fn seats(&self) -> usize { self.0.seats }
|
||||
|
@ -225,26 +186,28 @@ macro_rules! impl_type {
|
|||
}}
|
||||
}
|
||||
|
||||
//impl_type!(DynNum);
|
||||
impl_type!(Fixed);
|
||||
impl_type!(GuardedFixed);
|
||||
impl_type!(NativeFloat64);
|
||||
impl_type!(Rational);
|
||||
//impl_type!(Fixed);
|
||||
//impl_type!(GuardedFixed);
|
||||
//impl_type!(NativeFloat64);
|
||||
//impl_type!(Rational);
|
||||
|
||||
impl_type!(DynNum);
|
||||
|
||||
/// Wrapper for [stv::STVOptions]
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[wasm_bindgen]
|
||||
pub struct STVOptions(stv::STVOptions);
|
||||
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[wasm_bindgen]
|
||||
impl STVOptions {
|
||||
/// Wrapper for [stv::STVOptions::new]
|
||||
pub fn new(
|
||||
round_surplus_fractions: Option<usize>,
|
||||
round_values: Option<usize>,
|
||||
round_tvs: Option<usize>,
|
||||
round_weights: Option<usize>,
|
||||
round_votes: Option<usize>,
|
||||
round_quota: Option<usize>,
|
||||
round_subtransfers: &str,
|
||||
meek_surplus_tolerance: String,
|
||||
sum_surplus_transfers: &str,
|
||||
meek_surplus_tolerance: &str,
|
||||
normalise_ballots: bool,
|
||||
quota: &str,
|
||||
quota_criterion: &str,
|
||||
quota_mode: &str,
|
||||
|
@ -252,59 +215,48 @@ impl STVOptions {
|
|||
random_seed: String,
|
||||
surplus: &str,
|
||||
surplus_order: &str,
|
||||
papers: &str,
|
||||
transferable_only: bool,
|
||||
exclusion: &str,
|
||||
meek_nz_exclusion: bool,
|
||||
sample: &str,
|
||||
sample_per_ballot: bool,
|
||||
early_bulk_elect: bool,
|
||||
bulk_exclude: bool,
|
||||
defer_surpluses: bool,
|
||||
immediate_elect: bool,
|
||||
min_threshold: String,
|
||||
meek_immediate_elect: bool,
|
||||
constraints_path: Option<String>,
|
||||
constraint_mode: &str,
|
||||
pp_decimals: usize,
|
||||
) -> Self {
|
||||
Self(stv::STVOptions::new(
|
||||
round_surplus_fractions,
|
||||
round_values,
|
||||
round_tvs,
|
||||
round_weights,
|
||||
round_votes,
|
||||
round_quota,
|
||||
round_subtransfers.into(),
|
||||
sum_surplus_transfers,
|
||||
meek_surplus_tolerance,
|
||||
quota.into(),
|
||||
quota_criterion.into(),
|
||||
quota_mode.into(),
|
||||
ties::from_strs(ties.iter().map(|v| v.as_string().unwrap()).collect(), Some(random_seed)),
|
||||
surplus.into(),
|
||||
surplus_order.into(),
|
||||
if papers == "transferable" || papers == "subtract_nontransferable" { true } else { false },
|
||||
if papers == "assume_progress_total" || papers == "subtract_nontransferable" { true } else { false },
|
||||
exclusion.into(),
|
||||
normalise_ballots,
|
||||
quota,
|
||||
quota_criterion,
|
||||
quota_mode,
|
||||
&ties.iter().map(|v| v.as_string().unwrap()).collect(),
|
||||
&Some(random_seed),
|
||||
surplus,
|
||||
surplus_order,
|
||||
transferable_only,
|
||||
exclusion,
|
||||
meek_nz_exclusion,
|
||||
sample.into(),
|
||||
sample_per_ballot,
|
||||
early_bulk_elect,
|
||||
bulk_exclude,
|
||||
defer_surpluses,
|
||||
immediate_elect,
|
||||
min_threshold,
|
||||
constraints_path,
|
||||
constraint_mode.into(),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
meek_immediate_elect,
|
||||
constraints_path.as_deref(),
|
||||
constraint_mode,
|
||||
pp_decimals,
|
||||
))
|
||||
}
|
||||
|
||||
/// Wrapper for [stv::STVOptions::validate]
|
||||
pub fn validate(&self) {
|
||||
match self.0.validate() {
|
||||
Ok(_) => {}
|
||||
Err(err) => { wasm_error!("Error", err) }
|
||||
}
|
||||
self.0.validate();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -324,38 +276,133 @@ impl STVOptions {
|
|||
|
||||
// Reporting
|
||||
|
||||
/// Generate the lead-in description of the count in HTML
|
||||
fn describe_count<N: Number>(filename: String, election: &Election<N>, opts: &stv::STVOptions) -> String {
|
||||
let mut result = String::from("<p>Count computed by OpenTally (revision ");
|
||||
result.push_str(crate::VERSION);
|
||||
let total_ballots = election.ballots.iter().fold(N::zero(), |acc, b| { acc + &b.orig_value });
|
||||
result.push_str(&format!(r#"). Read {:.0} ballots from ‘{}’ for election ‘{}’. There are {} candidates for {} vacancies. "#, total_ballots, filename, election.name, election.candidates.len(), election.seats));
|
||||
|
||||
let opts_str = opts.describe::<N>();
|
||||
if opts_str.len() > 0 {
|
||||
result.push_str(&format!(r#"Counting using options <span style="font-family: monospace;">{}</span>.</p>"#, opts_str))
|
||||
} else {
|
||||
result.push_str(r#"Counting using default options.</p>"#);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Generate the first column of the HTML results table
|
||||
pub fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions, report_style: &str) -> String {
|
||||
return stv::html::init_results_table(election, opts, report_style).join("");
|
||||
fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions) -> String {
|
||||
let mut result = String::from(r#"<tr class="stage-no"><td rowspan="3"></td></tr><tr class="stage-kind"></tr><tr class="stage-comment"></tr>"#);
|
||||
for candidate in election.candidates.iter() {
|
||||
result.push_str(&format!(r#"<tr class="candidate transfers"><td rowspan="2">{}</td></tr><tr class="candidate votes"></tr>"#, candidate.name));
|
||||
}
|
||||
result.push_str(r#"<tr class="info transfers"><td rowspan="2">Exhausted</td></tr><tr class="info votes"></tr><tr class="info transfers"><td rowspan="2">Loss by fraction</td></tr><tr class="info votes"></tr><tr class="info transfers"><td>Total</td></tr><tr class="info transfers"><td>Quota</td></tr>"#);
|
||||
if opts.quota_mode == stv::QuotaMode::ERS97 {
|
||||
result.push_str(r#"<tr class="info transfers"><td>Vote required for election</td></tr>"#);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Generate subsequent columns of the HTML results table
|
||||
pub fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts: &stv::STVOptions, report_style: &str) -> Array {
|
||||
return stv::html::update_results_table(stage_num, state, opts, report_style)
|
||||
.into_iter()
|
||||
.map(|s| JsValue::from(s))
|
||||
.collect();
|
||||
fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts: &stv::STVOptions) -> Array {
|
||||
let result = Array::new();
|
||||
|
||||
// Insert borders to left of new exclusions in Wright STV
|
||||
let mut tdclasses1 = "";
|
||||
let mut tdclasses2 = "";
|
||||
if opts.exclusion == stv::ExclusionMethod::Wright && state.kind == Some("Exclusion of") {
|
||||
tdclasses1 = r#" class="blw""#;
|
||||
tdclasses2 = r#"blw "#;
|
||||
}
|
||||
|
||||
result.push(&format!(r#"<td{}>{}</td>"#, tdclasses1, stage_num).into());
|
||||
result.push(&format!(r#"<td{}>{}</td>"#, tdclasses1, state.kind.unwrap_or("")).into());
|
||||
result.push(&format!(r#"<td{}>{}</td>"#, tdclasses1, state.title).into());
|
||||
for candidate in state.election.candidates.iter() {
|
||||
let count_card = &state.candidates[candidate];
|
||||
match count_card.state {
|
||||
CandidateState::Hopeful | CandidateState::Guarded => {
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
CandidateState::Elected => {
|
||||
result.push(&format!(r#"<td class="{}count elected">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count elected">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
CandidateState::Doomed => {
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
CandidateState::Withdrawn => {
|
||||
result.push(&format!(r#"<td class="{}count excluded"></td>"#, tdclasses2).into());
|
||||
result.push(&format!(r#"<td class="{}count excluded">WD</td>"#, tdclasses2).into());
|
||||
}
|
||||
CandidateState::Excluded => {
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||
if count_card.votes.is_zero() {
|
||||
result.push(&format!(r#"<td class="{}count excluded">Ex</td>"#, tdclasses2).into());
|
||||
} else {
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.exhausted.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.exhausted.votes, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.loss_fraction.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.loss_fraction.votes, opts.pp_decimals)).into());
|
||||
|
||||
// Calculate total votes
|
||||
let mut total_vote = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes });
|
||||
total_vote += &state.exhausted.votes;
|
||||
total_vote += &state.loss_fraction.votes;
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&total_vote, opts.pp_decimals)).into());
|
||||
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into());
|
||||
if opts.quota_mode == stv::QuotaMode::ERS97 {
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(state.vote_required_election.as_ref().unwrap(), opts.pp_decimals)).into());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Get the comment for the current stage
|
||||
pub fn update_stage_comments<N: Number>(state: &CountState<N>, stage_num: usize) -> String {
|
||||
let mut comments = state.logger.render().join(" ");
|
||||
if state.transfer_table.is_some() {
|
||||
comments.push_str(&format!(r##" <a href="#" class="detailedTransfersLink" onclick="viewDetailedTransfers({});return false;">[View detailed transfers]</a>"##, stage_num));
|
||||
}
|
||||
return comments;
|
||||
fn update_stage_comments<N: Number>(state: &CountState<N>) -> String {
|
||||
return state.logger.render().join(" ");
|
||||
}
|
||||
|
||||
/// Generate the final column of the HTML results table
|
||||
pub fn finalise_results_table<N: Number>(state: &CountState<N>, report_style: &str) -> Array {
|
||||
return stv::html::finalise_results_table(state, report_style)
|
||||
.into_iter()
|
||||
.map(|s| JsValue::from(s))
|
||||
.collect();
|
||||
fn finalise_results_table<N: Number>(state: &CountState<N>) -> Array {
|
||||
let result = Array::new();
|
||||
|
||||
// Header rows
|
||||
result.push(&r#"<td rowspan="3"></td>"#.into());
|
||||
result.push(&"".into());
|
||||
result.push(&"".into());
|
||||
|
||||
// Candidate states
|
||||
for candidate in state.election.candidates.iter() {
|
||||
let count_card = &state.candidates[candidate];
|
||||
if count_card.state == stv::CandidateState::Elected {
|
||||
result.push(&format!(r#"<td rowspan="2" class="bb elected">ELECTED {}</td>"#, count_card.order_elected).into());
|
||||
} else if count_card.state == stv::CandidateState::Excluded {
|
||||
result.push(&format!(r#"<td rowspan="2" class="bb excluded">Excluded {}</td>"#, -count_card.order_elected).into());
|
||||
} else if count_card.state == stv::CandidateState::Withdrawn {
|
||||
result.push(&r#"<td rowspan="2" class="bb excluded">Withdrawn</td>"#.into());
|
||||
} else {
|
||||
result.push(&r#"<td rowspan="2" class="bb"></td>"#.into());
|
||||
}
|
||||
result.push(&"".into());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Generate the final lead-out text summarising the result of the election
|
||||
pub fn final_result_summary<N: Number>(state: &CountState<N>, opts: &stv::STVOptions) -> String {
|
||||
fn final_result_summary<N: Number>(state: &CountState<N>, opts: &stv::STVOptions) -> String {
|
||||
let mut result = String::from("<p>Count complete. The winning candidates are, in order of election:</p><ol>");
|
||||
|
||||
let mut winners = Vec::new();
|
||||
|
@ -377,3 +424,22 @@ pub fn final_result_summary<N: Number>(state: &CountState<N>, opts: &stv::STVOpt
|
|||
result.push_str("</ol>");
|
||||
return result;
|
||||
}
|
||||
|
||||
/// HTML pretty-print the number to the specified decimal places
|
||||
fn pp<N: Number>(n: &N, dps: usize) -> String {
|
||||
if n.is_zero() {
|
||||
return "".to_string();
|
||||
}
|
||||
|
||||
let mut raw = format!("{:.dps$}", n, dps=dps);
|
||||
if raw.contains('.') {
|
||||
raw = raw.replacen(".", ".<sup>", 1);
|
||||
raw.push_str("</sup>");
|
||||
}
|
||||
|
||||
if raw.starts_with('-') {
|
||||
raw = raw.replacen("-", "−", 1);
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
|
170
src/ties.rs
170
src/ties.rs
|
@ -1,5 +1,5 @@
|
|||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021–2022 Lee Yingtong Li (RunasSudo)
|
||||
* 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
|
||||
|
@ -18,7 +18,7 @@
|
|||
use crate::election::{Candidate, CountState};
|
||||
use crate::logger::smart_join;
|
||||
use crate::numbers::Number;
|
||||
use crate::stv::{STVError, STVOptions};
|
||||
use crate::stv::STVError;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
@ -27,7 +27,7 @@ use wasm_bindgen::prelude::wasm_bindgen;
|
|||
use std::io::{stdin, stdout, Write};
|
||||
|
||||
/// Strategy for breaking ties
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[derive(PartialEq)]
|
||||
pub enum TieStrategy {
|
||||
/// Break ties according to the candidate who first had more/fewer votes
|
||||
Forwards,
|
||||
|
@ -39,17 +39,6 @@ pub enum TieStrategy {
|
|||
Prompt,
|
||||
}
|
||||
|
||||
/// Get a [Vec] of [TieStrategy] based on string representations
|
||||
pub fn from_strs<S: AsRef<str>>(strs: Vec<S>, mut random_seed: Option<String>) -> Vec<TieStrategy> {
|
||||
strs.into_iter().map(|t| match t.as_ref() {
|
||||
"forwards" => TieStrategy::Forwards,
|
||||
"backwards" => TieStrategy::Backwards,
|
||||
"random" => TieStrategy::Random(random_seed.take().expect("Must provide a --random-seed if using --ties random")),
|
||||
"prompt" => TieStrategy::Prompt,
|
||||
_ => panic!("Invalid --ties"),
|
||||
}).collect()
|
||||
}
|
||||
|
||||
impl TieStrategy {
|
||||
/// Convert to CLI argument representation
|
||||
pub fn describe(&self) -> String {
|
||||
|
@ -64,43 +53,33 @@ impl TieStrategy {
|
|||
/// Break a tie between the given candidates, selecting the highest candidate
|
||||
///
|
||||
/// The given candidates are assumed to be tied in this round
|
||||
pub fn choose_highest<'c, N: Number>(&self, state: &mut CountState<N>, opts: &STVOptions, candidates: &[&'c Candidate], prompt_text: &str) -> Result<&'c Candidate, STVError> {
|
||||
pub fn choose_highest<'c, N: Number>(&self, state: &mut CountState<N>, candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> {
|
||||
match self {
|
||||
Self::Forwards => {
|
||||
match &state.forwards_tiebreak {
|
||||
Some(tb) => {
|
||||
let mut candidates: Vec<&Candidate> = candidates.iter().copied().collect();
|
||||
// Compare b to a to sort high-to-low
|
||||
candidates.sort_unstable_by(|a, b| tb[b].cmp(&tb[a]));
|
||||
if tb[candidates[0]] == tb[candidates[1]] {
|
||||
return Err(STVError::UnresolvedTie);
|
||||
} else {
|
||||
state.logger.log_literal(format!("Tie between {} broken forwards.", smart_join(&candidates.iter().map(|c| c.name.as_str()).collect())));
|
||||
return Ok(candidates[0]);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// First stage
|
||||
return Err(STVError::UnresolvedTie);
|
||||
}
|
||||
let mut candidates = candidates.clone();
|
||||
candidates.sort_unstable_by(|a, b|
|
||||
// Compare b to a to sort high-to-low
|
||||
state.forwards_tiebreak.as_ref().unwrap()[b]
|
||||
.cmp(&state.forwards_tiebreak.as_ref().unwrap()[a])
|
||||
);
|
||||
if state.forwards_tiebreak.as_ref().unwrap()[candidates[0]] == state.forwards_tiebreak.as_ref().unwrap()[candidates[1]] {
|
||||
return Err(STVError::UnresolvedTie);
|
||||
} else {
|
||||
state.logger.log_literal(format!("Tie between {} broken forwards.", smart_join(&candidates.iter().map(|c| c.name.as_str()).collect())));
|
||||
return Ok(candidates[0]);
|
||||
}
|
||||
}
|
||||
Self::Backwards => {
|
||||
match &state.backwards_tiebreak {
|
||||
Some(tb) => {
|
||||
let mut candidates: Vec<&Candidate> = candidates.iter().copied().collect();
|
||||
candidates.sort_unstable_by(|a, b| tb[b].cmp(&tb[a]));
|
||||
if tb[candidates[0]] == tb[candidates[1]] {
|
||||
return Err(STVError::UnresolvedTie);
|
||||
} else {
|
||||
state.logger.log_literal(format!("Tie between {} broken backwards.", smart_join(&candidates.iter().map(|c| c.name.as_str()).collect())));
|
||||
return Ok(candidates[0]);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// First stage
|
||||
return Err(STVError::UnresolvedTie);
|
||||
}
|
||||
let mut candidates = candidates.clone();
|
||||
candidates.sort_unstable_by(|a, b|
|
||||
state.backwards_tiebreak.as_ref().unwrap()[b]
|
||||
.cmp(&state.backwards_tiebreak.as_ref().unwrap()[a])
|
||||
);
|
||||
if state.backwards_tiebreak.as_ref().unwrap()[candidates[0]] == state.backwards_tiebreak.as_ref().unwrap()[candidates[1]] {
|
||||
return Err(STVError::UnresolvedTie);
|
||||
} else {
|
||||
state.logger.log_literal(format!("Tie between {} broken backwards.", smart_join(&candidates.iter().map(|c| c.name.as_str()).collect())));
|
||||
return Ok(candidates[0]);
|
||||
}
|
||||
}
|
||||
Self::Random(_) => {
|
||||
|
@ -108,7 +87,7 @@ impl TieStrategy {
|
|||
return Ok(candidates[state.random.as_mut().unwrap().next(candidates.len())]);
|
||||
}
|
||||
Self::Prompt => {
|
||||
match prompt(state, opts, candidates, prompt_text) {
|
||||
match prompt(candidates) {
|
||||
Ok(c) => {
|
||||
state.logger.log_literal(format!("Tie between {} broken by manual intervention.", smart_join(&candidates.iter().map(|c| c.name.as_str()).collect())));
|
||||
return Ok(c);
|
||||
|
@ -122,10 +101,10 @@ impl TieStrategy {
|
|||
/// Break a tie between the given candidates, selecting the lowest candidate
|
||||
///
|
||||
/// The given candidates are assumed to be tied in this round
|
||||
pub fn choose_lowest<'c, N: Number>(&self, state: &mut CountState<N>, opts: &STVOptions, candidates: &[&'c Candidate], prompt_text: &str) -> Result<&'c Candidate, STVError> {
|
||||
pub fn choose_lowest<'c, N: Number>(&self, state: &mut CountState<N>, candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> {
|
||||
match self {
|
||||
Self::Forwards => {
|
||||
let mut candidates: Vec<&Candidate> = candidates.iter().copied().collect();
|
||||
let mut candidates = candidates.clone();
|
||||
candidates.sort_unstable_by(|a, b|
|
||||
state.forwards_tiebreak.as_ref().unwrap()[a]
|
||||
.cmp(&state.forwards_tiebreak.as_ref().unwrap()[b])
|
||||
|
@ -138,7 +117,7 @@ impl TieStrategy {
|
|||
}
|
||||
}
|
||||
Self::Backwards => {
|
||||
let mut candidates: Vec<&Candidate> = candidates.iter().copied().collect();
|
||||
let mut candidates = candidates.clone();
|
||||
candidates.sort_unstable_by(|a, b|
|
||||
state.backwards_tiebreak.as_ref().unwrap()[a]
|
||||
.cmp(&state.backwards_tiebreak.as_ref().unwrap()[b])
|
||||
|
@ -151,17 +130,17 @@ impl TieStrategy {
|
|||
}
|
||||
}
|
||||
Self::Random(_seed) => {
|
||||
return self.choose_highest(state, opts, candidates, prompt_text);
|
||||
return self.choose_highest(state, candidates);
|
||||
}
|
||||
Self::Prompt => {
|
||||
return self.choose_highest(state, opts, candidates, prompt_text);
|
||||
return self.choose_highest(state, candidates);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return all maximal items according to the given key
|
||||
pub fn multiple_max_by<E: Copy, K, C: Ord>(items: &[E], key: K) -> Vec<E>
|
||||
pub fn multiple_max_by<E: Copy, K, C: Ord>(items: &Vec<E>, key: K) -> Vec<E>
|
||||
where
|
||||
K: Fn(&E) -> C
|
||||
{
|
||||
|
@ -190,7 +169,7 @@ where
|
|||
}
|
||||
|
||||
/// Return all minimal items according to the given key
|
||||
pub fn multiple_min_by<E: Copy, K, C: Ord>(items: &[E], key: K) -> Vec<E>
|
||||
pub fn multiple_min_by<E: Copy, K, C: Ord>(items: &Vec<E>, key: K) -> Vec<E>
|
||||
where
|
||||
K: Fn(&E) -> C
|
||||
{
|
||||
|
@ -220,29 +199,14 @@ where
|
|||
|
||||
/// Prompt the candidate for input, depending on CLI or WebAssembly target
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn prompt<'c, N: Number>(state: &CountState<N>, opts: &STVOptions, candidates: &[&'c Candidate], prompt_text: &str) -> Result<&'c Candidate, STVError> {
|
||||
// Show intrastage progress if required
|
||||
if !state.logger.entries.is_empty() {
|
||||
// Print stage details
|
||||
println!("Tie during: {}", state.title);
|
||||
println!("{}", state.logger.render().join(" "));
|
||||
|
||||
// Print candidates
|
||||
print!("{}", state.describe_candidates(opts));
|
||||
|
||||
// Print summary rows
|
||||
print!("{}", state.describe_summary(opts));
|
||||
|
||||
println!();
|
||||
}
|
||||
|
||||
fn prompt<'c>(candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> {
|
||||
println!("Multiple tied candidates:");
|
||||
for (i, candidate) in candidates.iter().enumerate() {
|
||||
println!("{}. {}", i + 1, candidate.name);
|
||||
}
|
||||
let mut buffer = String::new();
|
||||
loop {
|
||||
print!("{} [1-{}] ", prompt_text, candidates.len());
|
||||
print!("Which candidate to select? [1-{}] ", candidates.len());
|
||||
stdout().flush().expect("IO Error");
|
||||
stdin().read_line(&mut buffer).expect("IO Error");
|
||||
match buffer.trim().parse::<usize>() {
|
||||
|
@ -266,60 +230,36 @@ pub fn prompt<'c, N: Number>(state: &CountState<N>, opts: &STVOptions, candidate
|
|||
#[cfg(target_arch = "wasm32")]
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
fn get_user_input(s: &str) -> Option<String>;
|
||||
fn read_user_input_buffer(s: &str) -> Option<String>;
|
||||
}
|
||||
|
||||
/// Prompt the candidate for input, depending on CLI or WebAssembly target
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn prompt<'c, N: Number>(state: &CountState<N>, opts: &STVOptions, candidates: &[&'c Candidate], prompt_text: &str) -> Result<&'c Candidate, STVError> {
|
||||
let mut message = String::new();
|
||||
|
||||
// Show intrastage progress if required
|
||||
if !state.logger.entries.is_empty() {
|
||||
// Print stage details
|
||||
message.push_str(&format!("Tie during: {}\n", state.title));
|
||||
message.push_str(&state.logger.render().join(" "));
|
||||
message.push('\n');
|
||||
|
||||
// Print candidates
|
||||
message.push_str(&state.describe_candidates(opts));
|
||||
message.push('\n');
|
||||
|
||||
// Print summary rows
|
||||
message.push_str(&state.describe_summary(opts));
|
||||
message.push('\n');
|
||||
}
|
||||
|
||||
message.push_str(&"Multiple tied candidates:\n");
|
||||
fn prompt<'c>(candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> {
|
||||
let mut message = String::from("Multiple tied candidates:\n");
|
||||
for (i, candidate) in candidates.iter().enumerate() {
|
||||
message.push_str(&format!("{}. {}\n", i + 1, candidate.name));
|
||||
}
|
||||
message.push_str(&format!("{} [1-{}] ", prompt_text, candidates.len()));
|
||||
message.push_str(&format!("Which candidate to select? [1-{}] ", candidates.len()));
|
||||
|
||||
loop {
|
||||
let response = get_user_input(&message);
|
||||
|
||||
match response {
|
||||
Some(response) => {
|
||||
match response.trim().parse::<usize>() {
|
||||
Ok(val) => {
|
||||
if val >= 1 && val <= candidates.len() {
|
||||
return Ok(candidates[val - 1]);
|
||||
} else {
|
||||
// Invalid selection
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Invalid selection
|
||||
continue;
|
||||
match read_user_input_buffer(&message) {
|
||||
Some(response) => {
|
||||
match response.trim().parse::<usize>() {
|
||||
Ok(val) => {
|
||||
if val >= 1 && val <= candidates.len() {
|
||||
return Ok(candidates[val - 1]);
|
||||
} else {
|
||||
let _ = read_user_input_buffer(&message);
|
||||
return Err(STVError::RequireInput);
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
let _ = read_user_input_buffer(&message);
|
||||
return Err(STVError::RequireInput);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// No available user input in buffer - stack will be unwound
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return Err(STVError::RequireInput);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::election::Election;
|
||||
use crate::numbers::Number;
|
||||
|
||||
use rkyv::ser::{Serializer, serializers::AllocSerializer};
|
||||
|
||||
use std::io::{BufWriter, Write};
|
||||
|
||||
/// Write the [Election] into BIN format
|
||||
pub fn write<W: Write, N: Number>(election: Election<N>, output: W) {
|
||||
// Serialize data using rkyv
|
||||
let mut serializer = AllocSerializer::<256>::default();
|
||||
serializer.serialize_value(&election).unwrap();
|
||||
let buffer = serializer.into_serializer().into_inner();
|
||||
|
||||
// Write output
|
||||
let mut output = BufWriter::new(output);
|
||||
output.write_all(&buffer).expect("IO Error");
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::election::Election;
|
||||
use crate::numbers::Number;
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
use std::io::{BufWriter, Write};
|
||||
|
||||
/// Write the [Election] into BLT format
|
||||
pub fn write<W: Write, N: Number>(election: Election<N>, output: W) {
|
||||
let mut output = BufWriter::new(output);
|
||||
|
||||
// Writer header row
|
||||
output.write_fmt(format_args!("{} {}\n", election.candidates.len(), election.seats)).expect("IO Error");
|
||||
|
||||
// Write withdrawn candidates
|
||||
if !election.withdrawn_candidates.is_empty() {
|
||||
output.write_all(election.withdrawn_candidates.into_iter().map(|idx| format!("-{}", idx + 1)).join(" ").as_bytes()).expect("IO Error");
|
||||
output.write_all(b"\n").expect("IO Error");
|
||||
}
|
||||
|
||||
// Write ballots
|
||||
for ballot in election.ballots {
|
||||
output.write_fmt(format_args!("{}", ballot.orig_value)).expect("IO Error");
|
||||
|
||||
for preference in ballot.preferences {
|
||||
output.write_fmt(format_args!(" {}", preference.into_iter().map(|p| p + 1).join("="))).expect("IO Error");
|
||||
}
|
||||
|
||||
output.write_all(b" 0\n").expect("IO Error");
|
||||
}
|
||||
|
||||
output.write_all(b"0\n").expect("IO Error");
|
||||
|
||||
// Write candidate names
|
||||
for candidate in election.candidates {
|
||||
output.write_fmt(format_args!("\"{}\"\n", candidate.name)).expect("IO Error");
|
||||
}
|
||||
|
||||
// Write election name
|
||||
output.write_fmt(format_args!("\"{}\"\n", election.name)).expect("IO Error");
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::election::Election;
|
||||
use crate::numbers::Number;
|
||||
|
||||
use csv::Writer;
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
/// Write the [Election] into CSP format
|
||||
pub fn write<W: Write, N: Number>(election: Election<N>, output: W) {
|
||||
// Open writer
|
||||
// csv::Writer performs its own buffering
|
||||
let mut output = Writer::from_writer(output);
|
||||
|
||||
// Write header row
|
||||
for candidate in election.candidates.iter() {
|
||||
output.write_field(&candidate.name).expect("IO Error");
|
||||
}
|
||||
output.write_field("$mult").expect("IO Error");
|
||||
output.write_record(None::<&[u8]>).expect("IO Error");
|
||||
|
||||
// Write ballots
|
||||
for ballot in election.ballots {
|
||||
// Code preferences to rankings
|
||||
let mut rankings = vec![0_usize; election.candidates.len()];
|
||||
for (i, preference) in ballot.preferences.into_iter().enumerate() {
|
||||
for p in preference {
|
||||
rankings[p] = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Write rankings
|
||||
for ranking in rankings {
|
||||
output.write_field(format!("{}", ranking)).expect("IO Error");
|
||||
}
|
||||
output.write_field(format!("{}", ballot.orig_value)).expect("IO Error");
|
||||
output.write_record(None::<&[u8]>).expect("IO Error");
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/// BIN file writer
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod bin;
|
||||
|
||||
/// BLT file writer
|
||||
pub mod blt;
|
||||
/// CSP file writer
|
||||
pub mod csp;
|
|
@ -15,19 +15,17 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::utils;
|
||||
mod utils;
|
||||
|
||||
use opentally::election::Election;
|
||||
use opentally::numbers::Rational;
|
||||
use opentally::parser::blt;
|
||||
use opentally::stv;
|
||||
|
||||
use csv::StringRecord;
|
||||
use flate2::bufread::GzDecoder;
|
||||
use utf8_chars::BufReadCharsExt;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{self, BufReader};
|
||||
use std::io::{self, BufRead};
|
||||
|
||||
#[test]
|
||||
fn aec_tas19_rational() {
|
||||
|
@ -48,29 +46,41 @@ fn aec_tas19_rational() {
|
|||
let file = File::open("tests/data/aec-senate-formalpreferences-24310-TAS.blt.gz").expect("IO Error");
|
||||
let file_reader = io::BufReader::new(file);
|
||||
let gz_decoder = GzDecoder::new(file_reader);
|
||||
|
||||
let mut reader = BufReader::new(gz_decoder);
|
||||
let chars = reader.chars().map(|r| r.expect("IO Error")).peekable();
|
||||
let gz_reader = io::BufReader::new(gz_decoder);
|
||||
let lines = gz_reader.lines();
|
||||
|
||||
// Read BLT
|
||||
let election: Election<Rational> = blt::parse_iterator(chars).expect("Syntax Error");
|
||||
let election: Election<Rational> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
|
||||
|
||||
// Validate candidate names
|
||||
for (i, candidate) in candidates.iter().enumerate() {
|
||||
assert_eq!(election.candidates[i].name, *candidate);
|
||||
}
|
||||
|
||||
let stv_opts = stv::STVOptionsBuilder::default()
|
||||
.round_votes(Some(0))
|
||||
.round_quota(Some(0))
|
||||
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
||||
.surplus(stv::SurplusMethod::UIG)
|
||||
.surplus_order(stv::SurplusOrder::ByOrder)
|
||||
.exclusion(stv::ExclusionMethod::ByValue)
|
||||
.bulk_exclude(true)
|
||||
.build().unwrap();
|
||||
|
||||
assert_eq!(stv_opts.describe::<Rational>(), "--round-votes 0 --round-quota 0 --quota-criterion geq --surplus uig --surplus-order by_order --exclusion by_value --bulk-exclude");
|
||||
|
||||
let stv_opts = stv::STVOptions {
|
||||
round_tvs: None,
|
||||
round_weights: None,
|
||||
round_votes: Some(0),
|
||||
round_quota: Some(0),
|
||||
sum_surplus_transfers: stv::SumSurplusTransfersMode::SingleStep,
|
||||
meek_surplus_tolerance: String::new(),
|
||||
normalise_ballots: false,
|
||||
quota: stv::QuotaType::Droop,
|
||||
quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
|
||||
quota_mode: stv::QuotaMode::Static,
|
||||
ties: vec![],
|
||||
surplus: stv::SurplusMethod::UIG,
|
||||
surplus_order: stv::SurplusOrder::ByOrder,
|
||||
transferable_only: false,
|
||||
exclusion: stv::ExclusionMethod::ByValue,
|
||||
meek_nz_exclusion: false,
|
||||
early_bulk_elect: true,
|
||||
bulk_exclude: true,
|
||||
defer_surpluses: false,
|
||||
meek_immediate_elect: false,
|
||||
constraints_path: None,
|
||||
constraint_mode: stv::ConstraintMode::GuardDoom,
|
||||
pp_decimals: 2,
|
||||
};
|
||||
utils::validate_election::<Rational>(stages, records, election, stv_opts, None, &["exhausted", "lbf"]);
|
||||
}
|
|
@ -19,16 +19,9 @@ use assert_cmd::Command;
|
|||
use predicates::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn cli_ers97old_fixed5() {
|
||||
fn cli_ers97() {
|
||||
Command::cargo_bin("opentally").expect("Cargo Error")
|
||||
.args(&["stv", "tests/data/ers97old.blt", "--numbers", "fixed", "--decimals", "5", "--round-tvs", "2", "--round-weights", "2", "--round-votes", "2", "--round-quota", "2", "--quota", "droop_exact", "--quota-mode", "ers97", "--surplus", "eg", "--transferable-only", "--exclusion", "by_value"])
|
||||
.assert().success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_ers97_fixed5_transfers_detail() {
|
||||
Command::cargo_bin("opentally").expect("Cargo Error")
|
||||
.args(&["stv", "tests/data/ers97.blt", "--numbers", "fixed", "--decimals", "5", "--round-surplus-fractions", "2", "--round-values", "2", "--round-votes", "2", "--round-quota", "2", "--quota", "droop_exact", "--quota-criterion", "geq", "--quota-mode", "ers97", "--surplus", "eg", "--transferable-only", "--exclusion", "by_value", "--bulk-exclude", "--defer-surpluses", "--transfers-detail"])
|
||||
.args(&["stv", "tests/data/ers97.blt", "--numbers", "fixed", "--decimals", "5", "--round-tvs", "2", "--round-weights", "2", "--round-votes", "2", "--round-quota", "2", "--quota", "droop_exact", "--quota-mode", "ers97", "--surplus", "eg", "--transferable-only", "--exclusion", "by_value"])
|
||||
.assert().success();
|
||||
}
|
||||
|
|
@ -15,12 +15,11 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::utils;
|
||||
mod utils;
|
||||
|
||||
use opentally::constraints::Constraints;
|
||||
use opentally::election::{CandidateState, CountState, Election};
|
||||
use opentally::numbers::Rational;
|
||||
use opentally::parser::blt;
|
||||
use opentally::stv;
|
||||
|
||||
use std::fs::File;
|
||||
|
@ -31,27 +30,42 @@ fn prsa1_constr1_rational() {
|
|||
// FIXME: This is unvalidated!
|
||||
|
||||
// Read BLT
|
||||
let mut election: Election<Rational> = blt::parse_path("tests/data/prsa1.blt").expect("Syntax Error");
|
||||
let file = File::open("tests/data/prsa1.blt").expect("IO Error");
|
||||
let file_reader = io::BufReader::new(file);
|
||||
let lines = file_reader.lines();
|
||||
let mut election: Election<Rational> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
|
||||
|
||||
// Read CON
|
||||
let file = File::open("tests/data/prsa1_constr1.con").expect("IO Error");
|
||||
let file_reader = io::BufReader::new(file);
|
||||
let lines = file_reader.lines();
|
||||
election.constraints = Some(Constraints::from_con(lines.map(|r| r.expect("IO Error").to_string()).into_iter()).unwrap());
|
||||
election.constraints = Some(Constraints::from_con(lines.map(|r| r.expect("IO Error").to_string()).into_iter()));
|
||||
|
||||
let stv_opts = stv::STVOptionsBuilder::default()
|
||||
.round_surplus_fractions(Some(3))
|
||||
.round_values(Some(3))
|
||||
.round_votes(Some(3))
|
||||
.round_quota(Some(3))
|
||||
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
||||
.surplus(stv::SurplusMethod::EG)
|
||||
.surplus_order(stv::SurplusOrder::ByOrder)
|
||||
.transferable_only(true)
|
||||
.exclusion(stv::ExclusionMethod::ParcelsByOrder)
|
||||
.early_bulk_elect(false)
|
||||
.constraints_path(Some("tests/data/prsa1_constr1.con".to_string()))
|
||||
.build().unwrap();
|
||||
let stv_opts = stv::STVOptions {
|
||||
round_tvs: Some(3),
|
||||
round_weights: Some(3),
|
||||
round_votes: Some(3),
|
||||
round_quota: Some(3),
|
||||
sum_surplus_transfers: stv::SumSurplusTransfersMode::SingleStep,
|
||||
meek_surplus_tolerance: String::new(),
|
||||
normalise_ballots: false,
|
||||
quota: stv::QuotaType::Droop,
|
||||
quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
|
||||
quota_mode: stv::QuotaMode::Static,
|
||||
ties: vec![],
|
||||
surplus: stv::SurplusMethod::EG,
|
||||
surplus_order: stv::SurplusOrder::ByOrder,
|
||||
transferable_only: true,
|
||||
exclusion: stv::ExclusionMethod::ParcelsByOrder,
|
||||
meek_nz_exclusion: false,
|
||||
early_bulk_elect: false,
|
||||
bulk_exclude: false,
|
||||
defer_surpluses: false,
|
||||
meek_immediate_elect: false,
|
||||
constraints_path: Some("tests/data/prsa1_constr1.con".to_string()),
|
||||
constraint_mode: stv::ConstraintMode::GuardDoom,
|
||||
pp_decimals: 2,
|
||||
};
|
||||
|
||||
// Initialise count state
|
||||
let mut state = CountState::new(&election);
|
||||
|
@ -80,27 +94,42 @@ fn prsa1_constr2_rational() {
|
|||
// FIXME: This is unvalidated!
|
||||
|
||||
// Read BLT
|
||||
let mut election: Election<Rational> = blt::parse_path("tests/data/prsa1.blt").expect("Syntax Error");
|
||||
let file = File::open("tests/data/prsa1.blt").expect("IO Error");
|
||||
let file_reader = io::BufReader::new(file);
|
||||
let lines = file_reader.lines();
|
||||
let mut election: Election<Rational> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
|
||||
|
||||
// Read CON
|
||||
let file = File::open("tests/data/prsa1_constr2.con").expect("IO Error");
|
||||
let file_reader = io::BufReader::new(file);
|
||||
let lines = file_reader.lines();
|
||||
election.constraints = Some(Constraints::from_con(lines.map(|r| r.expect("IO Error").to_string()).into_iter()).unwrap());
|
||||
election.constraints = Some(Constraints::from_con(lines.map(|r| r.expect("IO Error").to_string()).into_iter()));
|
||||
|
||||
let stv_opts = stv::STVOptionsBuilder::default()
|
||||
.round_surplus_fractions(Some(3))
|
||||
.round_values(Some(3))
|
||||
.round_votes(Some(3))
|
||||
.round_quota(Some(3))
|
||||
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
||||
.surplus(stv::SurplusMethod::EG)
|
||||
.surplus_order(stv::SurplusOrder::ByOrder)
|
||||
.transferable_only(true)
|
||||
.exclusion(stv::ExclusionMethod::ParcelsByOrder)
|
||||
.early_bulk_elect(false)
|
||||
.constraints_path(Some("tests/data/prsa1_constr1.con".to_string()))
|
||||
.build().unwrap();
|
||||
let stv_opts = stv::STVOptions {
|
||||
round_tvs: Some(3),
|
||||
round_weights: Some(3),
|
||||
round_votes: Some(3),
|
||||
round_quota: Some(3),
|
||||
sum_surplus_transfers: stv::SumSurplusTransfersMode::SingleStep,
|
||||
meek_surplus_tolerance: String::new(),
|
||||
normalise_ballots: false,
|
||||
quota: stv::QuotaType::Droop,
|
||||
quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
|
||||
quota_mode: stv::QuotaMode::Static,
|
||||
ties: vec![],
|
||||
surplus: stv::SurplusMethod::EG,
|
||||
surplus_order: stv::SurplusOrder::ByOrder,
|
||||
transferable_only: true,
|
||||
exclusion: stv::ExclusionMethod::ParcelsByOrder,
|
||||
meek_nz_exclusion: false,
|
||||
early_bulk_elect: false,
|
||||
bulk_exclude: false,
|
||||
defer_surpluses: false,
|
||||
meek_immediate_elect: false,
|
||||
constraints_path: Some("tests/data/prsa1_constr2.con".to_string()),
|
||||
constraint_mode: stv::ConstraintMode::GuardDoom,
|
||||
pp_decimals: 2,
|
||||
};
|
||||
|
||||
// Initialise count state
|
||||
let mut state = CountState::new(&election);
|
||||
|
@ -129,27 +158,42 @@ fn prsa1_constr3_rational() {
|
|||
// FIXME: This is unvalidated!
|
||||
|
||||
// Read BLT
|
||||
let mut election: Election<Rational> = blt::parse_path("tests/data/prsa1.blt").expect("Syntax Error");
|
||||
let file = File::open("tests/data/prsa1.blt").expect("IO Error");
|
||||
let file_reader = io::BufReader::new(file);
|
||||
let lines = file_reader.lines();
|
||||
let mut election: Election<Rational> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
|
||||
|
||||
// Read CON
|
||||
let file = File::open("tests/data/prsa1_constr3.con").expect("IO Error");
|
||||
let file_reader = io::BufReader::new(file);
|
||||
let lines = file_reader.lines();
|
||||
election.constraints = Some(Constraints::from_con(lines.map(|r| r.expect("IO Error").to_string()).into_iter()).unwrap());
|
||||
election.constraints = Some(Constraints::from_con(lines.map(|r| r.expect("IO Error").to_string()).into_iter()));
|
||||
|
||||
let stv_opts = stv::STVOptionsBuilder::default()
|
||||
.round_surplus_fractions(Some(3))
|
||||
.round_values(Some(3))
|
||||
.round_votes(Some(3))
|
||||
.round_quota(Some(3))
|
||||
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
||||
.surplus(stv::SurplusMethod::EG)
|
||||
.surplus_order(stv::SurplusOrder::ByOrder)
|
||||
.transferable_only(true)
|
||||
.exclusion(stv::ExclusionMethod::ParcelsByOrder)
|
||||
.early_bulk_elect(false)
|
||||
.constraints_path(Some("tests/data/prsa1_constr1.con".to_string()))
|
||||
.build().unwrap();
|
||||
let stv_opts = stv::STVOptions {
|
||||
round_tvs: Some(3),
|
||||
round_weights: Some(3),
|
||||
round_votes: Some(3),
|
||||
round_quota: Some(3),
|
||||
sum_surplus_transfers: stv::SumSurplusTransfersMode::SingleStep,
|
||||
meek_surplus_tolerance: String::new(),
|
||||
normalise_ballots: false,
|
||||
quota: stv::QuotaType::Droop,
|
||||
quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
|
||||
quota_mode: stv::QuotaMode::Static,
|
||||
ties: vec![],
|
||||
surplus: stv::SurplusMethod::EG,
|
||||
surplus_order: stv::SurplusOrder::ByOrder,
|
||||
transferable_only: true,
|
||||
exclusion: stv::ExclusionMethod::ParcelsByOrder,
|
||||
meek_nz_exclusion: false,
|
||||
early_bulk_elect: false,
|
||||
bulk_exclude: false,
|
||||
defer_surpluses: false,
|
||||
meek_immediate_elect: false,
|
||||
constraints_path: Some("tests/data/prsa1_constr2.con".to_string()),
|
||||
constraint_mode: stv::ConstraintMode::GuardDoom,
|
||||
pp_decimals: 2,
|
||||
};
|
||||
|
||||
// Initialise count state
|
||||
let mut state = CountState::new(&election);
|
||||
|
@ -172,48 +216,3 @@ fn prsa1_constr3_rational() {
|
|||
assert_eq!(winners[2].0.name, "Thomson");
|
||||
assert_eq!(winners[3].0.name, "Reid");
|
||||
}
|
||||
|
||||
/// Same election data as ers97_rational, but with a constraint that prevents the bulk exclusion of Glazier and Wright
|
||||
#[test]
|
||||
fn ers97old_cantbulkexclude_rational() {
|
||||
// Read CSV file
|
||||
let reader = csv::ReaderBuilder::new()
|
||||
.has_headers(false)
|
||||
.from_path("tests/data/ers97old_cantbulkexclude.csv")
|
||||
.expect("IO Error");
|
||||
let records: Vec<csv::StringRecord> = reader.into_records().map(|r| r.expect("Syntax Error")).collect();
|
||||
|
||||
let mut candidates: Vec<&str> = records.iter().skip(2).map(|r| &r[0]).collect();
|
||||
// Remove exhausted/LBF rows
|
||||
candidates.truncate(candidates.len() - 2);
|
||||
|
||||
let stages: Vec<usize> = records.first().unwrap().iter().skip(1).step_by(2).map(|s| s.parse().unwrap()).collect();
|
||||
|
||||
// Read BLT
|
||||
let mut election: Election<Rational> = blt::parse_path("tests/data/ers97old.blt").expect("Syntax Error");
|
||||
|
||||
// Read CON
|
||||
let file = File::open("tests/data/ers97old_cantbulkexclude.con").expect("IO Error");
|
||||
let file_reader = io::BufReader::new(file);
|
||||
let lines = file_reader.lines();
|
||||
election.constraints = Some(Constraints::from_con(lines.map(|r| r.expect("IO Error").to_string()).into_iter()).unwrap());
|
||||
|
||||
let stv_opts = stv::STVOptionsBuilder::default()
|
||||
.round_surplus_fractions(Some(2))
|
||||
.round_values(Some(2))
|
||||
.round_votes(Some(2))
|
||||
.round_quota(Some(2))
|
||||
.quota(stv::QuotaType::DroopExact)
|
||||
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
||||
.quota_mode(stv::QuotaMode::ERS97)
|
||||
.surplus(stv::SurplusMethod::EG)
|
||||
.transferable_only(true)
|
||||
.exclusion(stv::ExclusionMethod::ByValue)
|
||||
.early_bulk_elect(false)
|
||||
.bulk_exclude(true)
|
||||
.defer_surpluses(true)
|
||||
.constraints_path(Some("tests/data/ers97old_cantbulkexclude".to_string()))
|
||||
.build().unwrap();
|
||||
|
||||
utils::validate_election::<Rational>(stages, records, election, stv_opts, None, &["nt", "vre"]);
|
||||
}
|
|
@ -15,22 +15,37 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::utils;
|
||||
mod utils;
|
||||
|
||||
use opentally::numbers::NativeFloat64;
|
||||
use opentally::stv;
|
||||
|
||||
#[test]
|
||||
fn csm15_float64() {
|
||||
let stv_opts = stv::STVOptionsBuilder::default()
|
||||
.round_quota(Some(0))
|
||||
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
||||
.exclusion(stv::ExclusionMethod::ResetAndReiterate)
|
||||
.early_bulk_elect(false)
|
||||
.bulk_exclude(true)
|
||||
.build().unwrap();
|
||||
|
||||
assert_eq!(stv_opts.describe::<NativeFloat64>(), "--numbers float64 --round-quota 0 --quota-criterion geq --exclusion reset_and_reiterate --no-early-bulk-elect --bulk-exclude");
|
||||
|
||||
let stv_opts = stv::STVOptions {
|
||||
round_tvs: None,
|
||||
round_weights: None,
|
||||
round_votes: None,
|
||||
round_quota: Some(0),
|
||||
sum_surplus_transfers: stv::SumSurplusTransfersMode::SingleStep,
|
||||
meek_surplus_tolerance: String::new(),
|
||||
normalise_ballots: false,
|
||||
quota: stv::QuotaType::Droop,
|
||||
quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
|
||||
quota_mode: stv::QuotaMode::Static,
|
||||
ties: vec![],
|
||||
surplus: stv::SurplusMethod::WIG,
|
||||
surplus_order: stv::SurplusOrder::BySize,
|
||||
transferable_only: false,
|
||||
exclusion: stv::ExclusionMethod::Wright,
|
||||
meek_nz_exclusion: false,
|
||||
early_bulk_elect: true,
|
||||
bulk_exclude: true,
|
||||
defer_surpluses: false,
|
||||
meek_immediate_elect: false,
|
||||
constraints_path: None,
|
||||
constraint_mode: stv::ConstraintMode::GuardDoom,
|
||||
pp_decimals: 2,
|
||||
};
|
||||
utils::read_validate_election::<NativeFloat64>("tests/data/CSM15.csv", "tests/data/CSM15.blt", stv_opts, Some(6), &["quota"]);
|
||||
}
|
|
@ -1,950 +0,0 @@
|
|||
# Comment: 2013 Minneapolis Park and Recreation Commissioner At Large election - all votes - Minneapolis STV
|
||||
# Source: https://vote.minneapolismn.gov/results-data/election-results/2013/
|
||||
# Contributor: RunasSudo
|
||||
11 3
|
||||
20638 0
|
||||
2231 1 0
|
||||
21 1 2 0
|
||||
5 1 2 3 0
|
||||
19 1 2 4 0
|
||||
31 1 2 5 0
|
||||
19 1 2 6 0
|
||||
35 1 2 7 0
|
||||
43 1 2 8 0
|
||||
13 1 2 9 0
|
||||
7 1 2 10 0
|
||||
47 1 3 0
|
||||
8 1 3 2 0
|
||||
68 1 3 4 0
|
||||
25 1 3 5 0
|
||||
14 1 3 6 0
|
||||
32 1 3 7 0
|
||||
35 1 3 8 0
|
||||
9 1 3 9 0
|
||||
13 1 3 10 0
|
||||
96 1 4 0
|
||||
33 1 4 2 0
|
||||
88 1 4 3 0
|
||||
51 1 4 5 0
|
||||
85 1 4 6 0
|
||||
61 1 4 7 0
|
||||
100 1 4 8 0
|
||||
17 1 4 9 0
|
||||
29 1 4 10 0
|
||||
6 1 4 11 0
|
||||
127 1 5 0
|
||||
44 1 5 2 0
|
||||
28 1 5 3 0
|
||||
96 1 5 4 0
|
||||
394 1 5 6 0
|
||||
66 1 5 7 0
|
||||
150 1 5 8 0
|
||||
60 1 5 9 0
|
||||
119 1 5 10 0
|
||||
2 1 5 11 0
|
||||
221 1 6 0
|
||||
35 1 6 2 0
|
||||
34 1 6 3 0
|
||||
139 1 6 4 0
|
||||
560 1 6 5 0
|
||||
55 1 6 7 0
|
||||
181 1 6 8 0
|
||||
44 1 6 9 0
|
||||
278 1 6 10 0
|
||||
4 1 6 11 0
|
||||
76 1 7 0
|
||||
35 1 7 2 0
|
||||
24 1 7 3 0
|
||||
40 1 7 4 0
|
||||
56 1 7 5 0
|
||||
29 1 7 6 0
|
||||
487 1 7 8 0
|
||||
28 1 7 9 0
|
||||
39 1 7 10 0
|
||||
257 1 8 0
|
||||
42 1 8 2 0
|
||||
47 1 8 3 0
|
||||
98 1 8 4 0
|
||||
124 1 8 5 0
|
||||
140 1 8 6 0
|
||||
820 1 8 7 0
|
||||
52 1 8 9 0
|
||||
110 1 8 10 0
|
||||
9 1 8 11 0
|
||||
38 1 9 0
|
||||
23 1 9 2 0
|
||||
12 1 9 3 0
|
||||
18 1 9 4 0
|
||||
36 1 9 5 0
|
||||
41 1 9 6 0
|
||||
26 1 9 7 0
|
||||
68 1 9 8 0
|
||||
27 1 9 10 0
|
||||
97 1 10 0
|
||||
23 1 10 2 0
|
||||
17 1 10 3 0
|
||||
28 1 10 4 0
|
||||
56 1 10 5 0
|
||||
174 1 10 6 0
|
||||
41 1 10 7 0
|
||||
113 1 10 8 0
|
||||
46 1 10 9 0
|
||||
16 1 11 0
|
||||
1 1 11 6 0
|
||||
2 1 11 8 0
|
||||
293 2 0
|
||||
23 2 1 0
|
||||
6 2 1 3 0
|
||||
7 2 1 4 0
|
||||
28 2 1 5 0
|
||||
25 2 1 6 0
|
||||
17 2 1 7 0
|
||||
24 2 1 8 0
|
||||
9 2 1 9 0
|
||||
7 2 1 10 0
|
||||
4 2 1 11 0
|
||||
2 2 3 0
|
||||
3 2 3 1 0
|
||||
11 2 3 4 0
|
||||
3 2 3 5 0
|
||||
2 2 3 6 0
|
||||
7 2 3 8 0
|
||||
4 2 3 9 0
|
||||
7 2 4 0
|
||||
12 2 4 1 0
|
||||
13 2 4 3 0
|
||||
5 2 4 5 0
|
||||
5 2 4 6 0
|
||||
8 2 4 7 0
|
||||
4 2 4 8 0
|
||||
5 2 4 9 0
|
||||
2 2 4 10 0
|
||||
2 2 4 11 0
|
||||
10 2 5 0
|
||||
28 2 5 1 0
|
||||
4 2 5 3 0
|
||||
4 2 5 4 0
|
||||
16 2 5 6 0
|
||||
6 2 5 7 0
|
||||
22 2 5 8 0
|
||||
13 2 5 9 0
|
||||
9 2 5 10 0
|
||||
1 2 5 11 0
|
||||
24 2 6 0
|
||||
38 2 6 1 0
|
||||
3 2 6 3 0
|
||||
14 2 6 4 0
|
||||
39 2 6 5 0
|
||||
10 2 6 7 0
|
||||
19 2 6 8 0
|
||||
16 2 6 9 0
|
||||
18 2 6 10 0
|
||||
1 2 6 11 0
|
||||
15 2 7 0
|
||||
23 2 7 1 0
|
||||
2 2 7 3 0
|
||||
6 2 7 4 0
|
||||
7 2 7 5 0
|
||||
8 2 7 6 0
|
||||
17 2 7 8 0
|
||||
18 2 7 9 0
|
||||
4 2 7 10 0
|
||||
28 2 8 0
|
||||
31 2 8 1 0
|
||||
8 2 8 3 0
|
||||
9 2 8 4 0
|
||||
12 2 8 5 0
|
||||
34 2 8 6 0
|
||||
17 2 8 7 0
|
||||
14 2 8 9 0
|
||||
13 2 8 10 0
|
||||
12 2 9 0
|
||||
5 2 9 1 0
|
||||
3 2 9 3 0
|
||||
12 2 9 4 0
|
||||
7 2 9 5 0
|
||||
15 2 9 6 0
|
||||
15 2 9 7 0
|
||||
13 2 9 8 0
|
||||
11 2 9 10 0
|
||||
2 2 9 11 0
|
||||
11 2 10 0
|
||||
11 2 10 1 0
|
||||
3 2 10 3 0
|
||||
5 2 10 4 0
|
||||
8 2 10 5 0
|
||||
16 2 10 6 0
|
||||
9 2 10 7 0
|
||||
10 2 10 8 0
|
||||
8 2 10 9 0
|
||||
5 2 11 0
|
||||
2520 3 0
|
||||
36 3 1 0
|
||||
8 3 1 2 0
|
||||
34 3 1 4 0
|
||||
35 3 1 5 0
|
||||
17 3 1 6 0
|
||||
22 3 1 7 0
|
||||
41 3 1 8 0
|
||||
9 3 1 9 0
|
||||
15 3 1 10 0
|
||||
4 3 2 0
|
||||
5 3 2 1 0
|
||||
9 3 2 4 0
|
||||
3 3 2 5 0
|
||||
1 3 2 6 0
|
||||
5 3 2 7 0
|
||||
2 3 2 8 0
|
||||
4 3 2 9 0
|
||||
4 3 2 10 0
|
||||
99 3 4 0
|
||||
99 3 4 1 0
|
||||
35 3 4 2 0
|
||||
34 3 4 5 0
|
||||
20 3 4 6 0
|
||||
73 3 4 7 0
|
||||
75 3 4 8 0
|
||||
14 3 4 9 0
|
||||
26 3 4 10 0
|
||||
1 3 4 11 0
|
||||
13 3 5 0
|
||||
10 3 5 1 0
|
||||
4 3 5 2 0
|
||||
9 3 5 4 0
|
||||
5 3 5 6 0
|
||||
6 3 5 7 0
|
||||
11 3 5 8 0
|
||||
10 3 5 9 0
|
||||
4 3 5 10 0
|
||||
31 3 6 0
|
||||
16 3 6 1 0
|
||||
3 3 6 2 0
|
||||
10 3 6 4 0
|
||||
18 3 6 5 0
|
||||
11 3 6 7 0
|
||||
10 3 6 8 0
|
||||
4 3 6 9 0
|
||||
14 3 6 10 0
|
||||
15 3 7 0
|
||||
20 3 7 1 0
|
||||
6 3 7 2 0
|
||||
12 3 7 4 0
|
||||
6 3 7 5 0
|
||||
8 3 7 6 0
|
||||
14 3 7 8 0
|
||||
8 3 7 9 0
|
||||
9 3 7 10 0
|
||||
18 3 8 0
|
||||
26 3 8 1 0
|
||||
11 3 8 2 0
|
||||
18 3 8 4 0
|
||||
7 3 8 5 0
|
||||
6 3 8 6 0
|
||||
14 3 8 7 0
|
||||
8 3 8 9 0
|
||||
8 3 8 10 0
|
||||
7 3 9 0
|
||||
6 3 9 1 0
|
||||
4 3 9 2 0
|
||||
3 3 9 4 0
|
||||
5 3 9 5 0
|
||||
3 3 9 6 0
|
||||
3 3 9 7 0
|
||||
7 3 9 8 0
|
||||
2 3 9 10 0
|
||||
1 3 9 11 0
|
||||
10 3 10 0
|
||||
14 3 10 1 0
|
||||
7 3 10 2 0
|
||||
9 3 10 4 0
|
||||
2 3 10 5 0
|
||||
10 3 10 6 0
|
||||
2 3 10 7 0
|
||||
5 3 10 8 0
|
||||
3 3 10 9 0
|
||||
6 3 11 0
|
||||
911 4 0
|
||||
100 4 1 0
|
||||
13 4 1 2 0
|
||||
70 4 1 3 0
|
||||
67 4 1 5 0
|
||||
69 4 1 6 0
|
||||
44 4 1 7 0
|
||||
96 4 1 8 0
|
||||
13 4 1 9 0
|
||||
23 4 1 10 0
|
||||
4 4 1 11 0
|
||||
14 4 2 0
|
||||
18 4 2 1 0
|
||||
22 4 2 3 0
|
||||
13 4 2 5 0
|
||||
7 4 2 6 0
|
||||
14 4 2 7 0
|
||||
29 4 2 8 0
|
||||
8 4 2 9 0
|
||||
4 4 2 10 0
|
||||
2 4 2 11 0
|
||||
97 4 3 0
|
||||
105 4 3 1 0
|
||||
39 4 3 2 0
|
||||
43 4 3 5 0
|
||||
25 4 3 6 0
|
||||
57 4 3 7 0
|
||||
74 4 3 8 0
|
||||
18 4 3 9 0
|
||||
31 4 3 10 0
|
||||
5 4 3 11 0
|
||||
22 4 5 0
|
||||
62 4 5 1 0
|
||||
18 4 5 2 0
|
||||
31 4 5 3 0
|
||||
54 4 5 6 0
|
||||
25 4 5 7 0
|
||||
15 4 5 8 0
|
||||
10 4 5 9 0
|
||||
30 4 5 10 0
|
||||
3 4 5 11 0
|
||||
39 4 6 0
|
||||
72 4 6 1 0
|
||||
12 4 6 2 0
|
||||
17 4 6 3 0
|
||||
53 4 6 5 0
|
||||
13 4 6 7 0
|
||||
18 4 6 8 0
|
||||
8 4 6 9 0
|
||||
36 4 6 10 0
|
||||
2 4 6 11 0
|
||||
12 4 7 0
|
||||
49 4 7 1 0
|
||||
12 4 7 2 0
|
||||
25 4 7 3 0
|
||||
16 4 7 5 0
|
||||
8 4 7 6 0
|
||||
38 4 7 8 0
|
||||
8 4 7 9 0
|
||||
9 4 7 10 0
|
||||
2 4 7 11 0
|
||||
56 4 8 0
|
||||
91 4 8 1 0
|
||||
16 4 8 2 0
|
||||
41 4 8 3 0
|
||||
31 4 8 5 0
|
||||
21 4 8 6 0
|
||||
50 4 8 7 0
|
||||
12 4 8 9 0
|
||||
17 4 8 10 0
|
||||
15 4 9 0
|
||||
8 4 9 1 0
|
||||
8 4 9 2 0
|
||||
9 4 9 3 0
|
||||
10 4 9 5 0
|
||||
8 4 9 6 0
|
||||
7 4 9 7 0
|
||||
5 4 9 8 0
|
||||
7 4 9 10 0
|
||||
22 4 10 0
|
||||
17 4 10 1 0
|
||||
10 4 10 2 0
|
||||
11 4 10 3 0
|
||||
11 4 10 5 0
|
||||
27 4 10 6 0
|
||||
7 4 10 7 0
|
||||
14 4 10 8 0
|
||||
3 4 10 9 0
|
||||
10 4 11 0
|
||||
2 4 11 2 0
|
||||
1 4 11 3 0
|
||||
1 4 11 5 0
|
||||
3 4 11 8 0
|
||||
1033 5 0
|
||||
150 5 1 0
|
||||
33 5 1 2 0
|
||||
32 5 1 3 0
|
||||
73 5 1 4 0
|
||||
405 5 1 6 0
|
||||
58 5 1 7 0
|
||||
98 5 1 8 0
|
||||
41 5 1 9 0
|
||||
74 5 1 10 0
|
||||
3 5 1 11 0
|
||||
17 5 2 0
|
||||
29 5 2 1 0
|
||||
6 5 2 3 0
|
||||
6 5 2 4 0
|
||||
31 5 2 6 0
|
||||
18 5 2 7 0
|
||||
15 5 2 8 0
|
||||
15 5 2 9 0
|
||||
15 5 2 10 0
|
||||
3 5 2 11 0
|
||||
20 5 3 0
|
||||
12 5 3 1 0
|
||||
6 5 3 2 0
|
||||
9 5 3 4 0
|
||||
13 5 3 6 0
|
||||
5 5 3 7 0
|
||||
9 5 3 8 0
|
||||
7 5 3 9 0
|
||||
5 5 3 10 0
|
||||
41 5 4 0
|
||||
59 5 4 1 0
|
||||
12 5 4 2 0
|
||||
25 5 4 3 0
|
||||
69 5 4 6 0
|
||||
13 5 4 7 0
|
||||
32 5 4 8 0
|
||||
5 5 4 9 0
|
||||
25 5 4 10 0
|
||||
1 5 4 11 0
|
||||
114 5 6 0
|
||||
535 5 6 1 0
|
||||
39 5 6 2 0
|
||||
12 5 6 3 0
|
||||
215 5 6 4 0
|
||||
19 5 6 7 0
|
||||
107 5 6 8 0
|
||||
63 5 6 9 0
|
||||
275 5 6 10 0
|
||||
2 5 6 11 0
|
||||
17 5 7 0
|
||||
34 5 7 1 0
|
||||
9 5 7 2 0
|
||||
5 5 7 3 0
|
||||
12 5 7 4 0
|
||||
23 5 7 6 0
|
||||
23 5 7 8 0
|
||||
19 5 7 9 0
|
||||
15 5 7 10 0
|
||||
83 5 8 0
|
||||
86 5 8 1 0
|
||||
24 5 8 2 0
|
||||
15 5 8 3 0
|
||||
29 5 8 4 0
|
||||
55 5 8 6 0
|
||||
59 5 8 7 0
|
||||
41 5 8 9 0
|
||||
47 5 8 10 0
|
||||
2 5 8 11 0
|
||||
50 5 9 0
|
||||
32 5 9 1 0
|
||||
24 5 9 2 0
|
||||
4 5 9 3 0
|
||||
13 5 9 4 0
|
||||
75 5 9 6 0
|
||||
21 5 9 7 0
|
||||
38 5 9 8 0
|
||||
37 5 9 10 0
|
||||
51 5 10 0
|
||||
58 5 10 1 0
|
||||
16 5 10 2 0
|
||||
8 5 10 3 0
|
||||
19 5 10 4 0
|
||||
194 5 10 6 0
|
||||
24 5 10 7 0
|
||||
41 5 10 8 0
|
||||
137 5 10 9 0
|
||||
2 5 10 11 0
|
||||
10 5 11 0
|
||||
1 5 11 9 0
|
||||
2062 6 0
|
||||
351 6 1 0
|
||||
89 6 1 2 0
|
||||
46 6 1 3 0
|
||||
178 6 1 4 0
|
||||
2047 6 1 5 0
|
||||
72 6 1 7 0
|
||||
287 6 1 8 0
|
||||
109 6 1 9 0
|
||||
472 6 1 10 0
|
||||
2 6 1 11 0
|
||||
40 6 2 0
|
||||
52 6 2 1 0
|
||||
11 6 2 3 0
|
||||
30 6 2 4 0
|
||||
62 6 2 5 0
|
||||
20 6 2 7 0
|
||||
30 6 2 8 0
|
||||
21 6 2 9 0
|
||||
37 6 2 10 0
|
||||
3 6 2 11 0
|
||||
53 6 3 0
|
||||
19 6 3 1 0
|
||||
12 6 3 2 0
|
||||
29 6 3 4 0
|
||||
23 6 3 5 0
|
||||
12 6 3 7 0
|
||||
10 6 3 8 0
|
||||
7 6 3 9 0
|
||||
16 6 3 10 0
|
||||
3 6 3 11 0
|
||||
58 6 4 0
|
||||
106 6 4 1 0
|
||||
17 6 4 2 0
|
||||
33 6 4 3 0
|
||||
128 6 4 5 0
|
||||
17 6 4 7 0
|
||||
72 6 4 8 0
|
||||
14 6 4 9 0
|
||||
50 6 4 10 0
|
||||
203 6 5 0
|
||||
875 6 5 1 0
|
||||
106 6 5 2 0
|
||||
35 6 5 3 0
|
||||
222 6 5 4 0
|
||||
47 6 5 7 0
|
||||
201 6 5 8 0
|
||||
142 6 5 9 0
|
||||
334 6 5 10 0
|
||||
6 6 5 11 0
|
||||
53 6 7 0
|
||||
54 6 7 1 0
|
||||
9 6 7 2 0
|
||||
18 6 7 3 0
|
||||
19 6 7 4 0
|
||||
59 6 7 5 0
|
||||
50 6 7 8 0
|
||||
35 6 7 9 0
|
||||
43 6 7 10 0
|
||||
194 6 8 0
|
||||
218 6 8 1 0
|
||||
44 6 8 2 0
|
||||
29 6 8 3 0
|
||||
75 6 8 4 0
|
||||
132 6 8 5 0
|
||||
71 6 8 7 0
|
||||
54 6 8 9 0
|
||||
154 6 8 10 0
|
||||
54 6 9 0
|
||||
60 6 9 1 0
|
||||
27 6 9 2 0
|
||||
6 6 9 3 0
|
||||
20 6 9 4 0
|
||||
77 6 9 5 0
|
||||
22 6 9 7 0
|
||||
45 6 9 8 0
|
||||
85 6 9 10 0
|
||||
3 6 9 11 0
|
||||
2031 6 10 0
|
||||
696 6 10 1 0
|
||||
55 6 10 2 0
|
||||
48 6 10 3 0
|
||||
110 6 10 4 0
|
||||
433 6 10 5 0
|
||||
115 6 10 7 0
|
||||
415 6 10 8 0
|
||||
158 6 10 9 0
|
||||
22 6 10 11 0
|
||||
9 6 11 0
|
||||
1 6 11 1 0
|
||||
1 6 11 5 0
|
||||
1 6 11 9 0
|
||||
2 6 11 10 0
|
||||
887 7 0
|
||||
80 7 1 0
|
||||
20 7 1 2 0
|
||||
26 7 1 3 0
|
||||
40 7 1 4 0
|
||||
73 7 1 5 0
|
||||
40 7 1 6 0
|
||||
463 7 1 8 0
|
||||
46 7 1 9 0
|
||||
43 7 1 10 0
|
||||
1 7 1 11 0
|
||||
5 7 2 0
|
||||
21 7 2 1 0
|
||||
7 7 2 3 0
|
||||
8 7 2 4 0
|
||||
12 7 2 5 0
|
||||
9 7 2 6 0
|
||||
12 7 2 8 0
|
||||
10 7 2 9 0
|
||||
10 7 2 10 0
|
||||
10 7 3 0
|
||||
25 7 3 1 0
|
||||
3 7 3 2 0
|
||||
22 7 3 4 0
|
||||
4 7 3 5 0
|
||||
4 7 3 6 0
|
||||
15 7 3 8 0
|
||||
3 7 3 9 0
|
||||
4 7 3 10 0
|
||||
1 7 3 11 0
|
||||
19 7 4 0
|
||||
32 7 4 1 0
|
||||
5 7 4 2 0
|
||||
15 7 4 3 0
|
||||
12 7 4 5 0
|
||||
8 7 4 6 0
|
||||
15 7 4 8 0
|
||||
5 7 4 9 0
|
||||
9 7 4 10 0
|
||||
20 7 5 0
|
||||
49 7 5 1 0
|
||||
21 7 5 2 0
|
||||
3 7 5 3 0
|
||||
8 7 5 4 0
|
||||
29 7 5 6 0
|
||||
45 7 5 8 0
|
||||
18 7 5 9 0
|
||||
13 7 5 10 0
|
||||
1 7 5 11 0
|
||||
19 7 6 0
|
||||
40 7 6 1 0
|
||||
6 7 6 2 0
|
||||
4 7 6 3 0
|
||||
8 7 6 4 0
|
||||
24 7 6 5 0
|
||||
39 7 6 8 0
|
||||
11 7 6 9 0
|
||||
32 7 6 10 0
|
||||
77 7 8 0
|
||||
358 7 8 1 0
|
||||
27 7 8 2 0
|
||||
14 7 8 3 0
|
||||
22 7 8 4 0
|
||||
23 7 8 5 0
|
||||
35 7 8 6 0
|
||||
28 7 8 9 0
|
||||
29 7 8 10 0
|
||||
3 7 8 11 0
|
||||
26 7 9 0
|
||||
21 7 9 1 0
|
||||
15 7 9 2 0
|
||||
4 7 9 3 0
|
||||
6 7 9 4 0
|
||||
32 7 9 5 0
|
||||
15 7 9 6 0
|
||||
31 7 9 8 0
|
||||
9 7 9 10 0
|
||||
17 7 10 0
|
||||
31 7 10 1 0
|
||||
10 7 10 2 0
|
||||
10 7 10 4 0
|
||||
25 7 10 5 0
|
||||
35 7 10 6 0
|
||||
27 7 10 8 0
|
||||
22 7 10 9 0
|
||||
1 7 10 11 0
|
||||
4 7 11 0
|
||||
1 7 11 1 0
|
||||
1 7 11 4 0
|
||||
2566 8 0
|
||||
250 8 1 0
|
||||
58 8 1 2 0
|
||||
67 8 1 3 0
|
||||
87 8 1 4 0
|
||||
127 8 1 5 0
|
||||
119 8 1 6 0
|
||||
597 8 1 7 0
|
||||
78 8 1 9 0
|
||||
114 8 1 10 0
|
||||
5 8 1 11 0
|
||||
24 8 2 0
|
||||
27 8 2 1 0
|
||||
14 8 2 3 0
|
||||
16 8 2 4 0
|
||||
18 8 2 5 0
|
||||
13 8 2 6 0
|
||||
20 8 2 7 0
|
||||
11 8 2 9 0
|
||||
16 8 2 10 0
|
||||
24 8 3 0
|
||||
28 8 3 1 0
|
||||
3 8 3 2 0
|
||||
41 8 3 4 0
|
||||
10 8 3 5 0
|
||||
6 8 3 6 0
|
||||
29 8 3 7 0
|
||||
4 8 3 9 0
|
||||
11 8 3 10 0
|
||||
1 8 3 11 0
|
||||
57 8 4 0
|
||||
65 8 4 1 0
|
||||
13 8 4 2 0
|
||||
41 8 4 3 0
|
||||
17 8 4 5 0
|
||||
21 8 4 6 0
|
||||
46 8 4 7 0
|
||||
15 8 4 9 0
|
||||
17 8 4 10 0
|
||||
52 8 5 0
|
||||
64 8 5 1 0
|
||||
25 8 5 2 0
|
||||
11 8 5 3 0
|
||||
18 8 5 4 0
|
||||
66 8 5 6 0
|
||||
37 8 5 7 0
|
||||
48 8 5 9 0
|
||||
43 8 5 10 0
|
||||
2 8 5 11 0
|
||||
131 8 6 0
|
||||
162 8 6 1 0
|
||||
22 8 6 2 0
|
||||
15 8 6 3 0
|
||||
44 8 6 4 0
|
||||
91 8 6 5 0
|
||||
98 8 6 7 0
|
||||
51 8 6 9 0
|
||||
126 8 6 10 0
|
||||
4 8 6 11 0
|
||||
101 8 7 0
|
||||
777 8 7 1 0
|
||||
19 8 7 2 0
|
||||
26 8 7 3 0
|
||||
34 8 7 4 0
|
||||
38 8 7 5 0
|
||||
46 8 7 6 0
|
||||
45 8 7 9 0
|
||||
51 8 7 10 0
|
||||
1 8 7 11 0
|
||||
66 8 9 0
|
||||
46 8 9 1 0
|
||||
10 8 9 2 0
|
||||
18 8 9 3 0
|
||||
17 8 9 4 0
|
||||
31 8 9 5 0
|
||||
36 8 9 6 0
|
||||
50 8 9 7 0
|
||||
41 8 9 10 0
|
||||
159 8 10 0
|
||||
111 8 10 1 0
|
||||
20 8 10 2 0
|
||||
14 8 10 3 0
|
||||
32 8 10 4 0
|
||||
35 8 10 5 0
|
||||
118 8 10 6 0
|
||||
56 8 10 7 0
|
||||
55 8 10 9 0
|
||||
3 8 10 11 0
|
||||
9 8 11 0
|
||||
1 8 11 1 0
|
||||
1 8 11 5 0
|
||||
2 8 11 7 0
|
||||
1 8 11 10 0
|
||||
1311 9 0
|
||||
47 9 1 0
|
||||
14 9 1 2 0
|
||||
10 9 1 3 0
|
||||
16 9 1 4 0
|
||||
39 9 1 5 0
|
||||
60 9 1 6 0
|
||||
33 9 1 7 0
|
||||
72 9 1 8 0
|
||||
24 9 1 10 0
|
||||
13 9 2 0
|
||||
11 9 2 1 0
|
||||
4 9 2 3 0
|
||||
6 9 2 4 0
|
||||
20 9 2 5 0
|
||||
21 9 2 6 0
|
||||
25 9 2 7 0
|
||||
21 9 2 8 0
|
||||
15 9 2 10 0
|
||||
6 9 3 0
|
||||
6 9 3 1 0
|
||||
2 9 3 2 0
|
||||
8 9 3 4 0
|
||||
4 9 3 5 0
|
||||
5 9 3 7 0
|
||||
7 9 3 8 0
|
||||
1 9 3 10 0
|
||||
22 9 4 0
|
||||
17 9 4 1 0
|
||||
15 9 4 2 0
|
||||
11 9 4 3 0
|
||||
8 9 4 5 0
|
||||
18 9 4 6 0
|
||||
7 9 4 7 0
|
||||
18 9 4 8 0
|
||||
10 9 4 10 0
|
||||
47 9 5 0
|
||||
41 9 5 1 0
|
||||
11 9 5 2 0
|
||||
7 9 5 3 0
|
||||
16 9 5 4 0
|
||||
60 9 5 6 0
|
||||
18 9 5 7 0
|
||||
50 9 5 8 0
|
||||
35 9 5 10 0
|
||||
60 9 6 0
|
||||
60 9 6 1 0
|
||||
21 9 6 2 0
|
||||
7 9 6 3 0
|
||||
17 9 6 4 0
|
||||
66 9 6 5 0
|
||||
40 9 6 7 0
|
||||
63 9 6 8 0
|
||||
98 9 6 10 0
|
||||
3 9 6 11 0
|
||||
39 9 7 0
|
||||
33 9 7 1 0
|
||||
13 9 7 2 0
|
||||
4 9 7 3 0
|
||||
8 9 7 4 0
|
||||
23 9 7 5 0
|
||||
18 9 7 6 0
|
||||
51 9 7 8 0
|
||||
26 9 7 10 0
|
||||
114 9 8 0
|
||||
78 9 8 1 0
|
||||
16 9 8 2 0
|
||||
12 9 8 3 0
|
||||
17 9 8 4 0
|
||||
44 9 8 5 0
|
||||
39 9 8 6 0
|
||||
85 9 8 7 0
|
||||
61 9 8 10 0
|
||||
2 9 8 11 0
|
||||
54 9 10 0
|
||||
31 9 10 1 0
|
||||
14 9 10 2 0
|
||||
4 9 10 3 0
|
||||
4 9 10 4 0
|
||||
61 9 10 5 0
|
||||
98 9 10 6 0
|
||||
14 9 10 7 0
|
||||
76 9 10 8 0
|
||||
1 9 10 11 0
|
||||
16 9 11 0
|
||||
1 9 11 1 0
|
||||
1 9 11 10 0
|
||||
1117 10 0
|
||||
83 10 1 0
|
||||
13 10 1 2 0
|
||||
15 10 1 3 0
|
||||
20 10 1 4 0
|
||||
52 10 1 5 0
|
||||
88 10 1 6 0
|
||||
39 10 1 7 0
|
||||
89 10 1 8 0
|
||||
25 10 1 9 0
|
||||
1 10 1 11 0
|
||||
8 10 2 0
|
||||
10 10 2 1 0
|
||||
2 10 2 3 0
|
||||
13 10 2 5 0
|
||||
16 10 2 6 0
|
||||
15 10 2 7 0
|
||||
13 10 2 8 0
|
||||
7 10 2 9 0
|
||||
7 10 3 0
|
||||
8 10 3 1 0
|
||||
3 10 3 2 0
|
||||
11 10 3 4 0
|
||||
3 10 3 5 0
|
||||
5 10 3 6 0
|
||||
5 10 3 7 0
|
||||
7 10 3 8 0
|
||||
3 10 3 9 0
|
||||
15 10 4 0
|
||||
14 10 4 1 0
|
||||
1 10 4 2 0
|
||||
24 10 4 3 0
|
||||
5 10 4 5 0
|
||||
16 10 4 6 0
|
||||
10 10 4 7 0
|
||||
23 10 4 8 0
|
||||
4 10 4 9 0
|
||||
52 10 5 0
|
||||
57 10 5 1 0
|
||||
13 10 5 2 0
|
||||
2 10 5 3 0
|
||||
9 10 5 4 0
|
||||
122 10 5 6 0
|
||||
25 10 5 7 0
|
||||
53 10 5 8 0
|
||||
56 10 5 9 0
|
||||
1 10 5 11 0
|
||||
1807 10 6 0
|
||||
407 10 6 1 0
|
||||
27 10 6 2 0
|
||||
29 10 6 3 0
|
||||
77 10 6 4 0
|
||||
313 10 6 5 0
|
||||
82 10 6 7 0
|
||||
304 10 6 8 0
|
||||
117 10 6 9 0
|
||||
10 10 6 11 0
|
||||
39 10 7 0
|
||||
40 10 7 1 0
|
||||
11 10 7 2 0
|
||||
3 10 7 3 0
|
||||
13 10 7 4 0
|
||||
19 10 7 5 0
|
||||
32 10 7 6 0
|
||||
18 10 7 8 0
|
||||
15 10 7 9 0
|
||||
170 10 8 0
|
||||
128 10 8 1 0
|
||||
17 10 8 2 0
|
||||
15 10 8 3 0
|
||||
30 10 8 4 0
|
||||
46 10 8 5 0
|
||||
100 10 8 6 0
|
||||
68 10 8 7 0
|
||||
48 10 8 9 0
|
||||
53 10 9 0
|
||||
34 10 9 1 0
|
||||
27 10 9 2 0
|
||||
4 10 9 3 0
|
||||
15 10 9 4 0
|
||||
38 10 9 5 0
|
||||
75 10 9 6 0
|
||||
24 10 9 7 0
|
||||
67 10 9 8 0
|
||||
1 10 9 11 0
|
||||
8 10 11 0
|
||||
289 11 0
|
||||
2 11 1 0
|
||||
1 11 1 2 0
|
||||
1 11 1 3 0
|
||||
1 11 1 7 0
|
||||
3 11 1 8 0
|
||||
1 11 2 4 0
|
||||
1 11 2 7 0
|
||||
1 11 2 8 0
|
||||
1 11 2 9 0
|
||||
3 11 3 0
|
||||
2 11 3 4 0
|
||||
1 11 4 0
|
||||
2 11 4 1 0
|
||||
1 11 4 7 0
|
||||
1 11 4 8 0
|
||||
1 11 5 0
|
||||
1 11 5 1 0
|
||||
1 11 5 3 0
|
||||
1 11 5 6 0
|
||||
1 11 5 7 0
|
||||
1 11 5 8 0
|
||||
1 11 5 9 0
|
||||
2 11 6 0
|
||||
1 11 6 3 0
|
||||
1 11 6 5 0
|
||||
2 11 6 10 0
|
||||
1 11 7 0
|
||||
1 11 7 1 0
|
||||
1 11 7 4 0
|
||||
1 11 7 8 0
|
||||
1 11 7 9 0
|
||||
3 11 8 0
|
||||
1 11 8 1 0
|
||||
1 11 8 5 0
|
||||
3 11 8 7 0
|
||||
1 11 9 4 0
|
||||
1 11 9 5 0
|
||||
1 11 10 1 0
|
||||
1 11 10 4 0
|
||||
1 11 10 5 0
|
||||
0
|
||||
"ANNIE YOUNG"
|
||||
"CASPER HILL"
|
||||
"HASHIM YONIS"
|
||||
"ISHMAEL ISRAEL"
|
||||
"JASON STONE"
|
||||
"JOHN ERWIN"
|
||||
"MARY LYNN MCPHERSON"
|
||||
"MEG FORNEY"
|
||||
"STEVE BARLAND"
|
||||
"TOM NORDYKE"
|
||||
"UWI"
|
||||
"2013-Park-At-Large-CVR"
|
|
@ -1,13 +0,0 @@
|
|||
Stage:,1,,2,,3,,4,,5,,6,,7,,8,,9,
|
||||
Comment:,First preferences,,"Exclusion of CASPER HILL, UWI",,Exclusion of ISHMAEL ISRAEL,,Surplus of JOHN ERWIN,,Exclusion of MARY LYNN MCPHERSON,,Exclusion of STEVE BARLAND,,Exclusion of HASHIM YONIS,,Exclusion of JASON STONE,,Exclusion of TOM NORDYKE,
|
||||
ANNIE YOUNG,9294,H,9452,H,9983,H,10055.9492,H,11055.9536,H,11528.0696,H,12030.423,H,13905.6980000001,H,13905.6980000001,EL
|
||||
CASPER HILL,1280,H,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
HASHIM YONIS,3762,H,3799,H,4329,H,4333.6314,H,4477.9662,H,4559.0778,H,0,EX,0,EX,0,EX
|
||||
ISHMAEL ISRAEL,3305,H,3374,H,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
JASON STONE,5357,H,5477,H,5766,H,5811.663,H,6090.7604,H,6544.1926,H,6736.6204,H,0,EX,0,EX
|
||||
JOHN ERWIN,14678,H,14866,H,15148,H,14866,EL,14866,EL,14866,EL,14866,EL,14866,EL,14866,EL
|
||||
MARY LYNN MCPHERSON,3373,H,3479,H,3681,H,3688.44,H,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
MEG FORNEY,7856,H,8031,H,8403,H,8423.646,H,9162.576,H,9833.413,H,10160.599,H,10973.3376,H,10973.3376,EL
|
||||
STEVE BARLAND,3705,H,3803,H,3893,H,3901.5374,H,4114.1884,H,0,EX,0,EX,0,EX,0,EX
|
||||
TOM NORDYKE,6511,H,6595,H,6723,H,6801.6408,H,7044.4406,H,7580.0216,H,7733.3192,H,8752.5316,H,8752.5316,EX
|
||||
UWI,342,H,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
|
Binary file not shown.
|
@ -1,93 +0,0 @@
|
|||
# Comment: 2021 Minneapolis Board of Estimate & Taxation election - all votes - Minneapolis STV
|
||||
# Source: https://vote.minneapolismn.gov/results-data/election-results/2021/bet/
|
||||
# Contributor: RunasSudo
|
||||
5 2
|
||||
49712 0
|
||||
2262 1 0
|
||||
125 1 2 0
|
||||
311 1 2 3 0
|
||||
279 1 2 4 0
|
||||
5 1 2 5 0
|
||||
223 1 3 0
|
||||
517 1 3 2 0
|
||||
428 1 3 4 0
|
||||
6 1 3 5 0
|
||||
470 1 4 0
|
||||
530 1 4 2 0
|
||||
607 1 4 3 0
|
||||
24 1 4 5 0
|
||||
27 1 5 0
|
||||
1 1 5 2 0
|
||||
4860 2 0
|
||||
131 2 1 0
|
||||
368 2 1 3 0
|
||||
231 2 1 4 0
|
||||
7 2 1 5 0
|
||||
3336 2 3 0
|
||||
1042 2 3 1 0
|
||||
5358 2 3 4 0
|
||||
77 2 3 5 0
|
||||
2518 2 4 0
|
||||
643 2 4 1 0
|
||||
2140 2 4 3 0
|
||||
34 2 4 5 0
|
||||
37 2 5 0
|
||||
1 2 5 3 0
|
||||
3 2 5 4 0
|
||||
9309 3 0
|
||||
301 3 1 0
|
||||
847 3 1 2 0
|
||||
881 3 1 4 0
|
||||
6 3 1 5 0
|
||||
3179 3 2 0
|
||||
2361 3 2 1 0
|
||||
3318 3 2 4 0
|
||||
84 3 2 5 0
|
||||
1731 3 4 0
|
||||
1441 3 4 1 0
|
||||
2013 3 4 2 0
|
||||
36 3 4 5 0
|
||||
81 3 5 0
|
||||
3 3 5 1 0
|
||||
5 3 5 2 0
|
||||
1 3 5 4 0
|
||||
23165 4 0
|
||||
1445 4 1 0
|
||||
1306 4 1 2 0
|
||||
2250 4 1 3 0
|
||||
76 4 1 5 0
|
||||
4254 4 2 0
|
||||
914 4 2 1 0
|
||||
2229 4 2 3 0
|
||||
65 4 2 5 0
|
||||
2609 4 3 0
|
||||
1716 4 3 1 0
|
||||
2389 4 3 2 0
|
||||
35 4 3 5 0
|
||||
206 4 5 0
|
||||
4 4 5 1 0
|
||||
3 4 5 2 0
|
||||
6 4 5 3 0
|
||||
679 5 0
|
||||
5 5 1 0
|
||||
1 5 1 2 0
|
||||
1 5 1 3 0
|
||||
2 5 1 4 0
|
||||
3 5 2 0
|
||||
1 5 2 1 0
|
||||
5 5 2 3 0
|
||||
4 5 2 4 0
|
||||
5 5 3 0
|
||||
2 5 3 1 0
|
||||
12 5 3 2 0
|
||||
12 5 4 0
|
||||
10 5 4 1 0
|
||||
7 5 4 2 0
|
||||
6 5 4 3 0
|
||||
0
|
||||
"Kevin Nikiforakis"
|
||||
"Pine Salica"
|
||||
"Samantha \"Sam\" Pree-Stinson"
|
||||
"Steve Brandt"
|
||||
"UWI"
|
||||
"2021-BET-Cast-Vote-Record"
|
|
@ -1,9 +0,0 @@
|
|||
Stage:,1,,2,,3,,4,
|
||||
Comment:,First preferences,,"Exclusion of Kevin Nikiforakis, UWI",,Surplus of Steve Brandt,,Exclusion of Pine Salica,
|
||||
Kevin Nikiforakis,5815,H,0,EX,0,EX,0,EX
|
||||
Pine Salica,20786,H,21521,H,24137.4788,H,24137.4788,EX
|
||||
"Samantha ""Sam"" Pree-Stinson",25597,H,26791,H,29494.6198,H,29494.6198,EL
|
||||
Steve Brandt,42672,H,44340,H,31876,EL,31876,EL
|
||||
UWI,755,H,0,EX,0,EX,0,EX
|
||||
Exhausted,49712,,52685,,59828.8754,,59828.8754,
|
||||
Loss by fraction,0,,0,,0.026,,0.026,
|
|
Binary file not shown.
|
@ -1,370 +0,0 @@
|
|||
# Source: Converted by RunasSudo from a file named 2021-Park-AL-Cast-Vote-Record.csv at https://vote.minneapolismn.gov/results-data/election-results/2021/park-board-at-large/, using minneapolis_to_blt.py
|
||||
8 3
|
||||
38687 0
|
||||
3326 1 0
|
||||
216 1 2 0
|
||||
132 1 2 3 0
|
||||
156 1 2 4 0
|
||||
199 1 2 5 0
|
||||
211 1 2 6 0
|
||||
180 1 2 7 0
|
||||
3 1 2 8 0
|
||||
271 1 3 0
|
||||
213 1 3 2 0
|
||||
116 1 3 4 0
|
||||
547 1 3 5 0
|
||||
608 1 3 6 0
|
||||
178 1 3 7 0
|
||||
2 1 3 8 0
|
||||
207 1 4 0
|
||||
151 1 4 2 0
|
||||
131 1 4 3 0
|
||||
107 1 4 5 0
|
||||
228 1 4 6 0
|
||||
192 1 4 7 0
|
||||
4 1 4 8 0
|
||||
222 1 5 0
|
||||
141 1 5 2 0
|
||||
313 1 5 3 0
|
||||
104 1 5 4 0
|
||||
586 1 5 6 0
|
||||
182 1 5 7 0
|
||||
3 1 5 8 0
|
||||
518 1 6 0
|
||||
168 1 6 2 0
|
||||
560 1 6 3 0
|
||||
195 1 6 4 0
|
||||
754 1 6 5 0
|
||||
331 1 6 7 0
|
||||
8 1 6 8 0
|
||||
279 1 7 0
|
||||
157 1 7 2 0
|
||||
149 1 7 3 0
|
||||
154 1 7 4 0
|
||||
235 1 7 5 0
|
||||
333 1 7 6 0
|
||||
7 1 7 8 0
|
||||
19 1 8 0
|
||||
1 1 8 5 0
|
||||
2 1 8 6 0
|
||||
3172 2 0
|
||||
285 2 1 0
|
||||
135 2 1 3 0
|
||||
160 2 1 4 0
|
||||
219 2 1 5 0
|
||||
269 2 1 6 0
|
||||
140 2 1 7 0
|
||||
6 2 1 8 0
|
||||
95 2 3 0
|
||||
125 2 3 1 0
|
||||
52 2 3 4 0
|
||||
101 2 3 5 0
|
||||
75 2 3 6 0
|
||||
76 2 3 7 0
|
||||
7 2 3 8 0
|
||||
145 2 4 0
|
||||
179 2 4 1 0
|
||||
73 2 4 3 0
|
||||
77 2 4 5 0
|
||||
108 2 4 6 0
|
||||
79 2 4 7 0
|
||||
5 2 4 8 0
|
||||
407 2 5 0
|
||||
268 2 5 1 0
|
||||
187 2 5 3 0
|
||||
51 2 5 4 0
|
||||
519 2 5 6 0
|
||||
90 2 5 7 0
|
||||
10 2 5 8 0
|
||||
438 2 6 0
|
||||
241 2 6 1 0
|
||||
96 2 6 3 0
|
||||
95 2 6 4 0
|
||||
488 2 6 5 0
|
||||
130 2 6 7 0
|
||||
3 2 6 8 0
|
||||
214 2 7 0
|
||||
137 2 7 1 0
|
||||
119 2 7 3 0
|
||||
296 2 7 4 0
|
||||
146 2 7 5 0
|
||||
162 2 7 6 0
|
||||
5 2 7 8 0
|
||||
20 2 8 0
|
||||
1 2 8 3 0
|
||||
2 2 8 5 0
|
||||
1 2 8 6 0
|
||||
2 2 8 7 0
|
||||
2392 3 0
|
||||
171 3 1 0
|
||||
62 3 1 2 0
|
||||
58 3 1 4 0
|
||||
306 3 1 5 0
|
||||
476 3 1 6 0
|
||||
109 3 1 7 0
|
||||
1 3 1 8 0
|
||||
83 3 2 0
|
||||
89 3 2 1 0
|
||||
55 3 2 4 0
|
||||
80 3 2 5 0
|
||||
65 3 2 6 0
|
||||
166 3 2 7 0
|
||||
4 3 2 8 0
|
||||
44 3 4 0
|
||||
64 3 4 1 0
|
||||
24 3 4 2 0
|
||||
42 3 4 5 0
|
||||
68 3 4 6 0
|
||||
31 3 4 7 0
|
||||
124 3 5 0
|
||||
271 3 5 1 0
|
||||
104 3 5 2 0
|
||||
53 3 5 4 0
|
||||
455 3 5 6 0
|
||||
109 3 5 7 0
|
||||
158 3 6 0
|
||||
356 3 6 1 0
|
||||
67 3 6 2 0
|
||||
81 3 6 4 0
|
||||
296 3 6 5 0
|
||||
99 3 6 7 0
|
||||
3 3 6 8 0
|
||||
111 3 7 0
|
||||
153 3 7 1 0
|
||||
108 3 7 2 0
|
||||
44 3 7 4 0
|
||||
150 3 7 5 0
|
||||
126 3 7 6 0
|
||||
3 3 7 8 0
|
||||
7 3 8 0
|
||||
2 3 8 7 0
|
||||
3387 4 0
|
||||
365 4 1 0
|
||||
209 4 1 2 0
|
||||
167 4 1 3 0
|
||||
128 4 1 5 0
|
||||
300 4 1 6 0
|
||||
329 4 1 7 0
|
||||
12 4 1 8 0
|
||||
216 4 2 0
|
||||
217 4 2 1 0
|
||||
79 4 2 3 0
|
||||
70 4 2 5 0
|
||||
91 4 2 6 0
|
||||
103 4 2 7 0
|
||||
7 4 2 8 0
|
||||
51 4 3 0
|
||||
89 4 3 1 0
|
||||
41 4 3 2 0
|
||||
48 4 3 5 0
|
||||
86 4 3 6 0
|
||||
46 4 3 7 0
|
||||
1 4 3 8 0
|
||||
91 4 5 0
|
||||
104 4 5 1 0
|
||||
45 4 5 2 0
|
||||
45 4 5 3 0
|
||||
190 4 5 6 0
|
||||
70 4 5 7 0
|
||||
472 4 6 0
|
||||
326 4 6 1 0
|
||||
103 4 6 2 0
|
||||
146 4 6 3 0
|
||||
166 4 6 5 0
|
||||
200 4 6 7 0
|
||||
6 4 6 8 0
|
||||
1557 4 7 0
|
||||
1583 4 7 1 0
|
||||
127 4 7 2 0
|
||||
71 4 7 3 0
|
||||
203 4 7 5 0
|
||||
295 4 7 6 0
|
||||
21 4 7 8 0
|
||||
38 4 8 0
|
||||
2 4 8 1 0
|
||||
2 4 8 3 0
|
||||
1 4 8 7 0
|
||||
2960 5 0
|
||||
247 5 1 0
|
||||
162 5 1 2 0
|
||||
310 5 1 3 0
|
||||
63 5 1 4 0
|
||||
509 5 1 6 0
|
||||
194 5 1 7 0
|
||||
2 5 1 8 0
|
||||
426 5 2 0
|
||||
260 5 2 1 0
|
||||
112 5 2 3 0
|
||||
40 5 2 4 0
|
||||
538 5 2 6 0
|
||||
124 5 2 7 0
|
||||
6 5 2 8 0
|
||||
159 5 3 0
|
||||
216 5 3 1 0
|
||||
96 5 3 2 0
|
||||
29 5 3 4 0
|
||||
281 5 3 6 0
|
||||
108 5 3 7 0
|
||||
2 5 3 8 0
|
||||
84 5 4 0
|
||||
60 5 4 1 0
|
||||
40 5 4 2 0
|
||||
38 5 4 3 0
|
||||
90 5 4 6 0
|
||||
56 5 4 7 0
|
||||
923 5 6 0
|
||||
1297 5 6 1 0
|
||||
782 5 6 2 0
|
||||
470 5 6 3 0
|
||||
128 5 6 4 0
|
||||
326 5 6 7 0
|
||||
5 5 6 8 0
|
||||
191 5 7 0
|
||||
184 5 7 1 0
|
||||
112 5 7 2 0
|
||||
98 5 7 3 0
|
||||
77 5 7 4 0
|
||||
249 5 7 6 0
|
||||
5 5 7 8 0
|
||||
13 5 8 0
|
||||
1 5 8 1 0
|
||||
1 5 8 7 0
|
||||
10018 6 0
|
||||
893 6 1 0
|
||||
415 6 1 2 0
|
||||
1191 6 1 3 0
|
||||
282 6 1 4 0
|
||||
1223 6 1 5 0
|
||||
516 6 1 7 0
|
||||
7 6 1 8 0
|
||||
554 6 2 0
|
||||
385 6 2 1 0
|
||||
160 6 2 3 0
|
||||
134 6 2 4 0
|
||||
1125 6 2 5 0
|
||||
231 6 2 7 0
|
||||
6 6 2 8 0
|
||||
335 6 3 0
|
||||
525 6 3 1 0
|
||||
146 6 3 2 0
|
||||
109 6 3 4 0
|
||||
531 6 3 5 0
|
||||
278 6 3 7 0
|
||||
4 6 3 8 0
|
||||
455 6 4 0
|
||||
263 6 4 1 0
|
||||
117 6 4 2 0
|
||||
136 6 4 3 0
|
||||
208 6 4 5 0
|
||||
204 6 4 7 0
|
||||
2 6 4 8 0
|
||||
1157 6 5 0
|
||||
2700 6 5 1 0
|
||||
3366 6 5 2 0
|
||||
630 6 5 3 0
|
||||
208 6 5 4 0
|
||||
594 6 5 7 0
|
||||
13 6 5 8 0
|
||||
630 6 7 0
|
||||
589 6 7 1 0
|
||||
295 6 7 2 0
|
||||
257 6 7 3 0
|
||||
206 6 7 4 0
|
||||
459 6 7 5 0
|
||||
5 6 7 8 0
|
||||
46 6 8 0
|
||||
1 6 8 2 0
|
||||
2 6 8 3 0
|
||||
1 6 8 4 0
|
||||
4967 7 0
|
||||
476 7 1 0
|
||||
210 7 1 2 0
|
||||
225 7 1 3 0
|
||||
351 7 1 4 0
|
||||
357 7 1 5 0
|
||||
474 7 1 6 0
|
||||
5 7 1 8 0
|
||||
284 7 2 0
|
||||
194 7 2 1 0
|
||||
268 7 2 3 0
|
||||
317 7 2 4 0
|
||||
200 7 2 5 0
|
||||
219 7 2 6 0
|
||||
15 7 2 8 0
|
||||
150 7 3 0
|
||||
163 7 3 1 0
|
||||
143 7 3 2 0
|
||||
84 7 3 4 0
|
||||
165 7 3 5 0
|
||||
182 7 3 6 0
|
||||
2 7 3 8 0
|
||||
1488 7 4 0
|
||||
3900 7 4 1 0
|
||||
139 7 4 2 0
|
||||
96 7 4 3 0
|
||||
608 7 4 5 0
|
||||
468 7 4 6 0
|
||||
26 7 4 8 0
|
||||
276 7 5 0
|
||||
546 7 5 1 0
|
||||
162 7 5 2 0
|
||||
168 7 5 3 0
|
||||
238 7 5 4 0
|
||||
557 7 5 6 0
|
||||
4 7 5 8 0
|
||||
660 7 6 0
|
||||
642 7 6 1 0
|
||||
194 7 6 2 0
|
||||
237 7 6 3 0
|
||||
292 7 6 4 0
|
||||
509 7 6 5 0
|
||||
6 7 6 8 0
|
||||
30 7 8 0
|
||||
1 7 8 1 0
|
||||
1 7 8 2 0
|
||||
1 7 8 3 0
|
||||
1 7 8 4 0
|
||||
1 7 8 5 0
|
||||
518 8 0
|
||||
3 8 1 0
|
||||
1 8 1 4 0
|
||||
1 8 1 6 0
|
||||
2 8 1 7 0
|
||||
2 8 2 0
|
||||
1 8 2 1 0
|
||||
1 8 2 3 0
|
||||
2 8 2 5 0
|
||||
2 8 2 6 0
|
||||
1 8 2 7 0
|
||||
1 8 3 2 0
|
||||
1 8 3 7 0
|
||||
7 8 4 0
|
||||
1 8 4 1 0
|
||||
1 8 4 6 0
|
||||
1 8 4 7 0
|
||||
3 8 5 0
|
||||
1 8 5 1 0
|
||||
1 8 5 7 0
|
||||
4 8 6 0
|
||||
1 8 6 1 0
|
||||
3 8 6 2 0
|
||||
1 8 6 4 0
|
||||
4 8 6 5 0
|
||||
4 8 6 7 0
|
||||
1 8 7 0
|
||||
1 8 7 1 0
|
||||
2 8 7 2 0
|
||||
1 8 7 3 0
|
||||
1 8 7 4 0
|
||||
1 8 7 5 0
|
||||
1 8 7 6 0
|
||||
0
|
||||
"Alicia D. Smith"
|
||||
"Charles Rucker"
|
||||
"Katherine Kelly"
|
||||
"Londel French"
|
||||
"Mary McKelvey"
|
||||
"Meg Forney"
|
||||
"Tom Olsen"
|
||||
"UWI"
|
||||
"2021-Park-AL-Cast-Vote-Record"
|
|
@ -1,12 +0,0 @@
|
|||
Stage:,1,,2,,3,,4,,5,,6,,7,
|
||||
Comment:,First preferences,,Exclusion of UWI,,Surplus of Meg Forney,,Exclusion of Katherine Kelly,,Exclusion of Charles Rucker,,Exclusion of Londel French,,Surplus of Tom Olsen,
|
||||
Alicia D. Smith,12799,H,12806,H,13516.896,H,15138.321,H,16868.766,H,19298.057,H,19656.5888,H
|
||||
Charles Rucker,9711,H,9720,H,10128.043,H,10760.965,H,0,EX,0,EX,0,EX
|
||||
Katherine Kelly,7270,H,7272,H,7575.01,H,0,EX,0,EX,0,EX,0,EX
|
||||
Londel French,11906,H,11916,H,12133.759,H,12504.872,H,13393.91,H,0,EX,0,EX
|
||||
Mary McKelvey,12074,H,12079,H,13440.504,H,14935.871,H,17317.496,H,18298.152,H,18458.3578,H
|
||||
Meg Forney,31612,H,31629,H,26663,EL,26663,EL,26663,EL,26663,EL,26663,EL
|
||||
Tom Olsen,20702,H,20710,H,21093.865,H,21934.511,H,23424.778,H,27774.806,H,26663,EL
|
||||
UWI,576,H,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
Exhausted,38687,,39205,,40785.676,,43399.213,,47668.803,,53302.738,,53894.74,
|
||||
Loss by fraction,0,,0,,0.247,,0.247,,0.247,,0.247,,1.3134,
|
|
Binary file not shown.
|
@ -1,17 +0,0 @@
|
|||
# Comment: BY STV
|
||||
# Comment: Number of truncated papers: 0
|
||||
# Comment: AKA R038 in Wichmann's database
|
||||
# Source: Nicolaus Tideman via Warren D Smith <https://rangevoting.org/TidemanData.html>
|
||||
# Contributor: RunasSudo
|
||||
18 3
|
||||
1 15 9 5 6 8 0
|
||||
1 18 7 16 1 12 14 0
|
||||
1 14 9 17 13 15 5 2 12 16 8 0
|
||||
1 6 5 3 4 15 12 9 7 1 14 13 16 18 17 8 2 10 11 0
|
||||
1 12 1 17 9 6 5 2 0
|
||||
1 15 8 5 3 0
|
||||
1 8 15 2 3 5 4 1 9 0
|
||||
1 3 5 1 4 11 2 8 0
|
||||
1 17 4 9 14 16 6 1 13 8 15 3 18 5 12 11 10 2 7 0
|
||||
0
|
||||
"A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "f:a33"
|
|
@ -1,70 +0,0 @@
|
|||
# Comment: BY STV
|
||||
# Comment: Number of truncated papers: 0
|
||||
# Comment: AKA R039 in Wichmann's database
|
||||
# Source: Nicolaus Tideman via Warren D Smith <https://rangevoting.org/TidemanData.html>
|
||||
14 12
|
||||
1 9 8 4 5 0
|
||||
1 9 12 1 14 4 10 0
|
||||
1 14 1 2 10 5 7 11 13 3 4 12 0
|
||||
1 2 7 10 6 0
|
||||
1 7 6 0
|
||||
1 7 11 10 1 5 6 14 2 8 4 12 3 0
|
||||
1 7 8 10 6 12 14 4 11 0
|
||||
1 11 4 10 3 13 8 14 2 12 7 6 1 0
|
||||
1 11 3 7 10 4 13 2 1 6 14 8 12 0
|
||||
1 11 1 13 12 9 10 6 5 4 3 2 7 0
|
||||
1 11 14 12 10 1 6 7 8 4 0
|
||||
1 14 3 4 5 12 13 6 9 0
|
||||
1 2 3 11 7 8 14 9 13 0
|
||||
1 3 2 7 11 6 5 14 1 12 8 4 10 0
|
||||
1 3 8 12 1 13 2 14 4 11 10 7 6 0
|
||||
1 3 11 4 5 8 12 10 13 0
|
||||
1 3 11 10 13 4 1 2 5 14 12 7 8 6 0
|
||||
1 5 2 1 8 12 10 0
|
||||
1 5 4 1 2 6 12 10 8 13 14 3 7 0
|
||||
1 5 8 6 7 10 12 1 4 11 0
|
||||
1 2 6 8 4 5 10 12 14 7 1 11 3 0
|
||||
1 2 6 14 10 4 1 8 13 5 12 0
|
||||
1 6 2 10 8 7 11 12 1 14 3 0
|
||||
1 6 14 8 4 5 12 1 0
|
||||
1 6 11 14 7 1 13 8 2 3 10 12 4 5 9 0
|
||||
1 5 10 13 12 2 6 4 11 7 1 8 0
|
||||
1 10 8 13 4 1 7 14 11 6 12 5 2 3 0
|
||||
1 13 8 12 5 14 4 0
|
||||
1 13 12 8 4 0
|
||||
1 13 10 6 4 14 8 11 3 1 7 2 12 5 0
|
||||
1 13 10 6 4 14 8 11 3 1 7 2 12 5 0
|
||||
1 14 12 13 10 5 4 6 2 11 8 7 1 3 9 0
|
||||
1 14 8 10 2 1 12 11 6 5 7 3 4 0
|
||||
1 14 12 2 5 8 10 1 11 6 13 4 3 0
|
||||
1 5 8 14 2 12 6 4 0
|
||||
1 5 12 6 2 1 4 7 10 11 9 8 3 14 13 0
|
||||
1 5 8 14 2 12 4 6 0
|
||||
1 10 12 8 13 3 7 6 5 4 11 14 1 0
|
||||
1 10 2 14 12 8 6 5 11 13 1 4 3 7 9 0
|
||||
1 2 5 12 13 14 8 1 7 10 11 3 6 0
|
||||
1 12 10 13 4 1 11 14 6 0
|
||||
1 12 10 6 13 11 3 2 1 4 14 8 0
|
||||
1 10 1 8 11 7 6 4 13 3 14 2 12 5 9 0
|
||||
1 10 2 8 1 4 5 11 0
|
||||
1 10 14 1 4 5 8 12 6 13 2 3 7 9 11 0
|
||||
1 2 10 1 5 14 4 7 9 12 3 6 0
|
||||
1 1 12 2 10 11 13 3 4 6 7 8 14 0
|
||||
1 1 7 6 10 11 2 9 4 8 12 0
|
||||
1 1 2 13 4 3 10 9 5 11 12 14 8 6 7 0
|
||||
1 5 4 12 10 2 0
|
||||
1 10 4 1 6 8 14 11 12 13 2 7 3 0
|
||||
1 10 4 1 2 14 5 6 13 8 12 0
|
||||
1 2 4 5 1 9 10 12 14 0
|
||||
1 2 4 10 13 14 1 5 11 12 7 3 8 6 0
|
||||
1 4 8 12 5 2 10 0
|
||||
1 4 10 13 9 5 12 8 2 1 14 11 7 6 3 0
|
||||
1 4 10 1 14 11 6 2 5 8 7 13 12 0
|
||||
1 8 10 5 1 6 7 11 4 14 13 3 12 2 9 0
|
||||
1 8 14 13 11 7 12 4 10 1 3 2 6 0
|
||||
1 8 10 14 4 1 11 6 5 12 7 2 0
|
||||
1 8 10 5 6 11 7 3 12 13 4 0
|
||||
1 8 6 7 5 11 13 3 0
|
||||
1 2 0
|
||||
0
|
||||
"A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "f:a34"
|
File diff suppressed because it is too large
Load Diff
|
@ -1,32 +0,0 @@
|
|||
Stage:,1,,2,,3,,4,,5,,6,,7,,8,,9,,10,,11,,12,,13,,14,,15,,16,,17,,18,,19,,20,,21,,22,,23,,24,,25,,26,,27,,28,,29,,30,,31,,32,,33,,34,,35,,36,,37,,38,,39,,40,,41,,42,,43,,44,,45,,46,,47,,48,,49,,50,,51,,52,,53,,54,,55,,56,,57,
|
||||
Comment:,First preferences,,"Surplus of BARR, Andrew",,"Exclusion of WILLIAMS, Robyn",,"Exclusion of WILLIAMS, Robyn",,"Exclusion of HOPPER, Alvin",,"Exclusion of HOPPER, Alvin",,"Exclusion of JOHNSON, Petar",,"Exclusion of JOHNSON, Petar",,"Exclusion of DAMIANO, Marilena",,"Exclusion of DAMIANO, Marilena",,"Exclusion of FORNER, Sophia",,"Exclusion of FORNER, Sophia",,"Exclusion of RUTLEDGE-PRIOR, Serrin",,"Exclusion of RUTLEDGE-PRIOR, Serrin",,"Exclusion of HAYDON, John",,"Exclusion of HAYDON, John",,"Exclusion of O'HARA, Alix",,"Exclusion of O'HARA, Alix",,"Exclusion of BRYANT, Peta Anne",,"Exclusion of BRYANT, Peta Anne",,"Exclusion of SMITH, Julie",,"Exclusion of SMITH, Julie",,"Exclusion of ANGEL, Joy",,"Exclusion of ANGEL, Joy",,"Exclusion of GUMBER, Rattesh",,"Exclusion of GUMBER, Rattesh",,"Exclusion of BREWER, Michael",,"Exclusion of BREWER, Michael",,"Exclusion of PAINE, Bruce",,"Exclusion of PAINE, Bruce",,"Exclusion of FAULKNER, Therese",,"Exclusion of FAULKNER, Therese",,"Exclusion of BOISEN, Adriana",,"Exclusion of BOISEN, Adriana",,"Exclusion of ANDERSON, Judy",,"Exclusion of ANDERSON, Judy",,"Exclusion of JOHNSON, Robert",,"Exclusion of JOHNSON, Robert",,"Exclusion of BOHM, Tim",,"Exclusion of BOHM, Tim",,"Surplus of RATTENBURY, Shane",,"Exclusion of INGRAM, Jacob",,"Exclusion of INGRAM, Jacob",,"Exclusion of INGRAM, Jacob",,"Exclusion of PENTONY, Patrick",,"Exclusion of PENTONY, Patrick",,"Exclusion of PENTONY, Patrick",,"Exclusion of NORTHAM, Maddy",,"Exclusion of NORTHAM, Maddy",,"Exclusion of NORTHAM, Maddy",,"Surplus of STEPHEN-SMITH, Rachel",,"Exclusion of BURCH, Candice",,"Exclusion of BURCH, Candice",,"Exclusion of BURCH, Candice",,"Exclusion of BURCH, Candice",,"Surplus of LEE, Elizabeth",,Bulk election,
|
||||
"BOISEN, Adriana",1250,H,1277.386971,H,1277.386971,H,1277.386971,H,1282.386971,H,1282.386971,H,1287.386971,H,1288.12055,H,1292.12055,H,1292.365076,H,1300.365076,H,1300.365076,H,1304.365076,H,1304.609602,H,1313.609602,H,1314.098655,H,1330.098655,H,1330.587708,H,1359.587708,H,1359.587708,H,1401.587708,H,1403.54392,H,1445.54392,H,1446.032973,H,1460.032973,H,1461.011079,H,1686.011079,H,1689.678977,H,1721.678977,H,1722.16803,H,1776.16803,H,1776.901609,H,37.901609,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
"BREWER, Michael",904,H,930.408865,H,931.408865,H,931.897918,H,934.897918,H,934.897918,H,934.897918,H,934.897918,H,938.897918,H,938.897918,H,951.897918,H,952.142444,H,960.142444,H,960.142444,H,967.142444,H,967.631497,H,981.631497,H,982.12055,H,997.12055,H,997.609603,H,1027.609603,H,1027.854129,H,1066.854129,H,1068.076761,H,1071.076761,H,1071.076761,H,30.076761,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
"RATTENBURY, Shane",6388,H,6703.439228,H,6704.439228,H,6704.928281,H,6711.928281,H,6712.417334,H,6718.417334,H,6718.906387,H,6724.906387,H,6725.150913,H,6735.150913,H,6735.884492,H,6748.884492,H,6749.862598,H,6761.862598,H,6763.08523,H,6819.08523,H,6822.753128,H,6853.753128,H,6854.242181,H,6946.242181,H,6948.442919,H,7020.442919,H,7023.13271,H,7034.13271,H,7034.621763,H,7298.621763,H,7302.534187,H,7373.534187,H,7374.756819,H,7487.756819,H,7490.935663,H,7978.935663,H,7985.293352,H,8078.293352,H,8097.121895,H,8133.121895,H,8134.83358,H,8728.83358,EL,8728.83358,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL
|
||||
"VASSAROTTI, Rebecca",3093,H,3142.394359,H,3143.394359,H,3143.394359,H,3150.394359,H,3150.394359,H,3151.394359,H,3151.394359,H,3157.394359,H,3157.394359,H,3174.394359,H,3175.861518,H,3186.861518,H,3187.106044,H,3194.106044,H,3194.35057,H,3220.35057,H,3220.839623,H,3254.839623,H,3255.084149,H,3337.084149,H,3339.040361,H,3426.040361,H,3427.50752,H,3432.50752,H,3432.752046,H,3729.752046,H,3732.686364,H,3783.686364,H,3785.398049,H,3949.398049,H,3951.598787,H,4800.598787,H,4810.135321,H,4906.135321,H,4915.182802,H,4941.182802,H,4941.427328,H,5552.427328,H,5555.606172,H,5780.295592,H,6028.295592,H,6031.717258,H,6063.99476,H,6167.99476,H,6167.99476,H,6170.684551,H,6636.684551,H,6643.527883,H,6848.685644,H,7343.315104,H,7466.315104,H,7467.455659,H,7479.485935,H,7487.066257,H,8013.066257,H,8013.066257,EL
|
||||
"HAYDON, John",365,H,370.379583,H,370.379583,H,370.868636,H,380.868636,H,381.113162,H,391.113162,H,392.091268,H,392.091268,H,392.091268,H,396.091268,H,396.091268,H,399.091268,H,399.091268,H,7.091268,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
"ANGEL, Joy",435,H,440.868636,H,442.868636,H,443.357689,H,450.357689,H,450.602215,H,452.602215,H,452.602215,H,458.602215,H,458.602215,H,477.602215,H,478.580321,H,495.580321,H,495.580321,H,746.580321,H,748.292006,H,812.292006,H,813.514638,H,825.514638,H,825.514638,H,934.514638,H,936.226323,H,12.226323,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
"GUMBER, Rattesh",929,H,934.379583,H,934.379583,H,934.379583,H,935.379583,H,935.868636,H,935.868636,H,935.868636,H,936.868636,H,936.868636,H,937.868636,H,937.868636,H,937.868636,H,937.868636,H,942.868636,H,942.868636,H,946.868636,H,946.868636,H,949.868636,H,950.357689,H,958.357689,H,958.602215,H,969.602215,H,969.602215,H,6.602215,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
"BURCH, Candice",3978,H,4011.255608,H,4012.255608,H,4012.500134,H,4021.500134,H,4021.500134,H,4022.500134,H,4022.74466,H,4025.74466,H,4025.74466,H,4028.74466,H,4028.989186,H,4035.989186,H,4035.989186,H,4039.989186,H,4039.989186,H,4044.989186,H,4045.722765,H,4046.722765,H,4047.211818,H,4073.211818,H,4073.700871,H,4116.700871,H,4116.700871,H,4336.700871,H,4337.189924,H,4373.189924,H,4374.16803,H,4431.16803,H,4431.412556,H,4455.412556,H,4455.901609,H,4476.901609,H,4478.368768,H,4545.368768,H,4548.058559,H,5049.058559,H,5050.525718,H,5198.525718,H,5199.503824,H,5204.066045,H,5254.066045,H,5254.636322,H,5265.884542,H,6810.884542,H,6810.884542,H,6816.997705,H,6902.997705,H,6903.567982,H,6914.082622,H,6936.292363,H,100.292363,EX,,EX,,EX,0,EX,0,EX,0,EX
|
||||
"LEE, Elizabeth",5040,H,5102.843319,H,5107.843319,H,5108.087845,H,5114.087845,H,5114.087845,H,5116.087845,H,5116.087845,H,5125.087845,H,5125.332371,H,5129.332371,H,5129.576897,H,5131.576897,H,5131.576897,H,5141.576897,H,5141.576897,H,5153.576897,H,5154.06595,H,5161.06595,H,5161.310476,H,5191.310476,H,5192.288582,H,5241.288582,H,5242.022161,H,5471.022161,H,5471.75574,H,5487.75574,H,5488.244793,H,5573.244793,H,5573.733846,H,5595.733846,H,5596.467425,H,5623.467425,H,5623.956478,H,5658.956478,H,5664.825114,H,6265.825114,H,6266.80322,H,6474.80322,H,6477.982064,H,6485.965952,H,6573.965952,H,6573.965952,H,6588.14849,H,8099.14849,H,8099.14849,H,8104.528073,H,8205.528073,H,8205.528073,H,8231.692412,H,8264.081618,H,14074.081618,EL,14074.081618,EL,14074.081618,EL,14074.081618,EL,8434,EL,8434,EL
|
||||
"PENTONY, Patrick",2384,H,2402.584016,H,2402.584016,H,2402.584016,H,2406.584016,H,2406.584016,H,2407.584016,H,2407.584016,H,2409.584016,H,2409.828542,H,2411.828542,H,2411.828542,H,2412.828542,H,2413.073068,H,2415.073068,H,2415.073068,H,2416.073068,H,2416.317594,H,2418.317594,H,2418.806647,H,2425.806647,H,2426.051173,H,2446.051173,H,2446.051173,H,2645.051173,H,2645.540226,H,2658.540226,H,2659.029279,H,2708.029279,H,2708.029279,H,2715.029279,H,2715.273805,H,2722.273805,H,2722.518331,H,2739.518331,H,2740.496437,H,3338.496437,H,3339.963596,H,3445.963596,H,3446.208122,H,3447.348677,H,3525.348677,H,3525.348677,H,3532.684473,H,32.684473,EX,31.543918,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
"JOHNSON, Robert",1628,H,1640.715379,H,1640.715379,H,1640.715379,H,1644.715379,H,1644.715379,H,1644.715379,H,1644.715379,H,1647.715379,H,1647.715379,H,1650.715379,H,1651.204432,H,1653.204432,H,1653.204432,H,1658.204432,H,1658.693485,H,1665.693485,H,1665.693485,H,1667.693485,H,1667.693485,H,1675.693485,H,1675.938011,H,1699.938011,H,1699.938011,H,1890.938011,H,1890.938011,H,1913.938011,H,1914.67159,H,1956.67159,H,1956.916116,H,1965.916116,H,1965.916116,H,1984.916116,H,1985.405169,H,2001.405169,H,2003.850434,H,17.850434,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
"HOPPER, Alvin",108,H,109.467159,H,149.467159,H,149.467159,H,1.467159,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
"WILLIAMS, Robyn",75,H,79.156951,H,4.156951,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
"STEPHEN-SMITH, Rachel",2786,H,3708.354085,H,3709.354085,H,3709.354085,H,3717.354085,H,3717.354085,H,3723.354085,H,3723.598611,H,3725.598611,H,3725.843137,H,3731.843137,H,3732.33219,H,3734.33219,H,3734.33219,H,3745.33219,H,3745.576716,H,3748.576716,H,3750.288401,H,3755.288401,H,3756.02198,H,3782.02198,H,3783.489139,H,3806.489139,H,3807.467245,H,3838.467245,H,3838.711771,H,3851.711771,H,3853.667983,H,3893.667983,H,3894.890615,H,3952.890615,H,3953.868721,H,4050.868721,H,4056.981884,H,4559.981884,H,4669.285244,H,4692.285244,H,4694.485982,H,4923.485982,H,4928.865565,H,4960.230839,H,5936.230839,H,5938.511949,H,6214.582405,H,6270.582405,H,6270.582405,H,6275.228409,H,8983.228409,EL,8983.228409,EL,8983.228409,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL
|
||||
"ANDERSON, Judy",1371,H,1655.384358,H,1657.384358,H,1657.384358,H,1658.384358,H,1658.384358,H,1659.384358,H,1660.117937,H,1660.117937,H,1660.362463,H,1667.362463,H,1667.362463,H,1676.362463,H,1676.851516,H,1677.851516,H,1678.096042,H,1681.096042,H,1681.829621,H,1687.829621,H,1687.829621,H,1708.829621,H,1709.318674,H,1717.318674,H,1718.29678,H,1725.29678,H,1726.030359,H,1765.030359,H,1769.431836,H,1793.431836,H,1793.920889,H,1827.920889,H,1829.388048,H,1862.388048,H,1865.077839,H,298.077839,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
"BARR, Andrew",11148,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL,8434,EL
|
||||
"INGRAM, Jacob",1736,H,2226.031173,H,2228.031173,H,2228.520226,H,2231.520226,H,2231.520226,H,2232.520226,H,2232.764752,H,2233.764752,H,2233.764752,H,2238.764752,H,2239.742858,H,2243.742858,H,2243.742858,H,2249.742858,H,2250.231911,H,2262.231911,H,2263.454543,H,2268.454543,H,2268.943596,H,2286.943596,H,2287.921702,H,2312.921702,H,2313.166228,H,2327.166228,H,2328.144334,H,2347.144334,H,2350.567705,H,2378.567705,H,2379.301284,H,2402.301284,H,2403.034863,H,2430.034863,H,2432.235601,H,2757.235601,H,2827.170189,H,2860.170189,H,2863.104507,H,3038.104507,H,3042.750511,H,3056.437176,H,594.437176,EX,580.750511,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
"NORTHAM, Maddy",2172,H,2529.497792,H,2530.497792,H,2530.497792,H,2533.497792,H,2533.497792,H,2535.497792,H,2535.742318,H,2542.742318,H,2542.986844,H,2543.986844,H,2545.209476,H,2547.209476,H,2547.698529,H,2548.698529,H,2548.943055,H,2559.943055,H,2560.676634,H,2571.676634,H,2571.92116,H,2601.92116,H,2602.410213,H,2636.410213,H,2637.388319,H,2644.388319,H,2644.388319,H,2675.388319,H,2677.833584,H,2705.833584,H,2707.056216,H,2744.056216,H,2745.278848,H,2815.278848,H,2818.213166,H,3142.213166,H,3214.59302,H,3276.59302,H,3278.549232,H,3441.549232,H,3445.950709,H,3457.356263,H,4278.356263,H,4282.348207,H,4494.108185,H,4539.108185,H,4539.108185,H,4542.531556,H,679.531556,EX,664.134058,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
"JOHNSON, Petar",156,H,164.313902,H,164.313902,H,164.313902,H,166.313902,H,166.313902,H,8.313902,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
"FORNER, Sophia",209,H,218.781061,H,220.781061,H,220.781061,H,220.781061,H,220.781061,H,255.781061,H,256.270114,H,260.270114,H,260.270114,H,10.270114,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
"O'HARA, Alix",195,H,208.448959,H,209.448959,H,209.448959,H,211.448959,H,211.448959,H,270.448959,H,272.160644,H,275.160644,H,275.160644,H,393.160644,H,394.383276,H,399.383276,H,399.872329,H,407.872329,H,408.116855,H,17.116855,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
"BRYANT, Peta Anne",472,H,475.178844,H,475.178844,H,475.42337,H,485.42337,H,485.42337,H,489.42337,H,490.156949,H,500.156949,H,500.156949,H,501.156949,H,501.401475,H,504.401475,H,504.890528,H,514.890528,H,514.890528,H,536.890528,H,537.135054,H,5.135054,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
"FAULKNER, Therese",901,H,912.003694,H,914.003694,H,914.737273,H,922.737273,H,922.737273,H,927.737273,H,928.470852,H,938.470852,H,938.470852,H,945.470852,H,945.715378,H,966.715378,H,966.715378,H,972.715378,H,972.959904,H,988.959904,H,989.448957,H,1180.448957,H,1180.93801,H,1221.93801,H,1222.182536,H,1312.182536,H,1313.160642,H,1319.160642,H,1319.405168,H,1344.405168,H,1345.872327,H,1493.872327,H,1494.36138,H,17.36138,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
"BOHM, Tim",1173,H,1192.317596,H,1195.317596,H,1195.562122,H,1208.562122,H,1208.562122,H,1212.562122,H,1212.562122,H,1225.562122,H,1225.562122,H,1230.562122,H,1230.806648,H,1257.806648,H,1258.540227,H,1267.540227,H,1267.540227,H,1299.540227,H,1300.273806,H,1440.273806,H,1440.273806,H,1491.273806,H,1492.740965,H,1581.740965,H,1581.740965,H,1585.740965,H,1585.740965,H,1598.740965,H,1599.474544,H,1897.474544,H,1898.45265,H,2743.45265,H,2745.408862,H,2777.408862,H,2777.408862,H,2807.408862,H,2809.365074,H,2837.365074,H,2838.098653,H,29.098653,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
"SMITH, Julie",447,H,457.270114,H,460.270114,H,460.270114,H,462.270114,H,462.270114,H,465.270114,H,466.003693,H,472.003693,H,472.003693,H,480.003693,H,481.226325,H,682.226325,H,683.93801,H,696.93801,H,697.182536,H,749.182536,H,751.627801,H,777.627801,H,777.872327,H,16.872327,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
"RUTLEDGE-PRIOR, Serrin",343,H,349.113163,H,349.113163,H,349.113163,H,352.113163,H,352.113163,H,353.113163,H,353.113163,H,359.113163,H,359.113163,H,365.113163,H,365.113163,H,6.113163,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
"PAINE, Bruce",693,H,699.602216,H,701.602216,H,701.602216,H,725.602216,H,725.602216,H,731.602216,H,731.602216,H,851.602216,H,853.313901,H,854.313901,H,854.313901,H,871.313901,H,871.313901,H,883.313901,H,883.802954,H,901.802954,H,902.292007,H,914.292007,H,914.292007,H,982.292007,H,982.78106,H,1106.78106,H,1106.78106,H,1115.78106,H,1116.025586,H,1117.025586,H,1118.003692,H,11.003692,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
"DAMIANO, Marilena",221,H,224.423371,H,227.423371,H,227.423371,H,230.423371,H,230.423371,H,232.423371,H,232.423371,H,3.423371,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
Exhausted,0,,0,,2,,2,,5,,5,,6,,6,,9,,9,,10,,10,,10,,10,,13,,13,,30,,30.489053,,30.489053,,30.489053,,102.489053,,103.467159,,247.467159,,248.934318,,261.934318,,262.667897,,288.667897,,290.135056,,444.135056,,445.602215,,532.602215,,536.025586,,605.025586,,610.405169,,671.405169,,676.051173,,754.051173,,758.208124,,1333.208124,,1340.299393,,1340.299393,,1541.299393,,1544.721059,,1572.597083,,1811.597083,,1812.737638,,1822.029646,,2324.029646,,2332.013534,,2754.310858,,2754.310858,,3657.310858,,3661.873079,,3672.052544,,3736.852075,,8850.933693,,8850.933693,
|
||||
Loss by fraction,0,,0.000015,,0.000015,,0.000018,,0.000018,,0.000019,,0.000019,,0.000025,,0.000025,,0.000029,,0.000029,,0.000035,,0.000035,,0.000038,,0.000038,,0.000042,,0.000042,,0.000046,,0.000046,,0.000046,,0.000046,,0.000048,,0.000048,,0.000048,,0.000048,,0.000051,,0.000051,,0.000053,,0.000053,,0.000056,,0.000056,,0.000058,,0.000058,,0.00006,,0.00006,,0.000059,,0.000059,,0.00006,,0.00006,,0.00006,,0.000063,,0.000063,,0.000065,,0.000062,,0.000062,,0.000062,,0.00006,,0.00006,,0.000061,,0.000055,,0.000057,,0.000057,,,,,,0.00005,,0.00005,,0.00005,
|
|
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue