Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More units #122

Merged
merged 7 commits into from
Mar 7, 2025
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
32 changes: 26 additions & 6 deletions docs/usage/expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,20 +246,40 @@ Repeats the "string" the specified number of times

#### Humanize Number (Add Commas)

Syntax: `{hf val}`, `{hi val}`, `{percent number [precision=1]}`
Syntax: `{hf val}`, `{hi val}`

* hf: Float
* hi: Int
* percent: format as a percentage

Formats a number based with appropriate placement of commas and decimals

#### ByteSize
#### Percent

Syntax: `{bytesize intVal [precision=0]}`
Syntax: `{percent val ["precision=1"] [[min=0] max=1]}`

Create a human-readable byte-size format (eg 1024 = 1KB). An optional precision
allows adding decimals.
Formats a number as a percentage. By default, assumes the range is 0-1, therefor
`0.1234` becomes `12.3%`.

Eg.

* `{percent 0.1234}` will result in `12.3%`
* `{percent 0.1234 2` will result in `12.34%`
* `{percent 25 0 100}` will result in `25%`
* `{percent 100 4 50 150}` will result in `50.0000%`

#### ByteSize, ByteSizeSi

Syntax: `{bytesize intVal [precision=0]}`, `{bytesizesi intVal [precision=0]}`

Create a human-readable byte-size format (eg 1024 = 1KB), or in SI units (1000 = 1KB).
An optional precision allows adding decimals.

#### Downscale

Syntax: `{downscale intVal [precision=0]}`

Formats numbers by thousands (k), Millions (M), Billions (B), or Trillions (T).
eg. `{downscale 10000}` will result in `10k`

### Collecting

Expand Down
10 changes: 6 additions & 4 deletions pkg/expressions/stdlib/funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ var StandardFunctions = map[string]KeyBuilderFunction{
"bucket": KeyBuilderFunction(kfBucket),
"clamp": KeyBuilderFunction(kfClamp),
"expbucket": KeyBuilderFunction(kfExpBucket),
"bytesize": KeyBuilderFunction(kfBytesize),

// Checks
"isint": KeyBuilderFunction(kfIsInt),
Expand Down Expand Up @@ -111,9 +110,12 @@ var StandardFunctions = map[string]KeyBuilderFunction{
"haskey": kfHasKey,

// Formatting
"hi": KeyBuilderFunction(kfHumanizeInt),
"hf": KeyBuilderFunction(kfHumanizeFloat),
"percent": kfPercent,
"hi": KeyBuilderFunction(kfHumanizeInt),
"hf": KeyBuilderFunction(kfHumanizeFloat),
"bytesize": kfBytesize,
"bytesizesi": kfBytesizeSi,
"downscale": kfDownscale,
"percent": kfPercent,

// Json
"json": KeyBuilderFunction(kfJsonQuery),
Expand Down
102 changes: 89 additions & 13 deletions pkg/expressions/stdlib/funcsStrings.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,23 +173,58 @@ func kfFormat(args []KeyBuilderStage) (KeyBuilderStage, error) {
}), nil
}

// {percent val [decimals=1] [[min] max]}
func kfPercent(args []KeyBuilderStage) (KeyBuilderStage, error) {
if !isArgCountBetween(args, 1, 2) {
return stageErrArgRange(args, "1-2")
if !isArgCountBetween(args, 1, 4) {
return stageErrArgRange(args, "1-4")
}

decimals, hasDecimals := EvalArgInt(args, 1, 1)
if !hasDecimals {
return stageArgError(ErrConst, 1)
}

return func(context KeyBuilderContext) string {
val, err := strconv.ParseFloat(args[0](context), 64)
if err != nil {
return "Err%"
}
return strconv.FormatFloat(val*100.0, 'f', decimals, 64) + "%"
}, nil
switch len(args) {
case 3: // max, no min (0)
return func(context KeyBuilderContext) string {
max, err := strconv.ParseFloat(args[2](context), 64)
if err != nil {
return ErrorNum
}

val, err := strconv.ParseFloat(args[0](context), 64)
if err != nil {
return ErrorNum
}
return strconv.FormatFloat(val*100.0/max, 'f', decimals, 64) + "%"
}, nil
case 4: // min, max
return func(context KeyBuilderContext) string {
min, err := strconv.ParseFloat(args[2](context), 64)
if err != nil {
return ErrorNum
}

max, err := strconv.ParseFloat(args[3](context), 64)
if err != nil {
return ErrorNum
}

val, err := strconv.ParseFloat(args[0](context), 64)
if err != nil {
return ErrorNum
}
return strconv.FormatFloat((val-min)*100.0/(max-min), 'f', decimals, 64) + "%"
}, nil
default:
return func(context KeyBuilderContext) string {
val, err := strconv.ParseFloat(args[0](context), 64)
if err != nil {
return ErrorNum
}
return strconv.FormatFloat(val*100.0, 'f', decimals, 64) + "%"
}, nil
}
}

func kfHumanizeInt(args []KeyBuilderStage) (KeyBuilderStage, error) {
Expand Down Expand Up @@ -218,13 +253,14 @@ func kfHumanizeFloat(args []KeyBuilderStage) (KeyBuilderStage, error) {
}), nil
}

