diff --git a/humanbytes.go b/humanbytes.go new file mode 100644 index 0000000..8d8e7f0 --- /dev/null +++ b/humanbytes.go @@ -0,0 +1,38 @@ +package util + +import ( + "fmt" + "math" +) + +const bytesPerUnit = 1024 + +var byteUnits = []string{"KiB", "MiB", "GiB", "TiB", "PiB", "EiB"} + +// HumanBytes writes a byte count the way a person would read it, using powers +// of 1024: "0 B", "1023 B", "1.0 KiB", "1.5 MiB", "16.0 EiB". Counts below 1024 +// are given as whole bytes, and anything larger gets one digit after the +// decimal point. +func HumanBytes(bytes uint64) string { + if bytes < bytesPerUnit { + return fmt.Sprintf("%d B", bytes) + } + + value := float64(bytes) + unit := -1 + + for value >= bytesPerUnit && unit < len(byteUnits)-1 { + value /= bytesPerUnit + unit++ + } + + // Rounding happens after the unit has been chosen, so a count just short + // of the next threshold would otherwise come out as "1024.0 KiB" rather + // than "1.0 MiB". + if unit < len(byteUnits)-1 && math.Round(value*10)/10 >= bytesPerUnit { + value /= bytesPerUnit + unit++ + } + + return fmt.Sprintf("%.1f %s", value, byteUnits[unit]) +} diff --git a/humanbytes_test.go b/humanbytes_test.go new file mode 100644 index 0000000..9d92d75 --- /dev/null +++ b/humanbytes_test.go @@ -0,0 +1,33 @@ +package util + +import "testing" + +func TestHumanBytes(t *testing.T) { + tests := []struct { + name string + input uint64 + expected string + }{ + {"zero", 0, "0 B"}, + {"one byte", 1, "1 B"}, + {"just below a kibibyte", 1023, "1023 B"}, + {"exactly a kibibyte", 1024, "1.0 KiB"}, + {"half a kibibyte more", 1536, "1.5 KiB"}, + {"rounds up into the next unit", 1048575, "1.0 MiB"}, + {"exactly a mebibyte", 1 << 20, "1.0 MiB"}, + {"a gibibyte", 1 << 30, "1.0 GiB"}, + {"a tebibyte", 1 << 40, "1.0 TiB"}, + {"a pebibyte", 1 << 50, "1.0 PiB"}, + {"an exbibyte", 1 << 60, "1.0 EiB"}, + {"the largest count there is", ^uint64(0), "16.0 EiB"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := HumanBytes(test.input) + if got != test.expected { + t.Errorf("expected %q got %q", test.expected, got) + } + }) + } +}