1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
use crate::Trade;
use std::fs::File;
use std::io::{BufRead, BufReader, Read, Write};
use tempfile::NamedTempFile;
const BITPANDA_CSV_COL_HEADER: &str = r#""Transaction ID",Timestamp,"Transaction Type",In/Out,"Amount Fiat",Fiat,"Amount Asset",Asset,"Asset market price","Asset market price currency","Asset class","Product ID",Fee,"Fee asset",Spread,"Spread Currency""#;
pub struct BitpandaTradeParser;
impl BitpandaTradeParser {
pub fn parse(reader: impl Read) -> csv::Result<Vec<Trade>> {
debug!("parsing CSV...");
let sanitized_csv = Self::sanitize_csv(reader)?;
debug!("parsing CSV data from {}", sanitized_csv.path().display());
let file = File::open(sanitized_csv.path())?;
debug!("tempfile opened");
let mut reader = csv::Reader::from_reader(file);
let mut trades: Vec<Trade> = Vec::new();
for trade in reader.deserialize::<Trade>() {
let trade = trade?;
debug!("found trade {:?}", trade);
trades.push(trade);
}
info!("found {} trades in CSV file", trades.len());
Ok(trades)
}
fn sanitize_csv(reader: impl Read) -> csv::Result<NamedTempFile> {
debug!("opening tempfile");
let work_file = NamedTempFile::new()?;
let mut writer = File::create(work_file.path())?;
debug!("tempfile opened at {}", work_file.path().display());
let mut keep = false;
for line in BufReader::new(reader).lines() {
let line = line?;
if line == BITPANDA_CSV_COL_HEADER {
debug!("found column header");
keep = true;
}
if keep {
writeln!(writer, "{}", line)?;
}
}
debug!("csv file sanitized");
Ok(work_file)
}
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
use std::path::Path;
#[test]
fn should_parse_csv_data() {
let reader = File::open(Path::new("./test/bitpanda.csv")).unwrap();
let trades = BitpandaTradeParser::parse(reader).unwrap();
assert_eq!(trades.len(), 12);
}
}