Navigation Menu

Skip to content
This repository has been archived by the owner on Oct 25, 2023. It is now read-only.

Commit

Permalink
Merge pull request #21 from cljoly/dev
Browse files Browse the repository at this point in the history
New price extraction engine, expired rates are removed
  • Loading branch information
cljoly committed Oct 2, 2019
2 parents aa4e834 + 5219786 commit 99ed337
Show file tree
Hide file tree
Showing 13 changed files with 1,125 additions and 312 deletions.
80 changes: 47 additions & 33 deletions Cargo.lock

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions Cargo.toml
Expand Up @@ -6,8 +6,8 @@ repository = "https://github.com/cljoly/sesters"
readme = "Readme.md"
keywords = ["currency_converter", "cli"]
categories = ["command-line-utilities", "text-processing"]
version = "0.1.3"
authors = ["Leo <oss+sesters@131719.xyz>"]
version = "0.2.0"
authors = ["Clément Joly <oss+sesters@131719.xyz>"]
edition = "2018"
license = "GPL-3.0-or-later"

Expand All @@ -22,7 +22,7 @@ lazy_static = "1.2"
serde = "1"
serde_derive = "1"
serde_json = "1"
env_logger = "0.6.*"
env_logger = "0.7.*"
log = { version = "0.4", features = ["max_level_trace", "release_max_level_info"] }
confy = "0.3.*"
kv = { version = "0.9.*", features = ["bincode-value"] }
Expand All @@ -32,3 +32,7 @@ dirs = "2.0"
encoding = "0.2"
clap = "2"
itertools = "0.8"
test-case = "0.3"

[profile.release]
debug = true
8 changes: 5 additions & 3 deletions Readme.md
Expand Up @@ -42,14 +42,16 @@ EUR 2345.00 ➜ USD 2586.53

🏗️ This is a work in progress, only checked features are implemented yet.

