Skip to content

Commit

Permalink
Refactor byte sizes and byte rates
Browse files Browse the repository at this point in the history
  • Loading branch information
kenshaw committed Jan 10, 2025
1 parent 6c6bbce commit 3e67344
Show file tree
Hide file tree
Showing 9 changed files with 391 additions and 379 deletions.
36 changes: 14 additions & 22 deletions conv.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,9 @@ func asString[T stringi](val any) (T, error) {
}
return T(v.String()), nil
case Size:
return T(v.String()), nil
return T(FormatSize(int64(v), 'f', -2, true)), nil
case Rate:
return T(v.String()), nil
return T(FormatRate(v, 'f', -2, true)), nil
case interface{ String() string }:
return T(v.String()), nil
case interface{ Bytes() []byte }:
Expand Down Expand Up @@ -486,26 +486,22 @@ func asSize(val any) (Size, error) {
return v.Size(), nil
}
if v, err := asInt[int64](val); err == nil {
return Size{v * B, DefaultPrec, DefaultIEC}, nil
return Size(v), nil
}
if v, err := asUint[uint64](val); err == nil {
return Size{int64(v * uint64(B)), DefaultPrec, DefaultIEC}, nil
return Size(v), nil
}
if v, err := asFloat[float64](val); err == nil {
return Size{int64(v * float64(B)), DefaultPrec, DefaultIEC}, nil
return Size(v), nil
}
s, err := asString[string](val)
switch {
case err != nil:
return Size{0, DefaultPrec, DefaultIEC}, err
return 0, err
case s == "":
return Size{0, DefaultPrec, DefaultIEC}, nil
}
size, prec, iec, err := ParseSize(s)
if err != nil {
return Size{0, DefaultPrec, DefaultIEC}, err
return 0, nil
}
return Size{size, prec, iec}, nil
return ParseSize(s)
}

// asRate converts the value to a [Rate].
Expand All @@ -517,26 +513,22 @@ func asRate(val any) (Rate, error) {
return v.Rate(), nil
}
if v, err := asInt[int64](val); err == nil {
return Rate{v * B, DefaultPrec, DefaultIEC, DefaultUnit}, nil
return Rate{Size(v), DefaultRateUnit}, nil
}
if v, err := asUint[uint64](val); err == nil {
return Rate{int64(v * uint64(B)), DefaultPrec, DefaultIEC, DefaultUnit}, nil
return Rate{Size(v), DefaultRateUnit}, nil
}
if v, err := asFloat[float64](val); err == nil {
return Rate{int64(v * float64(B)), DefaultPrec, DefaultIEC, DefaultUnit}, nil
return Rate{Size(v), DefaultRateUnit}, nil
}
s, err := asString[string](val)
switch {
case err != nil:
return Rate{0, DefaultPrec, DefaultIEC, DefaultUnit}, err
return Rate{}, err
case s == "":
return Rate{0, DefaultPrec, DefaultIEC, DefaultUnit}, nil
}
rate, prec, iec, unit, err := ParseRate(s)
if err != nil {
return Rate{0, DefaultPrec, DefaultIEC, DefaultUnit}, err
return Rate{}, nil
}
return Rate{rate, prec, iec, unit}, nil
return ParseRate(s)
}

// asUnmarshal creates a new value as T, and unmarshals the value to it.
Expand Down
178 changes: 4 additions & 174 deletions ox.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,12 +219,10 @@ var (
}
return v
}
// DefaultPrec is the default [Size]/[Rate] display precision.
DefaultPrec = -2
// DefaultIEC is the default [Size]/[Rate] IEC display setting.
DefaultIEC = true
// DefaultUnit is the default [Rate] unit.
DefaultUnit = time.Second
// DefaultSizePrec is the default [Size] display precision.
DefaultSizePrec = -2
// DefaultRateUnit is the default [Rate] unit.
DefaultRateUnit = time.Second
)

// Run creates and builds the execution [Context] based on the passed
Expand Down Expand Up @@ -709,174 +707,6 @@ func BuildVersion() string {
return ver
}

