A much smarter money format?

impaktor
Posts: 1008
Joined: Fri Dec 20, 2013 9:54 am
Location: Tellus
Contact:

Re: A much smarter money format?

Post by impaktor »

That was ten very long lines. :)
My code outputs "-$0" for something like -0.41 without cents. Though that was not really intended at first, I'm starting to consider this a feature rather than a bug. Being below zero is qualitatively different to above zero.
Consider rubble and radioactive waste. They have exactly that form, so I don't consider that a feature.

I have now rewritten the code -- again. The previous code I posted worked perfectly in my opinion, only "bad" thing about it was the stackoverflow-snippet, and me not understanding it, and perhaps the C++/C mix.

This version (below) borrows some of your (lwho) C++ ideas, and does not use the stackoverflow code at all (unless having a variable "pos" that starts from decimal point is considered copying the stackoverflow solution?). It is more readable (at least to me) and "only" 30 lines. It doesn't use streams and uses more C++ then my previous code.

Anyway, to answer my question in my previous post, stackoverflow code is released under cc-wiki / creative commons if I understand it correctly.
I'm starting to wonder, if "$" with localized numbers makes so much sense

I think it's ugly, as we all know, in the future the currency will be "credits" or "crowns", not "dollar", but I guess for now this is the best we got.

Unless you have any more comments, I'm preparing this for a PR.

Code: Select all

#include <iostream>
#include <cstdio>
#include <string>
#include <cmath>

std::string format_money(long int money, bool showCents=true){
  std::string decimalPoint = ",";
  std::string groupSeperator = " ";
  int groupDigits = 3;

  double Money = 0.01*double(money);              // convert from cents to $
  const char *format = (Money < 0) ? "-$%.2f" : "$%.2f";
  char buf[64];
  snprintf(buf, sizeof(buf), format, std::abs(Money));
  std::string result(buf);

  size_t pos;                                     // pos to decimal point
  for(pos = 0; pos < result.length() && result[pos] != '.'; ++pos){}

  result.replace(pos, 1, decimalPoint);           // replace decimal point

  if(!showCents){                                 // remove cents...
    bool allAreZero = true;
    for(size_t i = pos+1; i < result.length(); ++i)
      if(result[i] != '0') allAreZero = false;    //... only if all are zero

    if(allAreZero) result.erase(result.begin() + pos, result.end());
  }

  int stepp = (Money < 0) ? 2 : 1;                // compensate for "$" or "-$"
  while(pos - stepp > groupDigits){               // insert thousand seperator
    pos = pos - groupDigits;
    result.insert(pos, groupSeperator);
  }

  return result;
}


int main(int argc, char **argv) {
  // in cents
  double values[] = { 0, 1, 10, 100, 12, 123, 1234, 12300, 123456, 1234567, 12345678, 123456789 };

  for(double sign : {1.0,-1.0}){
    for(long int x : values){
      int money = sign*x;
      std::cout << "number: \t" << money << std::endl;
      std::cout << "money cent:\t" << format_money(money) << std::endl;
      std::cout << "money nocent:\t" << format_money(money,false) << "\n" << std::endl;
    }
  }
  return 0;
}
lwho
Posts: 72
Joined: Thu Jul 04, 2013 9:26 pm
Location: Germany

Re: A much smarter money format?

Post by lwho »

impaktor wrote:
My code outputs "-$0" for something like -0.41 without cents. Though that was not really intended at first, I'm starting to consider this a feature rather than a bug. Being below zero is qualitatively different to above zero.
Consider rubble and radioactive waste. They have exactly that form, so I don't consider that a feature.
You're missing the point here. I'm talking about when you call it with "showCents = false". You still see at least, whether the small number is positive or negative. Of course, you wouldn't call it with "showCents = false" for the commodity market, as for those prices cents do matter.
impaktor wrote:[stackoverflow and licensing]
Yes, it is supposed to be CC-BY-SA (but the code could be taken from another codebase with another license). Relicensing CC-BY-SA as GPL seems to be problematic. It is only allowed for compatible licenses and the list of those is empty. Anyway, I'm mostly taking Stackoverflow code samples as examples for illustration. You read them, understand them and implement the idea from scratch, so that it also fits the codebase.
impaktor wrote:Unless you have any more comments, I'm preparing this for a PR.
Only the same as ever. Mixing two formats (with and without cents) in one table looks ugly. The FormatMoney*) function is supposed to be used in tables, so it should better do, what it is told to do ("omit cents") and not try to outsmart the caller. showCents=false is not an assertion that there are no cents, it's an order, not to show them, because they don't fit into the layout. So, showing them nevertheless is just a bug. Fixing this bug even makes the code shorter and easier to read :P

And BTW: It's not as if a rounded number is a wrong number. For example, we don't show meters for planet radius, and you can be sure, that they aren't whole kilometers. Or we switch the vertical speed display from m/s to km/s at the same time, the altitude display switches from meters to kilometers, even though you lose a lot of precision, only to avoid numbers being to long and to have a consistent look.

*) According to our naming convention it should be FormatMoney, rather than format_money.
impaktor
Posts: 1008
Joined: Fri Dec 20, 2013 9:54 am
Location: Tellus
Contact:

Re: A much smarter money format?

Post by impaktor »

According to our naming convention it should be FormatMoney, rather than format_money.
OK, will fix. It was simply inherited that way from the way it is now.

