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/// Interned `&str` for commodities 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/// Manages [`Commodity`] instances.
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    /// Registers given `value` as always alias of `canonical`.
163    pub(super) fn register_alias(
164        &mut self,
165        value: &str,
166        canonical: CommodityTag<'arena>,
167    ) -> Result<(), OccupiedError<Commodity<'arena>>> {
168        self.intern.register_alias(Commodity(value), canonical.0)
169    }
170
171    /// Returns the scale (the number of digits under decimal point) of the `commodity` if specified.
172    #[inline]
173    pub(super) fn scale(&self, commodity: CommodityTag<'arena>) -> Option<u32> {
174        self.formatting.get(commodity).map(|x| x.scale())
175    }
176
177    /// Sets the format of the `commodity` as [`PrettyDecimal`].
178    #[inline]
179    pub(super) fn set_format(&mut self, commodity: CommodityTag<'arena>, format: PrettyDecimal) {
180        self.formatting.set(commodity, format);
181    }
182
183    /// Returns if the commodity store is empty.
184    #[inline]
185    pub fn is_empty(&self) -> bool {
186        self.intern.is_empty()
187    }
188
189    /// Returns the total length of the commodity.
190    #[inline]
191    pub fn len(&self) -> usize {
192        self.intern.len()
193    }
194}
195
196/// Map from CommodityTag<'arena> to value.
197#[derive(Debug, PartialEq, Eq, Clone)]
198pub struct CommodityMap<T> {
199    inner: Vec<Option<T>>,
200}
201
202impl<T> CommodityMap<T> {
203    /// Creates a new instance.
204    pub fn new() -> Self {
205        Self::with_capacity(0)
206    }
207
208    /// Creates a new instance with a given `capacity`.
209    pub fn with_capacity(capacity: usize) -> Self {
210        Self {
211            inner: Vec::with_capacity(capacity),
212        }
213    }
214
215    /// Returns the reference to the corresponding element.
216    pub fn get(&self, k: CommodityTag<'_>) -> Option<&T> {
217        match self.inner.get(k.as_index()) {
218            Some(Some(r)) => Some(r),
219            Some(None) | None => None,
220        }
221    }
222}
223
224impl<T: Clone> CommodityMap<T> {
225    /// Returns the mutable reference corresponding to the given `k`.
226    pub fn get_mut(&mut self, k: CommodityTag<'_>) -> &mut Option<T> {
227        self.ensure_size(k);
228        &mut self.inner[k.as_index()]
229    }
230
231    /// Sets the given key value.
232    pub fn set(&mut self, k: CommodityTag<'_>, v: T) {
233        self.ensure_size(k);
234        self.inner[k.as_index()] = Some(v);
235    }
236
237    /// Ensure size for given `k`.
238    #[inline]
239    fn ensure_size(&mut self, k: CommodityTag<'_>) {
240        if self.inner.len() <= k.as_index() {
241            self.inner.resize(k.as_index() + 1, None);
242        }
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    use pretty_assertions::assert_eq;
251    use rust_decimal_macros::dec;
252
253    #[test]
254    fn to_owned_lossy() {
255        let arena = Bump::new();
256        let mut commodities = CommodityStore::new(&arena);
257        let chf = commodities.insert("CHF").unwrap();
258
259        assert_eq!(
260            OwnedCommodity::from_string("CHF".to_string()),
261            chf.to_owned_lossy(&commodities)
262        );
263
264        let unknown = CommodityTag(InternTag::new(1));
265
266        assert_eq!(
267            OwnedCommodity::from_string("unknown#1".to_string()),
268            unknown.to_owned_lossy(&commodities)
269        );
270    }
271
272    #[test]
273    fn commodity_tag_ord() {
274        let arena = Bump::new();
275        let mut commodities = CommodityStore::new(&arena);
276        let usd = commodities.insert("USD").unwrap();
277        let chf = commodities.insert("CHF").unwrap();
278
279        assert_eq!(Some(std::cmp::Ordering::Equal), usd.partial_cmp(&usd));
280        assert!(usd < chf);
281    }
282
283    #[test]
284    fn is_empty_works() {
285        let arena = Bump::new();
286        let mut commodities = CommodityStore::new(&arena);
287        assert!(commodities.is_empty());
288
289        commodities.insert("JPY").unwrap();
290        assert!(!commodities.is_empty());
291    }
292
293    #[test]
294    fn scale_returns_none_if_unspecified() {
295        let arena = Bump::new();
296        let mut commodities = CommodityStore::new(&arena);
297        let jpy = commodities.insert("JPY").unwrap();
298
299        assert_eq!(None, commodities.scale(jpy));
300    }
301
302    #[test]
303    fn scale_returns_some_if_set() {
304        let arena = Bump::new();
305        let mut commodities = CommodityStore::new(&arena);
306        let jpy = commodities.insert("JPY").unwrap();
307        commodities.set_format(jpy, PrettyDecimal::comma3dot(dec!(1.234)));
308
309        assert_eq!(Some(3), commodities.scale(jpy));
310    }
311}