// ParseRate parses a byte rate string.
func ParseRate(s string) (int64, int, bool, time.Duration, error) {
unit := time.Second
if i := strings.LastIndexByte(s, '/'); i != -1 {
switch s[i+1:] {
// unitMap is the duration unit map.
case "ns":
unit = time.Nanosecond
case "us", "µs", "μs": // U+00B5 = micro symbol, U+03BC = Greek letter mu
unit = time.Microsecond
case "ms":
unit = time.Millisecond
case "s":
unit = time.Second
case "m":
unit = time.Minute
case "h":
unit = time.Hour
default:
return 0, 0, false, 0, fmt.Errorf("%w %q", ErrInvalidRate, s)
}
s = s[:i]
}
sz, prec, iec, err := ParseSize(s)
if err != nil {
return 0, 0, false, 0, err
}
return sz, prec, iec, unit, nil
}

// FormatSize formats a byte rate.
func FormatRate(size int64, prec int, iec bool, unit time.Duration) string {
return FormatSize(size, prec, iec) + "/" + unitString(unit)
}

// ParseSize parses a byte size string.
func ParseSize(s string) (int64, int, bool, error) {
m := sizeRE.FindStringSubmatch(s)
if m == nil {
return 0, 0, false, fmt.Errorf("%w %q", ErrInvalidSize, s)
}
f, err := strconv.ParseFloat(m[2], 64)
switch {
case err != nil:
return 0, 0, false, fmt.Errorf("%w: %w", ErrInvalidSize, err)
case m[1] == "-":
f = -f
}
sz, iec, err := sizeType(m[3])
if err != nil {
return 0, 0, false, fmt.Errorf("%w: %w", ErrInvalidSize, err)
}
prec := DefaultPrec
if i := strings.LastIndexByte(m[2], '.'); i != -1 {
prec = len(m[2]) - i - 1
}
return int64(f * float64(sz)), prec, iec, nil
}

// FormatSize formats a byte size.
//
// Formatting rules follow [strconv.FormatFloat] rules, with additional option
// for a precision of -2, which will remove trailing zeroes and the decimal if
// the decimal places are all zeroes.
func FormatSize(size int64, prec int, iec bool) string {
n, t, suffix := KB, "kMGTPE", "B"
if iec {
n, t, suffix = KiB, "KMGTPE", "iB"
}
var neg string
if size < 0 {
neg, size = "-", -size
}
if size < n {
return neg + strconv.FormatInt(size, 10) + " B"
}
e, d := 0, n
for i := size / n; n <= i; i /= n {
d *= n
e++
}
precabs := prec
if precabs < -1 {
precabs = -precabs
}
s := strconv.FormatFloat(float64(size)/float64(d), 'f', precabs, 64)
if prec < -1 {
s = strings.TrimRight(s, ".0")
}
return neg + s + " " + string(t[e]) + suffix
}

// sizeType returns the size of s.
func sizeType(s string) (int64, bool, error) {
switch strings.ToLower(s) {
case "", "b":
return B, false, nil
case "kb":
return KB, false, nil
case "mb":
return MB, false, nil
case "gb":
return GB, false, nil
case "tb":
return TB, false, nil
case "pb":
return PB, false, nil
case "eb":
return EB, false, nil
case "kib":
return KiB, true, nil
case "mib":
return MiB, true, nil
case "gib":
return GiB, true, nil
case "tib":
return TiB, true, nil
case "pib":
return PiB, true, nil
case "eib":
return EiB, true, nil
}
return 0, false, fmt.Errorf("%w %q", ErrUnknownSize, s)
}

// Byte sizes.
const (
B int64 = 1
KB int64 = 1_000
MB int64 = 1_000_000
GB int64 = 1_000_000_000
TB int64 = 1_000_000_000_000
PB int64 = 1_000_000_000_000_000
EB int64 = 1_000_000_000_000_000_000
)

// IEC byte sizes.
const (
KiB int64 = 1_024
MiB int64 = 1_048_576
GiB int64 = 1_073_741_824
TiB int64 = 1_099_511_627_776
PiB int64 = 1_125_899_906_842_624
EiB int64 = 1_152_921_504_606_846_976
)