EDIT: Actually, that seems to be the convention in utils.cpp: format_distance, format_date, etc. so I will leave it as it is for now.
You're missing the point here. I'm talking about when you call it with "showCents = false"...
OK.
Only the same as ever. Mixing two formats (with and without cents) in one table looks ugly... And BTW: It's not as if a rounded number is a wrong number...
OK, so this is where we currently disagree. Yes, it looks ugly to mix. Only situation my suggested code would cause this mix, is when the user is telling not to show cents, but has forgotten to floor the number passed to FormatMoney. That is a bug in my opinion. Rounding speed or distance does not affect the players interaction with the game in any way. However, rounding money will:

Consider buying X for price=10.40. The Lua author doesn't like cents, or thinks the price will always be integer, or assumes Lua integer division works as in C/C++, so uses FormatMoney(price, False). The value displayed is now $10 (the Lua-author is now satisfied), but when buying X that is not the amount withdrawn from the player's account. That in my opinion is a bug. It will only be discovered either by players with $10.00-$10.29 in their account (they will be denied the purchase, even though the displayed amount is sufficient), or players with great attention to their account balance.

I prefer to catch bugs as early as possible, therefore, with the way I implemented it, you see the cents in the table (yes it looks ugly), and the Lua-user will realize that he/she needs to floor the price before using it. So I consider it a bug finding feature. Alternatively, one could send a message to std::cerr.
lwho
Posts: 72
Joined: Thu Jul 04, 2013 9:26 pm
Location: Germany

Re: A much smarter money format?

Post by lwho »

impaktor wrote:
According to our naming convention it should be FormatMoney, rather than format_money.
OK, will fix. It was simply inherited that way from the way it is now.

EDIT: Actually, that seems to be the convention in utils.cpp: format_distance, format_date, etc. so I will leave it as it is for now.
Okay, I didn't check. I agree, you should adhere to the "local" code convention.
impaktor wrote:OK, so this is where we currently disagree.
Indeed.
impaktor wrote:I prefer to catch bugs as early as possible, therefore, with the way I implemented it, you see the cents in the table (yes it looks ugly), and the Lua-user will realize that he/she needs to floor the price before using it. So I consider it a bug finding feature. Alternatively, one could send a message to std::cerr.
I agree with catching bugs as early as possible. For the sake of argument, let's assume calling format_money with showCents=false and a non-whole amount would be a bug:

But showing something different than requested is not catching the bug, it's replacing it with another bug (and thus hiding the original bug). If you really consider it a bug, raise a Lua error when called in such a way. That will make the Lua author fix his code (either rounding the number before passing, or make all numbers whole). If raising a Lua error in this situation seems to drastic to you (it does for me, but I'm not considering it a bug anyway), you should reconsider, if you really think, it's a bug.
impaktor
Posts: 1008
Joined: Fri Dec 20, 2013 9:54 am
Location: Tellus
Contact:

Re: A much smarter money format?

Post by impaktor »

So as noted by radius75 on #2810, he wanted a way to set the minimum number to start grouping of thousands from. (i.e. "1000", not "1,000", followed by "10,000", and the rest as usual). I think the easiest is to have a *NUMBER_GROUP_MIN = "10000", like so:

Code: Select all

size_t groupMin = strtol(Lang::NUMBER_GROUP_MIN, &end, 10);
assert(*end == 0);
if(groupDigits != 0 && std::abs(money) >= groupMin){
//... group thousands, else skip it.
which will give:

Code: Select all

number: 	999999
money cent:	$9999.99
money nocent:	$10,000

number: 	123456
money cent:	$1234.56
money nocent:	$1235

number: 	1234567
money cent:	$12,345.67
money nocent:	$12,346
I just thought I'd drop this note, since I'll be adding one additional string (number) to be translated.

Full code to play with:

Code: Select all

#include <iostream>
#include <cstdio>
#include <string>
#include <cmath>
#include <cassert>

namespace Lang {
	const char *NUMBER_DECIMAL_POINT = ".";
	const char *NUMBER_GROUP_SEP = ",";
	const char *NUMBER_GROUP_NUM = "3";
	const char *NUMBER_GROUP_MIN = "10000";
}

std::string format_money(long int cents, bool showCents=true){
	char *end;                                   // for  error checking
	size_t groupDigits = strtol(Lang::NUMBER_GROUP_NUM, &end, 10);
	assert(*end == 0);

	double money = showCents ? 0.01*cents : roundf(0.01*cents);

	const char *format = (money < 0) ? "-$%.2f" : "$%.2f";
	char buf[64];
	snprintf(buf, sizeof(buf), format, std::abs(money));
	std::string result(buf);

	size_t pos = result.find_first_of('.');      // pos to decimal point

	if(showCents)                                // replace decimal point
		result.replace(pos, 1, Lang::NUMBER_DECIMAL_POINT);
	else                                         // or just remove frac. part
		result.erase(result.begin() + pos, result.end());

   size_t groupMin = strtol(Lang::NUMBER_GROUP_MIN, &end, 10);
	assert(*end == 0);

	if(groupDigits != 0 && std::abs(money) >= groupMin){
		size_t skip = (money < 0) ? 2 : 1;        // compensate for "$" or "-$"
		while(pos - skip > groupDigits){          // insert thousand seperator
			pos = pos - groupDigits;
			result.insert(pos, Lang::NUMBER_GROUP_SEP);
		}
	}
	return result;
}

int main(int argc, char **argv) {
  // in cents
  double values[] = { 0, 1, 12, 123, 1234, 12300, 999999, 123456, 1234567, 12345678, 123456789 };

  for(double sign : {1.0,-1.0}){
	 for(long int x : values){
		int money = sign*x;
		std::cout << "number: \t" << money << std::endl;
		std::cout << "money cent:\t" << format_money(money) << std::endl;
		std::cout << "money nocent:\t" << format_money(money,false) << "\n" << std::endl;
	 }
  }

  return 0;
}
Post Reply