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