Skip to content

Commit 8b4b58e

Browse files
committed
Merge pull request #24 from sebinsua/feat/list-counterparties
Added `list counterparties` command
2 parents f3c120a + d580793 commit 8b4b58e

6 files changed

Lines changed: 125 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
All notable changes to this project will be [documented](http://keepachangelog.com/) in this file.
44
This project adheres to [Semantic Versioning](http://semver.org/).
55

6+
## [v0.0.4](https://github.com/sebinsua/teller-cli/releases/tag/v0.0.4) - 2015-12-25
7+
8+
- `list counterparties` gives access to a list of outgoing transactions grouped by their counterparty.
9+
- Outgoing values are now positive since the word outgoing already describes a negative flow of money.
10+
611
## [v0.0.3](https://github.com/sebinsua/teller-cli/releases/tag/v0.0.3) - 2015-12-22
712

813
- `list totals` became `list balances|outgoing|incoming`.

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "teller_cli"
3-
version = "0.0.3"
3+
version = "0.0.4"
44
authors = ["Seb Insua <me@sebinsua.com>"]
55

66
[[bin]]

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# teller-cli ![Build Status](https://img.shields.io/travis/sebinsua/teller-cli.svg)
22
> Banking for your command line
33
4-
The purpose of this command line tool is to provide a human-interface for your bank and not merely to be a one-to-one match with the underlying API.
4+
This tool provides useful ways of interrogating your bank through your command line, and is not merely meant to be a one-to-one match with underlying APIs.
55

66
It uses [Teller](http://teller.io) behind-the-scenes to interact with your UK bank, so you will need to have an account there.
77

@@ -15,7 +15,7 @@ It uses [Teller](http://teller.io) behind-the-scenes to interact with your UK ba
1515

1616
*e.g.*
1717

18-
![Instructions](http://i.imgur.com/cR3IMAN.png)
18+
![Instructions](http://i.imgur.com/cvZRwev.png)
1919

2020
## Why?
2121

@@ -87,7 +87,7 @@ fi
8787
### From release
8888

8989
```
90-
> curl -L https://github.com/sebinsua/teller-cli/releases/download/v0.0.3/teller > /usr/local/bin/teller && chmod +x /usr/local/bin/teller
90+
> curl -L https://github.com/sebinsua/teller-cli/releases/download/v0.0.4/teller > /usr/local/bin/teller && chmod +x /usr/local/bin/teller
9191
```
9292

9393
### From source

src/client/mod.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use chrono::{Date, DateTime, UTC, Datelike};
99
use chrono::duration::Duration;
1010
use itertools::Itertools;
1111

12+
use std::collections::HashMap;
13+
1214
use std::io::prelude::*; // Required for read_to_string use later.
1315
use std::str::FromStr;
1416

@@ -121,6 +123,21 @@ impl TransactionsWithCurrrency {
121123
}
122124
}
123125

126+
#[derive(Debug)]
127+
pub struct CounterpartiesWithCurrrency {
128+
pub counterparties: Vec<(String, String)>,
129+
pub currency: String,
130+
}
131+
132+
impl CounterpartiesWithCurrrency {
133+
pub fn new<S: Into<String>>(counterparties: Vec<(String, String)>, currency: S) -> CounterpartiesWithCurrrency {
134+
CounterpartiesWithCurrrency {
135+
counterparties: counterparties,
136+
currency: currency.into(),
137+
}
138+
}
139+
}
140+
124141
fn get_auth_header(auth_token: &str) -> Authorization<Bearer> {
125142
Authorization(
126143
Bearer {
@@ -279,6 +296,56 @@ pub fn get_transactions_with_currency(config: &Config, account_id: &str, timefra
279296
Ok(TransactionsWithCurrrency::new(transactions, currency))
280297
}
281298

299+
fn convert_to_counterparty_to_date_amount_list<'a>(transactions: &'a Vec<Transaction>) -> HashMap<String, Vec<(String, String)>> {
300+
let grouped_counterparties = transactions.iter().fold(HashMap::new(), |mut acc: HashMap<String, Vec<&'a Transaction>>, t: &'a Transaction| {
301+
let counterparty = t.counterparty.to_owned();
302+
if acc.contains_key(&counterparty) {
303+
if let Some(txs) = acc.get_mut(&counterparty) {
304+
txs.push(t);
305+
}
306+
} else {
307+
let mut txs: Vec<&'a Transaction> = vec![];
308+
txs.push(t);
309+
acc.insert(counterparty, txs);
310+
}
311+
312+
acc
313+
});
314+
315+
grouped_counterparties.into_iter().fold(HashMap::new(), |mut acc, (counterparty, txs)| {
316+
let date_amount_tuples = txs.into_iter().map(|tx| (tx.date.to_owned(), tx.amount.to_owned())).collect();
317+
acc.insert(counterparty.to_string(), date_amount_tuples);
318+
acc
319+
})
320+
}
321+
322+
pub fn get_counterparties(config: &Config, account_id: &str, timeframe: &Timeframe) -> ApiServiceResult<CounterpartiesWithCurrrency> {
323+
let transactions_with_currency = try!(get_transactions_with_currency(&config, &account_id, &timeframe));
324+
325+
let to_cent_integer = |amount: &str| {
326+
(f64::from_str(&amount).unwrap() * 100f64).round() as i64
327+
};
328+
let from_cent_integer_to_float_string = |amount: &i64| {
329+
format!("{:.2}", *amount as f64 / 100f64)
330+
};
331+
332+
let transactions: Vec<Transaction> = transactions_with_currency.transactions.into_iter().filter(|tx| to_cent_integer(&tx.amount) < 0).collect();
333+
let currency = transactions_with_currency.currency;
334+
335+
let counterparty_to_date_amount_list = convert_to_counterparty_to_date_amount_list(&transactions);
336+
let sorted_counterparties = counterparty_to_date_amount_list.into_iter().map(|(counterparty, date_amount_tuples)| {
337+
let amount = date_amount_tuples.iter().fold(0i64, |acc, dat| acc + to_cent_integer(&dat.1));
338+
(counterparty, amount.abs())
339+
}).sort_by(|&(_, amount_a), &(_, amount_b)| {
340+
amount_a.cmp(&amount_b)
341+
});
342+
let counterparties = sorted_counterparties.into_iter().map(|(counterparty, amount)| {
343+
(counterparty, from_cent_integer_to_float_string(&amount))
344+
}).collect();
345+
346+
Ok(CounterpartiesWithCurrrency::new(counterparties, currency))
347+
}
348+
282349
fn get_grouped_transaction_aggregates(config: &Config, account_id: &str, interval: &Interval, timeframe: &Timeframe, aggregate_txs: &Fn((String, Vec<Transaction>)) -> (String, i64)) -> ApiServiceResult<Vec<(String, i64)>> {
283350
let transactions: Vec<Transaction> = get_transactions(&config, &account_id, &timeframe).unwrap_or(vec![]);
284351

src/main.rs

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ mod config;
1313
mod client;
1414
mod inquirer;
1515

16-
use client::{Account, Transaction, Money, HistoricalAmountsWithCurrency, Balances, Outgoings, Incomings, get_accounts, get_account_balance, get_transactions_with_currency, get_balances, get_outgoings, get_incomings, get_outgoing, get_incoming};
16+
use client::{Account, Transaction, Money, HistoricalAmountsWithCurrency, Balances, Outgoings, Incomings, get_accounts, get_account_balance, get_transactions_with_currency, get_counterparties, get_balances, get_outgoings, get_incomings, get_outgoing, get_incoming};
1717
use client::{Interval, Timeframe};
1818

1919
use std::path::PathBuf;
@@ -35,6 +35,7 @@ Usage:
3535
teller init
3636
teller [list] accounts
3737
teller [list] transactions [<account> --timeframe=<tf> --show-description]
38+
teller [list] counterparties [<account> --timeframe=<tf> --count=<n>]
3839
teller [list] (balances|outgoings|incomings) [<account> --interval=<itv> --timeframe=<tf> --output=<of>]
3940
teller [show] balance [<account> --hide-currency]
4041
teller [show] outgoing [<account> --hide-currency]
@@ -45,6 +46,7 @@ Commands:
4546
init Configure.
4647
list accounts List accounts.
4748
list transactions List transactions.
49+
list counterparties List outgoing amounts grouped by counterparties.
4850
list balances List balances during a timeframe.
4951
list outgoings List outgoings during a timeframe.
5052
list incomings List incomings during a timeframe.
@@ -57,8 +59,9 @@ Commands:
5759
Options:
5860
-h --help Show this screen.
5961
-V --version Show version.
60-
-i --interval=<itv> Group by an interval of time (default: monthly).
61-
-t --timeframe=<tf> Operate upon a named period of time (default: 6-months).
62+
-i --interval=<itv> Group by an interval of time [default: monthly].
63+
-t --timeframe=<tf> Operate upon a named period of time [default: 6-months].
64+
-c --count=<n> Only the top N elements [default: 10].
6265
-d --show-description Show descriptions against transactions.
6366
-c --hide-currency Show money without currency codes.
6467
-o --output=<of> Output in a particular format (e.g. spark).
@@ -71,6 +74,7 @@ struct Args {
7174
cmd_show: bool,
7275
cmd_accounts: bool,
7376
cmd_transactions: bool,
77+
cmd_counterparties: bool,
7478
cmd_balances: bool,
7579
cmd_outgoings: bool,
7680
cmd_incomings: bool,
@@ -80,6 +84,7 @@ struct Args {
8084
arg_account: AccountType,
8185
flag_interval: Interval,
8286
flag_timeframe: Timeframe,
87+
flag_count: i64,
8388
flag_show_description: bool,
8489
flag_hide_currency: bool,
8590
flag_output: OutputFormat,
@@ -312,6 +317,15 @@ fn pick_command(arguments: Args) {
312317
Some(config) => list_transactions(&config, &arg_account, &flag_timeframe, &flag_show_description),
313318
}
314319
},
320+
Args { cmd_counterparties, ref arg_account, ref flag_timeframe, flag_count, .. } if cmd_counterparties == true => {
321+
match get_config() {
322+
None => {
323+
error!("Configuration could not be found or created so command not executed");
324+
exit(1)
325+
}
326+
Some(config) => list_counterparties(&config, &arg_account, &flag_timeframe, &flag_count),
327+
}
328+
},
315329
Args { cmd_balances, ref arg_account, ref flag_interval, ref flag_timeframe, ref flag_output, .. } if cmd_balances == true => {
316330
match get_config() {
317331
None => {
@@ -471,6 +485,37 @@ fn list_transactions(config: &Config, account: &AccountType, timeframe: &Timefra
471485
}
472486
}
473487

488+
fn represent_list_counterparties(counterparties: &Vec<(String, String)>, currency: &str, count: &i64) {
489+
let mut counterparties_table = String::new();
490+
491+
counterparties_table.push_str(&format!("row\tcounterparty\tamount ({})\n", currency));
492+
let skip_n = counterparties.len() - (*count as usize);
493+
for (idx, counterparty) in counterparties.iter().skip(skip_n).enumerate() {
494+
let row_number = (idx + 1) as u32;
495+
let new_counterparty_row = format!("{}\t{}\t{}\n", row_number, counterparty.0, counterparty.1);
496+
counterparties_table = counterparties_table + &new_counterparty_row;
497+
}
498+
499+
let mut tw = TabWriter::new(Vec::new());
500+
write!(&mut tw, "{}", counterparties_table).unwrap();
501+
tw.flush().unwrap();
502+
503+
let counterparties_str = String::from_utf8(tw.unwrap()).unwrap();
504+
505+
println!("{}", counterparties_str)
506+
}
507+
508+
fn list_counterparties(config: &Config, account: &AccountType, timeframe: &Timeframe, count: &i64) {
509+
let account_id = get_account_id(&config, &account);
510+
match get_counterparties(&config, &account_id, &timeframe) {
511+
Ok(counterparties_with_currency) => represent_list_counterparties(&counterparties_with_currency.counterparties, &counterparties_with_currency.currency, &count),
512+
Err(e) => {
513+
error!("Unable to list counterparties: {}", e);
514+
exit(1)
515+
},
516+
}
517+
}
518+
474519
fn represent_list_amounts(amount_type: &str, hac: &HistoricalAmountsWithCurrency, output: &OutputFormat) {
475520
match *output {
476521
OutputFormat::Spark => {

0 commit comments

Comments
 (0)