diff --git a/divi/src/card_record.rs b/divi/src/card_record.rs index cf3d447f..f9b77c96 100644 --- a/divi/src/card_record.rs +++ b/divi/src/card_record.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; - use serde::{Deserialize, Serialize}; use crate::{ @@ -7,21 +5,6 @@ use crate::{ IsCard, }; -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -pub struct FixedCardName { - pub old: String, - pub fixed: String, -} - -impl FixedCardName { - pub fn new(old: &str, fixed: &str) -> FixedCardName { - FixedCardName { - old: String::from(old), - fixed: String::from(fixed), - } - } -} - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] pub struct DivinationCardRecord { pub name: String, @@ -54,55 +37,10 @@ impl DivinationCardRecord { self } - pub fn set_weight(&mut self, weight_sample: f32) -> &mut Self { - self.weight = Some((weight_sample * self.amount as f32).powf(1.0 / CONDENSE_FACTOR)); + pub fn set_weight(&mut self, weight_multiplier: f32) -> &mut Self { + self.weight = Some((weight_multiplier * self.amount as f32).powf(1.0 / CONDENSE_FACTOR)); self } - - fn most_similar_card(name: &str) -> (String, f64) { - let mut similarity_map = HashMap::::new(); - for card in CARDS { - let similarity = strsim::normalized_damerau_levenshtein(&name, card); - similarity_map.insert(card.to_string(), similarity); - } - - let most_similar = similarity_map - .iter() - .max_by(|a, b| a.1.partial_cmp(b.1).unwrap()) - .unwrap(); - - (most_similar.0.to_owned(), most_similar.1.to_owned()) - } - - pub fn fix_name(&mut self) -> Option { - match self.is_card() { - true => None, - false => self.fix_name_unchecked(), - } - } - - pub fn fix_name_unchecked(&mut self) -> Option { - let (similar, score) = Self::most_similar_card(&self.name); - match score >= 0.75 { - true => { - let fixed_name = FixedCardName::new(&self.name, &similar); - self.name = similar; - Some(fixed_name) - } - false => { - let the_name = format!("The {}", &self.name); - let (similar, score) = Self::most_similar_card(&the_name); - match score >= 0.75 { - true => { - let fixed_name = FixedCardName::new(&self.name, &similar); - self.name = similar; - Some(fixed_name) - } - false => None, - } - } - } - } } impl IsCard for DivinationCardRecord { diff --git a/divi/src/cards.rs b/divi/src/cards.rs index 25c2b9f4..b189cce7 100644 --- a/divi/src/cards.rs +++ b/divi/src/cards.rs @@ -8,6 +8,7 @@ use crate::{ use serde::{Deserialize, Serialize}; use serde_big_array::BigArray; +/// Holds an array of cards with length equal to number of all divination cards(For example, 440 in 2.23 patch) #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Cards(#[serde(with = "BigArray")] pub [DivinationCardRecord; CARDS_N]); diff --git a/divi/src/lib.rs b/divi/src/lib.rs index 8125beea..5a3568bc 100644 --- a/divi/src/lib.rs +++ b/divi/src/lib.rs @@ -1,5 +1,3 @@ -// #![allow(unused)] - use consts::{CARDS, LEGACY_CARDS}; pub mod card_record; pub mod cards; diff --git a/divi/src/prices.rs b/divi/src/prices.rs index 159b636d..24c09ab4 100644 --- a/divi/src/prices.rs +++ b/divi/src/prices.rs @@ -20,6 +20,7 @@ pub struct DivinationCardPrice { pub sparkline: Sparkline, } +/// Holds an array of card prices with length equal to number of all divination cards(For example, 440 in 2.23 patch) #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(transparent)] pub struct Prices(#[serde(with = "BigArray")] pub [DivinationCardPrice; CARDS_N]); diff --git a/divi/src/sample.rs b/divi/src/sample.rs index b38f2230..1e4c4f2a 100644 --- a/divi/src/sample.rs +++ b/divi/src/sample.rs @@ -1,10 +1,11 @@ +use std::collections::HashMap; + use csv::{ReaderBuilder, Trim}; use serde::{Deserialize, Serialize}; use crate::{ - card_record::{DivinationCardRecord, FixedCardName}, cards::Cards, - consts::RAIN_OF_CHAOS_WEIGHT, + consts::{CARDS, RAIN_OF_CHAOS_WEIGHT}, error::Error, prices::Prices, IsCard, @@ -39,7 +40,10 @@ impl DivinationCardsSample { source: SampleData, prices: Option, ) -> Result { - DivinationCardsSample::from_prices(prices).parse_data(source) + let mut sample = DivinationCardsSample::from_prices(prices); + let parsed = sample.parse_data(source)?; + parsed.get_sample_ready(); + Ok(sample) } pub fn merge( @@ -56,7 +60,8 @@ impl DivinationCardsSample { card.set_amount_and_sum(amount); } - merged.get_sample_ready() + merged.get_sample_ready(); + merged } /// Consumes Prices structure to set prices for Cards @@ -67,95 +72,78 @@ impl DivinationCardsSample { } } - /// Reads the source, sets amounts of cards, fills not_cards and fixed_names. Then gets sample ready by writing weights and polished csv. - fn parse_data(&mut self, source: SampleData) -> Result { - let sample = match source { + /// Parsing helper. Uses for CSV data + fn remove_lines_before_headers(s: &str) -> Result { + match s.lines().enumerate().into_iter().find(|(_index, line)| { + line.contains("name") + && ["amount", "stackSize"] + .iter() + .any(|variant| line.contains(variant)) + }) { + Some((index, _line)) => Ok(s + .lines() + .into_iter() + .skip(index) + .collect::>() + .join("\r\n")), + None => Err(Error::MissingHeaders), + } + } + + /// Reads the source to extract an amount of cards for each name + fn parse_data(&mut self, source: SampleData) -> Result<&mut Self, Error> { + match source { SampleData::Csv(s) => { let data = Self::remove_lines_before_headers(&s)?; let mut rdr = ReaderBuilder::new() .trim(Trim::All) .from_reader(data.as_bytes()); - for result in rdr.deserialize::() { - if let Ok(mut record) = result { - match &record.is_card() { - true => { - let mut_card = self.cards.get_card_mut(&record.name); - mut_card.set_amount_and_sum(mut_card.amount + record.amount); - } - false => match record.fix_name() { - Some(fixed) => { - let mut_card = self.cards.get_card_mut(&record.name); - mut_card.set_amount_and_sum(mut_card.amount + record.amount); - self.fixed_names.push(fixed); - } - None => self.not_cards.push(record.name), - }, - } - } else { - println!("{:?}", result.err()); + for result in rdr.deserialize::() { + match result { + Ok(card_name_amount) => self.extract_amount(card_name_amount), + Err(err) => println!("{:?}", err), } } - Ok::<&mut DivinationCardsSample, Error>(self) + Ok(self) } SampleData::CardNameAmountList(vec) => { - for CardNameAmount { name, amount } in vec { - let mut record = DivinationCardRecord { - name, - price: None, - amount, - sum: None, - weight: None, - }; - - match &record.is_card() { - true => { - let mut_card = self.cards.get_card_mut(&record.name); - mut_card.set_amount_and_sum(mut_card.amount + record.amount); - } - - false => match record.fix_name() { - Some(fixed) => { - let mut_card = self.cards.get_card_mut(&record.name); - mut_card.set_amount_and_sum(mut_card.amount + record.amount); - self.fixed_names.push(fixed); - } - None => self.not_cards.push(record.name), - }, - } + for card_name_amount in vec { + self.extract_amount(card_name_amount) } - Ok(self) } - }?; - Ok(sample.get_sample_ready()) + } } - /// Preparsing helper - fn remove_lines_before_headers(s: &str) -> Result { - match s.lines().enumerate().into_iter().find(|(_index, line)| { - line.contains("name") - && ["amount", "stackSize"] - .iter() - .any(|variant| line.contains(variant)) - }) { - Some((index, _line)) => Ok(s - .lines() - .into_iter() - .skip(index) - .collect::>() - .join("\r\n")), - None => Err(Error::MissingHeaders), + /// The part of parsing data process. Extracts amount from individual name-amount source. If name is not card, tries to fix the name + /// and pushes to fixed_names or to not_cards if fails. + fn extract_amount(&mut self, CardNameAmount { name, amount }: CardNameAmount) { + if name.as_str().is_card() { + let mut_card = self.cards.get_card_mut(&name); + mut_card.set_amount_and_sum(mut_card.amount + amount); + } else { + match fix_name(&name) { + Some(fixed) => { + self.fixed_names.push(FixedCardName { + old: name, + fixed: fixed.clone(), + }); + let mut_card = self.cards.get_card_mut(&fixed); + mut_card.set_amount_and_sum(mut_card.amount + amount); + } + None => self.not_cards.push(name), + } } } /// Writes weights for cards and writes final csv - write_weight and write_csv in one function - fn get_sample_ready(&mut self) -> Self { - self.write_weight().write_csv().to_owned() + fn get_sample_ready(&mut self) -> &mut Self { + self.write_weight().write_csv() } /// Helper function for write_weight - fn sample_weight(&self) -> f32 { + fn weight_multiplier(&self) -> f32 { let rain_of_chaos = self .cards .get("Rain of Chaos") @@ -165,10 +153,10 @@ impl DivinationCardsSample { /// (After parsing) Calculates special weight for each card and mutates it. Runs at the end of parsing. fn write_weight(&mut self) -> &mut Self { - let sample_weight = self.sample_weight(); + let weight_multiplier = self.weight_multiplier(); - for card in &mut self.cards.iter_mut() { - card.set_weight(sample_weight); + for card in self.cards.iter_mut() { + card.set_weight(weight_multiplier); } self @@ -186,9 +174,60 @@ impl DivinationCardsSample { } } +pub fn fix_name(name: &str) -> Option { + if name.is_card() { + return None; + } + + let (most_similar, score) = most_similar_card(name); + + match score >= 0.75 { + true => Some(most_similar), + false => { + // Try to prefix name with "The" - a lot of cards start with "The" + let (most_similar, score) = most_similar_card(&format!("The {name}")); + match score >= 0.75 { + true => Some(most_similar), + false => None, + } + } + } +} + +fn most_similar_card(name: &str) -> (String, f64) { + let mut similarity_map = HashMap::::new(); + for card in CARDS { + let similarity = strsim::normalized_damerau_levenshtein(&name, card); + similarity_map.insert(card.to_string(), similarity); + } + + let most_similar = similarity_map + .iter() + .max_by(|a, b| a.1.partial_cmp(b.1).unwrap()) + .unwrap(); + + (most_similar.0.to_owned(), most_similar.1.to_owned()) +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct FixedCardName { + pub old: String, + pub fixed: String, +} + +impl FixedCardName { + pub fn new(old: &str, fixed: &str) -> FixedCardName { + FixedCardName { + old: String::from(old), + fixed: String::from(fixed), + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CardNameAmount { pub name: String, + #[serde(alias = "stackSize")] pub amount: i32, } @@ -202,26 +241,8 @@ pub enum SampleData { #[cfg(test)] mod tests { - use crate::IsCard; - use super::*; - // #[tokio::test] - // async fn name_amount() { - // let json = std::fs::read_to_string("cardNameAmountList.json").unwrap(); - // let vec: Vec = serde_json::from_str(&json).unwrap(); - // let cards_total_amount: i32 = vec.iter().map(|card| card.amount).sum(); - // assert_eq!(cards_total_amount, 181); - // let sample = DivinationCardsSample::create( - // SampleData::CardNameAmountList(vec), - // Some(Prices::fetch(&TradeLeague::HardcoreAncestor).await.unwrap()), - // ) - // .unwrap(); - - // let sample_total_amount: i32 = sample.cards.iter().map(|card| card.amount).sum(); - // dbg!(sample_total_amount); - // } - #[test] fn trim() { let s = "something,something\r\nname,stackSize\r\nA Dab of Ink,2\r\nA Familiar Call,1\r\nA Fate Worse than Death,2\r\nA Mother's Parting Gift,15\r\nA Sea of Blue,22\r\nA Stone Perfected,2\r\nAbandoned Wealth,4\r\nAccumitisation,14\r\nAlluring Bounty,3\r\nAlone in the Darkness,30\r\nAnarchy's Price,5\r\nArrogance of the Vaal,5\r\nAssassin's Favour,44\r\nAstral Projection,7\r\nAtziri's Arsenal,6\r\nAudacity,3\r\nAzure Rage,14\r\nAzyran's Reward,4\r\nBaited Expectations,4\r\nBijoux,2\r\nBlind Venture,11\r\nBoon of Justice,20\r\nBoon of the First Ones,3\r\nBoundless Realms,23\r\nBroken Truce,15\r\nBrotherhood in Exile,1\r\n\"Brush, Paint and Palette\",6\r\nBuried Treasure,7\r\nCall to the First Ones,13\r\nCameria's Cut,3\r\nCartographer's Delight,20\r\nChaotic Disposition,13\r\nChasing Risk,6\r\nCheckmate,5\r\nCostly Curio,3\r\nCouncil of Cats,5\r\nCoveted Possession,7\r\nCursed Words,12\r\nDark Dreams,3\r\nDark Temptation,23\r\nDeadly Joy,1\r\nDeath,5\r\nDeathly Designs,4\r\nDementophobia,1\r\nDemigod's Wager,5\r\nDesperate Crusade,2\r\nDestined to Crumble,80\r\nDialla's Subjugation,12\r\nDisdain,1\r\nDivine Justice,3\r\nDoedre's Madness,44\r\nDoryani's Epiphany,1\r\nDying Anguish,16\r\nDying Light,1\r\nEarth Drinker,8\r\nEchoes of Love,2\r\nEmperor of Purity,14\r\nEmperor's Luck,79\r\nEndless Night,3\r\nForbidden Power,16\r\nFrom Bone to Ash,1\r\nFurther Invention,1\r\nGemcutter's Mercy,2\r\nGemcutter's Promise,21\r\nGift of Asenath,3\r\nGift of the Gemling Queen,10\r\nGlimmer of Hope,34\r\nGrave Knowledge,13\r\nGuardian's Challenge,13\r\nHarmony of Souls ,1\r\nHer Mask,40\r\nHeterochromia,8\r\nHome,1\r\nHope,8\r\nHubris,24\r\nHumility,27\r\nHunter's Resolve,25\r\nHunter's Reward,4\r\nImmortal Resolve,5\r\nImperfect Memories,1\r\nImperial Legacy,36\r\nJack in the Box,11\r\nJudging Voices,2\r\nJustified Ambition,6\r\nLachrymal Necrosis,2\r\nLantador's Lost Love,50\r\nLast Hope,29\r\nLeft to Fate,9\r\nLight and Truth,4\r\nLingering Remnants,12\r\nLost Worlds,30\r\nLove Through Ice,1\r\nLoyalty,96\r\nLucky Connections,25\r\nLucky Deck,2\r\nLuminous Trove,1\r\nLysah's Respite,18\r\nMawr Blaidd,2\r\nMerciless Armament,2\r\nMight is Right,16\r\nMisery in Darkness,3\r\nMitts,27\r\nMonochrome,4\r\nMore is Never Enough,4\r\nNo Traces,15\r\nParasitic Passengers,4\r\nPeaceful Moments,4\r\nPrejudice,20\r\nPride before the Fall,2\r\nPride of the First Ones,1\r\nPrometheus' Armoury,2\r\nProsperity,31\r\nRain of Chaos,188\r\nRain Tempter,34\r\nRats,48\r\nRebirth,2\r\nRebirth and Renewal,3\r\nReckless Ambition,4\r\nRemembrance,1\r\nSambodhi's Vow,31\r\nSambodhi's Wisdom,11\r\nScholar of the Seas,9\r\nSeven Years Bad Luck,1\r\nShard of Fate,27\r\nSilence and Frost,4\r\nSociety's Remorse,11\r\nSomething Dark,4\r\nStruck by @@ -232,18 +253,6 @@ Encroaching Darkness,5\r\nThe Endless Darkness,1\r\nThe Endurance,19\r\nThe Enfo assert_eq!(trimmed.lines().next().unwrap(), "name,stackSize"); } - #[test] - fn is_card() { - let record = DivinationCardRecord::new("Rain of Chaos", None, None); - assert_eq!(record.is_card(), true); - } - - #[test] - fn is_legacy_card() { - let record = DivinationCardRecord::new("Friendship", None, None); - assert_eq!(record.is_legacy_card(), true); - } - #[test] fn merge() { let csv1 = std::fs::read_to_string("example-1.csv").unwrap();