From 74474cd9efc833007397447a12d93c32febb17be Mon Sep 17 00:00:00 2001 From: dudaodong Date: Fri, 7 Apr 2023 14:18:28 +0800 Subject: [PATCH] feat: add feature for humanize byte unit --- formatter/byte.go | 178 +++++++++++++++++++++++++++++++++++++++++ formatter/byte_test.go | 87 ++++++++++++++++++++ 2 files changed, 265 insertions(+) create mode 100644 formatter/byte.go create mode 100644 formatter/byte_test.go diff --git a/formatter/byte.go b/formatter/byte.go new file mode 100644 index 0000000..a90c00b --- /dev/null +++ b/formatter/byte.go @@ -0,0 +1,178 @@ +package formatter + +import ( + "fmt" + "math" + "strconv" + "strings" + "unicode" +) + +// +// code logic come from: +// https://github.com/docker/go-units/blob/master/size.go + +// http://en.wikipedia.org/wiki/Binary_prefix +const ( + // Decimal + UnitB = 1 + UnitKB = 1000 + UnitMB = 1000 * UnitKB + UnitGB = 1000 * UnitMB + UnitTB = 1000 * UnitGB + UnitPB = 1000 * UnitTB + UnitEB = 1000 * UnitPB + + // Binary + UnitBiB = 1 + UnitKiB = 1024 + UnitMiB = 1024 * UnitKiB + UnitGiB = 1024 * UnitMiB + UnitTiB = 1024 * UnitGiB + UnitPiB = 1024 * UnitTiB + UnitEiB = 1024 * UnitPiB +) + +// type byteUnitMap map[byte]int64 + +var ( + decimalByteMap = map[string]uint64{ + "b": UnitB, + "kb": UnitKB, + "mb": UnitMB, + "gb": UnitGB, + "tb": UnitTB, + "pb": UnitPB, + "eb": UnitEB, + + // Without suffix + "": UnitB, + "k": UnitKB, + "m": UnitMB, + "g": UnitGB, + "t": UnitTB, + "p": UnitPB, + "e": UnitEB, + } + + binaryByteMap = map[string]uint64{ + "bi": UnitBiB, + "kib": UnitKiB, + "mib": UnitMiB, + "gib": UnitGiB, + "tib": UnitTiB, + "pib": UnitPiB, + "eib": UnitEiB, + + // Without suffix + "": UnitBiB, + "ki": UnitKiB, + "mi": UnitMiB, + "gi": UnitGiB, + "ti": UnitTiB, + "pi": UnitPiB, + "ei": UnitEiB, + } +) + +var ( + decimalByteUnits = []string{"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"} + binaryByteUnits = []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"} +) + +// DecimalBytes returns a human readable byte size under decimal standard (base 1000) +// The precision parameter specifies the number of digits after the decimal point, which defaults to 4. +// Play: todo +func DecimalBytes(size float64, precision ...int) string { + p := 5 + if len(precision) > 0 { + p = precision[0] + 1 + } + size, unit := calculateByteSize(size, 1000.0, decimalByteUnits) + + return fmt.Sprintf("%.*g%s", p, size, unit) +} + +// BinaryBytes returns a human-readable byte size under decimal standard (base 1024) +// The precision parameter specifies the number of digits after the decimal point, which defaults to 4. +// Play: todo +func BinaryBytes(size float64, precision ...int) string { + p := 5 + if len(precision) > 0 { + p = precision[0] + 1 + } + size, unit := calculateByteSize(size, 1024.0, binaryByteUnits) + + return fmt.Sprintf("%.*g%s", p, size, unit) +} + +func calculateByteSize(size float64, base float64, byteUnits []string) (float64, string) { + i := 0 + unitsLimit := len(byteUnits) - 1 + for size >= base && i < unitsLimit { + size = size / base + i++ + } + return size, byteUnits[i] +} + +// ParseDecimalBytes the human readable bytes size string into the amount it represents(base 1000). +// ParseDecimalBytes("42 MB") -> 42000000, nil +// Play: todo +func ParseDecimalBytes(size string) (uint64, error) { + return parseBytes(size, "decimal") +} + +// ParseBinaryBytes the human readable bytes size string into the amount it represents(base 1024). +// ParseBinaryBytes("42 mib") -> 44040192, nil +// Play: todo +func ParseBinaryBytes(size string) (uint64, error) { + return parseBytes(size, "binary") +} + +// see https://github.com/dustin/go-humanize/blob/master/bytes.go +func parseBytes(s string, kind string) (uint64, error) { + lastDigit := 0 + hasComma := false + for _, r := range s { + if !(unicode.IsDigit(r) || r == '.' || r == ',') { + break + } + if r == ',' { + hasComma = true + } + lastDigit++ + } + + num := s[:lastDigit] + if hasComma { + num = strings.Replace(num, ",", "", -1) + } + + f, err := strconv.ParseFloat(num, 64) + if err != nil { + return 0, err + } + + extra := strings.ToLower(strings.TrimSpace(s[lastDigit:])) + + if kind == "decimal" { + if m, ok := decimalByteMap[extra]; ok { + f *= float64(m) + if f >= math.MaxUint64 { + return 0, fmt.Errorf("too large: %v", s) + } + return uint64(f), nil + } + } else { + if m, ok := binaryByteMap[extra]; ok { + f *= float64(m) + if f >= math.MaxUint64 { + return 0, fmt.Errorf("too large: %v", s) + } + return uint64(f), nil + } + } + + return 0, fmt.Errorf("unhandled size name: %v", extra) +} diff --git a/formatter/byte_test.go b/formatter/byte_test.go new file mode 100644 index 0000000..b4eac59 --- /dev/null +++ b/formatter/byte_test.go @@ -0,0 +1,87 @@ +package formatter + +import ( + "testing" + + "github.com/duke-git/lancet/v2/internal" +) + +func TestDecimalBytes(t *testing.T) { + assert := internal.NewAssert(t, "TestDecimalBytes") + + assert.Equal("1KB", DecimalBytes(1000)) + assert.Equal("1.024KB", DecimalBytes(1024)) + assert.Equal("1.2346MB", DecimalBytes(1234567)) + assert.Equal("1.235MB", DecimalBytes(1234567, 3)) + assert.Equal("1.123GB", DecimalBytes(float64(1.123*UnitGB))) + assert.Equal("2.123TB", DecimalBytes(float64(2.123*UnitTB))) + assert.Equal("3.123PB", DecimalBytes(float64(3.123*UnitPB))) + assert.Equal("4.123EB", DecimalBytes(float64(4.123*UnitEB))) + assert.Equal("1EB", DecimalBytes(float64(1000*UnitPB))) +} + +func TestBinaryBytes(t *testing.T) { + assert := internal.NewAssert(t, "TestBinaryBytes") + + assert.Equal("1KiB", BinaryBytes(1024)) + assert.Equal("1MiB", BinaryBytes(1024*1024)) + assert.Equal("1.1774MiB", BinaryBytes(1234567)) + assert.Equal("1.18MiB", BinaryBytes(1234567, 2)) +} + +func TestParseDecimalBytes(t *testing.T) { + assert := internal.NewAssert(t, "TestParseDecimalBytes") + + cases := map[string]uint64{ + "12": uint64(12), + "12 k": uint64(12000), + "12 kb": uint64(12000), + "12kb": uint64(12000), + "12k": uint64(12000), + "12K": uint64(12000), + "12KB": uint64(12000), + "12 K": uint64(12000), + "12 KB": uint64(12000), + "12 Kb": uint64(12000), + "12 kB": uint64(12000), + "12.2 KB": uint64(12200), + } + + for k, v := range cases { + result, err := ParseDecimalBytes(k) + assert.Equal(v, result) + assert.IsNil(err) + } + + _, err := ParseDecimalBytes("12 AB") + assert.IsNotNil(err) +} + +func TestParseBinaryBytes(t *testing.T) { + assert := internal.NewAssert(t, "TestParseBinaryBytes") + + cases := map[string]uint64{ + "12": uint64(12), + "12 ki": uint64(12288), + "12 kib": uint64(12288), + "12kib": uint64(12288), + "12ki": uint64(12288), + "12KI": uint64(12288), + "12KIB": uint64(12288), + "12KiB": uint64(12288), + "12 Ki": uint64(12288), + "12 KiB": uint64(12288), + "12 Kib": uint64(12288), + "12 kiB": uint64(12288), + "12.2 KiB": uint64(12492), + } + + for k, v := range cases { + result, err := ParseBinaryBytes(k) + assert.Equal(v, result) + assert.IsNil(err) + } + + _, err := ParseDecimalBytes("12 AB") + assert.IsNotNil(err) +}