diff --git a/journal/src/journal.rs b/journal/src/journal.rs index 87fba26..4ceba26 100644 --- a/journal/src/journal.rs +++ b/journal/src/journal.rs @@ -1,9 +1,6 @@ use rand::Rng; -use crate::types::{ - CustomUnit, Experience, ExportFormat, Ingestion, - Unit, -}; +use crate::types::{CustomUnit, Experience, ExportFormat, Ingestion, Unit}; pub type JournalType = Box; @@ -52,7 +49,7 @@ impl Journal for InMemJournal { fn first_experience_by_title(&self, title: &str) -> Option<&Experience> { self.experiences .values() - .find(|&experience| &experience.title == title) + .find(|&experience| experience.title == title) } fn import_psychonaut(&mut self, data: psychonaut_journal_types::ExportData) { diff --git a/journal/src/types/dose_with_unit.rs b/journal/src/types/dose_with_unit.rs new file mode 100644 index 0000000..fd375d1 --- /dev/null +++ b/journal/src/types/dose_with_unit.rs @@ -0,0 +1,227 @@ +use std::fmt::Display; + +use super::{Dose, Estimation, Unit}; + +pub fn format_dose(dose: &Dose, unit: &Unit) -> String { + format!("{}", DoseWithUnitRefs { dose, unit }) +} + +#[derive(Default, PartialEq, Debug, Clone)] +pub struct DoseWithUnit { + pub dose: Dose, + pub unit: Unit, +} +pub(crate) struct DoseWithUnitRefs<'a> { + pub dose: &'a Dose, + pub unit: &'a Unit, +} + +impl Display for DoseWithUnit { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + DoseWithUnitRefs { + dose: &self.dose, + unit: &self.unit + } + ) + } +} + +impl Display for DoseWithUnitRefs<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let dose = self.dose; + let unit = self.unit; + + match unit { + Unit::Simple(unit) => { + let is_unknown = dose.contains_unknown && dose.value == 0.0; + let is_estimate = dose.estimation.is_estimate() && !is_unknown; + + if is_estimate { + f.write_str("~")?; + } + + if is_unknown { + f.write_str("Unknown")? + } else { + let dose_rounded = (dose.value * 100.0).round() / 100.0; + write!(f, "{}", dose_rounded)?; + + if dose.contains_unknown { + f.write_str("+Unknown")?; + } + } + + if let Estimation::StandardDeviation(deviation) = dose.estimation { + write!(f, "±{}", (deviation.deviation * 100.0).round() / 100.0)?; + } + + f.write_str((" ".to_string() + unit).as_str()) + } + Unit::Custom { id: _id, unit } => { + let unit = unit.clone().unwrap(); + let unit_unit = Unit::Simple(unit.unit.clone()); + + let ingestion_dose = dose * &unit.dose; + let ingestion_unit = Unit::Simple(unit.original_unit.clone()); + + // ingestion dose + write!( + f, + "{}", + DoseWithUnitRefs { + dose: &ingestion_dose, + unit: &ingestion_unit + } + )?; + + f.write_str(" (")?; + + // dose per unit + write!( + f, + "{}", + DoseWithUnitRefs { + dose: &unit.dose, + unit: &ingestion_unit + } + )?; + + f.write_str(" * ")?; + + // custom unit dose + write!( + f, + "{}", + DoseWithUnitRefs { + dose, + unit: &unit_unit + } + )?; + + f.write_str(")") + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::types::{AdministrationRoute, CustomUnit, StandardDeviation}; + + use super::*; + + #[test] + fn format_dose() { + let result = super::format_dose( + &Dose { + value: 0.0, + contains_unknown: false, + estimation: Estimation::Precise, + }, + &Unit::Simple("mg".to_string()), + ); + assert_eq!(result, "0 mg"); + } + + #[test] + fn format_dose_non_refs() { + let result = format!( + "{}", + DoseWithUnit { + dose: Dose { + value: 0.0, + contains_unknown: false, + estimation: Estimation::Precise, + }, + unit: Unit::Simple("mg".to_string()) + } + ); + assert_eq!(result, "0 mg"); + } + + #[test] + fn format_dose_estimate() { + let result = super::format_dose( + &Dose { + value: 0.0, + contains_unknown: false, + estimation: Estimation::Estimate, + }, + &Unit::Simple("mg".to_string()), + ); + assert_eq!(result, "~0 mg"); + } + + #[test] + fn format_dose_contains_unknown() { + let result = super::format_dose( + &Dose { + value: 10.0, + contains_unknown: true, + estimation: Estimation::Estimate, + }, + &Unit::Simple("mg".to_string()), + ); + assert_eq!(result, "~10+Unknown mg"); + } + + #[test] + fn format_dose_standard_derivation() { + let result = super::format_dose( + &Dose { + value: 0.0, + contains_unknown: false, + estimation: Estimation::StandardDeviation(StandardDeviation { + expectation: 0.0, + deviation: 10.0, + }), + }, + &Unit::Simple("mg".to_string()), + ); + assert_eq!(result, "~0±10 mg"); + } + + #[test] + fn format_unknown_dose() { + let result = super::format_dose( + &Dose { + value: 0.0, + contains_unknown: true, + estimation: Estimation::Estimate, + }, + &Unit::Simple("mg".to_string()), + ); + assert_eq!(result, "Unknown mg"); + } + + #[test] + fn format_unknown_dose_with_custom_unit() { + let result = super::format_dose( + &Dose { + value: 0.0, + contains_unknown: true, + estimation: Estimation::Estimate, + }, + &Unit::Custom { + id: 0, + unit: Some(CustomUnit { + name: "Beverage".to_string(), + substance_name: "Caffeine".to_string(), + unit: "sip".to_string(), + original_unit: "mg".to_string(), + dose: Dose { + value: 10.0, + contains_unknown: false, + estimation: Estimation::Estimate, + }, + administration_route: AdministrationRoute::Oral, + ..CustomUnit::default() + }), + }, + ); + assert_eq!(result, "Unknown mg (~10 mg * Unknown sip)"); + } +} diff --git a/journal/src/types/estimate.rs b/journal/src/types/estimate.rs index 42f7947..567d448 100644 --- a/journal/src/types/estimate.rs +++ b/journal/src/types/estimate.rs @@ -1,15 +1,13 @@ use std::ops::{Add, Mul}; -#[derive(PartialEq, Debug, Copy, Clone)] -#[derive(Default)] +#[derive(PartialEq, Debug, Copy, Clone, Default)] pub enum Estimation { #[default] - Precise, + Precise, Estimate, StandardDeviation(StandardDeviation), } - #[derive(PartialEq, Default, Debug, Copy, Clone)] pub struct StandardDeviation { pub expectation: f64, diff --git a/journal/src/types/experience.rs b/journal/src/types/experience.rs index 82b277c..2aae759 100644 --- a/journal/src/types/experience.rs +++ b/journal/src/types/experience.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use super::from_unix_millis; -#[derive(PartialEq, Debug, Clone)] +#[derive(PartialEq, Default, Debug, Clone)] pub struct Experience { pub id: i64, pub title: String, diff --git a/journal/src/types/ingestion.rs b/journal/src/types/ingestion.rs index 31afa4c..ad6b8c5 100644 --- a/journal/src/types/ingestion.rs +++ b/journal/src/types/ingestion.rs @@ -5,7 +5,7 @@ use super::{ Unit, }; -#[derive(PartialEq, Debug, Clone)] +#[derive(PartialEq, Default, Debug, Clone)] pub struct Ingestion { pub substance_name: String, pub ingestion_time: DateTime, diff --git a/journal/src/types/mod.rs b/journal/src/types/mod.rs index 2dd8a50..1fc116d 100644 --- a/journal/src/types/mod.rs +++ b/journal/src/types/mod.rs @@ -5,6 +5,12 @@ pub use estimate::{Estimation, StandardDeviation}; mod dose; pub use dose::Dose; +mod unit; +pub use unit::Unit; + +mod dose_with_unit; +pub use dose_with_unit::{format_dose, DoseWithUnit}; + mod consumer; pub use consumer::Consumer; @@ -23,9 +29,6 @@ pub use custom_unit::CustomUnit; mod export; pub use export::ExportFormat; -mod unit; -pub use unit::Unit; - pub type AdministrationRoute = psychonaut_journal_types::AdministrationRoute; pub(crate) fn from_unix_millis(time: u64) -> DateTime { diff --git a/journal/src/types/unit.rs b/journal/src/types/unit.rs index 0b30f77..4573969 100644 --- a/journal/src/types/unit.rs +++ b/journal/src/types/unit.rs @@ -5,3 +5,9 @@ pub enum Unit { Simple(String), Custom { id: i64, unit: Option }, } + +impl Default for Unit { + fn default() -> Self { + Unit::Simple("mg".to_string()) + } +} diff --git a/journal_cli/src/display.rs b/journal_cli/src/display.rs index f1c4451..13bd95e 100644 --- a/journal_cli/src/display.rs +++ b/journal_cli/src/display.rs @@ -1,9 +1,9 @@ use journal::{ journal::JournalType, - types::{Consumer, Experience}, + types::{format_dose, Consumer, Experience}, }; -use crate::formatting::{format_ingestion_dose, format_ingestion_roa, format_ingestion_time}; +use crate::formatting::{format_ingestion_roa, format_ingestion_time}; pub fn print_ingestion_log( journal: &JournalType, @@ -34,7 +34,7 @@ pub fn print_ingestion_log( println!( "{}|{}|{}|{}|{}", ingestion.substance_name, - format_ingestion_dose(&ingestion.dose, &unit), + format_dose(&ingestion.dose, &unit), format_ingestion_roa(ingestion, &unit), ingestion.consumer, format_ingestion_time(ingestion) diff --git a/journal_cli/src/formatting.rs b/journal_cli/src/formatting.rs index ef7ffd7..c61573b 100644 --- a/journal_cli/src/formatting.rs +++ b/journal_cli/src/formatting.rs @@ -1,60 +1,9 @@ -use journal::types::{Dose, Estimation, Experience, Ingestion, Unit}; +use journal::types::{Experience, Ingestion, Unit}; pub fn format_experience_title(experience: &Experience) -> String { format!("{}: {}", experience.title, experience.creation_time) } -pub fn format_ingestion_dose(dose: &Dose, unit: &Unit) -> String { - if dose.value != 0.0 && !dose.contains_unknown { - match unit { - Unit::Simple(unit) => { - let is_estimate = dose.estimation.is_estimate(); - - let estimate = if is_estimate { "~" } else { "" }; - let standard_deviation = { - if let Estimation::StandardDeviation(deviation) = dose.estimation { - format!("±{}", (deviation.deviation * 100.0).round() / 100.0) - } else { - "".to_string() - } - }; - let unknown = if dose.contains_unknown { - "+Unknown" - } else { - "" - }; - let dose = (dose.value * 100.0).round() / 100.0; - - format!("{estimate}{dose}{standard_deviation}{unknown} {unit}") - } - Unit::Custom { id: _id, unit } => { - let unit = unit.clone().unwrap(); - let unit_unit = Unit::Simple(unit.unit.clone()); - - let ingestion_dose = dose * &unit.dose; - - let ingestion_unit = Unit::Simple(unit.original_unit.clone()); - - let ingestion_dose = format_ingestion_dose(&ingestion_dose, &ingestion_unit); - - let dose_per_unit = format_ingestion_dose(&unit.dose, &ingestion_unit); - - let custom_unit_dose = format_ingestion_dose(dose, &unit_unit); - - format!("{ingestion_dose} ({dose_per_unit} * {custom_unit_dose})") - } - } - } else { - format!( - "Unknown {}", - match unit { - Unit::Simple(unit) => unit, - Unit::Custom { id: _id, unit } => &unit.as_ref().unwrap().original_unit, - } - ) - } -} - pub fn format_ingestion_time(ingestion: &Ingestion) -> String { ingestion.ingestion_time.format("%a %I:%M %p").to_string() } @@ -69,39 +18,57 @@ pub fn format_ingestion_roa(ingestion: &Ingestion, unit: &Unit) -> String { #[cfg(test)] mod tests { - use journal::types::CustomUnit; + use chrono::{TimeZone, Utc}; + use journal::types::{AdministrationRoute, CustomUnit}; use super::*; #[test] - fn format_unknown_dose() { - let result = format_ingestion_dose( - &Dose { - value: 0.0, - contains_unknown: true, - estimation: Estimation::Estimate, - }, - &Unit::Simple("mg".to_string()), - ); - assert_eq!(result, "Unknown mg"); + fn format_experience_title() { + let result = super::format_experience_title(&Experience { + title: "Title".to_string(), + creation_time: Utc.timestamp_millis_opt(0).unwrap(), + ..Experience::default() + }); + assert_eq!(result, "Title: 1970-01-01 00:00:00 UTC"); } #[test] - fn format_unknown_dose_custom_unit() { - let result = format_ingestion_dose( - &Dose { - value: 0.0, - contains_unknown: true, - estimation: Estimation::Estimate, + fn format_ingestion_time() { + let result = super::format_ingestion_time(&Ingestion { + creation_time: Utc.timestamp_millis_opt(0).unwrap(), + ..Ingestion::default() + }); + assert_eq!(result, "Thu 12:00 AM"); + } + + #[test] + fn format_ingestion_roa() { + let result = super::format_ingestion_roa( + &Ingestion { + roa: AdministrationRoute::Oral, + ..Ingestion::default() + }, + &Unit::default(), + ); + assert_eq!(result, "Oral"); + } + + #[test] + fn format_ingestion_roa_with_custom_unit() { + let result = super::format_ingestion_roa( + &Ingestion { + roa: AdministrationRoute::Oral, + ..Ingestion::default() }, &Unit::Custom { id: 0, unit: Some(CustomUnit { - original_unit: "mg".to_string(), + name: "32mg/ml".to_string(), ..CustomUnit::default() }), }, ); - assert_eq!(result, "Unknown mg"); + assert_eq!(result, "Oral (32mg/ml)"); } } diff --git a/psychonaut_journal_types/src/lib.rs b/psychonaut_journal_types/src/lib.rs index 8554be4..26d976c 100644 --- a/psychonaut_journal_types/src/lib.rs +++ b/psychonaut_journal_types/src/lib.rs @@ -1,8 +1,6 @@ use serde::{Deserialize, Serialize}; use std::fmt::{Debug, Display}; -mod tests; - #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] #[serde(rename_all = "UPPERCASE")] pub enum AdministrationRoute { @@ -119,3 +117,28 @@ pub struct ExportData { pub custom_substances: Vec, pub custom_units: Vec, } + +#[cfg(test)] +mod tests { + use super::{AdministrationRoute::*, *}; + + #[test] + fn default_administration_dose() { + assert_eq!(AdministrationRoute::default(), AdministrationRoute::Oral); + } + + #[test] + fn administration_dose_to_string() { + assert_eq!(Oral.to_string(), "Oral"); + assert_eq!(Sublingual.to_string(), "Sublingual"); + assert_eq!(Buccal.to_string(), "Buccal"); + assert_eq!(Insufflated.to_string(), "Insufflated"); + assert_eq!(Rectal.to_string(), "Rectal"); + assert_eq!(Transdermal.to_string(), "Transdermal"); + assert_eq!(Subcutaneous.to_string(), "Subcutaneous"); + assert_eq!(Intramuscular.to_string(), "Intramuscular"); + assert_eq!(Intravenous.to_string(), "Intravenous"); + assert_eq!(Smoked.to_string(), "Smoked"); + assert_eq!(Inhaled.to_string(), "Inhaled"); + } +} diff --git a/psychonaut_journal_types/src/tests.rs b/psychonaut_journal_types/src/tests.rs deleted file mode 100644 index 7f6e481..0000000 --- a/psychonaut_journal_types/src/tests.rs +++ /dev/null @@ -1,24 +0,0 @@ -#[cfg(test)] -mod tests { - use crate::AdministrationRoute::{self, *}; - - #[test] - fn default_administration_dose() { - assert_eq!(AdministrationRoute::default(), AdministrationRoute::Oral); - } - - #[test] - fn administration_dose_to_string() { - assert_eq!(Oral.to_string(), "Oral"); - assert_eq!(Sublingual.to_string(), "Sublingual"); - assert_eq!(Buccal.to_string(), "Buccal"); - assert_eq!(Insufflated.to_string(), "Insufflated"); - assert_eq!(Rectal.to_string(), "Rectal"); - assert_eq!(Transdermal.to_string(), "Transdermal"); - assert_eq!(Subcutaneous.to_string(), "Subcutaneous"); - assert_eq!(Intramuscular.to_string(), "Intramuscular"); - assert_eq!(Intravenous.to_string(), "Intravenous"); - assert_eq!(Smoked.to_string(), "Smoked"); - assert_eq!(Inhaled.to_string(), "Inhaled"); - } -}