From 8d9d4c98439bdfbfccaba28357944b6d866867bf Mon Sep 17 00:00:00 2001 From: "Luke T. Shumaker" Date: Mon, 19 Feb 2024 14:46:33 -0700 Subject: textui: Fix Metric() and IEC(), add tests, accept math/big values --- lib/fmtutil/fmt.go | 24 +++++- lib/textui/text.go | 203 ++++++++++++++++++++++++++++++++++++++---------- lib/textui/text_test.go | 138 +++++++++++++++++++++++++++++++- 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 +// Copyright (C) 2022-2024 Luke Shumaker // // 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 +// Copyright (C) 2022-2024 Luke Shumaker // // 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 +// Copyright (C) 2022-2024 Luke Shumaker // // 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") +} -- cgit v1.2.3