Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 63 additions & 38 deletions number.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,57 @@ func FormatFloat(format string, n float64) string {
return "-Infinity"
}

// default format
precision := 2
decimalStr := "."
thousandStr := ","
positiveStr := ""
precision, decimalStr, thousandStr, positiveStr := parseNumberFormat(format)
negativeStr := "-"

// generate sign part
var signStr string
if n >= 0.000000001 {
signStr = positiveStr
} else if n <= -0.000000001 {
signStr = negativeStr
n = -n
} else {
signStr = ""
n = 0.0
}

// split number into integer and fractional parts
intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision])

// generate integer part string
intStr := strconv.FormatFloat(intf, 'f', 0, 64)

// add thousand separator if required
if len(thousandStr) > 0 {
for i := len(intStr); i > 3; {
i -= 3
intStr = intStr[:i] + thousandStr + intStr[i:]
}
}

// no fractional part, we can leave now
if precision == 0 {
return signStr + intStr
}

// generate fractional part
fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision]))
// may need padding
if len(fracStr) < precision {
fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr
}

return signStr + intStr + decimalStr + fracStr
}

func parseNumberFormat(format string) (precision int, decimalStr, thousandStr, positiveStr string) {
// default format
precision = 2
decimalStr = "."
thousandStr = ","
positiveStr = ""

if len(format) > 0 {
format := []rune(format)

Expand Down Expand Up @@ -145,49 +189,30 @@ func FormatFloat(format string, n float64) string {
}
}

// generate sign part
var signStr string
if n >= 0.000000001 {
return
}

// FormatInteger produces a formatted number as string.
// See FormatFloat.
func FormatInteger(format string, n int) string {
precision, decimalStr, thousandStr, positiveStr := parseNumberFormat(format)
intStr := strconv.Itoa(n)
signStr := ""
if n < 0 {
signStr = "-"
intStr = intStr[1:]
} else if n > 0 {
signStr = positiveStr
} else if n <= -0.000000001 {
signStr = negativeStr
n = -n
} else {
signStr = ""
n = 0.0
}

// split number into integer and fractional parts
intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision])

// generate integer part string
intStr := strconv.FormatFloat(intf, 'f', 0, 64)

// add thousand separator if required
if len(thousandStr) > 0 {
for i := len(intStr); i > 3; {
i -= 3
intStr = intStr[:i] + thousandStr + intStr[i:]
}
}

// no fractional part, we can leave now
if precision == 0 {
return signStr + intStr
}

// generate fractional part
fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision]))
// may need padding
if len(fracStr) < precision {
fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr
}

return signStr + intStr + decimalStr + fracStr
}

// FormatInteger produces a formatted number as string.
// See FormatFloat.
func FormatInteger(format string, n int) string {
return FormatFloat(format, float64(n))
return signStr + intStr + decimalStr + "000000000"[:precision]
}
77 changes: 77 additions & 0 deletions number_integer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package humanize

import (
"strconv"
"strings"
"testing"
)

func TestFormatIntegerExact(t *testing.T) {
tests := []struct {
value string
grouped string
}{
{"0", "0"},
{"1", "1"},
{"-1", "-1"},
{"12345", "12,345"},
{"-12345", "-12,345"},
{"2147483647", "2,147,483,647"},
{"-2147483648", "-2,147,483,648"},
{"4503599627370497", "4,503,599,627,370,497"},
{"-4503599627370497", "-4,503,599,627,370,497"},
{"9007199254740991", "9,007,199,254,740,991"},
{"9007199254740993", "9,007,199,254,740,993"},
{"-9007199254740993", "-9,007,199,254,740,993"},
{"9223372036854775807", "9,223,372,036,854,775,807"},
{"-9223372036854775808", "-9,223,372,036,854,775,808"},
}
for _, tt := range tests {
t.Run(tt.value, func(t *testing.T) {
n, err := strconv.Atoi(tt.value)
if err != nil {
if strconv.IntSize == 32 {
t.Skip("value requires a 64-bit int")
}
t.Fatal(err)
}
positive := ""
if n > 0 {
positive = "+"
}
formats := []struct {
format string
want string
}{
{"#.", tt.value},
{"#", tt.value + ".000000000"},
{"", tt.grouped + ".00"},
{"#,###.", tt.grouped},
{"+#,###.##", positive + tt.grouped + ".00"},
{"#.###,######", strings.ReplaceAll(tt.grouped, ",", ".") + ",000000"},
{"#\u202f###,##", strings.ReplaceAll(tt.grouped, ",", "\u202f") + ",00"},
{"#,##", tt.value + ",00"},
}
for _, format := range formats {
t.Run(format.format, func(t *testing.T) {
if got := FormatInteger(format.format, n); got != format.want {
t.Fatalf("got %q, want %q", got, format.want)
}
})
}
})
}
}

func TestFormatIntegerInvalidFormat(t *testing.T) {
for _, format := range []string{"-", "0.01", "0.0000000000"} {
t.Run(format, func(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("expected invalid format to panic")
}
}()
FormatInteger(format, 12345)
})
}
}
Loading