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

Hi,

I'm creating a money manipulation library (just like the Dinero library in Javascript) that uses integers and makes the bankers rounding.

For now I don't know if I should use an enum with currencies, a Struct or a String. Using an enum is easy and I get autocomplete with all the possible options but I don't want to create a huge enum with hundreds of currencies. Using a String will make it a little cumbersome to create new instances as it is needed to use String::from("USD") and it is error prone.

Also, the currency will be compared between Money instances as it is not possible to make calculations with money that have different currencies or different precision.

What should I use?

For now I'm using an enum:

#[derive(Debug, PartialEq, Clone, Copy)]
pub enum Currency {
    USD,
    EUR,
    JPY,
    GBP,
    AUD,
    CNY
}
#[derive(Debug, Clone, Copy)]
pub struct Money {
    /// The value of the currency in a specified precision
    amount: i64,
    /// The number of decimal cases
    precision: u64,
    /// The name of the currency
    currency: Currency
}

impl Money {
    pub fn new(amount: i64, precision: u64) -> Money {
        Money {
            amount,
            precision,
            currency: Currency::USD,
        }
    }

    pub fn new_with_currency(amount: i64, precision: u64, currency: Currency) -> Money {
        Money {
            amount,
            precision,
            currency,
        }
    }

    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
    }

    pub fn match_precision(money_1: &Money, money_2: &Money) -> bool {
        money_1.get_precision() == money_2.get_precision()
    }

    pub fn match_currency(money_1: &Money, money_2: &Money) -> bool {
        money_1.get_currency() == money_2.get_currency()
    }

    fn check(money_1: &Money, money_2: &Money) {
        if Money::match_currency(money_1, money_2) == false {
            panic!("Cannot add values of different currencies");
        } else if Money::match_precision(money_1, money_2) == false {
            panic!("Cannot add values with different precisions.");
        }
    }
}

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(),
        }
    }
}

Edit: All the answer were very helpful. I chose to create a currency this way:

#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub struct Currency {
    name: &'static str,
    symbol: &'static str
}

impl Currency {
    /// Create a new currency with the specified name and symbol.
    ///
    ///
    /// ## Example
    ///
    ///
    /// ```
    /// use denarii::denarius::{ Denarius, Currency };
    /// let usd = Currency::new("USD", "$");
    /// ```
    pub fn new(name: &'static str, symbol: &'static str) -> Currency {
        Currency {
            name: name,
            symbol: symbol
        }
    }
}

I didn't use [u8; 3] because the struct wouldn't implement the Copy, Eq and PartialEq traits.

Another option is to use a trait and zero-sized structs:

trait Currency: Copy {
    fn abbrev(self) -> &'static str;
    fn symbol(self) -> &'static str;
}

#[derive(Copy)]
struct UsDollar;

impl Currency for UsDollar {
    fn abbrev(self)->&'static str {"USD"}
    fn symbol(self)->&'static str {"$"}
}

#[derive(Debug, Clone, Copy)]
pub struct Money<C:Currency> {
    /// The value of the currency in a specified precision
    amount: i64,
    /// The number of decimal cases
    precision: u64,
    /// The name of the currency
    currency: C
}
4 Likes

Unless I knew for certain that I had a closed set of currencies to deal with, I would probably use a struct like

#[derive(Copy,Clone,Eq,PartialEq)]
struct Currency {
    symbol: [u8; 3]
}

myself. It would depend a bit upon whether you expect to come across a different currency like NZD as input.

I'm not sure that there's a use case for Money with a different scale from the currency. Can you just assume that the scale is 2 for everything except JPY?

1 Like

Gas stations set prices to the $0.001/gallon, most general accounting is accurate to $0.01, and the IRS often lets you drop cents entirely from reported values.

Also, there are plenty of currencies that use a traditional fraction other that 0.01. ISK is usually tracked by 1, but sometimes by 1000.

2 Likes

Why not?

Why not? If you know you won't be able to handle arbitrary currencies, only the ones you explicitly prepare your library for, then enum is the way to go.

Definitely do not use just a String.

Do provide Display and FromStr implementations, for conversion to and from - human-readable representation.

Instead of panicking, return a Result<Money, SomeError> in your constructor. In general, never panic in a library unless you absolutely can't do anything else. In this manner, you give consumers the choice of handling the error gracefully, panicking, or ignoring it, or really just proceeding however they want. I wouldn't want a library force a panic upon my code if, for example, I can just display "invalid currency or amount" to the user and keep waiting for valid input instead.

Other than that, your current implementation is almost what I arrived at a couple years ago for a business project.

3 Likes

