367 lines
13 KiB
Plaintext
367 lines
13 KiB
Plaintext
# rust_decimal for Financial Applications: Complete Guide
|
|
|
|
rust_decimal provides a 128-bit fixed-precision decimal implementation designed specifically for financial calculations, eliminating floating-point rounding errors that plague traditional financial software. With a 96-bit mantissa and support for up to 28 decimal places, it offers the exact precision required for accounting and monetary calculations while maintaining performance suitable for high-throughput financial systems.
|
|
|
|
## Input handling best practices
|
|
|
|
### String parsing and validation patterns
|
|
|
|
rust_decimal provides multiple parsing methods optimized for different input scenarios. The **most robust approach for financial applications** uses `from_str_exact()` for strict validation combined with comprehensive error handling:
|
|
|
|
```rust
|
|
use rust_decimal::{Decimal, Error};
|
|
|
|
fn parse_financial_amount(input: &str) -> Result<Decimal, AmountError> {
|
|
let trimmed = input.trim();
|
|
|
|
// Pre-validation checks
|
|
if trimmed.is_empty() {
|
|
return Err(AmountError::EmptyInput);
|
|
}
|
|
|
|
if trimmed.len() > 50 {
|
|
return Err(AmountError::InputTooLong);
|
|
}
|
|
|
|
// Use from_str_exact for strict parsing
|
|
Decimal::from_str_exact(trimmed)
|
|
.map_err(|e| match e {
|
|
Error::InvalidOperation => AmountError::InvalidFormat,
|
|
Error::Underflow => AmountError::Underflow,
|
|
Error::Overflow => AmountError::Overflow,
|
|
_ => AmountError::ParseError,
|
|
})
|
|
}
|
|
```
|
|
|
|
The library supports **multiple input formats automatically**: standard decimal notation (`"123.45"`), scientific notation via `from_scientific("2.512e1")`, and different radix bases through `from_str_radix()`. For **compile-time optimization**, use the `dec!()` macro which parses literals at compile time with zero runtime cost.
|
|
|
|
### Precision and currency considerations
|
|
|
|
Different financial contexts require specific precision strategies. **Standard recommendations** include 2-4 decimal places for retail/e-commerce, 4-6 for forex trading, 8-18 for cryptocurrency, and 4-6 for GAAP accounting compliance. The library's maximum scale of 28 decimal places accommodates even the most demanding financial calculations.
|
|
|
|
**Currency-specific validation patterns** should enforce appropriate ranges and scales:
|
|
|
|
```rust
|
|
pub struct ValidatedAmount(Decimal);
|
|
|
|
impl ValidatedAmount {
|
|
pub fn new(value: Decimal) -> Result<Self, FinancialError> {
|
|
// Range validation
|
|
if value < Decimal::MIN || value > Decimal::MAX {
|
|
return Err(FinancialError::OutOfRange);
|
|
}
|
|
|
|
// Precision validation for currency context
|
|
if value.scale() > 28 {
|
|
return Err(FinancialError::ScaleExceeded);
|
|
}
|
|
|
|
Ok(ValidatedAmount(value))
|
|
}
|
|
}
|
|
```
|
|
|
|
### Handling different input formats
|
|
|
|
For **production systems processing various input formats**, implement a unified parsing strategy that handles integers, decimals, and scientific notation:
|
|
|
|
```rust
|
|
// Automatic conversion from integers
|
|
let amount = Decimal::from(12345_i64); // 12345
|
|
|
|
// Float conversion with precision control
|
|
let price = Decimal::from_f64(123.45).unwrap();
|
|
|
|
// Scientific notation parsing
|
|
let large_amount = Decimal::from_scientific("1.23e6").unwrap(); // 1230000
|
|
|
|
// String parsing with validation
|
|
let user_input = "99.99";
|
|
let parsed = Decimal::from_str(user_input)?;
|
|
```
|
|
|
|
## Output formatting best practices
|
|
|
|
### Display and precision control
|
|
|
|
rust_decimal provides multiple formatting approaches optimized for different financial contexts. The **standard approach** uses the `Display` trait for human-readable output, while **precision-controlled formatting** uses `round_dp()` for specific decimal places:
|
|
|
|
```rust
|
|
let amount = dec!(123.456789);
|
|
|
|
// Standard string representation
|
|
let display = amount.to_string(); // "123.456789"
|
|
|
|
// Precision-controlled output
|
|
let currency_format = amount.round_dp(2).to_string(); // "123.46"
|
|
|
|
// Scientific notation for large numbers
|
|
let scientific = format!("{:e}", amount); // "1.23456789e2"
|
|
```
|
|
|
|
### Rounding strategies for accounting
|
|
|
|
The library implements **comprehensive rounding strategies** including banker's rounding (IEEE 754 compliant) which eliminates systematic bias in large datasets:
|
|
|
|
```rust
|
|
use rust_decimal::RoundingStrategy;
|
|
|
|
let tax = dec!(3.4395);
|
|
|
|
// Banker's rounding (default) - preferred for financial compliance
|
|
let rounded = tax.round_dp(2); // Uses MidpointNearestEven
|
|
|
|
// Explicit rounding strategies
|
|
let away_from_zero = tax.round_dp_with_strategy(2, RoundingStrategy::MidpointAwayFromZero);
|
|
let truncated = tax.round_dp_with_strategy(2, RoundingStrategy::ToZero);
|
|
```
|
|
|
|
### Currency formatting and localization
|
|
|
|
For **multi-currency applications**, implement currency-aware formatting that maintains precision requirements:
|
|
|
|
```rust
|
|
#[derive(Debug, Clone)]
|
|
pub struct Money {
|
|
amount: Decimal,
|
|
currency: Currency,
|
|
}
|
|
|
|
impl Money {
|
|
pub fn format_for_display(&self, precision: u32) -> String {
|
|
match self.currency {
|
|
Currency::USD => format!("${}", self.amount.round_dp(precision)),
|
|
Currency::EUR => format!("€{}", self.amount.round_dp(precision)),
|
|
Currency::BTC => format!("₿{}", self.amount.round_dp(8)),
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Robust conversion patterns
|
|
|
|
### String-to-Decimal-to-String pipeline
|
|
|
|
The **most efficient conversion pipeline** for financial applications uses compile-time optimization where possible and validated parsing for runtime inputs:
|
|
|
|
```rust
|
|
// Compile-time optimization for known values
|
|
const COMMISSION_RATE: Decimal = dec!(0.0025);
|
|
const TAX_RATE: Decimal = dec!(0.15);
|
|
|
|
// Runtime parsing with validation
|
|
fn process_transaction(amount_str: &str) -> Result<String, ProcessingError> {
|
|
let amount = parse_financial_amount(amount_str)?;
|
|
let commission = amount * COMMISSION_RATE;
|
|
let total = amount + commission;
|
|
|
|
Ok(total.round_dp(2).to_string())
|
|
}
|
|
```
|
|
|
|
### Edge case handling
|
|
|
|
**Production-ready edge case handling** requires comprehensive validation and error recovery:
|
|
|
|
```rust
|
|
pub trait SafeDecimalOps {
|
|
fn safe_add(&self, other: Self) -> Result<Self, FinancialError>;
|
|
fn safe_multiply(&self, other: Self) -> Result<Self, FinancialError>;
|
|
fn safe_divide(&self, other: Self) -> Result<Self, FinancialError>;
|
|
}
|
|
|
|
impl SafeDecimalOps for Decimal {
|
|
fn safe_add(&self, other: Self) -> Result<Self, FinancialError> {
|
|
self.checked_add(other).ok_or(FinancialError::Overflow)
|
|
}
|
|
|
|
fn safe_divide(&self, other: Self) -> Result<Self, FinancialError> {
|
|
if other.is_zero() {
|
|
return Err(FinancialError::DivisionByZero);
|
|
}
|
|
self.checked_div(other).ok_or(FinancialError::Overflow)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Performance considerations
|
|
|
|
For **high-frequency financial calculations**, rust_decimal offers significant advantages over floating-point arithmetic despite being 2-6x slower. **Key performance characteristics** include 10-20ns for addition/subtraction, 50-100ns for multiplication, and 100-200ns for division. The library uses **stack allocation** (16 bytes per Decimal) and provides **zero-cost abstractions** through compile-time macros.
|
|
|
|
**Memory optimization strategies** include using the `Copy` trait for efficient stack-based operations, implementing batch processing patterns, and pre-allocating constants:
|
|
|
|
```rust
|
|
// Efficient batch processing
|
|
fn calculate_portfolio_value(positions: &[Position]) -> Decimal {
|
|
positions.iter()
|
|
.map(|pos| pos.quantity * pos.average_price)
|
|
.sum() // Decimal implements Sum trait
|
|
}
|
|
```
|
|
|
|
## Financial-specific features
|
|
|
|
### Scale and precision handling
|
|
|
|
rust_decimal's **128-bit architecture** provides optimal balance between precision and performance for financial applications. The **96-bit mantissa** supports approximately 28-29 significant digits, while the **32-bit metadata** handles scale (0-28) and sign information.
|
|
|
|
**Database integration patterns** vary by storage backend:
|
|
- **PostgreSQL**: Use `NUMERIC(19,4)` for standard applications, `NUMERIC(28,8)` for high precision
|
|
- **MySQL**: Use `DECIMAL(13,4)` for general use, `DECIMAL(19,4)` for large amounts
|
|
- **GAAP compliance**: Often requires 4-6 decimal places with specific rounding rules
|
|
|
|
### Monetary arithmetic best practices
|
|
|
|
**Core principles for financial calculations** include always using rust_decimal instead of floating-point, maintaining consistent scale throughout calculations, using explicit rounding strategies, and implementing overflow protection:
|
|
|
|
```rust
|
|
// Safe financial calculation with tax
|
|
let subtotal = dec!(199.99);
|
|
let tax_rate = dec!(0.0875); // 8.75%
|
|
let tax = (subtotal * tax_rate).round_dp(2);
|
|
let total = subtotal.checked_add(tax).expect("Calculation overflow");
|
|
|
|
// Interest calculation with proper rounding
|
|
let principal = dec!(10000.00);
|
|
let rate = dec!(0.045); // 4.5% annual
|
|
let compound_interest = principal * (dec!(1) + rate / dec!(12)).powi(12);
|
|
let final_amount = compound_interest.round_dp(2);
|
|
```
|
|
|
|
### Integration with accounting systems
|
|
|
|
**Database integration** requires appropriate feature flags and schema design:
|
|
|
|
```rust
|
|
// PostgreSQL integration
|
|
[dependencies]
|
|
rust_decimal = { version = "1.37", features = ["db-postgres"] }
|
|
|
|
// Usage with tokio-postgres
|
|
let amount: Decimal = dec!(1234.56);
|
|
client.execute(
|
|
"INSERT INTO transactions (amount) VALUES ($1)",
|
|
&[&amount]
|
|
)?;
|
|
```
|
|
|
|
**JSON serialization** maintains precision through string-based representation:
|
|
|
|
```rust
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
struct Invoice {
|
|
#[serde(with = "rust_decimal::serde::str")]
|
|
total: Decimal,
|
|
#[serde(with = "rust_decimal::serde::arbitrary_precision")]
|
|
tax: Decimal,
|
|
}
|
|
```
|
|
|
|
## Integration patterns
|
|
|
|
### Codebase integration strategies
|
|
|
|
**Domain-driven design patterns** provide robust abstractions for financial applications:
|
|
|
|
```rust
|
|
// Value object pattern for type safety
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct Balance(Decimal);
|
|
|
|
impl Balance {
|
|
pub fn new(amount: Decimal) -> Result<Self, Error> {
|
|
if amount < Decimal::ZERO {
|
|
return Err(Error::NegativeBalance);
|
|
}
|
|
Ok(Balance(amount))
|
|
}
|
|
|
|
pub fn add(&self, other: &Balance) -> Result<Balance, Error> {
|
|
let new_amount = self.0.checked_add(other.0)
|
|
.ok_or(Error::Overflow)?;
|
|
Ok(Balance(new_amount))
|
|
}
|
|
}
|
|
```
|
|
|
|
**Aggregate root patterns** encapsulate business logic and maintain consistency:
|
|
|
|
```rust
|
|
pub struct Account {
|
|
id: AccountId,
|
|
balance: Balance,
|
|
transactions: Vec<Transaction>,
|
|
}
|
|
|
|
impl Account {
|
|
pub fn debit(&mut self, amount: Decimal) -> Result<(), DomainError> {
|
|
if self.balance.value() < amount {
|
|
return Err(DomainError::InsufficientFunds);
|
|
}
|
|
|
|
self.balance = Balance::new(self.balance.value() - amount)?;
|
|
self.transactions.push(Transaction::debit(amount));
|
|
Ok(())
|
|
}
|
|
}
|
|
```
|
|
|
|
### Testing approaches
|
|
|
|
**Property-based testing** with proptest ensures mathematical correctness:
|
|
|
|
```rust
|
|
use proptest::prelude::*;
|
|
|
|
proptest! {
|
|
#[test]
|
|
fn test_addition_commutative(a in any::<Decimal>(), b in any::<Decimal>()) {
|
|
prop_assert_eq!(a + b, b + a);
|
|
}
|
|
|
|
#[test]
|
|
fn test_compound_interest_monotonic(
|
|
principal in 0.01f64..1000000.0,
|
|
rate in 0.001f64..0.5,
|
|
periods in 1u32..100
|
|
) {
|
|
let p = Decimal::from_f64(principal).unwrap();
|
|
let r = Decimal::from_f64(rate).unwrap();
|
|
let result = p * (Decimal::ONE + r).powi(periods as i64);
|
|
|
|
// Property: compound interest should always be >= principal
|
|
prop_assert!(result >= p);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Error handling strategies
|
|
|
|
**Comprehensive error handling** uses the `thiserror` crate for production systems:
|
|
|
|
```rust
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum FinancialError {
|
|
#[error("Insufficient funds: available {available}, required {required}")]
|
|
InsufficientFunds { available: Decimal, required: Decimal },
|
|
|
|
#[error("Currency mismatch: expected {expected}, got {actual}")]
|
|
CurrencyMismatch { expected: String, actual: String },
|
|
|
|
#[error("Precision overflow in calculation")]
|
|
PrecisionOverflow,
|
|
|
|
#[error("Division by zero")]
|
|
DivisionByZero,
|
|
}
|
|
```
|
|
|
|
## Conclusion
|
|
|
|
rust_decimal provides a mature, production-ready foundation for financial applications requiring exact precision. Its 128-bit fixed-precision architecture, comprehensive rounding strategies, and extensive ecosystem integration make it ideal for everything from simple e-commerce transactions to complex multi-currency trading platforms. The library's emphasis on correctness over raw performance, combined with Rust's memory safety guarantees, creates a robust platform for mission-critical financial systems where precision errors can have significant monetary consequences.
|
|
|
|
The key to successful implementation lies in proper domain modeling, comprehensive error handling, appropriate precision management, and thorough testing including property-based testing for financial invariants. With these practices, rust_decimal serves as a reliable foundation for financial software systems handling high-throughput transaction processing while maintaining the exact precision required for regulatory compliance and accounting accuracy.
|