Skip to main content

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