This commit is contained in:
chaos 2024-11-20 19:15:07 +00:00
parent 4b7244e4b3
commit e034d2387b
17 changed files with 916 additions and 382 deletions

26
Cargo.lock generated
View file

@ -279,9 +279,18 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.11" version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "js-sys" name = "js-sys"
@ -373,12 +382,21 @@ version = "0.1.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"clap", "clap",
"journal",
"log", "log",
"prettytable-rs", "prettytable-rs",
"psychonaut_journal_types",
"serde", "serde",
"serde_json", "serde_json",
] ]
[[package]]
name = "psychonaut_journal_types"
version = "0.1.0"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.37" version = "1.0.37"
@ -433,9 +451,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.132" version = "1.0.133"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",

View file

@ -1,24 +1,8 @@
[package] [workspace]
name = "psychonaut_journal_cli" resolver = "2"
version = "0.1.0"
edition = "2021"
[[bin]] members = [
name = "journal-cli" "psychonaut_journal_types",
path = "journal_cli/src/main.rs" "journal",
"journal_cli"
[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"

330
journal/Cargo.lock generated Normal file
View file

@ -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"

View file

@ -6,4 +6,4 @@ edition = "2021"
[dependencies] [dependencies]
serde = { version = "1.0.215", features = ["std", "derive", "serde_derive"] } serde = { version = "1.0.215", features = ["std", "derive", "serde_derive"] }
chrono = { version = "0.4.38", features = ["serde"] } chrono = { version = "0.4.38", features = ["serde"] }
chrono-tz = { version = "0.10.0", features = ["serde"] } psychonaut_journal_types = { path = "../psychonaut_journal_types" }

View file

@ -1,20 +1,4 @@
use crate::types::{CustomUnits, CustomUnitsType, Ingestion}; use crate::types::{CustomUnit, Estimation, IngestionDose, StandardIngestionDose};
pub fn ingestion_dose(ingestion: &Ingestion, custom_units: &CustomUnitsType) -> Option<f64> {
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
}
}
fn add_standard_deviation( fn add_standard_deviation(
expectation_x: f64, expectation_x: f64,
@ -41,69 +25,48 @@ fn add_standard_deviation(
} }
} }
pub fn ingestion_contains_estimate(ingestion: &Ingestion, custom_units: &CustomUnitsType) -> bool { pub fn canonical_dose(dose: &IngestionDose, custom_unit: Option<&CustomUnit>) -> IngestionDose {
if ingestion.is_estimate { if let IngestionDose::Custom(dose) = dose {
return true; 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 estimation = match (&dose.estimation, &custom_unit_dose.estimation) {
let custom_unit = custom_units (
.get_by_id(custom_unit_id) &Estimation::StandardDeviation(dose_standard_deviation),
.expect("Custom Unit could not be found"); &Estimation::StandardDeviation(custom_unit_standard_deviation),
) => {
custom_unit.is_estimate let result = add_standard_deviation(
} else { dose.dose,
false dose_standard_deviation,
} custom_unit_dose.dose,
} custom_unit_standard_deviation,
);
pub fn ingestion_standard_deviation( if let Some(result) = result {
ingestion: &Ingestion, Estimation::StandardDeviation(result)
custom_units: &CustomUnitsType, } else {
) -> Option<f64> { Estimation::Estimate
if ingestion.dose.is_none() { }
return None; }
} (&Estimation::StandardDeviation(dose_standard_deviation), _) => {
Estimation::StandardDeviation(dose_standard_deviation)
if !ingestion_contains_estimate(ingestion, custom_units) { }
return None; (_, &Estimation::StandardDeviation(custom_unit_standard_deviation)) => {
} Estimation::StandardDeviation(custom_unit_standard_deviation)
}
if let Some(custom_unit_id) = ingestion.custom_unit_id { (Estimation::Precise, Estimation::Precise) => Estimation::Precise,
let custom_unit = custom_units _ => Estimation::Estimate,
.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;
}; };
if ingestion.estimate_standard_deviation.is_none() { IngestionDose::Standard(StandardIngestionDose {
return custom_unit.estimate_standard_deviation; dose: dose.dose * custom_unit_dose.dose,
} unit: custom_unit.original_unit.clone(),
estimation,
return add_standard_deviation( })
ingestion.dose.unwrap(),
ingestion.estimate_standard_deviation.unwrap(),
custom_unit.dose,
custom_unit.estimate_standard_deviation.unwrap(),
);
} else { } else {
return ingestion.estimate_standard_deviation; dose.clone()
} }
} }
pub fn ingestion_unit( pub fn canonical_unit(dose: &IngestionDose, custom_unit: Option<&CustomUnit>) -> String {
ingestion: &Ingestion, canonical_dose(dose, custom_unit).unit()
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()
}
}

