diff --git a/Cargo.lock b/Cargo.lock index 9dc9389..428cdb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,6 +19,26 @@ dependencies = [ "version_check", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.0.1" @@ -77,6 +97,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + [[package]] name = "codepage" version = "0.1.1" @@ -157,6 +192,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "libc" version = "0.2.109" @@ -285,6 +329,12 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + [[package]] name = "syn" version = "1.0.82" @@ -296,6 +346,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.30" @@ -327,6 +386,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + [[package]] name = "unicode-xid" version = "0.2.2" @@ -339,6 +404,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "version_check" version = "0.9.3" @@ -379,6 +450,7 @@ version = "0.1.0" dependencies = [ "calamine", "chrono", + "clap", "rusqlite", "smallvec", ] diff --git a/Cargo.toml b/Cargo.toml index 3463701..5691fcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] calamine = "0.18.0" chrono = "0.4.19" +clap = { version = "2.34.0"} rusqlite = { version = "0.26.3", features = ["bundled"] } smallvec = "1.7.0" diff --git a/data.db b/data.db index 13d52bd..57cb995 100644 Binary files a/data.db and b/data.db differ diff --git a/sql/get_top.sql b/sql/get_top.sql new file mode 100644 index 0000000..df40a98 --- /dev/null +++ b/sql/get_top.sql @@ -0,0 +1,64 @@ + SELECT + description, + sku, + level2, + level3, + revenue, + quantity, + trend + from + ( + SELECT + *, + ROW_NUMBER () OVER ( + PARTITION BY level2 + ORDER BY + SUM(revenue) DESC + ) rownum, + SUM(revenue) OVER (PARTITION BY level2) total, + ROUND(SUM(revenue), 2) as revenue, + SUM(quantity) as quantity, + trend + FROM + ( + select + *, + cast(past as float) / 3 as past, + round( + cast(present as float) /(cast(past as float) / 3.0), + 2 + ) as trend + from + ( + select + *, + sum(quantity) FILTER ( + WHERE + year = $year + and month between $($month - 4) + and $($month - 1) + ) over (PARTITION BY description) as past, + sum(quantity) FILTER ( + WHERE + year = $year + and month = %[2]s + ) over (PARTITION BY description) as present + from + data + ) + ) + where + year = %[1]s + and month = %[2]s + GROUP BY + description + ORDER BY + total DESC + ) + WHERE + rownum <= 10 + and revenue > 0 + order by + (sum(revenue) over ( + partition by level2 + )) desc; diff --git a/src/converter/mod.rs b/src/converter/mod.rs new file mode 100644 index 0000000..0044c93 --- /dev/null +++ b/src/converter/mod.rs @@ -0,0 +1,137 @@ +use std::{error::Error}; +use std::{collections::HashMap}; + +use rusqlite::{Connection, Result}; +use calamine::{open_workbook, Reader, Xlsx,DataType}; + +use super::types::Data; + +const SCHEMA: &str = r#"CREATE TABLE IF NOT EXISTS data ( + description text, + sku text, + level2 text, + level3 text, + revenue float, + cost float, + quantity integer, + year integer, + month integer +); +"#; + +pub fn convert_excel(path: &str) -> Result<(),Box> { + let r = get_excel(path)?; + let (indexs,skip_count) = get_index(&r); + let data_vec = parse_data(&r,indexs,skip_count)?; + write_sql(&data_vec)?; + Ok(()) +} + +fn get_excel(path: &str) -> Result, Box> { + let mut excel: Xlsx<_> = open_workbook(path)?; + let r = excel + .worksheet_range("Export") + .expect("Is There") + ?; + Ok(r) +} + +fn get_index(r: &calamine::Range) -> (HashMap<&'static str, usize>,usize) { + let mut indexs = HashMap::new(); + let mut skip_count = 0; + 'outer: for row in r.rows() { + skip_count += 1; + for (i, elm) in row.iter().enumerate() { + let header = elm.get_string().expect("header issue"); + match header { + "FiscalYearMonth" => indexs.insert("date", i), + "Level2" => indexs.insert("level2", i), + "Level3" => indexs.insert("level3", i), + "MaterialEntered" => indexs.insert("sku", i), + "Quantity" => indexs.insert("quantity", i), + "SalesRevenue" => indexs.insert("revenue", i), + "CostOfGoodsSold" => indexs.insert("cost", i), + "ProductDescription" => indexs.insert("description", i), + _ => None, + }; + if indexs.len() == 8 { + break 'outer; + } + } + } + (indexs,skip_count) +} + +fn parse_data(r: &calamine::Range, indexs: HashMap<&str,usize>, skip_count: usize) -> Result,Box> { + let mut data_vec = Vec::with_capacity(100000); + for row in r.rows().skip(skip_count) { + if row[indexs["sku"]].get_string() == None { + break; + } + let datestring = row[indexs["date"]].get_string().expect("Date Parsing Issue"); + let year = datestring[0..4].parse::()?; + let month = datestring[5..].parse::()?; + data_vec.push(Data { + description: String::from(row[indexs["description"]].get_string().unwrap().replace("\"", "")), + sku: String::from(row[indexs["sku"]].get_string().unwrap()), + level2: String::from(row[indexs["level2"]].get_string().unwrap()), + level3: String::from(row[indexs["level3"]].get_string().unwrap()), + revenue: match row[indexs["revenue"]].get_float() { + Some(x) => x, + None => match row[indexs["revenue"]].get_int() { + Some(x) => x as f64, + None => 0.0, + }, + }, + cost: match row[indexs["cost"]].get_float() { + Some(x) => x, + None => match row[indexs["cost"]].get_int() { + Some(x) => x as f64, + None => 0.0, + }, + }, + quantity: match row[indexs["quantity"]].get_int() { + Some(x) => x, + None => match row[indexs["quantity"]].get_float() { + Some(x) => x as i64, + None => 0, + }, + }, + year: year, + month: month, + }); + } + Ok(data_vec) +} + +fn write_sql(data_vec: &Vec) -> Result<(), Box> { + let conn = Connection::open("data.db")?; + conn.execute("DROP TABLE IF EXISTS data;", [])?; + conn.execute(SCHEMA, [])?; + + let mut inserts = Vec::with_capacity(100000); + + inserts.push(String::from("INSERT INTO data (description,sku,level2,level3,revenue,cost,quantity,year,month) VALUES ")); + + for row in data_vec { + let other: String = format!( + "(\"{}\",\"{}\",\"{}\",\"{}\",{},{},{},{},{}),", + row.description, + row.sku, + row.level2, + row.level3, + row.revenue, + row.cost, + row.quantity, + row.year, + row.month + ); + inserts.push(other); + } + let mut ins = inserts.join(""); + ins.pop(); + ins.push(';'); + + conn.execute(&ins, [])?; + Ok(()) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 2c3321a..158ed9c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,135 +1,58 @@ -use std::{collections::HashMap, error::Error}; +use std::error::Error; -use calamine::{open_workbook, Reader, Xlsx}; -use rusqlite::{Connection, Result}; +mod converter; +mod types; -const SCHEMA: &str = r#"CREATE TABLE IF NOT EXISTS data ( - description text, - sku text, - level2 text, - level3 text, - revenue float, - cost float, - quantity integer, - year integer, - month integer -); -"#; +use clap::{App, Arg, SubCommand}; -#[derive(Debug)] -struct Data { - description: String, - sku: String, - level2: String, - level3: String, - revenue: f64, - cost: f64, - quantity: i64, - year: i64, - month: i64, -} fn main() -> Result<(), Box> { - let now = chrono::offset::Local::now(); - let mut data_vec = Vec::with_capacity(100000); - let mut excel: Xlsx<_> = open_workbook("data.xlsx").unwrap(); - let r = excel - .worksheet_range("Export") - .expect("Is There") - .expect("Excel Read Error"); - - let mut indexs = HashMap::new(); - let mut skip_count = 0; - 'outer: for row in r.rows() { - skip_count += 1; - for (i, elm) in row.iter().enumerate() { - let header = elm.get_string().expect("header issue"); - match header { - "FiscalYearMonth" => indexs.insert("date", i), - "Level2" => indexs.insert("level2", i), - "Level3" => indexs.insert("level3", i), - "MaterialEntered" => indexs.insert("sku", i), - "Quantity" => indexs.insert("quantity", i), - "SalesRevenue" => indexs.insert("revenue", i), - "CostOfGoodsSold" => indexs.insert("cost", i), - "ProductDescription" => indexs.insert("description", i), - _ => None, - }; - if indexs.len() == 8 { - break 'outer; - } - } - } - - for row in r.rows().skip(skip_count) { - if row[indexs["sku"]].get_string() == None { - break; - } - let datestring = row[indexs["date"]].get_string().expect("Date Parsing Issue"); - let year = datestring[0..4].parse::().unwrap(); - let month = datestring[5..].parse::().unwrap(); - data_vec.push(Data { - description: String::from(row[indexs["description"]].get_string().unwrap().replace("\"", "")), - sku: String::from(row[indexs["sku"]].get_string().unwrap()), - level2: String::from(row[indexs["level2"]].get_string().unwrap()), - level3: String::from(row[indexs["level3"]].get_string().unwrap()), - revenue: match row[indexs["revenue"]].get_float() { - Some(x) => x, - None => match row[indexs["revenue"]].get_int() { - Some(x) => x as f64, - None => 0.0, - }, - }, - cost: match row[indexs["cost"]].get_float() { - Some(x) => x, - None => match row[indexs["cost"]].get_int() { - Some(x) => x as f64, - None => 0.0, - }, - }, - quantity: match row[indexs["quantity"]].get_int() { - Some(x) => x, - None => match row[indexs["quantity"]].get_float() { - Some(x) => x as i64, - None => 0, - }, - }, - year: year, - month: month, - }); - } - - let conn = Connection::open("data.db")?; - conn.execute("DROP TABLE IF EXISTS data;", [])?; - conn.execute(SCHEMA, [])?; - - let mut inserts = Vec::with_capacity(100000); - - inserts.push(String::from("INSERT INTO data (description,sku,level2,level3,revenue,cost,quantity,year,month) VALUES ")); - - for row in data_vec { - let other: String = format!( - "(\"{}\",\"{}\",\"{}\",\"{}\",{},{},{},{},{}),", - row.description, - row.sku, - row.level2, - row.level3, - row.revenue, - row.cost, - row.quantity, - row.year, - row.month + let mut app = App::new("xlr") + .arg( + Arg::with_name("convert") + .short("c") + .long("convert") + .value_name("FILE") + .help("File to convert") + .takes_value(true), + ) + .arg( + Arg::with_name("get-top") + .short("gt") + .long("get-top") + .value_names(&["YEAR", "MONTH"]) + .help("Year and month to get top info") + .takes_value(true), ); - inserts.push(other); + + let mut helpper = app.clone(); + let matches = app.get_matches(); + let mut did_run = false; + + //Convert + match matches.value_of("convert") { + Some(val) => { + let now = chrono::offset::Local::now(); + converter::convert_excel(val)?; + println!( + "{:?}", + (chrono::offset::Local::now() - now).num_milliseconds() + ); + did_run = true; + }, + None => {}, } - let mut ins = inserts.join(""); - ins.pop(); - ins.push(';'); - conn.execute(&ins, [])?; + //Get Top + match matches.values_of("get-top") { + Some(vals) => { + did_run = true; + }, + None => {} + } - let after = chrono::offset::Local::now(); - let diff = after - now; - println!("{:?}", diff.num_milliseconds()); + if !did_run { + helpper.print_help().unwrap(); + } Ok(()) } diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 0000000..d934b1c --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,12 @@ +#[derive(Debug)] +pub struct Data { + pub description: String, + pub sku: String, + pub level2: String, + pub level3: String, + pub revenue: f64, + pub cost: f64, + pub quantity: i64, + pub year: i64, + pub month: i64, +} \ No newline at end of file