stv_rs/
blt.rs

1// Copyright 2023 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Utilities to write elections into the BLT format.
16
17use crate::types::{Candidate, Election};
18use std::io::{self, Write};
19
20/// Policy to write the tie order.
21#[derive(Clone, Copy, PartialEq, Eq)]
22pub enum WriteTieOrder {
23    /// Always write the tie order, even if it is trivial.
24    Always,
25    /// Never write the tie order, but panics if it is non-trivial.
26    Never,
27    /// Only write the tie order if it is non-trivial.
28    OnlyNonTrivial,
29}
30
31/// Whether to write candidates using their nicknames or in numeric format.
32#[derive(Clone, Copy, PartialEq, Eq)]
33pub enum CandidateFormat {
34    /// Use nicknames.
35    Nicknames,
36    /// Use numeric format.
37    Numeric,
38}
39
40/// Serializes an election into the BLT format.
41pub fn write_blt(
42    output: &mut impl Write,
43    election: &Election,
44    write_tie_order: WriteTieOrder,
45    candidate_format: CandidateFormat,
46) -> io::Result<()> {
47    let has_non_trivial_tie_order = election
48        .tie_order
49        .iter()
50        .any(|(candidate, order)| candidate != order);
51    if write_tie_order == WriteTieOrder::Never && has_non_trivial_tie_order {
52        panic!("Writing the tie order is disabled, but the tie order is non-trivial");
53    }
54
55    writeln!(output, "{} {}", election.num_candidates, election.num_seats)?;
56
57    if candidate_format == CandidateFormat::Nicknames {
58        write!(output, "[nick")?;
59        for candidate in &election.candidates {
60            write!(output, " {}", candidate.nickname)?;
61        }
62        writeln!(output, "]")?;
63    }
64
65    if election
66        .candidates
67        .iter()
68        .any(|candidate| candidate.is_withdrawn)
69    {
70        write!(output, "[withdrawn")?;
71        for (i, candidate) in election
72            .candidates
73            .iter()
74            .enumerate()
75            .filter(|(_, candidate)| candidate.is_withdrawn)
76        {
77            write!(output, " ")?;
78            write_candidate(output, i, candidate, candidate_format)?;
79        }
80        writeln!(output, "]")?;
81    }
82
83    let write_tie_order = match write_tie_order {
84        WriteTieOrder::Always => true,
85        WriteTieOrder::Never => false,
86        WriteTieOrder::OnlyNonTrivial => has_non_trivial_tie_order,
87    };
88    if write_tie_order {
89        let mut tie_order = vec![!0; election.num_candidates];
90        for (&candidate, &rank) in &election.tie_order {
91            tie_order[rank] = candidate;
92        }
93
94        write!(output, "[tie")?;
95        for candidate in tie_order {
96            assert_ne!(candidate, !0);
97            write!(output, " ")?;
98            write_candidate(
99                output,
100                candidate,
101                &election.candidates[candidate],
102                candidate_format,
103            )?;
104        }
105        writeln!(output, "]")?;
106    }
107
108    for ballot in &election.ballots {
109        write!(output, "{}", ballot.count())?;
110        for ranking in ballot.order() {
111            write!(output, " ")?;
112            for (i, candidate) in ranking.iter().map(|&x| x.into()).enumerate() {
113                if i != 0 {
114                    write!(output, "=")?;
115                }
116                write_candidate(
117                    output,
118                    candidate,
119                    &election.candidates[candidate],
120                    candidate_format,
121                )?;
122            }
123        }
124        writeln!(output, " 0")?;
125    }
126
127    writeln!(output, "0")?;
128    for candidate in &election.candidates {
129        writeln!(output, "\"{}\"", candidate.name)?;
130    }
131    writeln!(output, "\"{}\"", election.title)?;
132
133    Ok(())
134}
135
136fn write_candidate(
137    output: &mut impl Write,
138    index: usize,
139    candidate: &Candidate,
140    candidate_format: CandidateFormat,
141) -> io::Result<()> {
142    match candidate_format {
143        CandidateFormat::Nicknames => write!(output, "{}", candidate.nickname),
144        CandidateFormat::Numeric => write!(output, "{}", index + 1),
145    }
146}
147
148#[cfg(test)]
149mod test {
150    use super::*;
151    use crate::types::{Ballot, ElectionBuilder};
152
153    fn test_election_builder() -> ElectionBuilder {
154        Election::builder()
155            .title("Vegetable contest")
156            .num_seats(2)
157            .candidates([
158                Candidate::new("apple", false),
159                Candidate::new("banana", false),
160                Candidate::new("cherry", false),
161                Candidate::new("date", false),
162            ])
163            .ballots([
164                Ballot::new(1, [vec![0]]),
165                Ballot::new(2, [vec![2, 1], vec![3]]),
166                Ballot::new(3, [vec![1, 2]]),
167                Ballot::new(4, [vec![3, 1], vec![0, 2]]),
168                Ballot::new(5, [vec![1, 2, 3, 0]]),
169            ])
170    }
171
172    #[test]
173    fn test_write_blt_withdrawn() {
174        let election = Election::builder()
175            .title("Vegetable contest")
176            .num_seats(2)
177            .candidates([
178                Candidate::new("apple", true),
179                Candidate::new("banana", false),
180                Candidate::new("cherry", true),
181                Candidate::new("date", false),
182            ])
183            .ballots([
184                Ballot::new(1, [vec![0]]),
185                Ballot::new(2, [vec![2, 1], vec![3]]),
186            ])
187            .build();
188
189        let mut buf = Vec::new();
190        write_blt(
191            &mut buf,
192            &election,
193            WriteTieOrder::Always,
194            CandidateFormat::Nicknames,
195        )
196        .unwrap();
197
198        assert_eq!(
199            std::str::from_utf8(&buf).unwrap(),
200            r#"4 2
201[nick apple banana cherry date]
202[withdrawn apple cherry]
203[tie apple banana cherry date]
2041 apple 0
2052 cherry=banana date 0
2060
207"Apple"
208"Banana"
209"Cherry"
210"Date"
211"Vegetable contest"
212"#
213        );
214    }
215
216    #[test]
217    fn test_write_blt_with_ties_trivial_ties() {
218        let mut buf = Vec::new();
219        write_blt(
220            &mut buf,
221            &test_election_builder().build(),
222            WriteTieOrder::Always,
223            CandidateFormat::Nicknames,
224        )
225        .unwrap();
226
227        assert_eq!(
228            std::str::from_utf8(&buf).unwrap(),
229            r#"4 2
230[nick apple banana cherry date]
231[tie apple banana cherry date]
2321 apple 0
2332 cherry=banana date 0
2343 banana=cherry 0
2354 date=banana apple=cherry 0
2365 banana=cherry=date=apple 0
2370
238"Apple"
239"Banana"
240"Cherry"
241"Date"
242"Vegetable contest"
243"#
244        );
245    }
246
247    #[test]
248    fn test_write_blt_with_ties_has_ties() {
249        let mut buf = Vec::new();
250        write_blt(
251            &mut buf,
252            &test_election_builder().tie_order([1, 3, 2, 0]).build(),
253            WriteTieOrder::Always,
254            CandidateFormat::Nicknames,
255        )
256        .unwrap();
257
258        assert_eq!(
259            std::str::from_utf8(&buf).unwrap(),
260            r#"4 2
261[nick apple banana cherry date]
262[tie banana date cherry apple]
2631 apple 0
2642 cherry=banana date 0
2653 banana=cherry 0
2664 date=banana apple=cherry 0
2675 banana=cherry=date=apple 0
2680
269"Apple"
270"Banana"
271"Cherry"
272"Date"
273"Vegetable contest"
274"#
275        );
276    }
277
278    #[test]
279    fn test_write_blt_no_ties_trivial_ties() {
280        let mut buf = Vec::new();
281        write_blt(
282            &mut buf,
283            &test_election_builder().build(),
284            WriteTieOrder::Never,
285            CandidateFormat::Nicknames,
286        )
287        .unwrap();
288
289        assert_eq!(
290            std::str::from_utf8(&buf).unwrap(),
291            r#"4 2
292[nick apple banana cherry date]
2931 apple 0
2942 cherry=banana date 0
2953 banana=cherry 0
2964 date=banana apple=cherry 0
2975 banana=cherry=date=apple 0
2980
299"Apple"
300"Banana"
301"Cherry"
302"Date"
303"Vegetable contest"
304"#
305        );
306    }
307
308    #[test]
309    #[should_panic(
310        expected = "Writing the tie order is disabled, but the tie order is non-trivial"
311    )]
312    fn test_write_blt_no_ties_has_ties() {
313        let mut buf = Vec::new();
314        write_blt(
315            &mut buf,
316            &test_election_builder().tie_order([1, 3, 2, 0]).build(),
317            WriteTieOrder::Never,
318            CandidateFormat::Nicknames,
319        )
320        .unwrap();
321
322        assert_eq!(
323            std::str::from_utf8(&buf).unwrap(),
324            r#"4 2
325[nick apple banana cherry date]
326[tie banana date cherry apple]
3271 apple 0
3282 cherry=banana date 0
3293 banana=cherry 0
3304 date=banana apple=cherry 0
3315 banana=cherry=date=apple 0
3320
333"Apple"
334"Banana"
335"Cherry"
336"Date"
337"Vegetable contest"
338"#
339        );
340    }
341
342    #[test]
343    fn test_write_blt_maybe_ties_trivial_ties() {
344        let mut buf = Vec::new();
345        write_blt(
346            &mut buf,
347            &test_election_builder().build(),
348            WriteTieOrder::OnlyNonTrivial,
349            CandidateFormat::Nicknames,
350        )
351        .unwrap();
352
353        assert_eq!(
354            std::str::from_utf8(&buf).unwrap(),
355            r#"4 2
356[nick apple banana cherry date]
3571 apple 0
3582 cherry=banana date 0
3593 banana=cherry 0
3604 date=banana apple=cherry 0
3615 banana=cherry=date=apple 0
3620
363"Apple"
364"Banana"
365"Cherry"
366"Date"
367"Vegetable contest"
368"#
369        );
370    }
371
372    #[test]
373    fn test_write_blt_maybe_ties_has_ties() {
374        let mut buf = Vec::new();
375        write_blt(
376            &mut buf,
377            &test_election_builder().tie_order([1, 3, 2, 0]).build(),
378            WriteTieOrder::OnlyNonTrivial,
379            CandidateFormat::Nicknames,
380        )
381        .unwrap();
382
383        assert_eq!(
384            std::str::from_utf8(&buf).unwrap(),
385            r#"4 2
386[nick apple banana cherry date]
387[tie banana date cherry apple]
3881 apple 0
3892 cherry=banana date 0
3903 banana=cherry 0
3914 date=banana apple=cherry 0
3925 banana=cherry=date=apple 0
3930
394"Apple"
395"Banana"
396"Cherry"
397"Date"
398"Vegetable contest"
399"#
400        );
401    }
402
403    #[test]
404    fn test_write_blt_numeric_with_ties() {
405        let mut buf = Vec::new();
406        write_blt(
407            &mut buf,
408            &test_election_builder().build(),
409            WriteTieOrder::Always,
410            CandidateFormat::Numeric,
411        )
412        .unwrap();
413
414        assert_eq!(
415            std::str::from_utf8(&buf).unwrap(),
416            r#"4 2
417[tie 1 2 3 4]
4181 1 0
4192 3=2 4 0
4203 2=3 0
4214 4=2 1=3 0
4225 2=3=4=1 0
4230
424"Apple"
425"Banana"
426"Cherry"
427"Date"
428"Vegetable contest"
429"#
430        );
431    }
432
433    #[test]
434    fn test_write_blt_numeric_no_ties() {
435        let mut buf = Vec::new();
436        write_blt(
437            &mut buf,
438            &test_election_builder().build(),
439            WriteTieOrder::Never,
440            CandidateFormat::Numeric,
441        )
442        .unwrap();
443
444        assert_eq!(
445            std::str::from_utf8(&buf).unwrap(),
446            r#"4 2
4471 1 0
4482 3=2 4 0
4493 2=3 0
4504 4=2 1=3 0
4515 2=3=4=1 0
4520
453"Apple"
454"Banana"
455"Cherry"
456"Date"
457"Vegetable contest"
458"#
459        );
460    }
461}