1use rust_decimal::Decimal;
35use std::collections::HashMap;
36
37#[derive(Debug, Clone, Default)]
42pub struct DisplayContext {
43 precisions: HashMap<String, u32>,
45
46 render_commas: bool,
48
49 fixed_precisions: HashMap<String, u32>,
52}
53
54impl DisplayContext {
55 #[must_use]
57 pub fn new() -> Self {
58 Self::default()
59 }
60
61 pub fn update(&mut self, number: Decimal, currency: &str) {
66 let precision = Self::decimal_precision(number);
67 let entry = self.precisions.entry(currency.to_string()).or_insert(0);
68 *entry = (*entry).max(precision);
69 }
70
71 pub fn update_from(&mut self, other: &Self) {
75 for (currency, precision) in &other.precisions {
76 let entry = self.precisions.entry(currency.clone()).or_insert(0);
77 *entry = (*entry).max(*precision);
78 }
79 }
80
81 pub const fn set_render_commas(&mut self, render_commas: bool) {
83 self.render_commas = render_commas;
84 }
85
86 #[must_use]
88 pub const fn render_commas(&self) -> bool {
89 self.render_commas
90 }
91
92 pub fn set_fixed_precision(&mut self, currency: &str, precision: u32) {
96 self.fixed_precisions
97 .insert(currency.to_string(), precision);
98 }
99
100 #[must_use]
105 pub fn get_precision(&self, currency: &str) -> Option<u32> {
106 if let Some(&precision) = self.fixed_precisions.get(currency) {
108 return Some(precision);
109 }
110 self.precisions.get(currency).copied()
111 }
112
113 #[must_use]
119 pub fn format(&self, number: Decimal, currency: &str) -> String {
120 let precision = self.get_precision(currency);
121
122 if let Some(dp) = precision {
123 let rounded = number.round_dp(dp);
127 let formatted = format!("{rounded}");
128 let formatted = Self::ensure_decimal_places(&formatted, dp);
130 if self.render_commas {
131 Self::add_commas(&formatted)
132 } else {
133 formatted
134 }
135 } else {
136 let formatted = number.normalize().to_string();
138 if self.render_commas {
139 Self::add_commas(&formatted)
140 } else {
141 formatted
142 }
143 }
144 }
145
146 #[must_use]
148 pub fn format_amount(&self, number: Decimal, currency: &str) -> String {
149 format!("{} {}", self.format(number, currency), currency)
150 }
151
152 const fn decimal_precision(number: Decimal) -> u32 {
154 number.scale()
156 }
157
158 fn ensure_decimal_places(s: &str, dp: u32) -> String {
161 if dp == 0 {
162 return s.split('.').next().unwrap_or(s).to_string();
164 }
165
166 let dp = dp as usize;
167 if let Some(dot_pos) = s.find('.') {
168 let current_decimals = s.len() - dot_pos - 1;
169 if current_decimals >= dp {
170 s.to_string()
172 } else {
173 let zeros_needed = dp - current_decimals;
175 format!("{s}{}", "0".repeat(zeros_needed))
176 }
177 } else {
178 format!("{s}.{}", "0".repeat(dp))
180 }
181 }
182
183 fn add_commas(s: &str) -> String {
185 let (integer_part, decimal_part) = match s.find('.') {
187 Some(pos) => (&s[..pos], Some(&s[pos..])),
188 None => (s, None),
189 };
190
191 let (sign, digits) = if let Some(stripped) = integer_part.strip_prefix('-') {
193 ("-", stripped)
194 } else {
195 ("", integer_part)
196 };
197
198 let mut result = String::with_capacity(digits.len() + digits.len() / 3);
200 for (i, c) in digits.chars().rev().enumerate() {
201 if i > 0 && i % 3 == 0 {
202 result.push(',');
203 }
204 result.push(c);
205 }
206 let integer_with_commas: String = result.chars().rev().collect();
207
208 match decimal_part {
210 Some(dec) => format!("{sign}{integer_with_commas}{dec}"),
211 None => format!("{sign}{integer_with_commas}"),
212 }
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use rust_decimal_macros::dec;
220
221 #[test]
222 fn test_update_and_get_precision() {
223 let mut ctx = DisplayContext::new();
224
225 ctx.update(dec!(100), "USD");
226 assert_eq!(ctx.get_precision("USD"), Some(0));
227
228 ctx.update(dec!(50.25), "USD");
229 assert_eq!(ctx.get_precision("USD"), Some(2));
230
231 ctx.update(dec!(1), "USD");
233 assert_eq!(ctx.get_precision("USD"), Some(2));
234
235 assert_eq!(ctx.get_precision("EUR"), None);
237 }
238
239 #[test]
240 fn test_format_with_precision() {
241 let mut ctx = DisplayContext::new();
242 ctx.update(dec!(100), "USD");
243 ctx.update(dec!(50.25), "USD");
244
245 assert_eq!(ctx.format(dec!(100), "USD"), "100.00");
247 assert_eq!(ctx.format(dec!(50.25), "USD"), "50.25");
248 assert_eq!(ctx.format(dec!(7.5), "USD"), "7.50");
249 }
250
251 #[test]
252 fn test_format_unknown_currency() {
253 let ctx = DisplayContext::new();
254
255 assert_eq!(ctx.format(dec!(100), "EUR"), "100");
257 assert_eq!(ctx.format(dec!(50.25), "EUR"), "50.25");
258 }
259
260 #[test]
261 fn test_fixed_precision_override() {
262 let mut ctx = DisplayContext::new();
263 ctx.update(dec!(100), "USD");
264 ctx.update(dec!(50.25), "USD");
265
266 assert_eq!(ctx.get_precision("USD"), Some(2));
268
269 ctx.set_fixed_precision("USD", 4);
271 assert_eq!(ctx.get_precision("USD"), Some(4));
272
273 assert_eq!(ctx.format(dec!(100), "USD"), "100.0000");
275 }
276
277 #[test]
278 fn test_render_commas() {
279 let mut ctx = DisplayContext::new();
280 ctx.set_render_commas(true);
281 ctx.update(dec!(1234567.89), "USD");
282
283 assert_eq!(ctx.format(dec!(1234567.89), "USD"), "1,234,567.89");
284 assert_eq!(ctx.format(dec!(1000), "USD"), "1,000.00");
285 }
286
287 #[test]
288 fn test_add_commas() {
289 assert_eq!(DisplayContext::add_commas("1234567"), "1,234,567");
290 assert_eq!(DisplayContext::add_commas("1234567.89"), "1,234,567.89");
291 assert_eq!(DisplayContext::add_commas("-1234567.89"), "-1,234,567.89");
292 assert_eq!(DisplayContext::add_commas("123"), "123");
293 assert_eq!(DisplayContext::add_commas("1"), "1");
294 }
295
296 #[test]
297 fn test_update_from() {
298 let mut ctx1 = DisplayContext::new();
299 ctx1.update(dec!(100), "USD");
300
301 let mut ctx2 = DisplayContext::new();
302 ctx2.update(dec!(50.25), "USD");
303 ctx2.update(dec!(1.5), "EUR");
304
305 ctx1.update_from(&ctx2);
306
307 assert_eq!(ctx1.get_precision("USD"), Some(2));
308 assert_eq!(ctx1.get_precision("EUR"), Some(1));
309 }
310
311 #[test]
312 fn test_format_amount() {
313 let mut ctx = DisplayContext::new();
314 ctx.update(dec!(50.25), "USD");
315
316 assert_eq!(ctx.format_amount(dec!(100), "USD"), "100.00 USD");
317 }
318}