Published First Library, Looking for Feedback

Hello, I just published my first library. It's very simple - it allows precise manipulation of monetary amounts, and can generate strings from those amounts. This was partially a learning process to really dig into all the details of making a full library. But it hopefully can be useful to others for particular applications (in fact, I have a use-case for it myself).

Any thoughts, observations, criticisms, questions are welcomed and appreciated!

https://crates.io/crates/nmoney

1 Like

Some quick observations:

  • You should also provide implementations for checked operations (i.e. CheckedAdd). CheckedAdd in num - Rust.
  • The MoneyErrorCents and MoneyErrorString error types provide no information whatsoever about what went wrong. You might also want to implement the Error trait for them: Error in std::error - Rust.
  • Your as_cents method will overflow since you are converting from u64 to i64.
  • Making the sign part of the API and making it a boolean called positive makes me itch. Making it an enum would probably be better, something like MoneySign::Positive and MoneySign::Negative.
2 Likes

These were are very helpful, and exactly the kind of feedback I was hoping for, thank you!

#1: I have not come across this trait yet, and I can immediately see how it will be useful.
#2: This is a great idea.
#3: Great catch! Guess I got a little sloppy there.
#4: I like this a lot better.

I will look into making these improvements. I really appreciate you offering these suggestions!

1 Like

There are other helpful traits found in the num crate. For example:

It's worth taking a look :wink: .

2 Likes

There's so many things beyond the Book that I can only seem to find by accident.
I only today discovered Combinators...

Having used the rusty_money crate for similar reasons, one of the missing features I would point out is an implementation for the multiplication operators, which is necessary for currency conversion [1]. Or make it an explicit type for the purpose of rate conversions, like rusty_money::ExchangeRate.

In terms of types, the sign boolean could be a type parameter [2]:

pub struct Positive;

pub struct Negative;

pub struct Money<Sign> {
    sign: Sign,
    dollars: u64,
    cents: u8,
    // ...
}

pub type PMoney = Money<Positive>;
pub type NMoney = Money<Negative>;

impl PMoney {
    pub fn new(dollars: u64, cents: u8) -> Self {
        Self {
            sign: Positive,
            dollars,
            cents,
            // ...
        }
    }
}

impl NMoney {
    pub fn new(dollars: u64, cents: u8) -> Self {
        Self {
            sign: Negative,
            dollars,
            cents,
            // ...
        }
    }
}

One problem with this is now you have different types representing monetary values, depending on whether they are positive or negative. You'll have to implement the operators for each combination separately. :frowning:

And I guess the last thing is that rusty_money offers higher precision than just two digits. It comes in handy for rounding fractional cents and other unusual cases that have to be dealt with when doing any kind of accounting. In fact, there are multiple rounding strategies to consider, and it might be important to offer more than just away-from-zero.

Sorry to compare your project to a more mature crate, but it is what came to mind.

Quick code review, while I'm looking: you don't need to make internal state mutable in some cases. This for instance: nmoney/src/money.rs at main · 0xMattB/nmoney (github.com)

Rather than binding the value mutably, mutating, and returning, you can return from either branch of an if expression. And negation is an operator, so you do not need to multiply:

fn convert_money_to_whole(money: &Money) -> i64 {
	let whole = ((money.dollars * 100) + money.cents as u64) as i64;
	
	if money.positive {
		whole
	} else {
		-whole
	}
}

Similarly, this one: nmoney/src/money.rs at main · 0xMattB/nmoney (github.com) also does not need mutable state.

fn convert_whole_to_money(whole: i64) -> Money {
	let (positive, whole) = if whole < 0 {
		(false, -whole)
	} else {
		(true, whole)
	};
	
	Money {
		dollars: (whole / 100) as u64,
		cents: (whole % 100) as u8,
		positive,
		options: Options::new()
	}
}

This shows a use of if expression in a let statement and pattern destructuring to "do two things at once" without mutating state [3].

Looks like you have a good number of tests! Love to see it.


  1. It would save a round-trip to/from i64. ↩︎

  2. Or an enum as mentioned by another commenter. ↩︎

  3. The benefit of this, as far as I am concerned is that it's more difficult to accidentally mutate the state later. By virtue that it is no longer mutable! ↩︎

1 Like

I saw rusty_money when I was scoping out the "competition" lol. I thought about including multiplication, but then it'd feel lacking without division, and that led me down a path of uncertainty.

Since this project was largely intended to be academic, I chose to keep it simple. But I also wrote it "for myself". Many years ago, I wrote a fairly complex finance GUI using pure C (with GTK+). When I was looking for Rust practice ideas, this seemed like a great idea. The conversion from precise types to strings made it a no-brainer. I guess I hoped it'd be useful for someone looking for a similar simple solution.

Sorry to compare your project to a more mature crate, but it is what came to mind.

No need to apologize, sharing different ideas is exactly what I'm looking for in coming here!

Quick code review...

These are great, thank you! It seems so obvious now. I definitely plan in incorporating these improvements.

Looks like you have a good number of tests! Love to see it.

This is the first programming language I've learned that makes it so easy to make test cases! It's actually a pleasure to implement them.

Thank you so much!

4 Likes

I'm not sure which I would prefer but I thought you might be interested to know that your could support formatting with/without a '$' by checking the alternate flag in your display impl.

Then it could go like this:

let m = Money::new(5, 25, true);
println!("amount: {m}");  // amount: $5.25
println!("amount: {m:#}");  // amount: 5.25

One important candidate to consider is amountMicros - that's micro dollars for USD, micro euros for EUR, micro yen for JPY. Gives plenty of precision for calculations, still has decent range, is still an integer, and avoids most need for shifting exchange rates.

Hello, just wanted to thank everyone again for their help and suggestions.
I've implemented most of them, the new version is 1.0.0.

https://crates.io/crates/nmoney