wybr 0.0.6

Collection of preferential voting methods
Documentation
use std::collections::{HashSet, VecDeque};
use std::io::prelude::*;

use crate::tally::VoteReadError::*;
use crate::tally::VoteToken;
use crate::tally::VoteToken::*;

pub struct BltReader<'a, B: BufRead> {
    token_queue: VecDeque<VoteToken>,
    candidates: Option<u32>,
    line: usize,
    footer: bool,
    broken: bool,
    footer_line: usize,
    stream: &'a mut B,
}

impl<'a, B: BufRead> BltReader<'a, B> {
    pub fn new(stream: &mut B) -> BltReader<B> {
        BltReader {
            stream,
            token_queue: VecDeque::new(),
            line: 0,
            broken: false,
            candidates: None,
            footer: false,
            footer_line: 0,
        }
    }
    fn read_line1(&mut self, buf: &str) {
        let mut iter = buf.split(char::is_whitespace).filter(|x| !x.is_empty());
        for i in &[0, 1] {
            self.token_queue.push_back(if let Some(eval) = iter.next() {
                match (*i, eval.parse::<u32>()) {
                    (0, Ok(val)) => {
                        self.candidates = Some(val);
                        DeclareCandidates(val)
                    }
                    (1, Ok(val)) => DeclareSeats(val),
                    _ => ReadFailure(CannotParse(self.line, eval.to_string())),
                }
            } else {
                ReadFailure(WrongSyntax(self.line))
            });
        }
    }

    fn read_line2(&mut self, buf: &str) {
        let cmax: i64 = self.candidates.unwrap().into();
        if buf.trim().starts_with('-') {
            let line = self.line;
            let iter = buf
                .split(char::is_whitespace)
                .filter(|x| !x.is_empty())
                .map(|x| {
                    if let Ok(val) = x.parse::<i64>() {
                        if val < 0 && val >= -cmax {
                            WithdrawCandidate((-val - 1) as u32)
                        } else {
                            ReadFailure(CannotParse(line, x.to_string()))
                        }
                    } else {
                        ReadFailure(CannotParse(line, x.to_string()))
                    }
                });
            for e in iter {
                self.token_queue.push_back(e);
            }
        } else {
            self.read_body(buf);
        }
    }

    fn read_body(&mut self, buf: &str) {
        let line = self.line;
        let cmax: u32 = self.candidates.unwrap();
        let mut seen: HashSet<u32> = HashSet::with_capacity(cmax as usize);
        let mut pref = 0i32;
        let mut iter = buf
            .split(char::is_whitespace)
            //Required because end-of-line throws ""
            .filter(|x| !x.is_empty())
            .enumerate()
            .map(|(i, x)| {
                if i == 0 {
                    //Repeat or end-of-ballot
                    if let Ok(val) = x.parse::<u64>() {
                        if val == 0 {
                            EndBallots
                        } else {
                            BallotRepeat(val)
                        }
                    } else {
                        ReadFailure(CannotParse(line, x.to_string()))
                    }
                } else {
                    //Vote
                    let (eq, val) = if let Some(eqval) = x.strip_prefix('=') {
                        if let Ok(val) = eqval.parse::<u32>() {
                            (true, val)
                        } else {
                            return ReadFailure(CannotParse(line, x.to_string()));
                        }
                    } else if let Ok(val) = x.parse::<u32>() {
                        (false, val)
                    } else {
                        return ReadFailure(CannotParse(line, x.to_string()));
                    };
                    if val == 0 {
                        EndBallot
                    } else if val > cmax || seen.contains(&val) {
                        ReadFailure(InvalidValue(line, x.to_string()))
                    } else {
                        seen.insert(val);
                        if !eq {
                            pref += 1;
                        }
                        Vote(pref, val - 1)
                    }
                }
            })
            .peekable();
        if let Some(EndBallots) = iter.peek() {
            //Either end-of-ballots or 0-rep ballot, which we ignore doing nothing
            if iter.count() == 1 {
                self.footer = true;
                self.token_queue.push_back(EndBallots);
            }
        } else {
            //Normal ballot
            while let Some(e) = iter.next() {
                if let EndBallot = e {
                    if iter.peek().is_none() {
                        //Proper ballot end
                        self.token_queue.push_back(e);
                    } else {
                        //Zero somewhere inside, ignore
                    }
                } else {
                    //Normal token
                    self.token_queue.push_back(e);
                }
            }
        }
    }

