Compare commits

..

192 Commits

Author SHA1 Message Date
582fdb8fe2
Add test case for Western Australia STV 2023-06-12 15:53:41 +10:00
4cf9053681
Implement --surplus-assume-total
This replaces and extends the former --subtract-nontransferable option

#4
2023-06-11 21:47:55 +10:00
c140ef0a90
Fix rounding of subtransfers by parcel
Was erroneously segmenting transfers by outgoing parcel rather than incoming parcel
2023-06-11 21:01:29 +10:00
9fc0457844
Fix typo in documentation 2023-06-11 15:56:17 +10:00
74d8829fbd
Modify WA Legislative Council STV preset to conform to WAEC's implementation 2023-06-11 15:31:55 +10:00
ed44f4f8a2
Add documentation for Victorian Legislative Council STV validation 2023-02-19 18:03:27 +11:00
f488d50f4d
Update description of presets 2023-02-08 21:38:52 +11:00
ba6ad8964f
Add preset and test for Victorian Legislative Council STV 2023-02-08 21:38:52 +11:00
eaf864062d
Implement --exclusion first_prefs_then_by_value 2023-02-07 21:12:26 +11:00
0ee5fd3285
Update to prettytable 0.10.0
prettytable 0.8.0 has a bug leading to arithmetic overflow/segfault on latest Rust
2023-02-01 18:26:45 +11:00
cece60dd69
Reduce unnecessary verbosity when only 1 vacancy 2023-02-01 18:26:45 +11:00
ab0ec44049
Add test cases for 2021 Minneapolis elections 2022-11-20 18:59:21 +11:00
b317affd08
Use --round-subtransfers per_ballot and --round-values 4 for Minneapolis STV 2022-11-20 18:59:02 +11:00
1a1153b666
Defer surplus distribution if not affecting bulk exclusion, even if otherwise affecting trailing 2 candidates 2022-11-20 18:58:32 +11:00
f7f9727146
Allow escaped quotation marks in BLT parser 2022-11-20 18:57:30 +11:00
d068ce6137
Add draft functional specifications 2022-11-06 21:50:25 +11:00
131d32c781
Disallow --no-immediate-elect with --quota-mode ers97/ers76 2022-11-06 14:57:19 +11:00
631d4e770a
Hide CandidateMap iterator implementation details 2022-11-06 14:45:35 +11:00
1a57fba093
CLI HTML output: Correctly generate print view for other report styles 2022-08-27 01:04:10 +10:00
fafb093c1a
Allow print from CLI HTML output 2022-08-27 00:58:51 +10:00
44ea09d7d3
Allow customising report style in CLI HTML output 2022-08-26 13:50:54 +10:00
815055d6e6
Initial implementation of HTML output on CLI 2022-08-26 02:27:25 +10:00
6bb127a124
Precompute Ballot::has_equal_rankings at parse time
Before: 5.3943 +- 0.0287
After: 5.27661 +- 0.00472
2022-08-25 21:49:50 +10:00
254c04b574
Update nomenclature for van der Craats (‘Wright’) STV 2022-08-23 18:37:42 +10:00
cb1cc5fb72
Clean up utility files 2022-08-22 19:19:22 +10:00
395de771fa
Tidying up
Refactor STV options implementations into separate file
Fix/update documentation
2022-08-22 11:35:20 +10:00
55f2e8816a
Update scripts 2022-08-21 07:31:32 +10:00
823f06a32b
Refactor calculation of totals, etc. 2022-08-21 07:20:21 +10:00
6ff111054c
Profile using binary input 2022-08-21 06:14:34 +10:00
3902c37768
Refactor CandidateMap 2022-08-21 06:02:06 +10:00
876be9c55a
Write more efficient implementation for CandidateMap which does not rely on hashing 2022-08-21 05:24:54 +10:00
4ad02c052c
Refactor HashMap for Candidates into dedicated struct (CandidateMap) 2022-08-21 03:57:37 +10:00
af4c6336aa
Utils for benchmarking and profiling 2022-08-21 03:56:54 +10:00
974a56dffd
Improve performance of Candidate in HashMap by simplifying equality check 2022-08-20 23:39:15 +10:00
422a198cf5
Use NoHashHasher for Candidate HashMaps to improve performance 2022-08-20 23:25:16 +10:00
61b22b388d
Remove unnecessary usage of HashMap.contains_key 2022-08-20 22:43:57 +10:00
9a4af322ca
Avoid more allocations in fold 2022-08-20 22:33:48 +10:00
2bce8cfc3f
Avoid String allocations in BLT parser 2022-08-19 00:16:29 +10:00
35104055d9
Avoid excess allocations during fold operations 2022-08-18 23:55:39 +10:00
ee7ac064c7
Improve performance of realise_equal_rankings 2022-08-18 00:15:44 +10:00
e825ca1491
Update for Rust 1.62
Add Cargo profile for profiling
2022-08-18 00:15:33 +10:00
f187975601
--no-immediate-elect requires --surplus-order by_size 2022-07-02 23:46:02 +10:00
566cdeb185
Permit --surplus meek with --quota-mode static 2022-06-18 23:07:00 +10:00
2f6614c0c1
Update wasm-bindgen 2022-06-18 22:44:32 +10:00
8d16f55289
Update for clap stable 2022-06-18 22:36:00 +10:00
82e90a0e10
Update documentation 2022-06-18 02:02:00 +10:00
384dde9c52
--subtract-nontransferable requires --surplus wig 2022-06-18 02:00:07 +10:00
2987eca0c3
Do not defer a surplus distribution if it exactly equals the difference between the 2 trailing candidates (as, depending on tie-breaking rules, this could change the order of exclusion) 2022-06-18 01:59:29 +10:00
8cc694e609
Cosmetic improvements
Hide transfers column in transposed report if no transfers
Report "Rollback complete" at end of stage when completed
2022-04-21 21:56:21 +10:00
8a0cd9b0a9
Start a glossary 2022-04-20 20:16:32 +10:00
c2621b2965
Improve messages for --constraint-mode repeat_count 2022-04-20 20:12:50 +10:00
4aafecb857
Use "ballots" consistently vs "ballot papers" 2022-04-20 20:03:20 +10:00
f0e3b02051
Autodetect when to normalise ballots, remove explicit --normalise-ballots 2022-04-20 19:54:58 +10:00
03af86733e
Initial implementation of --constraint-mode repeat_count 2022-04-20 19:42:20 +10:00
df9223ebe6
Implement --round-subtransfers by_parcel for NSW Local Government rules 2022-03-27 21:33:55 +11:00
26d45cac50
Implement --subtract-nontransferable for NSW Local Government rules 2022-03-25 02:46:30 +11:00
4119a293b1
Fix bug in c5d6b8d - by_value inadvertently changed in --exclusion 2022-03-24 02:53:27 +11:00
495ac5b514
Document NSW Local Government STV validation 2022-03-23 02:04:24 +11:00
8af0fa1178
Complete implementation and tests for NSW Local Government STV 2022-03-23 01:55:02 +11:00
c5d6b8d460
Refactor implementation of --sum-surplus-transfers -> --round-subtransfers in preparation for NSW Local Gov't STV 2022-03-23 00:35:00 +11:00
d94549dc42
Initial implementation of NSW Local Government STV 2022-03-21 21:15:17 +11:00
9fcb643fe5
Avoid wrapping candidate names/statuses in HTML report 2022-03-14 22:27:58 +11:00
67bf6f33d8
Update documentation 2022-03-11 15:58:32 +11:00
544d7fea5d
Update dates 2022-03-11 15:44:28 +11:00
6968df5c9b
Fix incorrect crediting of votes when surplus votes transferred at values received
Many thanks to J Groves for pointing this out
2022-03-11 13:33:28 +11:00
f5114bccda
Rename docs files to avoid reserved Windows paths 2022-01-04 18:26:15 +11:00
6304e1128a
Validate well-formedness of constraints, better constraint errors 2021-10-29 23:33:27 +11:00
ba82828046
Fix web UI crash when tie requires manual intervention in first stage 2021-10-29 20:28:50 +11:00
5a652cb466
Improve online documentation 2021-10-28 16:28:53 +11:00
116d4c385c
Build docs as HTML 2021-10-28 01:23:55 +11:00
63a649405b
Add link to home page 2021-10-28 00:30:55 +11:00
15614a4e8f
rust-clippy linting 2021-10-28 00:30:55 +11:00
69bc30b333
Downgrade some FIXMEs 2021-10-26 00:55:49 +11:00
0c97755813
Add CSP parsing tests 2021-10-26 00:55:42 +11:00
be8a6e83fc
Suppress unused import warning in WebAssembly 2021-10-26 00:55:31 +11:00
e867e85142
Update rustfilt path 2021-10-26 00:55:04 +11:00
f120cf2eee
Make DynNum thread-safe 2021-10-19 15:31:22 +11:00
75ec78b1a6
Add test cases for DynNum (single-threaded only) 2021-10-19 15:27:11 +11:00
414a1482c3
Implement dynamic dispatch for different number types 2021-10-19 14:52:08 +11:00
5a53574366
Allow opting out of building default wasm bindings 2021-10-18 18:06:42 +11:00
e78d06289a
Return Result from CSP parse_reader, better error messages 2021-10-17 17:18:14 +11:00
71dc671c34
Build CSV parser for WebAssembly 2021-10-17 16:34:15 +11:00
46654f8c5a
Implement --omit-informal for opentally convert 2021-10-17 16:32:35 +11:00
3ceaf67091
Implement stricter validation modes for CSP input 2021-09-30 00:48:57 +10:00
047a53d0d9
Ignore --round-surplus-fractions with Hare method 2021-09-27 19:19:33 +10:00
93cb72c33a
Update terminology and remove "stratify (floor)" support
Cincinnati -> Inclusive Hare (previous usage was erroneous/nonstandard)
Every n-th ballot -> Cincinnati
Remove "stratify (floor)" as it is not in contemporary use
2021-09-27 19:02:30 +10:00
0506283ae4
Add test case (Dail-like rules) from Grey-Fitzgerald ERS booklet 2021-09-26 02:27:50 +10:00
cf75943829
Fixes to edge cases in stratify (LR) sample method 2021-09-26 02:27:37 +10:00
2f7abf9f0a
Fix print view for votes (transposed) report 2021-09-26 02:25:44 +10:00
3a4e53e1f0
Implement Dáil Éireann STV 2021-09-14 23:13:45 +10:00
a641b97d1f
More work on unit/integration tests 2021-09-14 02:27:35 +10:00
f12db205b9
Make Fixed, GuardedFixed thread-safe 2021-09-13 04:33:36 +10:00
e1e347c255
More unit/integration tests 2021-09-13 03:43:17 +10:00
b05e0e06f2
Adjust formatting of detailed transfers table in web UI 2021-09-13 03:42:53 +10:00
59f79444e8
Show only continuing candidates in detailed transfers table 2021-09-12 00:24:48 +10:00
2c3470b91c
Hide detailed transfers link in print view 2021-09-11 21:17:35 +10:00
c1ccf54501
Don't reapply wasm-bindgen/wasm-opt if no changes 2021-09-11 21:17:35 +10:00
df1b2f7bdc
Implement detailed transfers in web UI 2021-09-11 21:08:36 +10:00
9817d6c199
Implement --transfers-detail 2021-09-11 18:42:15 +10:00
056242514d
Implement TransferTable for surpluses (WIP) 2021-09-11 02:43:11 +10:00
fbdc32ba30
Implement TransferTable for exclusions (WIP) 2021-09-11 01:19:38 +10:00
99dbbcd5d5
Hide votes required for election in Meek STV 2021-09-10 02:41:40 +10:00
c9b189fefe
Update quota/VRE in certain rare cases 2021-09-10 01:42:42 +10:00
de19324d2c
Report vote required for election in relation to early bulk election 2021-09-10 01:32:31 +10:00
473c8bcb39
Test cases comparing PRSA 1977 results with count.nl at https://gitlab.com/RunasSudo/prsa_count 2021-09-10 01:05:37 +10:00
4dd748186f
Fix logic error with CSV reporting of nontransferables with invalid votes 2021-09-10 00:51:09 +10:00
d222207318
Bundle all integration tests in single binary
Reduces test build time from ~2m30s to ~30s
2021-09-10 00:02:52 +10:00
523b039d2a
CSV output for bulk election 2021-09-09 13:46:10 +10:00
ab3067566d
Fix bug attempting to defer surplus with 0 or 1 continuing candidates
Add regression test
2021-09-09 13:36:27 +10:00
3b41eae11b
Implement eSTV-style CSV report 2021-09-09 04:07:18 +10:00
260dee1bb5
Fix bugs
Fix bug excluding-by-value/source candidates with no votes
Fix bug electing too many candidates if more reach the quota than vacancies remain
Add regression test
2021-09-09 01:24:50 +10:00
e4bfe45f49
Display up to 5 names only in web UI header, separate with line breaks 2021-09-06 02:43:33 +10:00
18c974117e
(cont.) Take num_to_exclude into consideration for bulk exclusion 2021-09-05 23:13:37 +10:00
09c4a375a7
Better error messages when insufficient candidates to fill vacancies 2021-09-05 22:53:59 +10:00
0a7189e54f
Complete ccc3266 2021-09-05 00:13:29 +10:00
2475b42056
Calculate loss by fraction introduced by minivoters with equal rankings 2021-09-05 00:07:59 +10:00
90971e976a
Fix --round-votes being ignored in first stage 2021-09-04 23:54:28 +10:00
e3ca9fac47
Refactor stv::preprocess_election 2021-09-04 22:46:29 +10:00
ccc3266d2c
Add signs to Votes display, change default to Votes (transposed) 2021-09-04 22:28:02 +10:00
a24ac3658a
Implement support for equal rankings 2021-09-04 02:30:01 +10:00
b0f869bf02
Initial framework for equal rankings 2021-09-04 01:56:04 +10:00
27ead09960
Complete BLT writer and implement tests for file conversions 2021-09-02 22:35:10 +10:00
e9e1c63c9c
Implement serialised binary format 2021-09-02 17:17:45 +10:00
31cdf3d99d
Add validation test for Church of England rules 2021-08-26 22:13:23 +10:00
cb97a44b73
Update documentation 2021-08-23 14:26:39 +10:00
59c1da794e
Implement transposed votes report 2021-08-22 17:53:55 +10:00
61e4eefca3
convert: Allow --seats to override input file 2021-08-21 01:19:54 +10:00
88ab06d633
Add subcommand for BLT/CSP file conversion 2021-08-20 02:29:47 +10:00
e7bae376e9
Fix error with forwards/backwards tiebreaking on first stage 2021-08-20 02:06:45 +10:00
85b695c133
Improve performance of Scottish STV
Remove reliance on normalising ballot papers
2021-08-19 20:11:42 +10:00
b9e66fde73
Correct number of ballot papers reported to be transferred in exclusive Gregory method 2021-08-17 01:56:43 +10:00
baffdce9e3
Implement print view for ballots+votes report 2021-08-17 01:37:40 +10:00
8a3361f20d
Implement papers+votes report 2021-08-17 01:14:05 +10:00
c9faa2ef01
Update documentation 2021-08-16 18:47:38 +10:00
94787e7677
Store vote values at the parcel level rather than the vote level
~50% increase in performance
2021-08-16 00:46:05 +10:00
7341522ba8
Update web UI defaults 2021-08-11 21:34:42 +10:00
eb3c7d0f53
Fix STVOptions::describe for --min-threshold 2021-08-09 23:27:58 +10:00
b1f2e42ce6
Update documentation 2021-08-09 19:56:51 +10:00
9f1476da63
Complete ERS76 implementation and add test case 2021-08-09 19:50:49 +10:00
5024496f61
Add new ERS97 test 2021-08-09 19:02:51 +10:00
764ebd98e6
Various tidyups
Use "Droop" as default quota (since same as "Droop (exact)" when quota not rounded)
Rename ers97.blt
Update documentation
2021-08-09 17:58:05 +10:00
46e895ee5a
Correct handling of exhausted votes during random sample surplus distribution 2021-08-09 00:17:14 +10:00
ae0d1d8411
Implement dynamic quotas 2021-08-08 21:41:10 +10:00
ee1008b509
Prepare for dynamic quota: independent flag for completion of surplus transfers/exclusions 2021-08-08 21:35:03 +10:00
0581571440
Update documentation 2021-08-08 18:59:36 +10:00
dc78692c72
Use new names for STVOptions::describe 2021-08-07 23:27:11 +10:00
b58922c57b
WIP: no immediate election? 2021-08-07 22:34:55 +10:00
7eb3b46628
Remove Minneapolis STV preset for now
Minneapolis STV is complicated by unusual procedures which are not currently implemented
2021-08-06 14:36:09 +10:00
f706d7423b
Fix interaction between --exclusion parcels_by_order and --min-threshold when excluding candidates with 0 votes 2021-08-06 01:33:31 +10:00
0af8d8a4d6
Update documentation on Minneapolis STV 2021-08-06 01:33:04 +10:00
8a4219303a
Implement Minneapolis STV 2021-08-05 21:47:34 +10:00
429191dc81
With --sample-per-ballot, terminate immediately on electing the required number 2021-08-05 20:23:54 +10:00
33594c110e
Implement stratified and by-order sampling 2021-08-05 18:41:39 +10:00
f3e4071886
Refactor tests specification using builder pattern 2021-08-05 01:13:54 +10:00
0800701960
Implement configurable --sample-per-ballot 2021-08-04 13:46:32 +10:00
0efc1e6eab
Complete implementation of Cambridge STV
Implement --min-threshold
Add test
2021-08-03 23:42:59 +10:00
f182ca02bd
Implement Cambridge STV - Cincinnati/Hare methods of surpluses 2021-08-03 18:38:45 +10:00
6da51837a5
Rename --round-tvs to --round-surplus-fractions and --round-weights to --round-values 2021-08-03 16:46:21 +10:00
77fe5effb2
Update documentation on bulk exclusion 2021-08-03 16:44:26 +10:00
c4fab9dc75
Correct description of backwards tie breaking algorithm 2021-08-02 20:14:03 +10:00
a2915b034b
Fix bug with attempted bulk exclusion during exclusion of doomed candidates 2021-08-02 00:24:41 +10:00
ea8c452737
Prevent bulk election and bulk exclusion violating constraints 2021-08-02 00:10:17 +10:00
116ff39fa5
Change tiebreaking prompt according to nature of tie 2021-07-31 17:51:09 +10:00
32e89312fa
Show stage progress during tie that occurs in the middle of a stage 2021-07-31 17:42:33 +10:00
83d0a9bb80
Better error messages 2021-07-31 15:24:23 +10:00
bfeec6f839
Give information on BLT syntax errors 2021-07-29 17:34:34 +10:00
3801d30527
Switch to handwritten BLT parser 2021-07-29 03:24:51 +10:00
470f1e550e
Simplify stack unwinding logic 2021-07-28 16:03:34 +10:00
49feb09bf8
Prefer election by quota/VRE to early bulk election 2021-07-28 00:12:57 +10:00
a5a61731b5
Use Asyncify to process ties in web UI 2021-07-27 23:31:37 +10:00
a64110b6a1
Update documentation 2021-07-26 18:50:51 +10:00
5f48a88bbe
Validated against 2019 NSW Senate election
Disable bulk election for Senate STV
Update documentation
2021-07-23 20:30:14 +10:00
efbcfd7f6c
Simply BLT grammar specification 2021-07-23 17:07:03 +10:00
4312bf89f6
Tweak layout of presets menu 2021-07-23 17:00:45 +10:00
e3419b6462
Add comments to supplied BLT files 2021-07-23 16:58:46 +10:00
cca097f943
Use Pest-based parser for BLT files
Support comments, optional newlines, etc.
2021-07-23 16:45:54 +10:00
3b8ccd097e
Extend early bulk election to multiple vacancies if the leading candidates cannot be overtaken 2021-07-23 01:38:37 +10:00
4690c32607
Fix unnecessary recursion in ERS97 algorithm 2021-07-23 00:42:15 +10:00
65b1d8e42b
Fix "Ex" display in web UI 2021-07-23 00:10:53 +10:00
85eda02d4d
Make stage number link to comment 2021-07-23 00:04:43 +10:00
bea51611b0
Implement Australian Capital Territory STV 2021-07-22 20:31:06 +10:00
12635decec
Use rational numbers/more decimal places in presets
Previous settings introduced rounding error in transfer values leading to incorrect results in some circumstances
2021-07-22 20:30:07 +10:00
3ea1eef7c5
Implement WA STV and update documentation 2021-07-22 00:41:20 +10:00
2ef7bf24f2
Correctly compute vote required for election when using different quotas/quota criteria 2021-07-21 13:43:16 +10:00
b5ee76f159
Further aggressive early bulk election 2021-07-21 10:59:06 +10:00
ed4a86e699
More aggressive early bulk election 2021-07-21 00:46:32 +10:00
a97ee591e5
Tweak dropdown formatting 2021-07-21 00:44:22 +10:00
11496a133c
Use custom dropdown box for presets 2021-07-20 14:32:08 +10:00
f80875b583
Implement --exclusion by_source 2021-07-19 23:15:17 +10:00
7f16090395
Fix crash on attempting segmented exclusion of candidate with no votes 2021-07-19 18:35:23 +10:00
d144ab0cb4
Implement ERS76 rules 2021-07-18 21:14:37 +10:00
bc8ed9a7e0
Add ERS73 preset 2021-07-16 17:04:20 +10:00
250 changed files with 143318 additions and 3492 deletions