- [X] Find prices in free text with currency (partial)
- More to come ![GitHub issues by-label](https://img.shields.io/github/issues/cljoly/sesters/text-extraction.svg)
- [X] Find prices in free text with several currencies
- [X] Store exchange rates locally
- [X] Retrieve exchange rate (partial)
- [ ] More to come ![GitHub issues by-label](https://img.shields.io/github/issues/cljoly/sesters/rate-source.svg)
- [X] Cache retrieved rate
- [ ] More sources to be added ![GitHub issues by-label](https://img.shields.io/github/issues/cljoly/sesters/rate-source.svg)
- [ ] Save recent searches
- [ ] Display this history in a table

### Maybe

- [ ] GUI with [azul.rs](https://azul.rs/)

## About the name
Expand Down
6 changes: 3 additions & 3 deletions src/api.rs
Expand Up @@ -19,7 +19,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
//! Access several API used by Sesters

use chrono::Duration;
use log::{debug, error, info, trace};
use log::{debug, error, trace};
use reqwest;
use std::error::Error;

Expand Down Expand Up @@ -60,7 +60,7 @@ pub trait RateApi {
fn rate<'c>(&self, client: &Client, src: &'c Currency, dst: &'c Currency) -> Option<Rate<'c>> {
let rate_err = || -> Result<Rate, Box<dyn Error>> {
debug!("Performing conversion request for {} -> {}", src, dst);
let mut res = self.rate_query(client, src, dst).send()?;
let res = self.rate_query(client, src, dst).send()?;
debug!("Conversion request for {} -> {} done", src, dst);
trace!("Conversion request result: {:?}", &res);
self.treat_result(res, src, dst)
Expand Down Expand Up @@ -148,7 +148,7 @@ impl RateApi for ExchangeRatesApiIo {
&self,
client: &Client,
src: &'c Currency,
dst: &'c Currency,
_dst: &'c Currency,
) -> RequestBuilder {
client
.get("https://api.exchangeratesapi.io/latest")
Expand Down
41 changes: 41 additions & 0 deletions src/clap.rs
@@ -0,0 +1,41 @@
/*
Sesters: easily convert one currency to another
Copyright (C) 2018-2019 Clément Joly <oss+sesters@131719.xyz>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

//! Define clap subcommand

use clap::{clap_app,crate_version,crate_authors,crate_description, App};

pub fn get_app() -> App<'static, 'static> {
clap_app!(sesters =>
// (@setting DontCollapseArgsInUsage)
(version: crate_version!())
(author: crate_authors!())
(about: concat!(crate_description!(), "\n", "https://seste.rs"))
// TODO Implement -c
// (@arg CONFIG: -c --config +global +takes_value "Sets a custom config file")
// TODO Add flag for verbosity, for preferred currency
(@arg TO: -t --to +takes_value +global +multiple "Currency to convert to, uses defaults from the configuration file if not set")
(@subcommand convert =>
(@setting TrailingVarArg)
// (@setting DontDelimitTrailingValues)
(about: "Perform currency conversion to your preferred currency, from a price tag found in plain text")
(visible_alias: "c")
(@arg PLAIN_TXT: +multiple !use_delimiter "Plain text to extract a price tag from. If not set, plain text will be read from stdin")
)
)
}
153 changes: 153 additions & 0 deletions src/convert.rs
@@ -0,0 +1,153 @@
/*
Sesters: easily convert one currency to another
Copyright (C) 2018-2019 Clément Joly <oss+sesters@131719.xyz>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

//! Module for the convert subcommand

use log::{trace, debug, info, log_enabled};
use clap::ArgMatches;
use clap::Values as ClapValues;
use std::io::{self, BufRead};
use itertools::Itertools;

use crate::api;
use crate::api::RateApi;
use crate::rate::Rate;
use crate::MainContext;

/// Concat the args with spaces, if args are not `None`. Read text from stdin
/// otherwise.
fn concat_or_stdin(arg_text: Option<ClapValues>) -> String {
fn read_stdin() -> String {
info!("Reading stdin…");
eprintln!("Enter the plain text on the first line");
let stdin = io::stdin();
let txt = stdin
.lock()
.lines()
.next()
.expect("Please provide some text on stdin")
.unwrap();
trace!("txt: {}", txt);
txt
}
fn space_join(values: ClapValues) -> String {
let mut txt = String::new();
let spaced_values = values.intersperse(" ");
for s in spaced_values {
txt.push_str(s);
}
txt
}
arg_text.map_or_else(read_stdin, space_join)
}

/// Parse arguments for convert subcommand and run it
pub(crate) fn run(ctxt: MainContext, matches: &ArgMatches) {
let txt = concat_or_stdin(matches.values_of("PLAIN_TXT"));
trace!("plain text: {}", &txt);
let engine: crate::price_in_text::Engine = crate::price_in_text::Engine::new().unwrap();
let price_tags = engine.all_price_tags(&txt);
if let Some(price_tag) = price_tags.get(0) {
let src_currency = price_tag.currency();
trace!("src_currency: {}", &src_currency);

// Get rate
trace!("Get db handler");
let sh = ctxt.db.store_handle().write().unwrap();
trace!("Get rate bucket");
let bucket = ctxt.db.bucket_rate(&sh);
trace!("Got bucket");
let endpoint = api::ExchangeRatesApiIo::new(&ctxt.cfg);
trace!("Got API Endpoint");
{
let rate_from_db = |dst_currency| -> Option<Rate> {
debug!("Create read transaction");
let txn = sh.read_txn().unwrap();
trace!("Get rate from db");
let (uptodate_rates, outdated_rates) = ctxt.db.get_rates(
&txn,
&sh,
src_currency,
dst_currency,
&endpoint.provider_id(),
);

// Remove outdated_rates
let mut txnw = sh.write_txn().unwrap();
for rate in outdated_rates {
ctxt.db.del_rate(&mut txnw, &bucket, rate);
}

let rate = uptodate_rates.last();
trace!("rate_from_db: {:?}", rate);
rate.map(|r| r.clone())
};

let add_to_db = |rate: Rate| {
debug!("Get write transaction");
let mut txn = sh.write_txn().unwrap();
trace!("Set rate to db");
let r = ctxt.db.set_rate(&mut txn, &bucket, rate);
trace!("Rate set, result: {:?}", &r);
txn.commit().unwrap();
};

let rate_from_api = |dst_currency| -> Option<Rate> {
info!("Retrieve rate online");
let client = reqwest::Client::new();
endpoint.rate(&client, &src_currency, dst_currency)
};

let rates = ctxt.destination_currencies.iter().map(|dst| {
rate_from_db(&dst).or_else(|| {
let rate = rate_from_api(&dst);
if let Some(rate) = &rate {
info!("Set rate to db");
add_to_db(rate.clone());
}
rate
})
});

for rate in rates {
if log_enabled!(log::Level::Info) {
if let Some(rate) = &rate {
info!("Rate retrieved: {}", &rate);
} else {
info!("No rate retrieved");
}
}
trace!("Final rate: {:?}", &rate);
if let Some(rate) = rate {
// Skip conversion that wouldn’t change currency (like 1 BTC -> 1 BTC)
if price_tag.currency() == rate.dst() {
continue;
}
println!(
"{} ➜ {}",
&price_tag,
&price_tag.convert(&rate).unwrap()
);
}
}
}
} else {
println!("No currency found.")
}
}

23 changes: 14 additions & 9 deletions src/currency.rs
Expand Up @@ -45,7 +45,7 @@ mod tests {
}

/// Position of a symbol against an amount
#[derive(Debug, PartialOrd, PartialEq, Serialize, Deserialize)]
#[derive(Debug, PartialOrd, PartialEq, Serialize, Deserialize, Copy, Clone)]
pub enum Pos {
Before,
After,
Expand All @@ -59,14 +59,14 @@ impl Default for Pos {

/// An association between currency & amount, TODO with a position
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct CurrencyAmount<'c> {
pub struct PriceTag<'c> {
currency: &'c Currency,
amount: f64,
// TODO /// Position of the currency indicator against amount
// position: Pos,
}

impl<'c> CurrencyAmount<'c> {
impl<'c> PriceTag<'c> {
/// Create new amount associated to a currency
pub fn new(currency: &'c Currency, amount: f64) -> Self {
Self { currency, amount }
Expand All @@ -83,16 +83,16 @@ impl<'c> CurrencyAmount<'c> {
pub fn convert<'a, 'r>(
&'a self,
rate: &'r Rate<'c>,
) -> Result<CurrencyAmount<'r>, ConversionError<'a, 'c, 'r>> {
) -> Result<PriceTag<'r>, ConversionError<'a, 'c, 'r>> {
if self.currency != rate.src() {
Err(ConversionError::new(rate, &self))
} else {
Ok(CurrencyAmount::new(rate.dst(), rate.rate() * self.amount))
Ok(PriceTag::new(rate.dst(), rate.rate() * self.amount))
}
}
}

impl<'c> fmt::Display for CurrencyAmount<'c> {
impl<'c> fmt::Display for PriceTag<'c> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO Use symbol, proper separator (, or .), proper number of cents (usually 2 or 3)
write!(f, "{} {:.*}", self.currency.get_main_iso(), 2, self.amount)
Expand All @@ -103,19 +103,19 @@ impl<'c> fmt::Display for CurrencyAmount<'c> {
#[derive(Debug, Clone)]
pub struct ConversionError<'a, 'c, 'r> {
rate: &'r Rate<'c>,
amount: &'a CurrencyAmount<'c>,
amount: &'a PriceTag<'c>,
}

impl<'a, 'c, 'r> ConversionError<'a, 'c, 'r> {
/// New conversion error
pub fn new(rate: &'r Rate<'c>, amount: &'a CurrencyAmount<'c>) -> Self {
pub fn new(rate: &'r Rate<'c>, amount: &'a PriceTag<'c>) -> Self {
ConversionError { rate, amount }
}
}

/// Represent a currency like US Dollar or Euro, with its symbols
// TODO Improve serialization/deserialization
#[derive(Debug, Default, PartialOrd, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Default, PartialOrd, PartialEq, Serialize, Deserialize, Clone)]
pub struct Currency {
/// Symbols, like ₿, ฿ or Ƀ for Bitcoin. Slice must not be empty
#[serde(skip)]
Expand Down Expand Up @@ -148,6 +148,10 @@ impl Currency {
&self.symbols
}

pub fn pos(&self) -> Pos {
self.pos
}

/// Constructor, copies the &str given. Panics if vectors are empty TODO Use Result type instead
pub fn new(
symbols: &'static [&'static str],
Expand Down Expand Up @@ -249,3 +253,4 @@ pub fn existing_from_iso(code: &str) -> Option<&'static Currency> {
_ => None,
}
}

9 changes: 2 additions & 7 deletions src/db.rs
Expand Up @@ -18,8 +18,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.

//! Module grouping all db related concern

use kv::{Config as KvConfig, Manager, Store, Txn};
use log::{debug, info, trace};
use kv::{Config as KvConfig, Manager, Store};
use log::trace;

mod rate;

Expand All @@ -34,11 +34,6 @@ pub struct Db {
rbr: RateBucketRegistered,
}

/// All supported bucket
enum BucketList {
RateBucket,
}

impl Db {
/// Initialize the rate database
pub fn new(mut kcfg: KvConfig, mgr: &mut Manager) -> Self {
Expand Down

0 comments on commit 99ed337

Please sign in to comment.