├── .travis.yml ├── LICENSE ├── README.md ├── bytesize.go ├── conversion_test.go ├── example_test.go └── go.mod /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - release 5 | 6 | install: 7 | - go get -v -t ./... 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ByteSize 2 | ======== 3 | 4 | Bytesize is a package for working with measurements of bytes. Using this package 5 | allows you to easily add 100KB to 4928MB and get a nicely formatted string 6 | representation of the result. 7 | 8 | [![Go Reference](https://pkg.go.dev/badge/github.com/inhies/go-bytesize.svg)](https://pkg.go.dev/github.com/inhies/go-bytesize) 9 | [![Build Status](https://travis-ci.org/inhies/go-bytesize.png)](https://travis-ci.org/inhies/go-bytesize) 10 | [![Coverage Status](https://coveralls.io/repos/inhies/go-bytesize/badge.svg?branch=master&service=github)](https://coveralls.io/github/inhies/go-bytesize?branch=master) 11 | 12 | Usage 13 | ----- 14 | 15 | Check the built in documentation for examples using the godoc link above or by 16 | running godoc locally. 17 | -------------------------------------------------------------------------------- /bytesize.go: -------------------------------------------------------------------------------- 1 | // Package bytesize provides functionality for measuring and formatting byte 2 | // sizes. 3 | // 4 | // You can also perfom mathmatical operation with ByteSize's and the result 5 | // will be a valid ByteSize with the correct size suffix. 6 | package bytesize 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | "strconv" 12 | "strings" 13 | "unicode" 14 | ) 15 | 16 | // This code was originally based on http://golang.org/doc/progs/eff_bytesize.go 17 | // 18 | // Since then many improvements have been made. The following is the original 19 | // copyright notice: 20 | 21 | // Copyright 2009 The Go Authors. All rights reserved. Use of this source code 22 | // is governed by a BSD-style license that can be found in the LICENSE file. 23 | 24 | // ByteSize represents a number of bytes 25 | type ByteSize uint64 26 | 27 | // Byte size size suffixes. 28 | const ( 29 | B ByteSize = 1 30 | KB ByteSize = 1 << (10 * iota) 31 | MB 32 | GB 33 | TB 34 | PB 35 | EB 36 | ) 37 | 38 | // Used for returning long unit form of string representation. 39 | var longUnitMap = map[ByteSize]string{ 40 | B: "byte", 41 | KB: "kilobyte", 42 | MB: "megabyte", 43 | GB: "gigabyte", 44 | TB: "terabyte", 45 | PB: "petabyte", 46 | EB: "exabyte", 47 | } 48 | 49 | // Used for returning string representation. 50 | var shortUnitMap = map[ByteSize]string{ 51 | B: "B", 52 | KB: "KB", 53 | MB: "MB", 54 | GB: "GB", 55 | TB: "TB", 56 | PB: "PB", 57 | EB: "EB", 58 | } 59 | 60 | // Used to convert user input to ByteSize 61 | var unitMap = map[string]ByteSize{ 62 | "B": B, 63 | "BYTE": B, 64 | "BYTES": B, 65 | 66 | "KB": KB, 67 | "KILOBYTE": KB, 68 | "KILOBYTES": KB, 69 | 70 | "MB": MB, 71 | "MEGABYTE": MB, 72 | "MEGABYTES": MB, 73 | 74 | "GB": GB, 75 | "GIGABYTE": GB, 76 | "GIGABYTES": GB, 77 | 78 | "TB": TB, 79 | "TERABYTE": TB, 80 | "TERABYTES": TB, 81 | 82 | "PB": PB, 83 | "PETABYTE": PB, 84 | "PETABYTES": PB, 85 | 86 | "EB": EB, 87 | "EXABYTE": EB, 88 | "EXABYTES": EB, 89 | } 90 | 91 | var ( 92 | // Use long units, such as "megabytes" instead of "MB". 93 | LongUnits bool = false 94 | 95 | // String format of bytesize output. The unit of measure will be appended 96 | // to the end. Uses the same formatting options as the fmt package. 97 | Format string = "%.2f" 98 | ) 99 | 100 | // Parse parses a byte size string. A byte size string is a number followed by 101 | // a unit suffix, such as "1024B" or "1 MB". Valid byte units are "B", "KB", 102 | // "MB", "GB", "TB", "PB" and "EB". You can also use the long 103 | // format of units, such as "kilobyte" or "kilobytes". 104 | func Parse(s string) (ByteSize, error) { 105 | // Remove leading and trailing whitespace 106 | s = strings.TrimSpace(s) 107 | 108 | split := make([]string, 0) 109 | for i, r := range s { 110 | if !unicode.IsDigit(r) && r != '.' { 111 | // Split the string by digit and size designator, remove whitespace 112 | split = append(split, strings.TrimSpace(string(s[:i]))) 113 | split = append(split, strings.TrimSpace(string(s[i:]))) 114 | break 115 | } 116 | } 117 | 118 | // Check to see if we split successfully 119 | if len(split) != 2 { 120 | return 0, errors.New("Unrecognized size suffix") 121 | } 122 | 123 | // Check for MB, MEGABYTE, and MEGABYTES 124 | unit, ok := unitMap[strings.ToUpper(split[1])] 125 | if !ok { 126 | return 0, errors.New("Unrecognized size suffix " + split[1]) 127 | 128 | } 129 | 130 | value, err := strconv.ParseFloat(split[0], 64) 131 | if err != nil { 132 | return 0, err 133 | } 134 | 135 | bytesize := ByteSize(value * float64(unit)) 136 | return bytesize, nil 137 | 138 | } 139 | 140 | // Satisfy the flag package Value interface. 141 | func (b *ByteSize) Set(s string) error { 142 | bs, err := Parse(s) 143 | if err != nil { 144 | return err 145 | } 146 | *b = bs 147 | return nil 148 | } 149 | 150 | // Satisfy the pflag package Value interface. 151 | func (b *ByteSize) Type() string { return "byte_size" } 152 | 153 | // Satisfy the encoding.TextUnmarshaler interface. 154 | func (b *ByteSize) UnmarshalText(text []byte) error { 155 | return b.Set(string(text)) 156 | } 157 | 158 | // Satisfy the flag package Getter interface. 159 | func (b *ByteSize) Get() interface{} { return ByteSize(*b) } 160 | 161 | // New returns a new ByteSize type set to s. 162 | func New(s float64) ByteSize { 163 | return ByteSize(s) 164 | } 165 | 166 | // Returns a string representation of b with the specified formatting and units. 167 | func (b ByteSize) Format(format string, unit string, longUnits bool) string { 168 | return b.format(format, unit, longUnits) 169 | } 170 | 171 | // String returns the string form of b using the package global Format and 172 | // LongUnits options. 173 | func (b ByteSize) String() string { 174 | return b.format(Format, "", LongUnits) 175 | } 176 | 177 | func (b ByteSize) format(format string, unit string, longUnits bool) string { 178 | var unitSize ByteSize 179 | if unit != "" { 180 | var ok bool 181 | unitSize, ok = unitMap[strings.ToUpper(unit)] 182 | if !ok { 183 | return "Unrecognized unit: " + unit 184 | } 185 | } else { 186 | switch { 187 | case b >= EB: 188 | unitSize = EB 189 | case b >= PB: 190 | unitSize = PB 191 | case b >= TB: 192 | unitSize = TB 193 | case b >= GB: 194 | unitSize = GB 195 | case b >= MB: 196 | unitSize = MB 197 | case b >= KB: 198 | unitSize = KB 199 | default: 200 | unitSize = B 201 | } 202 | } 203 | 204 | if longUnits { 205 | var s string 206 | value := fmt.Sprintf(format, float64(b)/float64(unitSize)) 207 | if printS, _ := strconv.ParseFloat(strings.TrimSpace(value), 64); printS > 0 && printS != 1 { 208 | s = "s" 209 | } 210 | return fmt.Sprintf(format+longUnitMap[unitSize]+s, float64(b)/float64(unitSize)) 211 | } 212 | return fmt.Sprintf(format+shortUnitMap[unitSize], float64(b)/float64(unitSize)) 213 | } 214 | -------------------------------------------------------------------------------- /conversion_test.go: -------------------------------------------------------------------------------- 1 | package bytesize 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_Overflow(t *testing.T) { 8 | b, err := Parse("1797693134862315708145274237317043567981000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000B") 9 | if err == nil || b != 0 { 10 | t.Fatal("Max float64 test did not fail") 11 | } 12 | } 13 | 14 | var formatTable = []struct { 15 | Bytes float64 16 | Format string 17 | Result string 18 | }{ 19 | {1, "byte", "1 B"}, 20 | {1024, "kb", "1 KB"}, 21 | {1099511627776, "GB", "1024 GB"}, 22 | {1125899906842624, "GB", "1048576 GB"}, 23 | {1125899906842624, "potato", "Unrecognized unit: potato"}, 24 | } 25 | 26 | func Test_Format(t *testing.T) { 27 | for _, v := range formatTable { 28 | bSize := New(v.Bytes) 29 | b := bSize.Format("%.0f ", v.Format, false) 30 | if b != v.Result { 31 | t.Fatalf("Expected %s, received %s", v.Result, b) 32 | } 33 | } 34 | } 35 | 36 | var newTable = []struct { 37 | Bytes float64 38 | Result string 39 | }{ 40 | {1, "1.00B"}, 41 | {1023, "1023.00B"}, 42 | {1024, "1.00KB"}, 43 | {1048576, "1.00MB"}, 44 | {1073741824, "1.00GB"}, 45 | {1099511627776, "1.00TB"}, 46 | {1125899906842624, "1.00PB"}, 47 | {1152921504606846976, "1.00EB"}, 48 | } 49 | 50 | func Test_New(t *testing.T) { 51 | for _, v := range newTable { 52 | b := New(v.Bytes) 53 | if b.String() != v.Result { 54 | t.Fatalf("Expected %s, received %s", v.Result, b) 55 | } 56 | } 57 | } 58 | 59 | var globalFormatTable = []struct { 60 | Bytes float64 61 | Result string 62 | }{ 63 | {1, "1 byte"}, 64 | {1023, "1023 bytes"}, 65 | {1024, "1 kilobyte"}, 66 | {1048576, "1 megabyte"}, 67 | {1073741824, "1 gigabyte"}, 68 | {1099511627776, "1 terabyte"}, 69 | {1125899906842624, "1 petabyte"}, 70 | {1152921504606846976, "1 exabyte"}, 71 | {2 * 1, "2 bytes"}, 72 | {2 * 1024, "2 kilobytes"}, 73 | {2 * 1048576, "2 megabytes"}, 74 | {2 * 1073741824, "2 gigabytes"}, 75 | {2 * 1099511627776, "2 terabytes"}, 76 | {2 * 1125899906842624, "2 petabytes"}, 77 | {2 * 1152921504606846976, "2 exabytes"}, 78 | } 79 | 80 | func Test_GlobalFormat(t *testing.T) { 81 | Format = "%.0f " 82 | LongUnits = true 83 | for _, v := range globalFormatTable { 84 | b := New(v.Bytes) 85 | if b.String() != v.Result { 86 | t.Fatalf("Expected %s, received %s", v.Result, b) 87 | } 88 | } 89 | Format = "%.2f" 90 | LongUnits = false 91 | } 92 | 93 | var parseTable = []struct { 94 | Input string 95 | Result string 96 | Fail bool 97 | }{ 98 | {"1B", "1.00B", false}, 99 | {"1 B", "1.00B", false}, 100 | {"1 byte", "1.00B", false}, 101 | {"2 bytes", "2.00B", false}, 102 | {"1B ", "1.00B", false}, 103 | {" 1 B ", "1.00B", false}, 104 | {"1023B", "1023.00B", false}, 105 | {"1024B", "1.00KB", false}, 106 | {"1KB 1023B", "", true}, 107 | {"1.5GB", "1.50GB", false}, 108 | {"1", "", true}, 109 | } 110 | 111 | func Test_Parse(t *testing.T) { 112 | for _, v := range parseTable { 113 | b, err := Parse(v.Input) 114 | if err != nil && !v.Fail { 115 | t.Fatal(err) 116 | } 117 | if b.String() != v.Result && !v.Fail { 118 | t.Fatalf("Expected %s, received %s", v.Result, b) 119 | } 120 | } 121 | } 122 | 123 | func Test_Set(t *testing.T) { 124 | for _, v := range parseTable { 125 | var b ByteSize 126 | var err error 127 | err = b.Set(v.Input) 128 | if err != nil && !v.Fail { 129 | t.Fatal(err) 130 | } 131 | if b.String() != v.Result && !v.Fail { 132 | t.Fatalf("Expected %s, received %s", v.Result, b) 133 | } 134 | } 135 | } 136 | 137 | var getTable = []struct { 138 | Input string 139 | Result ByteSize 140 | }{ 141 | {"1 byte", 1 * B}, 142 | } 143 | 144 | func Test_Get(t *testing.T) { 145 | for _, v := range getTable { 146 | b, err := Parse(v.Input) 147 | if err != nil { 148 | t.Fatal(err) 149 | } 150 | get := b.Get() 151 | if get != v.Result { 152 | t.Fatalf("Expected %s, received %s", v.Result, get) 153 | } 154 | } 155 | } 156 | 157 | var mathTable = []struct { 158 | B1 ByteSize 159 | Function rune 160 | B2 ByteSize 161 | Result string 162 | }{ 163 | {1024, '+', 1024, "2.00KB"}, 164 | {1073741824, '+', 10485760, "1.01GB"}, 165 | {1073741824, '-', 536870912, "512.00MB"}, 166 | } 167 | 168 | func Test_Math(t *testing.T) { 169 | for _, v := range mathTable { 170 | switch v.Function { 171 | case '+': 172 | total := v.B1 + v.B2 173 | if total.String() != v.Result { 174 | t.Fatalf("Fail: %s + %s = %s, received %s", v.B1, v.B2, v.Result, total) 175 | } 176 | case '-': 177 | total := v.B1 - v.B2 178 | if total.String() != v.Result { 179 | t.Fatalf("Fail: %s - %s = %s, received %s", v.B1, v.B2, v.Result, total) 180 | } 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package bytesize_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/inhies/go-bytesize" 7 | ) 8 | 9 | func ExampleNew() { 10 | b := bytesize.New(1024) 11 | fmt.Printf("%s", b) 12 | 13 | // Output: 14 | // 1.00KB 15 | } 16 | 17 | // Demonstrates using different output formatting and units. 18 | func ExampleByteSize_Format() { 19 | b := 1 * bytesize.TB // Create a new 1 terabyte ByteSize. 20 | fmt.Printf("%s\n", b) 21 | fmt.Printf("%s\n", b.Format("%.8f ", "petabyte", true)) 22 | 23 | // Output: 24 | // 1.00TB 25 | // 0.00097656 petabytes 26 | } 27 | 28 | func ExampleNew_math() { 29 | b1 := bytesize.New(1024) 30 | b2 := bytesize.New(4096) 31 | sum := b1 + b2 32 | fmt.Printf("%s", sum) 33 | 34 | // Output: 35 | // 5.00KB 36 | } 37 | 38 | func ExampleParse() { 39 | b, _ := bytesize.Parse("1024 GB") 40 | fmt.Printf("%s\n", b) 41 | 42 | b, _ = bytesize.Parse("3 petabytes") 43 | fmt.Printf("%s\n", b) 44 | 45 | bytesize.LongUnits = true 46 | bytesize.Format = "%.0f " 47 | fmt.Printf("%s\n", b) 48 | 49 | // Output: 50 | // 1.00TB 51 | // 3.00PB 52 | // 3 petabytes 53 | } 54 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/inhies/go-bytesize 2 | 3 | go 1.12 4 | --------------------------------------------------------------------------------