From aca5be2a9e90ccaae7ecab15b91837c1b0e75e5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20Dani=C3=ABl=20Maat?= <tm@tlater.net> Date: Sun, 9 Mar 2025 06:04:29 +0800 Subject: [PATCH] feat(lib): Implement draft list parser --- src/draft_list.rs | 122 ++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 123 insertions(+) create mode 100644 src/draft_list.rs create mode 100644 src/lib.rs diff --git a/src/draft_list.rs b/src/draft_list.rs new file mode 100644 index 0000000..14451cb --- /dev/null +++ b/src/draft_list.rs @@ -0,0 +1,122 @@ +use std::{collections::HashMap, io::BufRead}; +use thiserror::Error; + +/// Parser for Will's special banlist format that lists extra copies +/// of cards in comments +pub struct DraftList { + pub title: String, + pub cards: HashMap<u32, usize>, +} + +impl DraftList { + /// Parse a draft list from a u8 slice. + pub fn from_slice(file: &[u8]) -> Result<Self> { + let mut title = None; + let mut cards = HashMap::new(); + + for line in file.lines() { + let line = line?; + + fn parse_card(card_id: &str, quantity: &str) -> Result<(u32, usize)> { + let id = card_id + .parse() + .map_err(|err| DraftListParseError::InvalidCardID(card_id.to_owned(), err))?; + + let quantity = quantity.parse().map_err(|err| { + DraftListParseError::InvalidCardQuantity(quantity.to_owned(), err) + })?; + + Ok((id, quantity)) + } + + if let Some(spare) = line.strip_prefix("###") { + let spare = spare.trim(); + let Some((id, quantity)) = spare.split_once(' ') else { + // Just assume that this is a comment + continue; + }; + + let (id, quantity) = parse_card(id, quantity)?; + // The spare quantity does *not* include the base 3 + // cards we should have if we have spares. + cards.insert(id, quantity + 3); + } else if let Some(t) = line.strip_prefix('!') { + title = Some(t.to_owned()); + } else if line.starts_with('#') { + // Skip any comments with less than 3 # + continue; + } else if let Some((id, quantity)) = line.split_once(' ') { + // We record invalid cards with -1, which obviously + // doesn't parse to usize, so just skip these + if quantity.starts_with('-') { + continue; + } + + let (id, quantity) = parse_card(id, quantity)?; + cards.insert(id, quantity); + } else if line == "$blacklist" { + return Err(DraftListParseError::InputIsBlacklist); + } + } + + Ok(Self { + title: title.ok_or(DraftListParseError::MissingTitle)?, + cards, + }) + } +} + +#[derive(Debug, Error)] +pub enum DraftListParseError { + #[error("draft list is corrupted")] + MalformedInput(#[from] std::io::Error), + #[error("draft list is actually a blacklist; currently unsupported")] + InputIsBlacklist, + #[error("draft list lacks a title")] + MissingTitle, + #[error("draft list contains an invalid card id: {}", 0)] + InvalidCardID(String, #[source] std::num::ParseIntError), + #[error("draft list contains an invalid card quantity: {}", 0)] + InvalidCardQuantity(String, #[source] std::num::ParseIntError), +} + +type Result<T> = std::result::Result<T, DraftListParseError>; + +#[cfg(test)] +mod tests { + use dedent::dedent; + + use super::DraftList; + + #[test] + fn simple_list() { + let list = dedent!( + r#" + !Treestan - Mar2024 17 + $whitelist + + 84177693 1 + 70491682 2 + 13361027 3 + + # Some irrelevant comment + 160405002 -1 + + ### 10731333 5 + ### 46136942 3 + ### 36211150 12 + "# + ) + .as_bytes(); + + let list = DraftList::from_slice(list).expect("draft list must parse correctly"); + + assert_eq!(list.title, "Treestan - Mar2024 17"); + assert_eq!(list.cards.get(&84177693), Some(&1)); + assert_eq!(list.cards.get(&70491682), Some(&2)); + assert_eq!(list.cards.get(&13361027), Some(&3)); + assert_eq!(list.cards.get(&10731333), Some(&8)); + assert_eq!(list.cards.get(&46136942), Some(&6)); + assert_eq!(list.cards.get(&36211150), Some(&15)); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..58a3ab2 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ +pub mod draft_list;