Skip to content

Commit 1094cb3

Browse files
committed
crhumanize: add Percent
Add a `Percent` function which prints a percentage and automatically decides to use 0 or 1 decimal digits.
1 parent eb2d55d commit 1094cb3

File tree

3 files changed

+79
-0
lines changed

3 files changed

+79
-0
lines changed

crhumanize/float.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@
1515
package crhumanize
1616

1717
import (
18+
"math"
1819
"strconv"
1920
"strings"
2021
)
2122

23+
// Float formats the given float with the specified number of decimal digits.
24+
// Trailing 0 decimals are stripped.
2225
func Float(value float64, decimalDigits int) SafeString {
2326
s := strconv.FormatFloat(value, 'f', decimalDigits, 64)
2427
s = stripTrailingZeroDecimals(s)
@@ -37,3 +40,29 @@ func stripTrailingZeroDecimals(s string) string {
3740
}
3841
return s
3942
}
43+
44+
// Percent formats (numerator/denominator) as a percentage. At most one decimal
45+
// digit is used (only when the integer part is a single digit). If denominator
46+
// is 0, returns the empty string.
47+
//
48+
// Values very close to 0 are formatted as ~0% to indicate that the value is
49+
// non-zero.
50+
//
51+
// Examples: "0.2%", "12%".
52+
func Percent[T Numeric](numerator, denominator T) SafeString {
53+
if denominator == 0 {
54+
return ""
55+
}
56+
if numerator == 0 {
57+
return "0%"
58+
}
59+
value := (float64(numerator) / float64(denominator)) * 100
60+
if math.Abs(value) < 0.05 {
61+
return "~0%"
62+
}
63+
decimalDigits := 0
64+
if math.Abs(value) < 9.95 {
65+
decimalDigits = 1
66+
}
67+
return Float(value, decimalDigits) + "%"
68+
}

crhumanize/float_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,17 @@ func TestFloat(t *testing.T) {
3333
{0.01, 1, "0"},
3434
{0.01, 2, "0.01"},
3535
{0.01, 4, "0.01"},
36+
{-1.23456789, 2, "-1.23"},
3637
{1.23456789, 2, "1.23"},
3738
{1.23456789, 3, "1.235"},
3839
{1.23456789, 3, "1.235"},
3940
{123456.7777, 1, "123456.8"},
4041
{123456.7777, 2, "123456.78"},
4142
{123456.1010, 4, "123456.101"},
4243
{123456.1010, 2, "123456.1"},
44+
{-123456.1010, 1, "-123456.1"},
4345
{123456.1010, 1, "123456.1"},
46+
{-123456.1010, 0, "-123456"},
4447
{123456.1010, 0, "123456"},
4548
}
4649

@@ -52,6 +55,34 @@ func TestFloat(t *testing.T) {
5255
}
5356
}
5457

58+
func TestPercent(t *testing.T) {
59+
tests := []struct {
60+
a, b float64
61+
expected string
62+
}{
63+
{a: 0, b: 0, expected: ""},
64+
{a: 0, b: 100, expected: "0%"},
65+
{a: 0.0001, b: 100.0, expected: "~0%"},
66+
{a: 0.044, b: 100, expected: "~0%"},
67+
{a: 0.05, b: 100, expected: "0.1%"},
68+
{a: 0.1234, b: 100.0, expected: "0.1%"},
69+
{a: -0.1234, b: 100.0, expected: "-0.1%"},
70+
{a: 0.05, b: 100.0, expected: "0.1%"},
71+
{a: 9.95, b: 100.0, expected: "10%"},
72+
{a: 9.94, b: 100.0, expected: "9.9%"},
73+
{a: -9.95, b: 100.0, expected: "-10%"},
74+
{a: -9.94, b: 100.0, expected: "-9.9%"},
75+
{a: 10.52345, b: 100.0, expected: "11%"},
76+
}
77+
78+
for _, test := range tests {
79+
result := string(Percent(test.a, test.b))
80+
if result != test.expected {
81+
t.Errorf("Percent(%f,%f) = %s; expected %s", test.a, test.b, result, test.expected)
82+
}
83+
}
84+
}
85+
5586
func ExampleFloat() {
5687
fmt.Println(Float(100.1234, 3))
5788
fmt.Println(Float(100.12, 3))
@@ -63,3 +94,17 @@ func ExampleFloat() {
6394
// 100.1
6495
// 100
6596
}
97+
98+
func ExamplePercent() {
99+
fmt.Println(Percent(uint64(0), uint64(10000)))
100+
fmt.Println(Percent(uint64(1), uint64(10000)))
101+
fmt.Println(Percent(uint64(12), uint64(10000)))
102+
fmt.Println(Percent(uint64(123), uint64(10000)))
103+
fmt.Println(Percent(uint64(1234), uint64(10000)))
104+
// Output:
105+
// 0%
106+
// ~0%
107+
// 0.1%
108+
// 1.2%
109+
// 12%
110+
}

crhumanize/humanize.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ type Integer interface {
7676
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
7777
}
7878

79+
// Numeric is a constraint that permits any integer or floating-point type.
80+
type Numeric interface {
81+
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64
82+
}
83+
7984
// SafeString represents a human readable representation of a value. It
8085
// implements a `SafeValue()` marker method (implementing the
8186
// github.com/cockroachdb/redact.SafeValue interface) to signal that it

0 commit comments

Comments
 (0)