okane_core/report/
commodity.rs

1//! Defines commodity and its related types.
2
3use std::borrow::Cow;
4use std::fmt::Display;
5
6use bumpalo::Bump;
7use bumpalo_intern::dense::{DenseInternStore, InternTag, Interned, Keyed, OccupiedError};
8use pretty_decimal::PrettyDecimal;
9
10/// `&str` for commodities, interned within the `'arena` bounded allocator lifetime.
11#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
12pub struct Commodity<'arena>(&'arena str);
13
14impl<'a> Keyed<'a> for Commodity<'a> {
15    fn intern_key(&self) -> &'a str {
16        self.0
17    }
18}
19impl<'a> Interned<'a> for Commodity<'a> {
20    type View<'b> = Commodity<'b>;
21
22    fn intern_from<'b>(arena: &'a Bump, view: Self::View<'b>) -> (&'a str, Self) {
23        let key = arena.alloc_str(view.0);
24        (key, Commodity(key))
25    }
26}
27
28impl Display for Commodity<'_> {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        self.as_str().fmt(f)
31    }
32}
33
34impl<'a> Commodity<'a> {
35    /// Returns the `&str`.
36    pub fn as_str(&self) -> &'a str {
37        self.0
38    }
39}
40
41/// Owned [`Commodity`], which is just [`String`].
42/// Useful to store in the error.
43#[derive(Debug, PartialEq, Eq, Hash, Clone)]
44pub struct OwnedCommodity(String);
45
46impl OwnedCommodity {
47    /// Creates a new [`OwnedCommodity`] instance.
48    pub fn from_string(v: String) -> Self {
49        Self(v)
50    }
51
52    /// Returns the underlying [`&str`].
53    pub fn as_str(&self) -> &str {
54        self.0.as_str()
55    }
56
57    /// Returns the underlying [`String`].
58    pub fn into_string(self) -> String {
59        self.0
60    }
61}
62
63impl From<Commodity<'_>> for OwnedCommodity {
64    fn from(value: Commodity<'_>) -> Self {
65        Self(value.as_str().to_string())
66    }
67}
68
69impl Display for OwnedCommodity {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        self.0.fmt(f)
72    }
73}
74
75#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
76pub struct CommodityTag<'a>(InternTag<Commodity<'a>>);
77
78impl<'ctx> CommodityTag<'ctx> {
79    /// Returns the index of the commodity.
80    /// Note this index is dense, so you can assume it fits in 0..len range.
81    pub fn as_index(&self) -> usize {
82        self.0.as_index()
83    }
84
85    /// Takes the str if possible.
86    pub(super) fn to_str_lossy(self, commodity_store: &CommodityStore<'ctx>) -> Cow<'ctx, str> {
87        match commodity_store.get(self) {
88            Some(x) => Cow::Borrowed(x.as_str()),
89            None => Cow::Owned(format!("unknown#{}", self.as_index())),
90        }
91    }
92
93    /// Converts the self into [`OwnedCommodity`].
94    /// If the tag isn't registered in the `commodity_store`,
95    /// it'll print "unknown#xx" as the place holder.
96    pub(super) fn to_owned_lossy(self, commodity_store: &CommodityStore<'ctx>) -> OwnedCommodity {
97        OwnedCommodity::from_string(self.to_str_lossy(commodity_store).into_owned())
98    }
99}
100
101/// Interner for [`Commodity`].
102pub(super) struct CommodityStore<'arena> {
103    intern: DenseInternStore<'arena, Commodity<'arena>>,
104    formatting: CommodityMap<PrettyDecimal>,
105}
106
107impl<'arena> std::fmt::Debug for CommodityStore<'arena> {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        f.debug_struct("CommodityStore")
110            .field("intern", &format!("[{} commodities]", self.intern.len()))
111            .finish()
112    }
113}
114
115impl<'arena> CommodityStore<'arena> {
116    /// Creates a new instance.
117    pub fn new(arena: &'arena Bump) -> Self {
118        Self {
119            intern: DenseInternStore::new(arena),
120            formatting: CommodityMap::new(),
121        }
122    }
123
124    /// Returns the Commodity with the given `value`,
125    /// potentially resolving the alias.
126    /// If not available, registers the given `value` as the canonical.
127    pub fn ensure(&mut self, value: &'_ str) -> CommodityTag<'arena> {
128        CommodityTag(self.intern.ensure(Commodity(value)))
129    }
130
131    /// Returns the tag corresponding [`Commodity`].
132    pub fn get(&self, tag: CommodityTag<'arena>) -> Option<Commodity<'arena>> {
133        self.intern.get(tag.0)
134    }
135
136    /// Returns the Commodity with the given `value` if and only if it's already registered.
137    pub fn resolve(&self, value: &str) -> Option<CommodityTag<'arena>> {
138        self.intern.resolve(value).map(CommodityTag)
139    }
140
141    #[cfg(test)]
142    pub fn insert(
143        &mut self,
144        value: &str,
145    ) -> Result<CommodityTag<'arena>, OccupiedError<Commodity<'arena>>> {
146        self.intern.try_insert(Commodity(value)).map(CommodityTag)
147    }
148
149    /// Inserts given `value` as always alias of `canonical`.
150    /// Returns error if given `value` is already registered as canonical.
151    /// Facade for [InternStore::insert_alias].
152    pub(super) fn insert_alias(
153        &mut self,
154        value: &str,
155        canonical: CommodityTag<'arena>,
156    ) -> Result<(), OccupiedError<Commodity<'arena>>> {
157        self.intern.insert_alias(Commodity(value), canonical.0)
158    }
159
160    /// Returns the precision of the `commodity` if specified.
161    #[inline]
162    pub(super) fn get_decimal_point(&self, commodity: CommodityTag<'arena>) -> Option<u32> {
163        self.formatting.get(commodity).map(|x| x.scale())
164    }
165
166    /// Sets the format of the `commodity` as [`PrettyDecimal`].
167    #[inline]
168    pub(super) fn set_format(&mut self, commodity: CommodityTag<'arena>, format: PrettyDecimal) {
169        self.formatting.set(commodity, format);
170    }
171
172    /// Returns the total length of the commodity.
173    #[inline]
174    pub fn len(&self) -> usize {
175        self.intern.len()
176    }
177}
178
179/// Map from CommodityTag<'arena> to value.
180#[derive(Debug, PartialEq, Eq, Clone)]
181pub struct CommodityMap<T> {
182    inner: Vec<Option<T>>,
183}
184
185impl<T> CommodityMap<T> {
186    /// Creates a new instance.
187    pub fn new() -> Self {
188        Self::with_capacity(0)
189    }
190
191    /// Creates a new instance with a given `capacity`.
192    pub fn with_capacity(capacity: usize) -> Self {
193        Self {
194            inner: Vec::with_capacity(capacity),
195        }
196    }
197
198    /// Returns the reference to the corresponding element.
199    pub fn get(&self, k: CommodityTag<'_>) -> Option<&T> {
200        match self.inner.get(k.as_index()) {
201            Some(Some(r)) => Some(r),
202            Some(None) | None => None,
203        }
204    }
205}
206
207impl<T: Clone> CommodityMap<T> {
208    /// Returns the mutable reference corresponding to the given `k`.
209    pub fn get_mut(&mut self, k: CommodityTag<'_>) -> &mut Option<T> {
210        self.ensure_size(k);
211        &mut self.inner[k.as_index()]
212    }
213
214    /// Sets the given key value.
215    pub fn set(&mut self, k: CommodityTag<'_>, v: T) {
216        self.ensure_size(k);
217        self.inner[k.as_index()] = Some(v);
218    }
219
220    /// Ensure size for given `k`.
221    #[inline]
222    fn ensure_size(&mut self, k: CommodityTag<'_>) {
223        if self.inner.len() <= k.as_index() {
224            self.inner.resize(k.as_index() + 1, None);
225        }
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    use pretty_assertions::assert_eq;
234    use rust_decimal_macros::dec;
235
236    #[test]
237    fn to_owned_lossy() {
238        let arena = Bump::new();
239        let mut commodities = CommodityStore::new(&arena);
240        let chf = commodities.insert("CHF").unwrap();
241
242        assert_eq!(
243            OwnedCommodity::from_string("CHF".to_string()),
244            chf.to_owned_lossy(&commodities)
245        );
246
247        let unknown = CommodityTag(InternTag::new(1));
248
249        assert_eq!(
250            OwnedCommodity::from_string("unknown#1".to_string()),
251            unknown.to_owned_lossy(&commodities)
252        );
253    }
254
255    #[test]
256    fn get_decimal_point_returns_none_if_unspecified() {
257        let arena = Bump::new();
258        let mut commodities = CommodityStore::new(&arena);
259        let jpy = commodities.insert("JPY").unwrap();
260
261        assert_eq!(None, commodities.get_decimal_point(jpy));
262    }
263
264    #[test]
265    fn get_decimal_point_returns_some_if_set() {
266        let arena = Bump::new();
267        let mut commodities = CommodityStore::new(&arena);
268        let jpy = commodities.insert("JPY").unwrap();
269        commodities.set_format(jpy, PrettyDecimal::comma3dot(dec!(1.234)));
270
271        assert_eq!(Some(3), commodities.get_decimal_point(jpy));
272    }
273}