summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke T. Shumaker <lukeshu@lukeshu.com>2024-02-19 14:46:33 -0700
committerLuke T. Shumaker <lukeshu@lukeshu.com>2024-02-19 21:49:29 -0700
commit8d9d4c98439bdfbfccaba28357944b6d866867bf (patch)
treeea1c03dac3b0b7e197bdf150a8392ce7731aa9ad
parent76f668122f9feeb20e885be330990a750107d5dd (diff)
textui: Fix Metric() and IEC(), add tests, accept math/big values
-rw-r--r--lib/fmtutil/fmt.go24
-rw-r--r--lib/textui/text.go203
-rw-r--r--lib/textui/text_test.go138
3 files changed, 321 insertions, 44 deletions
diff --git a/lib/fmtutil/fmt.go b/lib/fmtutil/fmt.go
index bad4a30..3d5fcb5 100644
--- a/lib/fmtutil/fmt.go
+++ b/lib/fmtutil/fmt.go
@@ -1,4 +1,4 @@
-// Copyright (C) 2022-2023 Luke Shumaker <lukeshu@lukeshu.com>
+// Copyright (C) 2022-2024 Luke Shumaker <lukeshu@lukeshu.com>
//
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -35,6 +35,28 @@ func FmtStateString(st fmt.State, verb rune) string {
return ret.String()
}
+// FmtStateStringWidth is like [FmtStateString], but overrides
+// st.Width().
+func FmtStateStringWidth(st fmt.State, verb rune, width int) string {
+ var ret strings.Builder
+ ret.WriteByte('%')
+ for _, flag := range []int{'-', '+', '#', ' ', '0'} {
+ if st.Flag(flag) {
+ ret.WriteByte(byte(flag))
+ }
+ }
+ fmt.Fprintf(&ret, "%v", width)
+ if prec, ok := st.Precision(); ok {
+ if prec == 0 {
+ ret.WriteByte('.')
+ } else {
+ fmt.Fprintf(&ret, ".%v", prec)
+ }
+ }
+ ret.WriteRune(verb)
+ return ret.String()
+}
+
// FormatByteArrayStringer is function for helping to implement
// fmt.Formatter for []byte or [n]byte types that have a custom string
// representation. Use it like:
diff --git a/lib/textui/text.go b/lib/textui/text.go
index aba946b..b0df463 100644
--- a/lib/textui/text.go
+++ b/lib/textui/text.go
@@ -1,4 +1,4 @@
-// Copyright (C) 2022-2023 Luke Shumaker <lukeshu@lukeshu.com>
+// Copyright (C) 2022-2024 Luke Shumaker <lukeshu@lukeshu.com>
//
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -10,6 +10,8 @@ import (
"fmt"
"io"
"math"
+ "math/big"
+ "unicode/utf8"
"golang.org/x/exp/constraints"
"golang.org/x/text/language"
@@ -37,6 +39,8 @@ func Sprintf(key string, a ...any) string {
return printer.Sprintf(key, a...)
}
+////////////////////////////////////////////////////////////////////////////////
+
// Humanized wraps a value such that formatting of it can make use of
// the `golang.org/x/text/message.Printer` extensions even when used
// with plain-old `fmt`.
@@ -63,6 +67,8 @@ func (h humanized) String() string {
return fmt.Sprint(h)
}
+////////////////////////////////////////////////////////////////////////////////
+
// Portion renders a fraction N/D as both a percentage and
// parenthetically as the exact fractional value, rendered with
// human-friendly commas.
@@ -85,19 +91,101 @@ func (p Portion[T]) String() string {
return printer.Sprintf("%d%% (%v/%v)", pct, uint64(p.N), uint64(p.D))
}
-type metric[T constraints.Integer | constraints.Float] struct {
- Val T
+////////////////////////////////////////////////////////////////////////////////
+
+// toRat(x) returns `x` as a [*big.Rat], or `nil` if `x` is NaN.
+func toRat[T constraints.Integer | constraints.Float | *big.Int | *big.Float | *big.Rat](x T) *big.Rat {
+ var y *big.Rat
+ switch x := any(x).(type) {
+ case *big.Rat:
+ y = new(big.Rat).Set(x)
+ case *big.Float:
+ y, _ = x.Rat(nil)
+ case *big.Int:
+ y = new(big.Rat).SetInt(x)
+
+ case uint:
+ y = new(big.Rat).SetUint64(uint64(x))
+ case uint8:
+ y = new(big.Rat).SetUint64(uint64(x))
+ case uint16:
+ y = new(big.Rat).SetUint64(uint64(x))
+ case uint32:
+ y = new(big.Rat).SetUint64(uint64(x))
+ case uint64:
+ y = new(big.Rat).SetUint64(x)
+ case uintptr:
+ y = new(big.Rat).SetUint64(uint64(x))
+
+ case int:
+ y = new(big.Rat).SetInt64(int64(x))
+ case int8:
+ y = new(big.Rat).SetInt64(int64(x))
+ case int16:
+ y = new(big.Rat).SetInt64(int64(x))
+ case int32:
+ y = new(big.Rat).SetInt64(int64(x))
+ case int64:
+ y = new(big.Rat).SetInt64(x)
+
+ case float32:
+ if math.IsNaN(float64(x)) {
+ y = nil
+ } else {
+ y, _ = big.NewFloat(float64(x)).Rat(nil)
+ }
+ case float64:
+ if math.IsNaN(x) {
+ y = nil
+ } else {
+ y, _ = big.NewFloat(x).Rat(nil)
+ }
+
+ default:
+ panic(fmt.Errorf("should not happen: unmatched type %T", x))
+ }
+ return y
+}
+
+func formatFloatWithSuffix(f fmt.State, verb rune, val float64, suffix string) {
+ var wrapped any = val // float64 or number.Decimal[float64]
+ if !math.IsNaN(val) {
+ var options []number.Option
+ if width, ok := f.Width(); ok {
+ width -= utf8.RuneCountInString(suffix)
+ options = append(options, number.FormatWidth(width))
+ }
+ if prec, ok := f.Precision(); ok {
+ options = append(options, number.Precision(prec))
+ }
+ wrapped = number.Decimal(val, options...)
+ }
+ var format string
+ if width, ok := f.Width(); ok {
+ width -= utf8.RuneCountInString(suffix)
+ format = fmtutil.FmtStateStringWidth(f, verb, width)
+ } else {
+ format = fmtutil.FmtStateString(f, verb)
+ }
+ _, _ = printer.Fprintf(f, format+"%s",
+ wrapped, suffix)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+type metric struct {
+ Val *big.Rat
Unit string
}
var (
- _ fmt.Formatter = metric[int]{}
- _ fmt.Stringer = metric[int]{}
+ _ fmt.Formatter = metric{}
+ _ fmt.Stringer = metric{}
)
-func Metric[T constraints.Integer | constraints.Float](x T, unit string) metric[T] {
- return metric[T]{
- Val: x,
+func Metric[T constraints.Integer | constraints.Float | *big.Int | *big.Float | *big.Rat](x T, unit string) metric {
+ return metric{
+ Val: toRat(x),
Unit: unit,
}
}
@@ -128,46 +216,67 @@ var metricBigPrefixes = []string{
"Q",
}
+var (
+ one = big.NewRat(1, 1)
+ kilo = big.NewRat(1000, 1)
+ kiloInv = new(big.Rat).Inv(kilo)
+)
+
+func lt(a, b *big.Rat) bool {
+ return a.Cmp(b) < 0
+}
+
+func gte(a, b *big.Rat) bool {
+ return a.Cmp(b) >= 0
+}
+
// String implements fmt.Formatter.
-func (v metric[T]) Format(f fmt.State, verb rune) {
+func (v metric) Format(f fmt.State, verb rune) {
var prefix string
- y := math.Abs(float64(v.Val))
- if y < 1 {
- for i := 0; y < 1 && i <= len(metricSmallPrefixes); i++ {
- y *= 1000
- prefix = metricSmallPrefixes[i]
- }
+ var float float64
+ if v.Val == nil {
+ float = math.NaN()
} else {
- for i := 0; y > 1000 && i <= len(metricBigPrefixes); i++ {
- y /= 1000
- prefix = metricBigPrefixes[i]
+ rat := new(big.Rat).Abs(v.Val)
+ if lt(rat, one) {
+ for i := 0; lt(rat, one) && i < len(metricSmallPrefixes); i++ {
+ rat.Mul(rat, kilo)
+ prefix = metricSmallPrefixes[i]
+ }
+ } else {
+ for i := 0; gte(rat, kilo) && i < len(metricBigPrefixes); i++ {
+ rat.Mul(rat, kiloInv)
+ prefix = metricBigPrefixes[i]
+ }
}
+ if v.Val.Sign() < 0 {
+ rat.Neg(rat)
+ }
+ float, _ = rat.Float64()
}
- if v.Val < 0 {
- y = -y
- }
- _, _ = printer.Fprintf(f, fmtutil.FmtStateString(f, verb)+"%s%s",
- y, prefix, v.Unit)
+ formatFloatWithSuffix(f, verb, float, prefix+v.Unit)
}
// String implements fmt.Stringer.
-func (v metric[T]) String() string {
+func (v metric) String() string {
return fmt.Sprint(v)
}
-type iec[T constraints.Integer | constraints.Float] struct {
- Val T
+////////////////////////////////////////////////////////////////////////////////
+
+type iec struct {
+ Val *big.Rat
Unit string
}
var (
- _ fmt.Formatter = iec[int]{}
- _ fmt.Stringer = iec[int]{}
+ _ fmt.Formatter = iec{}
+ _ fmt.Stringer = iec{}
)
-func IEC[T constraints.Integer | constraints.Float](x T, unit string) iec[T] {
- return iec[T]{
- Val: x,
+func IEC[T constraints.Integer | constraints.Float | *big.Int | *big.Float | *big.Rat](x T, unit string) iec {
+ return iec{
+ Val: toRat(x),
Unit: unit,
}
}
@@ -183,22 +292,32 @@ var iecPrefixes = []string{
"Yi",
}
+var (
+ kibi = big.NewRat(1024, 1)
+ kibiInv = new(big.Rat).Inv(kibi)
+)
+
// String implements fmt.Formatter.
-func (v iec[T]) Format(f fmt.State, verb rune) {
+func (v iec) Format(f fmt.State, verb rune) {
var prefix string
- y := math.Abs(float64(v.Val))
- for i := 0; y > 1024 && i <= len(iecPrefixes); i++ {
- y /= 1024
- prefix = iecPrefixes[i]
- }
- if v.Val < 0 {
- y = -y
+ var float float64
+ if v.Val == nil {
+ float = math.NaN()
+ } else {
+ rat := new(big.Rat).Abs(v.Val)
+ for i := 0; gte(rat, kibi) && i < len(iecPrefixes); i++ {
+ rat.Mul(rat, kibiInv)
+ prefix = iecPrefixes[i]
+ }
+ if v.Val.Sign() < 0 {
+ rat.Neg(rat)
+ }
+ float, _ = rat.Float64()
}
- _, _ = printer.Fprintf(f, fmtutil.FmtStateString(f, verb)+"%s%s",
- number.Decimal(y), prefix, v.Unit)
+ formatFloatWithSuffix(f, verb, float, prefix+v.Unit)
}
// String implements fmt.Stringer.
-func (v iec[T]) String() string {
+func (v iec) String() string {
return fmt.Sprint(v)
}
diff --git a/lib/textui/text_test.go b/lib/textui/text_test.go
index 4b93683..6494b36 100644
--- a/lib/textui/text_test.go
+++ b/lib/textui/text_test.go
@@ -1,4 +1,4 @@
-// Copyright (C) 2022-2023 Luke Shumaker <lukeshu@lukeshu.com>
+// Copyright (C) 2022-2024 Luke Shumaker <lukeshu@lukeshu.com>
//
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -6,6 +6,8 @@ package textui_test
import (
"fmt"
+ "math"
+ "math/big"
"strings"
"testing"
@@ -40,3 +42,137 @@ func TestPortion(t *testing.T) {
assert.Equal(t, "100% (0/0)", fmt.Sprint(textui.Portion[btrfsvol.PhysicalAddr]{}))
assert.Equal(t, "0% (1/12,345)", fmt.Sprint(textui.Portion[btrfsvol.PhysicalAddr]{N: 1, D: 12345}))
}
+
+func TestMetric(t *testing.T) {
+ t.Parallel()
+
+ // _1e(n) returns `1e{n}`
+ _1e := func(n int64) *big.Int {
+ return new(big.Int).Exp(big.NewInt(10), big.NewInt(n), nil)
+ }
+
+ // _1e(n) returns `1e-{n}`
+ _1em := func(n int64) *big.Rat {
+ ret := new(big.Rat).SetInt(_1e(n))
+ ret.Inv(ret)
+ return ret
+ }
+
+ // I've flipped the "actual" end "expected" fields for these
+ // tests, so that it's more readable as a table.
+
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Int](_1e(33), "s")), "1,000Qs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Int](_1e(32), "s")), "100Qs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Int](_1e(31), "s")), "10Qs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Int](_1e(30), "s")), "1Qs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Int](_1e(29), "s")), "100Rs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Int](_1e(28), "s")), "10Rs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Int](_1e(27), "s")), "1Rs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Int](_1e(26), "s")), "100Ys")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Int](_1e(25), "s")), "10Ys")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Int](_1e(24), "s")), "1Ys")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Int](_1e(23), "s")), "100Zs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Int](_1e(22), "s")), "10Zs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Int](_1e(21), "s")), "1Zs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Int](_1e(20), "s")), "100Es")
+ assert.Equal(t, fmt.Sprint(textui.Metric[uint64](1e19, "s")), "10Es")
+ assert.Equal(t, fmt.Sprint(textui.Metric[uint64](1e18, "s")), "1Es")
+ assert.Equal(t, fmt.Sprint(textui.Metric[uint64](1e17, "s")), "100Ps")
+ assert.Equal(t, fmt.Sprint(textui.Metric[uint64](1e16, "s")), "10Ps")
+ assert.Equal(t, fmt.Sprint(textui.Metric[uint64](1e15, "s")), "1Ps")
+ assert.Equal(t, fmt.Sprint(textui.Metric[uint64](1e14, "s")), "100Ts")
+ assert.Equal(t, fmt.Sprint(textui.Metric[uint64](1e13, "s")), "10Ts")
+ assert.Equal(t, fmt.Sprint(textui.Metric[uint64](1e12, "s")), "1Ts")
+ assert.Equal(t, fmt.Sprint(textui.Metric[uint64](1e11, "s")), "100Gs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[uint64](1e10, "s")), "10Gs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[uint64](1e9, "s")), "1Gs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[uint64](1e8, "s")), "100Ms")
+ assert.Equal(t, fmt.Sprint(textui.Metric[uint64](1e7, "s")), "10Ms")
+ assert.Equal(t, fmt.Sprint(textui.Metric[uint64](1e6, "s")), "1Ms")
+ assert.Equal(t, fmt.Sprint(textui.Metric[uint64](1e5, "s")), "100ks")
+ assert.Equal(t, fmt.Sprint(textui.Metric[uint64](1e4, "s")), "10ks")
+ assert.Equal(t, fmt.Sprint(textui.Metric[uint64](1e3, "s")), "1ks")
+ assert.Equal(t, fmt.Sprint(textui.Metric[uint64](1e2, "s")), "100s")
+ assert.Equal(t, fmt.Sprint(textui.Metric[uint64](1e1, "s")), "10s")
+
+ assert.Equal(t, fmt.Sprint(textui.Metric(1e0, "s")), "1s")
+
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(1), "s")), "100ms")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(2), "s")), "10ms")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(3), "s")), "1ms")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(4), "s")), "100μs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(5), "s")), "10μs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(6), "s")), "1μs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(7), "s")), "100ns")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(8), "s")), "10ns")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(9), "s")), "1ns")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(10), "s")), "100ps")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(11), "s")), "10ps")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(12), "s")), "1ps")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(13), "s")), "100fs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(14), "s")), "10fs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(15), "s")), "1fs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(16), "s")), "100as")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(17), "s")), "10as")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(18), "s")), "1as")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(19), "s")), "100zs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(20), "s")), "10zs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(21), "s")), "1zs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(22), "s")), "100ys")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(23), "s")), "10ys")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(24), "s")), "1ys")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(25), "s")), "100rs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(26), "s")), "10rs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(27), "s")), "1rs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(28), "s")), "100qs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(29), "s")), "10qs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(30), "s")), "1qs")
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(31), "s")), "0.1qs")
+
+ assert.Equal(t, fmt.Sprint(textui.Metric[*big.Rat](_1em(31), "s")), "0.1qs")
+
+ assert.Equal(t, fmt.Sprint(textui.Metric(math.NaN(), "s")), "NaNs")
+ assert.Equal(t, fmt.Sprintf("%5.f", textui.Metric(1, "s")), " 1s")
+ assert.Equal(t, fmt.Sprintf("%5.f", textui.Metric(1000, "s")), " 1ks")
+ assert.Equal(t, fmt.Sprintf("%5.f", textui.Metric(_1em(6), "s")), " 1μs")
+
+}
+
+func TestIEC(t *testing.T) {
+ t.Parallel()
+
+ // _1ll(n) returns `1<<{n}`
+ _1ll := func(n int) *big.Int {
+ bs := make([]byte, 1+(n/8)) // = ⌈(n+1)/8⌉ = ((n+1)+(8-1))/8 = (n+8)/8 = 1+(n/8)
+ bs[0] = 1 << (n % 8)
+ return new(big.Int).SetBytes(bs)
+ }
+
+ // I've flipped the "actual" end "expected" fields for these
+ // tests, so that it's more readable as a table.
+
+ assert.Equal(t, fmt.Sprint(textui.IEC[*big.Int](_1ll(90), "B")), "1,024YiB")
+ assert.Equal(t, fmt.Sprint(textui.IEC[*big.Int](_1ll(85), "B")), "32YiB")
+ assert.Equal(t, fmt.Sprint(textui.IEC[*big.Int](_1ll(80), "B")), "1YiB")
+ assert.Equal(t, fmt.Sprint(textui.IEC[*big.Int](_1ll(75), "B")), "32ZiB")
+ assert.Equal(t, fmt.Sprint(textui.IEC[*big.Int](_1ll(70), "B")), "1ZiB")
+ assert.Equal(t, fmt.Sprint(textui.IEC[*big.Int](_1ll(65), "B")), "32EiB")
+ assert.Equal(t, fmt.Sprint(textui.IEC[uint64](1<<60, "B")), "1EiB")
+ assert.Equal(t, fmt.Sprint(textui.IEC[uint64](1<<55, "B")), "32PiB")
+ assert.Equal(t, fmt.Sprint(textui.IEC[uint64](1<<50, "B")), "1PiB")
+ assert.Equal(t, fmt.Sprint(textui.IEC[uint64](1<<45, "B")), "32TiB")
+ assert.Equal(t, fmt.Sprint(textui.IEC[uint64](1<<40, "B")), "1TiB")
+ assert.Equal(t, fmt.Sprint(textui.IEC[uint64](1<<35, "B")), "32GiB")
+ assert.Equal(t, fmt.Sprint(textui.IEC[uint64](1<<30, "B")), "1GiB")
+ assert.Equal(t, fmt.Sprint(textui.IEC[uint64](1<<25, "B")), "32MiB")
+ assert.Equal(t, fmt.Sprint(textui.IEC[uint64](1<<20, "B")), "1MiB")
+ assert.Equal(t, fmt.Sprint(textui.IEC[uint64](1<<15, "B")), "32KiB")
+ assert.Equal(t, fmt.Sprint(textui.IEC[uint64](1<<10, "B")), "1KiB")
+ assert.Equal(t, fmt.Sprint(textui.IEC[uint64](1<<5, "B")), "32B")
+
+ assert.Equal(t, fmt.Sprint(textui.IEC(1<<0, "B")), "1B")
+
+ assert.Equal(t, fmt.Sprint(textui.IEC(math.NaN(), "B")), "NaNB")
+ assert.Equal(t, fmt.Sprintf("%5.f", textui.IEC(1, "B")), " 1B")
+ assert.Equal(t, fmt.Sprintf("%5.f", textui.IEC(1024, "B")), " 1KiB")
+}