diff --git a/Cargo.lock b/Cargo.lock index 4d5b3d8..b18d7db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,6 +63,18 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "bstr" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -97,6 +109,28 @@ dependencies = [ "vec_map", ] +[[package]] +name = "csv" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" +dependencies = [ + "bstr", + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", +] + [[package]] name = "ctor" version = "0.1.20" @@ -204,6 +238,18 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.81" @@ -332,6 +378,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + [[package]] name = "regex-syntax" version = "0.6.25" @@ -354,6 +406,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + [[package]] name = "serde" version = "1.0.126" @@ -472,6 +530,7 @@ dependencies = [ "ansi_term 0.12.1", "chrono", "clap", + "csv", "dirs", "itertools", "pretty_assertions", diff --git a/Cargo.toml b/Cargo.toml index 4030624..2338df6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ itertools = "0.10" textwrap = "0.14" terminal_size = "0.1" ansi_term = "0.12" +csv = "1.1" [dev-dependencies] pretty_assertions = "0.7.2" diff --git a/src/error.rs b/src/error.rs index e44dd47..f5d2c5f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -33,6 +33,9 @@ pub enum Error { #[error("IOError: {0}")] IOError(#[from] std::io::Error), + #[error("CSV Error: {0}")] + CSVError(#[from] csv::Error), + #[error("Corrupted data found in the database: {0}")] CorruptedData(String), diff --git a/src/formatters.rs b/src/formatters.rs index f23c2b9..0872c7c 100644 --- a/src/formatters.rs +++ b/src/formatters.rs @@ -8,11 +8,13 @@ use crate::error; use crate::models::Entry; pub mod text; +pub mod csv; #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum Formatter { Text, + Csv, Custom(String), } @@ -32,6 +34,7 @@ impl Formatter { pub fn print_formatted(&self, entries: Vec, out: &mut W, now: DateTime, ids: bool, term_width: usize) -> error::Result<()> { match &self { Formatter::Text => text::print_formatted(entries, out, now, ids, term_width)?, + Formatter::Csv => csv::print_formatted(entries, out, ids)?, Formatter::Custom(name) => { } } diff --git a/src/formatters/csv.rs b/src/formatters/csv.rs new file mode 100644 index 0000000..216aec8 --- /dev/null +++ b/src/formatters/csv.rs @@ -0,0 +1,81 @@ +use std::io::Write; + +use csv::Writer; +use chrono::SecondsFormat; + +use crate::error::Result; +use crate::models::Entry; + +pub fn print_formatted(entries: Vec, out: &mut W, ids: bool) -> Result<()> { + let mut wtr = Writer::from_writer(out); + + if ids { + wtr.write_record(&["id", "start", "end", "note", "sheet"])?; + } else { + wtr.write_record(&["start", "end", "note", "sheet"])?; + } + + for entry in entries { + if ids { + wtr.write_record(&[ + entry.id.to_string(), + entry.start.to_rfc3339_opts(SecondsFormat::Secs, true), + entry.end.map(|t| t.to_rfc3339_opts(SecondsFormat::Secs, true)).unwrap_or("".into()), + entry.note.unwrap_or("".into()), + entry.sheet, + ])?; + } else { + wtr.write_record(&[ + entry.start.to_rfc3339_opts(SecondsFormat::Secs, true), + entry.end.map(|t| t.to_rfc3339_opts(SecondsFormat::Secs, true)).unwrap_or("".into()), + entry.note.unwrap_or("".into()), + entry.sheet, + ])?; + } + } + + wtr.flush()?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use chrono::{TimeZone, Utc}; + + use crate::test_utils::PrettyString; + + use super::*; + + #[test] + fn test_print_formatted() { + let entries = vec![ + Entry::new_sample(1, Utc.ymd(2021, 6, 30).and_hms(18, 12, 34), Some(Utc.ymd(2021, 6, 30).and_hms(19, 0, 0))), + Entry::new_sample(2, Utc.ymd(2021, 6, 30).and_hms(18, 12, 34), None), + ]; + let mut out = Vec::new(); + + print_formatted(entries, &mut out, false).unwrap(); + + assert_eq!(PrettyString(&String::from_utf8_lossy(&out)), PrettyString("start,end,note,sheet +2021-06-30T18:12:34Z,2021-06-30T19:00:00Z,entry 1,default +2021-06-30T18:12:34Z,,entry 2,default +")); + } + + #[test] + fn test_print_formatted_ids() { + let entries = vec![ + Entry::new_sample(1, Utc.ymd(2021, 6, 30).and_hms(18, 12, 34), Some(Utc.ymd(2021, 6, 30).and_hms(19, 0, 0))), + Entry::new_sample(2, Utc.ymd(2021, 6, 30).and_hms(18, 12, 34), None), + ]; + let mut out = Vec::new(); + + print_formatted(entries, &mut out, true).unwrap(); + + assert_eq!(PrettyString(&String::from_utf8_lossy(&out)), PrettyString("id,start,end,note,sheet +1,2021-06-30T18:12:34Z,2021-06-30T19:00:00Z,entry 1,default +2,2021-06-30T18:12:34Z,,entry 2,default +")); + } +}