diff --git a/src/bot.rs b/src/bot.rs index 24f8f46..6f97cd5 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -498,7 +498,7 @@ async fn from_to_repl( msg: Message, db: Arc, ) -> Result<(), Box> { - if to == from { + if from == to || from.is_empty() || to.is_empty() { bot.send_message(msg.chat.id, DUNNO).await?; return Ok(()); } diff --git a/src/collector.rs b/src/collector.rs index a776dc9..69dee2c 100644 --- a/src/collector.rs +++ b/src/collector.rs @@ -38,12 +38,21 @@ pub fn filter_collection( match result { Ok(v) => { if v.is_empty() { - log::warn!("no rate found, source: {}", src); continue; } + let v = v + .iter() + .filter(|v| { + (!v.from.is_empty() && !v.to.is_empty()) + && v.from != v.to + && (v.buy.is_some_and(|v| v > dec!(0.0)) + || v.sell.is_some_and(|v| v > dec!(0.0))) + }) + .cloned() + .collect::>(); rates.insert(src, v); } - Err(err) => log::error!("failed to get rate: {err}, source: {}", src), + Err(err) => log::error!("failed to get rate: {err}, src: {src}"), } } rates @@ -175,17 +184,19 @@ async fn collect_arm_swiss(client: &Client) -> anyhow::Result> { let resp: arm_swiss::Response = arm_swiss::Response::get_rates(&client).await?; let lmasbrate = resp.lmasbrate.ok_or(Error::NoRates)?; let mut rates = vec![]; + let to = Currency::default(); for rate in lmasbrate { + let from = rate.iso; rates.push(Rate { - from: rate.iso.clone(), - to: Currency::default(), + from: from.clone(), + to: to.clone(), rate_type: RateType::NoCash, buy: Some(rate.bid), sell: Some(rate.offer), }); rates.push(Rate { - from: rate.iso.clone(), - to: Currency::default(), + from: from.clone(), + to: to.clone(), rate_type: RateType::Cash, buy: Some(rate.bid_cash), sell: Some(rate.offer_cash), @@ -251,25 +262,23 @@ async fn collect_ineco(client: &Client) -> anyhow::Result> { } let items = resp.items.ok_or(Error::NoRates)?; let mut rates = vec![]; + let to = Currency::default(); for item in items { - if item.cashless.buy.is_some() && item.cashless.sell.is_some() { - rates.push(Rate { - from: item.code.clone(), - to: Currency::default(), - rate_type: RateType::NoCash, - buy: item.cashless.buy, - sell: item.cashless.sell, - }); - } - if item.cash.buy.is_some() && item.cash.sell.is_some() { - rates.push(Rate { - from: item.code.clone(), - to: Currency::default(), - rate_type: RateType::Cash, - buy: item.cash.buy, - sell: item.cash.sell, - }); - } + let from = item.code; + rates.push(Rate { + from: from.clone(), + to: to.clone(), + rate_type: RateType::NoCash, + buy: item.cashless.buy, + sell: item.cashless.sell, + }); + rates.push(Rate { + from: from.clone(), + to: to.clone(), + rate_type: RateType::Cash, + buy: item.cash.buy, + sell: item.cash.sell, + }); } Ok(rates) } @@ -278,17 +287,19 @@ async fn collect_mellat(client: &Client) -> anyhow::Result> { let resp: mellat::Response = mellat::Response::get_rates(&client).await?; let result = resp.result.ok_or(Error::NoRates)?; let mut rates = vec![]; + let to = Currency::default(); for rate in result.data { + let from = rate.currency; rates.push(Rate { - from: rate.currency.clone(), - to: Currency::default(), + from: from.clone(), + to: to.clone(), rate_type: RateType::NoCash, buy: Some(rate.buy), sell: Some(rate.sell), }); rates.push(Rate { - from: rate.currency.clone(), - to: Currency::default(), + from: from.clone(), + to: to.clone(), rate_type: RateType::Cash, buy: Some(rate.buy_cash), sell: Some(rate.sell_cash), @@ -323,11 +334,11 @@ async fn collect_aeb(client: &Client) -> anyhow::Result> { let resp: aeb::Response = aeb::Response::get_rates(&client).await?; let mut rates = vec![]; for item in resp.rate_currency_settings { - for rate in item.rates.iter().filter(|v| { - [RateType::NoCash, RateType::Cash].contains(&v.rate_type) - && v.buy_rate.is_some() - && v.sell_rate.is_some() - }) { + for rate in item + .rates + .iter() + .filter(|v| [RateType::NoCash, RateType::Cash].contains(&v.rate_type)) + { rates.push(Rate { from: item.currency_code.clone(), to: resp.main_currency_code.clone(), @@ -374,25 +385,23 @@ async fn collect_unibank(client: &Client) -> anyhow::Result> { fn collect_lsoft(resp: lsoft::Response) -> anyhow::Result> { let items = resp.get_currency_list.currency_list.ok_or(Error::NoRates)?; let mut rates = vec![]; + let to = Currency::default(); for item in items { - if item.buy.is_some() && item.sell.is_some() { - rates.push(Rate { - from: item.external_id.clone(), - to: Currency::default(), - rate_type: RateType::NoCash, - buy: item.buy, - sell: item.sell, - }); - } - if item.csh_buy.is_some() && item.csh_sell.is_some() { - rates.push(Rate { - from: item.external_id.clone(), - to: Currency::default(), - rate_type: RateType::Cash, - buy: item.csh_buy, - sell: item.csh_sell, - }); - } + let from = item.external_id; + rates.push(Rate { + from: from.clone(), + to: to.clone(), + rate_type: RateType::NoCash, + buy: item.buy, + sell: item.sell, + }); + rates.push(Rate { + from: from.clone(), + to: to.clone(), + rate_type: RateType::Cash, + buy: item.csh_buy, + sell: item.csh_sell, + }); } Ok(rates) } @@ -421,25 +430,23 @@ async fn collect_idbank(client: &Client) -> anyhow::Result> { let resp: idbank::Response = idbank::Response::get_rates(&client).await?; let result = resp.result.ok_or(Error::NoRates)?; let mut rates = vec![]; + let to = Currency::default(); for rate in result.currency_rate { - if rate.buy.is_some() && rate.sell.is_some() { - rates.push(Rate { - from: rate.iso_txt.clone(), - to: Currency::default(), - rate_type: RateType::NoCash, - buy: rate.buy, - sell: rate.sell, - }); - } - if rate.csh_buy.is_some() && rate.csh_sell.is_some() { - rates.push(Rate { - from: rate.iso_txt.clone(), - to: Currency::default(), - rate_type: RateType::Cash, - buy: rate.csh_buy, - sell: rate.csh_sell, - }); - } + let from = rate.iso_txt; + rates.push(Rate { + from: from.clone(), + to: to.clone(), + rate_type: RateType::NoCash, + buy: rate.buy, + sell: rate.sell, + }); + rates.push(Rate { + from: from.clone(), + to: to.clone(), + rate_type: RateType::Cash, + buy: rate.csh_buy, + sell: rate.csh_sell, + }); } Ok(rates) } @@ -489,30 +496,46 @@ fn parse_moex(resp: moex::moex::Response) -> anyhow::Result> { fn parse_tinkoff(resp: moex::tinkoff::Response) -> anyhow::Result> { let mut rates = vec![]; - let (Some(bid), Some(ask)) = (resp.bids.first(), resp.asks.first()) else { - return Ok(rates); + let mut rate_buy = None; + let mut rate_sell = None; + let find_nominal = |d: Decimal| { + let mut nominal = dec!(0.0); + for i in 0..=10 { + let j = 10_i64.pow(i); + nominal = Decimal::new(j, 0); + if nominal % d != nominal { + break; + } + } + nominal }; - const NANO: usize = 9; - let buy = format!("{}.{:0NANO$}", ask.price.units, ask.price.nano).parse::()?; - let sell = format!("{}.{:0NANO$}", bid.price.units, bid.price.nano).parse::()?; - if buy.is_zero() || sell.is_zero() { - return Ok(rates); + let to_decimal = |units: String, nano: i32| { + const NANO: usize = 9; + format!("{}.{:0NANO$}", units, nano).parse::() + }; + if let Some(bid) = resp.bids.first() { + let sell = to_decimal(bid.price.units.clone(), bid.price.nano)?; + if sell > dec!(0.0) { + let nominal = find_nominal(sell); + rate_sell = Some(nominal / sell); + } } - let mut nominal = dec!(0.0); - for i in 0..=10 { - let j = 10_i64.pow(i); - nominal = Decimal::new(j, 0); - if nominal % buy != nominal { - break; + if let Some(ask) = resp.asks.first() { + let buy = to_decimal(ask.price.units.clone(), ask.price.nano)?; + if buy > dec!(0.0) { + let nominal = find_nominal(buy); + rate_buy = Some(nominal / buy); } } - rates.push(Rate { - from: Currency::rub(), - to: Currency::default(), - rate_type: RateType::NoCash, - buy: Some(nominal / buy), - sell: Some(nominal / sell), - }); + if rate_buy.is_some() || rate_sell.is_some() { + rates.push(Rate { + from: Currency::rub(), + to: Currency::default(), + rate_type: RateType::NoCash, + buy: rate_buy, + sell: rate_sell, + }); + } Ok(rates) } @@ -534,18 +557,28 @@ async fn collect_idpay(client: &Client) -> anyhow::Result> { let rates = result .currency_rate .iter() - .filter(|v| v.buy.is_some() && v.sell.is_some() && v.iso_txt == Currency::rub()) + .filter(|v| v.iso_txt == Currency::rub()) .map(|v| { - let buy = v.buy.unwrap(); - let sell = v.sell.unwrap(); + let mut rate_buy = None; + let mut rate_sell = None; + if let Some(buy) = v.buy { + if buy > dec!(0.0) { + rate_buy = Some(buy - (COMMISSION_RATE / dec!(100.0) * buy)); + } + }; + if let Some(sell) = v.sell { + if sell > dec!(0.0) { + rate_sell = Some( + sell + ((COMMISSION_RATE + RU_CARD_COMMISSION_RATE) / dec!(100.0) * sell), + ); + } + } Rate { from: v.iso_txt.clone(), to: Currency::default(), rate_type: RateType::NoCash, - buy: Some(buy - (COMMISSION_RATE / dec!(100.0) * buy)), - sell: Some( - sell + ((COMMISSION_RATE + RU_CARD_COMMISSION_RATE) / dec!(100.0) * sell), - ), + buy: rate_buy, + sell: rate_sell, } }) .collect(); @@ -559,24 +592,26 @@ async fn collect_mir(client: &Client) -> anyhow::Result> { .iter() .filter(|v| v.currency.strcode == Currency::default()) .map(|v| { - let buy = Some(dec!(1.0) / v.value_sell); - let sell = Some(dec!(1.0) / v.value_buy); - [ - Rate { - from: Currency::rub(), - to: Currency::default(), - rate_type: RateType::NoCash, - buy, - sell, - }, - Rate { - from: Currency::rub(), - to: Currency::default(), - rate_type: RateType::Cash, - buy, - sell, - }, - ] + let buy = if v.value_sell > dec!(0.0) { + Some(dec!(1.0) / v.value_sell) + } else { + None + }; + let sell = if v.value_buy > dec!(0.0) { + Some(dec!(1.0) / v.value_buy) + } else { + None + }; + let from = Currency::rub(); + let to = Currency::default(); + let new_rate = |rate_type: RateType| Rate { + from: from.clone(), + to: to.clone(), + rate_type, + buy, + sell, + }; + [new_rate(RateType::NoCash), new_rate(RateType::Cash)] }) .flatten() .collect(); @@ -585,24 +620,12 @@ async fn collect_mir(client: &Client) -> anyhow::Result> { async fn collect_sas(client: &Client) -> anyhow::Result> { let resp: sas::Response = sas::Response::get_rates(&client).await?; - let rates = resp - .rates - .iter() - .filter(|v| v.buy.is_some() && v.sell.is_some()) - .cloned() - .collect(); - Ok(rates) + Ok(resp.rates) } async fn collect_hsbc(client: &Client) -> anyhow::Result> { let resp: hsbc::Response = hsbc::Response::get_rates(&client).await?; - let rates = resp - .rates - .iter() - .filter(|v| v.buy.is_some() && v.sell.is_some()) - .cloned() - .collect(); - Ok(rates) + Ok(resp.rates) } async fn collect_avosend(client: &Client) -> anyhow::Result> { diff --git a/src/generator.rs b/src/generator.rs index 1d23842..1985a30 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -100,7 +100,7 @@ pub fn generate_from_to_table( rate_type: RateType, is_inv: bool, ) -> String { - if from == to { + if from == to || from.is_empty() || to.is_empty() { return DUNNO.into(); } @@ -138,15 +138,12 @@ pub fn generate_from_to_table( } } src_width = src_width.max(src.to_string().len()); - for (path, rate) in paths.iter().filter(|v| v.1 > dec!(0.0)) { - let rate_str = rate - .round_dp_with_strategy(RATE_DP, RoundingStrategy::MidpointAwayFromZero) - .normalize() - .to_string(); + for (path, rate) in paths { + let rate_str = decimal_to_string(rate, RATE_DP); rate_width = rate_width.max(rate_str.len()); table.push(Row { src: src.clone(), - rate: *rate, + rate, rate_str, diff: dec!(0.0), diff_str: "".into(), @@ -168,7 +165,6 @@ pub fn generate_from_to_table( .map(|r| r.rate) .next() .unwrap_or_default(); - let mut diff_width: usize = 0; let mut is_desc = false; let mut rate = dec!(0.0); for (idx, row) in table.iter().enumerate() { @@ -183,16 +179,13 @@ pub fn generate_from_to_table( break; } } + let mut diff_width: usize = 0; table.iter_mut().for_each(|row| { row.diff = ((best_rate - row.rate) / row.rate) * dec!(100.0); if is_desc && !row.diff.is_zero() { row.diff = -row.diff; } - row.diff_str = row - .diff - .round_dp_with_strategy(DIFF_DP, RoundingStrategy::MidpointAwayFromZero) - .normalize() - .to_string(); + row.diff_str = decimal_to_string(row.diff, DIFF_DP); diff_width = diff_width.max(row.diff_str.len()); }); let mut s = String::new(); @@ -219,6 +212,13 @@ pub fn generate_from_to_table( } } +fn decimal_to_string(value: Decimal, dp: u32) -> String { + value + .round_dp_with_strategy(dp, RoundingStrategy::MidpointAwayFromZero) + .normalize() + .to_string() +} + pub fn generate_src_table( src: Source, rates: &HashMap>, @@ -242,23 +242,22 @@ pub fn generate_src_table( let mut table = vec![]; let mut buy_width: usize = 0; let mut sell_width: usize = 0; - for rate in rates.iter().filter(|v| { - v.rate_type == rate_type && v.buy.is_some() && v.sell.is_some() && v.from != v.to - }) { - let buy = rate.buy.unwrap(); - let sell = rate.sell.unwrap(); - if buy.is_zero() || sell.is_zero() { - continue; - } + const NO_RATE: &str = "-"; + for rate in rates + .iter() + .filter(|v| v.rate_type == rate_type && v.from != v.to) + { + let buy_str = match rate.buy { + Some(buy) => decimal_to_string(buy, RATE_DP), + _ => NO_RATE.to_string(), + }; + let sell_str = match rate.sell { + Some(sell) => decimal_to_string(sell, RATE_DP), + _ => NO_RATE.to_string(), + }; let row = Row { - buy_str: buy - .round_dp_with_strategy(RATE_DP, RoundingStrategy::MidpointAwayFromZero) - .normalize() - .to_string(), - sell_str: sell - .round_dp_with_strategy(RATE_DP, RoundingStrategy::MidpointAwayFromZero) - .normalize() - .to_string(), + buy_str, + sell_str, from: rate.from.clone(), to: rate.to.clone(), }; diff --git a/src/sources/mod.rs b/src/sources/mod.rs index 1938434..ee5eeb5 100644 --- a/src/sources/mod.rs +++ b/src/sources/mod.rs @@ -136,7 +136,7 @@ impl Source { } #[derive(Debug, PartialEq, Clone, Eq, Hash, derive_more::Display)] -pub struct Currency(String); +pub struct Currency(pub String); impl Currency { pub const AMD: &'static str = "AMD"; @@ -159,6 +159,10 @@ impl Currency { pub fn rub() -> Self { Self(Self::RUB.into()) } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } } impl Default for Currency {