Skip to content
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ humanize dependency, so the output is exactly as below):

| Function | Example | Result | Notes |
| ------------------------------ | --------------------------------- | --------- | ------------------------------------------- |
| `humanize(result)` | `humanize(93166031.0)` | `93.17M` | SI metric prefixes (powers of 1000), 4 sig figs, unit-less |
| `humanizeBytes(result)` | `humanizeBytes(1500000.0)` | `1.5MB` | SI decimal units (powers of 1000), no space |
| `humanizeCommas(result)` | `humanizeCommas(157121.0)` | `157,121` | comma thousands grouping |
| `humanizeFloat(result)` | `humanizeFloat(2.50)` | `2.5` | plain decimal, trailing zeros stripped |
Expand Down
1 change: 1 addition & 0 deletions internal/kromgo/expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func unaryStringFunc(name string, impl func(float64) string) cel.EnvOption {
// humanizerFuncs registers kromgo's value-formatting helpers (see formatting.go).
func humanizerFuncs() []cel.EnvOption {
return []cel.EnvOption{
unaryStringFunc("humanize", humanize),
unaryStringFunc("humanizeBytes", humanizeBytes),
unaryStringFunc("humanizeCommas", humanizeCommas),
unaryStringFunc("humanizeFloat", humanizeFloat),
Expand Down
1 change: 1 addition & 0 deletions internal/kromgo/expr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func TestCEL_Expressions(t *testing.T) {
{"int threshold", `result < 35 ? "green" : "red"`, 17.5, nil, "green"},
{"int threshold high", `result < 35 ? "green" : "red"`, 80, nil, "red"},
{"label", `labels["version"]`, 0, map[string]string{"version": "v1.2.3"}, "v1.2.3"},
{"humanize", `humanize(result)`, 93166031, nil, "93.17M"},
{"humanizeBytes", `humanizeBytes(result)`, 1572864, nil, "1.6MB"},
{"humanizeDuration short", `humanizeDuration(result)`, 9000, nil, "2h30m"},
{"humanizeDuration long", `humanizeDuration(result)`, 467 * 86400, nil, "1y3mo12d"},
Expand Down
36 changes: 36 additions & 0 deletions internal/kromgo/formatting.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,42 @@ func humanizeBytes(f float64) string {
return trimOneDecimal(v) + byteUnits[i]
}

// SI prefix ladders for humanize (powers of 1000): metricPrefixes for |f| >= 1,
// smallMetricPrefixes for |f| < 1.
var (
metricPrefixes = [...]string{"k", "M", "G", "T", "P", "E", "Z", "Y"}
smallMetricPrefixes = [...]string{"m", "u", "n", "p", "f", "a", "z", "y"}
)

// humanize formats f with SI metric prefixes and four significant figures, like
// Prometheus's `humanize`: 93166031 -> "93.17M", 1000 -> "1k", 0.0000123 -> "12.3u".
// Unit-less; 0/NaN/Inf degrade via humanizeFloat.
func humanize(f float64) string {
if f == 0 || math.IsInf(f, 0) || math.IsNaN(f) {
return humanizeFloat(f) // "0" / "+Inf" / "-Inf" / "NaN"
}
if math.Abs(f) >= 1 {
prefix := ""
for _, p := range metricPrefixes {
if math.Abs(f) < 1000 {
break
}
prefix = p
f /= 1000
}
return fmt.Sprintf("%.4g%s", f, prefix)
}
prefix := ""
for _, p := range smallMetricPrefixes {
if math.Abs(f) >= 1 {
break
}
prefix = p
f *= 1000
}
return fmt.Sprintf("%.4g%s", f, prefix)
}

// humanizeCommas formats a number with comma thousands separators in the integer part,
// e.g. 157121 -> "157,121", 1234.56 -> "1,234.56", -1234 -> "-1,234".
func humanizeCommas(f float64) string {
Expand Down
35 changes: 35 additions & 0 deletions internal/kromgo/formatting_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,38 @@ func TestHumanizeDuration(t *testing.T) {
})
}
}

func TestHumanize(t *testing.T) {
t.Parallel()
cases := []struct {
name string
in float64
want string
}{
// |f| >= 1: scale down, metric prefixes, 4 sig figs.
{"plain", 999, "999"},
{"exact k", 1000, "1k"},
{"thousands", 1500, "1.5k"},
{"k four sig figs", 157121, "157.1k"},
{"exact M", 1000000, "1M"},
{"millions", 93166031, "93.17M"},
{"giga", 2147483648, "2.147G"},
// Prometheus parity: 999.999k rounds to "1000k", no carry.
{"rounds within prefix", 999999, "1000k"},
// |f| < 1: scale up, small prefixes.
{"milli", 0.5, "500m"},
{"micro", 0.0000123, "12.3u"},
{"negative", -1500, "-1.5k"},
// 0/NaN/Inf degrade cleanly (no "0y" / "NaNk").
{"zero", 0, "0"},
{"NaN", math.NaN(), "NaN"},
{"+Inf", math.Inf(1), "+Inf"},
{"-Inf", math.Inf(-1), "-Inf"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tc.want, humanize(tc.in))
})
}
}
Loading