    fn read_footer(&mut self, buf: &str) {
        let line = self.line;
        let trimmed = buf.trim();
        if !trimmed.is_empty() {
            self.footer_line += 1;
            self.token_queue
                .push_back(if !trimmed.starts_with('"') || !trimmed.ends_with('"') {
                    ReadFailure(InvalidToken(line, trimmed.to_string()))
                } else if let Some(candidates) = self.candidates {
                    let name = trimmed[1..(trimmed.len() - 1)].to_string();
                    let id = self.footer_line as u32 - 1;
                    use std::cmp::Ordering;
                    match id.cmp(&candidates) {
                        Ordering::Less => CandidateName(id, name),
                        Ordering::Equal => ElectionTitle(name),
                        _ => ReadFailure(WrongSyntax(line)),
                    }
                } else {
                    panic!("Candidates not emitted before footer; this shall never happen!")
                });
        }
    }
}

impl<'a, B: BufRead> Iterator for BltReader<'a, B> {
    type Item = VoteToken;
    fn next(&mut self) -> Option<VoteToken> {
        if self.broken {
            None
        } else if let Some(x) = self.token_queue.pop_front() {
            if let ReadFailure(_) = x {
                self.broken = true;
            }
            Some(x)
        } else {
            let mut buf = String::new();
            self.line += 1;
            match self.stream.read_line(&mut buf) {
                Ok(read) => {
                    if read == 0 {
                        None
                    } else {
                        if self.line == 1 {
                            self.read_line1(&buf)
                        } else if self.line == 2 {
                            self.read_line2(&buf)
                        } else if !self.footer {
                            self.read_body(&buf)
                        } else {
                            self.read_footer(&buf)
                        }
                        //Deque shall be replenished by now
                        self.next()
                    }
                }
                Err(err) => {
                    self.broken = true;
                    Some(ReadFailure(IOError(err)))
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Cursor;
    #[test]
    fn parse_sample_blt() {
        const EASY_BLT: &'static [u8] =
            b" 3  1\n -3 \n 7  1 =2  =3 0\n \n  17 0 2 0 0 0 3 1  0\n 0 17 8 0\n27 3 2  1 0\n 0 \n \"A\"\n  \n\"B\" \n\"C\"\n \"Title\" \n";
        let mut cursor = Cursor::new(EASY_BLT);
        let blt_reader = BltReader::new(&mut cursor);
        for e in blt_reader.enumerate() {
            match e {
                (0, DeclareCandidates(3))
                | (1, DeclareSeats(1))
                | (2, WithdrawCandidate(2))
                | (3, BallotRepeat(7))
                | (4, Vote(1, 0))
                | (5, Vote(1, 1))
                | (6, Vote(1, 2))
                | (7, EndBallot)
                | (8, BallotRepeat(17))
                | (9, Vote(1, 1))
                | (10, Vote(2, 2))
                | (11, Vote(3, 0))
                | (12, EndBallot)
                | (13, BallotRepeat(27))
                | (14, Vote(1, 2))
                | (15, Vote(2, 1))
                | (16, Vote(3, 0))
                | (17, EndBallot)
                | (18, EndBallots) => (),
                (19, CandidateName(0, x)) => assert_eq!(x, "A"),
                (20, CandidateName(1, x)) => assert_eq!(x, "B"),
                (21, CandidateName(2, x)) => assert_eq!(x, "C"),
                (22, ElectionTitle(x)) => assert_eq!(x, "Title"),
                _ => unreachable!(),
            }
        }
    }
    #[test]
    fn blt_cannot_parse() {
        const BAD: &'static [u8] = b" eee  1\n";
        let mut cursor = Cursor::new(BAD);
        let mut blt_reader = BltReader::new(&mut cursor);
        match blt_reader.next() {
            Some(ReadFailure(CannotParse(1, x))) => assert_eq!(x, "eee"),
            _ => unreachable!(),
        }
    }
    #[test]
    fn blt_header() {
        const BAD: &'static [u8] = b" 1 \n";
        let mut cursor = Cursor::new(BAD);
        let blt_reader = BltReader::new(&mut cursor);
        match blt_reader.skip(1).next() {
            Some(ReadFailure(WrongSyntax(l))) => assert_eq!(l, 1),
            _ => unreachable!(),
        }
    }
    #[test]
    fn blt_bad_cand() {
        const BAD: &'static [u8] = b"3 1\n77 1 2 3 0\n99 2 4 1 0\n";
        let mut cursor = Cursor::new(BAD);
        let blt_reader = BltReader::new(&mut cursor);
        match blt_reader
            .filter(|x| if let ReadFailure(_) = x { true } else { false })
            .next()
        {
            Some(ReadFailure(InvalidValue(l, s))) => {
                assert_eq!(l, 3);
                assert_eq!(s, "4")
            }
            _ => unreachable!(),
        }
    }
}