Browse Source

init

master v0.0.1
andrey1s 4 weeks ago
parent
commit
779818e067
  1. 15
      .drone.yml
  2. 50
      .golangci.yml
  3. 76
      example_test.go
  4. 3
      go.mod
  5. 357
      size.go
  6. 9
      size_json.go
  7. 190
      size_test.go
  8. 20
      size_text.go
  9. 55
      size_text_test.go

15
.drone.yml

@ -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

@ -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

@ -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"
}

3
go.mod

@ -0,0 +1,3 @@
module gitoa.ru/go-4devs/bytesize
go 1.22.5

357
size.go

@ -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

@ -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

@ -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)
}
}
}

20
size_text.go

@ -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

@ -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…
Cancel
Save