View file

@ -1,2 +1,2 @@
pub mod types; pub mod types;
pub mod helpers; pub mod helpers;

View file

@ -1,95 +1,177 @@
use chrono::serde::ts_milliseconds; use chrono::{DateTime, TimeZone, Utc};
use chrono::{DateTime, Utc}; use std::collections::HashMap;
use serde::{Deserialize, Serialize}; use std::fmt::{Debug, Display};
use std::fmt::Debug; use std::str::FromStr;
use std::fmt::Display;
#[derive(Serialize, Deserialize, Debug, Clone)] pub type AdministrationRoute = psychonaut_journal_types::AdministrationRoute;
#[serde(rename_all = "UPPERCASE")]
pub enum AdministrationRoute { #[derive(PartialEq, Debug, Clone)]
Oral, pub enum Estimation {
Sublingual, Precise,
Buccal, Estimate,
Insufflated, StandardDeviation(f64),
Rectal,
Transdermal,
Subcutaneous,
Intramuscular,
Intravenous,
Smoked,
Inhaled,
} }
impl Display for AdministrationRoute { impl Estimation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { pub fn is_precise(&self) -> bool {
write!(f, "{:?}", self) 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)] #[derive(Debug, Clone)]
#[serde(rename_all = "camelCase")] 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<Self, Self::Err> {
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 struct Ingestion {
pub substance_name: String, pub substance_name: String,
#[serde(with = "ts_milliseconds", rename = "time")]
pub ingestion_time: DateTime<Utc>, pub ingestion_time: DateTime<Utc>,
#[serde(with = "ts_milliseconds", rename = "creationDate")]
pub creation_time: DateTime<Utc>, pub creation_time: DateTime<Utc>,
pub dose: Option<f64>, pub dose: IngestionDose,
#[serde(rename = "units")]
pub unit: String,
#[serde(rename = "isDoseAnEstimate")]
pub is_estimate: bool,
#[serde(rename = "estimatedDoseStandardDeviation")]
pub estimate_standard_deviation: Option<f64>,
pub custom_unit_id: Option<i64>,
#[serde(rename = "administrationRoute")]
pub roa: AdministrationRoute, pub roa: AdministrationRoute,
pub consumer_name: Option<String>, pub consumer: Consumer,
pub notes: String, pub notes: String,
pub stomach_fullness: Option<String>, pub stomach_fullness: Option<String>,
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct CustomSubstance { pub struct CustomSubstance {
pub name: String, pub name: String,
pub description: String, pub description: String,
pub units: String, pub units: String,
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Experience { pub struct Experience {
pub title: String, pub title: String,
pub text: String, pub text: String,
#[serde(with = "ts_milliseconds", rename = "creationDate")]
pub creation_time: DateTime<Utc>, pub creation_time: DateTime<Utc>,
#[serde(with = "ts_milliseconds", rename = "sortDate")]
pub modified_time: DateTime<Utc>, pub modified_time: DateTime<Utc>,
pub ingestions: Vec<Ingestion>, pub ingestions: Vec<Ingestion>,
} }
pub type ExperiencesType = Vec<Experience>; #[derive(Debug, Clone)]
pub struct CustomUnit {
pub id: i64,
pub name: String,
pub substance_name: String,
pub trait Experiences { pub administration_route: AdministrationRoute,
fn filter_by_title(&self, title: &String) -> Vec<Experience>; pub dose: StandardIngestionDose,
fn get_by_title(&self, title: &String) -> Option<Experience>; pub original_unit: String,
pub creation_time: DateTime<Utc>,
pub is_archived: bool,
} }
impl Experiences for ExperiencesType { #[derive(Debug, Default, Clone)]
fn filter_by_title(&self, title: &String) -> Vec<Experience> { pub struct Journal {
self.iter() pub experiences: Vec<Experience>,
.filter_map(|experience| { //pub substance_colours: HashMap<String, String>,
if &experience.title == title { //pub custom_substances: HashMap<String, CustomSubstance>,
Some(experience.clone()) pub custom_units: HashMap<i64, CustomUnit>,
} else { }
None
} impl Journal {
}) pub fn get_custom_unit(&self, id: i64) -> Option<CustomUnit> {
.collect() self.custom_units.get(&id).cloned()
} }
fn get_by_title(&self, title: &String) -> Option<Experience> {
for experience in self.iter() { pub fn maybe_get_custom_unit_for(&self, ingestion: &Ingestion) -> Option<CustomUnit> {
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<Experience> {
for experience in self.experiences.iter() {
if &experience.title == title { if &experience.title == title {
return Some(experience.clone()); return Some(experience.clone());
} }
@ -97,56 +179,116 @@ impl Experiences for ExperiencesType {
None None
} }
}
#[derive(Serialize, Deserialize, Debug, Clone)] pub fn import(data: psychonaut_journal_types::ExportData) -> Self {
#[serde(rename_all = "camelCase")] fn from_unix_millis(time: u64) -> DateTime<Utc> {
pub struct SubstanceCompanion { Utc.timestamp_millis_opt(time as i64).unwrap()
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<Utc>,
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<f64>,
pub is_archived: bool,
}
pub type CustomUnitsType = Vec<CustomUnit>;
pub trait CustomUnits {
fn get_by_id(&self, id: i64) -> Option<CustomUnit>;
}
impl CustomUnits for CustomUnitsType {
fn get_by_id(&self, id: i64) -> Option<CustomUnit> {
for custom_unit in self.iter() {
if custom_unit.id == id {
return Some(custom_unit.clone());
}
} }
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<Ingestion> = 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<SubstanceCompanion>,
pub custom_substances: Vec<CustomSubstance>,
pub custom_units: CustomUnitsType,
}

23
journal_cli/Cargo.toml Normal file
View file

@ -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"

View file

@ -1,9 +1,9 @@
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
#[derive(Debug, Clone, Subcommand)] #[derive(Debug, Clone, Subcommand)]
#[clap(rename_all = "camelCase")] #[clap(rename_all = "camelCase")]
pub enum Commands { pub enum Commands {
PrintExperience(crate::commands::print_experience::PrintExperienceArgs) PrintExperience(crate::commands::print_experience::PrintExperienceArgs),
} }
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
@ -11,4 +11,4 @@ pub enum Commands {
pub struct Args { pub struct Args {
#[clap(subcommand)] #[clap(subcommand)]
pub command: Commands, pub command: Commands,
} }

View file

@ -1,44 +1,62 @@
use std::str::FromStr;
use journal::types::Consumer;
use crate::args::Args; use crate::args::Args;
use crate::utils::{ use crate::display::print_ingestion_log;
format_ingestion_dose, format_ingestion_roa, format_ingestion_time, load_export_data, use crate::formatting::format_experience_title;
}; use crate::utils::load_journal;
use journal;
use journal::types::Experiences;
#[derive(Debug, Clone, clap::Args)] #[derive(Debug, Clone, clap::Args)]
pub struct PrintExperienceArgs { pub struct PrintExperienceArgs {
pub experience_title: String, pub experience_title: String,
#[clap(long, env = "EXPORT_FILE")] #[clap(long, env = "EXPORT_FILE")]
pub export_file: String, pub export_file: String,
#[clap(long)]
pub substance_filter: Option<Vec<String>>,
#[clap(long, value_delimiter = ',')]
pub consumer_filter: Option<Vec<String>>,
}
pub fn parse_consumer_filter(consumer_filter: Option<Vec<String>>) -> Option<Vec<Consumer>> {
match consumer_filter {
Some(consumer_filter) => {
let mut consumers: Vec<Consumer> = 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( pub fn print_experience(
_global_args: &Args, _global_args: &Args,
args: &PrintExperienceArgs, args: &PrintExperienceArgs,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
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 let experience = journal
.experiences .first_experience_by_title(&args.experience_title)
.get_by_title(&args.experience_title)
.expect("could not find experience"); .expect("could not find experience");
for ingestion in experience.ingestions.iter() { println!("{}", format_experience_title(&experience));
println!(
"{}|{}|{}|{}|{}", let substance_filter = args.substance_filter.clone();
ingestion.substance_name, let consumer_filter = parse_consumer_filter(args.consumer_filter.clone());
format_ingestion_dose(&ingestion, &export_data.custom_units),
format_ingestion_roa(&ingestion, &export_data.custom_units), print_ingestion_log(
ingestion &journal,
.consumer_name &experience,
.clone() substance_filter.as_ref(),
.or(Some("default".to_string())) consumer_filter.as_ref(),
.unwrap(), );
format_ingestion_time(&ingestion)
)
}
Ok(()) Ok(())
} }

View file

@ -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<String>>,
consumer_filter: Option<&Vec<Consumer>>,
) {
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)
)
}
}

View file

@ -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)
}
}

View file

@ -1,21 +1,25 @@
use clap::Parser; use clap::Parser;
use commands::print_experience::print_experience;
pub mod args; pub mod args;
pub mod commands; pub mod commands;
pub mod display;
pub mod formatting;
pub mod utils; pub mod utils;
use commands::print_experience::print_experience;
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = args::Args::parse(); let args = args::Args::parse();
let command = args.command.to_owned(); let command = args.command.to_owned();
//println!("{:#?}", args); println!("{:#?}", args);
match command { match command {
args::Commands::PrintExperience(print_experience_args) => args::Commands::PrintExperience(print_experience_args) => {
print_experience(&args, &print_experience_args)?, print_experience(&args, &print_experience_args)?
} }
}
Ok(()) Ok(())
} }

View file

@ -1,137 +1,14 @@
use journal::{ use journal::types::Journal;
helpers::{ use psychonaut_journal_types::ExportData;
ingestion_contains_estimate, ingestion_dose, ingestion_standard_deviation, ingestion_unit,
},
types::{CustomUnits, CustomUnitsType, Experience, ExportData, Ingestion},
};
use std::fs::File; use std::fs::File;
pub fn load_export_data(filename: &String) -> Result<ExportData, Box<dyn std::error::Error>> { pub fn load_journal(filename: &String) -> Result<Journal, Box<dyn std::error::Error>> {
let file = File::open(filename)?; let file = File::open(filename)?;
let export_data: ExportData = serde_json::from_reader(file)?; let export_data: ExportData = serde_json::from_reader(file)?;
// export_data let journal = Journal::import(export_data);
// .experiences
// .sort_by(|a, b| a.modified_time.cmp(&b.modified_time));
//for experience in export_data.experiences.iter_mut() { Ok(journal)
// experience
// .ingestions
// .sort_by(|a, b| a.ingestion_time.cmp(&b.ingestion_time));
//}
Ok(export_data)
} }
pub fn format_experience_title(experience: &Experience) -> String {
format!("{}: {}", experience.title, experience.creation_time)
}
pub fn format_dose(
dose: Option<f64>,
unit: &String,
estimate: bool,
standard_deviation: Option<f64>,
) -> 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);
*/

65
psychonaut_journal_types/Cargo.lock generated Normal file
View file

@ -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"

View file

@ -1,5 +1,5 @@
[package] [package]
name = "journal" name = "psychonaut_journal_types"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"

View file

@ -63,9 +63,9 @@ pub struct Experience {
pub title: String, pub title: String,
pub text: String, pub text: String,
#[serde(rename = "creationDate")] #[serde(rename = "creationDate")]
pub creation_time: DateTime<Utc>, pub creation_time: u64,
#[serde(rename = "sortDate")] #[serde(rename = "sortDate")]
pub modified_time: DateTime<Utc>, pub modified_time: u64,
pub ingestions: Vec<Ingestion>, pub ingestions: Vec<Ingestion>,
} }
@ -83,7 +83,7 @@ pub struct CustomUnit {
pub substance_name: String, pub substance_name: String,
pub name: String, pub name: String,
#[serde(rename = "creationDate")] #[serde(rename = "creationDate")]
pub creation_time: DateTime<Utc>, pub creation_time: u64,
pub administration_route: AdministrationRoute, pub administration_route: AdministrationRoute,
pub dose: f64, pub dose: f64,
pub unit: String, pub unit: String,
@ -97,8 +97,8 @@ pub struct CustomUnit {
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ExportData { pub struct ExportData {
pub experiences: Vec<Experiences>, pub experiences: Vec<Experience>,
pub substance_companions: Vec<SubstanceCompanion>, pub substance_companions: Vec<SubstanceCompanion>,
pub custom_substances: Vec<CustomSubstance>, pub custom_substances: Vec<CustomSubstance>,
pub custom_units: Vec<CustomUnits>, pub custom_units: Vec<CustomUnit>,
} }