amount.rs (16928B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2024, 2025, 2026 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 //! Type for the Taler Amount <https://docs.taler.net/core/api-common.html#tsref-type-Amount> 18 19 use std::{ 20 fmt::{Debug, Display}, 21 num::ParseIntError, 22 str::FromStr, 23 }; 24 25 use compact_str::format_compact; 26 27 use super::utils::InlineStr; 28 use crate::signature::Buf; 29 30 /** Number of characters we use to represent currency names */ 31 // We use the same value than the exchange -1 because we use a byte for the len instead of 0 termination 32 pub const CURRENCY_LEN: usize = 11; 33 34 /** Maximum legal value for an amount, based on IEEE double */ 35 pub const MAX_VALUE: u64 = 2 << 51; 36 37 /** The number of digits in a fraction part of an amount */ 38 pub const FRAC_BASE_NB_DIGITS: u8 = 8; 39 40 /** The fraction part of an amount represents which fraction of the value */ 41 pub const FRAC_BASE: u32 = 10u32.pow(FRAC_BASE_NB_DIGITS as u32); 42 43 const CENT_FRACTION: u32 = 10u32.pow((FRAC_BASE_NB_DIGITS - 2) as u32); 44 45 #[derive( 46 Clone, Copy, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay, 47 )] 48 /// Inlined ISO 4217 currency string 49 pub struct Currency(InlineStr<CURRENCY_LEN>); 50 51 impl AsRef<str> for Currency { 52 fn as_ref(&self) -> &str { 53 self.0.as_ref() 54 } 55 } 56 57 #[derive(Debug, thiserror::Error)] 58 pub enum CurrencyErrorKind { 59 #[error("contains illegal characters (only A-Z allowed)")] 60 Invalid, 61 #[error("too long (max {CURRENCY_LEN} chars)")] 62 Big, 63 #[error("is empty")] 64 Empty, 65 } 66 67 #[derive(Debug, thiserror::Error)] 68 #[error("currency code name '{currency}' {kind}")] 69 pub struct ParseCurrencyError { 70 currency: String, 71 pub kind: CurrencyErrorKind, 72 } 73 74 impl Currency { 75 pub const TEST: Self = Self::const_parse("TEST"); 76 pub const KUDOS: Self = Self::const_parse("KUDOS"); 77 pub const EUR: Self = Self::const_parse("EUR"); 78 pub const CHF: Self = Self::const_parse("CHF"); 79 pub const HUF: Self = Self::const_parse("HUF"); 80 81 pub const fn const_parse(s: &str) -> Currency { 82 let bytes = s.as_bytes(); 83 let len = bytes.len(); 84 85 if bytes.is_empty() { 86 panic!("empty") 87 } else if len > CURRENCY_LEN { 88 panic!("too big") 89 } 90 let mut i = 0; 91 while i < bytes.len() { 92 if !bytes[i].is_ascii_uppercase() { 93 panic!("invalid") 94 } 95 i += 1; 96 } 97 Self(InlineStr::copy_from_slice(bytes)) 98 } 99 } 100 101 impl FromStr for Currency { 102 type Err = ParseCurrencyError; 103 104 fn from_str(s: &str) -> Result<Self, Self::Err> { 105 let bytes = s.as_bytes(); 106 let len = bytes.len(); 107 if bytes.is_empty() { 108 Err(CurrencyErrorKind::Empty) 109 } else if len > CURRENCY_LEN { 110 Err(CurrencyErrorKind::Big) 111 } else if !bytes.iter().all(|c| c.is_ascii_uppercase()) { 112 Err(CurrencyErrorKind::Invalid) 113 } else { 114 Ok(Self(InlineStr::copy_from_slice(bytes))) 115 } 116 .map_err(|kind| ParseCurrencyError { 117 currency: s.to_owned(), 118 kind, 119 }) 120 } 121 } 122 123 impl Debug for Currency { 124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 125 Debug::fmt(&self.as_ref(), f) 126 } 127 } 128 129 impl Display for Currency { 130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 131 Display::fmt(&self.as_ref(), f) 132 } 133 } 134 135 #[derive(sqlx::Type)] 136 #[sqlx(type_name = "taler_amount")] 137 struct PgTalerAmount { 138 pub val: i64, 139 pub frac: i32, 140 } 141 142 #[derive( 143 Clone, 144 Copy, 145 PartialEq, 146 Eq, 147 PartialOrd, 148 Ord, 149 serde_with::DeserializeFromStr, 150 serde_with::SerializeDisplay, 151 )] 152 pub struct Decimal { 153 /** Integer part */ 154 pub val: u64, 155 /** Factional part, multiple of FRAC_BASE */ 156 pub frac: u32, 157 } 158 159 impl Decimal { 160 pub const fn new(val: u64, frac: u32) -> Self { 161 Self { val, frac } 162 } 163 164 pub const ZERO: Self = Self::new(0, 0); 165 pub const MAX: Self = Self::new(MAX_VALUE, FRAC_BASE - 1); 166 167 const fn normalize(mut self) -> Option<Self> { 168 let Some(val) = self.val.checked_add((self.frac / FRAC_BASE) as u64) else { 169 return None; 170 }; 171 self.val = val; 172 self.frac %= FRAC_BASE; 173 if self.val > MAX_VALUE { 174 return None; 175 } 176 Some(self) 177 } 178 179 pub fn try_add(mut self, rhs: &Self) -> Option<Self> { 180 self.val = self.val.checked_add(rhs.val)?; 181 self.frac = self 182 .frac 183 .checked_add(rhs.frac) 184 .expect("amount fraction overflow should never happen with normalized amounts"); 185 self.normalize() 186 } 187 188 pub fn try_sub(mut self, rhs: &Self) -> Option<Self> { 189 if rhs.frac > self.frac { 190 self.val = self.val.checked_sub(1)?; 191 self.frac += FRAC_BASE; 192 } 193 self.val = self.val.checked_sub(rhs.val)?; 194 self.frac = self.frac.checked_sub(rhs.frac)?; 195 self.normalize() 196 } 197 198 pub const fn to_amount(self, currency: &Currency) -> Amount { 199 Amount::new_decimal(currency, self) 200 } 201 } 202 203 #[derive(Debug, thiserror::Error)] 204 pub enum DecimalErrKind { 205 #[error("value overflow (must be <= {MAX_VALUE})")] 206 Overflow, 207 #[error("invalid value ({0})")] 208 InvalidValue(ParseIntError), 209 #[error("invalid fraction ({0})")] 210 InvalidFraction(ParseIntError), 211 #[error("fraction overflow (max {FRAC_BASE_NB_DIGITS} digits)")] 212 FractionOverflow, 213 } 214 215 #[derive(Debug, thiserror::Error)] 216 #[error("decimal '{decimal}' {kind}")] 217 pub struct ParseDecimalErr { 218 decimal: String, 219 pub kind: DecimalErrKind, 220 } 221 222 impl FromStr for Decimal { 223 type Err = ParseDecimalErr; 224 225 fn from_str(s: &str) -> Result<Self, Self::Err> { 226 let (value, fraction) = s.split_once('.').unwrap_or((s, "")); 227 228 // TODO use try block when stable 229 (|| { 230 let value: u64 = value.parse().map_err(DecimalErrKind::InvalidValue)?; 231 if value > MAX_VALUE { 232 return Err(DecimalErrKind::Overflow); 233 } 234 235 if fraction.len() > FRAC_BASE_NB_DIGITS as usize { 236 return Err(DecimalErrKind::FractionOverflow); 237 } 238 let fraction: u32 = if fraction.is_empty() { 239 0 240 } else { 241 fraction 242 .parse::<u32>() 243 .map_err(DecimalErrKind::InvalidFraction)? 244 * 10u32.pow(FRAC_BASE_NB_DIGITS as u32 - fraction.len() as u32) 245 }; 246 Ok(Self { 247 val: value, 248 frac: fraction, 249 }) 250 })() 251 .map_err(|kind| ParseDecimalErr { 252 decimal: s.to_owned(), 253 kind, 254 }) 255 } 256 } 257 258 impl Display for Decimal { 259 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 260 if self.frac == 0 { 261 f.write_fmt(format_args!("{}", self.val)) 262 } else { 263 let num = format_compact!("{:08}", self.frac); 264 f.write_fmt(format_args!("{}.{}", self.val, num.trim_end_matches('0'))) 265 } 266 } 267 } 268 269 impl Debug for Decimal { 270 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 271 Display::fmt(&self, f) 272 } 273 } 274 275 impl sqlx::Type<sqlx::Postgres> for Decimal { 276 fn type_info() -> sqlx::postgres::PgTypeInfo { 277 PgTalerAmount::type_info() 278 } 279 } 280 281 impl<'q> sqlx::Encode<'q, sqlx::Postgres> for Decimal { 282 fn encode_by_ref( 283 &self, 284 buf: &mut sqlx::postgres::PgArgumentBuffer, 285 ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> { 286 PgTalerAmount { 287 val: self.val as i64, 288 frac: self.frac as i32, 289 } 290 .encode_by_ref(buf) 291 } 292 } 293 294 impl<'r> sqlx::Decode<'r, sqlx::Postgres> for Decimal { 295 fn decode(value: sqlx::postgres::PgValueRef<'r>) -> Result<Self, sqlx::error::BoxDynError> { 296 let pg = PgTalerAmount::decode(value)?; 297 Ok(Self { 298 val: pg.val as u64, 299 frac: pg.frac as u32, 300 }) 301 } 302 } 303 304 #[track_caller] 305 pub fn decimal(decimal: impl AsRef<str>) -> Decimal { 306 decimal.as_ref().parse().expect("Invalid decimal constant") 307 } 308 309 /// <https://docs.taler.net/core/api-common.html#tsref-type-Amount> 310 #[derive( 311 Clone, Copy, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay, 312 )] 313 pub struct Amount { 314 pub currency: Currency, 315 pub val: u64, 316 pub frac: u32, 317 } 318 319 impl Amount { 320 pub const fn new_decimal(currency: &Currency, decimal: Decimal) -> Self { 321 Self { 322 currency: *currency, 323 val: decimal.val, 324 frac: decimal.frac, 325 } 326 } 327 328 pub const fn new(currency: &Currency, val: u64, frac: u32) -> Self { 329 Self::new_decimal(currency, Decimal { val, frac }) 330 } 331 332 pub const fn max(currency: &Currency) -> Self { 333 Self::new_decimal(currency, Decimal::MAX) 334 } 335 336 pub const fn zero(currency: &Currency) -> Self { 337 Self::new_decimal(currency, Decimal::ZERO) 338 } 339 340 pub fn is_zero(&self) -> bool { 341 self.decimal() == Decimal::ZERO 342 } 343 344 /* Check is amount has fractional amount < 0.01 */ 345 pub const fn is_sub_cent(&self) -> bool { 346 !self.frac.is_multiple_of(CENT_FRACTION) 347 } 348 349 pub const fn decimal(&self) -> Decimal { 350 Decimal { 351 val: self.val, 352 frac: self.frac, 353 } 354 } 355 356 pub fn normalize(self) -> Option<Self> { 357 let decimal = self.decimal().normalize()?; 358 Some((self.currency, decimal).into()) 359 } 360 361 pub fn try_add(self, rhs: &Self) -> Option<Self> { 362 if self.currency != rhs.currency { 363 return None; 364 } 365 let decimal = self.decimal().try_add(&rhs.decimal())?; 366 Some((self.currency, decimal).into()) 367 } 368 369 pub fn try_sub(self, rhs: &Self) -> Option<Self> { 370 if self.currency != rhs.currency { 371 return None; 372 } 373 let decimal = self.decimal().try_sub(&rhs.decimal())?; 374 Some((self.currency, decimal).into()) 375 } 376 377 /** Encode amount for signature */ 378 pub fn signature_bytes(self) -> [u8; 24] { 379 Buf::new() 380 .put_u64(self.val) 381 .put_u32(self.frac) 382 .put(&self.currency.0) 383 .finish() 384 } 385 } 386 387 impl From<(Currency, Decimal)> for Amount { 388 fn from((currency, decimal): (Currency, Decimal)) -> Self { 389 Self::new_decimal(¤cy, decimal) 390 } 391 } 392 393 #[track_caller] 394 pub fn amount(amount: impl AsRef<str>) -> Amount { 395 amount.as_ref().parse().expect("Invalid amount constant") 396 } 397 398 #[derive(Debug, thiserror::Error)] 399 pub enum AmountErrKind { 400 #[error("invalid format")] 401 Format, 402 #[error("currency {0}")] 403 Currency(#[from] CurrencyErrorKind), 404 #[error(transparent)] 405 Decimal(#[from] DecimalErrKind), 406 } 407 408 #[derive(Debug, thiserror::Error)] 409 #[error("amount '{amount}' {kind}")] 410 pub struct ParseAmountErr { 411 amount: String, 412 pub kind: AmountErrKind, 413 } 414 415 impl FromStr for Amount { 416 type Err = ParseAmountErr; 417 418 fn from_str(s: &str) -> Result<Self, Self::Err> { 419 // TODO use try block when stable 420 (|| { 421 let (currency, amount) = s.trim().split_once(':').ok_or(AmountErrKind::Format)?; 422 let currency = currency.parse().map_err(|e: ParseCurrencyError| e.kind)?; 423 let decimal = amount.parse().map_err(|e: ParseDecimalErr| e.kind)?; 424 Ok((currency, decimal).into()) 425 })() 426 .map_err(|kind| ParseAmountErr { 427 amount: s.to_owned(), 428 kind, 429 }) 430 } 431 } 432 433 impl Display for Amount { 434 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 435 f.write_fmt(format_args!("{}:{}", self.currency, self.decimal())) 436 } 437 } 438 439 impl Debug for Amount { 440 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 441 Display::fmt(&self, f) 442 } 443 } 444 445 impl sqlx::Type<sqlx::Postgres> for Amount { 446 fn type_info() -> sqlx::postgres::PgTypeInfo { 447 PgTalerAmount::type_info() 448 } 449 } 450 451 impl<'q> sqlx::Encode<'q, sqlx::Postgres> for Amount { 452 fn encode_by_ref( 453 &self, 454 buf: &mut sqlx::postgres::PgArgumentBuffer, 455 ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> { 456 self.decimal().encode_by_ref(buf) 457 } 458 } 459 460 #[test] 461 fn constants() { 462 assert_eq!(format!("{}", Amount::zero(&Currency::KUDOS)), "KUDOS:0"); 463 assert_eq!( 464 format!("{}", Amount::max(&Currency::KUDOS)), 465 "KUDOS:4503599627370496.99999999" 466 ); 467 } 468 469 #[test] 470 fn test_amount_parse() { 471 const TALER_AMOUNT_FRAC_BASE: u32 = 100000000; 472 // https://git.taler.net/exchange.git/tree/src/util/test_amount.c 473 474 const INVALID_AMOUNTS: [&str; 6] = [ 475 "EUR:4a", // non-numeric, 476 "EUR:4.4a", // non-numeric 477 "EUR:4.a4", // non-numeric 478 ":4.a4", // no currency 479 "EUR:4.123456789", // precision to high 480 "EUR:1234567890123456789012345678901234567890123456789012345678901234567890", // value to big 481 ]; 482 483 for str in INVALID_AMOUNTS { 484 let amount = Amount::from_str(str); 485 assert!(amount.is_err(), "invalid {} got {:?}", str, &amount); 486 } 487 488 let eur: Currency = Currency::EUR; 489 let local: Currency = Currency::CHF; 490 let valid_amounts: Vec<(&str, &str, Amount)> = vec![ 491 ("EUR:4", "EUR:4", Amount::new(&eur, 4, 0)), // without fraction 492 ( 493 "EUR:0.02", 494 "EUR:0.02", 495 Amount::new(&eur, 0, TALER_AMOUNT_FRAC_BASE / 100 * 2), 496 ), // leading zero fraction 497 ( 498 " EUR:4.12", 499 "EUR:4.12", 500 Amount::new(&eur, 4, TALER_AMOUNT_FRAC_BASE / 100 * 12), 501 ), // leading space and fraction 502 ( 503 " CHF:4444.1000", 504 "CHF:4444.1", 505 Amount::new(&local, 4444, TALER_AMOUNT_FRAC_BASE / 10), 506 ), // local currency 507 ]; 508 for (raw, expected, goal) in valid_amounts { 509 let amount = Amount::from_str(raw); 510 assert!(amount.is_ok(), "Valid {} got {:?}", raw, amount); 511 assert_eq!( 512 *amount.as_ref().unwrap(), 513 goal, 514 "Expected {:?} got {:?} for {}", 515 goal, 516 amount, 517 raw 518 ); 519 let amount = amount.unwrap(); 520 let str = amount.to_string(); 521 assert_eq!(str, expected); 522 assert_eq!(amount, Amount::from_str(&str).unwrap(), "{str}"); 523 } 524 } 525 526 #[test] 527 fn test_amount_add() { 528 let eur: Currency = Currency::EUR; 529 assert_eq!( 530 Amount::max(&eur).try_add(&Amount::zero(&eur)), 531 Some(Amount::max(&eur)) 532 ); 533 assert_eq!( 534 Amount::zero(&eur).try_add(&Amount::zero(&eur)), 535 Some(Amount::zero(&eur)) 536 ); 537 assert_eq!( 538 amount("EUR:6.41").try_add(&amount("EUR:4.69")), 539 Some(amount("EUR:11.1")) 540 ); 541 assert_eq!( 542 amount(format!("EUR:{MAX_VALUE}")).try_add(&amount("EUR:0.99999999")), 543 Some(Amount::max(&eur)) 544 ); 545 546 assert_eq!( 547 amount(format!("EUR:{}", MAX_VALUE - 5)).try_add(&amount("EUR:6")), 548 None 549 ); 550 assert_eq!( 551 Amount::new(&eur, u64::MAX, 0).try_add(&amount("EUR:1")), 552 None 553 ); 554 assert_eq!( 555 amount(format!("EUR:{}.{}", MAX_VALUE - 5, FRAC_BASE - 1)) 556 .try_add(&amount("EUR:5.00000002")), 557 None 558 ); 559 } 560 561 #[test] 562 fn test_amount_normalize() { 563 let eur: Currency = "EUR".parse().unwrap(); 564 assert_eq!( 565 Amount::new(&eur, 4, 2 * FRAC_BASE).normalize(), 566 Some(amount("EUR:6")) 567 ); 568 assert_eq!( 569 Amount::new(&eur, 4, 2 * FRAC_BASE + 1).normalize(), 570 Some(amount("EUR:6.00000001")) 571 ); 572 assert_eq!( 573 Amount::new(&eur, MAX_VALUE, FRAC_BASE - 1).normalize(), 574 Some(Amount::new(&eur, MAX_VALUE, FRAC_BASE - 1)) 575 ); 576 assert_eq!(Amount::new(&eur, u64::MAX, FRAC_BASE).normalize(), None); 577 assert_eq!(Amount::new(&eur, MAX_VALUE, FRAC_BASE).normalize(), None); 578 579 for amount in [Amount::max(&eur), Amount::zero(&eur)] { 580 assert_eq!(amount.normalize(), Some(amount)) 581 } 582 }