1use hashbrown::HashMap;
2use std::{
3 cmp::Ordering,
4 fmt::{Debug, Display},
5 hash::Hash,
6 iter::{Sum, repeat},
7 ops::{Add, AddAssign, Deref, Mul, Neg, Sub, SubAssign},
8};
9use strum_macros::Display;
10
11pub trait BookingTypes: Clone + Debug {
12 type Account: Eq + Hash + Clone + Display + Debug;
13 type Date: Eq + Hash + Ord + Copy + Display + Debug;
14 type Currency: Eq + Hash + Ord + Clone + Display + Debug;
15 type Number: Number + Display + Debug;
16 type Label: Eq + Hash + Ord + Clone + Display + Debug;
17}
18
19pub trait PostingSpec: Clone + Debug {
21 type Types: BookingTypes;
22
23 type CostSpec: CostSpec<Types = Self::Types> + Clone + Debug;
24 type PriceSpec: PriceSpec<Types = Self::Types> + Clone + Debug;
25
26 fn account(&self) -> PostingSpecAccount<Self>;
27 fn units(&self) -> Option<PostingSpecNumber<Self>>;
28 fn currency(&self) -> Option<PostingSpecCurrency<Self>>;
29 fn cost(&self) -> Option<&Self::CostSpec>;
30 fn price(&self) -> Option<&Self::PriceSpec>;
31}
32
33pub type PostingSpecAccount<T> = <<T as PostingSpec>::Types as BookingTypes>::Account;
34pub type PostingSpecNumber<T> = <<T as PostingSpec>::Types as BookingTypes>::Number;
35pub type PostingSpecCurrency<T> = <<T as PostingSpec>::Types as BookingTypes>::Currency;
36
37pub trait CostSpec: Clone + Debug {
41 type Types: BookingTypes;
42
43 fn date(&self) -> Option<CostSpecDate<Self>>;
44 fn per_unit(&self) -> Option<CostSpecNumber<Self>>;
45 fn total(&self) -> Option<CostSpecNumber<Self>>;
46 fn currency(&self) -> Option<CostSpecCurrency<Self>>;
47 fn label(&self) -> Option<CostSpecLabel<Self>>;
48 fn merge(&self) -> bool;
49}
50
51pub type CostSpecDate<T> = <<T as CostSpec>::Types as BookingTypes>::Date;
52pub type CostSpecNumber<T> = <<T as CostSpec>::Types as BookingTypes>::Number;
53pub type CostSpecCurrency<T> = <<T as CostSpec>::Types as BookingTypes>::Currency;
54pub type CostSpecLabel<T> = <<T as CostSpec>::Types as BookingTypes>::Label;
55
56pub trait PriceSpec: Clone + Debug {
60 type Types: BookingTypes;
61
62 fn per_unit(&self) -> Option<PriceSpecNumber<Self>>;
63 fn total(&self) -> Option<PriceSpecNumber<Self>>;
64 fn currency(&self) -> Option<PriceSpecCurrency<Self>>;
65}
66
67pub type PriceSpecNumber<T> = <<T as PriceSpec>::Types as BookingTypes>::Number;
68pub type PriceSpecCurrency<T> = <<T as PriceSpec>::Types as BookingTypes>::Currency;
69
70#[derive(PartialEq, Eq, Clone, Debug)]
75pub struct Position<B>
76where
77 B: BookingTypes,
78{
79 pub units: B::Number,
80 pub currency: B::Currency,
81 pub cost: Option<Cost<B>>,
82}
83
84impl<B> Display for Position<B>
85where
86 B: BookingTypes,
87{
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 write!(f, "{} {}", &self.currency, self.units)?;
90 if let Some(cost) = self.cost.as_ref() {
91 write!(f, " {cost}")?;
92 }
93 Ok(())
94 }
95}
96
97impl<B> From<(B::Number, B::Currency)> for Position<B>
98where
99 B: BookingTypes,
100{
101 fn from(value: (B::Number, B::Currency)) -> Self {
102 Self {
103 currency: value.1,
104 units: value.0,
105 cost: None,
106 }
107 }
108}
109
110impl<B> Position<B>
111where
112 B: BookingTypes,
113{
114 pub(crate) fn with_accumulated(&self, units: B::Number) -> Self {
115 let cost = self.cost.as_ref().cloned();
116 Position {
117 currency: self.currency.clone(),
118 units: self.units + units,
119 cost,
120 }
121 }
122}
123
124#[derive(Clone, Debug)]
130pub struct Cost<B>
131where
132 B: BookingTypes,
133{
134 pub date: B::Date,
135 pub per_unit: B::Number,
136 pub total: B::Number,
137 pub currency: B::Currency,
138 pub label: Option<B::Label>,
139 pub merge: bool,
140}
141
142impl<B> Display for Cost<B>
143where
144 B: BookingTypes,
145{
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 write!(f, "{{{}, {} {}", &self.date, &self.per_unit, &self.currency)?;
148
149 if let Some(label) = &self.label {
150 write!(f, ", \"{label}\"")?;
151 }
152
153 if self.merge {
154 write!(f, ", *",)?;
155 }
156
157 f.write_str("}")
158 }
159}
160
161impl<B> PartialEq for Cost<B>
162where
163 B: BookingTypes,
164{
165 fn eq(&self, other: &Self) -> bool {
166 self.date == other.date
167 && self.per_unit == other.per_unit
168 && self.currency == other.currency
169 && self.label == other.label
170 && self.merge == other.merge
171 }
172}
173
174impl<B> Eq for Cost<B> where B: BookingTypes {}
175
176impl<B> Hash for Cost<B>
177where
178 B: BookingTypes,
179{
180 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
181 self.date.hash(state);
182 self.per_unit.hash(state);
183 self.total.hash(state);
184 self.currency.hash(state);
185 self.label.hash(state);
186 self.merge.hash(state);
187 }
188}
189
190impl<B> Ord for Cost<B>
191where
192 B: BookingTypes,
193 B::Date: Ord,
194 B::Currency: Ord,
195 B::Number: Ord,
196 B::Label: Ord,
197{
198 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
199 match self.date.cmp(&other.date) {
200 core::cmp::Ordering::Equal => {}
201 ord => return ord,
202 }
203
204 match self.currency.cmp(&other.currency) {
205 core::cmp::Ordering::Equal => {}
206 ord => return ord,
207 }
208
209 match self.per_unit.cmp(&other.per_unit) {
210 core::cmp::Ordering::Equal => {}
211 ord => return ord,
212 }
213
214 match self.label.cmp(&other.label) {
215 core::cmp::Ordering::Equal => {}
216 ord => return ord,
217 }
218
219 self.merge.cmp(&other.merge)
220 }
221}
222
223impl<B> PartialOrd for Cost<B>
224where
225 B: BookingTypes,
226 B::Date: Ord,
227 B::Currency: Ord,
228 B::Number: Ord,
229 B::Label: Ord,
230{
231 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
232 Some(self.cmp(other))
233 }
234}
235
236#[derive(PartialEq, Eq, Clone, Debug)]
242pub struct PostingCosts<B>
243where
244 B: BookingTypes,
245{
246 pub(crate) cost_currency: B::Currency,
247 pub(crate) adjustments: Vec<PostingCost<B>>,
248}
249
250impl<B> PostingCosts<B>
251where
252 B: BookingTypes,
253{
254 pub fn iter(&self) -> impl Iterator<Item = (&B::Currency, &PostingCost<B>)> {
255 repeat(&self.cost_currency).zip(self.adjustments.iter())
256 }
257
258 pub fn into_currency_costs(self) -> impl Iterator<Item = (B::Currency, PostingCost<B>)> {
259 repeat(self.cost_currency).zip(self.adjustments)
260 }
261}
262
263#[derive(PartialEq, Eq, Clone, Debug)]
265pub struct PostingCost<B>
266where
267 B: BookingTypes,
268{
269 pub date: B::Date,
270 pub units: B::Number,
271 pub per_unit: B::Number,
272 pub total: B::Number,
273 pub label: Option<B::Label>,
274 pub merge: bool,
275}
276
277impl<B> From<(&B::Currency, &PostingCost<B>)> for Cost<B>
278where
279 B: BookingTypes,
280{
281 fn from(value: (&B::Currency, &PostingCost<B>)) -> Self {
282 let (
283 currency,
284 PostingCost {
285 date,
286 units: _,
287 total,
288 per_unit,
289 label,
290 merge,
291 },
292 ) = value;
293 Self {
294 date: *date,
295 per_unit: *per_unit,
296 total: *total,
297 currency: currency.clone(),
298 label: label.clone(),
299 merge: *merge,
300 }
301 }
302}
303
304#[derive(Clone, Debug)]
310pub struct Price<B>
311where
312 B: BookingTypes,
313{
314 pub per_unit: B::Number,
315 pub total: Option<B::Number>,
316 pub currency: B::Currency,
317}
318
319impl<B> Display for Price<B>
320where
321 B: BookingTypes,
322{
323 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
324 write!(f, "@ {} {}", &self.per_unit, &self.currency)
325 }
326}
327
328impl<B> PartialEq for Price<B>
329where
330 B: BookingTypes,
331{
332 fn eq(&self, other: &Self) -> bool {
333 self.per_unit == other.per_unit
334 && self.total == other.total
335 && self.currency == other.currency
336 }
337}
338
339impl<B> Eq for Price<B> where B: BookingTypes {}
340
341impl<B> Hash for Price<B>
342where
343 B: BookingTypes,
344{
345 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
346 self.per_unit.hash(state);
347 self.total.hash(state);
348 self.currency.hash(state);
349 }
350}
351impl<B> Ord for Price<B>
352where
353 B: BookingTypes,
354 B::Date: Ord,
355 B::Currency: Ord,
356 B::Number: Ord,
357 B::Label: Ord,
358{
359 fn cmp(&self, other: &Self) -> Ordering {
360 match self.currency.cmp(&other.currency) {
361 core::cmp::Ordering::Equal => {}
362 ord => return ord,
363 }
364
365 match self.per_unit.cmp(&other.per_unit) {
366 core::cmp::Ordering::Equal => {}
367 ord => return ord,
368 }
369
370 self.total.cmp(&other.total)
371 }
372}
373
374impl<B> PartialOrd for Price<B>
375where
376 B: BookingTypes,
377 B::Date: Ord,
378 B::Currency: Ord,
379 B::Number: Ord,
380 B::Label: Ord,
381{
382 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
383 Some(self.cmp(other))
384 }
385}
386
387#[derive(Debug)]
389pub struct Bookings<'p, B, P>
390where
391 B: BookingTypes,
392 P: PostingSpec<Types = B>,
393{
394 pub interpolated_postings: Vec<Interpolated<'p, B, P>>,
395 pub updated_inventory: Inventory<B>,
396}
397
398#[derive(Clone, Debug)]
400pub struct Interpolated<'p, B, P>
401where
402 B: BookingTypes,
403 P: PostingSpec<Types = B>,
404{
405 pub(crate) posting: &'p P,
406 pub(crate) idx: usize,
407 pub units: B::Number,
408 pub currency: B::Currency,
409 pub cost: Option<PostingCosts<B>>,
410 pub price: Option<Price<B>>,
411}
412
413pub trait Tolerance: Clone + Debug {
414 type Types: BookingTypes;
415
416 fn inferred_tolerance_default(
419 &self,
420 cur: &ToleranceCurrency<Self>,
421 ) -> Option<ToleranceNumber<Self>>;
422
423 fn inferred_tolerance_multiplier(&self) -> Option<ToleranceNumber<Self>>;
424}
425
426pub type ToleranceNumber<T> = <<T as Tolerance>::Types as BookingTypes>::Number;
427pub type ToleranceCurrency<T> = <<T as Tolerance>::Types as BookingTypes>::Currency;
428
429pub trait Number:
431 Copy
432 + Add<Output = Self>
433 + AddAssign
434 + Sub<Output = Self>
435 + SubAssign
436 + Neg<Output = Self>
437 + Mul<Output = Self>
438 + Sum
439 + Eq
440 + Hash
441 + Ord
442 + Sized
443 + Default
444{
445 fn abs(&self) -> Self;
446
447 fn sign(&self) -> Option<Sign>;
449
450 fn zero() -> Self;
451
452 fn new(m: i64, scale: u32) -> Self;
453
454 fn checked_div(self, other: Self) -> Option<Self>;
455
456 fn scale(&self) -> u32;
458
459 fn rescaled(self, scale: u32) -> Self;
461}
462
463#[derive(PartialEq, Eq, Clone, Copy, Display, Debug)]
465pub enum Sign {
466 Positive,
467 Negative,
468}
469
470#[derive(PartialEq, Eq, Default, Clone, Copy, Display, Debug)]
472pub enum Booking {
473 #[default]
474 Strict,
475 StrictWithSize,
476 None,
477 Average,
478 Fifo,
479 Lifo,
480 Hifo,
481}
482
483#[derive(PartialEq, Eq, Clone, Debug)]
485pub struct Positions<B>(Vec<Position<B>>)
486where
487 B: BookingTypes;
488
489impl<B> Display for Positions<B>
490where
491 B: BookingTypes,
492{
493 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
494 for (i, p) in self.0.iter().enumerate() {
495 write!(f, "{}{}", if i > 0 { ", " } else { "" }, p)?;
496 }
497 Ok(())
498 }
499}
500
501impl<B> Positions<B>
502where
503 B: BookingTypes,
504{
505 pub(crate) fn from_previous(positions: Vec<Position<B>>) -> Self {
507 Self(positions)
508 }
509
510 pub(crate) fn get_mut(&mut self, i: usize) -> Option<&mut Position<B>> {
511 self.0.get_mut(i)
512 }
513
514 pub(crate) fn insert(&mut self, i: usize, element: Position<B>) {
515 self.0.insert(i, element)
516 }
517
518 pub fn units(&self) -> HashMap<&B::Currency, B::Number> {
519 let mut units_by_currency = HashMap::default();
520 for Position {
521 currency, units, ..
522 } in &self.0
523 {
524 if units_by_currency.contains_key(currency) {
525 *units_by_currency.get_mut(currency).unwrap() += *units;
526 } else {
527 units_by_currency.insert(currency, *units);
528 }
529 }
530 units_by_currency
531 }
532
533 pub fn accumulate(
534 &mut self,
535 units: B::Number,
536 currency: B::Currency,
537 cost: Option<Cost<B>>,
538 method: Booking,
539 ) {
540 use Ordering::*;
541
542 let insertion_idx = match method {
543 Booking::Strict
544 | Booking::StrictWithSize
545 | Booking::Fifo
546 | Booking::Lifo
547 | Booking::Hifo => {
548 self.binary_search_by(|existing| match &existing.currency.cmp(¤cy) {
549 ordering @ (Less | Greater) => *ordering,
550 Equal => match (&existing.cost, &cost) {
551 (None, None) => Equal,
552 (Some(_), None) => Greater,
553 (None, Some(_)) => Less,
554 (Some(existing_cost), Some(cost)) => {
555 existing_cost.partial_cmp(cost).unwrap_or(Equal)
556 }
557 },
558 })
559 }
560 Booking::None => {
561 self.binary_search_by(|existing| match &existing.currency.cmp(¤cy) {
562 ordering @ (Less | Greater) => *ordering,
563 Equal => match (&existing.cost, &cost) {
564 (None, None) => Equal,
565 (Some(_), None) => Greater,
566 (_, Some(_)) => Less,
567 },
568 })
569 }
570 Booking::Average => todo!("average booking method is not yet implemented"),
571 };
572
573 match (insertion_idx, cost) {
574 (Ok(i), None) => {
575 let position = self.get_mut(i).unwrap();
576 position.units += units;
577 }
578 (Ok(i), Some(_cost)) => {
579 let position = self.get_mut(i).unwrap();
580 position.units += units;
581 }
582 (Err(i), None) => {
583 let position = Position {
584 units,
585 currency,
586 cost: None,
587 };
588 self.insert(i, position)
589 }
590 (Err(i), Some(cost)) => {
591 let position = Position {
592 units,
593 currency,
594 cost: Some(cost),
595 };
596 self.insert(i, position)
597 }
598 }
599 }
600}
601
602impl<B> Default for Positions<B>
603where
604 B: BookingTypes,
605{
606 fn default() -> Self {
607 Self(Default::default())
608 }
609}
610
611impl<B> Deref for Positions<B>
612where
613 B: BookingTypes,
614{
615 type Target = Vec<Position<B>>;
616
617 fn deref(&self) -> &Self::Target {
618 &self.0
619 }
620}
621
622impl<B> IntoIterator for Positions<B>
623where
624 B: BookingTypes,
625{
626 type Item = Position<B>;
627 type IntoIter = std::vec::IntoIter<Self::Item>;
628
629 fn into_iter(self) -> Self::IntoIter {
630 self.0.into_iter()
631 }
632}
633
634#[derive(PartialEq, Eq, Debug)]
636pub struct Inventory<B>
637where
638 B: BookingTypes,
639{
640 value: HashMap<B::Account, Positions<B>>,
641}
642
643impl<B> Default for Inventory<B>
644where
645 B: BookingTypes,
646{
647 fn default() -> Self {
648 Self {
649 value: Default::default(),
650 }
651 }
652}
653
654impl<B> From<HashMap<B::Account, Positions<B>>> for Inventory<B>
655where
656 B: BookingTypes,
657{
658 fn from(value: HashMap<B::Account, Positions<B>>) -> Self {
659 Self { value }
660 }
661}
662
663impl<B> Deref for Inventory<B>
664where
665 B: BookingTypes,
666{
667 type Target = HashMap<B::Account, Positions<B>>;
668
669 fn deref(&self) -> &Self::Target {
670 &self.value
671 }
672}
673
674impl<B> IntoIterator for Inventory<B>
675where
676 B: BookingTypes,
677{
678 type Item = (B::Account, Positions<B>);
679 type IntoIter = hashbrown::hash_map::IntoIter<B::Account, Positions<B>>;
680
681 fn into_iter(self) -> hashbrown::hash_map::IntoIter<B::Account, Positions<B>> {
682 self.value.into_iter()
683 }
684}
685
686impl<B> Inventory<B>
687where
688 B: BookingTypes,
689{
690 pub(crate) fn insert(&mut self, k: B::Account, v: Positions<B>) -> Option<Positions<B>> {
691 self.value.insert(k, v)
692 }
693}