commit 2eeffb057d71c97173b9d66f4e589cadf206790a Author: scoobybejesus Date: Sun Aug 25 19:57:07 2019 -0400 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..59b349c --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/target +**/*.rs.bk +/private +.DS_Store +.vscode/* +rls* diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..7d18cfd --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +scoobybejesus diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1943cd5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,730 @@ +[[package]] +name = "aho-corasick" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "arrayref" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "arrayvec" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "atty" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "autocfg" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "backtrace" +version = "0.3.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "backtrace-sys 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "backtrace-sys" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "base64" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "bitflags" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "blake2b_simd" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "arrayref 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "arrayvec 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "constant_time_eq 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "bstr" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-automata 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "byteorder" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cc" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cfg-if" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "chrono" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "chrono-tz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", + "parse-zoneinfo 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "clap" +version = "2.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "crossbeam-utils" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "cryptools-rs" +version = "0.6.0" +dependencies = [ + "chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono-tz 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "csv 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "decimal 2.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "rustyline 5.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)", + "structopt 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "csv" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bstr 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", + "csv-core 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "ryu 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "csv-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "decimal" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cc 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "ord_subset 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "dirs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", + "dirs-sys 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "dirs-sys" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_users 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "failure" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "backtrace 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", + "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "failure_derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)", + "synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-segmentation 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "itoa" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "lazy_static" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "log" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "memchr" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "nix" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cc 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "nodrop" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "num-integer" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ord_subset" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "parse-zoneinfo" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "regex 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "proc-macro2" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "proc-macro2" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quote" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quote" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "redox_syscall" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "redox_users" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", + "rust-argon2 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-automata" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "rust-argon2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "blake2b_simd 0.5.7 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "rustc-serialize" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "rustyline" +version = "5.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "nix 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-segmentation 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "utf8parse 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ryu" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde_derive 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_derive" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "structopt" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", + "structopt-derive 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "structopt-derive" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "syn" +version = "0.15.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "syn" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "synstructure" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thread_local" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "time" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-segmentation" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-width" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-xid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "utf8parse" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "vec_map" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)" = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d" +"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +"checksum arrayref 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "0d382e583f07208808f6b1249e60848879ba3543f57c32277bf52d69c2f0f0ee" +"checksum arrayvec 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "b8d73f9beda665eaa98ab9e4f7442bd4e7de6652587de55b2525e52e29c1b0ba" +"checksum atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90" +"checksum autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "b671c8fb71b457dd4ae18c4ba1e59aa81793daacc361d82fcd410cef0d491875" +"checksum backtrace 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)" = "1371048253fa3bac6704bfd6bbfc922ee9bdcee8881330d40f308b81cc5adc55" +"checksum backtrace-sys 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)" = "82a830b4ef2d1124a711c71d263c5abdc710ef8e907bd508c88be475cebc422b" +"checksum base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" +"checksum bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d155346769a6855b86399e9bc3814ab343cd3d62c7e985113d46a0ec3c281fd" +"checksum blake2b_simd 0.5.7 (registry+https://github.com/rust-lang/crates.io-index)" = "bf775a81bb2d464e20ff170ac20316c7b08a43d11dbc72f0f82e8e8d3d6d0499" +"checksum bstr 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "94cdf78eb7e94c566c1f5dbe2abf8fc70a548fc902942a48c4b3a98b48ca9ade" +"checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" +"checksum cc 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)" = "b548a4ee81fccb95919d4e22cfea83c7693ebfd78f0495493178db20b3139da7" +"checksum cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33" +"checksum chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "77d81f58b7301084de3b958691458a53c3f7e0b1d702f77e550b6a88e3a88abe" +"checksum chrono-tz 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e0e430fad0384e4defc3dc6b1223d1b886087a8bf9b7080e5ae027f73851ea15" +"checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" +"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +"checksum constant_time_eq 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "995a44c877f9212528ccc74b21a232f66ad69001e40ede5bcee2ac9ef2657120" +"checksum crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6" +"checksum csv 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "37519ccdfd73a75821cac9319d4fce15a81b9fcf75f951df5b9988aa3a0af87d" +"checksum csv-core 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9b5cadb6b25c77aeff80ba701712494213f4a8418fcda2ee11b6560c3ad0bf4c" +"checksum decimal 2.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "e6458723bc760383275fbc02f4c769b2e5f3de782abaf5e7e0b9b7f0368a63ed" +"checksum dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" +"checksum dirs-sys 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "afa0b23de8fd801745c471deffa6e12d248f962c9fd4b4c33787b055599bde7b" +"checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2" +"checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1" +"checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +"checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" +"checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14" +"checksum libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)" = "34fcd2c08d2f832f376f4173a231990fa5aef4e99fb569867318a227ef4c06ba" +"checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +"checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e" +"checksum nix 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6c722bee1037d430d0f8e687bbdbf222f27cc6e4e68d5caf630857bb2b6dbdce" +"checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945" +"checksum num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09" +"checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32" +"checksum ord_subset 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d7ce14664caf5b27f5656ff727defd68ae1eb75ef3c4d95259361df1eb376bef" +"checksum parse-zoneinfo 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "089a398ccdcdd77b8c38909d5a1e4b67da1bc4c9dbfe6d5b536c828eddb779e5" +"checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +"checksum proc-macro2 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4c5c2380ae88876faae57698be9e9775e3544decad214599c3a6266cca6ac802" +"checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" +"checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +"checksum rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" +"checksum rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +"checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +"checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" +"checksum redox_users 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4ecedbca3bf205f8d8f5c2b44d83cd0690e39ee84b951ed649e9f1841132b66d" +"checksum regex 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88c3d9193984285d544df4a30c23a4e62ead42edf70a4452ceb76dac1ce05c26" +"checksum regex-automata 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "92b73c2a1770c255c240eaa4ee600df1704a38dc3feaa6e949e7fcd4f8dc09f9" +"checksum regex-syntax 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)" = "b143cceb2ca5e56d5671988ef8b15615733e7ee16cd348e064333b251b89343f" +"checksum rust-argon2 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4ca4eaef519b494d1f2848fc602d18816fed808a981aedf4f1f00ceb7c9d32cf" +"checksum rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" +"checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" +"checksum rustyline 5.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f8ee0838a6594169a1c5f4bb9af0fe692cc99691941710a8cc6576395ede804e" +"checksum ryu 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c92464b447c0ee8c4fb3824ecc8383b81717b9f1e74ba2e72540aef7b9f82997" +"checksum serde 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)" = "fec2851eb56d010dc9a21b89ca53ee75e6528bab60c11e89d38390904982da9f" +"checksum serde_derive 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)" = "cb4dc18c61206b08dc98216c98faa0232f4337e1e1b8574551d5bad29ea1b425" +"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +"checksum structopt 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "16c2cdbf9cc375f15d1b4141bc48aeef444806655cd0e904207edc8d68d86ed7" +"checksum structopt-derive 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "53010261a84b37689f9ed7d395165029f9cc7abb9f56bbfe86bee2597ed25107" +"checksum syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)" = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" +"checksum syn 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c65d951ab12d976b61a41cf9ed4531fc19735c6e6d84a4bb1453711e762ec731" +"checksum synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "02353edf96d6e4dc81aea2d8490a7e9db177bf8acb0e951c24940bf866cb313f" +"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" +"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" +"checksum unicode-segmentation 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1967f4cdfc355b37fd76d2a954fb2ed3871034eb4f26d60537d88795cfc332a9" +"checksum unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7007dbd421b92cc6e28410fe7362e2e0a2503394908f417b68ec8d1c364c4e20" +"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" +"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" +"checksum utf8parse 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8772a4ccbb4e89959023bc5b7cb8623a795caa7092d99f3aa9501b9484d4557d" +"checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" +"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +"checksum winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f10e386af2b13e47c89e7236a7a14a086791a2b88ebad6df9bf42040195cf770" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6ed36d4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "cryptools-rs" +version = "0.6.0" +authors = ["scoobybejesus "] +edition = "2018" +description = "This is a command-line utility for processing cryptocurrency transactions into 'lots' and 'movements'." + +[dependencies] +csv = "1.0.0" +serde = { version = "1.0.75", features = ["derive"] } +serde_derive = "1.0.75" +decimal = "2.0.4" +chrono = { version = "0.4", features = ["serde"] } +chrono-tz = "0.5" +time = "0.1.42" +structopt = "0.2.10" +rustyline = "5.0.0" + +[profile.release] +lto = true + diff --git a/LEGAL.txt b/LEGAL.txt new file mode 100644 index 0000000..5b6edd5 --- /dev/null +++ b/LEGAL.txt @@ -0,0 +1,31 @@ +// Copyright (c) 2017-2019, scoobybejesus +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions, and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// 4. Individual files may or may not contain the entire contents of this file. If a an +// individual file is copied/redistributed/so on, this license must be pasted in place +// of the license notice on that page. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..74be297 --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# cryptools-rs + +### Accounting library for cryptocurrency transaction activity. + +It provides a way to measure cryptocurrency activity in one's home currency (the default value is USD, but anything can be used). +Reports may be exported as CSV files that reflect income/expense/gains/losses. + +The activity that gets imported **must** be in a prescribed form that effectively looks like this: + + +|txDate |proceeds|memo |1 |2 |3 |4 |5 | +|-------|--------|--------|------|--------|--------|--------|------------| +| | | |Bank |Exchange|Exchange|Exchange|Simplewallet| +| | | |USD |BTC |BTC |XMR |XMR | +| | | |non |non |non |non |non | +|2/1/16 |0 |FIRST |-220 |0.25 | | | | +|3/1/16 |250 |SECOND | |-0.25 | |180 | | +|4/1/16 |0 |THIRD | | | |-90 |90 | +|5/1/16 |0 |FOURTH | | | |90 |-90 | +|5/2/16 |160 |FIFTH | |0.3 | |-90 | | +|6/1/16 |0 |SIXTH | |-0.3 |0.3 | | | +|7/1/16 |200 |SEVENTH | | |0.7 |-90 | | +|8/1/16 |0 |EIGHTH | |0.5 |-0.5 | | | +|9/1/16 |400 |NINTH | | |-0.5 |200 | | +|10/1/16|900 |TENTH | |1 | |-200 | | +|11/1/16|0 |ELEVENTH| |-1.5 |1.5 | | | +|12/1/16|2000 |TWELFTH | | |-1.5 |400 | | + + +#### CSV file components + +* **txDate** is currently set to parse dates of the format MM/dd/YY. + +* **proceeds** may seem tricky. +The way to understand it, since it can apply to any transaction (aside from transfers from one owned-account to another owned-account), is that this is the value transferred in the transaction. +For example, if one spends 0.01 BTC for an item at a time when BTC/USD is $10,000/BTC, then the user received value of $100, therefore the proceeds of that transaction would be $100. +This field is ignored when the user's home currency is used to purchase cryptocurrency. + +* **memo** is useful for evaluating the final output but isn't important. +Currently, commas in the memo are **not** supported. + +After three column of transaction metadata, the *Account* columns follow. + +* *Accounts* (**1**, **2**, **3**, **4**, **5**, ...): the top row reflects the account number (which currently must start at 1 and increase sequentially). +The three other values are the *name*, *ticker*, and *margin_bool*. +*name* and *ticker* should be self-explanatory. +*margin_bool* is set usually set as 'no', 'non' (i.e., non-margin), or 'false.' +To indicate a margin account, set it as 'yes', 'margin' or 'true'. + +###### Margin accounts + +* Margin accounts must come in pairs, the base account and the quote account. +The base account is the coin being longed or shorted, and its ticker is reflected like normal. +The quote account is the market. +The quote account's ticker requires a different formatting. +For example, when using BTC to long XMR, the BTC account must be reflected with the ticker BTC_xmr. + +#### Constraints + +* *All* cryptocurrency-related activity for the user generally must be included in the input CSV file. + +* There can only be either one or two accounts used in a given transaction (i.e., if a Counterparty token or Ethereum token transaction must be recorded, the XCP or ETH transaction fee must be reflected in a separate transaction/row). + +* Currently, manual adjustments may need to be made to the output files in cases, for example, when the user used cryptocurrency to make atax-deductible charitable contribution. + +## Installation + +1. `cargo build` (or include `--release` for a non-debug build) + +This will build `./cryptools-rs`. + +## Usage + +Run `./cryptools-rs` with no arguments (or `--help`, or `-h`) to see usage. + +## Contributing + +* Contributors welcome. New authors should add themselves to the `AUTHORS` file. + +* Roadmap and todos: we're working through items in [Issues](https://github.com/scoobybejesus/cryptools-rs/issues); feel free to tackle or add issues. + +## Legal + +See LEGAL.txt diff --git a/src/account.rs b/src/account.rs new file mode 100644 index 0000000..a2d0e84 --- /dev/null +++ b/src/account.rs @@ -0,0 +1,249 @@ +// Copyright (c) 2017-2019, scoobybejesus +// Redistributions must include the license: https://github.com/scoobybejesus/cryptools-rs/LEGAL.txt + +use std::rc::{Rc, Weak}; +use std::cell::{Cell, RefCell}; +use std::fmt; +use std::collections::{HashMap}; + +use chrono::{NaiveDate, NaiveTime, NaiveDateTime, DateTime, Utc, TimeZone}; +use chrono_tz::US::Eastern; +use decimal::d128; +use serde_derive::{Serialize, Deserialize}; + +use crate::transaction::{Transaction, ActionRecord, Polarity, TxType}; + +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct RawAccount { + pub account_num: u16, + pub name: String, + pub ticker: String, + pub is_margin: bool, +} + +impl RawAccount { + pub fn is_home_currency(&self, compare: &String) -> bool { + &self.ticker == compare + } +} + +#[derive(Clone, Debug)] +pub struct Account { + pub raw_key: u16, + pub list_of_lots: RefCell>>, + // pub vec_of_lot_keys: (RawAccount, u32), +} + +impl Account { + + pub fn is_home_currency(&self, compare: &String, raw_acct_map: &HashMap) -> bool { + let raw_acct = raw_acct_map.get(&self.raw_key).unwrap(); + &raw_acct.ticker == compare + } + + pub fn get_sum_of_amts_in_lots(&self) -> d128 { + let lots = self.list_of_lots.borrow(); + let mut total_amount = d128!(0); + for lot in lots.iter() { + let sum = lot.get_sum_of_amts_in_lot(); + total_amount += sum; + } + total_amount + } + + pub fn get_sum_of_basis_in_lots(&self) -> d128 { + let lots = self.list_of_lots.borrow(); + let mut total_amount = d128!(0); + for lot in lots.iter() { + let sum = lot.get_sum_of_basis_in_lot(); + total_amount += sum; + } + total_amount + } +} + +#[derive(Clone, Debug)] +pub struct RawMarginPair (pub Weak, pub Weak); // always (base_acct, quote_acct) + +#[derive(Clone, Debug)] +pub struct Lot { + pub date_as_string: String, + pub date_of_first_mvmt_in_lot: NaiveDate, + pub date_for_basis_purposes: NaiveDate, + pub lot_number: u32, // Does NOT start at zero. First lot is lot 1. + pub account_key: u16, + pub movements: RefCell>>, +} + +impl Lot { + pub fn get_sum_of_amts_in_lot(&self) -> d128 { + let mut amts = d128!(0); + self.movements.borrow().iter().for_each(|movement| amts += movement.amount); + amts + } + pub fn sum_of_amts_in_lot_is_zero(&self) -> bool { + d128!(0) == Self::get_sum_of_amts_in_lot(&self) + } + pub fn get_sum_of_basis_in_lot(&self) -> d128 { + let mut amts = d128!(0); + self.movements.borrow().iter().for_each(|movement| amts += movement.cost_basis.get()); + amts + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Movement { + pub amount: d128, + pub date_as_string: String, + pub date: NaiveDate, + pub transaction_key: u32, + pub action_record_key: u32, + pub cost_basis: Cell, // Initialized with 0. Set in add_cost_basis_to_movements() + pub ratio_of_amt_to_incoming_mvmts_in_a_r: d128, // Set in process_multiple_incoming_lots_and_mvmts() and incoming flow dual actionrecord transactions + pub ratio_of_amt_to_outgoing_mvmts_in_a_r: Cell, // Set in wrap_mvmt_and_push() + pub lot_num: u32, + pub proceeds: Cell, // Initialized with 0. Set in add_proceeds_to_movements() +} + +impl Movement { + + pub fn get_lot( + &self, + acct_map: &HashMap, + ar_map: &HashMap + ) -> Rc { + let ar = ar_map.get(&self.action_record_key).unwrap(); + let acct = acct_map.get(&ar.account_key).unwrap(); + let lot = acct.list_of_lots.borrow()[self.lot_num as usize - 1].clone(); // lots start at 1 and indexes at 0 + lot + } + + pub fn ratio_of_amt_to_lots_first_mvmt( + &self, + acct_map: &HashMap, + ar_map: &HashMap + ) -> d128 { + // println!("Lot #: {}", self.lot.lot_number); + let ar = ar_map.get(&self.action_record_key).unwrap(); + let acct = acct_map.get(&ar.account_key).unwrap(); + let lot = &acct.list_of_lots.borrow()[self.lot_num as usize - 1]; // lots start at 1 and indexes at 0 + let borrowed_mvmt_list = lot.movements.borrow(); + let ratio = self.amount / borrowed_mvmt_list.first().unwrap().amount; + // println!("ratio_of_amt_to_lots_first_mvmt: {}", ratio.abs()); + ratio.abs() + } + + pub fn get_gain_or_loss(&self) -> d128 { + self.proceeds.get() + self.cost_basis.get() + } + + pub fn get_term(&self, acct_map: &HashMap, ar_map: &HashMap,) -> Term { + + use time::Duration; + let ar = ar_map.get(&self.action_record_key).unwrap(); + let lot = Self::get_lot(&self, acct_map, ar_map); + + match ar.direction() { + Polarity::Incoming => { + let today = Utc::now(); + let utc_lot_date = Self::create_date_time_from_atlantic(lot.date_for_basis_purposes, NaiveTime::from_hms_milli(12, 34, 56, 789)); + // if today.signed_duration_since(self.lot.date_for_basis_purposes) > 365 + if (today - utc_lot_date) > Duration::days(365) { // TODO: figure out how to instantiate today's date and convert it to compare to NaiveDate + Term::LT + } + else { + Term::ST + } + } + Polarity::Outgoing => { + let lot_date_for_basis_purposes = lot.date_for_basis_purposes; + if self.date.signed_duration_since(lot_date_for_basis_purposes) > Duration::days(365) { + return Term::LT + } + Term::ST + } + } + } + + pub fn create_date_time_from_atlantic(date: NaiveDate, time: NaiveTime) -> DateTime { + let naive_datetime = NaiveDateTime::new(date, time); + let east_time = Eastern.from_local_datetime(&naive_datetime).unwrap(); + east_time.with_timezone(&Utc) + } + + pub fn get_income( + &self, + ar_map: &HashMap, + raw_accts: &HashMap, + acct_map: &HashMap, + txns_map: &HashMap, + )-> d128 { + + let txn = txns_map.get(&self.transaction_key).expect("Couldn't get txn. Tx num invalid?"); + match txn.transaction_type(ar_map, raw_accts, acct_map) { + TxType::Flow => { + let ar = ar_map.get(&self.action_record_key).unwrap(); + if ar.direction() == Polarity::Incoming { + self.proceeds.get() + } + else { d128!(0) } + } + TxType::Exchange => { d128!(0) } + TxType::ToSelf => { d128!(0) } + } + } + + pub fn get_expense( + &self, + ar_map: &HashMap, + raw_accts: &HashMap, + acct_map: &HashMap, + txns_map: &HashMap, + )-> d128 { + + let txn = txns_map.get(&self.transaction_key).expect("Couldn't get txn. Tx num invalid?"); + match txn.transaction_type(ar_map, raw_accts, acct_map) { + TxType::Flow => { + let ar = ar_map.get(&self.action_record_key).unwrap(); + if ar.direction() == Polarity::Outgoing { + self.proceeds.get() + } + else { d128!(0) } + } + TxType::Exchange => { d128!(0) } + TxType::ToSelf => { d128!(0) } + } + } + + pub fn direction(&self) -> Polarity { + if self.amount < d128!(0.0) { Polarity::Outgoing } + else { Polarity::Incoming } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Term { + LT, + ST, +} + +impl Term { + + pub fn abbr_string(&self) -> String { + match *self { + Term::LT => "LT".to_string(), + Term::ST => "ST".to_string(), + } + } +} + +impl fmt::Display for Term { + + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Term::LT => write!(f, "LT"), + Term::ST => write!(f, "ST"), + } + } +} diff --git a/src/core_functions.rs b/src/core_functions.rs new file mode 100644 index 0000000..6c76ffa --- /dev/null +++ b/src/core_functions.rs @@ -0,0 +1,147 @@ +// Copyright (c) 2017-2019, scoobybejesus +// Redistributions must include the license: https://github.com/scoobybejesus/cryptools-rs/LEGAL.txt + +use std::path::PathBuf; +use std::error::Error; +use std::fs::File; +use std::process; +use std::collections::{HashMap}; + +use chrono::NaiveDate; + +use crate::account::{Account, RawAccount, Lot}; +use crate::transaction::{Transaction, ActionRecord}; +use crate::user_choices::{LotProcessingChoices, LikeKindSettings}; +use crate::import_accts_txns; +use crate::import_cost_proceeds_etc; +use crate::create_lots_mvmts; + +pub fn import_and_process_final( + input_file_path: PathBuf, + settings: &LotProcessingChoices, +) -> ( + HashMap, + HashMap, + HashMap, + HashMap, + Option +) { + + let mut transactions_map: HashMap = HashMap::new(); + let mut action_records_map: HashMap = HashMap::new(); + let mut raw_account_map: HashMap = HashMap::new(); + let mut account_map: HashMap = HashMap::new(); + let mut lot_map: HashMap<(RawAccount, u32), Lot> = HashMap::new(); + + match import_from_csv( + input_file_path, + &mut transactions_map, + &mut action_records_map, + &mut raw_account_map, + &mut account_map + ) { + Ok(()) => { println!("Successfully imported csv file."); } + Err(err) => { println!("\nFailed to import accounts and transactions from CSV."); println!("{}", err); process::exit(1); } + }; + + pub fn import_from_csv( + import_file_path: PathBuf, + transactions_map: &mut HashMap, + action_records: &mut HashMap, + raw_acct_map: &mut HashMap, + acct_map: &mut HashMap, + ) -> Result<(), Box> { + + let file = File::open(import_file_path)?; println!("CSV ledger file opened successfully.\n"); + + let mut rdr = csv::ReaderBuilder::new() + .has_headers(true) + .from_reader(file); + + match import_accts_txns::import_accounts(&mut rdr, raw_acct_map, acct_map) { + Ok(()) => {} + Err(err) => { println!("\nFailed to import accounts from CSV."); println!("{}", err); } + }; + + match import_accts_txns::import_transactions( + &mut rdr, + transactions_map, + action_records, + raw_acct_map, + acct_map + ) { + Ok(()) => {} + Err(err) => { println!("\nFailed to import transactions from CSV."); println!("{}", err); } + }; + + Ok(()) + } + // println!("like_kind_cutoff_date is: {}...", like_kind_cutoff_date); + + let likekind_settings: Option = if settings.enable_like_kind_treatment { + + let like_kind_cutoff_date = &settings.lk_cutoff_date_string; + + Some( + LikeKindSettings { + like_kind_cutoff_date: NaiveDate::parse_from_str(&like_kind_cutoff_date, "%y-%m-%d") + .unwrap_or(NaiveDate::parse_from_str(&like_kind_cutoff_date, "%Y-%m-%d") + .expect("Found date string with improper format")), + like_kind_basis_date_preserved: true, + } + ) + } else { + None + }; + + transactions_map = create_lots_mvmts::create_lots_and_movements( + transactions_map, + &settings, + &likekind_settings, + &action_records_map, + &mut raw_account_map, + &mut account_map, + &mut lot_map, + ); + + println!(" Successfully created lots and movements."); + + import_cost_proceeds_etc::add_cost_basis_to_movements( + &settings, + &action_records_map, + &raw_account_map, + &account_map, + &transactions_map + ); + + println!(" Successfully added cost basis to movements."); + + import_cost_proceeds_etc::add_proceeds_to_movements( + &action_records_map, + &raw_account_map, + &account_map, + &transactions_map + ); + + println!(" Successfully added proceeds to movements."); + + + if let Some(lk_settings) = &likekind_settings { + + let cutoff_date = lk_settings.like_kind_cutoff_date; + println!(" Applying like-kind treatment for cut-off date: {}.", cutoff_date); + + import_cost_proceeds_etc::apply_like_kind_treatment( + cutoff_date, + &settings, + &action_records_map, + &raw_account_map, + &account_map, + &transactions_map + ); + + println!(" Successfully applied like-kind treatment."); + }; + + (account_map, raw_account_map, action_records_map, transactions_map, likekind_settings) +} diff --git a/src/create_lots_mvmts.rs b/src/create_lots_mvmts.rs new file mode 100644 index 0000000..6bb2ecb --- /dev/null +++ b/src/create_lots_mvmts.rs @@ -0,0 +1,889 @@ +// Copyright (c) 2017-2019, scoobybejesus +// Redistributions must include the license: https://github.com/scoobybejesus/cryptools-rs/LEGAL.txt + +use std::rc::{Rc}; +use std::cell::{RefCell, Ref, Cell}; +use std::collections::{HashMap}; + +use decimal::d128; +use chrono::NaiveDate; + +use crate::transaction::{Transaction, ActionRecord, TxType, Polarity, TxHasMargin}; +use crate::account::{Account, RawAccount, Lot, Movement}; +use crate::user_choices::{LotProcessingChoices, InventoryCostingMethod, LikeKindSettings}; +use crate::utils::{round_d128_1e8}; + +pub fn create_lots_and_movements( + txns_map: HashMap, + settings: &LotProcessingChoices, + likekind_settings: &Option, + ar_map: &HashMap, + raw_acct_map: &HashMap, + acct_map: &HashMap, + lot_map: &HashMap<(RawAccount, u32), Lot>, +) -> HashMap { + + // Set values to be referred to repeatedly, potentially, in Incoming Exchange transactions + let multiple_incoming_mvmts_per_ar = match &likekind_settings { + Some(likekind_settings) => { + likekind_settings.like_kind_basis_date_preserved + } + None => { + false + } + }; + + let mut like_kind_cutoff_date: NaiveDate = NaiveDate::parse_from_str("1/1/1", "%m/%d/%y") + .expect("NaiveDate string parsing failed. It shouldn't have. Try again."); + // Above sets date never to be used. Below modifies date in case it will be used. + if likekind_settings.is_some() { + let likekind_settings_clone = likekind_settings.clone().unwrap(); + like_kind_cutoff_date = likekind_settings_clone.like_kind_cutoff_date; + } + + // On with the creating of lots and movements. + let length = txns_map.len(); + for num in 1..=length { + + let txn_num = num as u32; + let txn = txns_map.get(&(txn_num)).expect("Couldn't get txn. Tx num invalid?"); + if txn.marginness(&ar_map, &raw_acct_map, &acct_map) == TxHasMargin::TwoARs { + assert_eq!(txn.transaction_type(&ar_map, &raw_acct_map, &acct_map), TxType::Exchange); + assert_eq!(txn.action_record_idx_vec.len(), 2); + + let the_raw_pair_keys = txn.get_base_and_quote_raw_acct_keys(&ar_map, &raw_acct_map, &acct_map); + let base_acct = acct_map.get(&the_raw_pair_keys.0).expect("Couldn't get acct. Raw pair keys invalid?"); + let quote_acct = acct_map.get(&the_raw_pair_keys.1).expect("Couldn't get acct. Raw pair keys invalid?"); + + let (base_ar_idx, quote_ar_idx) = get_base_and_quote_ar_idxs( + the_raw_pair_keys, + &txn, + &ar_map, + &raw_acct_map, + &acct_map + ); + + let base_ar = ar_map.get(&base_ar_idx).unwrap(); + let quote_ar = ar_map.get("e_ar_idx).unwrap(); + + let mut base_acct_lot_list = base_acct.list_of_lots.borrow_mut(); + let mut quote_acct_lot_list = quote_acct.list_of_lots.borrow_mut(); + + let base_number_of_lots = base_acct_lot_list.len() as u32; + let quote_number_of_lots = quote_acct_lot_list.len() as u32; + assert_eq!(base_number_of_lots, quote_number_of_lots, ""); + + let acct_balances_are_zero: bool; + + if !base_acct_lot_list.is_empty() && !quote_acct_lot_list.is_empty() { + let base_balance_is_zero = base_acct_lot_list.last().unwrap().get_sum_of_amts_in_lot() == d128!(0); + let quote_balance_is_zero = quote_acct_lot_list.last().unwrap().get_sum_of_amts_in_lot() == d128!(0); + if base_balance_is_zero && quote_balance_is_zero { + acct_balances_are_zero = true + } else { + acct_balances_are_zero = false + } + } else { + assert_eq!(true, base_acct_lot_list.is_empty(), + "One margin account's list_of_lots is empty, but its pair's isn't."); + assert_eq!(true, quote_acct_lot_list.is_empty(), + "One margin account's list_of_lots is empty, but its pair's isn't."); + acct_balances_are_zero = true + } + + let mut base_lot: Rc; + let mut quote_lot: Rc; + + if acct_balances_are_zero { + base_lot = + Rc::new( + Lot { + date_as_string: txn.date_as_string.clone(), + date_of_first_mvmt_in_lot: txn.date, + date_for_basis_purposes: txn.date, + lot_number: base_number_of_lots + 1, + account_key: the_raw_pair_keys.0, + movements: RefCell::new([].to_vec()), + } + ) + ; + quote_lot = + Rc::new( + Lot { + date_as_string: txn.date_as_string.clone(), + date_of_first_mvmt_in_lot: txn.date, + date_for_basis_purposes: txn.date, + lot_number: quote_number_of_lots + 1, + account_key: the_raw_pair_keys.1, + movements: RefCell::new([].to_vec()), + } + ) + ; + } else { + base_lot = base_acct_lot_list.last().expect("Couldn't get lot. Base acct lot list empty?").clone(); + quote_lot = quote_acct_lot_list.last().expect("Couldn't get lot. Quote acct lot list empty?").clone(); + } + + let base_mvmt = Movement { + amount: base_ar.amount, + date_as_string: txn.date_as_string.clone(), + date: txn.date, + transaction_key: txn_num, + action_record_key: base_ar_idx, + cost_basis: Cell::new(d128!(0.0)), + ratio_of_amt_to_incoming_mvmts_in_a_r: d128!(1.0), + ratio_of_amt_to_outgoing_mvmts_in_a_r: Cell::new(d128!(1.0)), + lot_num: base_lot.lot_number, + proceeds: Cell::new(d128!(0.0)), + }; + wrap_mvmt_and_push(base_mvmt, &base_ar, &base_lot, &settings, &raw_acct_map, &acct_map); + + let quote_mvmt = Movement { + amount: quote_ar.amount, + date_as_string: txn.date_as_string.clone(), + date: txn.date, + transaction_key: txn_num, + action_record_key: quote_ar_idx, + cost_basis: Cell::new(d128!(0.0)), + ratio_of_amt_to_incoming_mvmts_in_a_r: d128!(1.0), + ratio_of_amt_to_outgoing_mvmts_in_a_r: Cell::new(d128!(1.0)), + lot_num: quote_lot.lot_number, + proceeds: Cell::new(d128!(0.0)), + }; + wrap_mvmt_and_push(quote_mvmt, "e_ar, "e_lot, &settings, &raw_acct_map, &acct_map); + + if acct_balances_are_zero { + base_acct_lot_list.push(base_lot); + quote_acct_lot_list.push(quote_lot); + } + continue + } else { + for ar_num in txn.action_record_idx_vec.iter() { + let ar = ar_map.get(ar_num).unwrap(); + + let acct = acct_map.get(&ar.account_key).unwrap(); + let raw_acct = raw_acct_map.get(&acct.raw_key).unwrap(); + let length_of_list_of_lots = acct.list_of_lots.borrow().len(); + + if raw_acct.is_home_currency(&settings.home_currency) { + if length_of_list_of_lots == 0 { + let lot = + Rc::new( + Lot { + date_as_string: txn.date_as_string.clone(), + date_of_first_mvmt_in_lot: txn.date, + date_for_basis_purposes: txn.date, + lot_number: 1, + account_key: acct.raw_key, + movements: RefCell::new([].to_vec()), + } + ) + ; + let whole_mvmt = Movement { + amount: ar.amount, + date_as_string: txn.date_as_string.clone(), + date: txn.date, + transaction_key: txn_num, + action_record_key: *ar_num, + cost_basis: Cell::new(d128!(0.0)), + ratio_of_amt_to_incoming_mvmts_in_a_r: d128!(1.0), + ratio_of_amt_to_outgoing_mvmts_in_a_r: Cell::new(d128!(1.0)), + lot_num: lot.lot_number, + proceeds: Cell::new(d128!(0.0)), + }; + wrap_mvmt_and_push(whole_mvmt, &ar, &lot, &settings, &raw_acct_map, &acct_map); + acct.list_of_lots.borrow_mut().push(lot); + continue + } + else { + assert_eq!(1, length_of_list_of_lots); // Only true for home currency + let lot = acct.list_of_lots.borrow_mut()[0 as usize].clone(); + let whole_mvmt = Movement { + amount: ar.amount, + date_as_string: txn.date_as_string.clone(), + date: txn.date, + transaction_key: txn_num, + action_record_key: *ar_num, + cost_basis: Cell::new(d128!(0.0)), + ratio_of_amt_to_incoming_mvmts_in_a_r: d128!(1.0), + ratio_of_amt_to_outgoing_mvmts_in_a_r: Cell::new(d128!(1.0)), + lot_num: lot.lot_number, + proceeds: Cell::new(d128!(0.0)), + }; + wrap_mvmt_and_push(whole_mvmt, &ar, &lot, &settings, &raw_acct_map, &acct_map); + continue + } + } + // Note: a_r is not in home currency if here or below + let polarity = ar.direction(); + let tx_type = txn.transaction_type(&ar_map, &raw_acct_map, &acct_map); + + match polarity { + Polarity::Outgoing => { + // println!("Txn: {}, outgoing {:?}-type of {} {}", + // txn.tx_number, txn.transaction_type(), ar.amount, acct.ticker); + // + if raw_acct.is_margin { + let this_acct = acct_map.get(&ar.account_key).unwrap(); + let lot = this_acct.list_of_lots.borrow().last() + .expect("Couldn't get lot. Acct lot list empty?").clone(); + let whole_mvmt = Movement { + amount: ar.amount, + date_as_string: txn.date_as_string.clone(), + date: txn.date, + transaction_key: txn_num, + action_record_key: *ar_num, + cost_basis: Cell::new(d128!(0.0)), + ratio_of_amt_to_incoming_mvmts_in_a_r: d128!(1.0), + ratio_of_amt_to_outgoing_mvmts_in_a_r: Cell::new(d128!(1.0)), + lot_num: lot.lot_number, + proceeds: Cell::new(d128!(0.0)), + }; + wrap_mvmt_and_push(whole_mvmt, &ar, &lot, &settings, &raw_acct_map, &acct_map); + continue + } else { + let list_of_lots_to_use = acct.list_of_lots.clone(); + + // the following returns vec to be iterated from beginning to end, which provides the index for the correct lot + let vec_of_ordered_index_values = match settings.costing_method { + InventoryCostingMethod::LIFObyLotCreationDate => { + get_lifo_by_creation_date(&list_of_lots_to_use.borrow())} + InventoryCostingMethod::LIFObyLotBasisDate => { + get_lifo_by_lot_basis_date(&list_of_lots_to_use.borrow())} + InventoryCostingMethod::FIFObyLotCreationDate => { + get_fifo_by_creation_date(&list_of_lots_to_use.borrow())} + InventoryCostingMethod::FIFObyLotBasisDate => { + get_fifo_by_lot_basis_date(&list_of_lots_to_use.borrow())} + }; + + fn get_lifo_by_creation_date(list_of_lots: &Ref>>) -> Vec { + let mut vec_of_indexes = [].to_vec(); + for (idx, lot) in list_of_lots.iter().enumerate() { + vec_of_indexes.insert(0, idx) + } + let vec = vec_of_indexes; + vec + } + + fn get_lifo_by_lot_basis_date(list_of_lots: &Ref>>) -> Vec { + let mut reordered_vec = list_of_lots.clone().to_vec(); + let length = reordered_vec.len(); + for _ in 0..length { + for j in 0..length-1 { + if reordered_vec[j].date_for_basis_purposes > reordered_vec[j+1].date_for_basis_purposes { + reordered_vec.swap(j, j+1) + } + } + } + let mut vec_of_indexes = [].to_vec(); + for (idx, lot) in reordered_vec.iter().enumerate() { + vec_of_indexes.insert(0, idx) + } + let vec = vec_of_indexes; + vec + } + + fn get_fifo_by_creation_date(list_of_lots: &Ref>>) -> Vec { + let mut vec_of_indexes = [].to_vec(); + for (idx, lot) in list_of_lots.iter().enumerate() { + vec_of_indexes.push(idx) + } + let vec = vec_of_indexes; + vec + } + + fn get_fifo_by_lot_basis_date(list_of_lots: &Ref>>) -> Vec { + let mut reordered_vec = list_of_lots.clone().to_vec(); + let length = reordered_vec.len(); + for _ in 0..length { + for j in 0..length-1 { + if reordered_vec[j].date_for_basis_purposes > reordered_vec[j+1].date_for_basis_purposes { + reordered_vec.swap(j, j+1) + } + } + } + let mut vec_of_indexes = [].to_vec(); + for (idx, lot) in reordered_vec.iter().enumerate() { + vec_of_indexes.push(idx) + } + let vec = vec_of_indexes; + vec + } + + let index_position: usize = 0; + let lot_index = vec_of_ordered_index_values[index_position]; + + let lot_to_use = list_of_lots_to_use.borrow()[lot_index].clone(); + let whole_mvmt = Movement { + amount: ar.amount, + date_as_string: txn.date_as_string.clone(), + date: txn.date, + transaction_key: txn_num, + action_record_key: *ar_num, + cost_basis: Cell::new(d128!(0.0)), + ratio_of_amt_to_incoming_mvmts_in_a_r: d128!(1.0), + ratio_of_amt_to_outgoing_mvmts_in_a_r: Cell::new(d128!(1.0)), + lot_num: lot_to_use.lot_number, + proceeds: Cell::new(d128!(0.0)), + }; + + fit_into_lots( + acct.raw_key, + txn_num, + *ar_num, + whole_mvmt, + list_of_lots_to_use, + vec_of_ordered_index_values, + index_position, + &settings, + &ar_map, + &raw_acct_map, + &acct_map, + ); + continue + } + } + Polarity::Incoming => { + // println!("Txn: {}, Incoming {:?}-type of {} {}", + // txn.tx_number, txn.transaction_type(), ar.amount, acct.ticker); + match tx_type { + TxType::Flow => { + let mut lot: Rc; + if raw_acct.is_margin { + let this_acct = acct_map.get(&ar.account_key).unwrap(); + let lot_list = this_acct.list_of_lots.borrow_mut(); + lot = lot_list.last().unwrap().clone(); + + let mvmt = Movement { + amount: ar.amount, + date_as_string: txn.date_as_string.clone(), + date: txn.date, + transaction_key: txn_num, + action_record_key: *ar_num, + cost_basis: Cell::new(d128!(0.0)), + ratio_of_amt_to_incoming_mvmts_in_a_r: d128!(1.0), + ratio_of_amt_to_outgoing_mvmts_in_a_r: Cell::new(d128!(1.0)), + lot_num: lot.lot_number, + proceeds: Cell::new(d128!(0.0)), + }; + wrap_mvmt_and_push(mvmt, &ar, &lot, &settings, &raw_acct_map, &acct_map); + continue + } else { + let mvmt: Movement; + if txn.action_record_idx_vec.len() == 1 { + lot = + Rc::new( + Lot { + date_as_string: txn.date_as_string.clone(), + date_of_first_mvmt_in_lot: txn.date, + date_for_basis_purposes: txn.date, + + lot_number: length_of_list_of_lots as u32 + 1, + account_key: acct.raw_key, + movements: RefCell::new([].to_vec()), + } + ) + ; + mvmt = Movement { + amount: ar.amount, + date_as_string: txn.date_as_string.clone(), + date: txn.date, + transaction_key: txn_num, + action_record_key: *ar_num, + cost_basis: Cell::new(d128!(0.0)), + ratio_of_amt_to_incoming_mvmts_in_a_r: d128!(1.0), + ratio_of_amt_to_outgoing_mvmts_in_a_r: Cell::new(d128!(1.0)), + lot_num: lot.lot_number, + proceeds: Cell::new(d128!(0.0)), + }; + } else { + assert_eq!(txn.action_record_idx_vec.len(), 2); + // create list of incoming/positive amounts(mvmts) in margin lot, add them + let mut positive_mvmt_list: Vec> = [].to_vec(); + let mut total_positive_amounts = d128!(0); + + let (base_acct_key, quote_acct_key) = get_base_and_quote_acct_for_dual_actionrecord_flow_tx( + txn_num, + &ar_map, + &raw_acct_map, + &acct_map, + &txns_map, + ); + + let base_acct = acct_map.get(&base_acct_key).unwrap(); + let base_acct_lot = base_acct.list_of_lots.borrow().last().unwrap().clone(); + // TODO: generalize this to work with margin shorts as well + for mvmt in base_acct_lot.movements.borrow().iter() { + if mvmt.amount > d128!(0) { + // println!("In lot# {}, positive mvmt amount: {} {},", + // base_acct_lot.lot_number, + // mvmt.borrow().amount, + // base_acct_lot.account.raw.ticker); + total_positive_amounts += mvmt.amount; + positive_mvmt_list.push(mvmt.clone()) + } + } + let mut amounts_used = d128!(0); + let mut percentages_used = d128!(0); + + for pos_mvmt in positive_mvmt_list.iter().take(positive_mvmt_list.len()-1) { + let inner_lot = + Rc::new( + Lot { + date_as_string: txn.date_as_string.clone(), + date_of_first_mvmt_in_lot: txn.date, + date_for_basis_purposes: pos_mvmt.date, + lot_number: acct.list_of_lots.borrow().len() as u32 + 1, + account_key: acct.raw_key, + movements: RefCell::new([].to_vec()), + } + ); + let percentage_used = round_d128_1e8(&(pos_mvmt.amount/&total_positive_amounts)); + let amount_used = round_d128_1e8(&(ar.amount*percentage_used)); + let inner_mvmt = Movement { + amount: amount_used, + date_as_string: txn.date_as_string.clone(), + date: txn.date, + transaction_key: txn_num, + action_record_key: *ar_num, + cost_basis: Cell::new(d128!(0.0)), + ratio_of_amt_to_incoming_mvmts_in_a_r: percentage_used, + ratio_of_amt_to_outgoing_mvmts_in_a_r: Cell::new(d128!(1.0)), + lot_num: inner_lot.lot_number, + proceeds: Cell::new(d128!(0.0)), + }; + wrap_mvmt_and_push(inner_mvmt, &ar, &inner_lot, &settings, &raw_acct_map, &acct_map); + acct.list_of_lots.borrow_mut().push(inner_lot); + // acct.push_lot(inner_lot); + amounts_used += amount_used; + percentages_used += percentage_used; + } + + let final_mvmt = positive_mvmt_list.last().unwrap(); + lot = + Rc::new( + Lot { + date_as_string: txn.date_as_string.clone(), + date_of_first_mvmt_in_lot: txn.date, + date_for_basis_purposes: final_mvmt.date, + lot_number: acct.list_of_lots.borrow().len() as u32 + 1, + account_key: acct.raw_key, + movements: RefCell::new([].to_vec()), + } + ) + ; + mvmt = Movement { + amount: round_d128_1e8(&(ar.amount - amounts_used)), + date_as_string: txn.date_as_string.clone(), + date: txn.date, + transaction_key: txn_num, + action_record_key: *ar_num, + cost_basis: Cell::new(d128!(0.0)), + ratio_of_amt_to_incoming_mvmts_in_a_r: round_d128_1e8(&(d128!(1.0) - percentages_used)), + ratio_of_amt_to_outgoing_mvmts_in_a_r: Cell::new(d128!(1.0)), + lot_num: lot.lot_number, + proceeds: Cell::new(d128!(0.0)), + }; + } + wrap_mvmt_and_push(mvmt, &ar, &lot, &settings, &raw_acct_map, &acct_map); + acct.list_of_lots.borrow_mut().push(lot); + continue + } + } + TxType::Exchange => { + let both_are_non_home_curr: bool; + let og_ar = ar_map.get(txn.action_record_idx_vec.first().unwrap()).unwrap(); + let og_acct = acct_map.get(&og_ar.account_key).unwrap(); + let og_raw_acct = raw_acct_map.get(&og_acct.raw_key).unwrap(); + let ic_ar = ar; + let ic_raw_acct = raw_acct; + both_are_non_home_curr = !og_raw_acct.is_home_currency(&settings.home_currency) + && !ic_raw_acct.is_home_currency(&settings.home_currency); + + if both_are_non_home_curr && multiple_incoming_mvmts_per_ar && (txn.date <= like_kind_cutoff_date) { + process_multiple_incoming_lots_and_mvmts( + txn_num, + &og_ar, + &ic_ar, + &settings, + *ar_num, + &raw_acct_map, + &acct_map, + &txns_map, + &ar_map, + ); + continue + } + + else { + let lot = + Rc::new( + Lot { + date_as_string: txn.date_as_string.clone(), + date_of_first_mvmt_in_lot: txn.date, + date_for_basis_purposes: txn.date, + lot_number: length_of_list_of_lots as u32 + 1, + account_key: acct.raw_key, + movements: RefCell::new([].to_vec()), + } + ) + ; + let whole_mvmt = Movement { + amount: ar.amount, + date_as_string: txn.date_as_string.clone(), + date: txn.date, + transaction_key: txn_num, + action_record_key: *ar_num, + cost_basis: Cell::new(d128!(0.0)), + ratio_of_amt_to_incoming_mvmts_in_a_r: d128!(1.0), + ratio_of_amt_to_outgoing_mvmts_in_a_r: Cell::new(d128!(1.0)), + lot_num: lot.lot_number, + proceeds: Cell::new(d128!(0.0)), + }; + wrap_mvmt_and_push(whole_mvmt, &ar, &lot, &settings, &raw_acct_map, &acct_map); + acct.list_of_lots.borrow_mut().push(lot); + continue + } + } + TxType::ToSelf => { + if raw_acct.is_margin { + { println!("\n Found margin actionrecord in toself txn # {} \n", txn.tx_number); use std::process::exit; exit(1) }; + } else { + process_multiple_incoming_lots_and_mvmts( + txn_num, + &ar_map.get(txn.action_record_idx_vec.first().unwrap()).unwrap(), // outgoing + &ar, // incoming + &settings, + *ar_num, + &raw_acct_map, + &acct_map, + &txns_map, + &ar_map, + ); + } + continue + } + } + } + } + } // end for ar in txn.actionrecords + } // end of tx does not have marginness of TwoARs + } // end for txn in transactions + txns_map +} + +fn get_base_and_quote_acct_for_dual_actionrecord_flow_tx( + txn_num: u32, + ar_map: &HashMap, + raw_acct_map: &HashMap, + acct_map: &HashMap, + txns_map: &HashMap, +) -> (u16, u16) { + + let txn = txns_map.get(&txn_num).expect("Couldn't get txn. Tx num invalid?"); + let og_flow_ar = ar_map.get(txn.action_record_idx_vec.first().unwrap()).unwrap(); + // println!("Acct: {}, Amount: {}, Tx: {}, ar: {}", + // outgoing_flow_ar.account_key, outgoing_flow_ar.amount, outgoing_flow_ar.tx_key, outgoing_flow_ar.self_ar_key); + let og_ar_mvmts_list = &og_flow_ar.get_mvmts_in_ar(acct_map, txns_map); // TODO: ... in margin profit, this just takes a list of one mvmt + let og_ar_list_first_mvmt = &og_ar_mvmts_list.first().unwrap(); // TODO: then this takes the one mvmt + let og_ar_list_first_mvmt_ar = ar_map.get(&og_ar_list_first_mvmt.action_record_key).unwrap(); + let og_ar_list_first_mvmt_ar_acct = acct_map.get(&og_ar_list_first_mvmt_ar.account_key).unwrap(); + let og_mvmt_lot = &og_ar_list_first_mvmt_ar_acct.list_of_lots.borrow()[(og_ar_list_first_mvmt.lot_num - 1) as usize]; + // let og_mvmt_lot_strong = &og_mvmt_lot; + let og_mvmt_lot_mvmts = og_mvmt_lot.movements.borrow(); + let og_mvmt_lot_first_mvmt = &og_mvmt_lot_mvmts.first().unwrap(); + let txn_of_og_mvmt_lot_first_mvmt = txns_map.get(&og_mvmt_lot_first_mvmt.transaction_key).unwrap(); + let (base_key,quote_key) = txn_of_og_mvmt_lot_first_mvmt.get_base_and_quote_raw_acct_keys( + ar_map, + &raw_acct_map, + &acct_map); // TODO: should this panic on margin loss? + (base_key, quote_key) +} + +fn get_base_and_quote_ar_idxs( + pair_keys: (u16,u16), + txn: &Transaction, + ars: &HashMap, + raw_acct_map: &HashMap, + acct_map: &HashMap + ) -> (u32, u32){ + + let incoming_ar = ars.get(&txn.action_record_idx_vec[0]).unwrap(); + let incoming_acct = acct_map.get(&incoming_ar.account_key).unwrap(); + let raw_ic_acct = raw_acct_map.get(&incoming_acct.raw_key).unwrap(); + let compare = raw_acct_map.get(&pair_keys.0).unwrap(); // key.0 is base, and key.1 is quote + + if raw_ic_acct == compare { + (txn.action_record_idx_vec[0], txn.action_record_idx_vec[1]) + } else { + (txn.action_record_idx_vec[1], txn.action_record_idx_vec[0]) + } +} + +fn wrap_mvmt_and_push( + this_mvmt: Movement, + ar: &ActionRecord, + lot: &Lot, + settings: &LotProcessingChoices, + raw_acct_map: &HashMap, + acct_map: &HashMap, +) { + + let acct = acct_map.get(&ar.account_key).unwrap(); + let raw_acct = raw_acct_map.get(&acct.raw_key).unwrap(); + + if ar.direction() == Polarity::Outgoing && !raw_acct.is_home_currency(&settings.home_currency) { + let ratio = this_mvmt.amount / ar.amount; + this_mvmt.ratio_of_amt_to_outgoing_mvmts_in_a_r.set(round_d128_1e8(&ratio)); + } + + let amt = this_mvmt.amount; + let amt2 = round_d128_1e8(&amt); + assert_eq!(amt, amt2); + // println!("Unrounded: {}; Rounded: {}; on {}", amt, amt2, mvmt_ref.borrow().date); + + let mvmt = Rc::from(this_mvmt); + lot.movements.borrow_mut().push(mvmt.clone()); + ar.movements.borrow_mut().push(mvmt); +} + +fn fit_into_lots( + acct_key: u16, + txn_num: u32, + spawning_ar_key: u32, + mvmt_to_fit: Movement, + list_of_lots_to_use: RefCell>>, + vec_of_ordered_index_values: Vec, + index_position: usize, + settings: &LotProcessingChoices, + ar_map: &HashMap, + raw_acct_map: &HashMap, + acct_map: &HashMap, + ) { + + let ar = ar_map.get(&spawning_ar_key).unwrap(); + let acct = acct_map.get(&ar.account_key).unwrap(); + let raw_acct = raw_acct_map.get(&acct.raw_key).unwrap(); + assert_eq!(raw_acct.is_home_currency(&settings.home_currency), false); + + let spawning_ar = ar_map.get(&spawning_ar_key).unwrap(); + let mut current_index_position = index_position; + + let lot = mvmt_to_fit.get_lot(acct_map, ar_map); + + let mut mut_sum_of_mvmts_in_lot: d128 = d128!(0.0); + for movement in lot.movements.borrow().iter() { + mut_sum_of_mvmts_in_lot += movement.amount; + } + let sum_of_mvmts_in_lot = mut_sum_of_mvmts_in_lot; + assert!(sum_of_mvmts_in_lot >= d128!(0.0)); + + if sum_of_mvmts_in_lot == d128!(0.0) { // If the lot is "full", go to the next + current_index_position += 1; + assert!(current_index_position < vec_of_ordered_index_values.len()); + let lot_index = vec_of_ordered_index_values[current_index_position]; + let newly_chosen_lot = list_of_lots_to_use.borrow()[lot_index].clone(); + let possible_mvmt_to_fit = Movement { + amount: mvmt_to_fit.amount, + date_as_string: mvmt_to_fit.date_as_string.clone(), + date: mvmt_to_fit.date, + transaction_key: mvmt_to_fit.transaction_key, + action_record_key: mvmt_to_fit.action_record_key, + cost_basis: mvmt_to_fit.cost_basis, + ratio_of_amt_to_incoming_mvmts_in_a_r: d128!(1.0), // Outgoing mvmt, so it's irrelevant + ratio_of_amt_to_outgoing_mvmts_in_a_r: Cell::new(d128!(1.0)), + lot_num: newly_chosen_lot.lot_number, + proceeds: Cell::new(d128!(0.0)), + }; + fit_into_lots( + acct.raw_key, + txn_num, + spawning_ar_key, + possible_mvmt_to_fit, + list_of_lots_to_use, + vec_of_ordered_index_values, + current_index_position, + &settings, + &ar_map, + &raw_acct_map, + &acct_map + ); + return; + } + assert!(sum_of_mvmts_in_lot > d128!(0.0)); + let remainder_amt = mvmt_to_fit.amount; + // println!("Sum of mvmts in lot: {}; Remainder amount: {}; Net: {}", + // sum_of_mvmts_in_lot, remainder_amt, sum_of_mvmts_in_lot + remainder_amt); + + let does_remainder_fit: bool = (sum_of_mvmts_in_lot + remainder_amt) >= d128!(0.0); + + if does_remainder_fit { + let remainder_that_fits = Movement { + amount: mvmt_to_fit.amount, + date_as_string: mvmt_to_fit.date_as_string.clone(), + date: mvmt_to_fit.date, + transaction_key: mvmt_to_fit.transaction_key, + action_record_key: mvmt_to_fit.action_record_key, + cost_basis: mvmt_to_fit.cost_basis, + ratio_of_amt_to_incoming_mvmts_in_a_r: d128!(1.0), // Outgoing mvmt, so it's irrelevant + ratio_of_amt_to_outgoing_mvmts_in_a_r: Cell::new(d128!(1.0)), + lot_num: lot.lot_number, + proceeds: Cell::new(d128!(0.0)), + }; + wrap_mvmt_and_push(remainder_that_fits, &spawning_ar, &lot, &settings, &raw_acct_map, &acct_map); + return // And we're done + } + // Note: at this point, we know the movement doesn't fit in a single lot & sum_of_mvmts_in_lot > 0 + let mvmt = RefCell::new(mvmt_to_fit); + let mvmt_rc = Rc::from(mvmt); + + let mvmt_that_fits_in_lot = Movement { + amount: (-sum_of_mvmts_in_lot).reduce(), + date_as_string: mvmt_rc.borrow().date_as_string.clone(), + date: mvmt_rc.borrow().date, + transaction_key: txn_num, + action_record_key: spawning_ar_key, + cost_basis: Cell::new(d128!(0.0)), + ratio_of_amt_to_incoming_mvmts_in_a_r: d128!(1.0), // Outgoing mvmt, so it's irrelevant + ratio_of_amt_to_outgoing_mvmts_in_a_r: Cell::new(d128!(1.0)), + lot_num: lot.lot_number, + proceeds: Cell::new(d128!(0.0)), + }; + wrap_mvmt_and_push(mvmt_that_fits_in_lot, &spawning_ar, &lot, &settings, &raw_acct_map, &acct_map); + let remainder_amt_to_recurse = remainder_amt + sum_of_mvmts_in_lot; + // println!("Remainder amount to recurse: {}", remainder_amt_to_recurse); + current_index_position += 1; + let lot_index = vec_of_ordered_index_values[current_index_position]; + let newly_chosen_lot = list_of_lots_to_use.borrow()[lot_index].clone(); + + let remainder_mvmt_to_recurse = Movement { + amount: remainder_amt_to_recurse.reduce(), + date_as_string: mvmt_rc.borrow().date_as_string.clone(), + date: mvmt_rc.borrow().date, + transaction_key: txn_num, + action_record_key: spawning_ar_key, + cost_basis: Cell::new(d128!(0.0)), // This acts as a dummy value. + ratio_of_amt_to_incoming_mvmts_in_a_r: d128!(1.0), // Outgoing mvmt, so it's irrelevant + ratio_of_amt_to_outgoing_mvmts_in_a_r: Cell::new(d128!(1.0)), // This acts as a dummy value. + lot_num: newly_chosen_lot.lot_number, + proceeds: Cell::new(d128!(0.0)), + }; + assert!(current_index_position < vec_of_ordered_index_values.len()); + fit_into_lots( + acct.raw_key, + txn_num, + spawning_ar_key, + remainder_mvmt_to_recurse, + list_of_lots_to_use, + vec_of_ordered_index_values, + current_index_position, + &settings, + &ar_map, + &raw_acct_map, + &acct_map + ); +} + +fn process_multiple_incoming_lots_and_mvmts( + txn_num: u32, + outgoing_ar: &ActionRecord, + incoming_ar: &ActionRecord, + settings: &LotProcessingChoices, + incoming_ar_key: u32, + raw_acct_map: &HashMap, + acct_map: &HashMap, + txns_map: &HashMap, + ar_map: &HashMap, +) { + + let round_to_places = d128::from(1).scaleb(d128::from(-8)); + let txn = txns_map.get(&txn_num).expect("Couldn't get txn. Tx num invalid?"); + + let acct_of_incoming_ar = acct_map.get(&incoming_ar.account_key).unwrap(); + + let mut all_but_last_incoming_mvmt_amt = d128!(0.0); + let mut all_but_last_incoming_mvmt_ratio = d128!(0.0); + // println!("Txn date: {}. Outgoing mvmts: {}, Outgoing amount: {}", txn.date, outgoing_ar.movements.borrow().len(), outgoing_ar.amount); + let list_of_mvmts_of_outgoing_ar = outgoing_ar.get_mvmts_in_ar(acct_map, txns_map); + let final_mvmt = list_of_mvmts_of_outgoing_ar.last().unwrap(); + // First iteration, for all but final movement + for outgoing_mvmt in list_of_mvmts_of_outgoing_ar + .iter() + .take(outgoing_ar.get_mvmts_in_ar(acct_map, txns_map).len() - 1) { + let ratio_of_outgoing_mvmt_to_total_ar = outgoing_mvmt.amount / outgoing_ar.amount; // Negative divided by negative is positive + // println!("Ratio of outgoing amt to total actionrecord amt: {:.8}", ratio_of_outgoing_to_total_ar); + let tentative_incoming_amt = ratio_of_outgoing_mvmt_to_total_ar * incoming_ar.amount; + // println!("Unrounded incoming amt: {}", tentative_incoming_amt); + let corresponding_incoming_amt = tentative_incoming_amt.quantize(round_to_places); + // println!("Rounded incoming amt: {}", corresponding_incoming_amt); + if corresponding_incoming_amt == d128!(0) { continue } // Due to rounding, this could be zero. + assert!(corresponding_incoming_amt > d128!(0.0)); + let this_acct = acct_of_incoming_ar; + let length_of_list_of_lots: usize = this_acct.list_of_lots.borrow().len(); + let inherited_date = outgoing_mvmt.get_lot(acct_map, ar_map).date_of_first_mvmt_in_lot; + let lot = + Rc::new( + Lot { + date_as_string: txn.date_as_string.clone(), + date_of_first_mvmt_in_lot: txn.date, + date_for_basis_purposes: inherited_date, + lot_number: length_of_list_of_lots as u32 + 1, + account_key: this_acct.raw_key, + movements: RefCell::new([].to_vec()), + } + ) + ; + let incoming_mvmt = Movement { + amount: corresponding_incoming_amt.reduce(), + date_as_string: txn.date_as_string.clone(), + date: txn.date, + transaction_key: txn_num, + action_record_key: incoming_ar_key, + cost_basis: Cell::new(d128!(0.0)), + ratio_of_amt_to_incoming_mvmts_in_a_r: round_d128_1e8(&ratio_of_outgoing_mvmt_to_total_ar), + ratio_of_amt_to_outgoing_mvmts_in_a_r: Cell::new(d128!(1.0)), + lot_num: lot.lot_number, + proceeds: Cell::new(d128!(0.0)), + }; + // println!("From first set of incoming movements, amount: {} {} to account: {}", + // incoming_mvmt.amount, acct_incoming_ar.ticker, acct_incoming_ar.account_num); + all_but_last_incoming_mvmt_ratio += round_d128_1e8(&ratio_of_outgoing_mvmt_to_total_ar); + all_but_last_incoming_mvmt_amt += incoming_mvmt.amount; + wrap_mvmt_and_push(incoming_mvmt, &incoming_ar, &lot, &settings, &raw_acct_map, &acct_map); + this_acct.list_of_lots.borrow_mut().push(lot); + } + // Second iteration, for final movement + let corresponding_incoming_amt = incoming_ar.amount - all_but_last_incoming_mvmt_amt; + assert!(corresponding_incoming_amt > d128!(0.0)); + let this_acct = acct_of_incoming_ar; + let length_of_list_of_lots = this_acct.list_of_lots.borrow().len(); + let inherited_date = final_mvmt.get_lot(acct_map, ar_map).date_of_first_mvmt_in_lot; + let lot = + Rc::new( + Lot { + date_as_string: txn.date_as_string.clone(), + date_of_first_mvmt_in_lot: txn.date, + date_for_basis_purposes: inherited_date, + lot_number: length_of_list_of_lots as u32 + 1, + account_key: this_acct.raw_key, + movements: RefCell::new([].to_vec()), + } + ) + ; + let incoming_mvmt = Movement { + amount: corresponding_incoming_amt.reduce(), + date_as_string: txn.date_as_string.clone(), + date: txn.date, + transaction_key: txn_num, + action_record_key: incoming_ar_key, + cost_basis: Cell::new(d128!(0.0)), + ratio_of_amt_to_incoming_mvmts_in_a_r: d128!(1.0) - all_but_last_incoming_mvmt_ratio, + ratio_of_amt_to_outgoing_mvmts_in_a_r: Cell::new(d128!(1.0)), + lot_num: lot.lot_number, + proceeds: Cell::new(d128!(0.0)), + }; + // println!("Final incoming mvmt for this actionrecord, amount: {} {} to account: {}", + // incoming_mvmt.amount, acct_incoming_ar.ticker, acct_incoming_ar.account_num); + wrap_mvmt_and_push(incoming_mvmt, &incoming_ar, &lot, &settings, &raw_acct_map, &acct_map); + this_acct.list_of_lots.borrow_mut().push(lot); +} diff --git a/src/export.rs b/src/export.rs new file mode 100644 index 0000000..a31a857 --- /dev/null +++ b/src/export.rs @@ -0,0 +1,433 @@ +// Copyright (c) 2017-2019, scoobybejesus +// Redistributions must include the license: https://github.com/scoobybejesus/cryptools-rs/LEGAL.txt + +use std::rc::{Rc}; +use std::fs::File; +use std::collections::{HashMap}; +use std::path::PathBuf; + +use decimal::d128; + +use crate::transaction::{Transaction, ActionRecord, Polarity, TxType}; +use crate::account::{Account, RawAccount, Term}; +use crate::user_choices::{LotProcessingChoices}; + + +pub fn _1_account_sums_to_csv( + settings: &LotProcessingChoices, + raw_acct_map: &HashMap, + acct_map: &HashMap +) { + + let mut rows: Vec> = [].to_vec(); + let mut header: Vec = [].to_vec(); + + header.extend_from_slice(&[ + "Account".to_string(), + "Balance".to_string(), + "Ticker".to_string(), + "Cost Basis".to_string(), + "Total lots".to_string(), + ]); + rows.push(header); + + let length = acct_map.len(); + + for j in 1..=length { + + let acct = acct_map.get(&(j as u16)).unwrap(); + let mut row: Vec = [].to_vec(); + + let balance: String; + let tentative_balance = acct.get_sum_of_amts_in_lots(); + + if tentative_balance == d128!(0) { + balance = "0.00".to_string() + } else { balance = tentative_balance.to_string() } + + let raw_acct = raw_acct_map.get(&acct.raw_key).unwrap(); + let cost_basis: String; + + if raw_acct.is_margin { cost_basis = "0.00".to_string() } else { + let tentative_cost_basis = acct.get_sum_of_basis_in_lots(); + if tentative_cost_basis == d128!(0) { + cost_basis = "0.00".to_string() + } else { cost_basis = tentative_cost_basis.to_string() } + } + + row.push(raw_acct.name.to_string()); + row.push(balance); + row.push(raw_acct.ticker.to_string()); + row.push(cost_basis); + row.push(acct.list_of_lots.borrow().len().to_string()); + rows.push(row); + } + let file_name = PathBuf::from("1_Acct_Sum_with_cost_basis.csv"); + let path = PathBuf::from(&settings.export_path.clone()); + + let full_path: PathBuf = [path, file_name].iter().collect(); + let buffer = File::create(full_path).unwrap(); + let mut wtr = csv::Writer::from_writer(buffer); + + for row in rows.iter() { + wtr.write_record(row).expect("Could not write row to CSV file"); + } + wtr.flush().expect("Could not flush Writer, though file should exist and be complete"); +} + +pub fn _2_account_sums_nonzero_to_csv( + acct_map: &HashMap, + settings: &LotProcessingChoices, + raw_acct_map: &HashMap +) { + + let mut rows: Vec> = [].to_vec(); + let mut header: Vec = [].to_vec(); + + header.extend_from_slice(&[ + "Account".to_string(), + "Balance".to_string(), + "Ticker".to_string(), + "Cost basis".to_string(), + "Total lots".to_string(), + ]); + rows.push(header); + + let length = acct_map.len(); + + for j in 1..=length { + + let acct = acct_map.get(&(j as u16)).unwrap(); + let mut row: Vec = [].to_vec(); + + let raw_acct = raw_acct_map.get(&acct.raw_key).unwrap(); + let name = raw_acct.name.to_string(); + + let balance: String; + let mut balance_d128 = d128!(0); + let tentative_balance = acct.get_sum_of_amts_in_lots(); + + if tentative_balance == d128!(0) { + balance = "0.00".to_string() + } else { balance_d128 += tentative_balance; balance = tentative_balance.to_string() } + + let cost_basis: String; + + if raw_acct.is_margin { cost_basis = "0.00".to_string() } else { + let tentative_cost_basis = acct.get_sum_of_basis_in_lots(); + if tentative_cost_basis == d128!(0) { + cost_basis = "0.00".to_string() + } else { cost_basis = tentative_cost_basis.to_string() } + } + + if balance_d128 != d128!(0) { + row.push(name); + row.push(balance); + row.push(raw_acct.ticker.to_string()); + row.push(cost_basis); + row.push(acct.list_of_lots.borrow().len().to_string()); + rows.push(row); + } + } + + let file_name = PathBuf::from("2_Acct_Sum_with_nonzero_cost_basis.csv"); + let path = PathBuf::from(&settings.export_path.clone()); + + let full_path: PathBuf = [path, file_name].iter().collect(); + let buffer = File::create(full_path).unwrap(); + let mut wtr = csv::Writer::from_writer(buffer); + + for row in rows.iter() { + wtr.write_record(row).expect("Could not write row to CSV file"); + } + wtr.flush().expect("Could not flush Writer, though file should exist and be complete"); +} + +// pub fn transactions_to_csv( +// transactions: &[Rc], +// ars: &HashMap, +// raw_acct_map: &HashMap, +// acct_map: &HashMap, +// txns_map: &HashMap,) { + +// let mut rows: Vec> = [].to_vec(); +// let mut header: Vec = [].to_vec(); +// header.extend_from_slice(&[ +// "Date".to_string(), +// "Txn#".to_string(), +// "Type".to_string(), +// "Memo".to_string(), +// "Amount".to_string(), +// "Ticker".to_string(), +// "Proceeds".to_string(), +// "Cost basis".to_string(), +// "Gain/loss".to_string(), +// "Term".to_string(), +// "Income".to_string(), +// "Expense".to_string(), +// ]); +// rows.push(header); +// for txn in transactions { +// for mvmt in txn.flow_or_outgoing_exchange_movements.borrow().iter() { +// let lot = mvmt.borrow().get_lot(acct_map, ars); +// let acct = acct_map.get(&lot.account_key).unwrap(); +// let raw_acct = raw_acct_map.get(&acct.raw_key).unwrap(); +// let mut row: Vec = [].to_vec(); +// row.push(txn.date.format("%Y/%m/%d").to_string()); +// row.push(txn.tx_number.to_string()); +// row.push(txn.transaction_type(&ars, &raw_acct_map, &acct_map).to_string()); +// row.push(txn.memo.to_string()); +// row.push(mvmt.borrow().amount.to_string()); +// row.push(raw_acct.ticker.to_string()); +// row.push(mvmt.borrow().proceeds.to_string()); +// row.push(mvmt.borrow().cost_basis.to_string()); +// row.push(mvmt.borrow().get_gain_or_loss().to_string()); +// row.push(mvmt.borrow().get_term(acct_map, ars).to_string()); +// row.push(mvmt.borrow().get_income(ars, &raw_acct_map, &acct_map, &txns_map).to_string()); +// row.push(mvmt.borrow().get_expense(ars, &raw_acct_map, &acct_map, &txns_map).to_string()); +// rows.push(row); +// } +// } +// let buffer = File::create("/Users/scoob/Documents/Testing/rust_exports/test/txns-3rd-try.csv").unwrap(); +// let mut wtr = csv::Writer::from_writer(buffer); +// for row in rows.iter() { +// wtr.write_record(row).expect("Could not write row to CSV file"); +// } +// wtr.flush().expect("Could not flush Writer, though file should exist and be complete"); +// } + +pub fn _5_transaction_mvmt_summaries_to_csv( + settings: &LotProcessingChoices, + ars: &HashMap, + raw_acct_map: &HashMap, + acct_map: &HashMap, + txns_map: &HashMap, +) { + + let mut rows: Vec> = [].to_vec(); + let mut header: Vec = [].to_vec(); + + header.extend_from_slice(&[ + "Date".to_string(), + "Txn#".to_string(), + "Type".to_string(), + "Memo".to_string(), + "Amount".to_string(), + "Ticker".to_string(), + "Term".to_string(), + "Proceeds".to_string(), + "Cost basis".to_string(), + "Gain/loss".to_string(), + "Income".to_string(), + "Expense".to_string(), + ]); + rows.push(header); + + let length = txns_map.len(); + + for txn_num in 1..=length { + + let txn_num = txn_num as u32; + let txn = txns_map.get(&(txn_num)).unwrap(); + let txn_date_string = txn.date.format("%Y/%m/%d").to_string(); + let tx_num_string = txn.tx_number.to_string(); + let tx_type_string = txn.transaction_type(ars, &raw_acct_map, &acct_map).to_string(); + let tx_memo_string = txn.memo.to_string(); + let mut term_st: Option = None; + let mut term_lt: Option = None; + let mut ticker: Option = None; + let mut polarity: Option = None; + + let mut amount_st = d128!(0); + let mut proceeds_st = d128!(0); + let mut cost_basis_st = d128!(0); + + let mut income_st = d128!(0); + let mut expense_st = d128!(0); + + let mut amount_lt = d128!(0); + let mut proceeds_lt = d128!(0); + let mut cost_basis_lt = d128!(0); + + let mut income_lt = d128!(0); + let mut expense_lt = d128!(0); + + let flow_or_outgoing_exchange_movements = txn.get_outgoing_exchange_and_flow_mvmts( + settings, + ars, + raw_acct_map, + acct_map, + txns_map + ); + + for mvmt in flow_or_outgoing_exchange_movements.iter() { + let lot = mvmt.get_lot(acct_map, ars); + let acct = acct_map.get(&lot.account_key).unwrap(); + let raw_acct = raw_acct_map.get(&acct.raw_key).unwrap(); + + if let None = ticker { ticker = Some(raw_acct.ticker.clone()) }; + + if let None = polarity { + polarity = if mvmt.amount > d128!(0) { + Some(Polarity::Incoming) + } else { Some(Polarity::Outgoing) + }; + } + + let term = mvmt.get_term(acct_map, ars); + + if term == Term::LT { + amount_lt += mvmt.amount; + proceeds_lt += mvmt.proceeds.get(); + cost_basis_lt += mvmt.cost_basis.get(); + match term_lt { + None => { term_lt = Some(term)} + _ => {} + } + } else { + assert_eq!(term, Term::ST); + amount_st += mvmt.amount; + proceeds_st += mvmt.proceeds.get(); + cost_basis_st += mvmt.cost_basis.get(); + if term_st == None { + term_st = Some(term); + } + } + } + + if (txn.transaction_type(ars, &raw_acct_map, &acct_map) == TxType::Flow) & (polarity == Some(Polarity::Incoming)) { + // println!("Incoming flow {}", txn.tx_number); + income_st = proceeds_st; + proceeds_st = d128!(0); + cost_basis_st = d128!(0); + income_lt = proceeds_lt; + proceeds_lt = d128!(0); + cost_basis_lt = d128!(0); + } + + if (txn.transaction_type(ars, &raw_acct_map, &acct_map) == TxType::Flow) & (polarity == Some(Polarity::Outgoing)) { + // println!("Outgoing flow {}, proceeds_st {}, proceeds_lt {}", txn.tx_number, proceeds_st, proceeds_lt); + expense_st -= proceeds_st; + expense_lt -= proceeds_lt; + } + + if let Some(term) = term_st { + + let mut row: Vec = [].to_vec(); + + row.push(txn_date_string.clone()); + row.push(tx_num_string.clone()); + row.push(tx_type_string.clone()); + row.push(tx_memo_string.clone()); + row.push(amount_st.to_string()); + row.push(ticker.clone().unwrap()); + row.push(term.abbr_string()); + row.push(proceeds_st.to_string()); + row.push(cost_basis_st.to_string()); + row.push((proceeds_st + cost_basis_st).to_string()); + row.push(income_st.to_string()); + row.push(expense_st.to_string()); + + rows.push(row); + } + if let Some(term) = term_lt { + + let mut row: Vec = [].to_vec(); + + row.push(txn_date_string); + row.push(tx_num_string); + row.push(tx_type_string); + row.push(tx_memo_string); + row.push(amount_lt.to_string()); + row.push(ticker.unwrap()); + row.push(term.abbr_string()); + row.push(proceeds_lt.to_string()); + row.push(cost_basis_lt.to_string()); + row.push((proceeds_lt + cost_basis_lt).to_string()); + row.push(income_lt.to_string()); + row.push(expense_lt.to_string()); + + rows.push(row); + } + } + + let file_name = PathBuf::from("5_Txns_mvmts_summary.csv"); + let path = PathBuf::from(&settings.export_path); + + let full_path: PathBuf = [path, file_name].iter().collect(); + let buffer = File::create(full_path).unwrap(); + let mut wtr = csv::Writer::from_writer(buffer); + + for row in rows.iter() { + wtr.write_record(row).expect("Could not write row to CSV file"); + } + wtr.flush().expect("Could not flush Writer, though file should exist and be complete"); +} + +pub fn accounts_to_csv( + accounts: &[Rc], + ars: &HashMap, + raw_acct_map: &HashMap, + acct_map: &HashMap, + txns_map: &HashMap, +) { + + let mut rows: Vec> = [].to_vec(); + let mut header: Vec = [].to_vec(); + + header.extend_from_slice(&[ + "#".to_string(), + "Account".to_string(), + "Ticker".to_string(), + "Margin".to_string(), + "Date".to_string(), + "Txn#".to_string(), + "Type".to_string(), + "Memo".to_string(), + "Amount".to_string(), + "Proceeds".to_string(), + "Cost basis\n".to_string(), + "Gain/loss".to_string(), + "Term".to_string(), + "Income".to_string(), + "Expense".to_string(), + ]); + rows.push(header); + + for acct in accounts { + for lot in acct.list_of_lots.borrow().iter() { + for mvmt in lot.movements.borrow().iter() { + + let raw_acct = raw_acct_map.get(&acct.raw_key).unwrap(); + let txn = txns_map.get(&mvmt.transaction_key).unwrap(); + let mut row: Vec = [].to_vec(); + + row.push(raw_acct.account_num.to_string()); + row.push(raw_acct.name.to_string()); + row.push(raw_acct.ticker.to_string()); + row.push(raw_acct.is_margin.to_string()); + row.push(mvmt.date.format("%Y/%m/%d").to_string()); + row.push(txn.tx_number.to_string()); + row.push(txn.transaction_type(ars, &raw_acct_map, &acct_map).to_string()); + row.push(txn.memo.to_string()); + row.push(mvmt.amount.to_string()); + row.push(mvmt.proceeds.get().to_string()); + row.push(mvmt.cost_basis.get().to_string()); + row.push(mvmt.get_gain_or_loss().to_string()); + row.push(mvmt.get_term(acct_map, ars).to_string()); + row.push(mvmt.get_income(ars, &raw_acct_map, &acct_map, &txns_map).to_string()); + row.push(mvmt.get_expense(ars, &raw_acct_map, &acct_map, &txns_map).to_string()); + + rows.push(row); + } + } + } + + let buffer = File::create("/Users/scoob/Documents/Testing/rust_exports/test/accts-1st-try.csv").unwrap(); + let mut wtr = csv::Writer::from_writer(buffer); + + for row in rows.iter() { + wtr.write_record(row).expect("Could not write row to CSV file"); + } + wtr.flush().expect("Could not flush Writer, though file should exist and be complete"); +} diff --git a/src/import_accts_txns.rs b/src/import_accts_txns.rs new file mode 100644 index 0000000..e7232f5 --- /dev/null +++ b/src/import_accts_txns.rs @@ -0,0 +1,230 @@ +// Copyright (c) 2017-2019, scoobybejesus +// Redistributions must include the license: https://github.com/scoobybejesus/cryptools-rs/LEGAL.txt + +use std::error::Error; +use std::process; +use std::fs::File; +use std::cell::{RefCell}; +use std::collections::{HashMap}; + +use chrono::NaiveDate; +use decimal::d128; + +use crate::transaction::{Transaction, ActionRecord}; +use crate::account::{Account, RawAccount}; +use crate::utils::{round_d128_1e8}; + + +pub fn import_accounts( + rdr: &mut csv::Reader, + raw_acct_map: &mut HashMap, + acct_map: &mut HashMap, +) -> Result<(), Box> { + + let header1: csv::StringRecord; + header1 = rdr.headers()?.clone(); // account_num + + // Declare remaining headers, and initialize with None + let mut header2: Option = None; // name + let mut header3: Option = None; // ticker + let header4: csv::StringRecord; // is_margin + + // Declare string vector (array) to which [all but the first three] Strings of the header row will be appended + let mut headerstrings: Vec = [].to_vec(); + + // Basically, append three empty strings, and then the list of account numbers one by one + for element in header1.into_iter() { + match element { // Previously had `let element_str: String = element.to_string();` and then `match &*element_str` + "txDate" => { headerstrings.push("".to_string()) }, + "proceeds" => { headerstrings.push("".to_string()) }, + "memo" => { headerstrings.push("".to_string()) }, + _ => headerstrings.push(element.to_string()) + }; + } // End result is headerstrings = ["","","",1,2,3...n] + + // Account Creation loop. Iterate through 'data' records. We set hasheaders() to true above, so the first record here is the second row of the CSV + for result in rdr.records() { + // This initial iteration through records will break after the 4th row, after accounts have been created + let record = result?; + if header2 == None { + header2 = Some(record.clone()); + continue // After header2 is set, continue to next record + } + else if header3 == None { + header3 = Some(record.clone()); + continue // After header3 is set, continue to next record + } + else { + header4 = record.clone(); + // println!("Assigned last header, record: {:?}", record); + + let warn = "FATAL: Transactions will not import correctly if account numbers in the CSV import file aren't ordered chronologically (i.e., beginning in column 4 - the 1st account column - the values should be 1. The next column should be 2, then 3, etc, until the final account)."; + + // We've got all our header rows. It's now that we set up the accounts. + println!("Attempting to create accounts..."); + + let mut no_dup_acct_nums = HashMap::new(); + let length = &headerstrings.len(); + + for num in headerstrings[3..*length].iter().enumerate() { + let counter = no_dup_acct_nums.entry(num).or_insert(0); + *counter += 1; + } + + for acct_num in no_dup_acct_nums.keys() { + assert_eq!(no_dup_acct_nums[acct_num], 1, "Found accounts with duplicate numbers during import."); + } + + for (idx, item) in headerstrings[3..*length].iter().enumerate() { + + // println!("Headerstrings value: {:?}", item); + let ind = idx+3; // Add three because the idx skips the first three 'key' columns + let account_num = item.parse::()?; + assert_eq!((idx + 1) as u16, account_num, "Found improper Account Number usage: {}", warn); + let name:String = header2.clone().unwrap()[ind].trim().to_string(); + let ticker:String = header3.clone().unwrap()[ind].trim().to_string(); + let margin_string = &header4.clone()[ind]; + + let is_margin:bool = match margin_string.trim().to_lowercase().as_str() { + "no" | "non" | "false" => false, + "yes" | "margin" | "true" => true, + _ => { println!("\n Couldn't parse margin value for acct {} {} \n",account_num, name); process::exit(1) } + }; + + let just_account: RawAccount = RawAccount { + account_num: account_num, + name: name, + ticker: ticker, + is_margin: is_margin, + }; + + raw_acct_map.insert(account_num, just_account); + + let account: Account = Account { + raw_key: account_num, + list_of_lots: RefCell::new([].to_vec()) + }; + + acct_map.insert(account_num, account); + } + break // This `break` exits this scope so `accounts` can be accessed in `import_transactions`. The rdr stays put. + } + }; + Ok(()) +} + +pub fn import_transactions( + rdr: &mut csv::Reader, + txns_map: &mut HashMap, + action_records: &mut HashMap, + raw_acct_map: &mut HashMap, + acct_map: &mut HashMap, +) -> Result<(), Box> { + + let mut this_tx_number = 0; + let mut this_ar_number = 0; + let mut changed_action_records = 0; + let mut changed_txn_num = Vec::new(); + + println!("Attempting to create transactions..."); + + for result in rdr.records() { + + // rdr's cursor is at row 5, which is the first transaction row + let record = result?; + this_tx_number += 1; + + // First, initialize metadata fields. + let mut this_tx_date: &str = ""; + let mut this_proceeds: &str = ""; + let mut this_memo: &str = ""; + let mut this: String = "".to_string(); + + // Next, create action_records. + let mut action_records_map_keys_vec: Vec = [].to_vec(); + let mut outgoing_ar: Option = None; + let mut incoming_ar: Option = None; + let mut outgoing_ar_num: Option = None; + let mut incoming_ar_num: Option = None; + + for (idx, field) in record.iter().enumerate() { + + // Set metadata fields on first three fields. + if idx == 0 { this_tx_date = field; } + else if idx == 1 { + this = field.replace(",", ""); + this_proceeds = this.as_str(); + } + else if idx == 2 { this_memo = field; } + + // Check for empty strings. If not empty, it's a value for an action_record. + else if field != "" { + this_ar_number += 1; + let ind = idx; // starts at 3, which is the fourth field + let acct_idx = ind - 2; // acct_num and acct_key would be idx + 1, so subtract 2 from ind to get 1 + let account_key = acct_idx as u16; + + let amount_str = field.replace(",", ""); + let amount = amount_str.parse::().unwrap(); + let amount_rounded = round_d128_1e8(&amount); + if amount != amount_rounded { changed_action_records += 1; changed_txn_num.push(this_tx_number); } + + let action_record = ActionRecord { + account_key: account_key, + amount: amount_rounded, + tx_key: this_tx_number, + self_ar_key: this_ar_number, + movements: RefCell::new([].to_vec()), + }; + + if amount > d128!(0.0) { + incoming_ar = Some(action_record); + incoming_ar_num = Some(this_ar_number); + action_records_map_keys_vec.push(incoming_ar_num.unwrap()) + } else { + outgoing_ar = Some(action_record); + outgoing_ar_num = Some(this_ar_number); + action_records_map_keys_vec.insert(0, outgoing_ar_num.unwrap()) + }; + } + } + + match incoming_ar { + Some(incoming_ar) => { + let x = incoming_ar_num.unwrap(); + action_records.insert(x, incoming_ar); + }, + None => {} + } + + match outgoing_ar { + Some(outgoing_ar) => { + let y = outgoing_ar_num.unwrap(); + action_records.insert(y, outgoing_ar); + }, + None => {} + } + + let tx_date = NaiveDate::parse_from_str(this_tx_date, "%m/%d/%y") + .unwrap_or(NaiveDate::parse_from_str(this_tx_date, "%m/%d/%Y") + .expect("%m/%d/%y (or %Y) format required for ledger import") + ); + + let transaction = Transaction { + tx_number: this_tx_number, + date_as_string: this_tx_date.to_string(), + date: tx_date, + memo: this_memo.to_string(), + proceeds: this_proceeds.parse::()?, + action_record_idx_vec: action_records_map_keys_vec, + }; + + txns_map.insert(this_tx_number, transaction); + }; + + if changed_action_records > 0 { + println!(" Changed actionrecord amounts: {}. Changed txn numbers: {:?}.", changed_action_records, changed_txn_num); + } + + Ok(()) +} diff --git a/src/import_cost_proceeds_etc.rs b/src/import_cost_proceeds_etc.rs new file mode 100644 index 0000000..123d35c --- /dev/null +++ b/src/import_cost_proceeds_etc.rs @@ -0,0 +1,373 @@ +// Copyright (c) 2017-2019, scoobybejesus +// Redistributions must include the license: https://github.com/scoobybejesus/cryptools-rs/LEGAL.txt + +use std::collections::{HashMap}; + +use chrono::NaiveDate; +use decimal::d128; + +use crate::transaction::{Transaction, TxType, ActionRecord, Polarity}; +use crate::account::{Account, RawAccount}; +use crate::utils::{round_d128_1e2}; +use crate::user_choices::{LotProcessingChoices}; + +pub fn add_cost_basis_to_movements( + settings: &LotProcessingChoices, + ars: &HashMap, + raw_acct_map: &HashMap, + acct_map: &HashMap, + txns_map: &HashMap, +) { + + let length = txns_map.len(); + + for txn_num in 1..=length { + + let txn_num = txn_num as u32; + let txn = txns_map.get(&(txn_num)).unwrap(); + + for ar_num in txn.action_record_idx_vec.iter() { + + let ar = ars.get(ar_num).unwrap(); + let acct = acct_map.get(&ar.account_key).unwrap(); + let raw_acct = raw_acct_map.get(&acct.raw_key).unwrap(); + let movements = ar.get_mvmts_in_ar(acct_map, txns_map); + + for mvmt in movements.iter() { + + let polarity = ar.direction(); + let tx_type = txn.transaction_type(ars, raw_acct_map, acct_map); + let is_home_curr = raw_acct.is_home_currency(&settings.home_currency); + let mvmt_copy = mvmt.clone(); + let borrowed_mvmt = mvmt_copy.clone(); + // println!("Txn: {} on {} of type: {:?}",txn.tx_number,txn.date, txn.transaction_type()); + + if !raw_acct.is_margin { + match polarity { + Polarity::Outgoing => { + if is_home_curr { + let mvmts_amt = mvmt_copy.amount; + mvmt.cost_basis.set(mvmts_amt); + } else { + let mvmt_lot = mvmt_copy.get_lot(acct_map, ars); + let borrowed_mvmt_list = mvmt_lot.movements.borrow().clone(); + let lots_first_mvmt = borrowed_mvmt_list.first().unwrap().clone(); + let cb_of_lots_first_mvmt = lots_first_mvmt.cost_basis.get(); + let ratio_of_amt_to_lots_first_mvmt = borrowed_mvmt.ratio_of_amt_to_lots_first_mvmt(acct_map, ars); + let unrounded_basis = -(cb_of_lots_first_mvmt * ratio_of_amt_to_lots_first_mvmt); + let rounded_basis = round_d128_1e2(&unrounded_basis); + mvmt.cost_basis.set(rounded_basis); + // if txn.tx_number == 5 { + // println!("Txn#: {}, ratio: {}, cb of lot's 1st mvmt: {}, cost basis: {}", txn.tx_number, ratio_of_amt_to_lots_first_mvmt, cb_of_lots_first_mvmt, cost_basis) + // }; + } + // println!("Outgoing a_r from txn: {} of type: {:?}, with {} {} with cost basis: {}", + // txn.tx_number, txn.transaction_type(), borrowed_mvmt.amount, ar.account.ticker, cost_basis); + assert!(mvmt.cost_basis.get() <= d128!(0)); + continue + } + Polarity::Incoming => { + if is_home_curr { + let mvmts_amt = mvmt_copy.amount; + mvmt.cost_basis.set(mvmts_amt); + } else { + match tx_type { + TxType::Exchange => { + let other_ar = ars.get(&txn.action_record_idx_vec[0]).unwrap(); + let other_acct = acct_map.get(&other_ar.account_key).unwrap(); + let raw_other_acct = raw_acct_map.get(&other_acct.raw_key).unwrap(); + assert_eq!(other_ar.direction(), Polarity::Outgoing); + let other_ar_is_home_curr = raw_other_acct.is_home_currency(&settings.home_currency); + + if other_ar_is_home_curr { + mvmt.cost_basis.set(-(other_ar.amount)); + } else { + + let ratio_of_amt_to_incoming_mvmts_in_a_r = + borrowed_mvmt.ratio_of_amt_to_incoming_mvmts_in_a_r; + let txn_proceeds = txn.proceeds.to_string().parse::().unwrap(); + let unrounded_basis = txn_proceeds * ratio_of_amt_to_incoming_mvmts_in_a_r; + let rounded_basis = round_d128_1e2(&unrounded_basis); + mvmt.cost_basis.set(rounded_basis); + } + } + TxType::ToSelf => { + let cb_outgoing_ar = retrieve_cost_basis_from_corresponding_outgoing_toself( + txn_num, &ars, txns_map, acct_map); + let ratio_of_amt_to_incoming_mvmts_in_a_r = borrowed_mvmt.ratio_of_amt_to_incoming_mvmts_in_a_r; + let this_ratio = (ratio_of_amt_to_incoming_mvmts_in_a_r) + .to_string() + .parse::() + .unwrap(); + let unrounded_basis = cb_outgoing_ar * this_ratio; + let rounded_basis = round_d128_1e2(&unrounded_basis); + mvmt.cost_basis.set(-rounded_basis); + } + TxType::Flow => { + let txn_proceeds = txn.proceeds.to_string().parse::().unwrap(); + let mvmt_proceeds = round_d128_1e2(&(txn_proceeds * + borrowed_mvmt.ratio_of_amt_to_incoming_mvmts_in_a_r)); + mvmt.cost_basis.set(mvmt_proceeds); + } + } + } + // println!("Incoming a_r from txn: {} of type: {:?}, with {} {} with cost basis: {}", + // txn.tx_number, txn.transaction_type(), borrowed_mvmt.amount, ar.account.ticker, cost_basis); + assert!(mvmt.cost_basis.get() >= d128!(0)); + continue + } + } + } + } + } + } + fn retrieve_cost_basis_from_corresponding_outgoing_toself( + txn_num: u32, + ars: &HashMap, + txns_map: &HashMap, + acct_map: &HashMap, + ) -> d128 { + + let txn = txns_map.get(&txn_num).unwrap(); + let other_ar_borrowed = &ars.get(&txn.action_record_idx_vec[0]).unwrap(); + + assert_eq!(other_ar_borrowed.direction(), Polarity::Outgoing); + + let mut basis = d128!(0); + let movements = other_ar_borrowed.get_mvmts_in_ar(acct_map, txns_map); + + for mvmt in movements.iter() { + basis += mvmt.cost_basis.get(); + } + + basis + }; +} + +pub fn add_proceeds_to_movements( + ars: &HashMap, + raw_acct_map: &HashMap, + acct_map: &HashMap, + txns_map: &HashMap, +) { + + let length = txns_map.len(); + + for txn_num in 1..=length { + + let txn_num = txn_num as u32; + let txn = txns_map.get(&(txn_num)).unwrap(); + + for ar_num in txn.action_record_idx_vec.iter() { + + let ar = ars.get(ar_num).unwrap(); + let movements = ar.get_mvmts_in_ar(acct_map, txns_map); + + for mvmt in movements.iter() { + + let polarity = ar.direction(); + let tx_type = txn.transaction_type(ars, raw_acct_map, acct_map); + let mvmt_copy = mvmt.clone(); + let borrowed_mvmt = mvmt_copy.clone(); + + match tx_type { + TxType::Exchange => { + match polarity { + Polarity::Outgoing => { + let ratio = borrowed_mvmt.amount / ar.amount; + let proceeds_unrounded = txn.proceeds.to_string().parse::().unwrap() * ratio; + let proceeds_rounded = round_d128_1e2(&proceeds_unrounded); + mvmt.proceeds.set(proceeds_rounded); + } + Polarity::Incoming => {} + } + } + TxType::Flow => { + let ratio = borrowed_mvmt.amount / ar.amount; + let proceeds_unrounded = txn.proceeds.to_string().parse::().unwrap() * ratio; + let proceeds_rounded = round_d128_1e2(&proceeds_unrounded); + mvmt.proceeds.set(proceeds_rounded); + } + TxType::ToSelf => {} + } + // println!("Txn: {}, type: {:?} of {} {} w/ proceeds: {} & basis: {}", + // txn.tx_number, txn.transaction_type(), borrowed_mvmt.amount, ar.account.ticker, proceeds, borrowed_mvmt.cost_basis); + } + } + } +} + +pub fn apply_like_kind_treatment( + cutoff_date: NaiveDate, + settings: &LotProcessingChoices, + ars: &HashMap, + raw_acct_map: &HashMap, + acct_map: &HashMap, + txns_map: &HashMap, +) { + + let length = txns_map.len(); + for txn_num in 1..=length { + + let txn_num = txn_num as u32; + let txn = txns_map.get(&(txn_num)).unwrap(); + + update_current_txn_for_prior_likekind_treatment(txn_num, &settings, &ars, &raw_acct_map, &acct_map, &txns_map); + + if txn.date <= cutoff_date { + perform_likekind_treatment_on_txn(txn_num, &settings, &ars, &raw_acct_map, &acct_map, &txns_map); + } + } +} + +fn update_current_txn_for_prior_likekind_treatment( + txn_num: u32, + settings: &LotProcessingChoices, + ars: &HashMap, + raw_acct_map: &HashMap, + acct_map: &HashMap, + txns_map: &HashMap, +) { + + let mut sum_of_outgoing_cost_basis_in_ar = d128!(0); + let txn = txns_map.get(&txn_num).unwrap(); + + for ar_num in txn.action_record_idx_vec.iter() { + + let ar = ars.get(ar_num).unwrap(); + let acct = acct_map.get(&ar.account_key).unwrap(); + let raw_acct = raw_acct_map.get(&acct.raw_key).unwrap(); + let movements = ar.get_mvmts_in_ar(acct_map, txns_map); + + for mvmt in movements.iter() { + + let polarity = ar.direction(); + let tx_type = txn.transaction_type(ars, raw_acct_map, acct_map); + let is_home_curr = raw_acct.is_home_currency(&settings.home_currency); + + let mvmt_copy = mvmt.clone(); + let borrowed_mvmt = mvmt_copy.clone(); + + if !raw_acct.is_margin { + match polarity { + Polarity::Outgoing => { + if !is_home_curr { + let borrowed_mvmt_lot = borrowed_mvmt.get_lot(acct_map, ars); + let borrowed_mvmt_list = borrowed_mvmt_lot.movements.borrow(); + let cb_of_lots_first_mvmt = borrowed_mvmt_list.first().unwrap().cost_basis.get(); + let ratio_of_amt_to_lots_first_mvmt = borrowed_mvmt.ratio_of_amt_to_lots_first_mvmt(acct_map, ars); + let unrounded_basis = -(cb_of_lots_first_mvmt * ratio_of_amt_to_lots_first_mvmt); + let rounded_basis = round_d128_1e2(&unrounded_basis); + mvmt.cost_basis.set(rounded_basis); + } + sum_of_outgoing_cost_basis_in_ar += mvmt.cost_basis.get() + } + Polarity::Incoming => { + match tx_type { + TxType::Exchange => {} + TxType::Flow => {} + TxType::ToSelf => { + if !is_home_curr { + let ratio_of_amt_to_incoming_mvmts_in_a_r = + borrowed_mvmt.ratio_of_amt_to_incoming_mvmts_in_a_r; + let unrounded_basis = sum_of_outgoing_cost_basis_in_ar * + ratio_of_amt_to_incoming_mvmts_in_a_r; + let rounded_basis = round_d128_1e2(&unrounded_basis); + mvmt.cost_basis.set(-rounded_basis); + } + } + } + } + } + } + } + } +} + +fn perform_likekind_treatment_on_txn( + txn_num: u32, + settings: &LotProcessingChoices, + ars: &HashMap, + raw_acct_map: &HashMap, + acct_map: &HashMap, + txns_map: &HashMap, +) { + + let txn = txns_map.get(&txn_num).unwrap(); + let tx_type = txn.transaction_type(ars, raw_acct_map, acct_map); + + let og_ar = ars.get(&txn.action_record_idx_vec.first().unwrap()).unwrap(); + let ic_ar = ars.get(&txn.action_record_idx_vec.last().unwrap()).unwrap(); + let og_acct = acct_map.get(&og_ar.account_key).unwrap(); + let ic_acct = acct_map.get(&ic_ar.account_key).unwrap(); + let raw_og_acct = raw_acct_map.get(&og_acct.raw_key).unwrap(); + let raw_ic_acct = raw_acct_map.get(&ic_acct.raw_key).unwrap(); + + fn both_are_non_home_curr(raw_og_acct: &RawAccount, raw_ic_acct: &RawAccount, settings: &LotProcessingChoices) -> bool { + let og_is_home_curr = raw_og_acct.is_home_currency(&settings.home_currency); + let ic_is_home_curr = raw_ic_acct.is_home_currency(&settings.home_currency); + let both_are_non_home_curr = !ic_is_home_curr && !og_is_home_curr; + both_are_non_home_curr + } + + match tx_type { + TxType::Exchange => { + if both_are_non_home_curr(raw_og_acct, raw_ic_acct, settings) { + let mut sum_of_outgoing_cost_basis_in_ar = d128!(0); + for ar_num in txn.action_record_idx_vec.iter() { + let ar = ars.get(ar_num).unwrap(); + let movements = ar.get_mvmts_in_ar(acct_map, txns_map); + for mvmt in movements.iter() { + + let polarity = ar.direction(); + + let mvmt_copy = mvmt.clone(); + let borrowed_mvmt = mvmt_copy.clone(); + + match polarity { + Polarity::Outgoing => { + let cb = borrowed_mvmt.cost_basis.get(); + sum_of_outgoing_cost_basis_in_ar += cb; + mvmt.proceeds.set(-cb); + } + Polarity::Incoming => { + let ratio_of_amt_to_incoming_mvmts_in_a_r = + borrowed_mvmt.ratio_of_amt_to_incoming_mvmts_in_a_r; + let unrounded_basis = sum_of_outgoing_cost_basis_in_ar * + ratio_of_amt_to_incoming_mvmts_in_a_r; + let rounded_basis = round_d128_1e2(&unrounded_basis); + mvmt.cost_basis.set(-rounded_basis); + } + } + } + } + } + } + TxType::Flow => { + if txn.action_record_idx_vec.len() == 2 { + for ar_num in txn.action_record_idx_vec.iter() { + + let ar = ars.get(ar_num).unwrap(); + let movements = ar.get_mvmts_in_ar(acct_map, txns_map); + + let polarity = ar.direction(); + + for mvmt in movements.iter() { + + match polarity { + Polarity::Outgoing => {} + Polarity::Incoming => { + // Reminder: May need extra logic here if margin exchange trades get cost_basis and proceeds + mvmt.cost_basis.set(d128!(0)); + mvmt.proceeds.set(d128!(0)); + } + } + } + } + } + } + TxType::ToSelf => { + return + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1386e62 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,300 @@ +// Copyright (c) 2017-2019, scoobybejesus +// Redistributions must include the license: https://github.com/scoobybejesus/cryptools-rs/LEGAL.txt + +#![allow(dead_code)] +#![allow(unused_variables)] +#![allow(unused_assignments)] +// Note: the above are possibly temporary, to silence "x was not used" warnings. +// #[warn(dead_code)] is the default (same for unused_variables) + + +use std::ffi::OsString; +use structopt::StructOpt; +use std::path::PathBuf; +use std::process; +use std::io::{self, BufRead}; +use std::error::Error; + +mod account; +mod transaction; +mod core_functions; +mod import_accts_txns; +mod create_lots_mvmts; +mod import_cost_proceeds_etc; +mod user_choices; +mod export; +mod utils; +mod tests; + +use crate::user_choices::LotProcessingChoices; + + +#[derive(StructOpt, Debug)] +#[structopt(name = "cryptools-rs")] +struct Cli { + + /// File to be imported. (Currently, the only supported date format is %m/%d/%y.) + #[structopt(name = "file", parse(from_os_str))] + file_to_import: Option, + + /// Output directory for exported reports. + #[structopt(name = "output directory", short, long = "output", default_value = ".", parse(from_os_str))] + output_dir_path: PathBuf, + + /// When the 'accept' flag is set, this program will ultimately export CSV files to either the default or chosen 'output directory' unless this flag is set. + #[structopt(name = "suppress reports", short, long = "suppress")] + suppress_reports: bool, + + /// Cutoff date through which like-kind exchange treatment should be applied. + /// Please use %y-%m-%d (or %Y-%m-%d) format for like-kind cutoff date entry. + #[structopt(name = "like-kind cutoff date", short, long = "cutoff", parse(from_os_str))] + cutoff_date: Option, + + /// Inventory costing method (in terms of lot selection, i.e., LIFO, FIFO, etc.). There are currently four options (1 through 4). + #[structopt(name = "method", short, long, default_value = "1", parse(from_os_str), long_help = + r" 1. LIFO according to the order the lot was created. + 2. LIFO according to the basis date of the lot. + 3. FIFO according to the order the lot was created. + 4. FIFO according to the basis date of the lot. + ")] + inv_costing_method: OsString, + + /// Home currency (currency in which all resulting reports are denominated). (Only available as a command line setting.) + #[structopt(name = "home currency", short = "c", long = "currency", default_value = "USD", parse(from_os_str))] + home_currency: OsString, + + /// User is instructing the program to use the command line flags/options/arg they provide without confirming them during runtime. (Faster) + #[structopt(name = "accept args", short, long = "accept")] + accept_args: bool, +} + + +fn main() -> Result<(), Box> { + + let args = Cli::from_args(); + + println!( + " + Hello, crypto-folk! Welcome to cryptools-rs! + + This software will import your csv file's ledger of cryptocurrency transactions. + It will then process it by creating 'lots' and posting 'movements' to those lots. + Along the way, it will keep track of income, expenses, gains, and losses. + + Note: it is designed to import a full history. Gains and losses may be incorrect otherwise. + "); + + let input_file_path; + let output_dir_path = args.output_dir_path; + let should_export; + + let account_map; + let raw_acct_map; + let action_records_map; + let transactions_map; + let mut settings; + let like_kind_settings; + + let home_currency_choice = args.home_currency.into_string().expect("Home currency should be in the form of a ticker in CAPS."); + let costing_method_choice; + + + if !args.accept_args { + + shall_we_proceed(); + + fn shall_we_proceed() { + + println!("Shall we proceed? [Y/n] "); + + match _proceed() { + Ok(()) => {} + Err(err) => { println!("Failure to proceed. {}", err); process::exit(1); } + }; + + fn _proceed() -> Result<(), Box> { + + let mut input = String::new(); + let stdin = io::stdin(); + stdin.lock().read_line(&mut input)?; + + match input.trim().to_ascii_lowercase().as_str() { + "y" | "ye" | "yes" | "" => { Ok(()) }, + "n" | "no" => { println!("We have NOT proceeded..."); process::exit(0); }, + _ => { println!("Please respond with 'y' or 'n' (or 'yes' or 'no')."); _proceed() } + } + } + } + + if let Some(file) = args.file_to_import { + input_file_path = file + } else { + input_file_path = user_choices::choose_file_for_import(); + } + + costing_method_choice = LotProcessingChoices::choose_inventory_costing_method(); + + let lk_cutoff_date_opt_string; + + if let Some(lk_cutoff) = args.cutoff_date { + lk_cutoff_date_opt_string = Some(lk_cutoff.into_string().unwrap()) + } else { + lk_cutoff_date_opt_string = None + }; + + let (like_kind_election, like_kind_cutoff_date) = LotProcessingChoices::elect_like_kind_treatment(&lk_cutoff_date_opt_string); + + settings = LotProcessingChoices { + export_path: output_dir_path, + home_currency: home_currency_choice, + costing_method: costing_method_choice, + enable_like_kind_treatment: like_kind_election, + lk_cutoff_date_string: like_kind_cutoff_date, + }; + + let ( + account_map1, + raw_acct_map1, + action_records_map1, + transactions_map1, + like_kind_settings1 + ) = core_functions::import_and_process_final(input_file_path, &settings); + + account_map = account_map1; + raw_acct_map = raw_acct_map1; + action_records_map = action_records_map1; + transactions_map = transactions_map1; + like_kind_settings = like_kind_settings1; + + should_export = export_reports_to_output_dir(&mut settings); + + fn export_reports_to_output_dir(settings: &mut LotProcessingChoices) -> bool { + + println!("\nThe directory currently selected for exporting reports is: {}", settings.export_path.to_str().unwrap()); + + if &settings.export_path.to_str().unwrap() == &"." { + println!(" (A 'dot' denotes the default value: current working directory.)"); + } + println!("\nExport reports to selected directory? [Y/n/c] ('c' to 'change') "); + + let choice = match _export(settings) { + Ok(choice) => { choice } + Err(err) => { println!("Export choice error. {}", err); process::exit(1); } + }; + + fn _export(settings: &mut LotProcessingChoices) -> Result<(bool), Box> { + + let mut input = String::new(); + let stdin = io::stdin(); + stdin.lock().read_line(&mut input)?; + + match input.trim().to_ascii_lowercase().as_str() { + + "y" | "ye" | "yes" | "" => { println!("Creating reports now."); Ok(true) }, + "n" | "no" => { println!("Okay, no reports were created."); Ok(false) }, + "c" | "change" => { + let new_dir = user_choices::choose_export_dir(); + settings.export_path = PathBuf::from(new_dir); + println!("Creating reports now in newly chosen path."); + Ok(true) + }, + _ => { println!("Please respond with 'y', 'n', or 'c' (or 'yes' or 'no' or 'change')."); + _export(settings) + } + } + } + + choice + } + + } else { + + if let Some(file) = args.file_to_import { + input_file_path = file + } else { + println!("Flag to 'accept args' was set, but 'file' is missing, though it is a required field. Exiting."); + process::exit(0); + } + + let like_kind_election; + let like_kind_cutoff_date_string: String; + + if let Some(date) = args.cutoff_date { + like_kind_election = true; + like_kind_cutoff_date_string = date.into_string().unwrap(); + } else { + like_kind_election = false; + like_kind_cutoff_date_string = "1-1-1".to_string(); + }; + + match args.inv_costing_method.clone().into_string().expect("Invalid choice on costing method. Aborting.").trim() { + "1" | "2" | "3" | "4" => {} + _ => { println!("Invalid choice for inventory costing method. Exiting."); process::exit(0); } + } + + let costing_method_choice = LotProcessingChoices::inv_costing_from_cmd_arg(args.inv_costing_method.into_string().unwrap()); + + settings = LotProcessingChoices { + export_path: output_dir_path, + home_currency: home_currency_choice, + costing_method: costing_method_choice, + enable_like_kind_treatment: like_kind_election, + lk_cutoff_date_string: like_kind_cutoff_date_string, + }; + + let ( + account_map1, + raw_acct_map1, + action_records_map1, + transactions_map1, + like_kind_settings1 + ) = core_functions::import_and_process_final(input_file_path, &settings); + + account_map = account_map1; + raw_acct_map = raw_acct_map1; + action_records_map = action_records_map1; + transactions_map = transactions_map1; + like_kind_settings = like_kind_settings1; + + should_export = !args.suppress_reports; + } + + if should_export { + + export::_1_account_sums_to_csv( + &settings, + &raw_acct_map, + &account_map + ); + + export::_2_account_sums_nonzero_to_csv( + &account_map, + &settings, + &raw_acct_map + ); + + export::_5_transaction_mvmt_summaries_to_csv( + &settings, + &action_records_map, + &raw_acct_map, + &account_map, + &transactions_map + ); + + } + + // use tests::test; + // test::run_tests( + // &transactions_map, + // &action_records_map, + // &account_map + // ); + + + Ok(()) + // // export::transactions_to_csv(&transactions); + // // println!("\nReturned from `fn transactions_to_csv`. It worked!! Right?"); + + // export::accounts_to_csv(&accounts); + // println!("\nReturned from `fn accounts_to_csv`. It worked!! Right?"); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 0000000..bd93356 --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1,4 @@ +// Copyright (c) 2017-2019, scoobybejesus +// Redistributions must include the license: https://github.com/scoobybejesus/cryptools-rs/LEGAL.txt + +pub mod test; diff --git a/src/tests/test.rs b/src/tests/test.rs new file mode 100644 index 0000000..3c61e88 --- /dev/null +++ b/src/tests/test.rs @@ -0,0 +1,243 @@ +// Copyright (c) 2017-2019, scoobybejesus +// Redistributions must include the license: https://github.com/scoobybejesus/cryptools-rs/LEGAL.txt + +use std::fs; +use std::collections::{HashMap}; + +use decimal::d128; + +use crate::account::{Account}; +use crate::transaction::{Transaction, ActionRecord}; +use crate::utils::*; + +pub fn run_tests( + transactions_map: &HashMap, + action_records_map: &HashMap, + account_map: &HashMap, +) { + + compare_movements_across_implementations( + &transactions_map, + &action_records_map, + &account_map + ); + + do_mvmts_know_what_lot_they_are_in(&account_map); + + test_action_records_amts_vs_mvmt_amts( + &transactions_map, + &action_records_map, + &account_map + ); + + test_quantize_from_incoming_multiple_lots_fn(d128!(20), d128!(200), d128!(50)); + test_quantize_from_incoming_multiple_lots_fn(d128!(1), d128!(6), d128!(1234567.1234567896)); + // test_dec_rounded("123456789.123456789"); + // test_dec_rounded("123456.123456"); + // test_dec_rounded("1234567891234.1234567891234"); + // test_dec_rounded_1e8("123456789.123456789"); + // test_dec_rounded_1e8("123456.123456"); + // test_dec_rounded_1e8("1234567891234.1234567891234"); + // test_dec_rounded_1e2("123456789.123456789"); + // test_dec_rounded_1e2("123456.123456"); + // test_dec_rounded_1e2("1234567891234.1234567891234"); + +} + +fn compare_movements_across_implementations( + transactions_map: &HashMap, + action_records_map: &HashMap, + account_map: &HashMap, +) { + // THIS BIG ASS TEST CREATES TWO FILES TO COMPARE actionrecords.movements with ar.get_mvmts_in_ar() + // DELETE THIS WHEN movements FIELD OF ActionRecords IS DELETED + + let mut line: String = "".to_string(); + + for tx_num in 1..=transactions_map.len() { + let txn_num = tx_num as u32; + line += &("Transaction ".to_string() + &txn_num.to_string() + &"\n".to_string()); + let txn = transactions_map.get(&(txn_num)).unwrap(); + for (idx, ar_num) in txn.action_record_idx_vec.iter().enumerate() { + line += &("ActionRecord index: ".to_string() + &idx.to_string()); + let ar = action_records_map.get(&(ar_num)).unwrap(); + line += &( + " with actionRecord key: ".to_string() + + &ar_num.to_string() + + " with amount: " + + &ar.amount.to_string() + &"\n".to_string() + ); + let mvmts = ar.get_mvmts_in_ar(&account_map, &transactions_map); + let mut amts = d128!(0); + for mvmt in mvmts { + amts += mvmt.amount; + line += &("Movement ".to_string() + + &mvmt.amount.to_string() + + &" on ".to_string() + + &mvmt.date_as_string + + &"\n".to_string()); + } + line += &("Amount total: ".to_string() + &amts.to_string() + &"\n".to_string()); + if amts - ar.amount != d128!(0) { + line += &("&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&".to_string()); + println!("Movement amounts via get_mvmts_in_ar() different from actionRecord.amount. Aborting."); + use std::process::exit; exit(1) + }; + } + } + fs::write("/tmp/foo", &line).expect("Unable to write file"); + + let mut line2: String = "".to_string(); + + for tx_num in 1..=transactions_map.len() { + let txn_num = tx_num as u32; + line2 += &("Transaction ".to_string() + &txn_num.to_string() + &"\n".to_string()); + let txn = transactions_map.get(&(txn_num)).unwrap(); + for (idx, ar_num) in txn.action_record_idx_vec.iter().enumerate() { + line2 += &("ActionRecord index: ".to_string() + &idx.to_string()); + let ar = action_records_map.get(&(ar_num)).unwrap(); + line2 += &( + " with actionRecord key: ".to_string() + + &ar_num.to_string() + + " with amount: " + + &ar.amount.to_string() + + &"\n".to_string() + ); + // let mvmts = ar.get_mvmts_in_ar(&account_map); + let mut amts = d128!(0); + for mvmt in ar.movements.borrow().iter() { + amts += mvmt.amount; + line2 += &("Movement ".to_string() + + &mvmt.amount.to_string() + + &" on ".to_string() + + &mvmt.date_as_string + + &"\n".to_string()); + } + line2 += &("Amount total: ".to_string() + &amts.to_string() + &"\n".to_string()); + if amts - ar.amount != d128!(0) { + line2 += &("&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&*&".to_string()); + println!("Movement amounts via ar.movements different from actionRecord.amount. Aborting."); + use std::process::exit; exit(1) + }; + } + } + fs::write("/tmp/foo2", &line2).expect("Unable to write file"); +} + +fn do_mvmts_know_what_lot_they_are_in(account_map: &HashMap,) { + + for (acct_num, acct) in account_map.iter() { + for lot in acct.list_of_lots.borrow().iter() { + for mvmt in lot.movements.borrow().iter() { + if mvmt.lot_num != lot.lot_number { + println!("ERROR: For txn {} on {}, movement.lot_num != lot.lot_number.", + mvmt.transaction_key, mvmt.date); + } + } + } + } +} + +pub fn test_action_records_amts_vs_mvmt_amts( + transactions_map: &HashMap, + action_records_map: &HashMap, + account_map: &HashMap, +) { + + let mut mvmt_amt_acct_lot: d128 = d128!(0); + let mut mvmt_amt_ar: d128 = d128!(0); + let mut ar_amts: d128 = d128!(0); + + for tx_num in 1..=transactions_map.len() { + + let txn_num = tx_num as u32; + let txn = transactions_map.get(&(txn_num)).unwrap(); + + for ar_num in &txn.action_record_idx_vec { + + let ar = action_records_map.get(&(ar_num)).unwrap(); + let mvmts = ar.get_mvmts_in_ar(&account_map, &transactions_map); + for mvmt in mvmts { + mvmt_amt_ar += mvmt.amount + } + } + } + + for acct_num in 1..=account_map.len() { + + let acct = account_map.get(&(acct_num as u16)).unwrap(); + + for lot in acct.list_of_lots.borrow().iter() { + for mvmt in lot.movements.borrow().iter() { + mvmt_amt_acct_lot += mvmt.amount + } + } + } + + for tx_num in 1..=transactions_map.len() { + + let txn_num = tx_num as u32; + let txn = transactions_map.get(&(txn_num)).unwrap(); + + for ar_num in &txn.action_record_idx_vec { + let ar = action_records_map.get(&(ar_num)).unwrap(); + ar_amts += ar.amount + } + } + + println!(" \n\t\t\tActionRecord amounts pulled from iterating over transactions: {} + \n\t\t\tMovement amounts pulled from actionRecords dynamically after: {} + \n\t\t\tMovement amounts where movements are recorded directly in lots: {}\n", + ar_amts, + mvmt_amt_ar, + mvmt_amt_acct_lot + ); +} + +fn test_quantize_from_incoming_multiple_lots_fn ( + outgoing_mvmt_amt: d128, + outgoing_ar_amt: d128, + incoming_ar_amt: d128, +) { + let rounded_example = d128::from(1).scaleb(d128::from(-8)); + // + println!("Og mvmt amt: {:?}, Og ar amt: {:?}, Ic ar amt: {:?}", outgoing_mvmt_amt, outgoing_ar_amt, incoming_ar_amt); + let ratio_of_outgoing_to_total_ar = outgoing_mvmt_amt / outgoing_ar_amt; // Negative divided by negative is positive + println!("ratio_of_outgoing: {:.20}", ratio_of_outgoing_to_total_ar); + let tentative_incoming_amt = ratio_of_outgoing_to_total_ar * incoming_ar_amt; + println!("tentative_inc_amt: {:.20}", tentative_incoming_amt); + let corresponding_incoming_amt = tentative_incoming_amt.quantize(rounded_example); + println!("corresponding_inc_amt: {}", corresponding_incoming_amt); +} + +// Yields: +// Og mvmt amt: 20, Og ar amt: 200, Ic ar amt: 50 +// ratio_of_outgoing: 0.1 +// tentative_inc_amt: 5.0 +// corresponding_inc_amt: 5.00000000 +// Og mvmt amt: 1, Og ar amt: 6, Ic ar amt: 1234567.1234567896 +// ratio_of_outgoing: 0.166666666666666666 +// tentative_inc_amt: 205761.1872427982666 +// corresponding_inc_amt: 205761.18724280 + +fn test_dec_rounded(random_float_string: &str) { + let places_past_decimal = d128!(8); + let amt = random_float_string.parse::().unwrap(); + let amt2 = round_d128_generalized(&amt, places_past_decimal); + println!("Float into d128: {:?}; d128 rounded to {}: {:?}", amt, places_past_decimal, amt2); + // Results of this test suggest that quantize() is off by one. round_d128_1e8() was adjusted accordingly. +} + +fn test_dec_rounded_1e8(random_float_string: &str) { + let amt = random_float_string.parse::().unwrap(); + let amt2 = round_d128_1e8(&amt); + println!("Float into d128: {:?}; d128 rounded to 8 places: {:?}", amt, amt2); + // Results of this test suggest that quantize() is off by one. round_d128_1e8() was adjusted accordingly. +} + +fn test_dec_rounded_1e2(random_float_string: &str) { + let amt = random_float_string.parse::().unwrap(); + let amt2 = round_d128_1e2(&amt); + println!("String into d128: {:?}; d128 rounded to 2 places: {:?}", amt, amt2); + // Results of this test suggest that quantize() is off by one. round_d128_1e8() was adjusted accordingly. +} diff --git a/src/transaction.rs b/src/transaction.rs new file mode 100644 index 0000000..07c2594 --- /dev/null +++ b/src/transaction.rs @@ -0,0 +1,313 @@ +// Copyright (c) 2017-2019, scoobybejesus +// Redistributions must include the license: https://github.com/scoobybejesus/cryptools-rs/LEGAL.txt + +use std::rc::{Rc}; +use std::cell::{RefCell}; +use std::process::exit; +use std::fmt; +use std::collections::{HashMap}; + +use decimal::d128; +use chrono::NaiveDate; +use serde_derive::{Serialize, Deserialize}; + +use crate::user_choices::LotProcessingChoices; +use crate::account::{Account, Movement, RawAccount}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Transaction { + pub tx_number: u32, // Does NOT start at zero. First txn is 1. + pub date_as_string: String, + pub date: NaiveDate, + pub memo: String, + pub proceeds: f32, + pub action_record_idx_vec: Vec, +} + +impl Transaction { + + pub fn transaction_type( + &self, + ars: &HashMap, + raw_acct_map: &HashMap, + acct_map: &HashMap, + ) -> TxType { + + if self.action_record_idx_vec.len() == 1 { + TxType::Flow + } + else if self.action_record_idx_vec.len() == 2 { + // This exercise of splitting the strings is because of margin accounts, where BTC borrowed to buy XMR would reflect as BTC_xmr + let first_ar = ars.get(&self.action_record_idx_vec[0]).unwrap(); + let second_ar = ars.get(&self.action_record_idx_vec[1]).unwrap(); + let first_acct = acct_map.get(&first_ar.account_key).unwrap(); + let second_acct = acct_map.get(&second_ar.account_key).unwrap(); + let ar1_raw_acct = raw_acct_map.get(&first_acct.raw_key).unwrap(); + let ar2_raw_acct = raw_acct_map.get(&second_acct.raw_key).unwrap(); + let ar1_ticker_full = ar1_raw_acct.ticker.clone(); + let ar2_ticker_full = ar2_raw_acct.ticker.clone(); + let ar1_ticker_comp: Vec<&str> = ar1_ticker_full.split('_').collect(); + let ar2_ticker_comp: Vec<&str> = ar2_ticker_full.split('_').collect(); + let ar1_ticker = ar1_ticker_comp[0]; + let ar2_ticker = ar2_ticker_comp[0]; + + if first_ar.direction() == second_ar.direction() { + println!("Program exiting. Found transaction with two actionRecords with the same polarity: {:?}", self); exit(1); + } + if ar1_ticker == ar2_ticker { + if ar1_raw_acct.is_margin != ar2_raw_acct.is_margin { + TxType::Flow + } + else { + TxType::ToSelf + } + } + else { + TxType::Exchange + } + } + else if self.action_record_idx_vec.len() > 2 { + println!("Program exiting. Found transaction with too many actionRecords: {:?}", self); exit(1); + } + else { + println!("Program exiting. Found transaction with no actionRecords: {:?}", self); exit(1); + } + } + + pub fn marginness( + &self, + ars: &HashMap, + raw_acct_map: &HashMap, + acct_map: &HashMap + ) -> TxHasMargin { + + if self.action_record_idx_vec.len() == 1 { + let ar = ars.get(&self.action_record_idx_vec[0]).unwrap(); + let acct = acct_map.get(&ar.account_key).unwrap(); + let raw_acct = raw_acct_map.get(&acct.raw_key).unwrap(); + if raw_acct.is_margin { + TxHasMargin::OneAR + } else { + TxHasMargin::NoARs + } + } else { + assert_eq!(self.action_record_idx_vec.len(),2); + let first_ar = ars.get(&self.action_record_idx_vec[0]).unwrap(); + let second_ar = ars.get(&self.action_record_idx_vec[1]).unwrap(); + + let first_acct = acct_map.get(&first_ar.account_key).unwrap(); + let second_acct = acct_map.get(&second_ar.account_key).unwrap(); + + let first_raw_acct = &raw_acct_map.get(&first_acct.raw_key).unwrap(); + let second_raw_acct = &raw_acct_map.get(&second_acct.raw_key).unwrap(); + + if first_raw_acct.is_margin { + if second_raw_acct.is_margin {TxHasMargin::TwoARs} else {TxHasMargin::OneAR} + } else if second_raw_acct.is_margin {TxHasMargin::OneAR} else {TxHasMargin::NoARs} + } + } + + pub fn get_base_and_quote_raw_acct_keys( + &self, + ars: &HashMap, + raw_accts: &HashMap, + acct_map: &HashMap + ) -> (u16, u16) { + + assert_eq!(self.transaction_type(ars, raw_accts, acct_map), TxType::Exchange, + "This can only be called on exchange transactions."); + + let first_ar = ars.get(&self.action_record_idx_vec[0]).unwrap(); + let second_ar = ars.get(&self.action_record_idx_vec[1]).unwrap(); + let first_acct = acct_map.get(&first_ar.account_key).unwrap(); + let second_acct = acct_map.get(&second_ar.account_key).unwrap(); + let first_acct_raw_key = first_acct.raw_key; + let second_acct_raw_key = second_acct.raw_key; + let first_raw_acct = raw_accts.get(&first_acct_raw_key).unwrap(); + let second_raw_acct = raw_accts.get(&second_acct_raw_key).unwrap(); + + assert_eq!(first_raw_acct.is_margin, true, "First actionrecord wasn't a margin account. Both must be."); + assert_eq!(second_raw_acct.is_margin, true, "Second actionrecord wasn't a margin account. Both must be."); + + let quote: u16; + let base: u16; + + if first_raw_acct.ticker.contains('_') { + quote = first_acct_raw_key; + base = second_acct_raw_key; + (base, quote) + } else if second_raw_acct.ticker.contains('_') { + base = first_acct_raw_key; + quote = second_acct_raw_key; + (base, quote) + } else { + println!("{}", VariousErrors::MarginNoUnderbar); use std::process::exit; exit(1) + } + } + + pub fn get_outgoing_exchange_and_flow_mvmts( + &self, + settings: &LotProcessingChoices, + ars: &HashMap, + raw_acct_map: &HashMap, + acct_map: &HashMap, + txns_map: &HashMap, + ) -> Vec> { + + let mut flow_or_outgoing_exchange_movements = [].to_vec(); + + for ar_num in self.action_record_idx_vec.iter() { + + let ar = ars.get(ar_num).unwrap(); + let acct = acct_map.get(&ar.account_key).unwrap(); + let raw_acct = raw_acct_map.get(&acct.raw_key).unwrap(); + + if !raw_acct.is_home_currency(&settings.home_currency) & !raw_acct.is_margin { + + let movements = ar.get_mvmts_in_ar(acct_map, txns_map); + + match self.transaction_type(ars, raw_acct_map, acct_map) { + TxType::Exchange => { + if Polarity::Outgoing == ar.direction() { + for mvmt in movements.iter() { + flow_or_outgoing_exchange_movements.push(mvmt.clone()); + } + } + } + TxType::Flow => { + for mvmt in movements.iter() { + flow_or_outgoing_exchange_movements.push(mvmt.clone()); + } + } + _ => {} + } + } + } + flow_or_outgoing_exchange_movements + } +} + +#[derive(Clone, Debug)] +pub struct ActionRecord { + pub account_key: u16, + pub amount: d128, + pub tx_key: u32, + pub self_ar_key: u32, + pub movements: RefCell>>, +} + +impl ActionRecord { + + pub fn direction(&self) -> Polarity { + if self.amount < d128!(0.0) { Polarity::Outgoing} + else { Polarity::Incoming } + } + + pub fn is_quote_acct_for_margin_exch( + &self, + raw_accts: &HashMap, + acct_map: &HashMap + ) -> bool { + + let acct = acct_map.get(&self.account_key).unwrap(); + let raw_acct = raw_accts.get(&acct.raw_key).unwrap(); + raw_acct.ticker.contains('_') + } + + pub fn get_mvmts_in_ar( + &self, + acct_map: &HashMap, + txns_map: &HashMap, + ) -> Vec> { + + let polarity = Self::direction(self); + let txn = txns_map.get(&self.tx_key).unwrap(); + let mut movements_in_ar = [].to_vec(); + let acct = acct_map.get(&self.account_key).unwrap(); + + for lot in acct.list_of_lots.borrow().iter() { + for mvmt in lot.movements.borrow().iter() { + if (mvmt.date) <= txn.date { + if mvmt.action_record_key == self.self_ar_key { + // if polarity == Polarity::Incoming{ + // movements_in_ar.push(mvmt.clone()) + // } else { + movements_in_ar.insert(0, mvmt.clone()) + // } + // ^^ leaving that ugliness for this commit on purpose + } + } + } + } + movements_in_ar + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum TxType { + Exchange, + ToSelf, + Flow, +} + +impl fmt::Display for TxType { + + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + TxType::Exchange => write!(f, "Exchange"), + TxType::ToSelf => write!(f, "ToSelf"), + TxType::Flow => write!(f, "Flow"), + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum TxHasMargin { + NoARs, + OneAR, + TwoARs, +} + +impl fmt::Display for TxHasMargin { + + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + TxHasMargin::NoARs => write!(f, "No actionrecord is a margin account"), + TxHasMargin::OneAR => write!(f, "One actionrecord is a margin account"), + TxHasMargin::TwoARs => write!(f, "Two actionrecords are margin accounts"), + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Polarity { + Outgoing, + Incoming, +} + +impl fmt::Display for Polarity { + + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Polarity::Outgoing => write!(f, "Outgoing"), + Polarity::Incoming => write!(f, "Incoming"), + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum VariousErrors { + MarginNoUnderbar, +} + +impl fmt::Display for VariousErrors { + + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + VariousErrors::MarginNoUnderbar => write!(f, + "Neither account ticker contained an underbar '_', so the quote account couldn't be determined. + For example, for the 'USD/EUR' pair, USD is the base account and EUR is the quote account. + In order for this software to function correctly, the quote ticker should be denoted as 'EUR_usd'.") + } + } +} diff --git a/src/user_choices.rs b/src/user_choices.rs new file mode 100644 index 0000000..1e7a9e1 --- /dev/null +++ b/src/user_choices.rs @@ -0,0 +1,292 @@ +// Copyright (c) 2017-2019, scoobybejesus +// Redistributions must include the license: https://github.com/scoobybejesus/cryptools-rs/LEGAL.txt + +use std::error::Error; +use std::io::{self, BufRead}; +use std::process; +use std::path::PathBuf; + +use chrono::NaiveDate; +use rustyline::completion::{Completer, FilenameCompleter, Pair}; +use rustyline::{CompletionType, Config, Context, EditMode, Editor, Helper}; +use rustyline::config::OutputStreamType; +use rustyline::hint::{Hinter}; +use rustyline::error::ReadlineError; +use rustyline::highlight::{Highlighter}; +use structopt::StructOpt; + +use crate::utils; + + +pub fn choose_file_for_import() -> PathBuf { + + println!("Please input a file (absolute or relative path) to import: "); + + // PathBuf::from("/Users/scoob/Documents/Repos/cryptools-rs/private/RawTxForImport-pycleaned.csv") + + let file_str = _get_path(); + PathBuf::from(file_str.unwrap()) +} + +pub fn choose_export_dir() -> PathBuf { + + println!("Please input a file path for exports: "); + + // PathBuf::from("/Users/scoob/Documents/Testing/rust_exports/") + + let file_str = _get_path(); + PathBuf::from(file_str.unwrap()) +} + +fn _get_path() -> Result<(String), Box> { + + struct MyHelper { + completer: FilenameCompleter, + colored_prompt: String, + } + + impl Completer for MyHelper { + + type Candidate = Pair; + + fn complete( + &self, + line: &str, + pos: usize, + ctx: &Context<'_>, + ) -> Result<(usize, Vec), ReadlineError> { + self.completer.complete(line, pos, ctx) + } + } + + impl Hinter for MyHelper {} + impl Highlighter for MyHelper {} + impl Helper for MyHelper {} + + let h = MyHelper { + completer: FilenameCompleter::new(), + colored_prompt: "".to_owned(), + }; + + let config = Config::builder() + .history_ignore_space(true) + .completion_type(CompletionType::Circular) + .edit_mode(EditMode::Vi) + .output_stream(OutputStreamType::Stdout) + .build(); + + let count = 1; + let mut rl = Editor::with_config(config); + let p = format!("{}> ", count); + rl.set_helper(Some(h)); + rl.helper_mut().unwrap().colored_prompt = format!("\x1b[1;32m{}\x1b[0m", p); + let readline = rl.readline(">> "); + + match readline { + Ok(line) => { + rl.add_history_entry(line.as_str()); + println!(""); + Ok(line) + }, + Err(err) => { + println!("Error during Rustyline: {:?}", err); + process::exit(1) + } + } +} + +#[derive(Clone, Debug, PartialEq, StructOpt)] +pub enum InventoryCostingMethod { + /// 1. LIFO according to the order the lot was created. + LIFObyLotCreationDate, + /// 2. LIFO according to the basis date of the lot. + LIFObyLotBasisDate, + /// 3. FIFO according to the order the lot was created. + FIFObyLotCreationDate, + /// 4. FIFO according to the basis date of the lot. + FIFObyLotBasisDate, +} + +// impl std::convert::From for InventoryCostingMethod { +// fn from(osstr: OsStr) -> InventoryCostingMethod { +// let osstring1 = OsString::from(Box::); +// let new_string = osstr.into_string().expect("Invalid input. Could not convert to string."); +// let method = match new_string.trim() { +// "1" => InventoryCostingMethod::LIFObyLotCreationDate, +// "2" => InventoryCostingMethod::LIFObyLotBasisDate, +// "3" => InventoryCostingMethod::FIFObyLotCreationDate, +// "4" => InventoryCostingMethod::FIFObyLotBasisDate, +// _ => { println!("Invalid choice. Could not convert."); process::exit(1) +// } +// }; +// method +// } +// } + +pub struct LotProcessingChoices { + pub export_path: PathBuf, + pub home_currency: String, + pub enable_like_kind_treatment: bool, + pub costing_method: InventoryCostingMethod, + pub lk_cutoff_date_string: String, +} + +impl LotProcessingChoices { + + pub fn choose_inventory_costing_method() -> InventoryCostingMethod { + + println!("Choose the lot inventory costing method. [Default: 1]"); + println!("1. LIFO according to the order the lot was created."); + println!("2. LIFO according to the basis date of the lot."); + println!("3. FIFO according to the order the lot was created."); + println!("4. FIFO according to the basis date of the lot."); + + let method = match _costing_method() { + Ok(x) => {x}, + Err(err) => { process::exit(1) } + }; + + fn _costing_method() -> Result<(InventoryCostingMethod), Box> { + + let mut input = String::new(); + let stdin = io::stdin(); + stdin.lock().read_line(&mut input).expect("Failed to read stdin"); + + match input.trim() { // Without .trim(), there's a hidden \n or something preventing the match + "1" | "" => Ok(InventoryCostingMethod::LIFObyLotCreationDate), + "2" => Ok(InventoryCostingMethod::LIFObyLotBasisDate), + "3" => Ok(InventoryCostingMethod::FIFObyLotCreationDate), + "4" => Ok(InventoryCostingMethod::FIFObyLotBasisDate), + _ => { println!("Invalid choice. Please enter a valid number."); _costing_method() } + } + } + + method + } + + pub fn inv_costing_from_cmd_arg(arg: String) -> InventoryCostingMethod { + + let method = match _costing_method(arg) { + Ok(x) => {x}, + Err(err) => { process::exit(1) } + }; + + fn _costing_method(input: String) -> Result<(InventoryCostingMethod), Box> { + + match input.trim() { // Without .trim(), there's a hidden \n or something preventing the match + "1" => Ok(InventoryCostingMethod::LIFObyLotCreationDate), + "2" => Ok(InventoryCostingMethod::LIFObyLotBasisDate), + "3" => Ok(InventoryCostingMethod::FIFObyLotCreationDate), + "4" => Ok(InventoryCostingMethod::FIFObyLotBasisDate), + _ => { println!("Invalid choice. Please enter a valid number."); _costing_method(input) } + } + } + + method + } + + pub fn elect_like_kind_treatment(cutoff_date_arg: &Option) -> (bool, String) { + + let election: bool; + let date: String; + + if cutoff_date_arg.is_some() { + + let provided_date = NaiveDate::parse_from_str(&cutoff_date_arg.clone().unwrap(), "%y-%m-%d") + .unwrap_or(NaiveDate::parse_from_str(&cutoff_date_arg.clone().unwrap(), "%Y-%m-%d") + .expect("Date entered as -c command line arg has an incorrect format.")); + + println!("\nUse like-kind exchange treatment through {}? [Y/n/c] ('c' to 'change') ", provided_date); + + let (election, date) = match _elect_like_kind_arg(&cutoff_date_arg, provided_date) { + Ok(x) => {x}, + Err(err) => { println!("Fatal error in fn elect_like_kind_treatment()."); process::exit(1) } + }; + + fn _elect_like_kind_arg(cutoff_date_arg: &Option, provided_date: NaiveDate) -> Result<(bool, String), Box> { + + let mut input = String::new(); + let stdin = io::stdin(); + stdin.lock().read_line(&mut input)?; + + + match input.trim().to_ascii_lowercase().as_str() { + "y" | "ye" | "yes" | "" => { + println!(" Using like-kind treatment through {}.\n", provided_date); + Ok( (true, cutoff_date_arg.clone().unwrap()) ) + }, + "n" | "no" => { println!(" Proceeding without like-kind treatment.\n"); + Ok( (false, "1-1-1".to_string()) ) + }, + "c" | "change" => { + println!("Please enter your desired like-kind exchange treatment cutoff date."); + println!(" You must use the format %y-%m-%d (e.g., 2017-12-31, 17-12-31, and 9-6-1 are all acceptable).\n"); + let mut input = String::new(); + let stdin = io::stdin(); + stdin.lock().read_line(&mut input)?; + utils::trim_newline(&mut input); + let newly_chosen_date = NaiveDate::parse_from_str(&input, "%y-%m-%d") + .unwrap_or(NaiveDate::parse_from_str(&input, "%Y-%m-%d") + .expect("Date entered has an incorrect format. Program must abort.")); + // TODO: figure out how to make this fail gracefully and let the user input the date again + println!(" Using like-kind treatment through {}.\n", newly_chosen_date); + Ok( (true, input) ) + }, + _ => { + println!("Please respond with 'y', 'n', or 'c' (or 'yes' or 'no' or 'change')."); + _elect_like_kind_arg(&cutoff_date_arg, provided_date) + } + } + } + + return (election, date) + + } else { + + println!("\nContinue without like-kind exchange treatment? [Y/n] "); + + let (election, date) = match _no_elect_like_kind_arg() { + Ok(x) => {x}, + Err(err) => { println!("Fatal error in like_kind selection. Perhaps incorrect date format."); process::exit(1) } + }; + + fn _no_elect_like_kind_arg() -> Result<(bool, String), Box> { + + let mut input = String::new(); + let stdin = io::stdin(); + stdin.lock().read_line(&mut input)?; + + match input.trim().to_ascii_lowercase().as_str() { + "y" | "ye" | "yes" | "" => { println!(" Proceeding without like-kind treatment.\n"); + Ok( (false, "1-1-1".to_string()) ) + }, + "n" | "no" => { + println!("Please enter your desired like-kind exchange treatment cutoff date."); + println!(" You must use the format %y-%m-%d (e.g., 2017-12-31, 17-12-31, and 9-6-1 are all acceptable).\n"); + let mut input = String::new(); + let stdin = io::stdin(); + stdin.lock().read_line(&mut input)?; + utils::trim_newline(&mut input); + + let newly_chosen_date = NaiveDate::parse_from_str(&input, "%y-%m-%d") + .unwrap_or(NaiveDate::parse_from_str(&input, "%Y-%m-%d") + .expect("Date entered has an incorrect format. Program must abort.")); + // TODO: figure out how to make this fail gracefully and let the user input the date again + println!(" Using like-kind treatment through {}.\n", newly_chosen_date); + + Ok( (true, input) ) + }, + _ => { println!("Please respond with 'y' or 'n' (or 'yes' or 'no')."); _no_elect_like_kind_arg() } + } + } + + return (election, date) + } + } +} + +#[derive(Clone)] +pub struct LikeKindSettings { + pub like_kind_cutoff_date: NaiveDate, + pub like_kind_basis_date_preserved: bool, +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..c7aace1 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,34 @@ +// Copyright (c) 2017-2019, scoobybejesus +// Redistributions must include the license: https://github.com/scoobybejesus/cryptools-rs/LEGAL.txt + +use decimal::d128; + + +pub fn round_d128_generalized(to_round: &d128, places_past_decimal: d128) -> d128 { + let rounded: d128 = ((to_round * d128!(10).scaleb(places_past_decimal)).quantize(d128!(1e1))) / d128!(10).scaleb(places_past_decimal); + rounded//.reduce() +} +pub fn round_d128_1e2(to_round: &d128) -> d128 { + let rounded: d128 = ((to_round * d128!(10).scaleb(d128!(2))).quantize(d128!(1e1))) / d128!(10).scaleb(d128!(2)); + rounded//.reduce() +} + +pub fn round_d128_1e8(to_round: &d128) -> d128 { + let rounded: d128 = ((to_round * d128!(10).scaleb(d128!(8))).quantize(d128!(1e1))) / d128!(10).scaleb(d128!(8)); + rounded//.reduce() + // Note: quantize() rounds the number to the right of decimal and keeps it, discarding the rest to the right (it appears). See test. + // In other words, it's off by one. If you raise 0.123456789 by 10e8, quantize to 1e1 (which is 10), it'll get 12345678.9, round off to 12345679, and end up .12345679 + // If you quantize the same number to 1e2 (which is 100), it starts back toward the left, so it'll get 12345678.9, round off to 12345680 + // If you quantize the same number to 1e3 (which is 1000), it starts back toward the left, so it'll get 12345678.9, round off to 12345700 + // As you can see, the quantize is off by one. Quantizing to 10 rounds off the nearest one. Quantizing to 100 rounds off to nearest 10, etc. +} + + +pub fn trim_newline(s: &mut String) { + if s.ends_with('\n') { + s.pop(); + if s.ends_with('\r') { + s.pop(); + } + } +}