// unitString returns the string for a time unit (duration).
func unitString(unit time.Duration) string {
switch {
case unit == 0, unit > time.Hour:
return unitString(DefaultUnit)
case unit > time.Minute:
return "h"
case unit > time.Second:
return "m"
case unit > time.Millisecond:
return "s"
case unit > time.Microsecond:
return "ms"
case unit > time.Nanosecond:
return "µs"
}
return "ns"
}

// sizeRE matches sizes.
var sizeRE = regexp.MustCompile(`(?i)^([-+])?([0-9]+(?:\.[0-9]*)?)(?: ?([a-z]+))?$`)

// prepend is a generic prepend.
func prepend[S ~[]E, E any](v S, s ...E) S {
return append(s, v...)
Expand Down
82 changes: 0 additions & 82 deletions ox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"strconv"
"strings"
"testing"
"time"
)

func TestSuggestions(t *testing.T) {
Expand Down Expand Up @@ -112,87 +111,6 @@ func TestLdist(t *testing.T) {
}
}

func TestFormatSize(t *testing.T) {
tests := []struct {
size int64
prec int
iec bool
exp string
}{
{0, DefaultPrec, false, "0 B"},
{0, DefaultPrec, true, "0 B"},
{int64(0.754 * float64(MB)), DefaultPrec, false, "754 kB"},
{int64(1.5 * float64(GB)), 2, false, "1.50 GB"},
{int64(1.5 * float64(GB)), 1, false, "1.5 GB"},
{int64(1.54 * float64(GB)), 2, false, "1.54 GB"},
{int64(1.5 * float64(GiB)), 2, true, "1.50 GiB"},
{int64(1.5 * float64(PiB)), 2, true, "1.50 PiB"},
}
for i, test := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
s := FormatSize(test.size, test.prec, test.iec)
if s != test.exp {
t.Errorf("expected %q, got: %q", test.exp, s)
}
size, prec, iec, err := ParseSize(test.exp)
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
if size != test.size {
t.Errorf("expected size %d, got: %d", test.size, size)
}
if prec != test.prec {
t.Errorf("expected prec %d, got: %d", test.prec, prec)
}
if test.size != 0 && iec != test.iec {
t.Errorf("expected iec %t, got: %t", test.iec, iec)
}
})
}
}

func TestFormatRate(t *testing.T) {
tests := []struct {
rate int64
prec int
iec bool
unit time.Duration
exp string
}{
{0, DefaultPrec, false, time.Second, "0 B/s"},
{0, DefaultPrec, true, time.Second, "0 B/s"},
{int64(0.754 * float64(MB)), DefaultPrec, false, time.Hour, "754 kB/h"},
{int64(1.5 * float64(GB)), 1, false, time.Microsecond, "1.5 GB/µs"},
{int64(1.54 * float64(GB)), 2, false, time.Microsecond, "1.54 GB/µs"},
{int64(1.5 * float64(GiB)), 2, true, time.Millisecond, "1.50 GiB/ms"},
{int64(1.5 * float64(PiB)), 2, true, time.Second, "1.50 PiB/s"},
}
for i, test := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
s := FormatRate(test.rate, test.prec, test.iec, test.unit)
if s != test.exp {
t.Errorf("expected %q, got: %q", test.exp, s)
}
rate, prec, iec, unit, err := ParseRate(test.exp)
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
if rate != test.rate {
t.Errorf("expected rate %d, got: %d", test.rate, rate)
}
if prec != test.prec {
t.Errorf("expected prec %d, got: %d", test.prec, prec)
}
if test.rate != 0 && iec != test.iec {
t.Errorf("expected iec %t, got: %t", test.iec, iec)
}
if unit != test.unit {
t.Errorf("expected unit %v, got: %v", test.unit, unit)
}
})
}
}

func testContext(t *testing.T, code *int, args ...string) *Context {
t.Helper()
cmd := testCommand(t)
Expand Down
2 changes: 1 addition & 1 deletion parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ func parseTests() []parseTest {
"name: six",
"path: [six]",
"args: []",
"vars: [duration:15µs int:15 rate:186 MB/s size:15 MiB]",
"vars: [duration:15µs int:15 rate:177.38 MiB/s size:15 MiB]",
},
},
}
Expand Down
Loading

0 comments on commit 3e67344

Please sign in to comment.