andrey1s
2 months ago
9 changed files with 775 additions and 0 deletions
@ -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 |
@ -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 |
@ -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"
|
|||
} |
@ -0,0 +1,3 @@ |
|||
module gitoa.ru/go-4devs/bytesize |
|||
|
|||
go 1.22.5 |
@ -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 |
|||
} |
@ -0,0 +1,9 @@ |
|||
package bytesize |
|||
|
|||
// func (s Size) MarshalJSON() ([]byte, error) {
|
|||
// return nil, nil
|
|||
// }
|
|||
|
|||
// func (s *Size) UnmarshalJSON([]byte) error {
|
|||
// return nil
|
|||
// }
|
@ -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", `"�"`}, // utf8.RuneError
|
|||
{"\uFFFD hello \uFFFD world", `"� hello � 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) |
|||
} |
|||
} |
|||
} |
@ -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 |
|||
} |
@ -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) |
|||
} |
|||
} |
|||
} |
Loading…
Reference in new issue