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    /// Converts back the tag into `&str` if possible.
86    /// If not found, use `unknown#xx` placeholder.
87    pub(super) fn to_str_lossy(self, commodity_store: &CommodityStore<'ctx>) -> Cow<'ctx, str> {
88        match commodity_store.get(self) {
89            Some(x) => Cow::Borrowed(x.as_str()),
90            None => Cow::Owned(format!("unknown#{}", self.as_index())),
91        }
92    }
93
94    /// Converts the self into [`OwnedCommodity`].
95    /// If the tag isn't registered in the `commodity_store`,
96    /// it'll print "unknown#xx" as the place holder.
97    pub fn to_owned_lossy(self, commodity_store: &CommodityStore<'ctx>) -> OwnedCommodity {
98        OwnedCommodity::from_string(self.to_str_lossy(commodity_store).into_owned())
99    }
100}
101
102/// Interner for [`Commodity`].
103pub struct CommodityStore<'arena> {
104    intern: DenseInternStore<'arena, Commodity<'arena>>,
105    formatting: CommodityMap<PrettyDecimal>,
106}
107
108impl<'arena> std::fmt::Debug for CommodityStore<'arena> {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        f.debug_struct("CommodityStore")
111            .field("intern", &format!("[{} commodities]", self.intern.len()))
112            .finish()
113    }
114}
115
116impl<'arena> CommodityStore<'arena> {
117    /// Creates a new instance.
118    pub(super) fn new(arena: &'arena Bump) -> Self {
119        Self {
120            intern: DenseInternStore::new(arena),
121            formatting: CommodityMap::new(),
122        }
123    }
124
125    /// Returns the Commodity with the given `value`,
126    /// potentially resolving the alias.
127    /// If not available, registers the given `value` as the canonical.
128    pub fn ensure(&mut self, value: &'_ str) -> CommodityTag<'arena> {
129        CommodityTag(self.intern.ensure(Commodity(value)))
130    }
131
132    /// Returns [`Commodity`] corresponding to the given `tag`.
133    pub fn get(&self, tag: CommodityTag<'arena>) -> Option<Commodity<'arena>> {
134        self.intern.get(tag.0)
135    }
136
137    /// Returns the Commodity with the given `value` if and only if it's already registered.
138    pub fn resolve(&self, value: &str) -> Option<CommodityTag<'arena>> {
139        self.intern.resolve(value).map(CommodityTag)
140    }
141
142    #[cfg(test)]
143    pub fn insert(
144        &mut self,
145        value: &str,
146    ) -> Result<CommodityTag<'arena>, OccupiedError<Commodity<'arena>>> {
147        self.intern.try_insert(Commodity(value)).map(CommodityTag)
148    }
149
150    /// Inserts given `value` as always alias of `canonical`.
151    /// Returns error if given `value` is already registered as canonical.
152    /// Facade for [InternStore::insert_alias].
153    pub(super) fn insert_alias(
154        &mut self,
155        value: &str,
156        canonical: CommodityTag<'arena>,
157    ) -> Result<(), OccupiedError<Commodity<'arena>>> {
158        self.intern.insert_alias(Commodity(value), canonical.0)
159    }
160
161    /// Returns the precision of the `commodity` if specified.
162    #[inline]
163    pub(super) fn get_decimal_point(&self, commodity: CommodityTag<'arena>) -> Option<u32> {
164        self.formatting.get(commodity).map(|x| x.scale())
165    }
166
167    /// Sets the format of the `commodity` as [`PrettyDecimal`].
168    #[inline]
169    pub(super) fn set_format(&mut self, commodity: CommodityTag<'arena>, format: PrettyDecimal) {
170        self.formatting.set(commodity, format);
171    }
172
173    /// Returns if the commodity store is empty.
174    #[inline]
175    pub fn is_empty(&self) -> bool {
176        self.intern.is_empty()
177    }
178
179    /// Returns the total length of the commodity.
180    #[inline]
181    pub fn len(&self) -> usize {
182        self.intern.len()
183    }
184}
185
186/// Map from CommodityTag<'arena> to value.
187#[derive(Debug, PartialEq, Eq, Clone)]
188pub struct CommodityMap<T> {
189    inner: Vec<Option<T>>,
190}
191
192impl<T> CommodityMap<T> {
193    /// Creates a new instance.
194    pub fn new() -> Self {
195        Self::with_capacity(0)
196    }
197
198    /// Creates a new instance with a given `capacity`.
199    pub fn with_capacity(capacity: usize) -> Self {
200        Self {
201            inner: Vec::with_capacity(capacity),
202        }
203    }
204
205    /// Returns the reference to the corresponding element.
206    pub fn get(&self, k: CommodityTag<'_>) -> Option<&T> {
207        match self.inner.get(k.as_index()) {
208            Some(Some(r)) => Some(r),
209            Some(None) | None => None,
210        }
211    }
212}
213
214impl<T: Clone> CommodityMap<T> {
215    /// Returns the mutable reference corresponding to the given `k`.
216    pub fn get_mut(&mut self, k: CommodityTag<'_>) -> &mut Option<T> {
217        self.ensure_size(k);
218        &mut self.inner[k.as_index()]
219    }
220
221    /// Sets the given key value.
222    pub fn set(&mut self, k: CommodityTag<'_>, v: T) {
223        self.ensure_size(k);
224        self.inner[k.as_index()] = Some(v);
225    }
226
227    /// Ensure size for given `k`.
228    #[inline]
229    fn ensure_size(&mut self, k: CommodityTag<'_>) {
230        if self.inner.len() <= k.as_index() {
231            self.inner.resize(k.as_index() + 1, None);
232        }
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    use pretty_assertions::assert_eq;
241    use rust_decimal_macros::dec;
242
243    #[test]
244    fn to_owned_lossy() {
245        let arena = Bump::new();
246        let mut commodities = CommodityStore::new(&arena);
247        let chf = commodities.insert("CHF").unwrap();
248
249        assert_eq!(
250            OwnedCommodity::from_string("CHF".to_string()),
251            chf.to_owned_lossy(&commodities)
252        );
253
254        let unknown = CommodityTag(InternTag::new(1));
255
256        assert_eq!(
257            OwnedCommodity::from_string("unknown#1".to_string()),
258            unknown.to_owned_lossy(&commodities)
259        );
260    }
261
262    #[test]
263    fn is_empty_works() {
264        let arena = Bump::new();
265        let mut commodities = CommodityStore::new(&arena);
266        assert!(commodities.is_empty());
267
268        commodities.insert("JPY").unwrap();
269        assert!(!commodities.is_empty());
270    }
271
272    #[test]
273    fn get_decimal_point_returns_none_if_unspecified() {
274        let arena = Bump::new();
275        let mut commodities = CommodityStore::new(&arena);
276        let jpy = commodities.insert("JPY").unwrap();
277
278        assert_eq!(None, commodities.get_decimal_point(jpy));
279    }
280
281    #[test]
282    fn get_decimal_point_returns_some_if_set() {
283        let arena = Bump::new();
284        let mut commodities = CommodityStore::new(&arena);
285        let jpy = commodities.insert("JPY").unwrap();
286        commodities.set_format(jpy, PrettyDecimal::comma3dot(dec!(1.234)));
287
288        assert_eq!(Some(3), commodities.get_decimal_point(jpy));
289    }
290}