commit 98e714b166a6ac74e7ca862fb0d1601ee8be5154
parent 6d7f01c392d295fb610840982e09b53d0d075284
Author: triesap <triesap@radroots.dev>
Date: Mon, 19 Jan 2026 07:21:02 +0000
app-utils: add currency helpers
- add fiat currency enum and list
- add price formatting helpers
- add currency marker parsing helper
- add unit tests for currency helpers
Diffstat:
4 files changed, 133 insertions(+), 0 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1585,6 +1585,7 @@ dependencies = [
"regex",
"serde_json",
"uuid",
+ "wasm-bindgen",
"web-sys",
]
diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml
@@ -22,6 +22,7 @@ once_cell = { workspace = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
gloo-timers = { workspace = true }
+wasm-bindgen = { workspace = true }
[dev-dependencies]
futures = { workspace = true }
diff --git a/crates/utils/src/currency/mod.rs b/crates/utils/src/currency/mod.rs
@@ -0,0 +1,126 @@
+#![forbid(unsafe_code)]
+
+use crate::numbers::parse_float;
+use crate::validation::regex::UtilRegex;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum FiatCurrency {
+ Usd,
+ Eur,
+}
+
+impl FiatCurrency {
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ FiatCurrency::Usd => "usd",
+ FiatCurrency::Eur => "eur",
+ }
+ }
+
+ pub const fn as_upper(self) -> &'static str {
+ match self {
+ FiatCurrency::Usd => "USD",
+ FiatCurrency::Eur => "EUR",
+ }
+ }
+}
+
+pub const FIAT_CURRENCIES: [FiatCurrency; 2] = [FiatCurrency::Usd, FiatCurrency::Eur];
+
+pub fn price_to_formatted(value: f64) -> f64 {
+ (value * 100.0).round() / 100.0
+}
+
+pub fn parse_currency(val: Option<&str>) -> FiatCurrency {
+ match val.map(|value| value.trim().to_lowercase()) {
+ Some(value) if value == "eur" => FiatCurrency::Eur,
+ _ => FiatCurrency::Usd,
+ }
+}
+
+pub fn fmt_price(locale: &str, value: &str, currency: &str) -> String {
+ let value = parse_float(value, 0.0);
+ let currency = parse_currency(Some(currency));
+ fmt_price_value(locale, value, currency)
+}
+
+pub fn parse_currency_marker(locale: &str, currency: &str) -> String {
+ let currency = parse_currency(Some(currency));
+ let formatted = fmt_price_value(locale, 1.0, currency);
+ if let Some(match_value) = UtilRegex::currency_marker().find(&formatted) {
+ return match_value.as_str().to_string();
+ }
+ if let Some(match_value) = UtilRegex::currency_symbol().find(&formatted) {
+ return match_value.as_str().to_string();
+ }
+ if let Some(match_value) = formatted
+ .find(currency.as_upper())
+ .map(|start| &formatted[start..start + currency.as_upper().len()])
+ {
+ return match_value.to_string();
+ }
+ currency.as_upper().to_string()
+}
+
+#[cfg(target_arch = "wasm32")]
+fn fmt_price_value(locale: &str, value: f64, currency: FiatCurrency) -> String {
+ use js_sys::{Array, Object, Reflect};
+ use wasm_bindgen::JsValue;
+
+ let locales = Array::new();
+ locales.push(&JsValue::from_str(locale));
+ let options = Object::new();
+ let currency_upper = currency.as_upper();
+ let _ = Reflect::set(&options, &JsValue::from_str("style"), &JsValue::from_str("currency"));
+ let _ = Reflect::set(
+ &options,
+ &JsValue::from_str("currency"),
+ &JsValue::from_str(currency_upper),
+ );
+ let _ = Reflect::set(
+ &options,
+ &JsValue::from_str("minimumFractionDigits"),
+ &JsValue::from_f64(2.0),
+ );
+ let _ = Reflect::set(
+ &options,
+ &JsValue::from_str("maximumFractionDigits"),
+ &JsValue::from_f64(2.0),
+ );
+ let formatter = js_sys::Intl::NumberFormat::new(&locales, &options);
+ formatter.format(value).into()
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+fn fmt_price_value(_locale: &str, value: f64, currency: FiatCurrency) -> String {
+ format!("{} {:.2}", currency.as_upper(), value)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{fmt_price, parse_currency, parse_currency_marker, price_to_formatted, FiatCurrency};
+
+ #[test]
+ fn price_to_formatted_rounds() {
+ assert_eq!(price_to_formatted(1.234), 1.23);
+ }
+
+ #[test]
+ fn parse_currency_defaults() {
+ assert_eq!(parse_currency(Some("usd")), FiatCurrency::Usd);
+ assert_eq!(parse_currency(Some("eur")), FiatCurrency::Eur);
+ assert_eq!(parse_currency(None), FiatCurrency::Usd);
+ }
+
+ #[test]
+ fn fmt_price_formats_value() {
+ let formatted = fmt_price("en-US", "1.25", "usd");
+ assert!(formatted.contains("USD"));
+ }
+
+ #[test]
+ fn parse_currency_marker_returns_token() {
+ let marker = parse_currency_marker("en-US", "usd");
+ assert!(!marker.is_empty());
+ }
+}
diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs
@@ -4,6 +4,7 @@ pub mod error;
pub mod errors;
pub mod r#async;
pub mod binary;
+pub mod currency;
pub mod id;
pub mod numbers;
pub mod object;
@@ -16,6 +17,10 @@ pub mod validation;
pub use r#async::exe_iter;
pub use binary::{as_array_buffer, RadrootsAppArrayBuffer};
+pub use currency::{
+ fmt_price, parse_currency, parse_currency_marker, price_to_formatted, FiatCurrency,
+ FIAT_CURRENCIES,
+};
pub use id::{d_tag_create, uuidv4, uuidv4_b64url, uuidv7, uuidv7_b64url};
pub use errors::{err_msg, handle_err, throw_err, ERR_PREFIX_APP, ERR_PREFIX_UTILS};
pub use numbers::{num_interval_range, num_str, parse_float, parse_int};