diff --git a/bytes/bytes.go b/bytes/bytes.go index 2f6bcec..b07e31c 100644 --- a/bytes/bytes.go +++ b/bytes/bytes.go @@ -12,19 +12,31 @@ type ( Bytes struct{} ) +// binary units (IEC 60027) const ( _ = 1.0 << (10 * iota) // ignore first value by assigning to blank identifier - KB - MB - GB - TB - PB - EB + KiB + MiB + GiB + TiB + PiB + EiB +) + +// decimal units (SI international system of units) +const ( + KB = 1000 + MB = KB * 1000 + GB = MB * 1000 + TB = GB * 1000 + PB = TB * 1000 + EB = PB * 1000 ) var ( - pattern = regexp.MustCompile(`(?i)^(-?\d+(?:\.\d+)?)\s?([KMGTPE]B?|B?)$`) - global = New() + patternBinary = regexp.MustCompile(`(?i)^(-?\d+(?:\.\d+)?)\s?([KMGTPE]iB?)$`) + patternDecimal = regexp.MustCompile(`(?i)^(-?\d+(?:\.\d+)?)\s?([KMGTPE]B?|B?)$`) + global = New() ) // New creates a Bytes instance. @@ -32,44 +44,97 @@ func New() *Bytes { return &Bytes{} } -// Format formats bytes integer to human readable string. +// Format formats bytes integer to human readable string according to IEC 60027. +// For example, 31323 bytes will return 30.59KB. +func (b *Bytes) Format(value int64) string { + return b.FormatBinary(value) +} + +// FormatBinary formats bytes integer to human readable string according to IEC 60027. // For example, 31323 bytes will return 30.59KB. -func (*Bytes) Format(b int64) string { +func (*Bytes) FormatBinary(value int64) string { + multiple := "" + val := float64(value) + + switch { + case value >= EiB: + val /= EiB + multiple = "EiB" + case value >= PiB: + val /= PiB + multiple = "PiB" + case value >= TiB: + val /= TiB + multiple = "TiB" + case value >= GiB: + val /= GiB + multiple = "GiB" + case value >= MiB: + val /= MiB + multiple = "MiB" + case value >= KiB: + val /= KiB + multiple = "KiB" + case value == 0: + return "0" + default: + return strconv.FormatInt(value, 10) + "B" + } + + return fmt.Sprintf("%.2f%s", val, multiple) +} + +// FormatDecimal formats bytes integer to human readable string according to SI international system of units. +// For example, 31323 bytes will return 31.32KB. +func (*Bytes) FormatDecimal(value int64) string { multiple := "" - value := float64(b) + val := float64(value) switch { - case b >= EB: - value /= EB + case value >= EB: + val /= EB multiple = "EB" - case b >= PB: - value /= PB + case value >= PB: + val /= PB multiple = "PB" - case b >= TB: - value /= TB + case value >= TB: + val /= TB multiple = "TB" - case b >= GB: - value /= GB + case value >= GB: + val /= GB multiple = "GB" - case b >= MB: - value /= MB + case value >= MB: + val /= MB multiple = "MB" - case b >= KB: - value /= KB + case value >= KB: + val /= KB multiple = "KB" - case b == 0: + case value == 0: return "0" default: - return strconv.FormatInt(b, 10) + "B" + return strconv.FormatInt(value, 10) + "B" } - return fmt.Sprintf("%.2f%s", value, multiple) + return fmt.Sprintf("%.2f%s", val, multiple) } // Parse parses human readable bytes string to bytes integer. -// For example, 6GB (6G is also valid) will return 6442450944. -func (*Bytes) Parse(value string) (i int64, err error) { - parts := pattern.FindStringSubmatch(value) +// For example, 6GiB (6Gi is also valid) will return 6442450944, and +// 6GB (6G is also valid) will return 6000000000. +func (b *Bytes) Parse(value string) (int64, error) { + + i, err := b.ParseBinary(value) + if err == nil { + return i, err + } + + return b.ParseDecimal(value) +} + +// ParseBinary parses human readable bytes string to bytes integer. +// For example, 6GiB (6Gi is also valid) will return 6442450944. +func (*Bytes) ParseBinary(value string) (i int64, err error) { + parts := patternBinary.FindStringSubmatch(value) if len(parts) < 3 { return 0, fmt.Errorf("error parsing value=%s", value) } @@ -81,8 +146,38 @@ func (*Bytes) Parse(value string) (i int64, err error) { } switch multiple { + case "KI", "KIB": + return int64(bytes * KiB), nil + case "MI", "MIB": + return int64(bytes * MiB), nil + case "GI", "GIB": + return int64(bytes * GiB), nil + case "TI", "TIB": + return int64(bytes * TiB), nil + case "PI", "PIB": + return int64(bytes * PiB), nil + case "EI", "EIB": + return int64(bytes * EiB), nil default: return int64(bytes), nil + } +} + +// ParseDecimal parses human readable bytes string to bytes integer. +// For example, 6GB (6G is also valid) will return 6000000000. +func (*Bytes) ParseDecimal(value string) (i int64, err error) { + parts := patternDecimal.FindStringSubmatch(value) + if len(parts) < 3 { + return 0, fmt.Errorf("error parsing value=%s", value) + } + bytesString := parts[1] + multiple := strings.ToUpper(parts[2]) + bytes, err := strconv.ParseFloat(bytesString, 64) + if err != nil { + return + } + + switch multiple { case "K", "KB": return int64(bytes * KB), nil case "M", "MB": @@ -95,15 +190,27 @@ func (*Bytes) Parse(value string) (i int64, err error) { return int64(bytes * PB), nil case "E", "EB": return int64(bytes * EB), nil + default: + return int64(bytes), nil } } // Format wraps global Bytes's Format function. -func Format(b int64) string { - return global.Format(b) +func Format(value int64) string { + return global.Format(value) +} + +// FormatBinary wraps global Bytes's FormatBinary function. +func FormatBinary(value int64) string { + return global.FormatBinary(value) +} + +// FormatDecimal wraps global Bytes's FormatDecimal function. +func FormatDecimal(value int64) string { + return global.FormatDecimal(value) } // Parse wraps global Bytes's Parse function. -func Parse(val string) (int64, error) { - return global.Parse(val) +func Parse(value string) (int64, error) { + return global.Parse(value) } diff --git a/bytes/bytes_test.go b/bytes/bytes_test.go index 279275c..5549982 100644 --- a/bytes/bytes_test.go +++ b/bytes/bytes_test.go @@ -15,29 +15,53 @@ func TestBytesFormat(t *testing.T) { b = Format(515) assert.Equal(t, "515B", b) - // KB + // KiB b = Format(31323) - assert.Equal(t, "30.59KB", b) + assert.Equal(t, "30.59KiB", b) - // MB + // MiB b = Format(13231323) - assert.Equal(t, "12.62MB", b) + assert.Equal(t, "12.62MiB", b) - // GB + // GiB b = Format(7323232398) - assert.Equal(t, "6.82GB", b) + assert.Equal(t, "6.82GiB", b) - // TB + // TiB b = Format(7323232398434) - assert.Equal(t, "6.66TB", b) + assert.Equal(t, "6.66TiB", b) - // PB + // PiB b = Format(9923232398434432) - assert.Equal(t, "8.81PB", b) + assert.Equal(t, "8.81PiB", b) - // EB + // EiB b = Format(math.MaxInt64) - assert.Equal(t, "8.00EB", b) + assert.Equal(t, "8.00EiB", b) + + // KB + b = FormatDecimal(31323) + assert.Equal(t, "31.32KB", b) + + // MB + b = FormatDecimal(13231323) + assert.Equal(t, "13.23MB", b) + + // GB + b = FormatDecimal(7323232398) + assert.Equal(t, "7.32GB", b) + + // TB + b = FormatDecimal(7323232398434) + assert.Equal(t, "7.32TB", b) + + // PB + b = FormatDecimal(9923232398434432) + assert.Equal(t, "9.92PB", b) + + // EB + b = FormatDecimal(math.MaxInt64) + assert.Equal(t, "9.22EB", b) } func TestBytesParseErrors(t *testing.T) { @@ -49,7 +73,7 @@ func TestBytesParseErrors(t *testing.T) { func TestFloats(t *testing.T) { // From string: - str := "12.25KB" + str := "12.25KiB" value, err := Parse(str) assert.NoError(t, err) assert.Equal(t, int64(12544), value) @@ -60,11 +84,29 @@ func TestFloats(t *testing.T) { // To string: val := int64(13233029) str = Format(val) - assert.Equal(t, "12.62MB", str) + assert.Equal(t, "12.62MiB", str) val2, err := Parse(str) assert.NoError(t, err) assert.Equal(t, val, val2) + + // From string decimal: + strDec := "12.25KB" + valueDec, err := Parse(strDec) + assert.NoError(t, err) + assert.Equal(t, int64(12250), valueDec) + + strDec2 := FormatDecimal(valueDec) + assert.Equal(t, strDec, strDec2) + + // To string decimal: + valDec := int64(13230000) + strDec = FormatDecimal(valDec) + assert.Equal(t, "13.23MB", strDec) + + valDec2, err := Parse(strDec) + assert.NoError(t, err) + assert.Equal(t, valDec, valDec2) } func TestBytesParse(t *testing.T) { @@ -92,136 +134,267 @@ func TestBytesParse(t *testing.T) { assert.Equal(t, int64(515), b) } + // KiB + b, err = Parse("12.25KiB") + if assert.NoError(t, err) { + assert.Equal(t, int64(12544), b) + } + b, err = Parse("12KiB") + if assert.NoError(t, err) { + assert.Equal(t, int64(12288), b) + } + b, err = Parse("12Ki") + if assert.NoError(t, err) { + assert.Equal(t, int64(12288), b) + } + + // kib, lowercase multiple test + b, err = Parse("12.25kib") + if assert.NoError(t, err) { + assert.Equal(t, int64(12544), b) + } + b, err = Parse("12kib") + if assert.NoError(t, err) { + assert.Equal(t, int64(12288), b) + } + b, err = Parse("12ki") + if assert.NoError(t, err) { + assert.Equal(t, int64(12288), b) + } + + // KiB with space + b, err = Parse("12.25 KiB") + if assert.NoError(t, err) { + assert.Equal(t, int64(12544), b) + } + b, err = Parse("12 KiB") + if assert.NoError(t, err) { + assert.Equal(t, int64(12288), b) + } + b, err = Parse("12 Ki") + if assert.NoError(t, err) { + assert.Equal(t, int64(12288), b) + } + + // MiB + b, err = Parse("2MiB") + if assert.NoError(t, err) { + assert.Equal(t, int64(2097152), b) + } + b, err = Parse("2Mi") + if assert.NoError(t, err) { + assert.Equal(t, int64(2097152), b) + } + + // GiB with space + b, err = Parse("6 GiB") + if assert.NoError(t, err) { + assert.Equal(t, int64(6442450944), b) + } + b, err = Parse("6 Gi") + if assert.NoError(t, err) { + assert.Equal(t, int64(6442450944), b) + } + + // GiB + b, err = Parse("6GiB") + if assert.NoError(t, err) { + assert.Equal(t, int64(6442450944), b) + } + b, err = Parse("6Gi") + if assert.NoError(t, err) { + assert.Equal(t, int64(6442450944), b) + } + + // TiB + b, err = Parse("5TiB") + if assert.NoError(t, err) { + assert.Equal(t, int64(5497558138880), b) + } + b, err = Parse("5Ti") + if assert.NoError(t, err) { + assert.Equal(t, int64(5497558138880), b) + } + + // TiB with space + b, err = Parse("5 TiB") + if assert.NoError(t, err) { + assert.Equal(t, int64(5497558138880), b) + } + b, err = Parse("5 Ti") + if assert.NoError(t, err) { + assert.Equal(t, int64(5497558138880), b) + } + + // PiB + b, err = Parse("9PiB") + if assert.NoError(t, err) { + assert.Equal(t, int64(10133099161583616), b) + } + b, err = Parse("9Pi") + if assert.NoError(t, err) { + assert.Equal(t, int64(10133099161583616), b) + } + + // PiB with space + b, err = Parse("9 PiB") + if assert.NoError(t, err) { + assert.Equal(t, int64(10133099161583616), b) + } + b, err = Parse("9 Pi") + if assert.NoError(t, err) { + assert.Equal(t, int64(10133099161583616), b) + } + + // EiB + b, err = Parse("8EiB") + if assert.NoError(t, err) { + assert.True(t, math.MaxInt64 == b-1) + } + b, err = Parse("8Ei") + if assert.NoError(t, err) { + assert.True(t, math.MaxInt64 == b-1) + } + + // EiB with spaces + b, err = Parse("8 EiB") + if assert.NoError(t, err) { + assert.True(t, math.MaxInt64 == b-1) + } + b, err = Parse("8 Ei") + if assert.NoError(t, err) { + assert.True(t, math.MaxInt64 == b-1) + } + // KB b, err = Parse("12.25KB") if assert.NoError(t, err) { - assert.Equal(t, int64(12544), b) + assert.Equal(t, int64(12250), b) } b, err = Parse("12KB") if assert.NoError(t, err) { - assert.Equal(t, int64(12288), b) + assert.Equal(t, int64(12000), b) } b, err = Parse("12K") if assert.NoError(t, err) { - assert.Equal(t, int64(12288), b) + assert.Equal(t, int64(12000), b) } // kb, lowercase multiple test b, err = Parse("12.25kb") if assert.NoError(t, err) { - assert.Equal(t, int64(12544), b) + assert.Equal(t, int64(12250), b) } b, err = Parse("12kb") if assert.NoError(t, err) { - assert.Equal(t, int64(12288), b) + assert.Equal(t, int64(12000), b) } b, err = Parse("12k") if assert.NoError(t, err) { - assert.Equal(t, int64(12288), b) + assert.Equal(t, int64(12000), b) } - // KB with space b, err = Parse("12.25 KB") if assert.NoError(t, err) { - assert.Equal(t, int64(12544), b) + assert.Equal(t, int64(12250), b) } b, err = Parse("12 KB") if assert.NoError(t, err) { - assert.Equal(t, int64(12288), b) + assert.Equal(t, int64(12000), b) } b, err = Parse("12 K") if assert.NoError(t, err) { - assert.Equal(t, int64(12288), b) + assert.Equal(t, int64(12000), b) } // MB b, err = Parse("2MB") if assert.NoError(t, err) { - assert.Equal(t, int64(2097152), b) + assert.Equal(t, int64(2000000), b) } b, err = Parse("2M") if assert.NoError(t, err) { - assert.Equal(t, int64(2097152), b) + assert.Equal(t, int64(2000000), b) } // GB with space b, err = Parse("6 GB") if assert.NoError(t, err) { - assert.Equal(t, int64(6442450944), b) + assert.Equal(t, int64(6000000000), b) } b, err = Parse("6 G") if assert.NoError(t, err) { - assert.Equal(t, int64(6442450944), b) + assert.Equal(t, int64(6000000000), b) } // GB b, err = Parse("6GB") if assert.NoError(t, err) { - assert.Equal(t, int64(6442450944), b) + assert.Equal(t, int64(6000000000), b) } b, err = Parse("6G") if assert.NoError(t, err) { - assert.Equal(t, int64(6442450944), b) + assert.Equal(t, int64(6000000000), b) } // TB b, err = Parse("5TB") if assert.NoError(t, err) { - assert.Equal(t, int64(5497558138880), b) + assert.Equal(t, int64(5000000000000), b) } b, err = Parse("5T") if assert.NoError(t, err) { - assert.Equal(t, int64(5497558138880), b) + assert.Equal(t, int64(5000000000000), b) } // TB with space b, err = Parse("5 TB") if assert.NoError(t, err) { - assert.Equal(t, int64(5497558138880), b) + assert.Equal(t, int64(5000000000000), b) } b, err = Parse("5 T") if assert.NoError(t, err) { - assert.Equal(t, int64(5497558138880), b) + assert.Equal(t, int64(5000000000000), b) } // PB b, err = Parse("9PB") if assert.NoError(t, err) { - assert.Equal(t, int64(10133099161583616), b) + assert.Equal(t, int64(9000000000000000), b) } b, err = Parse("9P") if assert.NoError(t, err) { - assert.Equal(t, int64(10133099161583616), b) + assert.Equal(t, int64(9000000000000000), b) } // PB with space b, err = Parse("9 PB") if assert.NoError(t, err) { - assert.Equal(t, int64(10133099161583616), b) + assert.Equal(t, int64(9000000000000000), b) } b, err = Parse("9 P") if assert.NoError(t, err) { - assert.Equal(t, int64(10133099161583616), b) + assert.Equal(t, int64(9000000000000000), b) } // EB b, err = Parse("8EB") if assert.NoError(t, err) { - assert.True(t, math.MaxInt64 == b-1) + assert.Equal(t, int64(8000000000000000000), b) } b, err = Parse("8E") if assert.NoError(t, err) { - assert.True(t, math.MaxInt64 == b-1) + assert.Equal(t, int64(8000000000000000000), b) } // EB with spaces b, err = Parse("8 EB") if assert.NoError(t, err) { - assert.True(t, math.MaxInt64 == b-1) + assert.Equal(t, int64(8000000000000000000), b) } b, err = Parse("8 E") if assert.NoError(t, err) { - assert.True(t, math.MaxInt64 == b-1) + assert.Equal(t, int64(8000000000000000000), b) } }