What should I use to specify currencies in a money manipulation library?

Either of these can work:

use std::convert::TryInto;

fn main() {
    let x:[u8;3] = *b"USD";
    let y:[u8;3] = "USD".as_bytes().try_into().unwrap();
}
1 Like

For the second one, it feels like a good match for the FromStr trait, which already returns a Result instead of the object directly:

impl std::str::FromStr for Currency {
    type Err = std::array::TryFromSliceError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        s.as_bytes().try_into().map(|symbol| Currency { symbol })
    }
}

and, in release mode, all gets compiled away into direct initialisation of the three bytes.

	movb	$68, 10(%rsp)
	movw	$23118, 8(%rsp)
	leaq	8(%rsp), %rax
2 Likes

As in the above, the idea that to make Currency as enum and define all the currencies as variants in it. I feel that its really wrong to define it like that. See, there are 180 currencies currently active in the world. So, one has to define all those variants inside the Currency. That's fine but, consider this struct.

//this struct used to store amount of some currency type
struct Money{
    amount: u32,
    currency_type: Currrency,
}

Consider this situation, now I've some amount of a given currency(basically an instance of above struct). I have to convert the amount to some other currency. So, the function definition will be like this fn convert(inp_amt: Money, out_currency: Currency) -> Amount. So, the first argument will be the amount, second argument will be the currency to which the amount needs to be converted and return type is the converted amount to that given currency in the second argument.
Now, here comes the actual implementation problem. So, first you get what currency type is in the Amount. That means you have to do like this:

match inp_amt.currency_type {
...implement 180 branches now...
}.

Since there 180 currencies. We need to write all the 180 branches, just in the above match expression. And that's not all, Once we know the inp_currency variant, we also need to know the output currency variant so that we can convert the amount to the output variant. But we need to guess the output currency variant, that's again we have write the match statement again for the output currency variant.
See this example:

match inp_amt.currency_type {
     .
     .
     .
     USD => match out_currency { ...  again implement 180 branches here ...}
    .
}

