I want to format a float, e.g. 12345.67 to a string as if it were a currency, e.g. "12,345.67".
Is there a standard way of doing this with Nim?
insertSep from module strutils is able to insert separators, but it doesn’t work well with floats:
import strutils
echo ($12345.67).insertSep(',') # 12,345,.67.
So we have to do some work to get the right result:
import strutils
var s = ($12345.67).split('.')
s[0] = s[0].insertSep(',')
echo s.join(".") # 12,345.67
It’s not very satisfying, but it avoids to write a lot of code.
see previous attempt here: https://github.com/nim-lang/Nim/pull/15421
I still think this would be a useful API, but a new proposal should use the discussions in that PR into account.
I do this:
import math
import strformat
# ----------------------------------------------------------------------------------------------------------------------
proc fmtFloat*(value: float, decimals: int, format: string = ""): string =
if value != value:
return "NaN"
elif value == Inf:
return "Inf"
elif value == NegInf:
return "-Inf"
let
forceSign = format.find('s') >= 0
thousands = format.find('t') >= 0
removeZero = format.find('z') >= 0
var valueStr = ""
if decimals >= 0:
valueStr.formatValue(round(value, decimals), "." & $decimals & "f")
if decimals == 0:
valueStr &= '0'
else:
valueStr = $value
if valueStr[0] == '-':
valueStr = valueStr[1 .. ^1]
let
period = valueStr.find('.')
sign = if value < 0.0: "-" elif forceSign: "+" else: ""
var
integer = ""
integerTmp = valueStr[0 .. period - 1]
decimal = "." & valueStr[period + 1 .. ^1]
if thousands:
while true:
let integerCount = integerTmp.len
if integerCount > 3:
integer = ',' & integerTmp[^3 .. ^1] & integer
integerTmp = integerTmp[0 .. ^4]
else:
integer = integerTmp & integer
break
else:
integer = integerTmp
while removeZero:
if decimal[^1] == '0':
decimal = decimal[0 .. ^2]
else:
break
if decimal == ".":
decimal = ""
return sign & integer & decimal
# ----------------------------------------------------------------------------------------------------------------------
when isMainModule:
echo fmtFloat(12_000.758, 2) # 12000.76
echo fmtFloat(12_000.758, 2, "t") # 12,000.76
echo fmtFloat(12_000.758, 2, "st") # +12,000.76
echo fmtFloat(12_000.758, 5, "t") # 12,000.75800
echo fmtFloat(12_000.758, 5, "st") # +12,000.75800
echo fmtFloat(12_000.758, 5, "stz") # +12,000.758
echo fmtFloat(12_000.758, 0, "t") # 12,001.0
echo fmtFloat(12_000.758, 0, "stz") # +12,001
echo fmtFloat(10_000_000.0, 2, "t") # 10,000,000.00
echo fmtFloat(10_000_000.0, 2, "st") # +10,000,000.00
echo fmtFloat(10_000_000.0, 2, "tz") # 10,000,000
echo fmtFloat(10_000_000.0, 2, "stz") # +10,000,000
echo fmtFloat(-5_000.12345, -1) # -5000.12345
echo fmtFloat(-5_000.12345, -1, "t") # -5,000.12345
I like it; would you consider writing a PR?
there's a few things I would change in the API but it could be addressed in code review.
also, this case fails:
doAssert fmtFloat(-0.0, 1, "t") == "-0.0"
you can use math.signbit, or better, factor out the 1st part with math.classify to take care of all edge cases 1stThe API is indeed a bit "English-centric". The documentation for strutils.formatFloat() allows the user to specify the separator between the integer and rational part of a number via the decimalSep parameter.
While English speaking countries use the format 12,345.78, Germans switch the comma and period (not just in the context of money), i.e. 12.345,78. It shouldn't be hard to make the thousand-separator configurable as well. Though @xigoi's example does not separate a number by steps of a thousand consistently.
Other languages and cultures might use entirely different coventions to format an amount of money. This makes it hard to create one proc that covers all of them.
Hi,
I like it; would you consider writing a PR against nim repo refs https://github.com/timotheecour/Nim/issues/766) there's a few things I would change in the API but it could be addressed in code review.
Sure, no problem at all.
also, this case fails: doAssert fmtFloat(-0.0, 1, "t") == "-0.0"
Oh, yes, the -0.0 case... it's solved now (I've edited the code of the post). Maybe it could be a good idea never include the sign when the value is 0 (?).
I've also included optional params to define the thousand and decimal separators, and now there is no decimal part if decimals is 0.
Maybe it could be a good idea never include the sign when the value is 0 (?).
the sign is useful, eg: 1/-0.0 = -Inf 1/0.0 = Inf
the sign is useful, eg:
I was referring to the output of the function, once the float number is converted to string.
Example:
fmtFloat(-0.0, 2) == "0.00"
decimalSep: should be char, not string
Yes, I was going to use chars for the separators but I used strings to have the option of using as separators unicode symbols if necessary, very remote case, not sure if it's really necessary...
I was referring to the output of the function, once the float number is converted to string.
still, the sign is useful to have in output (but ok to have option to omit it for 0)
very remote case, not sure if it's really necessary...
if this impacts performance, it's not worth it, but it may be possible to have near-0 impact via:
...
if decimalSep.len == 1: specialCase for char
I like it; would you consider writing a PR against nim repo ? (refs https://github.com/timotheecour/Nim/issues/766)
Oh er... But the code is pretty bad, both slow and hard to understand...
But the code is pretty bad, both slow and hard to understand..
it can be improved during code review; at least it seems more correct than https://github.com/nim-lang/Nim/pull/15421 which I kept breaking during code review