Thank you for your response.
But if I want to make the library public, other people might need to add more currencies. It really is convenient to have an enum as I can get auto-completion and also make sure that there are no typos, but i guess it is not as convenient to extend.

Wouldn't it be ridiculous to have hundreds of options for a currency when most won't be used?
I'm thinking this as I would like to publish it to cargo and while I won't need most of the currencies, someone would need them.

In this case, Currency should be a trait, declaring whatever behavior these other people will have to implement.

3 Likes

Only if it's not your intention to provide support for a closed set of currencies, which was not clear from your original question.

"Hundreds" is irrelevant to the design decision here. If you only support a closed set of currencies, it doesn't matter whether there are 2 or 200, that's still a strong case for using enum.

Since you want to allow an open set (i.e. allow users to define additional currencies) then you need to figure out what data and/or behavior represents a currency for your application and either

  • put the data in a struct Currency that users may create, or
  • define a trait Currency that users may implement, and make things like Money generic over C: Currency.

Which one is better for you depends on your use case, but I'd actually lean toward the first one. It can be tempting to encode all your domain rules into the type system but that can actually be a huge mistake for some applications. If you're doing something like reading a bunch of spreadsheets that are defined in different currencies at runtime, you don't have the luxury of strongly typing them all (unless you put a big match in the code that enumerates all the possibilities, and then you might as well use an enum in the first place).

1 Like

Thank you. Indeed if you had to create all currencies, might as well have an enum with all of them. Just out of curiosity, if you had to make a program about handling money would you prefer to select a currency from an enum or would you prefer to create your own currency and then use references of it in every instance of money (avoiding to create the same currency for each instance)?

It depends on the particular application. In most cases, I’d limit it to a single hardcoded currency, or maybe two if it’s about conversions. More than that and I’d probably look into reading currency definitions from a config file at runtme.

1 Like

Thank you. Just one last question: should the Money struct own the Currency struct or just have a reference to it? Owning it might make the code a little bit more awkward if I use it like this:

let balance_a = Money::new(120_00, 2, Currency::new(String::from("EUR")));
let balance_b = Money::new(120_00, 2, Currency::new(String::from("EUR")));

Versus:

let eur = Currency::new(String::from("EUR"));
let balance_a = Money::new(120_00, 2, &eur);
let balance_b = Money::new(120_00, 2, &eur);

Thank you. Should the Money struct own the Currency struct or just have a reference to it? Owning it might make the code a little bit more awkward if I use it like this:

let balance_a = Money::new(120_00, 2, Currency::new(String::from("EUR")));
let balance_b = Money::new(120_00, 2, Currency::new(String::from("EUR")));

Versus:

let eur = Currency::new(String::from("EUR"));
let balance_a = Money::new(120_00, 2, &eur);
let balance_b = Money::new(120_00, 2, &eur);

It gets pointed out a lot here that putting references in structs is a bad idea. Besides that, if your struct contains [u8; 3], that takes up three bytes plus padding for an owned Currency, compared to 8 bytes for &Currency, 16 bytes for &str or 24 bytes for String.

3 Likes

Money isn't my usual field, so I'm not comfortable making a guess. If you don't have a solid idea what use cases you want to support with this library, you may want to narrow it down some more. "Deals with money" sounds like a very broad category.

2 Likes

Owning is certainly the right call here, but you can do a few things to make it more convenient to use depending on how it’s built internally. If new is a const fn, for example, the usage looks like this:

const EUR: Currency = Currency::new("EUR");
let balance_a = Money::new(120_00, 2, EUR);
let balance_b = Money::new(120_00, 2, EUR);

Alternatively, you might be able to #[derive(Copy,Clone)]:

let eur: Currency = Currency::new("EUR");
let balance_a = Money::new(120_00, 2, eur);
let balance_b = Money::new(120_00, 2, eur);

In any case, I’d strongly recommend you take &str as a parameter instead of String. If you want one internally, you can build it inside the constructor.

1 Like

Thank you. This looks easier than to implement references everywhere.

I can't use a String if I want the Currency to derive the Copy trait so, is this the correct way to use a &str:

struct Currency {
    symbol: &'static str
}

There are a few options:

  • &’static str isn’t bad, but requires leaking a few bytes of memory if you ever need to construct one from user input.
  • [u8;3] restricts the length to 3 bytes, which covers most currency identifiers
  • Cow<‘static, str> can’t be Copy, but it is Clone and can be constructed in a const fn.
1 Like

How can I transform "USD" into [u8;3]?

This gives me a mismatched type error:

#[derive(Debug, PartialEq, Clone, Copy)]
pub struct Currency {
    name: [u8;3]
}

impl Currency {
    pub fn new(name: &str) -> Currency {
        Currency {
            name
        }
    }
}