So, as you see for each branch, you have to implement 180 sub branches. So, the total branches you've to implement is 32400. There's other problems such as to give list of countries using a given currency(fn countries_list(currency: Currency) -> Vec<Country> . Here also you have to implement to 180 branches. Which gets tedious real fast.
I think best way to resolve this, writing separate types for each currency like this: struct AUS; struct USD; ... This might look stupid. I don't know, I'm still a rust noob. But, I think this can eliminate all that branch explosion problem. We have to use to create marker trait Currency(trait Currency {}) and implement it to all the currency types we have created(ex: impl Currency for USD {}). The Money struct also needs to written like this:

struct Money<T: Currency> {
    amount: u32,
    _phantom: PhantomData<T>,
}

If have you used the From trait, you can elegant write the convert like this:
let new_amount: Amount<AUS> = old_amount.into(); with zero match expressions.

The concerns you raise are valid, but the use of PhantomData seems counterproductive.

If you're assuming USD and other currencies are zero-sized, there's no reason to use PhantomData: just put the T in Money directly. Doing this also allows you to do stuff like define a custom enum that just supports a handful of currencies, and use Money<MyEnum> as well as Money<USD> and Money<AUS> by having them all defer to the inner currency field for custom code. Or maybe even Box<Money<dyn Currency>>. There's a lot of options that you close off by not having an actual value, even -- perhaps especially -- when the value is expected to be zero-sized.

What I'm doing for exchanging between currency rates is this:

pub fn exchange(from: &Money, to: &Money) -> Money {
    let result = (from.get_unit() * to.get_unit()) * 10.0_f64.powf(to.get_precision() as f64);

    Money {
        amount: Money::round(result),
        precision: to.get_precision(),
        currency: to.get_currency().clone(),
    }
}

And then use it like this:

let my_wallet = Money::new(2_55, 2);

// 1 USD = 0.8825 EUR
let usdeur = Money::new_with_currency(0_8825, 4, Currency::EUR);
let my_wallet_in_eur = Money::exchange(&my_wallet, &usdeur);

You might want a separate type for exchange rates that validates the input currency matches. As your code stands now, thereā€™s no protection against accidentally using the JPY->EUR rate when you shouldā€™ve used USD->EUR.

1 Like

Sorry, for the late reply. Yeah, now that I think about it, directly using currency is much better than using PhantomData.

1 Like

I have a function that converts from integer to float:

pub fn get_unit(&self) -> f64 {
    self.amount as f64 / ((10.0 as f64).powf(self.precision as f64))
}

Floating-point numbers are a big no-no for currency. Money usually comes in discrete quantities that are not exact binary fractions.

You might think that at the scale of an f64 it doesn't matter, but due to inflation some currencies are counted in units much smaller than the usable size. You don't want your accounting software to "round" $100 trillion to $99,999,999,999,999.98

Now that you mention it, I do store money as an i64 (because money can be negative). The get_unit function I wrote above is just to be able to print it. When doing mathematical operations I use something like this:

pub struct Money {
    /// The value of the currency in a specified precision
    amount: i64,
    /// The times that the amount is exponented by
    precision: u64,
    /// The name of the currency
    currency: Currency,
}

impl Money {
    pub fn convert_precision(&self, precision: u64) -> Money {
        let mut new_amount = self.amount as f64;
        let precision_diff = precision as f64 - self.precision as f64;

        // If the precision as increased
        if precision_diff > 0.0 {
            new_amount = new_amount * 10.0_f64.powf(precision_diff);
        } else if precision_diff < 0.0 {
            new_amount = new_amount / 10.0_f64.powf(precision_diff.abs());
        }

        Money {
            amount: Money::round(new_amount),
            precision,
            currency: self.currency.clone(),
        }
    }
    
    pub fn exchange(from: &Money, to: &Money) -> Money {
        let result = (from.get_unit() * to.get_unit()) * 10.0_f64.powf(to.get_precision() as f64);

        Money {
            amount: Money::round(result),
            precision: to.get_precision(),
            currency: to.get_currency().clone(),
        }
    }

    pub fn round(value: f64) -> i64 {
        // don't trust the rounding system of the compiler.
        // truncate all the decimals.
        let mut num_int: i64 = value.trunc() as i64;

        // calculate the decimals by the difference between
        // the truncated integer and the given value
        let num_dec = (value - (num_int as f64)).abs();

        if num_dec == 0.5 {
            // if the number is not even and is positive
            if num_int % 2 != 0 && num_int > 0 {
                // add one
                num_int += 1;
            }
            // if the number is not even and is negative
            else if num_int % 2 != 0 && num_int < 0 {
                // subtract one: this is the symmetry between positive and
                // negative numbers
                num_int -= 1;
            }
        }
        // round as normal:
        // - greater than 0.5: adds one
        // - lesser than 0.5: don't add
        else if num_dec > 0.5 && num_int > 0 {
            num_int += 1;
        }
        num_int
    }
}

impl std::ops::Add for Money {
    type Output = Self;

    fn add(self, value: Self) -> Self {
        Money::check(&self, &value);

        Money {
            amount: self.amount + value.amount,
            precision: self.precision,
            currency: self.currency.clone(),
        }
    }
}

impl std::ops::AddAssign for Money {
    fn add_assign(&mut self, value: Self) {
        Money::check(&self, &value);

        *self = Self {
            amount: self.amount + value.amount,
            precision: self.precision,
            currency: self.currency.clone(),
        };
    }
}

impl std::ops::Sub for Money {
    type Output = Self;

    fn sub(self, value: Self) -> Self {
        Self::check(&self, &value);

        Self {
            amount: self.amount - value.amount,
            precision: self.precision,
            currency: self.currency.clone(),
        }
    }
}

impl std::ops::SubAssign for Money {
    fn sub_assign(&mut self, value: Self) {
        Self::check(&self, &value);

        *self = Self {
            amount: self.amount - value.amount,
            precision: self.precision,
            currency: self.currency.clone(),
        };
    }
}

impl std::ops::Div for Money {
    type Output = Self;

    fn div(self, value: Self) -> Self {
        Self::check(&self, &value);

        let division = self.amount as f64 / value.amount as f64;
        let precision_multiplier = 10.0_f64.powf(self.precision as f64);

        let result = Self {
            amount: Self::round(division * precision_multiplier),
            precision: self.precision,
            currency: self.currency.clone(),
        };

        result
    }
}

impl std::ops::DivAssign for Money {
    fn div_assign(&mut self, value: Self) {
        Self::check(&self, &value);
        let division = self.amount as f64 / value.amount as f64;
        let precision_multiplier = 10.0_f64.powf(self.precision as f64);

        let result = Self {
            amount: Self::round(division * precision_multiplier),
            precision: self.precision,
            currency: self.currency.clone(),
        };

        *self = result
    }
}

impl std::ops::Mul for Money {
    type Output = Self;

    fn mul(self, value: Self) -> Self {
        Self::check(&self, &value);

        let result = Self {
            amount: self.amount * value.amount,
            precision: self.precision + value.precision,
            currency: self.currency.clone(),
        };

        result.convert_precision(self.precision)
    }
}

impl std::ops::MulAssign for Money {
    fn mul_assign(&mut self, value: Self) {
        Self::check(&self, &value);

        let result = Self {
            amount: self.amount * value.amount,
            precision: self.precision + value.precision,
            currency: self.currency.clone(),
        };

        *self = result.convert_precision(self.precision)
    }
}

The only times that the money gets converted to floats are when dividing, converting the precision and on exchange, but after the math is done, it gets rounded and then converted to integer again. Do you think that this might be an issue?

Sure, for example, convert_precision will turn $65,294,295,162,748,879 into $65,294,295,162,748,876.80, and there are bound to be less contrived examples. I found this one by fuzzing with a reference implementation (run it locally and stop it with Ctrl-C or it'll just keep going forever).

Another problem is what happens on overflow. Integers panic on overflow in debug mode and wrap in release mode, but floats gradually lose precision as they get bigger and saturate when converted back to integers. Most of the output from the code in that link is just a stream of integers that got "rounded" to -9223372036854775808 (i64::MIN). It's perhaps arguable whether saturating is worse than wrapping, but at least with integers you have the checked operations that never lose precision. With floating-point numbers you're essentially always gambling with the low bits.

TL;DR Discrete quantities and floating-point types don't mix.

1 Like

I thought I'd add that for storing a Currency in each Money, it's worth considering using Intern<Currency> with interment, which gives you a Copy reference to a single Currency, with fast comparisons for equality and at the size of a pointer.

2 Likes

Thank you for finding out about not working correctly. I guess I need to use BigInt.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.