diff --git a/comma.go b/comma.go index 520ae3e..8be9c32 100644 --- a/comma.go +++ b/comma.go @@ -84,6 +84,16 @@ func CommafWithDigits(f float64, decimals int) string { return stripTrailingDigits(Commaf(f), decimals) } + +// CommafWithMinDigits works like the Commaf but clamps the resulting +// string to the given number of decimal places, filling any missing +// decimal places with 0. This is useful for things like formatting money. +// +// e.g. CommafWithMinDigits(834142.3, 2) -> 834,142.30 +func CommafWithMinDigits(f float64, decimals int) string { + return clampTrailingDigits(Commaf(f), decimals) +} + // BigComma produces a string form of the given big.Int in base 10 // with commas after every three orders of magnitude. func BigComma(b *big.Int) string { diff --git a/comma_test.go b/comma_test.go index c37a518..52962be 100644 --- a/comma_test.go +++ b/comma_test.go @@ -45,6 +45,19 @@ func TestCommafWithDigits(t *testing.T) { }.validate(t) } +func TestCommafWithMinDigits(t *testing.T) { + testList{ + {"1.2, 0", CommafWithMinDigits(1, 0), "1"}, + {"1.2, 0", CommafWithMinDigits(1, 1), "1.0"}, + {"1.2, 0", CommafWithMinDigits(1, 2), "1.00"}, + {"1.2, 0", CommafWithMinDigits(1, 3), "1.000"}, + {"1.2, 0", CommafWithMinDigits(1.2, 0), "1"}, + {"1.2, 1", CommafWithMinDigits(1.2, 1), "1.2"}, + {"1.2, 2", CommafWithMinDigits(1.2, 2), "1.20"}, + {"1.2, 3", CommafWithMinDigits(1.2, 3), "1.200"}, + }.validate(t) +} + func TestCommafs(t *testing.T) { testList{ {"0", Commaf(0), "0"}, diff --git a/ftoa.go b/ftoa.go index 1c62b64..fe5ff81 100644 --- a/ftoa.go +++ b/ftoa.go @@ -34,6 +34,34 @@ func stripTrailingDigits(s string, digits int) string { return s } +func clampTrailingDigits(s string, digits int) string { + i := strings.Index(s, ".") + + // short circuit. if there is no decimal separator, add one, + // and the appropriate number of zeros + if i == -1 { + // second short circuit - if they request <= 0 digits, just + // return the string, as it already doesn't have any digits. + if digits <= 0 { + return s + } + return s + "." + strings.Repeat("0", digits) + } + + // third short circuit - if they request <= 0 digits, remove + // all digits and dot + if digits <= 0 { + return s[:i] + } + + currentDigitLength := len(s)-i-1 + if digits <= currentDigitLength { + return stripTrailingDigits(s,digits) + } + + return s + strings.Repeat("0",digits-currentDigitLength) +} + // Ftoa converts a float to a string with no trailing zeros. func Ftoa(num float64) string { return stripTrailingZeros(strconv.FormatFloat(num, 'f', 6, 64)) diff --git a/ftoa_test.go b/ftoa_test.go index 45ef9c5..4eca0c3 100644 --- a/ftoa_test.go +++ b/ftoa_test.go @@ -85,6 +85,65 @@ func TestStripTrailingDigits(t *testing.T) { } } + +func TestClampTrailingDigits(t *testing.T) { + err := quick.Check(func(s string, digits int) bool { + clamped := clampTrailingDigits(s, digits) + + fmt.Println(s,digits,clamped) + + if strings.ContainsRune(s, '.') { + // If there is a dot, the part on the left of the dot will never change + a := strings.Split(s, ".") + b := strings.Split(clamped, ".") + if a[0] != b[0] { + return false + } + } else { + // If there's no dot in the input, the output will never be the same as the input + // if the number of digits is > 0. + if digits != 0 && clamped == s { + return false + } + // If there's no dot in the input, the output will always be the same as the input + // if the number of digits is 0. + if digits == 0 && clamped != s { + return false + } + } + + return true + }, &quick.Config{ + MaxCount: 10000, + Values: func(v []reflect.Value, r *rand.Rand) { + rdigs := func(n int) string { + digs := []rune{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'} + var rv []rune + for i := 0; i < n; i++ { + rv = append(rv, digs[r.Intn(len(digs))]) + } + return string(rv) + } + + ls := r.Intn(20) + rs := r.Intn(20) + jc := "." + if rs == 0 { + jc = "" + } + s := rdigs(ls) + jc + rdigs(rs) + digits := r.Intn(len(s) + 1) + + v[0] = reflect.ValueOf(s) + v[1] = reflect.ValueOf(digits) + }, + }) + + if err != nil { + t.Error(err) + } +} + func BenchmarkFtoaRegexTrailing(b *testing.B) { trailingZerosRegex := regexp.MustCompile(`\.?0+$`)