diff --git a/units/duration_test.go b/units/duration_test.go index 4906f8a..a229474 100644 --- a/units/duration_test.go +++ b/units/duration_test.go @@ -44,9 +44,3 @@ func TestHumanDuration(t *testing.T) { assertEquals(t, "2.010959 years", HumanDuration(24*month+2*week)) assertEquals(t, "3.164384 years", HumanDuration(3*year+2*month)) } - -func assertEquals(t *testing.T, expected, actual interface{}) { - if expected != actual { - t.Errorf("Expected '%s' but got '%s'", expected, actual) - } -} diff --git a/units/size.go b/units/size.go index e6e9f21..88d91dd 100644 --- a/units/size.go +++ b/units/size.go @@ -7,54 +7,54 @@ import ( "strings" ) +// See: http://en.wikipedia.org/wiki/Binary_prefix +const ( + // Decimal + KB = 1000 + MB = 1000 * KB + GB = 1000 * MB + TB = 1000 * GB + PB = 1000 * TB + + // Binary + KiB = 1024 + MiB = 1024 * KiB + GiB = 1024 * MiB + TiB = 1024 * GiB + PiB = 1024 * TiB +) + +type unitMap map[string]int64 + +var ( + decimalMap = unitMap{"k": KB, "m": MB, "g": GB, "t": TB, "p": PB} + binaryMap = unitMap{"k": KiB, "m": MiB, "g": GiB, "t": TiB, "p": PiB} +) + +var sizeRegex *regexp.Regexp + +func init() { + sizeRegex = regexp.MustCompile("^(\\d+)([kKmMgGtTpP])?[bB]?$") +} + +var unitAbbrs = [...]string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"} + // HumanSize returns a human-readable approximation of a size // using SI standard (eg. "44kB", "17MB") func HumanSize(size int64) string { i := 0 - var sizef float64 - sizef = float64(size) - units := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"} + sizef := float64(size) for sizef >= 1000.0 { sizef = sizef / 1000.0 i++ } - return fmt.Sprintf("%.4g %s", sizef, units[i]) + return fmt.Sprintf("%.4g %s", sizef, unitAbbrs[i]) } -// FromHumanSize returns an integer from a human-readable specification of a size -// using SI standard (eg. "44kB", "17MB") +// FromHumanSize returns an integer from a human-readable specification of a +// size using SI standard (eg. "44kB", "17MB") func FromHumanSize(size string) (int64, error) { - re, err := regexp.Compile("^(\\d+)([kKmMgGtTpP])?[bB]?$") - if err != nil { - return -1, fmt.Errorf("%s does not specify not a size", size) - } - - matches := re.FindStringSubmatch(size) - - if len(matches) != 3 { - return -1, fmt.Errorf("Invalid size: '%s'", size) - } - - theSize, err := strconv.ParseInt(matches[1], 10, 0) - if err != nil { - return -1, err - } - - unit := strings.ToLower(matches[2]) - - if unit == "k" { - theSize *= 1000 - } else if unit == "m" { - theSize *= 1000 * 1000 - } else if unit == "g" { - theSize *= 1000 * 1000 * 1000 - } else if unit == "t" { - theSize *= 1000 * 1000 * 1000 * 1000 - } else if unit == "p" { - theSize *= 1000 * 1000 * 1000 * 1000 * 1000 - } - - return theSize, nil + return parseSize(size, decimalMap) } // Parses a human-readable string representing an amount of RAM @@ -62,33 +62,25 @@ func FromHumanSize(size string) (int64, error) { // returns the number of bytes, or -1 if the string is unparseable. // Units are case-insensitive, and the 'b' suffix is optional. func RAMInBytes(size string) (int64, error) { - re, err := regexp.Compile("^(\\d+)([kKmMgGtT])?[bB]?$") - if err != nil { - return -1, err - } - - matches := re.FindStringSubmatch(size) - - if len(matches) != 3 { - return -1, fmt.Errorf("Invalid size: '%s'", size) - } - - memLimit, err := strconv.ParseInt(matches[1], 10, 0) - if err != nil { - return -1, err - } - - unit := strings.ToLower(matches[2]) - - if unit == "k" { - memLimit *= 1024 - } else if unit == "m" { - memLimit *= 1024 * 1024 - } else if unit == "g" { - memLimit *= 1024 * 1024 * 1024 - } else if unit == "t" { - memLimit *= 1024 * 1024 * 1024 * 1024 - } - - return memLimit, nil + return parseSize(size, binaryMap) +} + +// Parses the human-readable size string into the amount it represents +func parseSize(sizeStr string, uMap unitMap) (int64, error) { + matches := sizeRegex.FindStringSubmatch(sizeStr) + if len(matches) != 3 { + return -1, fmt.Errorf("Invalid size: '%s'", sizeStr) + } + + size, err := strconv.ParseInt(matches[1], 10, 0) + if err != nil { + return -1, err + } + + unitPrefix := strings.ToLower(matches[2]) + if mul, ok := uMap[unitPrefix]; ok { + size *= mul + } + + return size, nil } diff --git a/units/size_test.go b/units/size_test.go index de8b44f..8dae7e7 100644 --- a/units/size_test.go +++ b/units/size_test.go @@ -1,6 +1,9 @@ package units import ( + "reflect" + "runtime" + "strings" "testing" ) @@ -9,81 +12,87 @@ func TestHumanSize(t *testing.T) { assertEquals(t, "1.024 kB", HumanSize(1024)) assertEquals(t, "1 MB", HumanSize(1000000)) assertEquals(t, "1.049 MB", HumanSize(1048576)) - assertEquals(t, "2 MB", HumanSize(2*1000*1000)) - assertEquals(t, "3.42 GB", HumanSize(3.42*1000*1000*1000)) - assertEquals(t, "5.372 TB", HumanSize(5.372*1000*1000*1000*1000)) - assertEquals(t, "2.22 PB", HumanSize(2.22*1000*1000*1000*1000*1000)) - assertEquals(t, "2.22 EB", HumanSize(2.22*1000*1000*1000*1000*1000*1000)) - assertEquals(t, "7.707 EB", HumanSize(7.707*1000*1000*1000*1000*1000*1000)) + assertEquals(t, "2 MB", HumanSize(2*MB)) + assertEquals(t, "3.42 GB", HumanSize(3.42*GB)) + assertEquals(t, "5.372 TB", HumanSize(5.372*TB)) + assertEquals(t, "2.22 PB", HumanSize(2.22*PB)) } func TestFromHumanSize(t *testing.T) { - assertFromHumanSize(t, "32", false, 32) - assertFromHumanSize(t, "32b", false, 32) - assertFromHumanSize(t, "32B", false, 32) - assertFromHumanSize(t, "32k", false, 32*1000) - assertFromHumanSize(t, "32K", false, 32*1000) - assertFromHumanSize(t, "32kb", false, 32*1000) - assertFromHumanSize(t, "32Kb", false, 32*1000) - assertFromHumanSize(t, "32Mb", false, 32*1000*1000) - assertFromHumanSize(t, "32Gb", false, 32*1000*1000*1000) - assertFromHumanSize(t, "32Tb", false, 32*1000*1000*1000*1000) - assertFromHumanSize(t, "8Pb", false, 8*1000*1000*1000*1000*1000) + assertSuccessEquals(t, 32, FromHumanSize, "32") + assertSuccessEquals(t, 32, FromHumanSize, "32b") + assertSuccessEquals(t, 32, FromHumanSize, "32B") + assertSuccessEquals(t, 32*KB, FromHumanSize, "32k") + assertSuccessEquals(t, 32*KB, FromHumanSize, "32K") + assertSuccessEquals(t, 32*KB, FromHumanSize, "32kb") + assertSuccessEquals(t, 32*KB, FromHumanSize, "32Kb") + assertSuccessEquals(t, 32*MB, FromHumanSize, "32Mb") + assertSuccessEquals(t, 32*GB, FromHumanSize, "32Gb") + assertSuccessEquals(t, 32*TB, FromHumanSize, "32Tb") + assertSuccessEquals(t, 32*PB, FromHumanSize, "32Pb") - assertFromHumanSize(t, "", true, -1) - assertFromHumanSize(t, "hello", true, -1) - assertFromHumanSize(t, "-32", true, -1) - assertFromHumanSize(t, " 32 ", true, -1) - assertFromHumanSize(t, "32 mb", true, -1) - assertFromHumanSize(t, "32m b", true, -1) - assertFromHumanSize(t, "32bm", true, -1) -} - -func assertFromHumanSize(t *testing.T, size string, expectError bool, expectedBytes int64) { - actualBytes, err := FromHumanSize(size) - if (err != nil) && !expectError { - t.Errorf("Unexpected error parsing '%s': %s", size, err) - } - if (err == nil) && expectError { - t.Errorf("Expected to get an error parsing '%s', but got none (bytes=%d)", size, actualBytes) - } - if actualBytes != expectedBytes { - t.Errorf("Expected '%s' to parse as %d bytes, got %d", size, expectedBytes, actualBytes) - } + assertError(t, FromHumanSize, "") + assertError(t, FromHumanSize, "hello") + assertError(t, FromHumanSize, "-32") + assertError(t, FromHumanSize, "32.3") + assertError(t, FromHumanSize, " 32 ") + assertError(t, FromHumanSize, "32.3Kb") + assertError(t, FromHumanSize, "32 mb") + assertError(t, FromHumanSize, "32m b") + assertError(t, FromHumanSize, "32bm") } func TestRAMInBytes(t *testing.T) { - assertRAMInBytes(t, "32", false, 32) - assertRAMInBytes(t, "32b", false, 32) - assertRAMInBytes(t, "32B", false, 32) - assertRAMInBytes(t, "32k", false, 32*1024) - assertRAMInBytes(t, "32K", false, 32*1024) - assertRAMInBytes(t, "32kb", false, 32*1024) - assertRAMInBytes(t, "32Kb", false, 32*1024) - assertRAMInBytes(t, "32Mb", false, 32*1024*1024) - assertRAMInBytes(t, "32MB", false, 32*1024*1024) - assertRAMInBytes(t, "32Gb", false, 32*1024*1024*1024) - assertRAMInBytes(t, "32G", false, 32*1024*1024*1024) - assertRAMInBytes(t, "32Tb", false, 32*1024*1024*1024*1024) + assertSuccessEquals(t, 32, RAMInBytes, "32") + assertSuccessEquals(t, 32, RAMInBytes, "32b") + assertSuccessEquals(t, 32, RAMInBytes, "32B") + assertSuccessEquals(t, 32*KiB, RAMInBytes, "32k") + assertSuccessEquals(t, 32*KiB, RAMInBytes, "32K") + assertSuccessEquals(t, 32*KiB, RAMInBytes, "32kb") + assertSuccessEquals(t, 32*KiB, RAMInBytes, "32Kb") + assertSuccessEquals(t, 32*MiB, RAMInBytes, "32Mb") + assertSuccessEquals(t, 32*GiB, RAMInBytes, "32Gb") + assertSuccessEquals(t, 32*TiB, RAMInBytes, "32Tb") + assertSuccessEquals(t, 32*PiB, RAMInBytes, "32Pb") + assertSuccessEquals(t, 32*PiB, RAMInBytes, "32PB") + assertSuccessEquals(t, 32*PiB, RAMInBytes, "32P") - assertRAMInBytes(t, "", true, -1) - assertRAMInBytes(t, "hello", true, -1) - assertRAMInBytes(t, "-32", true, -1) - assertRAMInBytes(t, " 32 ", true, -1) - assertRAMInBytes(t, "32 mb", true, -1) - assertRAMInBytes(t, "32m b", true, -1) - assertRAMInBytes(t, "32bm", true, -1) + assertError(t, RAMInBytes, "") + assertError(t, RAMInBytes, "hello") + assertError(t, RAMInBytes, "-32") + assertError(t, RAMInBytes, "32.3") + assertError(t, RAMInBytes, " 32 ") + assertError(t, RAMInBytes, "32.3Kb") + assertError(t, RAMInBytes, "32 mb") + assertError(t, RAMInBytes, "32m b") + assertError(t, RAMInBytes, "32bm") } -func assertRAMInBytes(t *testing.T, size string, expectError bool, expectedBytes int64) { - actualBytes, err := RAMInBytes(size) - if (err != nil) && !expectError { - t.Errorf("Unexpected error parsing '%s': %s", size, err) - } - if (err == nil) && expectError { - t.Errorf("Expected to get an error parsing '%s', but got none (bytes=%d)", size, actualBytes) - } - if actualBytes != expectedBytes { - t.Errorf("Expected '%s' to parse as %d bytes, got %d", size, expectedBytes, actualBytes) +func assertEquals(t *testing.T, expected, actual interface{}) { + if expected != actual { + t.Errorf("Expected '%v' but got '%v'", expected, actual) + } +} + +// func that maps to the parse function signatures as testing abstraction +type parseFn func(string) (int64, error) + +// Define 'String()' for pretty-print +func (fn parseFn) String() string { + fnName := runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name() + return fnName[strings.LastIndex(fnName, ".")+1:] +} + +func assertSuccessEquals(t *testing.T, expected int64, fn parseFn, arg string) { + res, err := fn(arg) + if err != nil || res != expected { + t.Errorf("%s(\"%s\") -> expected '%d' but got '%d' with error '%v'", fn, arg, expected, res, err) + } +} + +func assertError(t *testing.T, fn parseFn, arg string) { + res, err := fn(arg) + if err == nil && res != -1 { + t.Errorf("%s(\"%s\") -> expected error but got '%d'", fn, arg, res) } }