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 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 #[inline]
173 pub(super) fn scale(&self, commodity: CommodityTag<'arena>) -> Option<u32> {
174 self.formatting.get(commodity).map(|x| x.scale())
175 }
176
177 #[inline]
179 pub(super) fn set_format(&mut self, commodity: CommodityTag<'arena>, format: PrettyDecimal) {
180 self.formatting.set(commodity, format);
181 }
182
183 #[inline]
185 pub fn is_empty(&self) -> bool {
186 self.intern.is_empty()
187 }
188
189 #[inline]
191 pub fn len(&self) -> usize {
192 self.intern.len()
193 }
194}
195
196#[derive(Debug, PartialEq, Eq, Clone)]
198pub struct CommodityMap<T> {
199 inner: Vec<Option<T>>,
200}
201
202impl<T> CommodityMap<T> {
203 pub fn new() -> Self {
205 Self::with_capacity(0)
206 }
207
208 pub fn with_capacity(capacity: usize) -> Self {
210 Self {
211 inner: Vec::with_capacity(capacity),
212 }
213 }
214
215 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 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 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 #[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}