15
.gitignore vendored
View File

@ -1,3 +1,16 @@
/target
/html/opentally.js
/html/opentally_bg.wasm
/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

636
Cargo.lock generated
View File

@ -1,11 +1,24 @@
# 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"
@ -15,6 +28,18 @@ 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"
@ -74,12 +99,39 @@ 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"
@ -94,22 +146,23 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "3.0.0-beta.2"
source = "git+https://github.com/clap-rs/clap?branch=master#65b3892ef6c1ddf0cf837c76d164b8182103fa5d"
version = "3.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53da17d37dba964b9b3ecb5c5a1f193a2762c700e6829201e645b9381c99dc7"
dependencies = [
"bitflags",
"clap_derive",
"clap_lex",
"indexmap",
"lazy_static",
"os_str_bytes",
"once_cell",
"textwrap",
"vec_map",
]
[[package]]
name = "clap_derive"
version = "3.0.0-beta.2"
source = "git+https://github.com/clap-rs/clap?branch=master#65b3892ef6c1ddf0cf837c76d164b8182103fa5d"
version = "3.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c11d40217d16aee8508cc8e5fde8b4ff24639758608e5374e731b53f85749fb9"
dependencies = [
"heck",
"proc-macro-error",
@ -118,6 +171,15 @@ 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"
@ -181,14 +243,81 @@ dependencies = [
]
[[package]]
name = "derive_more"
version = "0.99.14"
name = "darling"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cc7b9cef1e351660e5443924e4f43ab25fbbed3e9a5f052df3677deb4d6b320"
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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40eebddd2156ce1bb37b20bbe5151340a31828b1f2d22ba4141f3531710e38df"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"rustc_version",
"syn",
]
@ -207,6 +336,27 @@ 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"
@ -219,6 +369,33 @@ 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"
@ -240,6 +417,12 @@ 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"
@ -250,6 +433,17 @@ 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"
@ -289,12 +483,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
[[package]]
name = "heck"
version = "0.3.2"
name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
dependencies = [
"unicode-segmentation",
"ahash",
]
[[package]]
name = "heck"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
[[package]]
name = "hermit-abi"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
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",
]
[[package]]
@ -310,6 +528,12 @@ 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"
@ -317,7 +541,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3"
dependencies = [
"autocfg",
"hashbrown",
"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",
]
[[package]]
@ -352,9 +598,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.95"
version = "0.2.139"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36"
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
[[package]]
name = "linux-raw-sys"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
[[package]]
name = "log"
@ -403,6 +655,12 @@ 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"
@ -411,9 +669,9 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
[[package]]
name = "num-bigint"
version = "0.4.0"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e0d047c1062aa51e256408c560894e5251f08925980e53cf1aa5bd00eec6512"
checksum = "74e768dff5fb39a41b3bcd30bb25cf989706c90d028d1ad71971987aa309d535"
dependencies = [
"autocfg",
"num-integer",
@ -460,6 +718,12 @@ 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"
@ -470,33 +734,40 @@ 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 = "3.1.0"
version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6acbef58a60fe69ab50510a55bc8cdd4d6cf2283d27ad338f54cb52747a9cf2d"
checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa"
[[package]]
name = "paste"
@ -504,6 +775,15 @@ 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"
@ -533,6 +813,20 @@ 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"
@ -565,11 +859,31 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
[[package]]
name = "proc-macro2"
version = "1.0.27"
version = "1.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038"
checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f"
dependencies = [
"unicode-xid",
"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",
]
[[package]]
@ -602,6 +916,26 @@ 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"
@ -628,6 +962,40 @@ 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"
@ -639,12 +1007,65 @@ 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"
@ -671,21 +1092,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "syn"
version = "1.0.72"
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "1.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
"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",
]
[[package]]
name = "textwrap"
version = "0.13.4"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd05616119e612a8041ef58f2b578906cc2531a6069047ae092cfb86a325d835"
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",
]
[[package]]
name = "treeline"
@ -700,22 +1158,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
[[package]]
name = "unicode-segmentation"
version = "1.7.1"
name = "ucd-trie"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
[[package]]
name = "unicode-xid"
version = "0.2.2"
name = "unicode-ident"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c"
[[package]]
name = "vec_map"
version = "0.8.2"
name = "unicode-width"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
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"
[[package]]
name = "version_check"
@ -733,10 +1206,16 @@ dependencies = [
]
[[package]]
name = "wasm-bindgen"
version = "0.2.74"
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "wasm-bindgen"
version = "0.2.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994"
dependencies = [
"cfg-if 1.0.0",
"wasm-bindgen-macro",
@ -744,9 +1223,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.74"
version = "0.2.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900"
checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a"
dependencies = [
"bumpalo",
"lazy_static",
@ -759,9 +1238,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.74"
version = "0.2.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4"
checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -769,9 +1248,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.74"
version = "0.2.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97"
checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048"
dependencies = [
"proc-macro2",
"quote",
@ -782,9 +1261,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.74"
version = "0.2.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f"
checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be"
[[package]]
name = "winapi"
@ -808,6 +1287,63 @@ 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"

View File

@ -7,43 +7,56 @@ 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"
predicates = "1.0.8"
num-traits = "0.2"
predicates = "1.0.8"
sha2 = "0.9.5"
wasm-bindgen = "0.2.74"
wasm-bindgen = "0.2.81"
# 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
# For tests/CLI only
[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

View File

@ -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.md), and can count votes using:
OpenTally accepts data in the [BLT file format](https://yingtongli.me/git/OpenTally/about/docs/blt-fmt.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,14 +24,11 @@ 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
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.
See the [quick start guide](/opentally/docs/quick-start.html) for how to use OpenTally online.
## Command line usage

View File

@ -1,3 +0,0 @@
#!/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

View File

@ -1,20 +0,0 @@
#!/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 Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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/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).
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).
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,14 +21,16 @@ The file format is as follows:
"Title"
```
The first line (`4 2`) indicates that there are 4 candidates for 2 vacancies. This must be on its own line.
The first line (`4 2`) indicates that there are 4 candidates for 2 vacancies.
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 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 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 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 end of the list of ballots must be indicated with a single `0`, which must be on its own line.
The end of the list of ballots must be indicated with a single `0`.
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 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 final line gives the name of the election, which 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.

View File

@ -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.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-fmt.md) used for ballot data.
Suppose there are 7 candidates in the election. An example CON file is as follows:

39
docs/glossary.md Normal file
View File

@ -0,0 +1,39 @@
# 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.

View File

@ -1,19 +1,49 @@
# 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:
* *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.
| 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).
This functionality is not available on the command line.
@ -23,29 +53,31 @@ 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* and *Droop (exact)*: *V*/(*S*+1)
* *Droop* (default) 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, *Droop* (or *Droop (exact)*) is also known as the Newland–Britton or Hagenbach-Bischoff quota.
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.
### Quota criterion (-c/--quota-criterion)
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).
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).
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.
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.
### 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*: 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).
* *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.
When *Surplus method* is set to *Meek method*, this setting is ignored, and the progressively reducing quota of the Meek method is instead applied.
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.
## STV variants
@ -62,29 +94,38 @@ 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 ballot papers of the transferring candidate are examined. Transfers are weighted according to the weights of the ballot papers.
* *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.
* *Meek method*: Transfers are computed as described at <http://www.dia.govt.nz/diawebsite.NSF/Files/meekm/%24file/meekm.pdf>.
Other methods are supported, but not recommended:
Other Gregory methods are supported, but not recommended:
* *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.
* *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.
Other surplus transfer methods, such as non-fractional transfers (e.g. random sample) are not supported at this time.
Random sample methods are also supported, but also not recommended:
### Papers to examine in surplus transfer (--transferable-only)
* *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.
* *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.
A random sample method will usually be used with a *Quota criterion* set to *>=*.
### Exclusion method (--exclusion)
### Ballots to examine in surplus transfer (--transferable-only/--surplus-assume-total)
* *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.
* *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.
When *Surplus method* is set to *Meek method*, this setting is ignored, and the Meek method is instead applied.
### (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.
### (Meek) NZ-style exclusion (--meek-nz-exclusion)
@ -93,11 +134,28 @@ 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 *most recent* stage where one tied candidate had more/fewer votes than the others, if such a stage exists.
* *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).
* *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.
@ -110,15 +168,52 @@ 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](rng.md).
The algorithm used by the random number generator is specified at [rng.md](https://yingtongli.me/git/OpenTally/about/docs/rng.md).
## Constraints (--constraints)
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.
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.
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.
### Constraint method (--constraint-method)
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)).
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.
## Numeric representation
@ -128,84 +223,94 @@ 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*: 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* (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.
* *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), 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.
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 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.
* 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.
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.
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.
### 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 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*.
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.
### 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 order of exclusion (including of 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 next exclusion (including a bulk exclusion, if that is enabled).
### (Meek) Immediate election (--meek-immediate-elect)
### Immediate election (--no-immediate-elect)
When *Surplus method* is set to *Meek method*, this option controls when candidates are elected:
When *Surplus method* is set to a Gregory or random sample method, this option controls when candidates are elected:
* 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.
* 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.
## Rounding
### Round quota/votes/surplus fractions/ballot weights to [n] d.p. (--round-quota, --round-votes, --round-tvs, --round-weights)
### Round quota/votes/surplus fractions/ballot values to [n] d.p. (--round-quota, --round-votes, --round-surplus-fractions, --round-values)
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 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 enabled, the quota is incremented or rounded up (according to the *Quota* option), whereas votes, surplus fractions and weights are always rounded down.
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.
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:
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:
* 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.
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.
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-weights instead controls the rounding of candidate keep values
* --round-tvs instead controls the rounding of each intermediate product when computing candidates' votes
* --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-votes controls the rounding of the final number of votes credited to each candidate
* Keep values, intermediate products and candidate votes are rounded *up*
### Sum surplus transfers (--sum-surplus-transfers)
### (Gregory) Round subtransfers (--round-subtransfers)
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:
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:
* *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.
* *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.
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.
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.
### (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 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, when summed together, is no greater than the specified number of votes. This is the simpler method specified in the 2006 Meek rules.

19
docs/quick-start.md Normal file
View File

@ -0,0 +1,19 @@
# 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%.

View File

@ -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 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 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 avoid modulo bias, if *k* ≥ ⌊*M*/*n*⌋ × *n* (where *M* = 2^256), *k* is discarded and the algorithm is repeated.

36
docs/validation.md Normal file
View File

@ -0,0 +1,36 @@
# 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/>

25
homepage/404.html Normal file
View File

@ -0,0 +1,25 @@
---
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>

15
homepage/Gemfile Normal file
View File

@ -0,0 +1,15 @@
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"

72
homepage/Gemfile.lock Normal file
View File

@ -0,0 +1,72 @@
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

31
homepage/_config.yml Normal file
View File

@ -0,0 +1,31 @@
# 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

View File

@ -0,0 +1,50 @@
---
---
<!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 &copy; <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>

View File

@ -0,0 +1,38 @@
---
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>

View File

@ -0,0 +1,26 @@
---
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>

View File

@ -0,0 +1,168 @@
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.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

6
homepage/docs/about.md Normal file
View File

@ -0,0 +1,6 @@
---
layout: docs
title: "About OpenTally"
---
<div class="md-content" markdown="1">{% include_absolute '../README.md' %}</div>

View File

@ -0,0 +1,6 @@
---
layout: docs
title: "Glossary"
---
<div class="md-content" markdown="1">{% include_absolute '../docs/glossary.md' %}</div>

10
homepage/docs/options.md Normal file
View File

@ -0,0 +1,10 @@
---
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>

View File

@ -0,0 +1,6 @@
---
layout: docs
title: "Quick start guide"
---
<div class="md-content" markdown="1">{% include_absolute '../docs/quick-start.md' %}</div>

74
homepage/index.html Normal file
View File

@ -0,0 +1,74 @@
---
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>

View File

@ -1,6 +1,6 @@
<!--
* OpenTally: Open-source election vote counting
* Copyright © 2021 Lee Yingtong Li (RunasSudo)
* 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
@ -35,21 +35,38 @@
<label>
Preset:
<select id="selPreset" onchange="changePreset()">
<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>
<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>
</select>
</label>
<button id="btnAdvancedOptions" onclick="clickAdvancedOptions()">Show advanced options</button>
OpenTally (revision <span id="spanRevNum"></span>)
<!--&middot; <a href="https://yingtongli.me/blog/2020/12/24/pyrcv2.html">Information and instructions</a> &middot;
<a href="blt/">Ballot input/editor</a>-->
&middot; <a href="https://yingtongli.me/opentally/">Information and instructions</a>
</div>
<div id="divAdvancedOptions" class="menudiv cols-12 cols-sm-6" style="display: none;">
@ -67,8 +84,8 @@
</label>
<label>
<select id="selQuota">
<option value="droop">Droop</option>
<option value="droop_exact" selected>Droop (exact)</option>
<option value="droop" selected>Droop</option>
<option value="droop_exact">Droop (exact)</option>
<option value="hare">Hare</option>
<option value="hare_exact">Hare (exact)</option>
</select>
@ -78,6 +95,9 @@
<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>
@ -91,28 +111,35 @@
</label>
<label>
Method:
<select id="selTransfers">
<select id="selMethod">
<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 papers</option>
<option value="transferable">Use transferable papers only</option>
<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>
</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="wright">Wright method (re-iterate)</option>
<option value="reset_and_reiterate">Reset and re-iterate</option>
</select>
</label>
<label>
@ -121,6 +148,22 @@
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>
@ -143,8 +186,35 @@
Constraints:
</div>
<div>
<input type="file" id="conFile">
<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>
</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">
@ -166,15 +236,6 @@
<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>
@ -191,10 +252,13 @@
Defer surpluses
</label>
<label class="col-6">
<input type="checkbox" id="chkMeekImmediateElect">
<span class="pill-grey" title="This option has effect only if “Method” is set to “Meek method”">Meek</span>
<input type="checkbox" id="chkImmediateElect" checked>
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>
@ -220,29 +284,32 @@
</div>
<div class="col-6">
<label>
<input type="checkbox" id="chkRoundTVs">
<input type="checkbox" id="chkRoundSFs">
Surplus fractions:
</label>
<label>
<input type="number" id="txtRoundTVs" value="0" min="0" style="width: 3em;">
<input type="number" id="txtRoundSFs" value="0" min="0" style="width: 3em;">
d.p.
</label>
</div>
<div class="col-6">
<label>
<input type="checkbox" id="chkRoundWeights">
Ballot weights:
<input type="checkbox" id="chkRoundValues">
Ballot values:
</label>
<label>
<input type="number" id="txtRoundWeights" value="0" min="0" style="width: 3em;">
<input type="number" id="txtRoundValues" value="0" min="0" style="width: 3em;">
d.p.
</label>
</div>
<label class="col-12">
Sum surplus transfers:
<span class="pill-grey" title="This option has effect only if “Method” is a Gregory method">Gregory</span>
Round subtransfers:
<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>
@ -278,7 +345,9 @@
<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="opentally.js?v=GITVERSION"></script>
<script src="vendor/vanilla-js-dropdown.min.js"></script>
<script src="index.js?v=GITVERSION"></script>
<script src="presets.js?v=GITVERSION"></script>
<script src="print.js?v=GITVERSION"></script>
</body>
</html>

View File

@ -1,5 +1,5 @@
/* OpenTally: Open-source election vote counting
* Copyright © 2021 Lee Yingtong Li (RunasSudo)
* Copyright © 20212022 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,9 @@ var tblResult = document.getElementById('result');
var divLogs2 = document.getElementById('resultLogs2');
var olStageComments;
var worker = new Worker('worker.js');
var detailedTransfers = {};
var worker = new Worker('worker.js?v=GITVERSION');
worker.onmessage = function(evt) {
if (evt.data.type === 'init') {
@ -37,26 +39,41 @@ 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 i = 0; i < evt.data.result.length; i++) {
if (evt.data.result[i]) {
tblResult.rows[i].insertAdjacentHTML('beforeend', evt.data.result[i]);
for (let row = 0; row < evt.data.result.length; row++) {
if (evt.data.result[row]) {
tblResult.rows[row].insertAdjacentHTML('beforeend', evt.data.result[row]);
// Update candidate status
if (i >= 3 && i % 2 == 1) {
if (tblResult.rows[i].lastElementChild.classList.contains('elected')) {
tblResult.rows[i].cells[0].classList.add('elected');
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');
} else {
tblResult.rows[i].cells[0].classList.remove('elected');
tblResult.rows[row].cells[0].classList.remove('elected');
}
}
}
@ -64,19 +81,34 @@ 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);
}
}
@ -109,444 +141,81 @@ async function clickCount() {
// Init STV options
let optsStr = [
document.getElementById('chkRoundTVs').checked ? parseInt(document.getElementById('txtRoundTVs').value) : null,
document.getElementById('chkRoundWeights').checked ? parseInt(document.getElementById('txtRoundWeights').value) : null,
document.getElementById('chkRoundSFs').checked ? parseInt(document.getElementById('txtRoundSFs').value) : null,
document.getElementById('chkRoundValues').checked ? parseInt(document.getElementById('txtRoundValues').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('selTransfers').value,
document.getElementById('selMethod').value,
document.getElementById('selSurplus').value,
document.getElementById('selPapers').value == 'transferable',
document.getElementById('selPapers').value,
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('chkMeekImmediateElect').checked,
document.getElementById('chkImmediateElect').checked,
document.getElementById('txtMinThreshold').value,
conPath,
"guard_doom",
document.getElementById('selConstraintMethod').value,
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,
'normaliseBallots': document.getElementById('chkNormaliseBallots').checked,
'reportStyle': document.getElementById('selReport').value,
});
}
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';
}
}

View File

@ -1,20 +1,19 @@
/*
pyRCV2: Preferential vote counting
Copyright © 20202021 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/>.
*/
/* OpenTally: Open-source election vote counting
* Copyright © 20212022 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');
@ -34,6 +33,13 @@ a:hover {
color: #1d3da2;
text-decoration: underline;
}
tr.stage-no a {
color: initial !important;
}
li.highlight {
background-color: #fffedd;
}
/* Menu styling */
@ -47,6 +53,10 @@ a:hover {
.menudiv .subheading {
font-size: 0.8em;
font-weight: 600;
margin-top: 0.5rem;
}
.menudiv > div > .subheading:first-child {
margin-top: 0;
}
.pill-grey {
@ -83,7 +93,7 @@ table {
color-adjust: exact;
-webkit-print-color-adjust: exact;
}
.result td {
table.result td, table.transfers td {
padding: 0px 8px;
height: 1em;
}
@ -94,15 +104,22 @@ td.count sup {
font-size: 0.6rem;
top: 0;
}
tr.stage-no td, tr.stage-kind td, tr.stage-comment td {
tr.stage-no td, tr.stage-kind td, tr.stage-comment td, tr.hint-papers-votes 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;
}
@ -112,23 +129,34 @@ td.elected {
tr.info td {
background-color: #f0f5fb;
}
tr.stage-no td:not(:empty), tr.transfers td {
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 {
border-top: 1px solid #76858c;
}
tr.info:last-child td, .bb {
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 {
border-bottom: 1px solid #76858c;
}
.blw {
/* Used to separate counts in Wright STV */
/* Used to separate counts in van der Craats (‘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) {
tr.candidate.votes td:nth-child(odd):not(.elected):not(.excluded),
table.transfers td:nth-child(even) {
background-color: #f9f9f9;
}
tr.candidate.transfers td.elected:nth-child(even),
@ -141,37 +169,13 @@ 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) {
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) {
background-color: #e8eef7;
}
/* 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;
a.detailedTransfersLink {
color: #aaa;
}
/* Print stylesheet */
@ -191,6 +195,9 @@ tr.info.votes td:nth-child(odd) {
#printWarning {
display: block;
}
a.detailedTransfersLink {
display: none;
}
}
/* Form styling */
@ -200,9 +207,9 @@ select, input, button {
line-height: 1.15;
}
select, input[type="text"], input[type="number"], textarea {
select, input[type="text"], input[type="number"], textarea, .js-Dropdown-title {
appearance: none;
background-color: #fff;
background-color: #fff !important;
border: 1px solid;
border-color: #999 #bbb #ddd;
border-radius: 0;
@ -211,7 +218,7 @@ select, input[type="text"], input[type="number"], textarea {
padding: 2px 3px;
}
select {
select, .js-Dropdown-title {
/* Dropdown arrow */
background-image: url(data:image/png;base64,R0lGODlhDQAEAIAAAAAAAP8A/yH5BAEHAAEALAAAAAANAAQAAAILhA+hG5jMDpxvhgIAOw==);
background-position: right center;
@ -277,10 +284,63 @@ input[type="checkbox"]:checked {
button:focus, select:focus, input:focus, textarea:focus {
outline: 0;
}
select:focus, input:focus, textarea:focus {
select:focus, .js-Dropdown-title: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 Normal file
View File

@ -0,0 +1,478 @@
/* OpenTally: Open-source election vote counting
* Copyright © 20212022 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 Normal file
View File

@ -0,0 +1,229 @@
/* OpenTally: Open-source election vote counting
* Copyright © 20212022 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();
}

View File

@ -0,0 +1,5 @@
/**
* 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)}}};

View File

@ -1,106 +1,162 @@
importScripts('opentally.js');
/* OpenTally: Open-source election vote counting
* Copyright © 20212022 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');
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() {
await wasm_bindgen('opentally_bg.wasm');
wasmRaw = await wasm_bindgen('opentally_async.wasm?v=GITVERSION');
new Int32Array(wasmRaw.memory.buffer, DATA_ADDR).set([DATA_START, DATA_END]);
postMessage({'type': 'init', 'version': wasm.version()});
}
initWasm();
var reportStyle;
var numbers, election, opts, state, stageNum;
onmessage = function(evt) {
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);
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
} else {
throw 'Unknown --numbers';
throw ex;
}
// 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 resume_count() {
function resumeCount() {
for (;; stageNum++) {
try {
let isDone = wasm['count_one_stage_' + numbers](state, opts);
if (isDone) {
break;
}
} catch (ex) {
if (ex === "RequireInput") {
return;
} else {
throw ex;
}
let isDone;
if (stageNum <= 1) {
isDone = wasm['count_init_' + numbers](state, opts);
} else {
isDone = wasm['count_one_stage_' + numbers](state, opts);
}
postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](stageNum, state, opts)});
postMessage({'type': 'updateStageComments', 'comment': wasm['update_stage_comments_' + numbers](state)});
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['finalise_results_table_' + numbers](state)});
postMessage({'type': 'updateResultsTable', 'result': wasm['finalise_results_table_' + numbers](state, reportStyle)});
postMessage({'type': 'finalResultSummary', 'summary': wasm['final_result_summary_' + numbers](state, opts)});
}
var user_input_buffer = null;
var errored = false;
function wasm_error(message) {
postMessage({'type': 'errorMessage', 'message': message});
errored = true;
}
function read_user_input_buffer(message) {
if (user_input_buffer === null) {
var userInputBuffer = null;
function get_user_input(message) {
if (userInputBuffer === 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 {
let user_input = user_input_buffer;
user_input_buffer = null;
return user_input;
// 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;
}
}

View File

@ -1,3 +0,0 @@
#!/bin/bash
RUSTC_BOOTSTRAP=1 rustc $@

7
scripts/benchmark.sh Executable file
View File

@ -0,0 +1,7 @@
#!/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

8
scripts/build_homepage.sh Executable file
View File

@ -0,0 +1,8 @@
#!/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"

21
scripts/build_wasm.sh Executable file
View File

@ -0,0 +1,21 @@
#!/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

24
scripts/coverage.sh Executable file
View File

@ -0,0 +1,24 @@
#!/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

31
scripts/deploy.sh Executable file
View File

@ -0,0 +1,31 @@
#!/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"

3
scripts/mkbaseline.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
mv target/benchmark.log target/benchmark.baseline.log
mv target/perf.data target/perf.baseline.data

9
scripts/profile.sh Executable file
View File

@ -0,0 +1,9 @@
#!/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 Normal file
View File

@ -0,0 +1,269 @@
/* OpenTally: Open-source election vote counting
* Copyright © 20212022 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 };
}
}

155
src/cli/convert.rs Normal file
View File

@ -0,0 +1,155 @@
/* 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(());
}

21
src/cli/mod.rs Normal file
View File

@ -0,0 +1,21 @@
/* 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 Normal file
View File

@ -0,0 +1,789 @@
/* OpenTally: Open-source election vote counting
* Copyright © 20212023 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(());
}

View File

@ -1,5 +1,5 @@
/* OpenTally: Open-source election vote counting
* Copyright © 2021 Lee Yingtong Li (RunasSudo)
* Copyright © 20212022 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,81 +15,55 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use crate::election::{Candidate, CandidateState, CountCard, CountState, Election};
use crate::candmap::CandidateMap;
use crate::election::{Candidate, CandidateState, CountCard, CountState, Election, StageKind, RollbackState};
use crate::numbers::Number;
use crate::stv::{ConstraintMode, STVOptions};
use crate::stv::{self, gregory, sample, ConstraintMode, STVError, STVOptions, SurplusMethod, SurplusOrder};
use crate::ties::{self, TieStrategy};
use itertools::Itertools;
use ndarray::{Array, Dimension, IxDyn};
use std::collections::HashMap;
#[cfg(not(target_arch = "wasm32"))]
use rkyv::{Archive, Deserialize, Serialize};
use std::fmt;
use std::num::ParseIntError;
use std::ops;
/// Constraints for an [crate::election::Election]
#[derive(Debug)]
#[derive(Clone, Debug)]
#[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))]
pub struct Constraints(pub Vec<Constraint>);
impl Constraints {
/// Parse the given CON file and return a [Constraints]
pub fn from_con<I: Iterator<Item=String>>(lines: I) -> Self {
pub fn from_con<S: AsRef<str>, I: Iterator<Item=S>>(lines: I) -> Result<Self, ParseError> {
let mut constraints = Constraints(Vec::new());
for line in lines {
let mut bits = line.split(" ").peekable();
for (line_no, line) in lines.enumerate() {
let mut bits = line.as_ref().split(' ').peekable();
// 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 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 min, max
let min: usize = bits.next().expect("Syntax Error").parse().expect("Syntax Error");
let max: usize = bits.next().expect("Syntax Error").parse().expect("Syntax Error");
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))?;
// Read candidates
let mut candidates: Vec<usize> = Vec::new();
for x in bits {
candidates.push(x.parse::<usize>().expect("Syntax Error") - 1);
candidates.push(x.parse::<usize>().map_err(|e| ParseError::InvalidNumber(line_no, e))? - 1);
}
// Insert constraint/group
let constraint = match constraints.0.iter_mut().filter(|c| c.name == constraint_name).next() {
let constraint = match constraints.0.iter_mut().find(|c| c.name == constraint_name) {
Some(c) => { c }
None => {
let c = Constraint {
@ -102,25 +76,201 @@ impl Constraints {
};
if constraint.groups.iter().any(|g| g.name == group_name) {
panic!("Duplicate group \"{}\" in constraint \"{}\"", group_name, constraint.name);
return Err(ParseError::DuplicateGroup(line_no, group_name, constraint.name.clone()));
}
constraint.groups.push(ConstrainedGroup {
name: group_name,
candidates: candidates,
min: min,
max: max,
candidates,
min,
max,
});
}
// TODO: Validate constraints
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()));
}
}
return 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));
}
}
/// A single dimension of constraint
#[derive(Debug)]
#[derive(Clone, Debug)]
#[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))]
pub struct Constraint {
/// Name of this constraint
pub name: String,
@ -129,7 +279,8 @@ pub struct Constraint {
}
/// A group of candidates, of which a certain minimum and maximum must be elected
#[derive(Debug)]
#[derive(Clone, Debug)]
#[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))]
pub struct ConstrainedGroup {
/// Name of this group
pub name: String,
@ -148,6 +299,10 @@ pub enum ConstraintError {
NoConformantResult,
}
// ----------------------
// GUARD/DOOM CONSTRAINTS
// ----------------------
/// Cell in a [ConstraintMatrix]
#[derive(Clone)]
pub struct ConstraintMatrixCell {
@ -161,7 +316,8 @@ pub struct ConstraintMatrixCell {
pub cands: usize,
}
/// Hypercube/tensor of [ConstraintMatrixCell]s representing the conformant combinations of elected candidates
/// N-dimensional cube of [ConstraintMatrixCell]s representing the conformant combinations of elected candidates
#[derive(Clone)]
pub struct ConstraintMatrix(pub Array<ConstraintMatrixCell, IxDyn>);
impl ConstraintMatrix {
@ -204,7 +360,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: &HashMap<&Candidate, CountCard<N>>) {
pub fn update_from_state<N: Number>(&mut self, election: &Election<N>, candidates: &CandidateMap<CountCard<N>>) {
let constraints = election.constraints.as_ref().unwrap();
// Reset innermost cells
@ -217,13 +373,18 @@ 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;
}
}
panic!("Candidate \"{}\" not represented in constraint \"{}\"", candidate.name, c.name);
// Should be caught by validate_constraints
unreachable!("Candidate \"{}\" not represented in constraint \"{}\"", candidate.name, c.name);
}).collect();
let cell = &mut self[&idx[..]];
@ -257,7 +418,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()).filter(|d| idx[*d] == 0).next().unwrap();
let zero_axis = (0..idx.ndim()).find(|d| idx[*d] == 0).unwrap();
// Traverse along the axis and sum the candidates
let mut idx2 = idx.clone();
@ -367,87 +528,87 @@ impl fmt::Display for ConstraintMatrix {
// TODO: >2 dimensions
if shape.len() == 1 {
result.push_str("+");
result.push('+');
for _ in 0..shape[0] {
result.push_str("-------------+");
}
result.push_str("\n");
result.push('\n');
result.push_str("|");
result.push('|');
for x in 0..shape[0] {
result.push_str(&format!(" Elected: {:2}", self[&[x]].elected));
result.push_str(if x == 0 { "" } else { " |" });
}
result.push_str("\n"