1use crate::NaiveDate;
10use rust_decimal::Decimal;
11use serde::{Deserialize, Serialize};
12use std::fmt;
13
14use crate::Amount;
15use crate::intern::InternedStr;
16
17#[cfg(feature = "rkyv")]
24use crate::intern::{AsDecimal, AsInternedStr, AsNaiveDate};
25
26#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
48#[cfg_attr(
49 feature = "rkyv",
50 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
51)]
52pub struct Cost {
53 #[cfg_attr(feature = "rkyv", rkyv(with = AsDecimal))]
55 pub number: Decimal,
56 #[cfg_attr(feature = "rkyv", rkyv(with = AsInternedStr))]
58 pub currency: InternedStr,
59 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<AsNaiveDate>))]
61 pub date: Option<NaiveDate>,
62 pub label: Option<String>,
64}
65
66impl Cost {
67 #[must_use]
72 pub fn new(number: Decimal, currency: impl Into<InternedStr>) -> Self {
73 Self {
74 number,
75 currency: currency.into(),
76 date: None,
77 label: None,
78 }
79 }
80
81 #[must_use]
86 pub fn new_calculated(number: Decimal, currency: impl Into<InternedStr>) -> Self {
87 Self::new(number, currency)
88 }
89
90 #[must_use]
92 pub const fn with_date(mut self, date: NaiveDate) -> Self {
93 self.date = Some(date);
94 self
95 }
96
97 #[must_use]
99 pub const fn with_date_opt(mut self, date: Option<NaiveDate>) -> Self {
100 self.date = date;
101 self
102 }
103
104 #[must_use]
106 pub fn with_label(mut self, label: impl Into<String>) -> Self {
107 self.label = Some(label.into());
108 self
109 }
110
111 #[must_use]
113 pub fn with_label_opt(mut self, label: Option<String>) -> Self {
114 self.label = label;
115 self
116 }
117
118 #[must_use]
120 pub fn as_amount(&self) -> Amount {
121 Amount::new(self.number, self.currency.clone())
122 }
123
124 #[must_use]
126 pub fn total_cost(&self, units: Decimal) -> Amount {
127 Amount::new(units * self.number, self.currency.clone())
128 }
129}
130
131impl fmt::Display for Cost {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 write!(f, "{{{} {}", self.number, self.currency)?;
134 if let Some(date) = self.date {
135 write!(f, ", {date}")?;
136 }
137 if let Some(label) = &self.label {
138 write!(f, ", \"{label}\"")?;
139 }
140 write!(f, "}}")
141 }
142}
143
144#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
176#[cfg_attr(
177 feature = "rkyv",
178 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
179)]
180pub struct CostSpec {
181 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<AsDecimal>))]
183 pub number_per: Option<Decimal>,
184 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<AsDecimal>))]
186 pub number_total: Option<Decimal>,
187 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<AsInternedStr>))]
189 pub currency: Option<InternedStr>,
190 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<AsNaiveDate>))]
192 pub date: Option<NaiveDate>,
193 pub label: Option<String>,
195 pub merge: bool,
197}
198
199impl CostSpec {
200 #[must_use]
202 pub fn empty() -> Self {
203 Self::default()
204 }
205
206 #[must_use]
208 pub const fn with_number_per(mut self, number: Decimal) -> Self {
209 self.number_per = Some(number);
210 self
211 }
212
213 #[must_use]
215 pub const fn with_number_total(mut self, number: Decimal) -> Self {
216 self.number_total = Some(number);
217 self
218 }
219
220 #[must_use]
222 pub fn with_currency(mut self, currency: impl Into<InternedStr>) -> Self {
223 self.currency = Some(currency.into());
224 self
225 }
226
227 #[must_use]
229 pub const fn with_date(mut self, date: NaiveDate) -> Self {
230 self.date = Some(date);
231 self
232 }
233
234 #[must_use]
236 pub fn with_label(mut self, label: impl Into<String>) -> Self {
237 self.label = Some(label.into());
238 self
239 }
240
241 #[must_use]
243 pub const fn with_merge(mut self) -> Self {
244 self.merge = true;
245 self
246 }
247
248 #[must_use]
250 pub const fn is_empty(&self) -> bool {
251 self.number_per.is_none()
252 && self.number_total.is_none()
253 && self.currency.is_none()
254 && self.date.is_none()
255 && self.label.is_none()
256 && !self.merge
257 }
258
259 #[must_use]
263 pub fn matches(&self, cost: &Cost) -> bool {
264 if let Some(n) = &self.number_per
266 && n != &cost.number
267 {
268 return false;
269 }
270 if let Some(c) = &self.currency
272 && c != &cost.currency
273 {
274 return false;
275 }
276 if let Some(d) = &self.date
278 && cost.date.as_ref() != Some(d)
279 {
280 return false;
281 }
282 if let Some(l) = &self.label
284 && cost.label.as_ref() != Some(l)
285 {
286 return false;
287 }
288 true
289 }
290
291 #[must_use]
299 pub fn resolve(&self, units: Decimal, date: NaiveDate) -> Option<Cost> {
300 let currency = self.currency.clone()?;
301
302 let number = if let Some(per) = self.number_per {
303 per
305 } else if let Some(total) = self.number_total {
306 total / units.abs()
308 } else {
309 return None;
310 };
311
312 Some(Cost {
313 number,
314 currency,
315 date: self.date.or(Some(date)),
316 label: self.label.clone(),
317 })
318 }
319}
320
321impl fmt::Display for CostSpec {
322 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323 write!(f, "{{")?;
324 let mut parts = Vec::with_capacity(6);
326
327 if let Some(n) = self.number_per {
328 parts.push(format!("{n}"));
329 }
330 if let Some(n) = self.number_total {
331 parts.push(format!("# {n}"));
332 }
333 if let Some(c) = &self.currency {
334 parts.push(c.to_string());
335 }
336 if let Some(d) = self.date {
337 parts.push(d.to_string());
338 }
339 if let Some(l) = &self.label {
340 parts.push(format!("\"{l}\""));
341 }
342 if self.merge {
343 parts.push("*".to_string());
344 }
345
346 write!(f, "{}", parts.join(", "))?;
347 write!(f, "}}")
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354 use rust_decimal_macros::dec;
355
356 fn date(year: i32, month: u32, day: u32) -> NaiveDate {
357 crate::naive_date(year, month, day).unwrap()
358 }
359
360 #[test]
361 fn test_cost_new() {
362 let cost = Cost::new(dec!(150.00), "USD");
363 assert_eq!(cost.number, dec!(150.00));
364 assert_eq!(cost.currency, "USD");
365 assert!(cost.date.is_none());
366 assert!(cost.label.is_none());
367 }
368
369 #[test]
370 fn test_cost_builder() {
371 let cost = Cost::new(dec!(150.00), "USD")
372 .with_date(date(2024, 1, 15))
373 .with_label("lot1");
374
375 assert_eq!(cost.date, Some(date(2024, 1, 15)));
376 assert_eq!(cost.label, Some("lot1".to_string()));
377 }
378
379 #[test]
380 fn test_cost_total() {
381 let cost = Cost::new(dec!(150.00), "USD");
382 let total = cost.total_cost(dec!(10));
383 assert_eq!(total.number, dec!(1500.00));
384 assert_eq!(total.currency, "USD");
385 }
386
387 #[test]
388 fn test_cost_display() {
389 let cost = Cost::new(dec!(150.00), "USD")
390 .with_date(date(2024, 1, 15))
391 .with_label("lot1");
392 let s = format!("{cost}");
393 assert!(s.contains("150.00"));
394 assert!(s.contains("USD"));
395 assert!(s.contains("2024-01-15"));
396 assert!(s.contains("lot1"));
397 }
398
399 #[test]
400 fn test_cost_spec_empty() {
401 let spec = CostSpec::empty();
402 assert!(spec.is_empty());
403 }
404
405 #[test]
406 fn test_cost_spec_matches() {
407 let cost = Cost::new(dec!(150.00), "USD")
408 .with_date(date(2024, 1, 15))
409 .with_label("lot1");
410
411 assert!(CostSpec::empty().matches(&cost));
413
414 let spec = CostSpec::empty().with_number_per(dec!(150.00));
416 assert!(spec.matches(&cost));
417
418 let spec = CostSpec::empty().with_number_per(dec!(160.00));
420 assert!(!spec.matches(&cost));
421
422 let spec = CostSpec::empty().with_currency("USD");
424 assert!(spec.matches(&cost));
425
426 let spec = CostSpec::empty().with_date(date(2024, 1, 15));
428 assert!(spec.matches(&cost));
429
430 let spec = CostSpec::empty().with_label("lot1");
432 assert!(spec.matches(&cost));
433
434 let spec = CostSpec::empty()
436 .with_number_per(dec!(150.00))
437 .with_currency("USD")
438 .with_date(date(2024, 1, 15))
439 .with_label("lot1");
440 assert!(spec.matches(&cost));
441 }
442
443 #[test]
444 fn test_cost_spec_resolve() {
445 let spec = CostSpec::empty()
446 .with_number_per(dec!(150.00))
447 .with_currency("USD");
448
449 let cost = spec.resolve(dec!(10), date(2024, 1, 15)).unwrap();
450 assert_eq!(cost.number, dec!(150.00));
451 assert_eq!(cost.currency, "USD");
452 assert_eq!(cost.date, Some(date(2024, 1, 15)));
453 }
454
455 #[test]
456 fn test_cost_spec_resolve_total() {
457 let spec = CostSpec::empty()
458 .with_number_total(dec!(1500.00))
459 .with_currency("USD");
460
461 let cost = spec.resolve(dec!(10), date(2024, 1, 15)).unwrap();
462 assert_eq!(cost.number, dec!(150.00)); assert_eq!(cost.currency, "USD");
464 }
465}