diff --git a/Cargo.lock b/Cargo.lock index e4a91c1..05e0c49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,9 +279,18 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "7a73e9fe3c49d7afb2ace819fa181a287ce54a0983eda4e0eb05c22f82ffe534" + +[[package]] +name = "journal" +version = "0.1.0" +dependencies = [ + "chrono", + "psychonaut_journal_types", + "serde", +] [[package]] name = "js-sys" @@ -373,12 +382,21 @@ version = "0.1.0" dependencies = [ "chrono", "clap", + "journal", "log", "prettytable-rs", + "psychonaut_journal_types", "serde", "serde_json", ] +[[package]] +name = "psychonaut_journal_types" +version = "0.1.0" +dependencies = [ + "serde", +] + [[package]] name = "quote" version = "1.0.37" @@ -433,9 +451,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", diff --git a/Cargo.toml b/Cargo.toml index 4bc1867..cfb797a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,24 +1,8 @@ -[package] -name = "psychonaut_journal_cli" -version = "0.1.0" -edition = "2021" +[workspace] +resolver = "2" -[[bin]] -name = "journal-cli" -path = "journal_cli/src/main.rs" - -[lib] -name = "journal" -path = "journal/src/lib.rs" - -[dependencies] -chrono = { version = "0.4.38", features = ["serde"] } -clap = { version = "4.5.21", features = ["derive", "env"] } -log = { version = "0.4.22", features = ["std", "serde"] } -prettytable-rs = "0.10.0" -serde = { version = "1.0.215", features = ["std", "derive", "serde_derive"] } -serde_json = "1.0.132" -#serde_with = "3.11.0" -#string-error = "0.1.0" -#termcolor = "1.4.1" -#thiserror = "2.0.3" +members = [ + "psychonaut_journal_types", + "journal", + "journal_cli" +] \ No newline at end of file diff --git a/journal/Cargo.lock b/journal/Cargo.lock new file mode 100644 index 0000000..f2b8669 --- /dev/null +++ b/journal/Cargo.lock @@ -0,0 +1,330 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "cc" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "journal" +version = "0.1.0" +dependencies = [ + "chrono", + "psychonaut_journal_types", + "serde", +] + +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psychonaut_journal_types" +version = "0.1.0" +dependencies = [ + "serde", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/journal/Cargo.toml b/journal/Cargo.toml index 7f5dd87..9253ed1 100644 --- a/journal/Cargo.toml +++ b/journal/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" [dependencies] serde = { version = "1.0.215", features = ["std", "derive", "serde_derive"] } chrono = { version = "0.4.38", features = ["serde"] } -chrono-tz = { version = "0.10.0", features = ["serde"] } \ No newline at end of file +psychonaut_journal_types = { path = "../psychonaut_journal_types" } \ No newline at end of file diff --git a/journal/src/helpers.rs b/journal/src/helpers.rs index 0437fc8..92944d3 100644 --- a/journal/src/helpers.rs +++ b/journal/src/helpers.rs @@ -1,20 +1,4 @@ -use crate::types::{CustomUnits, CustomUnitsType, Ingestion}; - -pub fn ingestion_dose(ingestion: &Ingestion, custom_units: &CustomUnitsType) -> Option { - if let Some(custom_unit_id) = ingestion.custom_unit_id { - if let Some(ingestion_dose) = ingestion.dose { - let custom_unit = custom_units - .get_by_id(custom_unit_id) - .expect("Custom Unit could not be found"); - - Some(ingestion_dose * custom_unit.dose) - } else { - None - } - } else { - ingestion.dose - } -} +use crate::types::{CustomUnit, Estimation, IngestionDose, StandardIngestionDose}; fn add_standard_deviation( expectation_x: f64, @@ -41,69 +25,48 @@ fn add_standard_deviation( } } -pub fn ingestion_contains_estimate(ingestion: &Ingestion, custom_units: &CustomUnitsType) -> bool { - if ingestion.is_estimate { - return true; - } +pub fn canonical_dose(dose: &IngestionDose, custom_unit: Option<&CustomUnit>) -> IngestionDose { + if let IngestionDose::Custom(dose) = dose { + let custom_unit = custom_unit.expect("custom unit not provided when dose is type custom"); + let custom_unit_dose = &custom_unit.dose; - if let Some(custom_unit_id) = ingestion.custom_unit_id { - let custom_unit = custom_units - .get_by_id(custom_unit_id) - .expect("Custom Unit could not be found"); - - custom_unit.is_estimate - } else { - false - } -} - -pub fn ingestion_standard_deviation( - ingestion: &Ingestion, - custom_units: &CustomUnitsType, -) -> Option { - if ingestion.dose.is_none() { - return None; - } - - if !ingestion_contains_estimate(ingestion, custom_units) { - return None; - } - - if let Some(custom_unit_id) = ingestion.custom_unit_id { - let custom_unit = custom_units - .get_by_id(custom_unit_id) - .expect("Custom Unit could not be found"); - - if custom_unit.estimate_standard_deviation.is_none() { - return ingestion.estimate_standard_deviation; + let estimation = match (&dose.estimation, &custom_unit_dose.estimation) { + ( + &Estimation::StandardDeviation(dose_standard_deviation), + &Estimation::StandardDeviation(custom_unit_standard_deviation), + ) => { + let result = add_standard_deviation( + dose.dose, + dose_standard_deviation, + custom_unit_dose.dose, + custom_unit_standard_deviation, + ); + if let Some(result) = result { + Estimation::StandardDeviation(result) + } else { + Estimation::Estimate + } + } + (&Estimation::StandardDeviation(dose_standard_deviation), _) => { + Estimation::StandardDeviation(dose_standard_deviation) + } + (_, &Estimation::StandardDeviation(custom_unit_standard_deviation)) => { + Estimation::StandardDeviation(custom_unit_standard_deviation) + } + (Estimation::Precise, Estimation::Precise) => Estimation::Precise, + _ => Estimation::Estimate, }; - if ingestion.estimate_standard_deviation.is_none() { - return custom_unit.estimate_standard_deviation; - } - - return add_standard_deviation( - ingestion.dose.unwrap(), - ingestion.estimate_standard_deviation.unwrap(), - custom_unit.dose, - custom_unit.estimate_standard_deviation.unwrap(), - ); + IngestionDose::Standard(StandardIngestionDose { + dose: dose.dose * custom_unit_dose.dose, + unit: custom_unit.original_unit.clone(), + estimation, + }) } else { - return ingestion.estimate_standard_deviation; + dose.clone() } } -pub fn ingestion_unit( - ingestion: &Ingestion, - custom_units: &CustomUnitsType, -) -> String { - if let Some(custom_unit_id) = ingestion.custom_unit_id { - let custom_unit = custom_units - .get_by_id(custom_unit_id) - .expect("Custom Unit could not be found"); - - custom_unit.original_unit.clone() - } else { - ingestion.unit.clone() - } -} \ No newline at end of file +pub fn canonical_unit(dose: &IngestionDose, custom_unit: Option<&CustomUnit>) -> String { + canonical_dose(dose, custom_unit).unit() +} diff --git a/journal/src/lib.rs b/journal/src/lib.rs index 5b5f982..96c1cb9 100644 --- a/journal/src/lib.rs +++ b/journal/src/lib.rs @@ -1,2 +1,2 @@ pub mod types; -pub mod helpers; \ No newline at end of file +pub mod helpers; diff --git a/journal/src/types.rs b/journal/src/types.rs index 5ea8ea3..bf2b947 100644 --- a/journal/src/types.rs +++ b/journal/src/types.rs @@ -1,95 +1,177 @@ -use chrono::serde::ts_milliseconds; -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use std::fmt::Debug; -use std::fmt::Display; +use chrono::{DateTime, TimeZone, Utc}; +use std::collections::HashMap; +use std::fmt::{Debug, Display}; +use std::str::FromStr; -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "UPPERCASE")] -pub enum AdministrationRoute { - Oral, - Sublingual, - Buccal, - Insufflated, - Rectal, - Transdermal, - Subcutaneous, - Intramuscular, - Intravenous, - Smoked, - Inhaled, +pub type AdministrationRoute = psychonaut_journal_types::AdministrationRoute; + +#[derive(PartialEq, Debug, Clone)] +pub enum Estimation { + Precise, + Estimate, + StandardDeviation(f64), } -impl Display for AdministrationRoute { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) +impl Estimation { + pub fn is_precise(&self) -> bool { + matches!(self, Estimation::Precise) + } + pub fn is_estimate(&self) -> bool { + !matches!(self, Estimation::Precise) + } + pub fn is_standard_deviation(&self) -> bool { + matches!(self, Estimation::StandardDeviation(_)) } } -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, Clone)] +pub struct UnknownIngestionDose { + pub unit: String, +} + +#[derive(Debug, Clone)] +pub struct StandardIngestionDose { + pub dose: f64, + pub unit: String, + pub estimation: Estimation, +} + +impl StandardIngestionDose { + pub fn is_estimate(&self) -> bool { + self.estimation.is_estimate() + } +} + +#[derive(Debug, Clone)] +pub struct CustomIngestionDose { + pub dose: f64, + pub unit: String, + pub original_unit: String, + pub estimation: Estimation, + pub custom_unit_id: i64, +} + +impl CustomIngestionDose { + pub fn is_estimate(&self) -> bool { + self.estimation.is_estimate() + } +} + +#[derive(Debug, Clone)] +pub enum IngestionDose { + Unknown(UnknownIngestionDose), + Standard(StandardIngestionDose), + Custom(CustomIngestionDose), +} + +impl IngestionDose { + pub fn is_estimate(&self) -> bool { + match self { + IngestionDose::Unknown(_) => false, + IngestionDose::Standard(dose) => dose.is_estimate(), + IngestionDose::Custom(dose) => dose.is_estimate(), + } + } + pub fn unit(&self) -> String { + match self { + IngestionDose::Unknown(dose) => dose.unit.clone(), + IngestionDose::Standard(dose) => dose.unit.clone(), + IngestionDose::Custom(dose) => dose.unit.clone(), + } + } +} + +#[derive(PartialEq, Debug, Clone)] +pub enum Consumer { + Default, + Named(String), +} + +impl FromStr for Consumer { + type Err = core::convert::Infallible; + + fn from_str(s: &str) -> Result { + if s == "default" { + Ok(Consumer::Default) + } else { + Ok(Consumer::Named(s.to_string())) + } + } +} + +impl Display for Consumer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Consumer::Default => f.write_str("default"), + Consumer::Named(name) => f.write_str(name), + } + } +} + +#[derive(Debug, Clone)] pub struct Ingestion { pub substance_name: String, - #[serde(with = "ts_milliseconds", rename = "time")] pub ingestion_time: DateTime, - #[serde(with = "ts_milliseconds", rename = "creationDate")] pub creation_time: DateTime, - pub dose: Option, - #[serde(rename = "units")] - pub unit: String, - #[serde(rename = "isDoseAnEstimate")] - pub is_estimate: bool, - #[serde(rename = "estimatedDoseStandardDeviation")] - pub estimate_standard_deviation: Option, - pub custom_unit_id: Option, - #[serde(rename = "administrationRoute")] + pub dose: IngestionDose, pub roa: AdministrationRoute, - pub consumer_name: Option, + pub consumer: Consumer, pub notes: String, pub stomach_fullness: Option, } -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, Clone)] pub struct CustomSubstance { pub name: String, pub description: String, pub units: String, } -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, Clone)] pub struct Experience { pub title: String, pub text: String, - #[serde(with = "ts_milliseconds", rename = "creationDate")] pub creation_time: DateTime, - #[serde(with = "ts_milliseconds", rename = "sortDate")] pub modified_time: DateTime, pub ingestions: Vec, } -pub type ExperiencesType = Vec; +#[derive(Debug, Clone)] +pub struct CustomUnit { + pub id: i64, + pub name: String, + pub substance_name: String, -pub trait Experiences { - fn filter_by_title(&self, title: &String) -> Vec; - fn get_by_title(&self, title: &String) -> Option; + pub administration_route: AdministrationRoute, + pub dose: StandardIngestionDose, + pub original_unit: String, + + pub creation_time: DateTime, + pub is_archived: bool, } -impl Experiences for ExperiencesType { - fn filter_by_title(&self, title: &String) -> Vec { - self.iter() - .filter_map(|experience| { - if &experience.title == title { - Some(experience.clone()) - } else { - None - } - }) - .collect() +#[derive(Debug, Default, Clone)] +pub struct Journal { + pub experiences: Vec, + //pub substance_colours: HashMap, + //pub custom_substances: HashMap, + pub custom_units: HashMap, +} + +impl Journal { + pub fn get_custom_unit(&self, id: i64) -> Option { + self.custom_units.get(&id).cloned() } - fn get_by_title(&self, title: &String) -> Option { - for experience in self.iter() { + + pub fn maybe_get_custom_unit_for(&self, ingestion: &Ingestion) -> Option { + match &ingestion.dose { + IngestionDose::Custom(dose) => self.get_custom_unit(dose.custom_unit_id), + _ => None, + } + } + + pub fn first_experience_by_title(&self, title: &String) -> Option { + for experience in self.experiences.iter() { if &experience.title == title { return Some(experience.clone()); } @@ -97,56 +179,116 @@ impl Experiences for ExperiencesType { None } -} -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct SubstanceCompanion { - pub substance_name: String, - pub color: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct CustomUnit { - pub id: i64, - pub substance_name: String, - pub name: String, - #[serde(with = "ts_milliseconds", rename = "creationDate")] - pub creation_time: DateTime, - pub administration_route: AdministrationRoute, - pub dose: f64, - pub unit: String, - pub original_unit: String, - pub is_estimate: bool, - #[serde(rename = "estimatedDoseStandardDeviation")] - pub estimate_standard_deviation: Option, - pub is_archived: bool, -} - -pub type CustomUnitsType = Vec; - -pub trait CustomUnits { - fn get_by_id(&self, id: i64) -> Option; -} - -impl CustomUnits for CustomUnitsType { - fn get_by_id(&self, id: i64) -> Option { - for custom_unit in self.iter() { - if custom_unit.id == id { - return Some(custom_unit.clone()); - } + pub fn import(data: psychonaut_journal_types::ExportData) -> Self { + fn from_unix_millis(time: u64) -> DateTime { + Utc.timestamp_millis_opt(time as i64).unwrap() } - None + let mut journal = Journal::default(); + + for custom_unit in data.custom_units.into_iter() { + journal.custom_units.insert( + custom_unit.id, + CustomUnit { + id: custom_unit.id, + name: custom_unit.name, + substance_name: custom_unit.substance_name, + + administration_route: custom_unit.administration_route, + dose: StandardIngestionDose { + dose: custom_unit.dose, + unit: custom_unit.unit, + estimation: if custom_unit.is_estimate { + if let Some(standard_deviation) = + custom_unit.estimate_standard_deviation + { + Estimation::StandardDeviation(standard_deviation) + } else { + Estimation::Estimate + } + } else { + Estimation::Precise + }, + }, + + original_unit: custom_unit.original_unit, + + creation_time: from_unix_millis(custom_unit.creation_time), + is_archived: custom_unit.is_archived, + }, + ); + } + + for experience in data.experiences.into_iter() { + let mut ingestions: Vec = Vec::new(); + + for ingestion in experience.ingestions.into_iter() { + fn ingestion_estimation( + ingestion: &psychonaut_journal_types::Ingestion, + ) -> Estimation { + if !ingestion.is_estimate { + Estimation::Precise + } else if let Some(standard_deviation) = ingestion.estimate_standard_deviation { + Estimation::StandardDeviation(standard_deviation) + } else { + Estimation::Estimate + } + } + + let estimation = ingestion_estimation(&ingestion); + + ingestions.push(Ingestion { + substance_name: ingestion.substance_name, + ingestion_time: from_unix_millis(ingestion.ingestion_time), + creation_time: from_unix_millis(ingestion.creation_time), + dose: { + if let Some(dose) = ingestion.dose { + if let Some(custom_unit_id) = ingestion.custom_unit_id { + IngestionDose::Custom(CustomIngestionDose { + dose, + unit: ingestion.unit.clone(), + original_unit: journal + .custom_units + .get(&custom_unit_id) + .expect("custom unit not found") + .original_unit + .clone(), + estimation, + custom_unit_id, + }) + } else { + IngestionDose::Standard(StandardIngestionDose { + dose, + unit: ingestion.unit, + estimation, + }) + } + } else { + IngestionDose::Unknown(UnknownIngestionDose { + unit: ingestion.unit, + }) + } + }, + roa: ingestion.roa, + consumer: match ingestion.consumer_name { + Some(name) => Consumer::Named(name), + None => Consumer::Default, + }, + notes: ingestion.notes, + stomach_fullness: ingestion.stomach_fullness, + }) + } + + journal.experiences.push(Experience { + title: experience.title, + text: experience.text, + creation_time: from_unix_millis(experience.creation_time), + modified_time: from_unix_millis(experience.modified_time), + ingestions, + }); + } + + journal } } - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct ExportData { - pub experiences: ExperiencesType, - pub substance_companions: Vec, - pub custom_substances: Vec, - pub custom_units: CustomUnitsType, -} diff --git a/journal_cli/Cargo.toml b/journal_cli/Cargo.toml new file mode 100644 index 0000000..f4028d3 --- /dev/null +++ b/journal_cli/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "psychonaut_journal_cli" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "journal-cli" +path = "src/main.rs" + +[dependencies] +journal = { path = "../journal" } +psychonaut_journal_types = { path = "../psychonaut_journal_types" } + +chrono = { version = "0.4.38", features = ["serde"] } +clap = { version = "4.5.21", features = ["derive", "env"] } +log = { version = "0.4.22", features = ["std", "serde"] } +prettytable-rs = "0.10.0" +serde = { version = "1.0.215", features = ["std", "derive", "serde_derive"] } +serde_json = "1.0.132" +#serde_with = "3.11.0" +#string-error = "0.1.0" +#termcolor = "1.4.1" +#thiserror = "2.0.3" diff --git a/journal_cli/src/args.rs b/journal_cli/src/args.rs index c45f5b6..294ff98 100644 --- a/journal_cli/src/args.rs +++ b/journal_cli/src/args.rs @@ -1,9 +1,9 @@ use clap::{Parser, Subcommand}; -#[derive(Debug, Clone, Subcommand)] -#[clap(rename_all = "camelCase")] +#[derive(Debug, Clone, Subcommand)] +#[clap(rename_all = "camelCase")] pub enum Commands { - PrintExperience(crate::commands::print_experience::PrintExperienceArgs) + PrintExperience(crate::commands::print_experience::PrintExperienceArgs), } #[derive(Debug, Parser)] @@ -11,4 +11,4 @@ pub enum Commands { pub struct Args { #[clap(subcommand)] pub command: Commands, -} \ No newline at end of file +} diff --git a/journal_cli/src/commands/print_experience.rs b/journal_cli/src/commands/print_experience.rs index d45721b..ddc853c 100644 --- a/journal_cli/src/commands/print_experience.rs +++ b/journal_cli/src/commands/print_experience.rs @@ -1,44 +1,62 @@ +use std::str::FromStr; + +use journal::types::Consumer; + use crate::args::Args; -use crate::utils::{ - format_ingestion_dose, format_ingestion_roa, format_ingestion_time, load_export_data, -}; - -use journal; - -use journal::types::Experiences; +use crate::display::print_ingestion_log; +use crate::formatting::format_experience_title; +use crate::utils::load_journal; #[derive(Debug, Clone, clap::Args)] pub struct PrintExperienceArgs { pub experience_title: String, #[clap(long, env = "EXPORT_FILE")] pub export_file: String, + #[clap(long)] + pub substance_filter: Option>, + #[clap(long, value_delimiter = ',')] + pub consumer_filter: Option>, +} + +pub fn parse_consumer_filter(consumer_filter: Option>) -> Option> { + match consumer_filter { + Some(consumer_filter) => { + let mut consumers: Vec = Vec::new(); + for consumer in consumer_filter.into_iter() { + let mut consumer_name = consumer.as_str(); + if consumer_name.is_empty() { + consumer_name = "default"; + } + + consumers.push(Consumer::from_str(consumer_name).unwrap()); + } + Some(consumers) + } + None => None, + } } pub fn print_experience( _global_args: &Args, args: &PrintExperienceArgs, ) -> Result<(), Box> { - let export_data = load_export_data(&args.export_file).expect("could not load export data"); + let journal = load_journal(&args.export_file).expect("could not load export data"); - let experience = export_data - .experiences - .get_by_title(&args.experience_title) + let experience = journal + .first_experience_by_title(&args.experience_title) .expect("could not find experience"); - for ingestion in experience.ingestions.iter() { - println!( - "{}|{}|{}|{}|{}", - ingestion.substance_name, - format_ingestion_dose(&ingestion, &export_data.custom_units), - format_ingestion_roa(&ingestion, &export_data.custom_units), - ingestion - .consumer_name - .clone() - .or(Some("default".to_string())) - .unwrap(), - format_ingestion_time(&ingestion) - ) - } + println!("{}", format_experience_title(&experience)); + + let substance_filter = args.substance_filter.clone(); + let consumer_filter = parse_consumer_filter(args.consumer_filter.clone()); + + print_ingestion_log( + &journal, + &experience, + substance_filter.as_ref(), + consumer_filter.as_ref(), + ); Ok(()) } diff --git a/journal_cli/src/display.rs b/journal_cli/src/display.rs new file mode 100644 index 0000000..23f785e --- /dev/null +++ b/journal_cli/src/display.rs @@ -0,0 +1,35 @@ +use journal::types::{Consumer, Experience, Journal}; + +use crate::formatting::{format_ingestion_dose, format_ingestion_roa, format_ingestion_time}; + +pub fn print_ingestion_log( + journal: &Journal, + experience: &Experience, + substance_filter: Option<&Vec>, + consumer_filter: Option<&Vec>, +) { + for ingestion in experience.ingestions.iter() { + if let Some(substance_filter) = substance_filter { + if !substance_filter.contains(&ingestion.substance_name) { + continue; + } + } + + if let Some(consumer_filter) = consumer_filter { + if !consumer_filter.contains(&ingestion.consumer) { + continue; + } + } + + let custom_unit = journal.maybe_get_custom_unit_for(ingestion); + + println!( + "{}|{}|{}|{}|{}", + ingestion.substance_name, + format_ingestion_dose(&ingestion.dose, custom_unit.as_ref()), + format_ingestion_roa(ingestion, custom_unit.as_ref()), + ingestion.consumer, + format_ingestion_time(ingestion) + ) + } +} diff --git a/journal_cli/src/formatting.rs b/journal_cli/src/formatting.rs new file mode 100644 index 0000000..3f1d430 --- /dev/null +++ b/journal_cli/src/formatting.rs @@ -0,0 +1,75 @@ +use journal::{ + helpers::canonical_dose, + types::{CustomUnit, Experience, Ingestion, IngestionDose, StandardIngestionDose}, +}; + +pub fn format_experience_title(experience: &Experience) -> String { + format!("{}: {}", experience.title, experience.creation_time) +} + +pub fn format_ingestion_dose(dose: &IngestionDose, custom_unit: Option<&CustomUnit>) -> String { + match dose { + IngestionDose::Unknown(dose) => format!("Unknown {}", dose.unit), + IngestionDose::Standard(dose) => { + let is_estimate = match dose.estimation { + journal::types::Estimation::Precise => false, + journal::types::Estimation::Estimate + | journal::types::Estimation::StandardDeviation(_) => true, + }; + + let estimate = if is_estimate { "~" } else { "" }; + let standard_deviation = + if let journal::types::Estimation::StandardDeviation(standard_deviation) = + dose.estimation + { + format!("±{}", (standard_deviation * 100.0).round() / 100.0) + } else { + "".to_string() + }; + let dose_value = (dose.dose * 100.0).round() / 100.0; + let unit = dose.unit.clone(); + + format!("{estimate}{dose_value}{standard_deviation} {unit}") + } + IngestionDose::Custom(dose) => { + let custom_unit: &CustomUnit = custom_unit.expect("custom unit required for dose type"); + + let canonical_dose = + canonical_dose(&IngestionDose::Custom(dose.clone()), Some(custom_unit)); + + let canonical_dose = format_ingestion_dose(&canonical_dose, None); + + let custom_unit_dose_per = format_ingestion_dose( + &IngestionDose::Standard(StandardIngestionDose { + dose: custom_unit.dose.dose, + unit: custom_unit.original_unit.clone(), + estimation: custom_unit.dose.estimation.clone(), + }), + None, + ); + + let custom_unit_dose = format_ingestion_dose( + &IngestionDose::Standard(StandardIngestionDose { + dose: dose.dose, + unit: custom_unit.dose.unit.clone(), + estimation: custom_unit.dose.estimation.clone(), + }), + None, + ); + + format!("{canonical_dose} ({custom_unit_dose_per} * {custom_unit_dose})") + } + } +} + +pub fn format_ingestion_time(ingestion: &Ingestion) -> String { + ingestion.ingestion_time.format("%a %I:%M %p").to_string() +} + +pub fn format_ingestion_roa(ingestion: &Ingestion, custom_unit: Option<&CustomUnit>) -> String { + if let Some(custom_unit) = custom_unit { + format!("{:?} ({})", ingestion.roa, custom_unit.name) + } else { + format!("{:?}", ingestion.roa) + } +} diff --git a/journal_cli/src/main.rs b/journal_cli/src/main.rs index a2fa8a4..11340a0 100644 --- a/journal_cli/src/main.rs +++ b/journal_cli/src/main.rs @@ -1,21 +1,25 @@ use clap::Parser; -use commands::print_experience::print_experience; pub mod args; pub mod commands; +pub mod display; +pub mod formatting; pub mod utils; +use commands::print_experience::print_experience; + fn main() -> Result<(), Box> { let args = args::Args::parse(); - let command = args.command.to_owned(); + let command = args.command.to_owned(); - //println!("{:#?}", args); + println!("{:#?}", args); - match command { - args::Commands::PrintExperience(print_experience_args) => - print_experience(&args, &print_experience_args)?, - } + match command { + args::Commands::PrintExperience(print_experience_args) => { + print_experience(&args, &print_experience_args)? + } + } Ok(()) } diff --git a/journal_cli/src/utils.rs b/journal_cli/src/utils.rs index d58e898..2182337 100644 --- a/journal_cli/src/utils.rs +++ b/journal_cli/src/utils.rs @@ -1,137 +1,14 @@ -use journal::{ - helpers::{ - ingestion_contains_estimate, ingestion_dose, ingestion_standard_deviation, ingestion_unit, - }, - types::{CustomUnits, CustomUnitsType, Experience, ExportData, Ingestion}, -}; +use journal::types::Journal; +use psychonaut_journal_types::ExportData; use std::fs::File; -pub fn load_export_data(filename: &String) -> Result> { +pub fn load_journal(filename: &String) -> Result> { let file = File::open(filename)?; let export_data: ExportData = serde_json::from_reader(file)?; - // export_data - // .experiences - // .sort_by(|a, b| a.modified_time.cmp(&b.modified_time)); + let journal = Journal::import(export_data); - //for experience in export_data.experiences.iter_mut() { - // experience - // .ingestions - // .sort_by(|a, b| a.ingestion_time.cmp(&b.ingestion_time)); - //} - - Ok(export_data) + Ok(journal) } -pub fn format_experience_title(experience: &Experience) -> String { - format!("{}: {}", experience.title, experience.creation_time) -} - -pub fn format_dose( - dose: Option, - unit: &String, - estimate: bool, - standard_deviation: Option, -) -> String { - if let Some(dose) = dose { - let estimate = if estimate { "~" } else { "" }; - let standard_deviation = if let Some(standard_deviation) = standard_deviation { - format!("±{}", (standard_deviation * 100.0).round() / 100.0) - } else { - "".to_string() - }; - let dose = (dose * 100.0).round() / 100.0; - - format!("{estimate}{dose}{standard_deviation} {unit}") - } else { - format!("Unknown {unit}") - } -} - -pub fn format_ingestion_dose(ingestion: &Ingestion, custom_units: &CustomUnitsType) -> String { - let dose = ingestion_dose(ingestion, custom_units); - let unit = ingestion_unit(ingestion, custom_units); - let standard_deviation = ingestion_standard_deviation(ingestion, custom_units); - - if let Some(dose) = dose { - if let Some(custom_unit_id) = ingestion.custom_unit_id { - let estimate = if ingestion_contains_estimate(&ingestion, &custom_units) { - "~" - } else { - "" - }; - - let custom_unit = custom_units - .get_by_id(custom_unit_id) - .expect("custom unit not found"); - - let canonical_dose = - format_dose(Some(dose), &unit, ingestion.is_estimate, standard_deviation); - - let custom_unit_dose_per = format_dose( - Some(custom_unit.dose), - &custom_unit.original_unit, - custom_unit.is_estimate, - custom_unit.estimate_standard_deviation, - ); - - let custom_unit_dose = format_dose( - ingestion.dose, - &custom_unit.unit, - ingestion.is_estimate, - ingestion.estimate_standard_deviation, - ); - - format!("{estimate}{canonical_dose} ({custom_unit_dose_per} * {custom_unit_dose})") - } else { - format_dose(Some(dose), &unit, ingestion.is_estimate, standard_deviation) - } - } else { - "Unknown".to_string() - } -} - -pub fn format_ingestion_time(ingestion: &Ingestion) -> String { - ingestion.ingestion_time.format("%a %I:%M %p").to_string() -} - -pub fn format_ingestion_roa(ingestion: &Ingestion, custom_units: &CustomUnitsType) -> String { - if let Some(custom_unit_id) = ingestion.custom_unit_id { - let custom_unit = custom_units - .get_by_id(custom_unit_id) - .expect("custom unit not found"); - - format!("{:?} ({})", ingestion.roa, custom_unit.name) - } else { - format!("{:?}", ingestion.roa) - } -} -/* - - - - -def formatIngestionROA($customUnits; $substitutions): - . as $ingestion | - $ingestion.administrationRoute as $roa | - - - $ingestion.customUnitId as $customUnitId | - - if - $customUnitId == null - then - $roaText - else - $customUnits | map(select(.id == $customUnitId))[0] as $customUnit | - "\($roaText) (\($customUnit.name))" - end; - -def formatIngestionROA($customUnits): formatIngestionROA($customUnits; {}); - -def formatIngestionInfo: - . as $ingestionInfo | - formatDose(.dose; .unit; .isEstimate; .standardDeviation); - -*/ diff --git a/psychonaut_journal_types/Cargo.lock b/psychonaut_journal_types/Cargo.lock new file mode 100644 index 0000000..9bf3e2b --- /dev/null +++ b/psychonaut_journal_types/Cargo.lock @@ -0,0 +1,65 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psychonaut_journal_types" +version = "0.1.0" +dependencies = [ + "serde", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" diff --git a/psychonaut_journal_types/Cargo.toml b/psychonaut_journal_types/Cargo.toml index fe4a412..52a5957 100644 --- a/psychonaut_journal_types/Cargo.toml +++ b/psychonaut_journal_types/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "journal" +name = "psychonaut_journal_types" version = "0.1.0" edition = "2021" diff --git a/psychonaut_journal_types/src/types.rs b/psychonaut_journal_types/src/lib.rs similarity index 93% rename from psychonaut_journal_types/src/types.rs rename to psychonaut_journal_types/src/lib.rs index b64baf9..af5a8a6 100644 --- a/psychonaut_journal_types/src/types.rs +++ b/psychonaut_journal_types/src/lib.rs @@ -63,9 +63,9 @@ pub struct Experience { pub title: String, pub text: String, #[serde(rename = "creationDate")] - pub creation_time: DateTime, + pub creation_time: u64, #[serde(rename = "sortDate")] - pub modified_time: DateTime, + pub modified_time: u64, pub ingestions: Vec, } @@ -83,7 +83,7 @@ pub struct CustomUnit { pub substance_name: String, pub name: String, #[serde(rename = "creationDate")] - pub creation_time: DateTime, + pub creation_time: u64, pub administration_route: AdministrationRoute, pub dose: f64, pub unit: String, @@ -97,8 +97,8 @@ pub struct CustomUnit { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct ExportData { - pub experiences: Vec, + pub experiences: Vec, pub substance_companions: Vec, pub custom_substances: Vec, - pub custom_units: Vec, + pub custom_units: Vec, }