// {bytesize val [precision]}
func kfBytesize(args []KeyBuilderStage) (KeyBuilderStage, error) {
if len(args) < 1 {
return stageErrArgRange(args, "1+")
if !isArgCountBetween(args, 1, 2) {
return stageErrArgRange(args, "1-2")
}

precision, err := strconv.Atoi(EvalStageIndexOrDefault(args, 1, "0"))
if err != nil {
precision, pOk := EvalArgInt(args, 1, 0)
if !pOk {
return stageArgError(ErrNum, 1)
}

Expand All @@ -237,6 +273,46 @@ func kfBytesize(args []KeyBuilderStage) (KeyBuilderStage, error) {
}), nil
}

// {bytesizesi val [precision]}
func kfBytesizeSi(args []KeyBuilderStage) (KeyBuilderStage, error) {
if !isArgCountBetween(args, 1, 2) {
return stageErrArgRange(args, "1-2")
}

precision, pOk := EvalArgInt(args, 1, 0)
if !pOk {
return stageArgError(ErrNum, 1)
}

return KeyBuilderStage(func(context KeyBuilderContext) string {
val, err := strconv.ParseUint(args[0](context), 10, 64)
if err != nil {
return ErrorNum
}
return humanize.AlwaysByteSizeSi(val, precision)
}), nil
}

// {downscale val [precision]}
func kfDownscale(args []KeyBuilderStage) (KeyBuilderStage, error) {
if !isArgCountBetween(args, 1, 2) {
return stageErrArgRange(args, "1-2")
}

precision, pOk := EvalArgInt(args, 1, 0)
if !pOk {
return stageArgError(ErrNum, 1)
}

return func(context KeyBuilderContext) string {
val, err := strconv.ParseInt(args[0](context), 10, 64)
if err != nil {
return ErrorNum
}
return humanize.AlwaysDownscale(val, precision)
}, nil
}

func kfJoin(delim rune) KeyBuilderFunction {
return func(args []KeyBuilderStage) (KeyBuilderStage, error) {
if len(args) == 0 {
Expand Down
16 changes: 15 additions & 1 deletion pkg/expressions/stdlib/funcsStrings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,25 @@ func TestPercentFunction(t *testing.T) {
testExpression(t, mockContext("0.12345"), "{percent {0}}", "12.3%")
testExpression(t, mockContext("0.12345"), "{percent {0} 2}", "12.35%")
testExpressionErr(t, mockContext("0.12345"), "{percent {0} {0}}", "<CONST>", ErrConst)

testExpression(t, mockContext("0.12345"), "{percent {0} 2 0.5}", "24.69%")
testExpression(t, mockContext("50"), "{percent {0} 0 25 75}", "50%")

testExpressionErr(t, mockContext(), "{percent 0 1 2 3 4 5}", "<ARGN>", ErrArgCount)
}

func TestByteSize(t *testing.T) {
func TestDownscalers(t *testing.T) {
testExpression(t, mockContext("1000000"), "{bytesize {0}}", "977 KB")
testExpression(t, mockContext("1000000"), "{bytesize {0} 2}", "976.56 KB")
testExpressionErr(t, mockContext("1000000"), "{bytesize {0} 2 3}", "<ARGN>", ErrArgCount)

testExpression(t, mockContext("1000000"), "{bytesizesi {0}}", "1 mB")
testExpression(t, mockContext("1000000"), "{bytesizesi {0} 2}", "1.00 mB")
testExpressionErr(t, mockContext("1000000"), "{bytesizesi {0} 2 3}", "<ARGN>", ErrArgCount)

testExpression(t, mockContext("5120000"), "{downscale {0}}", "5M")
testExpression(t, mockContext("5120000"), "{downscale {0} 2}", "5.12M")
testExpressionErr(t, mockContext("5120000"), "{downscale {0} 2 3}", "<ARGN>", ErrArgCount)
}

func TestFormat(t *testing.T) {
Expand Down
25 changes: 0 additions & 25 deletions pkg/humanize/humanize.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,28 +39,3 @@ func Hfd(arg float64, decimals int) string {
}
return humanizeFloat(arg, decimals)
}

var byteSizes = [...]string{"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB"}

func ByteSize(n uint64) string {
if !Enabled {
return strconv.FormatUint(n, 10)
}
return AlwaysByteSize(n, 2)
}

// AlwaysByteSize formats bytesize without checking `Enabled` first
func AlwaysByteSize(n uint64, precision int) string {
if n < 1024 { // Never a decimal for byte-unit
return strconv.FormatUint(n, 10) + " " + byteSizes[0]
}

var nf float64 = float64(n)
labelIdx := 0
for nf >= 1024.0 && labelIdx < len(byteSizes)-1 {
nf /= 1024.0
labelIdx++
}

return strconv.FormatFloat(nf, 'f', precision, 64) + " " + byteSizes[labelIdx]
}
19 changes: 2 additions & 17 deletions pkg/humanize/humanize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ func TestHDisabled(t *testing.T) {
assert.Equal(t, "1000.0000", Hf(1000.0))
assert.Equal(t, "1000.00000", Hfd(1000.0, 5))
assert.Equal(t, "12341234", ByteSize(12341234))
assert.Equal(t, "12341234", ByteSizeSi(12341234))
assert.Equal(t, "12341234", Downscale(12341234, 0))
Enabled = true
}

Expand All @@ -36,20 +38,3 @@ func TestHf(t *testing.T) {
func TestHfd(t *testing.T) {
assert.Equal(t, "1,234,567.89", Hfd(1234567.89121111, 2))
}

func TestByteSize(t *testing.T) {
assert.Equal(t, "123 B", ByteSize(123))
assert.Equal(t, "1000 B", ByteSize(1000))
assert.Equal(t, "1.46 KB", ByteSize(1500))
assert.Equal(t, "2.00 MB", ByteSize(2*1024*1024))
assert.Equal(t, "5.10 GB", ByteSize(5*1024*1024*1024+100*1024*1024))
assert.Equal(t, "5 GB", AlwaysByteSize(5*1024*1024*1024+100*1024*1024, 0))
}

// 459.8 ns/op 40 B/op 3 allocs/op
func BenchmarkByteSize(b *testing.B) {
Enabled = false
for i := 0; i < b.N; i++ {
AlwaysByteSize(5*1024*1024*1024+100*1024*1024, 2)
}
}
2 changes: 1 addition & 1 deletion pkg/humanize/numeric.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type IntType interface {
func humanizeInt[T IntType](v T) string {
var buf [32]byte // stack alloc

if v >= 0 && v < 100 { // faster for small numbers
if v >= 0 && v < 100 { // faster for small numbers (nSmalls in FormatInt)
return strconv.FormatInt(int64(v), 10)
}

Expand Down
78 changes: 78 additions & 0 deletions pkg/humanize/units.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package humanize

import (
"strconv"
)

var iecSizes = [...]string{"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB"} // 1024
var siSizes = [...]string{"b", "kB", "mB", "gB", "tB", "pB", "eB", "zB"} // 1000
var unitSize = [...]string{"", "k", "M", "B", "T"} // 1000

// Returns bytesize only if humanize is enabled
func ByteSize(n uint64) string {
if !Enabled {
return strconv.FormatUint(n, 10)
}
return AlwaysByteSize(n, 2)
}

// AlwaysByteSize formats bytesize (iec, power of 2) without checking `Enabled` first
func AlwaysByteSize(n uint64, precision int) string {
return unitize(int64(n), 1024, precision, " ", iecSizes[:])
}

// Bytesize using SI (1000) units. If enabled
func ByteSizeSi(n uint64) string {
if !Enabled {
return strconv.FormatUint(n, 10)
}
return AlwaysByteSizeSi(n, 2)
}

// Bytesize using SI (1000) units, even if disabled
func AlwaysByteSizeSi(n uint64, precision int) string {
return unitize(int64(n), 1000, precision, " ", siSizes[:])
}

// Downscale numbers by thousands (unless disabled)
func Downscale(n int64, precision int) string {
if !Enabled {
return strconv.FormatInt(n, 10)
}
return AlwaysDownscale(n, precision)
}

// Downscale number by thousands
func AlwaysDownscale(n int64, precision int) string {
return unitize(n, 1000, precision, "", unitSize[:])
}

// downscale a number to a set of units
func unitize(n, step int64, precision int, delim string, units []string) string {
buf := make([]byte, 0, 16)

if n > -step && n < step {
buf = strconv.AppendInt(buf, n, 10)
unit := units[0]
if len(unit) > 0 {
buf = append(buf, delim...)
buf = append(buf, unit...)
}
return string(buf)
}

nf, sf := float64(n), float64(step)
rank := 0
for (nf <= -sf || nf >= sf) && rank < len(units)-1 {
nf /= sf
rank++
}

buf = strconv.AppendFloat(buf, nf, 'f', precision, 64)
unit := units[rank]
if len(unit) > 0 {
buf = append(buf, delim...)
buf = append(buf, unit...)
}
return string(buf)
}
Loading