okane_core/report/
commodity.rs1use 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#[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 pub fn as_str(&self) -> &'a str {
37 self.0
38 }
39}
40
41#[derive(Debug, PartialEq, Eq, Hash, Clone)]
44pub struct OwnedCommodity(String);
45
46impl OwnedCommodity {
47 pub fn from_string(v: String) -> Self {
49 Self(v)
50 }
51
52 pub fn as_str(&self) -> &str {
54 self.0.as_str()
55 }
56
57 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 pub fn as_index(&self) -> usize {
94 self.0.as_index()
95 }
96
97 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 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
114pub 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 pub(super) fn new(arena: &'arena Bump) -> Self {
131 Self {
132 intern: DenseInternStore::new(arena),
133 formatting: CommodityMap::new(),
134 }
135 }
136
137 pub fn ensure(&mut self, value: &'_ str) -> CommodityTag<'arena> {
141 CommodityTag(self.intern.ensure(Commodity(value)))
142 }
143
144 pub fn get(&self, tag: CommodityTag<'arena>) -> Option<Commodity<'arena>> {
146 self.intern.get(tag.0)
147 }
148
149 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 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 #[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 #[inline]
181 pub(super) fn set_format(&mut self, commodity: CommodityTag<'arena>, format: PrettyDecimal) {
182 self.formatting.set(commodity, format);
183 }
184
185 #[inline]
187 pub fn is_empty(&self) -> bool {
188 self.intern.is_empty()
189 }
190
191 #[inline]
193 pub fn len(&self) -> usize {
194 self.intern.len()
195 }
196}
197
198#[derive(Debug, PartialEq, Eq, Clone)]
200pub struct CommodityMap<T> {
201 inner: Vec<Option<T>>,
202}
203
204impl<T> CommodityMap<T> {
205 pub fn new() -> Self {
207 Self::with_capacity(0)
208 }
209
210 pub fn with_capacity(capacity: usize) -> Self {
212 Self {
213 inner: Vec::with_capacity(capacity),
214 }
215 }
216
217 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 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 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 #[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}