1use jiff::civil::Date;
21use serde::{Deserialize, Serialize};
22
23pub const BEFORE: &str = "X-SIOUL-BEFORE";
24pub const AFTER: &str = "X-SIOUL-AFTER";
25pub const COST: &str = "X-SIOUL-COST";
26pub const GAIN: &str = "X-SIOUL-GAIN";
27pub const FELT_COST: &str = "X-SIOUL-FELT-COST";
29pub const FELT_GAIN: &str = "X-SIOUL-FELT-GAIN";
30pub const FELT_ON: &str = "X-SIOUL-ON";
32
33#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
35pub struct Margins {
36 #[serde(default)]
37 pub before: u32,
38 #[serde(default)]
39 pub after: u32,
40}
41
42impl Margins {
43 pub fn is_empty(self) -> bool {
44 self.before == 0 && self.after == 0
45 }
46}
47
48pub fn minutes_of(value: &str) -> u32 {
50 value.trim().parse::<u32>().unwrap_or(0).min(24 * 60)
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
56#[serde(rename_all = "lowercase")]
57pub enum Level {
58 Light,
59 Usual,
60 Heavy,
61 Rest,
63}
64
65impl Level {
66 pub fn energy(self) -> &'static str {
68 match self {
69 Level::Light => "light",
70 Level::Usual => "",
71 Level::Heavy => "heavy",
72 Level::Rest => "rest",
73 }
74 }
75
76 pub fn id(self) -> &'static str {
78 match self {
79 Level::Light => "light",
80 Level::Usual => "usual",
81 Level::Heavy => "heavy",
82 Level::Rest => "rest",
83 }
84 }
85
86 pub fn of_energy(word: &str) -> Level {
88 match word {
89 "light" => Level::Light,
90 "heavy" => Level::Heavy,
91 "rest" => Level::Rest,
92 _ => Level::Usual,
93 }
94 }
95
96 pub fn rating(self) -> f32 {
100 match self {
101 Level::Light | Level::Rest => 2.0,
102 Level::Usual => 4.0,
103 Level::Heavy => 7.0,
104 }
105 }
106
107 pub const HEAVY_FROM: u8 = 7;
109 pub const LIGHT_TOP: u8 = 3;
110 pub const GIVES_FROM: u8 = 5;
112
113 pub fn of(highest: u8, gain: Option<u8>) -> Level {
115 if highest <= Level::LIGHT_TOP && gain.is_some_and(|g| g >= Level::GIVES_FROM) {
116 Level::Rest
117 } else if highest <= Level::LIGHT_TOP {
118 Level::Light
119 } else if highest >= Level::HEAVY_FROM {
120 Level::Heavy
121 } else {
122 Level::Usual
123 }
124 }
125}
126
127#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
129pub struct Demands {
130 #[serde(default)]
131 pub cognitive: Option<u8>,
132 #[serde(default)]
133 pub emotional: Option<u8>,
134 #[serde(default)]
135 pub anxiety: Option<u8>,
136 #[serde(default)]
138 pub body: Option<u8>,
139 #[serde(default)]
140 pub gain: Option<u8>,
141}
142
143pub const COST_NAMES: [&str; 4] = ["COGNITIVE", "EMOTIONAL", "ANXIETY", "BODY"];
145
146impl Demands {
147 pub fn costs(&self) -> [Option<u8>; 4] {
149 [self.cognitive, self.emotional, self.anxiety, self.body]
150 }
151
152 pub fn set_cost(&mut self, index: usize, value: Option<u8>) {
154 let value = value.map(|v| v.min(10));
155 match index {
156 0 => self.cognitive = value,
157 1 => self.emotional = value,
158 2 => self.anxiety = value,
159 _ => self.body = value,
160 }
161 }
162
163 pub fn has_cost(&self) -> bool {
165 self.costs().iter().any(Option::is_some)
166 }
167
168 pub fn is_empty(&self) -> bool {
170 !self.has_cost() && self.gain.is_none()
171 }
172
173 pub fn highest(&self) -> Option<u8> {
175 self.costs().into_iter().flatten().max()
176 }
177
178 pub fn level(&self) -> Option<Level> {
182 self.highest().map(|h| Level::of(h, self.gain))
183 }
184
185 pub fn bounded(&self) -> Demands {
187 let b = |v: Option<u8>| v.map(|v| v.min(10));
188 Demands { cognitive: b(self.cognitive), emotional: b(self.emotional), anxiety: b(self.anxiety), body: b(self.body), gain: b(self.gain) }
189 }
190
191 pub fn cost_value(&self) -> String {
193 COST_NAMES.iter().zip(self.costs()).filter_map(|(name, value)| value.map(|v| format!("{name}={}", v.min(10)))).collect::<Vec<_>>().join(";")
194 }
195
196 pub fn read_cost(&mut self, value: &str) {
198 for part in value.split(';') {
199 let Some((name, number)) = part.split_once('=') else { continue };
200 let number = number.trim().parse::<u8>().ok().map(|n| n.min(10));
201 if let Some(index) = COST_NAMES.iter().position(|n| name.trim().eq_ignore_ascii_case(n)) {
202 self.set_cost(index, number);
203 }
204 }
205 }
206
207 pub fn read_gain(&mut self, value: &str) {
209 self.gain = value.trim().parse::<u8>().ok().map(|n| n.min(10));
210 }
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
215pub struct Felt {
216 pub on: Option<Date>,
218 pub demands: Demands,
219}
220
221pub fn felt_date(value: &str) -> Option<Date> {
223 let value = value.trim();
224 if value.len() != 8 {
225 return None;
226 }
227 Date::strptime("%Y%m%d", value).ok()
228}
229
230pub fn felt_of(lines: &[(String, Option<Date>, String)]) -> Vec<Felt> {
234 let mut out: Vec<Felt> = Vec::new();
235 for (name, on, value) in lines {
236 let index = match out.iter().position(|f| f.on == *on && on.is_some()) {
237 Some(i) => i,
238 None => {
239 out.push(Felt { on: *on, demands: Demands::default() });
240 out.len() - 1
241 }
242 };
243 if name.eq_ignore_ascii_case(FELT_COST) {
244 out[index].demands.read_cost(value);
245 } else if name.eq_ignore_ascii_case(FELT_GAIN) {
246 out[index].demands.read_gain(value);
247 }
248 }
249 out.sort_by_key(|f| f.on);
250 out
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn costs_written_read_and_weighed() {
259 let rated = Demands { cognitive: Some(2), emotional: Some(2), anxiety: Some(9), body: Some(4), gain: Some(1) };
260 assert_eq!(rated.cost_value(), "COGNITIVE=2;EMOTIONAL=2;ANXIETY=9;BODY=4");
261 let mut read = Demands::default();
262 read.read_cost("anxiety=9; cognitive=2 ;EMOTIONAL=2;body=4;OTHER=4");
263 read.read_gain(" 1 ");
264 assert_eq!(read, rated);
265 let mut big = Demands::default();
266 big.read_cost("COGNITIVE=40;BODY=12");
267 assert_eq!((big.cognitive, big.body), (Some(10), Some(10)), "never past 10");
268 assert_eq!(minutes_of(" 20 "), 20);
269 assert_eq!(minutes_of("3000"), 1440);
270 let mut old = Demands::default();
272 old.read_cost("COGNITIVE=3;EMOTIONAL=5;ANXIETY=7");
273 assert_eq!(old.body, None);
274 assert_eq!(Demands { body: Some(6), ..Demands::default() }.cost_value(), "BODY=6");
275 let json: Demands = serde_json::from_str(r#"{"cognitive":3,"gain":5}"#).unwrap();
277 assert_eq!((json.cognitive, json.body, json.gain), (Some(3), None, Some(5)));
278 }
279
280 #[test]
281 fn heaviness_is_the_highest_cost() {
282 let d = |c: Option<u8>, e: Option<u8>, a: Option<u8>, b: Option<u8>, g: Option<u8>| Demands { cognitive: c, emotional: e, anxiety: a, body: b, gain: g };
283 assert_eq!(d(Some(3), None, None, None, None).level(), Some(Level::Light));
285 assert_eq!(d(Some(4), None, None, None, None).level(), Some(Level::Usual));
286 assert_eq!(d(Some(6), None, None, None, None).level(), Some(Level::Usual));
287 assert_eq!(d(Some(7), None, None, None, None).level(), Some(Level::Heavy));
288 assert_eq!(d(Some(0), None, None, None, None).level(), Some(Level::Light));
289 assert_eq!(d(Some(1), Some(1), Some(1), Some(8), None).level(), Some(Level::Heavy));
291 assert_eq!(d(Some(2), Some(2), Some(9), None, Some(8)).level(), Some(Level::Heavy), "a gain never lightens a heavy cost");
292 assert_eq!(d(Some(3), None, None, Some(2), Some(5)).level(), Some(Level::Rest));
294 assert_eq!(d(Some(3), None, None, None, Some(4)).level(), Some(Level::Light));
295 assert_eq!(d(Some(4), None, None, None, Some(9)).level(), Some(Level::Usual));
296 assert_eq!(d(None, None, None, None, Some(9)).level(), None);
298 assert_eq!(Demands::default().level(), None);
299 assert_eq!((Level::Usual.energy(), Level::Rest.energy(), Level::of_energy("heavy"), Level::of_energy("")), ("", "rest", Level::Heavy, Level::Usual));
300 }
301
302 #[test]
303 fn felt_ratings_by_date() {
304 let on = felt_date;
305 let lines = vec![
306 (FELT_COST.to_string(), on("20261006"), "ANXIETY=3;BODY=1".to_string()),
307 (FELT_GAIN.to_string(), on("20261006"), "6".to_string()),
308 (FELT_COST.to_string(), on("20260929"), "ANXIETY=6".to_string()),
309 (FELT_GAIN.to_string(), None, "2".to_string()),
310 ];
311 let felt = felt_of(&lines);
312 assert_eq!(felt.len(), 3);
313 assert_eq!(felt[0], Felt { on: None, demands: Demands { gain: Some(2), ..Demands::default() } }, "undated first, as oldest");
314 assert_eq!(felt[1].demands.anxiety, Some(6));
315 assert_eq!(felt[2], Felt { on: on("20261006"), demands: Demands { anxiety: Some(3), body: Some(1), gain: Some(6), ..Demands::default() } });
316 assert_eq!(felt_date("2026-10-06"), None);
317 }
318}