├── go.mod ├── .travis.yml ├── .gitignore ├── LICENSE ├── datasize_test.go ├── README.md └── datasize.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/c2h5oh/datasize 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: go 4 | go: 5 | - 1.4 6 | - 1.5 7 | - 1.6 8 | - 1.7 9 | - 1.8 10 | - 1.9 11 | - tip 12 | 13 | script: 14 | - go test -v 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Maciej Lisiewski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /datasize_test.go: -------------------------------------------------------------------------------- 1 | package datasize_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/c2h5oh/datasize" 7 | ) 8 | 9 | func TestMarshalText(t *testing.T) { 10 | table := []struct { 11 | in ByteSize 12 | out string 13 | }{ 14 | {0, "0B"}, 15 | {B, "1B"}, 16 | {KB, "1KB"}, 17 | {MB, "1MB"}, 18 | {GB, "1GB"}, 19 | {TB, "1TB"}, 20 | {PB, "1PB"}, 21 | {EB, "1EB"}, 22 | {400 * TB, "400TB"}, 23 | {2048 * MB, "2GB"}, 24 | {B + KB, "1025B"}, 25 | {MB + 20*KB, "1044KB"}, 26 | {100*MB + KB, "102401KB"}, 27 | } 28 | 29 | for _, tt := range table { 30 | b, _ := tt.in.MarshalText() 31 | s := string(b) 32 | 33 | if s != tt.out { 34 | t.Errorf("MarshalText(%d) => %s, want %s", tt.in, s, tt.out) 35 | } 36 | } 37 | } 38 | 39 | func TestUnmarshalText(t *testing.T) { 40 | table := []struct { 41 | in string 42 | err bool 43 | out ByteSize 44 | }{ 45 | {"0", false, ByteSize(0)}, 46 | {"0B", false, ByteSize(0)}, 47 | {"0 KB", false, ByteSize(0)}, 48 | {"1", false, B}, 49 | {"1K", false, KB}, 50 | {"2MB", false, 2 * MB}, 51 | {"5 GB", false, 5 * GB}, 52 | {"20480 G", false, 20 * TB}, 53 | {"50 eB", true, ByteSize((1 << 64) - 1)}, 54 | {"200000 pb", true, ByteSize((1 << 64) - 1)}, 55 | {"10 Mb", true, ByteSize(0)}, 56 | {"g", true, ByteSize(0)}, 57 | {"10 kB ", false, 10 * KB}, 58 | {"10 kBs ", true, ByteSize(0)}, 59 | } 60 | 61 | for _, tt := range table { 62 | t.Run("UnmarshalText "+tt.in, func(t *testing.T) { 63 | var s ByteSize 64 | err := s.UnmarshalText([]byte(tt.in)) 65 | 66 | if (err != nil) != tt.err { 67 | t.Errorf("UnmarshalText(%s) => %v, want no error", tt.in, err) 68 | } 69 | 70 | if s != tt.out { 71 | t.Errorf("UnmarshalText(%s) => %d bytes, want %d bytes", tt.in, s, tt.out) 72 | } 73 | }) 74 | t.Run("Parse "+tt.in, func(t *testing.T) { 75 | s, err := Parse([]byte(tt.in)) 76 | 77 | if (err != nil) != tt.err { 78 | t.Errorf("Parse(%s) => %v, want no error", tt.in, err) 79 | } 80 | 81 | if s != tt.out { 82 | t.Errorf("Parse(%s) => %d bytes, want %d bytes", tt.in, s, tt.out) 83 | } 84 | }) 85 | t.Run("MustParse "+tt.in, func(t *testing.T) { 86 | defer func() { 87 | if err := recover(); (err != nil) != tt.err { 88 | t.Errorf("MustParse(%s) => %v, want no error", tt.in, err) 89 | } 90 | }() 91 | 92 | s := MustParse([]byte(tt.in)) 93 | if s != tt.out { 94 | t.Errorf("MustParse(%s) => %d bytes, want %d bytes", tt.in, s, tt.out) 95 | } 96 | }) 97 | t.Run("ParseString "+tt.in, func(t *testing.T) { 98 | s, err := ParseString(tt.in) 99 | 100 | if (err != nil) != tt.err { 101 | t.Errorf("ParseString(%s) => %v, want no error", tt.in, err) 102 | } 103 | 104 | if s != tt.out { 105 | t.Errorf("ParseString(%s) => %d bytes, want %d bytes", tt.in, s, tt.out) 106 | } 107 | }) 108 | t.Run("MustParseString "+tt.in, func(t *testing.T) { 109 | defer func() { 110 | if err := recover(); (err != nil) != tt.err { 111 | t.Errorf("MustParseString(%s) => %v, want no error", tt.in, err) 112 | } 113 | }() 114 | 115 | s := MustParseString(tt.in) 116 | if s != tt.out { 117 | t.Errorf("MustParseString(%s) => %d bytes, want %d bytes", tt.in, s, tt.out) 118 | } 119 | }) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # datasize [![Build Status](https://travis-ci.org/c2h5oh/datasize.svg?branch=master)](https://travis-ci.org/c2h5oh/datasize) 2 | 3 | Golang helpers for data sizes 4 | 5 | ### Constants 6 | 7 | Just like `time` package provides `time.Second`, `time.Day` constants `datasize` provides: 8 | 9 | * `datasize.B` 1 byte 10 | * `datasize.KB` 1 kilobyte 11 | * `datasize.MB` 1 megabyte 12 | * `datasize.GB` 1 gigabyte 13 | * `datasize.TB` 1 terabyte 14 | * `datasize.PB` 1 petabyte 15 | * `datasize.EB` 1 exabyte 16 | 17 | ### Helpers 18 | 19 | Just like `time` package provides `duration.Nanoseconds() uint64 `, `duration.Hours() float64` helpers `datasize` has. 20 | 21 | * `ByteSize.Bytes() uint64` 22 | * `ByteSize.Kilobytes() float64` 23 | * `ByteSize.Megabytes() float64` 24 | * `ByteSize.Gigabytes() float64` 25 | * `ByteSize.Terabytes() float64` 26 | * `ByteSize.Petabytes() float64` 27 | * `ByteSize.Exabytes() float64` 28 | 29 | Warning: see limitations at the end of this document about a possible precision loss 30 | 31 | ### Parsing strings 32 | 33 | `datasize.ByteSize` implements `TextUnmarshaler` interface and will automatically parse human readable strings into correct values where it is used: 34 | 35 | * `"10 MB"` -> `10* datasize.MB` 36 | * `"10240 g"` -> `10 * datasize.TB` 37 | * `"2000"` -> `2000 * datasize.B` 38 | * `"1tB"` -> `datasize.TB` 39 | * `"5 peta"` -> `5 * datasize.PB` 40 | * `"28 kilobytes"` -> `28 * datasize.KB` 41 | * `"1 gigabyte"` -> `1 * datasize.GB` 42 | 43 | You can also do it manually: 44 | 45 | ```go 46 | var v datasize.ByteSize 47 | err := v.UnmarshalText([]byte("100 mb")) 48 | ``` 49 | 50 | ### Printing 51 | 52 | `Bytesize.String()` uses largest unit allowing an integer value: 53 | 54 | * `(102400 * datasize.MB).String()` -> `"100GB"` 55 | * `(datasize.MB + datasize.KB).String()` -> `"1025KB"` 56 | 57 | Use `%d` format string to get value in bytes without a unit. 58 | 59 | ### JSON and other encoding 60 | 61 | Both `TextMarshaler` and `TextUnmarshaler` interfaces are implemented - JSON will just work. Other encoders will work provided they use those interfaces. 62 | 63 | ### Human readable 64 | 65 | `ByteSize.HumanReadable()` or `ByteSize.HR()` returns a string with 1-3 digits, followed by 1 decimal place, a space and unit big enough to get 1-3 digits 66 | 67 | * `(102400 * datasize.MB).String()` -> `"100.0 GB"` 68 | * `(datasize.MB + 512 * datasize.KB).String()` -> `"1.5 MB"` 69 | 70 | ### Limitations 71 | 72 | * The underlying data type for `data.ByteSize` is `uint64`, so values outside of 0 to 2^64-1 range will overflow 73 | * size helper functions (like `ByteSize.Kilobytes()`) return `float64`, which can't represent all possible values of `uint64` accurately: 74 | * if the returned value is supposed to have no fraction (ie `(10 * datasize.MB).Kilobytes()`) accuracy loss happens when value is more than 2^53 larger than unit: `.Kilobytes()` over 8 petabytes, `.Megabytes()` over 8 exabytes 75 | * if the returned value is supposed to have a fraction (ie `(datasize.PB + datasize.B).Megabytes()`) in addition to the above note accuracy loss may occur in fractional part too - larger integer part leaves fewer bytes to store fractional part, the smaller the remainder vs unit the move bytes are required to store the fractional part 76 | * Parsing a string with `Mb`, `Tb`, etc units will return a syntax error, because capital followed by lower case is commonly used for bits, not bytes 77 | * Parsing a string with value exceeding 2^64-1 bytes will return 2^64-1 and an out of range error 78 | -------------------------------------------------------------------------------- /datasize.go: -------------------------------------------------------------------------------- 1 | package datasize 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | type ByteSize uint64 11 | 12 | const ( 13 | B ByteSize = 1 14 | KB = B << 10 15 | MB = KB << 10 16 | GB = MB << 10 17 | TB = GB << 10 18 | PB = TB << 10 19 | EB = PB << 10 20 | 21 | fnUnmarshalText string = "UnmarshalText" 22 | maxUint64 uint64 = (1 << 64) - 1 23 | cutoff uint64 = maxUint64 / 10 24 | ) 25 | 26 | var ErrBits = errors.New("unit with capital unit prefix and lower case unit (b) - bits, not bytes ") 27 | 28 | func (b ByteSize) Bytes() uint64 { 29 | return uint64(b) 30 | } 31 | 32 | func (b ByteSize) KBytes() float64 { 33 | v := b / KB 34 | r := b % KB 35 | return float64(v) + float64(r)/float64(KB) 36 | } 37 | 38 | func (b ByteSize) MBytes() float64 { 39 | v := b / MB 40 | r := b % MB 41 | return float64(v) + float64(r)/float64(MB) 42 | } 43 | 44 | func (b ByteSize) GBytes() float64 { 45 | v := b / GB 46 | r := b % GB 47 | return float64(v) + float64(r)/float64(GB) 48 | } 49 | 50 | func (b ByteSize) TBytes() float64 { 51 | v := b / TB 52 | r := b % TB 53 | return float64(v) + float64(r)/float64(TB) 54 | } 55 | 56 | func (b ByteSize) PBytes() float64 { 57 | v := b / PB 58 | r := b % PB 59 | return float64(v) + float64(r)/float64(PB) 60 | } 61 | 62 | func (b ByteSize) EBytes() float64 { 63 | v := b / EB 64 | r := b % EB 65 | return float64(v) + float64(r)/float64(EB) 66 | } 67 | 68 | func (b ByteSize) String() string { 69 | switch { 70 | case b == 0: 71 | return fmt.Sprint("0B") 72 | case b%EB == 0: 73 | return fmt.Sprintf("%dEB", b/EB) 74 | case b%PB == 0: 75 | return fmt.Sprintf("%dPB", b/PB) 76 | case b%TB == 0: 77 | return fmt.Sprintf("%dTB", b/TB) 78 | case b%GB == 0: 79 | return fmt.Sprintf("%dGB", b/GB) 80 | case b%MB == 0: 81 | return fmt.Sprintf("%dMB", b/MB) 82 | case b%KB == 0: 83 | return fmt.Sprintf("%dKB", b/KB) 84 | default: 85 | return fmt.Sprintf("%dB", b) 86 | } 87 | } 88 | 89 | func (b ByteSize) HR() string { 90 | return b.HumanReadable() 91 | } 92 | 93 | func (b ByteSize) HumanReadable() string { 94 | switch { 95 | case b > EB: 96 | return fmt.Sprintf("%.1f EB", b.EBytes()) 97 | case b > PB: 98 | return fmt.Sprintf("%.1f PB", b.PBytes()) 99 | case b > TB: 100 | return fmt.Sprintf("%.1f TB", b.TBytes()) 101 | case b > GB: 102 | return fmt.Sprintf("%.1f GB", b.GBytes()) 103 | case b > MB: 104 | return fmt.Sprintf("%.1f MB", b.MBytes()) 105 | case b > KB: 106 | return fmt.Sprintf("%.1f KB", b.KBytes()) 107 | default: 108 | return fmt.Sprintf("%d B", b) 109 | } 110 | } 111 | 112 | func (b ByteSize) MarshalText() ([]byte, error) { 113 | return []byte(b.String()), nil 114 | } 115 | 116 | func (b *ByteSize) UnmarshalText(t []byte) error { 117 | var val uint64 118 | var unit string 119 | 120 | // copy for error message 121 | t0 := t 122 | 123 | var c byte 124 | var i int 125 | 126 | ParseLoop: 127 | for i < len(t) { 128 | c = t[i] 129 | switch { 130 | case '0' <= c && c <= '9': 131 | if val > cutoff { 132 | goto Overflow 133 | } 134 | 135 | c = c - '0' 136 | val *= 10 137 | 138 | if val > val+uint64(c) { 139 | // val+v overflows 140 | goto Overflow 141 | } 142 | val += uint64(c) 143 | i++ 144 | 145 | default: 146 | if i == 0 { 147 | goto SyntaxError 148 | } 149 | break ParseLoop 150 | } 151 | } 152 | 153 | unit = strings.TrimSpace(string(t[i:])) 154 | switch unit { 155 | case "Kb", "Mb", "Gb", "Tb", "Pb", "Eb": 156 | goto BitsError 157 | } 158 | unit = strings.ToLower(unit) 159 | switch unit { 160 | case "", "b", "byte": 161 | // do nothing - already in bytes 162 | 163 | case "k", "kb", "kilo", "kilobyte", "kilobytes": 164 | if val > maxUint64/uint64(KB) { 165 | goto Overflow 166 | } 167 | val *= uint64(KB) 168 | 169 | case "m", "mb", "mega", "megabyte", "megabytes": 170 | if val > maxUint64/uint64(MB) { 171 | goto Overflow 172 | } 173 | val *= uint64(MB) 174 | 175 | case "g", "gb", "giga", "gigabyte", "gigabytes": 176 | if val > maxUint64/uint64(GB) { 177 | goto Overflow 178 | } 179 | val *= uint64(GB) 180 | 181 | case "t", "tb", "tera", "terabyte", "terabytes": 182 | if val > maxUint64/uint64(TB) { 183 | goto Overflow 184 | } 185 | val *= uint64(TB) 186 | 187 | case "p", "pb", "peta", "petabyte", "petabytes": 188 | if val > maxUint64/uint64(PB) { 189 | goto Overflow 190 | } 191 | val *= uint64(PB) 192 | 193 | case "E", "EB", "e", "eb", "eB": 194 | if val > maxUint64/uint64(EB) { 195 | goto Overflow 196 | } 197 | val *= uint64(EB) 198 | 199 | default: 200 | goto SyntaxError 201 | } 202 | 203 | *b = ByteSize(val) 204 | return nil 205 | 206 | Overflow: 207 | *b = ByteSize(maxUint64) 208 | return &strconv.NumError{fnUnmarshalText, string(t0), strconv.ErrRange} 209 | 210 | SyntaxError: 211 | *b = 0 212 | return &strconv.NumError{fnUnmarshalText, string(t0), strconv.ErrSyntax} 213 | 214 | BitsError: 215 | *b = 0 216 | return &strconv.NumError{fnUnmarshalText, string(t0), ErrBits} 217 | } 218 | 219 | func Parse(t []byte) (ByteSize, error) { 220 | var v ByteSize 221 | err := v.UnmarshalText(t) 222 | return v, err 223 | } 224 | 225 | func MustParse(t []byte) ByteSize { 226 | v, err := Parse(t) 227 | if err != nil { 228 | panic(err) 229 | } 230 | return v 231 | } 232 | 233 | func ParseString(s string) (ByteSize, error) { 234 | return Parse([]byte(s)) 235 | } 236 | 237 | func MustParseString(s string) ByteSize { 238 | return MustParse([]byte(s)) 239 | } 240 | --------------------------------------------------------------------------------