├── COPYING ├── README.md ├── bytes.go ├── bytes_test.go ├── doc.go ├── go.mod ├── go.sum ├── renovate.json5 ├── si.go └── util.go /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014 Alec Thomas 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Reference](https://pkg.go.dev/badge/github.com/alecthomas/units.svg)](https://pkg.go.dev/github.com/alecthomas/units) 2 | 3 | # Units - Helpful unit multipliers and functions for Go 4 | 5 | The goal of this package is to have functionality similar to the [time](http://golang.org/pkg/time/) package. 6 | 7 | It allows for code like this: 8 | 9 | ```go 10 | n, err := ParseBase2Bytes("1KB") 11 | // n == 1024 12 | n = units.Mebibyte * 512 13 | ``` 14 | -------------------------------------------------------------------------------- /bytes.go: -------------------------------------------------------------------------------- 1 | package units 2 | 3 | // Base2Bytes is the old non-SI power-of-2 byte scale (1024 bytes in a kilobyte, 4 | // etc.). 5 | type Base2Bytes int64 6 | 7 | // Base-2 byte units. 8 | const ( 9 | Kibibyte Base2Bytes = 1024 10 | KiB = Kibibyte 11 | Mebibyte = Kibibyte * 1024 12 | MiB = Mebibyte 13 | Gibibyte = Mebibyte * 1024 14 | GiB = Gibibyte 15 | Tebibyte = Gibibyte * 1024 16 | TiB = Tebibyte 17 | Pebibyte = Tebibyte * 1024 18 | PiB = Pebibyte 19 | Exbibyte = Pebibyte * 1024 20 | EiB = Exbibyte 21 | ) 22 | 23 | var ( 24 | bytesUnitMap = MakeUnitMap("iB", "B", 1024) 25 | oldBytesUnitMap = MakeUnitMap("B", "B", 1024) 26 | ) 27 | 28 | // ParseBase2Bytes supports both iB and B in base-2 multipliers. That is, KB 29 | // and KiB are both 1024. 30 | // However "kB", which is the correct SI spelling of 1000 Bytes, is rejected. 31 | func ParseBase2Bytes(s string) (Base2Bytes, error) { 32 | n, err := ParseUnit(s, bytesUnitMap) 33 | if err != nil { 34 | n, err = ParseUnit(s, oldBytesUnitMap) 35 | } 36 | return Base2Bytes(n), err 37 | } 38 | 39 | func (b Base2Bytes) String() string { 40 | return ToString(int64(b), 1024, "iB", "B") 41 | } 42 | 43 | // MarshalText implement encoding.TextMarshaler to process json/yaml. 44 | func (b Base2Bytes) MarshalText() ([]byte, error) { 45 | return []byte(b.String()), nil 46 | } 47 | 48 | // UnmarshalText implement encoding.TextUnmarshaler to process json/yaml. 49 | func (b *Base2Bytes) UnmarshalText(text []byte) error { 50 | n, err := ParseBase2Bytes(string(text)) 51 | *b = n 52 | return err 53 | } 54 | 55 | // Floor returns Base2Bytes with all but the largest unit zeroed out. So that e.g. 1GiB1MiB1KiB → 1GiB. 56 | func (b Base2Bytes) Floor() Base2Bytes { 57 | switch { 58 | case b > Exbibyte: 59 | return (b / Exbibyte) * Exbibyte 60 | case b > Pebibyte: 61 | return (b / Pebibyte) * Pebibyte 62 | case b > Tebibyte: 63 | return (b / Tebibyte) * Tebibyte 64 | case b > Gibibyte: 65 | return (b / Gibibyte) * Gibibyte 66 | case b > Mebibyte: 67 | return (b / Mebibyte) * Mebibyte 68 | case b > Kibibyte: 69 | return (b / Kibibyte) * Kibibyte 70 | default: 71 | return b 72 | } 73 | } 74 | 75 | // Round returns Base2Bytes with all but the first n units zeroed out. So that e.g. 1GiB1MiB1KiB → 1GiB1MiB, if n is 2. 76 | func (b Base2Bytes) Round(n int) Base2Bytes { 77 | idx := 0 78 | 79 | switch { 80 | case b > Exbibyte: 81 | idx = n 82 | case b > Pebibyte: 83 | idx = n + 1 84 | case b > Tebibyte: 85 | idx = n + 2 86 | case b > Gibibyte: 87 | idx = n + 3 88 | case b > Mebibyte: 89 | idx = n + 4 90 | case b > Kibibyte: 91 | idx = n + 5 92 | } 93 | 94 | switch idx { 95 | case 1: 96 | return b - b%Exbibyte 97 | case 2: 98 | return b - b%Pebibyte 99 | case 3: 100 | return b - b%Tebibyte 101 | case 4: 102 | return b - b%Gibibyte 103 | case 5: 104 | return b - b%Mebibyte 105 | case 6: 106 | return b - b%Kibibyte 107 | default: 108 | return b 109 | } 110 | } 111 | 112 | var metricBytesUnitMap = MakeUnitMap("B", "B", 1000) 113 | 114 | // MetricBytes are SI byte units (1000 bytes in a kilobyte). 115 | type MetricBytes SI 116 | 117 | // SI base-10 byte units. 118 | const ( 119 | Kilobyte MetricBytes = 1000 120 | KB = Kilobyte 121 | Megabyte = Kilobyte * 1000 122 | MB = Megabyte 123 | Gigabyte = Megabyte * 1000 124 | GB = Gigabyte 125 | Terabyte = Gigabyte * 1000 126 | TB = Terabyte 127 | Petabyte = Terabyte * 1000 128 | PB = Petabyte 129 | Exabyte = Petabyte * 1000 130 | EB = Exabyte 131 | ) 132 | 133 | // ParseMetricBytes parses base-10 metric byte units. That is, KB is 1000 bytes. 134 | func ParseMetricBytes(s string) (MetricBytes, error) { 135 | n, err := ParseUnit(s, metricBytesUnitMap) 136 | return MetricBytes(n), err 137 | } 138 | 139 | // TODO: represents 1000B as uppercase "KB", while SI standard requires "kB". 140 | func (m MetricBytes) String() string { 141 | return ToString(int64(m), 1000, "B", "B") 142 | } 143 | 144 | // Floor returns MetricBytes with all but the largest unit zeroed out. So that e.g. 1GB1MB1KB → 1GB. 145 | func (b MetricBytes) Floor() MetricBytes { 146 | switch { 147 | case b > Exabyte: 148 | return (b / Exabyte) * Exabyte 149 | case b > Petabyte: 150 | return (b / Petabyte) * Petabyte 151 | case b > Terabyte: 152 | return (b / Terabyte) * Terabyte 153 | case b > Gigabyte: 154 | return (b / Gigabyte) * Gigabyte 155 | case b > Megabyte: 156 | return (b / Megabyte) * Megabyte 157 | case b > Kilobyte: 158 | return (b / Kilobyte) * Kilobyte 159 | default: 160 | return b 161 | } 162 | } 163 | 164 | // Round returns MetricBytes with all but the first n units zeroed out. So that e.g. 1GB1MB1KB → 1GB1MB, if n is 2. 165 | func (b MetricBytes) Round(n int) MetricBytes { 166 | idx := 0 167 | 168 | switch { 169 | case b > Exabyte: 170 | idx = n 171 | case b > Petabyte: 172 | idx = n + 1 173 | case b > Terabyte: 174 | idx = n + 2 175 | case b > Gigabyte: 176 | idx = n + 3 177 | case b > Megabyte: 178 | idx = n + 4 179 | case b > Kilobyte: 180 | idx = n + 5 181 | } 182 | 183 | switch idx { 184 | case 1: 185 | return b - b%Exabyte 186 | case 2: 187 | return b - b%Petabyte 188 | case 3: 189 | return b - b%Terabyte 190 | case 4: 191 | return b - b%Gigabyte 192 | case 5: 193 | return b - b%Megabyte 194 | case 6: 195 | return b - b%Kilobyte 196 | default: 197 | return b 198 | } 199 | } 200 | 201 | // ParseStrictBytes supports both iB and B suffixes for base 2 and metric, 202 | // respectively. That is, KiB represents 1024 and kB, KB represent 1000. 203 | func ParseStrictBytes(s string) (int64, error) { 204 | n, err := ParseUnit(s, bytesUnitMap) 205 | if err != nil { 206 | n, err = ParseUnit(s, metricBytesUnitMap) 207 | } 208 | return int64(n), err 209 | } 210 | -------------------------------------------------------------------------------- /bytes_test.go: -------------------------------------------------------------------------------- 1 | package units 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestBase2BytesString(t *testing.T) { 11 | assert.Equal(t, Base2Bytes(0).String(), "0B") 12 | assert.Equal(t, Base2Bytes(1025).String(), "1KiB1B") 13 | assert.Equal(t, Base2Bytes(1048577).String(), "1MiB1B") 14 | } 15 | 16 | func TestParseBase2Bytes(t *testing.T) { 17 | n, err := ParseBase2Bytes("0B") 18 | assert.NoError(t, err) 19 | assert.Equal(t, 0, int(n)) 20 | n, err = ParseBase2Bytes("1kB") 21 | assert.Error(t, err) 22 | n, err = ParseBase2Bytes("1KB") 23 | assert.NoError(t, err) 24 | assert.Equal(t, 1024, int(n)) 25 | n, err = ParseBase2Bytes("1MB1KB25B") 26 | assert.NoError(t, err) 27 | assert.Equal(t, 1049625, int(n)) 28 | n, err = ParseBase2Bytes("1.5MB") 29 | assert.NoError(t, err) 30 | assert.Equal(t, 1572864, int(n)) 31 | 32 | n, err = ParseBase2Bytes("1kiB") 33 | assert.Error(t, err) 34 | n, err = ParseBase2Bytes("1KiB") 35 | assert.NoError(t, err) 36 | assert.Equal(t, 1024, int(n)) 37 | n, err = ParseBase2Bytes("1MiB1KiB25B") 38 | assert.NoError(t, err) 39 | assert.Equal(t, 1049625, int(n)) 40 | n, err = ParseBase2Bytes("1.5MiB") 41 | assert.NoError(t, err) 42 | assert.Equal(t, 1572864, int(n)) 43 | } 44 | 45 | func TestBase2BytesUnmarshalText(t *testing.T) { 46 | var n Base2Bytes 47 | err := n.UnmarshalText([]byte("0B")) 48 | assert.NoError(t, err) 49 | assert.Equal(t, 0, int(n)) 50 | err = n.UnmarshalText([]byte("1kB")) 51 | assert.Error(t, err) 52 | err = n.UnmarshalText([]byte("1KB")) 53 | assert.NoError(t, err) 54 | assert.Equal(t, 1024, int(n)) 55 | err = n.UnmarshalText([]byte("1MB1KB25B")) 56 | assert.NoError(t, err) 57 | assert.Equal(t, 1049625, int(n)) 58 | err = n.UnmarshalText([]byte("1.5MB")) 59 | assert.NoError(t, err) 60 | assert.Equal(t, 1572864, int(n)) 61 | 62 | err = n.UnmarshalText([]byte("1kiB")) 63 | assert.Error(t, err) 64 | err = n.UnmarshalText([]byte("1KiB")) 65 | assert.NoError(t, err) 66 | assert.Equal(t, 1024, int(n)) 67 | err = n.UnmarshalText([]byte("1MiB1KiB25B")) 68 | assert.NoError(t, err) 69 | assert.Equal(t, 1049625, int(n)) 70 | err = n.UnmarshalText([]byte("1.5MiB")) 71 | assert.NoError(t, err) 72 | assert.Equal(t, 1572864, int(n)) 73 | } 74 | 75 | func TestBase2Floor(t *testing.T) { 76 | var n Base2Bytes = KiB 77 | assert.Equal(t, "1KiB", n.Floor().String()) 78 | n = MiB + KiB 79 | assert.Equal(t, "1MiB", n.Floor().String()) 80 | n = GiB + MiB + KiB 81 | assert.Equal(t, "1GiB", n.Floor().String()) 82 | n = 3*GiB + 2*MiB + KiB 83 | assert.Equal(t, "3GiB", n.Floor().String()) 84 | } 85 | 86 | func TestBase2Round(t *testing.T) { 87 | var n Base2Bytes = KiB 88 | assert.Equal(t, "1KiB", n.Round(1).String()) 89 | n = MiB + KiB 90 | assert.Equal(t, "1MiB", n.Round(1).String()) 91 | n = GiB + MiB + KiB 92 | assert.Equal(t, "1GiB", n.Round(1).String()) 93 | n = 3*GiB + 2*MiB + KiB 94 | assert.Equal(t, "3GiB", n.Round(1).String()) 95 | n = KiB 96 | assert.Equal(t, "1KiB", n.Round(2).String()) 97 | n = MiB + KiB 98 | assert.Equal(t, "1MiB1KiB", n.Round(2).String()) 99 | n = GiB + MiB + KiB 100 | assert.Equal(t, "1GiB1MiB", n.Round(2).String()) 101 | n = 3*GiB + 2*MiB + KiB 102 | assert.Equal(t, "3GiB2MiB", n.Round(2).String()) 103 | } 104 | 105 | func TestMetricBytesString(t *testing.T) { 106 | assert.Equal(t, MetricBytes(0).String(), "0B") 107 | // TODO: SI standard prefix is lowercase "kB" 108 | assert.Equal(t, MetricBytes(1001).String(), "1KB1B") 109 | assert.Equal(t, MetricBytes(1001025).String(), "1MB1KB25B") 110 | } 111 | 112 | func TestParseMetricBytes(t *testing.T) { 113 | n, err := ParseMetricBytes("0B") 114 | assert.NoError(t, err) 115 | assert.Equal(t, 0, int(n)) 116 | n, err = ParseMetricBytes("1kB") 117 | assert.NoError(t, err) 118 | assert.Equal(t, 1000, int(n)) 119 | n, err = ParseMetricBytes("1KB1B") 120 | assert.NoError(t, err) 121 | assert.Equal(t, 1001, int(n)) 122 | n, err = ParseMetricBytes("1MB1KB25B") 123 | assert.NoError(t, err) 124 | assert.Equal(t, 1001025, int(n)) 125 | n, err = ParseMetricBytes("1.5MB") 126 | assert.NoError(t, err) 127 | assert.Equal(t, 1500000, int(n)) 128 | } 129 | 130 | func TestParseStrictBytes(t *testing.T) { 131 | n, err := ParseStrictBytes("0B") 132 | assert.NoError(t, err) 133 | assert.Equal(t, 0, int(n)) 134 | 135 | n, err = ParseStrictBytes("1kiB") 136 | assert.Error(t, err) 137 | n, err = ParseStrictBytes("1KiB") 138 | assert.NoError(t, err) 139 | assert.Equal(t, 1024, int(n)) 140 | n, err = ParseStrictBytes("1MiB1KiB25B") 141 | assert.NoError(t, err) 142 | assert.Equal(t, 1049625, int(n)) 143 | n, err = ParseStrictBytes("1.5MiB") 144 | assert.NoError(t, err) 145 | assert.Equal(t, 1572864, int(n)) 146 | 147 | n, err = ParseStrictBytes("0B") 148 | assert.NoError(t, err) 149 | assert.Equal(t, 0, int(n)) 150 | n, err = ParseStrictBytes("1kB") 151 | assert.NoError(t, err) 152 | assert.Equal(t, 1000, int(n)) 153 | n, err = ParseStrictBytes("1KB1B") 154 | assert.NoError(t, err) 155 | assert.Equal(t, 1001, int(n)) 156 | n, err = ParseStrictBytes("1MB1KB25B") 157 | assert.NoError(t, err) 158 | assert.Equal(t, 1001025, int(n)) 159 | n, err = ParseStrictBytes("1.5MB") 160 | assert.NoError(t, err) 161 | assert.Equal(t, 1500000, int(n)) 162 | } 163 | 164 | func TestMetricFloor(t *testing.T) { 165 | var n MetricBytes = KB 166 | assert.Equal(t, "1KB", n.Floor().String()) 167 | n = MB + KB 168 | assert.Equal(t, "1MB", n.Floor().String()) 169 | n = GB + MB + KB 170 | assert.Equal(t, "1GB", n.Floor().String()) 171 | n = 3*GB + 2*MB + KB 172 | assert.Equal(t, "3GB", n.Floor().String()) 173 | } 174 | 175 | func TestMetricRound(t *testing.T) { 176 | var n MetricBytes = KB 177 | assert.Equal(t, "1KB", n.Round(1).String()) 178 | n = MB + KB 179 | assert.Equal(t, "1MB", n.Round(1).String()) 180 | n = GB + MB + KB 181 | assert.Equal(t, "1GB", n.Round(1).String()) 182 | n = 3*GB + 2*MB + KB 183 | assert.Equal(t, "3GB", n.Round(1).String()) 184 | n = KB 185 | assert.Equal(t, "1KB", n.Round(2).String()) 186 | n = MB + KB 187 | assert.Equal(t, "1MB1KB", n.Round(2).String()) 188 | n = GB + MB + KB 189 | assert.Equal(t, "1GB1MB", n.Round(2).String()) 190 | n = 3*GB + 2*MB + KB 191 | assert.Equal(t, "3GB2MB", n.Round(2).String()) 192 | } 193 | 194 | func TestJSON(t *testing.T) { 195 | type args struct { 196 | b Base2Bytes 197 | } 198 | tests := []struct { 199 | name string 200 | args args 201 | }{ 202 | { 203 | name: "0B", 204 | args: args{ 205 | b: 0, 206 | }, 207 | }, 208 | { 209 | name: "1B", 210 | args: args{ 211 | b: 1, 212 | }, 213 | }, 214 | } 215 | for _, tt := range tests { 216 | t.Run(tt.name, func(t *testing.T) { 217 | data, err := json.Marshal(tt.args.b) 218 | assert.NoError(t, err) 219 | var b Base2Bytes 220 | err = json.Unmarshal(data, &b) 221 | assert.NoError(t, err) 222 | assert.Equal(t, tt.args.b.String(), b.String()) 223 | }) 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package units provides helpful unit multipliers and functions for Go. 2 | // 3 | // The goal of this package is to have functionality similar to the time [1] package. 4 | // 5 | // 6 | // [1] http://golang.org/pkg/time/ 7 | // 8 | // It allows for code like this: 9 | // 10 | // n, err := ParseBase2Bytes("1KB") 11 | // // n == 1024 12 | // n = units.Mebibyte * 512 13 | package units 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/alecthomas/units 2 | 3 | go 1.15 4 | 5 | require github.com/stretchr/testify v1.9.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 8 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 9 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 10 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 11 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 12 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 13 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 14 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 17 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 18 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 19 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 20 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "https://docs.renovatebot.com/renovate-schema.json", 3 | extends: [ 4 | "config:recommended", 5 | ":semanticCommits", 6 | ":semanticCommitTypeAll(chore)", 7 | ":semanticCommitScope(deps)", 8 | "group:allNonMajor", 9 | "schedule:earlyMondays", // Run once a week. 10 | ], 11 | postUpdateOptions: [ 12 | "gomodTidy", 13 | "gomodUpdateImportPaths" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /si.go: -------------------------------------------------------------------------------- 1 | package units 2 | 3 | // SI units. 4 | type SI int64 5 | 6 | // SI unit multiples. 7 | const ( 8 | Kilo SI = 1000 9 | Mega = Kilo * 1000 10 | Giga = Mega * 1000 11 | Tera = Giga * 1000 12 | Peta = Tera * 1000 13 | Exa = Peta * 1000 14 | ) 15 | 16 | func MakeUnitMap(suffix, shortSuffix string, scale int64) map[string]float64 { 17 | res := map[string]float64{ 18 | shortSuffix: 1, 19 | // see below for "k" / "K" 20 | "M" + suffix: float64(scale * scale), 21 | "G" + suffix: float64(scale * scale * scale), 22 | "T" + suffix: float64(scale * scale * scale * scale), 23 | "P" + suffix: float64(scale * scale * scale * scale * scale), 24 | "E" + suffix: float64(scale * scale * scale * scale * scale * scale), 25 | } 26 | 27 | // Standard SI prefixes use lowercase "k" for kilo = 1000. 28 | // For compatibility, and to be fool-proof, we accept both "k" and "K" in metric mode. 29 | // 30 | // However, official binary prefixes are always capitalized - "KiB" - 31 | // and we specifically never parse "kB" as 1024B because: 32 | // 33 | // (1) people pedantic enough to use lowercase according to SI unlikely to abuse "k" to mean 1024 :-) 34 | // 35 | // (2) Use of capital K for 1024 was an informal tradition predating IEC prefixes: 36 | // "The binary meaning of the kilobyte for 1024 bytes typically uses the symbol KB, with an 37 | // uppercase letter K." 38 | // -- https://en.wikipedia.org/wiki/Kilobyte#Base_2_(1024_bytes) 39 | // "Capitalization of the letter K became the de facto standard for binary notation, although this 40 | // could not be extended to higher powers, and use of the lowercase k did persist.[13][14][15]" 41 | // -- https://en.wikipedia.org/wiki/Binary_prefix#History 42 | // See also the extensive https://en.wikipedia.org/wiki/Timeline_of_binary_prefixes. 43 | if scale == 1024 { 44 | res["K"+suffix] = float64(scale) 45 | } else { 46 | res["k"+suffix] = float64(scale) 47 | res["K"+suffix] = float64(scale) 48 | } 49 | return res 50 | } 51 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package units 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | siUnits = []string{"", "K", "M", "G", "T", "P", "E"} 11 | ) 12 | 13 | func ToString(n int64, scale int64, suffix, baseSuffix string) string { 14 | mn := len(siUnits) 15 | out := make([]string, mn) 16 | for i, m := range siUnits { 17 | if n%scale != 0 || i == 0 && n == 0 { 18 | s := suffix 19 | if i == 0 { 20 | s = baseSuffix 21 | } 22 | out[mn-1-i] = fmt.Sprintf("%d%s%s", n%scale, m, s) 23 | } 24 | n /= scale 25 | if n == 0 { 26 | break 27 | } 28 | } 29 | return strings.Join(out, "") 30 | } 31 | 32 | // Below code ripped straight from http://golang.org/src/pkg/time/format.go?s=33392:33438#L1123 33 | var errLeadingInt = errors.New("units: bad [0-9]*") // never printed 34 | 35 | // leadingInt consumes the leading [0-9]* from s. 36 | func leadingInt(s string) (x int64, rem string, err error) { 37 | i := 0 38 | for ; i < len(s); i++ { 39 | c := s[i] 40 | if c < '0' || c > '9' { 41 | break 42 | } 43 | if x >= (1<<63-10)/10 { 44 | // overflow 45 | return 0, "", errLeadingInt 46 | } 47 | x = x*10 + int64(c) - '0' 48 | } 49 | return x, s[i:], nil 50 | } 51 | 52 | func ParseUnit(s string, unitMap map[string]float64) (int64, error) { 53 | // [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+ 54 | orig := s 55 | f := float64(0) 56 | neg := false 57 | 58 | // Consume [-+]? 59 | if s != "" { 60 | c := s[0] 61 | if c == '-' || c == '+' { 62 | neg = c == '-' 63 | s = s[1:] 64 | } 65 | } 66 | // Special case: if all that is left is "0", this is zero. 67 | if s == "0" { 68 | return 0, nil 69 | } 70 | if s == "" { 71 | return 0, errors.New("units: invalid " + orig) 72 | } 73 | for s != "" { 74 | g := float64(0) // this element of the sequence 75 | 76 | var x int64 77 | var err error 78 | 79 | // The next character must be [0-9.] 80 | if !(s[0] == '.' || ('0' <= s[0] && s[0] <= '9')) { 81 | return 0, errors.New("units: invalid " + orig) 82 | } 83 | // Consume [0-9]* 84 | pl := len(s) 85 | x, s, err = leadingInt(s) 86 | if err != nil { 87 | return 0, errors.New("units: invalid " + orig) 88 | } 89 | g = float64(x) 90 | pre := pl != len(s) // whether we consumed anything before a period 91 | 92 | // Consume (\.[0-9]*)? 93 | post := false 94 | if s != "" && s[0] == '.' { 95 | s = s[1:] 96 | pl := len(s) 97 | x, s, err = leadingInt(s) 98 | if err != nil { 99 | return 0, errors.New("units: invalid " + orig) 100 | } 101 | scale := 1.0 102 | for n := pl - len(s); n > 0; n-- { 103 | scale *= 10 104 | } 105 | g += float64(x) / scale 106 | post = pl != len(s) 107 | } 108 | if !pre && !post { 109 | // no digits (e.g. ".s" or "-.s") 110 | return 0, errors.New("units: invalid " + orig) 111 | } 112 | 113 | // Consume unit. 114 | i := 0 115 | for ; i < len(s); i++ { 116 | c := s[i] 117 | if c == '.' || ('0' <= c && c <= '9') { 118 | break 119 | } 120 | } 121 | u := s[:i] 122 | s = s[i:] 123 | unit, ok := unitMap[u] 124 | if !ok { 125 | return 0, errors.New("units: unknown unit " + u + " in " + orig) 126 | } 127 | 128 | f += g * unit 129 | } 130 | 131 | if neg { 132 | f = -f 133 | } 134 | if f < float64(-1<<63) || f > float64(1<<63-1) { 135 | return 0, errors.New("units: overflow parsing unit") 136 | } 137 | return int64(f), nil 138 | } 139 | --------------------------------------------------------------------------------