This commit is contained in:
15
.drone.yml
Normal file
15
.drone.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
name: default
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: test
|
||||||
|
image: golang
|
||||||
|
commands:
|
||||||
|
# - go test -parallel 10 -race ./...
|
||||||
|
- go test ./...
|
||||||
|
|
||||||
|
- name: golangci-lint
|
||||||
|
image: golangci/golangci-lint:v1.61
|
||||||
|
commands:
|
||||||
|
- golangci-lint run
|
||||||
50
.golangci.yml
Normal file
50
.golangci.yml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
linters-settings:
|
||||||
|
dupl:
|
||||||
|
threshold: 100
|
||||||
|
funlen:
|
||||||
|
lines: 100
|
||||||
|
statements: 60
|
||||||
|
goconst:
|
||||||
|
min-len: 2
|
||||||
|
min-occurrences: 2
|
||||||
|
gocyclo:
|
||||||
|
min-complexity: 15
|
||||||
|
golint:
|
||||||
|
min-confidence: 0
|
||||||
|
lll:
|
||||||
|
line-length: 140
|
||||||
|
maligned:
|
||||||
|
suggest-new: true
|
||||||
|
misspell:
|
||||||
|
locale: US
|
||||||
|
varnamelen:
|
||||||
|
min-name-length: 2
|
||||||
|
ignore-decls:
|
||||||
|
- w io.Writer
|
||||||
|
- t testing.T
|
||||||
|
- e error
|
||||||
|
- i int
|
||||||
|
- b bytes.Buffer
|
||||||
|
- h Handle
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable-all: true
|
||||||
|
disable:
|
||||||
|
- gochecknoglobals
|
||||||
|
- ireturn
|
||||||
|
- mnd
|
||||||
|
- nolintlint
|
||||||
|
# deprecated
|
||||||
|
- gomnd
|
||||||
|
- exportloopref
|
||||||
|
- execinquery
|
||||||
|
|
||||||
|
issues:
|
||||||
|
# Excluding configuration per-path, per-linter, per-text and per-source
|
||||||
|
exclude-rules:
|
||||||
|
- path: _test\.go
|
||||||
|
linters:
|
||||||
|
- gochecknoglobals
|
||||||
|
- depguard
|
||||||
|
- gosec
|
||||||
|
- errchkjson
|
||||||
76
example_test.go
Normal file
76
example_test.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package bytesize_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitoa.ru/go-4devs/bytesize"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleParse() {
|
||||||
|
size, err := bytesize.Parse("100kB")
|
||||||
|
fmt.Printf("100kB:%[1]T(%[1]d),%[2]v\n", size, err)
|
||||||
|
|
||||||
|
size, err = bytesize.Parse("100PB")
|
||||||
|
fmt.Printf("100PB:%[1]T(%[1]d),%[2]v\n", size, err)
|
||||||
|
|
||||||
|
size, err = bytesize.Parse("100KiB")
|
||||||
|
fmt.Printf("100PB:%[1]T(%[1]d),%[2]v\n", size, err)
|
||||||
|
|
||||||
|
size, err = bytesize.Parse("100MiB")
|
||||||
|
fmt.Printf("100PB:%[1]T(%[1]d),%[2]v\n", size, err)
|
||||||
|
|
||||||
|
size, err = bytesize.Parse("100B")
|
||||||
|
fmt.Printf("100PB:%[1]T(%[1]d),%[2]v\n", size, err)
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// 100kB:bytesize.Size(100000),<nil>
|
||||||
|
// 100PB:bytesize.Size(100000000000000000),<nil>
|
||||||
|
// 100PB:bytesize.Size(102400),<nil>
|
||||||
|
// 100PB:bytesize.Size(104857600),<nil>
|
||||||
|
// 100PB:bytesize.Size(100),<nil>
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleSize_String() {
|
||||||
|
fmt.Printf("%[1]T(%[1]d) = %[1]s\n", bytesize.Size(100000))
|
||||||
|
fmt.Printf("%[1]T(%[1]d) = %[1]s\n", bytesize.Size(100000000000000000))
|
||||||
|
fmt.Printf("%[1]T(%[1]d) = %[1]s\n", bytesize.Size(102400))
|
||||||
|
fmt.Printf("%[1]T(%[1]d) = %[1]s\n", bytesize.Size(104857600))
|
||||||
|
fmt.Printf("%[1]T(%[1]d) = %[1]s\n", bytesize.Size(100))
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// bytesize.Size(100000) = 100kB
|
||||||
|
// bytesize.Size(100000000000000000) = 100PB
|
||||||
|
// bytesize.Size(102400) = 102.4kB
|
||||||
|
// bytesize.Size(104857600) = 104.8576MB
|
||||||
|
// bytesize.Size(100) = 100B
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleSize_jsonMarshal() {
|
||||||
|
size := bytesize.Size(100000)
|
||||||
|
data, _ := json.Marshal(size)
|
||||||
|
fmt.Printf("%[1]T(%[1]d) = %[2]s\n", size, data)
|
||||||
|
|
||||||
|
size = bytesize.Size(100000000000000000)
|
||||||
|
data, _ = json.Marshal(size)
|
||||||
|
fmt.Printf("%[1]T(%[1]d) = %[2]s\n", size, data)
|
||||||
|
|
||||||
|
size = bytesize.Size(102400)
|
||||||
|
data, _ = json.Marshal(size)
|
||||||
|
fmt.Printf("%[1]T(%[1]d) = %[2]s\n", size, data)
|
||||||
|
|
||||||
|
size = bytesize.Size(104857600)
|
||||||
|
data, _ = json.Marshal(size)
|
||||||
|
fmt.Printf("%[1]T(%[1]d) = %[2]s\n", size, data)
|
||||||
|
|
||||||
|
size = bytesize.Size(100)
|
||||||
|
data, _ = json.Marshal(size)
|
||||||
|
fmt.Printf("%[1]T(%[1]d) = %[2]s\n", size, data)
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// bytesize.Size(100000) = "100kB"
|
||||||
|
// bytesize.Size(100000000000000000) = "100PB"
|
||||||
|
// bytesize.Size(102400) = "102.4kB"
|
||||||
|
// bytesize.Size(104857600) = "104.8576MB"
|
||||||
|
// bytesize.Size(100) = "100B"
|
||||||
|
}
|
||||||
357
size.go
Normal file
357
size.go
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
package bytesize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Size int64
|
||||||
|
|
||||||
|
// Byte size prefix.
|
||||||
|
const (
|
||||||
|
Byte Size = 1 // byte
|
||||||
|
Kibibyte Size = 1 << (10 * iota) // kibibyte
|
||||||
|
Mebibyte // mebibyte
|
||||||
|
Gibibyte // gibibyte
|
||||||
|
Tebibyte // tebibyte
|
||||||
|
Pebibyte // pebibyte
|
||||||
|
|
||||||
|
Kilobyte Size = 1000 * Byte // kilobyte
|
||||||
|
Megabyte Size = 1000 * Kilobyte // megabyte
|
||||||
|
Gigabyte Size = 1000 * Megabyte // gigabyte
|
||||||
|
Terabyte Size = 1000 * Gigabyte // terabyte
|
||||||
|
Petabyte Size = 1000 * Terabyte // petabyte
|
||||||
|
)
|
||||||
|
|
||||||
|
var unitMap = map[string]uint64{
|
||||||
|
"B": uint64(Byte),
|
||||||
|
"KiB": uint64(Kibibyte),
|
||||||
|
"MiB": uint64(Mebibyte),
|
||||||
|
"GiB": uint64(Gibibyte),
|
||||||
|
"TiB": uint64(Tebibyte),
|
||||||
|
"PiB": uint64(Pebibyte),
|
||||||
|
|
||||||
|
"kB": uint64(Kilobyte),
|
||||||
|
"KB": uint64(Kilobyte),
|
||||||
|
"MB": uint64(Megabyte),
|
||||||
|
"GB": uint64(Gigabyte),
|
||||||
|
"TB": uint64(Terabyte),
|
||||||
|
"PB": uint64(Petabyte),
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidSize = errors.New("bytesize: invalid size")
|
||||||
|
ErrMissingUnit = errors.New("bytesize: missing unit")
|
||||||
|
ErrUnknownUnit = errors.New("bytesize: unknown unit")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parse parses a size string.
|
||||||
|
// A byte size string is a possibly signed sequence of
|
||||||
|
// decimal numbers, each with optional fraction and a unit suffix,
|
||||||
|
// such as "300kB", "-1.5GiB" or "2GB45MB".
|
||||||
|
// Valid units are "B", "KiB", "MiB", "GiB", "TiB", "PiB" for binary size.
|
||||||
|
// Valid units are "B", "kB", "MB", "GB", "TB", "PB" for human size.
|
||||||
|
//
|
||||||
|
// nolint: funlen,gocognit,gocyclo,cyclop
|
||||||
|
func Parse(in string) (Size, error) {
|
||||||
|
// [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+
|
||||||
|
orig := in
|
||||||
|
neg := false
|
||||||
|
|
||||||
|
var res uint64
|
||||||
|
|
||||||
|
// Consume [-+]?
|
||||||
|
if in != "" {
|
||||||
|
c := in[0]
|
||||||
|
if c == '-' || c == '+' {
|
||||||
|
neg = c == '-'
|
||||||
|
in = in[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Special case: if all that is left is "0", this is zero.
|
||||||
|
if in == "0" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if in == "" {
|
||||||
|
return 0, fmt.Errorf("%w %q", ErrInvalidSize, orig)
|
||||||
|
}
|
||||||
|
|
||||||
|
for in != "" {
|
||||||
|
var (
|
||||||
|
val, fVal uint64 // integers before, after decimal point
|
||||||
|
scale float64 = 1 // value = v + f/scale
|
||||||
|
)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// The next character must be [0-9.]
|
||||||
|
if !(in[0] == '.' || '0' <= in[0] && in[0] <= '9') {
|
||||||
|
return 0, fmt.Errorf("%w %q", ErrInvalidSize, orig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume [0-9]*
|
||||||
|
pl := len(in)
|
||||||
|
|
||||||
|
val, in, err = leadingInt(in)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("%w %q", ErrInvalidSize, orig)
|
||||||
|
}
|
||||||
|
|
||||||
|
pre := pl != len(in) // whether we consumed anything before a period
|
||||||
|
|
||||||
|
// Consume (\.[0-9]*)?
|
||||||
|
post := false
|
||||||
|
|
||||||
|
if in != "" && in[0] == '.' {
|
||||||
|
in = in[1:]
|
||||||
|
pl := len(in)
|
||||||
|
fVal, scale, in = leadingFraction(in)
|
||||||
|
post = pl != len(in)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pre && !post {
|
||||||
|
// no digits (e.g. ".s" or "-.s")
|
||||||
|
return 0, fmt.Errorf("%w %q", ErrInvalidSize, orig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume unit.
|
||||||
|
i := 0
|
||||||
|
for ; i < len(in); i++ {
|
||||||
|
c := in[i]
|
||||||
|
if c == '.' || '0' <= c && c <= '9' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
return 0, fmt.Errorf("%w %q", ErrMissingUnit, orig)
|
||||||
|
}
|
||||||
|
|
||||||
|
u := in[:i]
|
||||||
|
in = in[i:]
|
||||||
|
unit, ok := unitMap[u]
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return 0, fmt.Errorf("%w %s in size %q", ErrMissingUnit, u, orig)
|
||||||
|
}
|
||||||
|
|
||||||
|
if val > 1<<63/unit {
|
||||||
|
// overflow
|
||||||
|
return 0, fmt.Errorf("%w %q", ErrInvalidSize, orig)
|
||||||
|
}
|
||||||
|
|
||||||
|
val *= unit
|
||||||
|
if fVal > 0 {
|
||||||
|
// float64 is needed to be nanosecond accurate for fractions of hours.
|
||||||
|
// v >= 0 && (f*unit/scale) <= 3.6e+12 (ns/h, h is the largest unit)
|
||||||
|
val += uint64(float64(fVal) * (float64(unit) / scale))
|
||||||
|
if val > 1<<63 {
|
||||||
|
// overflow
|
||||||
|
return 0, fmt.Errorf("%w %q", ErrInvalidSize, orig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res += val
|
||||||
|
if res > 1<<63 {
|
||||||
|
return 0, fmt.Errorf("%w %q", ErrInvalidSize, orig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if neg {
|
||||||
|
return -Size(res), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if res > 1<<63-1 {
|
||||||
|
return 0, fmt.Errorf("%w %q", ErrInvalidSize, orig)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Size(res), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// leadingFraction consumes the leading [0-9]* from s.
|
||||||
|
// It is used only for fractions, so does not return an error on overflow,
|
||||||
|
// it just stops accumulating precision.
|
||||||
|
func leadingFraction(in string) (uint64, float64, string) {
|
||||||
|
i := 0
|
||||||
|
scale := float64(1)
|
||||||
|
overflow := false
|
||||||
|
|
||||||
|
var res uint64
|
||||||
|
|
||||||
|
for ; i < len(in); i++ {
|
||||||
|
cur := in[i]
|
||||||
|
if cur < '0' || cur > '9' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if overflow {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if res > (1<<63-1)/10 {
|
||||||
|
// It's possible for overflow to give a positive number, so take care.
|
||||||
|
overflow = true
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pres := res*10 + uint64(cur) - '0'
|
||||||
|
if pres > 1<<63 {
|
||||||
|
overflow = true
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
res = pres
|
||||||
|
scale *= 10
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, scale, in[i:]
|
||||||
|
}
|
||||||
|
|
||||||
|
var errLeadingInt = errors.New("bytesize: bad [0-9]*")
|
||||||
|
|
||||||
|
// leadingInt consumes the leading [0-9]* from in.
|
||||||
|
func leadingInt[bytes []byte | string](in bytes) (uint64, bytes, error) {
|
||||||
|
i := 0
|
||||||
|
|
||||||
|
var (
|
||||||
|
res uint64
|
||||||
|
rem bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
for ; i < len(in); i++ {
|
||||||
|
curr := in[i]
|
||||||
|
|
||||||
|
if curr < '0' || curr > '9' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if res > 1<<63/10 {
|
||||||
|
// overflow
|
||||||
|
return 0, rem, errLeadingInt
|
||||||
|
}
|
||||||
|
|
||||||
|
res = res*10 + uint64(curr) - '0'
|
||||||
|
if res > 1<<63 {
|
||||||
|
// overflow
|
||||||
|
return 0, rem, errLeadingInt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, in[i:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Size) String() string {
|
||||||
|
var arr [32]byte
|
||||||
|
n := s.format(&arr)
|
||||||
|
|
||||||
|
return string(arr[n:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// format formats the representation of d into the end of buf and
|
||||||
|
// returns the offset of the first character.
|
||||||
|
// nolint: gosec
|
||||||
|
func (s Size) format(buf *[32]byte) int {
|
||||||
|
wLen := len(buf)
|
||||||
|
|
||||||
|
data := uint64(s)
|
||||||
|
neg := s < 0
|
||||||
|
|
||||||
|
if neg {
|
||||||
|
data = -data
|
||||||
|
}
|
||||||
|
|
||||||
|
prec := 3
|
||||||
|
wLen--
|
||||||
|
buf[wLen] = 'B'
|
||||||
|
wLen--
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case data == 0:
|
||||||
|
buf[wLen] = '0'
|
||||||
|
|
||||||
|
return wLen
|
||||||
|
case data < uint64(Kilobyte):
|
||||||
|
// print bytes
|
||||||
|
wLen++
|
||||||
|
prec = 0
|
||||||
|
case data < uint64(Megabyte):
|
||||||
|
// print kipobytes
|
||||||
|
buf[wLen] = 'k'
|
||||||
|
case data < uint64(Gigabyte):
|
||||||
|
// print megabytes
|
||||||
|
prec = 6
|
||||||
|
buf[wLen] = 'M'
|
||||||
|
case data < uint64(Terabyte):
|
||||||
|
// print gigabytes
|
||||||
|
prec = 9
|
||||||
|
buf[wLen] = 'G'
|
||||||
|
case data < uint64(Petabyte):
|
||||||
|
// print terabytes
|
||||||
|
prec = 12
|
||||||
|
buf[wLen] = 'T'
|
||||||
|
default:
|
||||||
|
// print petobytes
|
||||||
|
prec = 15
|
||||||
|
buf[wLen] = 'P'
|
||||||
|
}
|
||||||
|
|
||||||
|
wLen, data = fmtFrac(buf[:wLen], data, prec)
|
||||||
|
wLen = fmtInt(buf[:wLen], data)
|
||||||
|
|
||||||
|
if neg {
|
||||||
|
wLen--
|
||||||
|
buf[wLen] = '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
return wLen
|
||||||
|
}
|
||||||
|
|
||||||
|
// fmtFrac formats the fraction of v/10**prec (e.g., ".12345") into the
|
||||||
|
// tail of buf, omitting trailing zeros. It omits the decimal
|
||||||
|
// point too when the fraction is 0. It returns the index where the
|
||||||
|
// output bytes begin and the value v/10**prec.
|
||||||
|
func fmtFrac(buf []byte, val uint64, prec int) (int, uint64) {
|
||||||
|
// Omit trailing zeros up to and including decimal point.
|
||||||
|
wLen := len(buf)
|
||||||
|
show := false
|
||||||
|
|
||||||
|
for range prec {
|
||||||
|
digit := val % 10
|
||||||
|
show = show || digit != 0
|
||||||
|
|
||||||
|
if show {
|
||||||
|
wLen--
|
||||||
|
buf[wLen] = byte(digit) + '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
val /= 10
|
||||||
|
}
|
||||||
|
|
||||||
|
if show {
|
||||||
|
wLen--
|
||||||
|
buf[wLen] = '.'
|
||||||
|
}
|
||||||
|
|
||||||
|
return wLen, val
|
||||||
|
}
|
||||||
|
|
||||||
|
// fmtInt formats v into the tail of buf.
|
||||||
|
// It returns the index where the output begins.
|
||||||
|
func fmtInt(buf []byte, val uint64) int {
|
||||||
|
wLen := len(buf)
|
||||||
|
if val == 0 {
|
||||||
|
wLen--
|
||||||
|
buf[wLen] = '0'
|
||||||
|
} else {
|
||||||
|
for val > 0 {
|
||||||
|
wLen--
|
||||||
|
buf[wLen] = byte(val%10) + '0'
|
||||||
|
val /= 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return wLen
|
||||||
|
}
|
||||||
9
size_json.go
Normal file
9
size_json.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package bytesize
|
||||||
|
|
||||||
|
// func (s Size) MarshalJSON() ([]byte, error) {
|
||||||
|
// return nil, nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (s *Size) UnmarshalJSON([]byte) error {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
190
size_test.go
Normal file
190
size_test.go
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
package bytesize_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitoa.ru/go-4devs/bytesize"
|
||||||
|
)
|
||||||
|
|
||||||
|
var parseTests = []struct {
|
||||||
|
in string
|
||||||
|
want bytesize.Size
|
||||||
|
}{
|
||||||
|
// simple
|
||||||
|
{"0", 0},
|
||||||
|
{"5KiB", 5 * bytesize.Kibibyte},
|
||||||
|
{"30KiB", 30 * bytesize.Kibibyte},
|
||||||
|
{"1478KiB", 1478 * bytesize.Kibibyte},
|
||||||
|
// sign
|
||||||
|
{"-5KiB", -5 * bytesize.Kibibyte},
|
||||||
|
{"+5KiB", 5 * bytesize.Kibibyte},
|
||||||
|
{"-0", 0},
|
||||||
|
{"+0", 0},
|
||||||
|
// decimal
|
||||||
|
{"5.0KiB", 5 * bytesize.Kibibyte},
|
||||||
|
{"5.6KiB", 5*bytesize.Kibibyte + bytesize.Kibibyte*6/10},
|
||||||
|
{"5.KiB", 5 * bytesize.Kibibyte},
|
||||||
|
{".5KiB", bytesize.Kibibyte / 2},
|
||||||
|
{"1.0KiB", 1 * bytesize.Kibibyte},
|
||||||
|
{"1.00KiB", 1 * bytesize.Kibibyte},
|
||||||
|
{"1.004KiB", 1*bytesize.Kibibyte + 4*bytesize.Byte},
|
||||||
|
{"1.0040KiB", 1*bytesize.Kibibyte + 4*bytesize.Byte},
|
||||||
|
{"100.00100KiB", 100*bytesize.Kibibyte + 1*bytesize.Byte},
|
||||||
|
// different units
|
||||||
|
{"10B", 10 * bytesize.Byte},
|
||||||
|
{"11KiB", 11 * bytesize.Kibibyte},
|
||||||
|
{"12MiB", 12 * bytesize.Mebibyte},
|
||||||
|
{"12GiB", 12 * bytesize.Gibibyte},
|
||||||
|
{"13TiB", 13 * bytesize.Tebibyte},
|
||||||
|
{"14PiB", 14 * bytesize.Pebibyte},
|
||||||
|
// composite durations
|
||||||
|
{"3PiB30TiB", 3*bytesize.Pebibyte + 30*bytesize.Tebibyte},
|
||||||
|
{"10.5MiB4TiB", 4*bytesize.Tebibyte + 10*bytesize.Mebibyte + bytesize.Mebibyte/2},
|
||||||
|
{"-2TiB3.4MiB", -(2*bytesize.Tebibyte + 3*bytesize.Mebibyte + bytesize.Mebibyte*4/10)},
|
||||||
|
{
|
||||||
|
"1PiB2TiB3GiB4MiB5KiB6B",
|
||||||
|
1*bytesize.Pebibyte + 2*bytesize.Tebibyte + 3*bytesize.Gibibyte + 4*bytesize.Mebibyte + 5*bytesize.Kibibyte + 6*bytesize.Byte,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"39PiB9TiB14.425GiB",
|
||||||
|
39*bytesize.Pebibyte + 9*bytesize.Tebibyte + 14*bytesize.Gibibyte + bytesize.Gibibyte*425/1000,
|
||||||
|
},
|
||||||
|
// large value
|
||||||
|
{"52763797000B", 52763797000 * bytesize.Byte},
|
||||||
|
// more than 9 digits after decimal point
|
||||||
|
{"0.3333333333333333333GiB", bytesize.Gibibyte * 3333333333 / 1e10},
|
||||||
|
// 9007199254740993 = 1<<53+1 cannot be stored precisely in a float64
|
||||||
|
{"9007199254740993B", (1<<53 + 1) * bytesize.Byte},
|
||||||
|
// largest duration that can be represented by int64 in nanoseconds
|
||||||
|
{"9223372036854775807B", (1<<63 - 1) * bytesize.Byte},
|
||||||
|
{"-9223372036854775808B", -1 << 63 * bytesize.Byte},
|
||||||
|
// largest negative value
|
||||||
|
{"-9223372036854775808B", -1 << 63 * bytesize.Byte},
|
||||||
|
{"0.100000000000000000000MiB", bytesize.Mebibyte * 1 / 10},
|
||||||
|
|
||||||
|
// simple
|
||||||
|
{"5GB", 5 * bytesize.Gigabyte},
|
||||||
|
{"30GB", 30 * bytesize.Gigabyte},
|
||||||
|
{"1478GB", 1478 * bytesize.Gigabyte},
|
||||||
|
// sign
|
||||||
|
{"-5GB", -5 * bytesize.Gigabyte},
|
||||||
|
{"+5GB", 5 * bytesize.Gigabyte},
|
||||||
|
// decimal
|
||||||
|
{"5.0GB", 5 * bytesize.Gigabyte},
|
||||||
|
{"5.6GB", 5*bytesize.Gigabyte + 600*bytesize.Megabyte},
|
||||||
|
{"5.GB", 5 * bytesize.Gigabyte},
|
||||||
|
{".5GB", 500 * bytesize.Megabyte},
|
||||||
|
{"1.0GB", 1 * bytesize.Gigabyte},
|
||||||
|
{"1.00GB", 1 * bytesize.Gigabyte},
|
||||||
|
{"1.004GB", 1*bytesize.Gigabyte + 4*bytesize.Megabyte},
|
||||||
|
{"1.0040GB", 1*bytesize.Gigabyte + 4*bytesize.Megabyte},
|
||||||
|
{"100.00100GB", 100*bytesize.Gigabyte + 1*bytesize.Megabyte},
|
||||||
|
// different units
|
||||||
|
{"10B", 10 * bytesize.Byte},
|
||||||
|
{"11kB", 11 * bytesize.Kilobyte},
|
||||||
|
{"12MB", 12 * bytesize.Megabyte},
|
||||||
|
{"12GB", 12 * bytesize.Gigabyte},
|
||||||
|
{"13TB", 13 * bytesize.Terabyte},
|
||||||
|
{"14PB", 14 * bytesize.Petabyte},
|
||||||
|
// composite durations
|
||||||
|
{"3GB30MB", 3*bytesize.Gigabyte + 30*bytesize.Megabyte},
|
||||||
|
{"10.5MB4GB", 4*bytesize.Gigabyte + 10*bytesize.Megabyte + 500*bytesize.Kilobyte},
|
||||||
|
{"-2TB3.4GB", -(2*bytesize.Terabyte + 3*bytesize.Gigabyte + 400*bytesize.Megabyte)},
|
||||||
|
{
|
||||||
|
"1PB2TB3GB4MB5kB6B",
|
||||||
|
1*bytesize.Petabyte + 2*bytesize.Terabyte + 3*bytesize.Gigabyte + 4*bytesize.Megabyte + 5*bytesize.Kilobyte + 6*bytesize.Byte,
|
||||||
|
},
|
||||||
|
{"39PB9TB14.425GB", 39*bytesize.Petabyte + 9*bytesize.Terabyte + 14*bytesize.Gigabyte + 425*bytesize.Megabyte},
|
||||||
|
{"9223372036854775.807kB", (1<<63 - 1) * bytesize.Byte},
|
||||||
|
{"9223372036GB854MB775kB807B", (1<<63 - 1) * bytesize.Byte},
|
||||||
|
{"-9223372036854775.808kB", -1 << 63 * bytesize.Byte},
|
||||||
|
{"-9223372036GB854MB775kB808B", -1 << 63 * bytesize.Byte},
|
||||||
|
// largest negative round trip value
|
||||||
|
{"-9223372036GB854MB775.808kB", -1 << 63 * bytesize.Byte},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParse(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, tc := range parseTests {
|
||||||
|
d, err := bytesize.Parse(tc.in)
|
||||||
|
if err != nil || d != tc.want {
|
||||||
|
t.Errorf("Parse(%q) = %v, %v, want %v, nil", tc.in, d, err, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var parseErrorTests = []struct {
|
||||||
|
in string
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
// invalid
|
||||||
|
{"", `""`},
|
||||||
|
{"3", `"3"`},
|
||||||
|
{"-", `"-"`},
|
||||||
|
{"kB", `"kB"`},
|
||||||
|
{".", `"."`},
|
||||||
|
{"-.", `"-."`},
|
||||||
|
{".MB", `".MB"`},
|
||||||
|
{"+.MB", `"+.MB"`},
|
||||||
|
{"1ExB", `"1ExB"`},
|
||||||
|
{"\x85\x85", `"\x85\x85"`},
|
||||||
|
{"\xffff", `"\xffff"`},
|
||||||
|
{"hello \xffff world", `"hello \xffff world"`},
|
||||||
|
{"\uFFFD", `"<22>"`}, // utf8.RuneError
|
||||||
|
{"\uFFFD hello \uFFFD world", `"<22> hello <20> world"`}, // utf8.RuneError
|
||||||
|
// overflow
|
||||||
|
{"9223372036854775810B", `"9223372036854775810B"`},
|
||||||
|
{"9223372036854775808B", `"9223372036854775808B"`},
|
||||||
|
{"-9223372036854775809B", `"-9223372036854775809B"`},
|
||||||
|
{"9223372036854776kB", `"9223372036854776kB"`},
|
||||||
|
{"3000000PB", `"3000000PB"`},
|
||||||
|
{"9223372036854775.808KiB", `"9223372036854775.808KiB"`},
|
||||||
|
{"9223372036854MB775kB808B", `"9223372036854MB775kB808B"`},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseErrors(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, tc := range parseErrorTests {
|
||||||
|
_, err := bytesize.Parse(tc.in)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Parse(%q) = _, nil, want _, non-nil", tc.in)
|
||||||
|
} else if !strings.Contains(err.Error(), tc.expect) {
|
||||||
|
t.Errorf("Parse(%q) = _, %q, error does not contain %q", tc.in, err, tc.expect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseRoundTrip(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
max0 := bytesize.Size(math.MaxInt64)
|
||||||
|
max1, err := bytesize.Parse(max0.String())
|
||||||
|
|
||||||
|
if err != nil || max0 != max1 {
|
||||||
|
t.Errorf("round-trip failed: %d => %q => %d, %v", max0, max0.String(), max1, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
min0 := bytesize.Size(math.MinInt64)
|
||||||
|
min1, err := bytesize.Parse(min0.String())
|
||||||
|
|
||||||
|
if err != nil || min0 != min1 {
|
||||||
|
t.Errorf("round-trip failed: %d => %q => %d, %v", min0, min0.String(), min1, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for range 100 {
|
||||||
|
// Resolutions finer than milliseconds will result in
|
||||||
|
// imprecise round-trips.
|
||||||
|
d0 := bytesize.Size(rand.Int31()) * bytesize.Megabyte
|
||||||
|
s := d0.String()
|
||||||
|
d1, err := bytesize.Parse(s)
|
||||||
|
|
||||||
|
if err != nil || d0 != d1 {
|
||||||
|
t.Errorf("round-trip failed: %d => %q => %d, %v", d0, s, d1, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
size_text.go
Normal file
20
size_text.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package bytesize
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func (s Size) MarshalText() ([]byte, error) {
|
||||||
|
val := s.String()
|
||||||
|
|
||||||
|
return []byte(val), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Size) UnmarshalText(text []byte) error {
|
||||||
|
val, err := Parse(string(text))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: unmarshal text", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*s = val
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
55
size_text_test.go
Normal file
55
size_text_test.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package bytesize_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitoa.ru/go-4devs/bytesize"
|
||||||
|
)
|
||||||
|
|
||||||
|
var marshalTextTests = []struct {
|
||||||
|
in bytesize.Size
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{bytesize.Size(0), `"0B"`},
|
||||||
|
{bytesize.Size(100), `"100B"`},
|
||||||
|
{bytesize.Size(1024), `"1.024kB"`},
|
||||||
|
{bytesize.Size(1024 * 1024), `"1.048576MB"`},
|
||||||
|
{bytesize.Size(1024 * 1024 * 1024), `"1.073741824GB"`},
|
||||||
|
{bytesize.Size(1024 * 1024 * 1024 * 1024), `"1.099511627776TB"`},
|
||||||
|
{bytesize.Size(math.MaxInt64), `"9223.372036854775807PB"`},
|
||||||
|
{bytesize.Size(1000), `"1kB"`},
|
||||||
|
{bytesize.Size(1000 * 1000), `"1MB"`},
|
||||||
|
{bytesize.Size(1000 * 1000 * 1000), `"1GB"`},
|
||||||
|
{bytesize.Size(1000 * 1000 * 1000 * 1000), `"1TB"`},
|
||||||
|
{bytesize.Size(1000 * 1000 * 1000 * 1000 * 1000), `"1PB"`},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalText(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, tc := range marshalTextTests {
|
||||||
|
data, err := json.Marshal(tc.in)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("json.Marshal(%q) = _, err:%q", tc.in, err)
|
||||||
|
} else if string(data) != tc.expect {
|
||||||
|
t.Errorf("json.Marshal(%q) = %q, data does not equals %q", tc.in, data, tc.expect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalText(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, tc := range marshalTextTests {
|
||||||
|
var data bytesize.Size
|
||||||
|
err := json.Unmarshal([]byte(tc.expect), &data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("json.Unmarshal(%q) = _, err:%q", tc.in, err)
|
||||||
|
} else if data != tc.in {
|
||||||
|
t.Errorf("json.Unmarshal(%q) = %q, data does not equals %q", tc.in, data, tc.expect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user