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<'a>(InternTag<Commodity<'a>>);
77
78impl<'ctx> CommodityTag<'ctx> {
79 pub fn as_index(&self) -> usize {
82 self.0.as_index()
83 }
84
85 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 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
102pub 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 pub(super) fn new(arena: &'arena Bump) -> Self {
119 Self {
120 intern: DenseInternStore::new(arena),
121 formatting: CommodityMap::new(),
122 }
123 }
124
125 pub fn ensure(&mut self, value: &'_ str) -> CommodityTag<'arena> {
129 CommodityTag(self.intern.ensure(Commodity(value)))
130 }
131
132 pub fn get(&self, tag: CommodityTag<'arena>) -> Option<Commodity<'arena>> {
134 self.intern.get(tag.0)
135 }
136
137 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 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 #[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 #[inline]
169 pub(super) fn set_format(&mut self, commodity: CommodityTag<'arena>, format: PrettyDecimal) {
170 self.formatting.set(commodity, format);
171 }
172
173 #[inline]
175 pub fn is_empty(&self) -> bool {
176 self.intern.is_empty()
177 }
178
179 #[inline]
181 pub fn len(&self) -> usize {
182 self.intern.len()
183 }
184}
185
186#[derive(Debug, PartialEq, Eq, Clone)]
188pub struct CommodityMap<T> {
189 inner: Vec<Option<T>>,
190}
191
192impl<T> CommodityMap<T> {
193 pub fn new() -> Self {
195 Self::with_capacity(0)
196 }
197
198 pub fn with_capacity(capacity: usize) -> Self {
200 Self {
201 inner: Vec::with_capacity(capacity),
202 }
203 }
204
205 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 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 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 #[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}