This commit is contained in:
24
.drone.yml
Normal file
24
.drone.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
kind: pipeline
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: golangci-lint
|
||||
image: golangci/golangci-lint:v1.49
|
||||
volumes:
|
||||
- name: deps
|
||||
path: /go/src/mod
|
||||
commands:
|
||||
- golangci-lint run --timeout 5m
|
||||
|
||||
- name: test
|
||||
image: golang
|
||||
volumes:
|
||||
- name: deps
|
||||
path: /go/src/mod
|
||||
commands:
|
||||
- go test ./...
|
||||
|
||||
volumes:
|
||||
- name: deps
|
||||
temp: {}
|
||||
|
||||
51
.golangci.yml
Normal file
51
.golangci.yml
Normal file
@@ -0,0 +1,51 @@
|
||||
linters-settings:
|
||||
dupl:
|
||||
threshold: 100
|
||||
funlen:
|
||||
lines: 100
|
||||
statements: 50
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 2
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
golint:
|
||||
min-confidence: 0
|
||||
govet:
|
||||
check-shadowing: true
|
||||
lll:
|
||||
line-length: 140
|
||||
maligned:
|
||||
suggest-new: true
|
||||
misspell:
|
||||
locale: US
|
||||
varnamelen:
|
||||
min-name-length: 2
|
||||
|
||||
linters:
|
||||
enable-all: true
|
||||
disable:
|
||||
- maligned
|
||||
- exhaustivestruct
|
||||
- golint
|
||||
- structcheck
|
||||
- varcheck
|
||||
- interfacer
|
||||
- deadcode
|
||||
- nosnakecase
|
||||
- scopelint
|
||||
- ifshort
|
||||
- rowserrcheck
|
||||
- sqlclosecheck
|
||||
- wastedassign
|
||||
|
||||
- varnamelen
|
||||
- wsl
|
||||
- nonamedreturns
|
||||
- nlreturn
|
||||
- gomnd
|
||||
- gochecknoglobals
|
||||
- funlen
|
||||
- gocognit
|
||||
- cyclop
|
||||
- gocyclo
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) <year> <copyright holders>
|
||||
Copyright (c) 2022 4devs
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
# iso8601
|
||||
|
||||
[](https://drone.gitoa.ru/go-4devs/iso8601)
|
||||
[](https://goreportcard.com/report/gitoa.ru/go-4devs/iso8601)
|
||||
[](http://godoc.org/gitoa.ru/go-4devs/iso8601)
|
||||
|
||||
A fast ISO8601 duration parse and format for Go.
|
||||
|
||||
382
duration.go
Normal file
382
duration.go
Normal file
@@ -0,0 +1,382 @@
|
||||
package iso8601
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidDuration = errors.New("invalid duration")
|
||||
ErrMissingUnit = errors.New("missing unit")
|
||||
ErrUnknownUnit = errors.New("unknown unit")
|
||||
ErrOverflow = errors.New("overflow")
|
||||
ErrLeadingInt = errors.New("leading int")
|
||||
)
|
||||
|
||||
// P(n)Y(n)M(n)DT(n)H(n)M(n)S.
|
||||
var (
|
||||
defaultOption = duration{
|
||||
from: time.Now,
|
||||
}
|
||||
units = map[string]func(from time.Time, v uint64, scale float64) uint64{
|
||||
"M": month,
|
||||
"Y": year,
|
||||
}
|
||||
dateUnit = sampleUnits{
|
||||
"D": uint64(time.Hour * 24),
|
||||
}
|
||||
timeUnits = sampleUnits{
|
||||
"S": uint64(time.Second),
|
||||
"M": uint64(time.Minute),
|
||||
"H": uint64(time.Hour),
|
||||
}
|
||||
)
|
||||
|
||||
func From(from func() time.Time) Option {
|
||||
return func(d *duration) {
|
||||
d.from = from
|
||||
}
|
||||
}
|
||||
|
||||
type Option func(*duration)
|
||||
|
||||
// ParseDuration parses a duration string format P(n)Y(n)M(n)DT(n)H(n)M(n)S.
|
||||
// use iso8601.From(time) when using the month and year, by default time.Now().
|
||||
func ParseDuration(s string, opts ...Option) (time.Duration, error) {
|
||||
option := defaultOption
|
||||
for _, opt := range opts {
|
||||
opt(&option)
|
||||
}
|
||||
|
||||
orig := s
|
||||
var d uint64
|
||||
neg := false
|
||||
|
||||
// Consume [-+]?
|
||||
if s != "" {
|
||||
c := s[0]
|
||||
if c == '-' || c == '+' {
|
||||
neg = c == '-'
|
||||
s = s[1:]
|
||||
}
|
||||
}
|
||||
|
||||
if s == "" {
|
||||
return 0, fmt.Errorf("iso8601: empty %w %q", ErrInvalidDuration, orig)
|
||||
}
|
||||
|
||||
if s[0] != 'P' {
|
||||
return 0, fmt.Errorf("iso8601: format %w %q", ErrInvalidDuration, orig)
|
||||
}
|
||||
|
||||
s = s[1:]
|
||||
unit := option.unit
|
||||
|
||||
for s != "" {
|
||||
var (
|
||||
v, f uint64 // integers before, after decimal point
|
||||
scale float64 = 1 // value = v + f/scale
|
||||
)
|
||||
|
||||
var err error
|
||||
|
||||
if s != "" && s[0] == 'T' {
|
||||
s = s[1:]
|
||||
unit = timeUnits.unit
|
||||
}
|
||||
|
||||
// The next character must be [0-9.]
|
||||
if !(s[0] == '.' || '0' <= s[0] && s[0] <= '9') {
|
||||
return 0, fmt.Errorf("iso8601: next character %w %q", ErrInvalidDuration, orig)
|
||||
}
|
||||
|
||||
// Consume [0-9]*
|
||||
pl := len(s)
|
||||
v, s, err = leadingInt(s)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("iso8601: leadingInt %w %q", ErrInvalidDuration, orig)
|
||||
}
|
||||
pre := pl != len(s) // whether we consumed anything before a period
|
||||
|
||||
// Consume (\.[0-9]*)?
|
||||
post := false
|
||||
if s != "" && s[0] == '.' {
|
||||
s = s[1:]
|
||||
pl := len(s)
|
||||
f, scale, s = leadingFraction(s)
|
||||
post = pl != len(s)
|
||||
}
|
||||
|
||||
if !pre && !post {
|
||||
// no digits (e.g. ".s" or "-.s")
|
||||
return 0, fmt.Errorf("iso8601: leadingFraction %w %q", ErrInvalidDuration, orig)
|
||||
}
|
||||
|
||||
// Consume unit.
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if c == '.' || '0' <= c && c <= '9' || c == 'T' {
|
||||
break
|
||||
}
|
||||
}
|
||||
if i == 0 {
|
||||
return 0, fmt.Errorf("iso8601: %w %q", ErrMissingUnit, orig)
|
||||
}
|
||||
u := s[:i]
|
||||
s = s[i:]
|
||||
|
||||
v, err = unit(u, v, 0)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("iso8601: %w unit %q", err, orig)
|
||||
}
|
||||
|
||||
if f > 0 {
|
||||
r, err := unit(u, f, scale)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("iso8601: %w fraction %q", err, orig)
|
||||
}
|
||||
log.Println(u, f, scale, r)
|
||||
|
||||
v += r
|
||||
}
|
||||
|
||||
if d > 1<<63-v {
|
||||
return 0, fmt.Errorf("iso8601: 1<<63 %w %q", ErrOverflow, orig)
|
||||
}
|
||||
d += v
|
||||
}
|
||||
|
||||
if neg {
|
||||
return -time.Duration(d), nil
|
||||
}
|
||||
|
||||
if d > 1<<63-1 {
|
||||
return 0, fmt.Errorf("iso8601: %w %q", ErrOverflow, orig)
|
||||
}
|
||||
|
||||
return time.Duration(d), nil
|
||||
}
|
||||
|
||||
// leadingInt consumes the leading [0-9]* from s.
|
||||
func leadingInt(s string) (x uint64, rem string, err error) {
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if c < '0' || c > '9' {
|
||||
break
|
||||
}
|
||||
if x > 1<<63/10 {
|
||||
// overflow
|
||||
return 0, "", ErrLeadingInt
|
||||
}
|
||||
x = x*10 + uint64(c) - '0'
|
||||
if x > 1<<63 {
|
||||
// overflow
|
||||
return 0, "", ErrLeadingInt
|
||||
}
|
||||
}
|
||||
return x, s[i:], 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(s string) (x uint64, scale float64, rem string) {
|
||||
i := 0
|
||||
scale = 1
|
||||
overflow := false
|
||||
for ; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if c < '0' || c > '9' {
|
||||
break
|
||||
}
|
||||
if overflow {
|
||||
continue
|
||||
}
|
||||
if x > (1<<63-1)/10 {
|
||||
// It's possible for overflow to give a positive number, so take care.
|
||||
overflow = true
|
||||
continue
|
||||
}
|
||||
y := x*10 + uint64(c) - '0'
|
||||
if y > 1<<63 {
|
||||
overflow = true
|
||||
continue
|
||||
}
|
||||
x = y
|
||||
scale *= 10
|
||||
}
|
||||
return x, scale, s[i:]
|
||||
}
|
||||
|
||||
func month(from time.Time, v uint64, scale float64) uint64 {
|
||||
if scale == 0 {
|
||||
return uint64(from.AddDate(0, int(v), 0).Sub(from))
|
||||
}
|
||||
|
||||
return uint64(float64(v) * (float64(from.AddDate(0, 1, 0).Sub(from)) / scale))
|
||||
}
|
||||
|
||||
func year(from time.Time, v uint64, scale float64) uint64 {
|
||||
if scale == 0 {
|
||||
return uint64(from.AddDate(int(v), 0, 0).Sub(from))
|
||||
}
|
||||
|
||||
return uint64(float64(v) * (float64(from.AddDate(1, 0, 0).Sub(from)) / scale))
|
||||
}
|
||||
|
||||
type sampleUnits map[string]uint64
|
||||
|
||||
func (s sampleUnits) unit(name string, v uint64, scale float64) (uint64, error) {
|
||||
if unit, ok := s[name]; ok {
|
||||
if scale != 0 {
|
||||
v = uint64(float64(v) * (float64(unit) / scale))
|
||||
if v > 1<<63 {
|
||||
// overflow
|
||||
return 0, fmt.Errorf("iso8601:%w", ErrOverflow)
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
if v > 1<<63/unit {
|
||||
// overflow
|
||||
return 0, fmt.Errorf("iso8601:%w", ErrOverflow)
|
||||
}
|
||||
|
||||
return v * unit, nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("iso8601:%w", ErrMissingUnit)
|
||||
}
|
||||
|
||||
type duration struct {
|
||||
from func() time.Time
|
||||
}
|
||||
|
||||
func (d *duration) unit(name string, v uint64, scale float64) (uint64, error) {
|
||||
if _, ok := dateUnit[name]; ok {
|
||||
return dateUnit.unit(name, v, scale)
|
||||
}
|
||||
|
||||
if unit, ok := units[name]; ok {
|
||||
from := d.from()
|
||||
out := unit(from, v, scale)
|
||||
if out > 1<<63 {
|
||||
// overflow
|
||||
return 0, ErrOverflow
|
||||
}
|
||||
|
||||
d.from = func() time.Time {
|
||||
return from.Add(time.Duration(out))
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("%w %q", ErrMissingUnit, name)
|
||||
}
|
||||
|
||||
// FormatDuration returns a string representing the duration in the form "P1Y2M3DT4H5M6S".
|
||||
// Leading zero units are omitted. The zero duration formats as PT0S.
|
||||
func FormatDuration(duration time.Duration) string {
|
||||
if duration == 0 {
|
||||
return "PT0S"
|
||||
}
|
||||
|
||||
var buf [32]byte
|
||||
w := len(buf)
|
||||
u := uint64(duration)
|
||||
neg := duration < 0
|
||||
if neg {
|
||||
u = -u
|
||||
}
|
||||
|
||||
w--
|
||||
buf[w] = 'S'
|
||||
w, u = fmtFrac(buf[:w], u, 9)
|
||||
|
||||
// u is now integer seconds
|
||||
w = fmtInt(buf[:w], u%60)
|
||||
|
||||
if u%60 == 0 && w+2 == len(buf) {
|
||||
w += 2
|
||||
}
|
||||
u /= 60
|
||||
|
||||
// u is now integer minutes
|
||||
if u > 0 {
|
||||
if u%60 > 0 {
|
||||
w--
|
||||
buf[w] = 'M'
|
||||
w = fmtInt(buf[:w], u%60)
|
||||
}
|
||||
u /= 60
|
||||
|
||||
if u > 0 && u%24 > 0 {
|
||||
w--
|
||||
buf[w] = 'H'
|
||||
w = fmtInt(buf[:w], u%24)
|
||||
}
|
||||
u /= 24
|
||||
}
|
||||
|
||||
if w != len(buf) {
|
||||
w--
|
||||
buf[w] = 'T'
|
||||
}
|
||||
|
||||
if u > 0 {
|
||||
w--
|
||||
buf[w] = 'D'
|
||||
w = fmtInt(buf[:w], u)
|
||||
}
|
||||
|
||||
w--
|
||||
buf[w] = 'P'
|
||||
|
||||
if neg {
|
||||
w--
|
||||
buf[w] = '-'
|
||||
}
|
||||
|
||||
return string(buf[w:])
|
||||
}
|
||||
|
||||
func fmtInt(buf []byte, v uint64) int {
|
||||
w := len(buf)
|
||||
if v == 0 {
|
||||
w--
|
||||
buf[w] = '0'
|
||||
} else {
|
||||
for v > 0 {
|
||||
w--
|
||||
buf[w] = byte(v%10) + '0'
|
||||
v /= 10
|
||||
}
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
func fmtFrac(buf []byte, v uint64, prec int) (nw int, nv uint64) {
|
||||
// Omit trailing zeros up to and including decimal point.
|
||||
w := len(buf)
|
||||
isPrint := false
|
||||
for i := 0; i < prec; i++ {
|
||||
digit := v % 10
|
||||
isPrint = isPrint || digit != 0
|
||||
if isPrint {
|
||||
w--
|
||||
buf[w] = byte(digit) + '0'
|
||||
}
|
||||
v /= 10
|
||||
}
|
||||
if isPrint {
|
||||
w--
|
||||
buf[w] = '.'
|
||||
}
|
||||
return w, v
|
||||
}
|
||||
43
duration_example_test.go
Normal file
43
duration_example_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package iso8601_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gitoa.ru/go-4devs/iso8601"
|
||||
)
|
||||
|
||||
func ExampleFormatDuration() {
|
||||
second := iso8601.FormatDuration(time.Second)
|
||||
hours := iso8601.FormatDuration(time.Hour * 25)
|
||||
partOfSecond := iso8601.FormatDuration(time.Second / 5)
|
||||
|
||||
fmt.Printf("%s = %s\n", time.Second, second)
|
||||
fmt.Printf("%s = %s\n", time.Hour*25, hours)
|
||||
fmt.Printf("%s = %s\n", time.Second/5, partOfSecond)
|
||||
// Output:
|
||||
// 1s = PT1S
|
||||
// 25h0m0s = P1DT1H
|
||||
// 200ms = PT0.2S
|
||||
}
|
||||
|
||||
func ExampleParseDuration() {
|
||||
year2020 := time.Date(2020, 1, 1, 1, 1, 1, 1, time.UTC)
|
||||
second, errSecond := iso8601.ParseDuration("PT1S")
|
||||
hours, errHours := iso8601.ParseDuration("P1DT1H")
|
||||
partOfSecond, errPartOfSecond := iso8601.ParseDuration("PT0.2S")
|
||||
yearFromZeroTime, errYearFromZeroTime := iso8601.ParseDuration("P1Y1M1D", iso8601.From(func() time.Time { return time.Time{} }))
|
||||
yearFrom2020, errYearFrom2020 := iso8601.ParseDuration("P1Y1M1D", iso8601.From(func() time.Time { return year2020 }))
|
||||
|
||||
fmt.Printf("PT1S = %s(%v)\n", second, errSecond)
|
||||
fmt.Printf("P1DT1H = %s(%v)\n", hours, errHours)
|
||||
fmt.Printf("PT0.2S = %s(%v)\n", partOfSecond, errPartOfSecond)
|
||||
fmt.Printf("P1Y1M1D(from %v) = %s(%v)\n", time.Time{}, yearFromZeroTime, errYearFromZeroTime)
|
||||
fmt.Printf("P1Y1M1D(form %v) = %s(%v)\n", year2020, yearFrom2020, errYearFrom2020)
|
||||
// Output:
|
||||
// PT1S = 1s(<nil>)
|
||||
// P1DT1H = 25h0m0s(<nil>)
|
||||
// PT0.2S = 200ms(<nil>)
|
||||
// P1Y1M1D(from 0001-01-01 00:00:00 +0000 UTC) = 9528h0m0s(<nil>)
|
||||
// P1Y1M1D(form 2020-01-01 01:01:01.000000001 +0000 UTC) = 9552h0m0s(<nil>)
|
||||
}
|
||||
151
duration_test.go
Normal file
151
duration_test.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package iso8601_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitoa.ru/go-4devs/iso8601"
|
||||
)
|
||||
|
||||
func TestFormatDuration(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := map[string]struct {
|
||||
val time.Duration
|
||||
expect string
|
||||
}{
|
||||
"1 day": {
|
||||
val: time.Hour * 24,
|
||||
expect: "P1D",
|
||||
},
|
||||
"1 hour": {
|
||||
val: time.Hour,
|
||||
expect: "PT1H",
|
||||
},
|
||||
"1 second": {
|
||||
val: time.Second,
|
||||
expect: "PT1S",
|
||||
},
|
||||
"1 nanosecond": {
|
||||
val: time.Nanosecond,
|
||||
expect: "PT0.000000001S",
|
||||
},
|
||||
"negative": {
|
||||
val: -time.Hour * 24,
|
||||
expect: "-P1D",
|
||||
},
|
||||
"zero": {
|
||||
val: time.Duration(0),
|
||||
expect: "PT0S",
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range cases {
|
||||
result := iso8601.FormatDuration(test.val)
|
||||
if result != test.expect {
|
||||
t.Errorf("test:%v got:%v, expect:%v", name, result, test.expect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDuration(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := map[string]struct {
|
||||
opts []iso8601.Option
|
||||
parse string
|
||||
expect time.Duration
|
||||
}{
|
||||
"base": {
|
||||
parse: "P3Y6M4DT12H30M17S",
|
||||
expect: parseDuration(t, "30780h30m17s"),
|
||||
},
|
||||
"base ofer time": {
|
||||
parse: "P3Y6M4DT12H30M17S",
|
||||
expect: parseDuration(t, "30756h30m17s"),
|
||||
opts: []iso8601.Option{
|
||||
iso8601.From(func() time.Time {
|
||||
return parseTime(t, "2006-01-02T15:04:05Z")
|
||||
}),
|
||||
},
|
||||
},
|
||||
"base ofer time with delimiter": {
|
||||
parse: "PT12H30.5M",
|
||||
expect: parseDuration(t, "12h30m30s"),
|
||||
opts: []iso8601.Option{
|
||||
iso8601.From(func() time.Time {
|
||||
return parseTime(t, "2006-01-02T15:04:05Z")
|
||||
}),
|
||||
},
|
||||
},
|
||||
"zero time": {
|
||||
parse: "P3Y6M4DT12H30M17S",
|
||||
expect: parseDuration(t, "30732h30m17s"),
|
||||
opts: []iso8601.Option{
|
||||
iso8601.From(func() time.Time {
|
||||
return time.Time{}
|
||||
}),
|
||||
},
|
||||
},
|
||||
"only time": {
|
||||
parse: "PT12H30M17S",
|
||||
expect: parseDuration(t, "12h30m17s"),
|
||||
},
|
||||
"time with days": {
|
||||
parse: "P10DT12H30M17S",
|
||||
expect: parseDuration(t, "252h30m17s"),
|
||||
},
|
||||
"time with days with options": {
|
||||
parse: "P10DT12H30M17S",
|
||||
expect: parseDuration(t, "252h30m17s"),
|
||||
opts: []iso8601.Option{
|
||||
iso8601.From(func() time.Time {
|
||||
return time.Time{}
|
||||
}),
|
||||
},
|
||||
},
|
||||
"one day": {
|
||||
parse: "P1D",
|
||||
expect: time.Hour * 24,
|
||||
},
|
||||
"1 nanosecond": {
|
||||
parse: "PT0.000000001S",
|
||||
expect: time.Nanosecond,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range cases {
|
||||
dur, err := iso8601.ParseDuration(test.parse, test.opts...)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", name, err)
|
||||
}
|
||||
|
||||
if dur != test.expect {
|
||||
t.Errorf("test: %v expect:%v given:%v", name, test.expect, dur)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseDuration(t *testing.T, in string) time.Duration {
|
||||
t.Helper()
|
||||
|
||||
duration, err := time.ParseDuration(in)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
return duration
|
||||
}
|
||||
|
||||
func parseTime(t *testing.T, in string) time.Time {
|
||||
t.Helper()
|
||||
|
||||
duration, err := time.Parse(time.RFC3339, in)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
return duration
|
||||
}
|
||||
Reference in New Issue
Block a user