diff --git a/Cargo.toml b/Cargo.toml index 53d2a87..0636c95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cryptools" -version = "0.9.4" +version = "0.10.0" authors = ["scoobybejesus "] edition = "2018" description = "Command-line utility for processing cryptocurrency transactions into 'lots' and 'movements'." diff --git a/InputFile_CSV.md b/InputFile_CSV.md index d6b6307..2591232 100644 --- a/InputFile_CSV.md +++ b/InputFile_CSV.md @@ -188,8 +188,9 @@ Until "spot" funds are spent to pay off the margin loans, it's simply an [unreco * **txDate**: As a default, this parser expects a format of `MM-dd-YY` or `MM-dd-YYYY`. The ISO 8601 date format (`YYYY-MM-dd` or `YY-MM-dd` both work) may be indicated by setting the environment variable `ISO_DATE` to `1` or `true`. -The hyphen, slash, or period delimiters (`-`, `/`, or `.`) may be indicated -by setting `DATE_SEPARATOR` to `h`, `s`, or `p`, respectively (hyphen, `-`, is default). +The hyphen date separator character (`-`) is the default. The slash date separator character (`/`) may be indicated +by setting the `DATE_SEPARATOR_IS_SLASH` environment variable (or in .env file) to `1` or `true`, +or by passing the `date_separator_is_slash` command line flag. * **proceeds**: This is can be any **positive** number that will parse into a floating point 32-bit number, as long as the **decimal separator** is a **period**. diff --git a/examples/.env.example b/examples/.env.example index 68c7215..cded985 100644 --- a/examples/.env.example +++ b/examples/.env.example @@ -2,18 +2,20 @@ ## ## If the defaults below are not suitable, copy this .env.example into a new .env file, ## uncomment the respective enviroment variable, and set the value according to your needs. +## Alternatively, command line flags are available for ISO_DATE and DATE_SEPARATOR_SWITCH. +## Command line flags will override enviroment variables. # Setting to `TRUE` or `1` will cause the program to expect the `txDate` field in the `file_to_import` to use -# the format YYYY-MM-dd or YY-MM-dd (or YYYY/MM/dd or YY/MM/dd, depending on the date-separator option) +# the format YYYY-MM-dd or YY-MM-dd (or YYYY/MM/dd or YY/MM/dd, depending on the date-separator character) # instead of the default US-style MM-dd-YYYY or MM-dd-YY (or MM/dd/YYYY or MM/dd/YY, depending on the # date separator option). # (bool; default is FALSE/0) #ISO_DATE=0 -# Choose "h", "s", or "p" for hyphen, slash, or period (i.e., "-", "/", or ".") to indicate the separator -# character used in the `file_to_import` `txDate` column (i.e. 2017/12/31, 2017-12-31, or 2017.12.31). -# (String; default is 'h') -#DATE_SEPARATOR=h +# Switches the default date separator from hyphen to slash (i.e., from "-" to "/") to indicate the separator +# character used in the file_to_import txDate column (i.e. 2017-12-31 to 2017/12/31). +# (bool; default is FALSE/0) +#DATE_SEPARATOR_IS_SLASH=0 # Home currency (currency in which all resulting reports are denominated). # (String; default is 'USD') diff --git a/src/cli_user_choices.rs b/src/cli_user_choices.rs index 93918ec..8524c34 100644 --- a/src/cli_user_choices.rs +++ b/src/cli_user_choices.rs @@ -5,6 +5,7 @@ use std::error::Error; use std::io::{self, BufRead}; use std::process; use std::path::PathBuf; +use std::fs::File; use chrono::NaiveDate; use rustyline::completion::{Completer, FilenameCompleter, Pair}; @@ -21,7 +22,7 @@ use crptls::string_utils; pub fn choose_file_for_import(flag_to_accept_cli_args: bool) -> Result> { if flag_to_accept_cli_args { - println!("WARN: Flag to 'accept args' was set, but 'file' is missing.\n"); + println!("\nWARN: Flag to 'accept args' was set, but 'file_to_import' is missing.\n"); } println!("Please input a file (absolute or relative path) to import: "); @@ -30,6 +31,9 @@ pub fn choose_file_for_import(flag_to_accept_cli_args: bool) -> Result Result Result> { + fn _costing_method(env_var_arg: String) -> Result> { 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 - "" => Ok(inv_costing_from_cmd_arg(cmd_line_arg)?), + "" => Ok(inv_costing_from_cmd_arg(env_var_arg)?), "1" => Ok(InventoryCostingMethod::LIFObyLotCreationDate), "2" => Ok(InventoryCostingMethod::LIFObyLotBasisDate), "3" => Ok(InventoryCostingMethod::FIFObyLotCreationDate), "4" => Ok(InventoryCostingMethod::FIFObyLotBasisDate), - _ => { println!("Invalid choice. Please enter a valid choice."); _costing_method(cmd_line_arg) } + _ => { println!("Invalid choice. Please enter a valid choice."); _costing_method(env_var_arg) } } } @@ -151,10 +155,9 @@ pub fn inv_costing_from_cmd_arg(arg: String) -> Result Ok(InventoryCostingMethod::FIFObyLotCreationDate), "4" => Ok(InventoryCostingMethod::FIFObyLotBasisDate), _ => { - eprintln!("WARN: Invalid environment variable for 'INV_COSTING_METHOD'. Using default."); + println!("WARN: Invalid environment variable for 'INV_COSTING_METHOD'. Using default."); Ok(InventoryCostingMethod::LIFObyLotCreationDate) - } - // _ => { Err("Invalid input parameter, probably from environment variable INV_COSTING_METHOD") } + } } } @@ -171,7 +174,7 @@ pub(crate) fn elect_like_kind_treatment(cutoff_date_arg: &mut Option) -> second_date_try_from_user(&mut cutoff_date_arg).unwrap() } ) ); - println!("\nUse like-kind exchange treatment through {}? [Y/n/c] ('c' to 'change') ", provided_date); + println!("Use like-kind exchange treatment through {}? [Y/n/c] ('c' to 'change') ", provided_date); let (election, date_string) = _elect_like_kind_arg(&cutoff_date_arg, provided_date)?; diff --git a/src/crptls_lib/core_functions.rs b/src/crptls_lib/core_functions.rs index 4d6fa3e..0ae6ffb 100644 --- a/src/crptls_lib/core_functions.rs +++ b/src/crptls_lib/core_functions.rs @@ -95,7 +95,7 @@ pub fn import_and_process_final( if settings.lk_treatment_enabled { - println!(" Applying like-kind treatment for cut-off date: {}.", settings.lk_cutoff_date); + println!(" Applying like-kind treatment through cut-off date: {}.", settings.lk_cutoff_date); import_cost_proceeds_etc::apply_like_kind_treatment( &settings, diff --git a/src/crptls_lib/csv_import_accts_txns.rs b/src/crptls_lib/csv_import_accts_txns.rs index a63c215..c4971d8 100644 --- a/src/crptls_lib/csv_import_accts_txns.rs +++ b/src/crptls_lib/csv_import_accts_txns.rs @@ -26,7 +26,17 @@ pub(crate) fn import_from_csv( transactions_map: &mut HashMap, ) -> Result<(), Box> { - let file = File::open(import_file_path)?; println!("\nCSV ledger file opened successfully.\n"); + let file = match File::open(import_file_path) { + Ok(x) => { + println!("\nCSV ledger file opened successfully.\n"); + x + }, + Err(e) => { + println!("Invalid import_file_path"); + eprintln!("System error: {}", e); + std::process::exit(1); + } + }; let mut rdr = csv::ReaderBuilder::new() .has_headers(true) @@ -184,6 +194,7 @@ fn import_transactions( 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; + // TODO: implement conversion for negative numbers surrounded in parentheses let amount_str = field.replace(",", ""); let amount = amount_str.parse::().unwrap(); let amount_rounded = round_d128_1e8(&amount); diff --git a/src/main.rs b/src/main.rs index 3a7b382..9157cb4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,24 +31,24 @@ pub struct Cli { /// User is instructing the program to skip the data entry wizard. /// When set, default settings will be assumed if they are not set by - /// environment variables (or .env file). + /// environment variables (or .env file) or certain command line flags. #[structopt(name = "accept args", short = "a", long = "accept")] accept_args: bool, - /// This flag will suppress the printing of "all" reports, except that it *will* trigger the + /// Suppresses the printing of "all" reports, except that it *will* trigger the /// exporting of a txt file containing an accounting journal entry for every transaction. /// Individual account and transaction reports may still be printed via the print_menu /// with the -p flag. Note: the journal entries are not suitable for like-kind transactions. #[structopt(name = "journal entries", short, long = "journal-entries")] journal_entries_only: bool, - /// Once the import file has been fully processed, the user will be presented + /// Once the file_to_import has been fully processed, the user will be presented /// with a menu for manually selecting which reports to print/export. If this flag is not /// set, the program will print/export all available reports. #[structopt(name = "print menu", short, long = "print-menu")] print_menu: bool, - /// This will prevent the program from writing reports to files. + /// Prevents the program from writing reports to files. /// This will be ignored if -a is not set (the wizard will always ask to output). #[structopt(name = "suppress reports", short, long = "suppress")] suppress_reports: bool, @@ -57,10 +57,26 @@ pub struct Cli { #[structopt(name = "output directory", short, long = "output", default_value = ".", parse(from_os_str))] output_dir_path: PathBuf, - /// File to be imported. By default, the program expects the `txDate` column to be formatted as %m/%d/%y. - /// You may alter this with ISO_DATE and DATE_SEPARATOR environment variables. See .env.example for - /// further details. - #[structopt(name = "file", parse(from_os_str))] + /// Causes the program to expect the `txDate` field in the file_to_import to use the format YYYY-MM-dd + /// or YY-MM-dd (or YYYY/MM/dd or YY/MM/dd) instead of the default US-style MM-dd-YYYY or MM-dd-YY + /// (or MM/dd/YYYY or MM/dd/YY). + /// NOTE: this flag overrides the ISO_DATE environment variable, including if set in the .env file. + #[structopt(name = "imported file uses ISO 8601 date format", short = "i", long = "iso")] + iso_date: bool, + + /// Tells the program a non-default date separator (instead of a hyphen "-", a slash "/") was used + /// in the file_to_import `txDate` column (i.e. 2017-12-31 instead of 2017/12/31). + /// NOTE: this flag overrides the DATE_SEPARATOR_IS_SLASH environment variable, including if set in the .env file. + #[structopt(name = "date separator character is slash", short = "d", long = "date-separator-is-slash")] + date_separator_is_slash: bool, + + /// File to be imported. Some notes on the columns: (a) by default, the program expects the `txDate` column to + /// be formatted as %m-%d-%y. You may alter this with ISO_DATE and DATE_SEPARATOR_IS_SLASH flags or environment + /// variables; (b) the `proceeds` column and any values in transactions must have a period (".") as the decimal + /// separator; and (c) any transactions with negative values must not be wrapped in parentheses (use the python + /// script for sanitizing/converting negative values). + /// See .env.example for further details on environment variables. + #[structopt(name = "file_to_import", parse(from_os_str))] file_to_import: Option, } @@ -72,10 +88,9 @@ pub struct Cfg { /// The default value is `false`, meaning the program will expect default US-style MM-dd-YYYY or MM-dd-YY (or MM/dd/YYYY /// or MM/dd/YY, depending on the date separator option). iso_date: bool, - /// Set the corresponding environment variable to "h", "s", or "p" for hyphen, slash, or period (i.e., "-", "/", or ".") - /// to indicate the separator character used in the `Cli::file_to_import` `txDate` column (i.e. 2017/12/31, 2017-12-31, or 2017.12.31). - /// The default is `h`. - date_separator: String, + /// Switches the default date separator from hyphen to slash (i.e., from "-" to "/") to indicate the separator + /// character used in the file_to_import txDate column (i.e. 2017-12-31 to 2017/12/31). + date_separator_is_slash: bool, /// Home currency (currency from the `proceeds` column of the `Cli::file_to_import` and in which all resulting reports are denominated). /// Default is `USD`. home_currency: String, @@ -109,7 +124,7 @@ change default program behavior. Note: The software is designed to import a full history. Gains and losses may be incorrect otherwise. "); - let cfg = setup::get_env()?; + let cfg = setup::get_env(&args)?; let (input_file_path, settings) = setup::run_setup(args, cfg)?; diff --git a/src/setup.rs b/src/setup.rs index 701ede2..4ef711e 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -3,8 +3,8 @@ use std::path::PathBuf; use std::error::Error; -use std::process; use std::env; +use std::fs::File; use chrono::NaiveDate; use dotenv; @@ -17,31 +17,53 @@ use crate::skip_wizard; use crate::wizard; -pub fn get_env() -> Result> { +pub fn get_env(cmd_args: &super::Cli) -> Result> { match dotenv::dotenv() { - Ok(_path) => {println!("Setting environment variables from .env file.")}, + Ok(_path) => { println!("Setting environment variables from .env file.") }, Err(_e) => println!("Did not find .env file.") } - let iso_date: bool = match env::var("ISO_DATE") { - Ok(val) => { - if val == "1" || val.to_lowercase() == "true" { - true - } else { + let iso_date: bool = if cmd_args.iso_date { + println!(" Command line flag for ISO_DATE was set. Using YY-mm-dd or YY/mm/dd."); + true + } else { + match env::var("ISO_DATE") { + Ok(val) => { + if val == "1" || val.to_lowercase() == "true" { + println!(" Found ISO_DATE env var: {}. Using YY-mm-dd or YY/mm/dd.", val); + true + } else { + println!(" Found ISO_DATE env var: {} (not 1 or true). Using MM-dd-YY or MM/dd/YY.", val); + false + } + }, + Err(_e) => { + println!(" Using default dating convention (MM-dd-YY or MM/dd/YY)."); false - } - }, - Err(_e) => false, + }, + } }; - let date_separator: String = match env::var("DATE_SEPARATOR") { - Ok(val) => { - println!(" Found DATE_SEPARATOR env var: {}", val); - val.to_lowercase()}, - Err(_e) => { - println!(" Using default date separator (hyphen)."); - "h".to_string()}, + let date_separator_is_slash: bool = if cmd_args.date_separator_is_slash { + println!(" Command line flag for DATE_SEPARATOR_IS_SLASH was set. Date separator set to slash (\"/\")."); + true + } else { + match env::var("DATE_SEPARATOR_IS_SLASH") { + Ok(val) => { + if val == "1" || val.to_ascii_lowercase() == "true" { + println!(" Found DATE_SEPARATOR_IS_SLASH env var: {}. Date separator set to slash (\"/\").", val); + true + } else { + println!(" Found DATE_SEPARATOR_IS_SLASH env var: {} (not 1 or true). Date separator set to hyphen (\"-\").", val); + false + } + } + Err(_e) => { + println!(" Using default date separator, hyphen (\"-\")."); + false + }, + } }; let home_currency = match env::var("HOME_CURRENCY") { @@ -71,7 +93,7 @@ pub fn get_env() -> Result> { let cfg = super::Cfg { iso_date, - date_separator, + date_separator_is_slash, home_currency, lk_cutoff_date, inv_costing_method, @@ -90,19 +112,26 @@ pub struct ArgsForImportVarsTBD { pub (crate) fn run_setup(cmd_args: super::Cli, cfg: super::Cfg) -> Result<(PathBuf, ImportProcessParameters), Box> { - let date_separator = match cfg.date_separator.as_str() { - "h" => { "-" } - "s" => { "/" } - "p" => { "." } - _ => { - println!("\nFATAL: ENV: The date-separator arg requires either an 'h', an 's', or a 'p'.\n"); - process::exit(1) - } + let date_separator = match cfg.date_separator_is_slash { + false => { "-" } // Default + true => { "/" } // Overridden by env var or cmd line flag }; let input_file_path = match cmd_args.file_to_import { - Some(file) => file, - None => cli_user_choices::choose_file_for_import(cmd_args.accept_args)? + Some(file) => { + if File::open(&file).is_ok() { + file + } else { + cli_user_choices::choose_file_for_import(cmd_args.accept_args)? + } + }, + None => { + if !cmd_args.accept_args { + wizard::shall_we_proceed()?; + println!("Note: No file was provided as a command line arg, or the provided file wasn't found.\n"); + } + cli_user_choices::choose_file_for_import(cmd_args.accept_args)? + } }; let wizard_or_not_args = ArgsForImportVarsTBD { diff --git a/src/wizard.rs b/src/wizard.rs index 123deed..5c43fdd 100644 --- a/src/wizard.rs +++ b/src/wizard.rs @@ -20,7 +20,8 @@ pub(crate) fn wizard(args: ArgsForImportVarsTBD) -> Result<( PathBuf, ), Box> { - shall_we_proceed()?; + println!("\nThe following wizard will guide you through your choices:\n"); + // shall_we_proceed()?; let costing_method_choice = cli_user_choices::choose_inventory_costing_method(args.inv_costing_method_arg)?; @@ -39,9 +40,9 @@ pub(crate) fn wizard(args: ArgsForImportVarsTBD) -> Result<( Ok((costing_method_choice, like_kind_election, like_kind_cutoff_date_string, should_export, output_dir_path.to_path_buf())) } -fn shall_we_proceed() -> Result<(), Box> { +pub fn shall_we_proceed() -> Result<(), Box> { - println!("Shall we proceed? [Y/n] "); + println!("\n Shall we proceed? [Y/n] "); _proceed()?; @@ -64,7 +65,7 @@ fn shall_we_proceed() -> Result<(), Box> { fn export_reports_to_output_dir(output_dir_path: PathBuf) -> Result<(bool, PathBuf), Box> { - println!("\nThe directory currently selected for exporting reports is: {}", output_dir_path.to_str().unwrap()); + println!("The directory currently selected for exporting reports is: {}", output_dir_path.to_str().unwrap()); if output_dir_path.to_str().unwrap() == "." { println!(" (A 'dot' denotes the default value: current working directory.)");