Merge 01b692d1ce4d329cac9290001673b0446acf599d into 6b43995a97dee4b2c7fc0bdff8e124da9f31a57e

This commit is contained in:
Arran Ubels 2023-02-15 14:39:41 -08:00 committed by GitHub
commit 8cb3a74e38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 263 additions and 89 deletions

29
.github/workflows/lint.yaml vendored Normal file
View File

@ -0,0 +1,29 @@
name: golangci-lint
on:
push:
tags:
- v*
branches:
- master
- main
pull_request:
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Cache-Go
uses: actions/cache@v1
with:
path: |
~/go/pkg/mod # Module download cache
~/.cache/go-build # Build cache (Linux)
~/Library/Caches/go-build # Build cache (Mac)
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest

39
.github/workflows/releaser.yml vendored Normal file
View File

@ -0,0 +1,39 @@
name: releaser
on:
push:
tags:
- 'v*.*.*'
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.19
- name: Cache-Go
uses: actions/cache@v1
with:
path: |
~/go/pkg/mod # Module download cache
~/.cache/go-build # Build cache (Linux)
~/Library/Caches/go-build # Build cache (Mac)
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
- name: Test
run: go test ./...
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
with:
distribution: goreleaser
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

28
.github/workflows/test.yaml vendored Normal file
View File

@ -0,0 +1,28 @@
on: [push, pull_request]
name: Test
jobs:
test:
strategy:
matrix:
go-version: [1.19.x]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Cache-Go
uses: actions/cache@v1
with:
path: |
~/go/pkg/mod # Module download cache
~/.cache/go-build # Build cache (Linux)
~/Library/Caches/go-build # Build cache (Mac)
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
- name: Test
run: go test ./...

38
.goreleaser.yml Normal file
View File

@ -0,0 +1,38 @@
project_name: dateparse
builds:
-
id: "dateparse"
binary: "dateparse"
dir: dateparse
-
id: "example"
binary: "example"
dir: example
archives:
-
format_overrides:
- goos: windows
format: zip
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ .Tag }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
nfpms:
-
vendor: dateparse
homepage: https://github.com/araddon/dateparse
maintainer: n/a <someone@eample.com>
description: NA
formats:
- apk
- deb
- rpm
release: 1
section: default
priority: extra

View File

@ -7,7 +7,6 @@ import (
)
/*
go test -bench Parse
BenchmarkShotgunParse 50000 37588 ns/op 13258 B/op 167 allocs/op
@ -21,14 +20,13 @@ BenchmarkParseAny-4 200000 8627 ns/op 144 B/op 3 allo
BenchmarkShotgunParse-8 50000 33940 ns/op 13136 B/op 169 allocs/op
BenchmarkParseAny-8 200000 10146 ns/op 912 B/op 29 allocs/op
BenchmarkParseDateString-8 10000 123077 ns/op 208 B/op 13 allocs/op
*/
func BenchmarkShotgunParse(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, dateStr := range testDates {
// This is the non dateparse traditional approach
parseShotgunStyle(dateStr)
_, _ = parseShotgunStyle(dateStr)
}
}
}
@ -37,7 +35,7 @@ func BenchmarkParseAny(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, dateStr := range testDates {
ParseAny(dateStr)
_, _ = ParseAny(dateStr)
}
}
}

11
go.mod
View File

@ -1,9 +1,16 @@
module github.com/araddon/dateparse
go 1.12
go 1.19
require (
github.com/mattn/go-runewidth v0.0.10 // indirect
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4
github.com/stretchr/testify v1.7.0
)
require (
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/mattn/go-runewidth v0.0.10 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.1.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)

1
go.sum
View File

@ -8,7 +8,6 @@ github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4 h1:8qmTC5ByIXO3GP/IzBkxcZ/99VITvnIETDhdFz/om7A=
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

View File

@ -133,11 +133,12 @@ const (
var (
// ErrAmbiguousMMDD for date formats such as 04/02/2014 the mm/dd vs dd/mm are
// ambiguous, so it is an error for strict parse rules.
ErrAmbiguousMMDD = fmt.Errorf("This date has ambiguous mm/dd vs dd/mm type format")
ErrAmbiguousMMDD = fmt.Errorf("this date has ambiguous mm/dd vs dd/mm type format")
ErrCouldntFindFormat = fmt.Errorf("could not find format for")
)
func unknownErr(datestr string) error {
return fmt.Errorf("Could not find format for %q", datestr)
return fmt.Errorf("%w %q", ErrCouldntFindFormat, datestr)
}
// ParseAny parse an unknown date format, detect the layout.
@ -170,15 +171,14 @@ func ParseIn(datestr string, loc *time.Location, opts ...ParserOption) (time.Tim
// Set Location to time.Local. Same as ParseIn Location but lazily uses
// the global time.Local variable for Location argument.
//
// denverLoc, _ := time.LoadLocation("America/Denver")
// time.Local = denverLoc
// denverLoc, _ := time.LoadLocation("America/Denver")
// time.Local = denverLoc
//
// t, err := dateparse.ParseLocal("3/1/2014")
// t, err := dateparse.ParseLocal("3/1/2014")
//
// Equivalent to:
//
// t, err := dateparse.ParseIn("3/1/2014", denverLoc)
//
// t, err := dateparse.ParseIn("3/1/2014", denverLoc)
func ParseLocal(datestr string, opts ...ParserOption) (time.Time, error) {
p, err := parseTime(datestr, time.Local, opts...)
if err != nil {
@ -204,9 +204,8 @@ func MustParse(datestr string, opts ...ParserOption) time.Time {
// ParseFormat parse's an unknown date-time string and returns a layout
// string that can parse this (and exact same format) other date-time strings.
//
// layout, err := dateparse.ParseFormat("2013-02-01 00:00:00")
// // layout = "2006-01-02 15:04:05"
//
// layout, err := dateparse.ParseFormat("2013-02-01 00:00:00")
// // layout = "2006-01-02 15:04:05"
func ParseFormat(datestr string, opts ...ParserOption) (string, error) {
p, err := parseTime(datestr, nil, opts...)
if err != nil {
@ -234,7 +233,10 @@ func ParseStrict(datestr string, opts ...ParserOption) (time.Time, error) {
func parseTime(datestr string, loc *time.Location, opts ...ParserOption) (p *parser, err error) {
p = newParser(datestr, loc, opts...)
p, err = newParser(datestr, loc, opts...)
if err != nil {
return
}
if p.retryAmbiguousDateWithSwap {
// month out of range signifies that a day/month swap is the correct solution to an ambiguous date
// this is because it means that a day is being interpreted as a month and overflowing the valid value for that
@ -250,7 +252,7 @@ func parseTime(datestr string, loc *time.Location, opts ...ParserOption) (p *par
// turn off the retry to avoid endless recursion
retryAmbiguousDateWithSwap := RetryAmbiguousDateWithSwap(false)
modifiedOpts := append(opts, preferMonthFirst, retryAmbiguousDateWithSwap)
p, err = parseTime(datestr, time.Local, modifiedOpts...)
p, _ = parseTime(datestr, time.Local, modifiedOpts...)
}
}
@ -268,7 +270,7 @@ iterRunes:
//r := rune(datestr[i])
r, bytesConsumed := utf8.DecodeRuneInString(datestr[i:])
if bytesConsumed > 1 {
i += (bytesConsumed - 1)
i += bytesConsumed - 1
}
// gou.Debugf("i=%d r=%s state=%d %s", i, string(r), p.stateDate, datestr)
@ -467,8 +469,6 @@ iterRunes:
switch r {
case ':':
p.set(p.offseti, "-07:00")
// case ' ':
// return nil, unknownErr(datestr)
}
case dateYearDashAlphaDash:
@ -538,7 +538,9 @@ iterRunes:
// I honestly don't know if this format ever shows up as yyyy/
switch r {
case ' ', ':':
case ' ':
fallthrough
case ':':
p.stateTime = timeStart
if p.daylen == 0 {
p.daylen = i - p.dayi
@ -566,7 +568,9 @@ iterRunes:
}
// We aren't breaking because we are going to re-use this case
// to find where the date starts, and possible time begins
case ' ', ':':
case ' ':
fallthrough
case ':':
p.stateTime = timeStart
if p.yearlen == 0 {
p.yearlen = i - p.yeari
@ -925,7 +929,9 @@ iterRunes:
switch r {
case '\'':
p.yeari = i + 1
case ' ', ',':
case ' ':
fallthrough
case ',':
// x
// May 8, 2009 5:57:51 PM
// x
@ -941,7 +947,9 @@ iterRunes:
// April 8, 2009
// April 8 2009
switch r {
case ' ', ',':
case ' ':
fallthrough
case ',':
// x
// June 8, 2009
// x
@ -1066,7 +1074,9 @@ iterRunes:
p.dayi = i
}
switch r {
case ' ', '-':
case ' ':
fallthrough
case '-':
if p.moi == 0 {
p.moi = i + 1
p.daylen = i - p.dayi
@ -1087,8 +1097,15 @@ iterRunes:
// Thu, 4 Jan 2018 17:53:36 +0000
// Tue, 11 Jul 2017 16:28:13 +0200 (CEST)
// Mon, 02-Jan-06 15:04:05 MST
var offset int
switch r {
case ' ', '-':
case ' ':
for i+1 < len(datestr) && datestr[i+1] == ' ' {
i++
offset++
}
fallthrough
case '-':
if p.dayi == 0 {
p.dayi = i + 1
} else if p.moi == 0 {
@ -1096,11 +1113,11 @@ iterRunes:
p.setDay()
p.moi = i + 1
} else if p.yeari == 0 {
p.molen = i - p.moi
p.molen = i - p.moi - offset
p.set(p.moi, "Jan")
p.yeari = i + 1
} else {
p.yearlen = i - p.yeari
p.yearlen = i - p.yeari - offset
p.setYear()
p.stateTime = timeStart
break iterRunes
@ -1328,7 +1345,12 @@ iterRunes:
// 15:44:11 UTC+0100 2015
switch r {
case '+', '-':
p.tzlen = i - p.tzi
if datestr[p.tzi:i] == "GMT" {
p.tzi = 0
p.tzlen = 0
} else {
p.tzlen = i - p.tzi
}
if p.tzlen == 4 {
p.set(p.tzi, " MST")
} else if p.tzlen == 3 {
@ -1439,7 +1461,6 @@ iterRunes:
if datestr[i-1] == 'm' {
p.extra = i - 2
p.trimExtra()
break
}
case '+', '-', '(':
// This really doesn't seem valid, but for some reason when round-tripping a go date
@ -1449,7 +1470,6 @@ iterRunes:
p.extra = i - 1
p.stateTime = timeWsOffset
p.trimExtra()
break
default:
switch {
case unicode.IsDigit(r):
@ -1596,7 +1616,6 @@ iterRunes:
// 00:00:00.000 +0300 +0300
p.extra = i - 1
p.trimExtra()
break
default:
if unicode.IsLetter(r) {
// 00:07:31.945167 +0000 UTC
@ -1665,10 +1684,13 @@ iterRunes:
p.trimExtra()
case timeWsAlphaZoneOffset:
// 06:20:00 UTC-05
if i-p.offseti < 4 {
switch i - p.offseti {
case 2, 3, 4:
p.set(p.offseti, "-07")
} else {
case 5:
p.set(p.offseti, "-0700")
case 6:
p.set(p.offseti, "-07:00")
}
case timePeriod:
@ -1982,7 +2004,6 @@ type parser struct {
msi int
mslen int
offseti int
offsetlen int
tzi int
tzlen int
t *time.Time
@ -2008,7 +2029,7 @@ func RetryAmbiguousDateWithSwap(retryAmbiguousDateWithSwap bool) ParserOption {
}
}
func newParser(dateStr string, loc *time.Location, opts ...ParserOption) *parser {
func newParser(dateStr string, loc *time.Location, opts ...ParserOption) (*parser, error) {
p := &parser{
stateDate: dateStart,
stateTime: timeIgnore,
@ -2021,9 +2042,11 @@ func newParser(dateStr string, loc *time.Location, opts ...ParserOption) *parser
// allow the options to mutate the parser fields from their defaults
for _, option := range opts {
option(p)
if err := option(p); err != nil {
return nil, fmt.Errorf("option error: %w", err)
}
}
return p
return p, nil
}
func (p *parser) nextIs(i int, b byte) bool {
@ -2141,17 +2164,6 @@ func (p *parser) trimExtra() {
}
}
// func (p *parser) remove(i, length int) {
// if len(p.format) > i+length {
// //append(a[:i], a[j:]...)
// p.format = append(p.format[0:i], p.format[i+length:]...)
// }
// if len(p.datestr) > i+length {
// //append(a[:i], a[j:]...)
// p.datestr = fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+length:])
// }
// }
func (p *parser) parse() (time.Time, error) {
if p.t != nil {
return *p.t, nil

View File

@ -10,8 +10,7 @@ import (
func TestOne(t *testing.T) {
time.Local = time.UTC
var ts time.Time
ts = MustParse("2020-07-20+08:00")
var ts = MustParse("2020-07-20+08:00")
assert.Equal(t, "2020-07-19 16:00:00 +0000 UTC", fmt.Sprintf("%v", ts.In(time.UTC)))
}
@ -417,6 +416,10 @@ var testInputs = []dateTest{
{in: "1384216367111", out: "2013-11-12 00:32:47.111 +0000 UTC"},
{in: "1384216367111222", out: "2013-11-12 00:32:47.111222 +0000 UTC"},
{in: "1384216367111222333", out: "2013-11-12 00:32:47.111222333 +0000 UTC"},
{in: "Wed, 8 Feb 2023 19:00:46 +1100 (AEDT)", out: "2023-02-08 08:00:46 +0000 UTC"},
{in: "FRI, 16 AUG 2013 9:39:51 +1000", out: "2013-08-15 23:39:51 +0000 UTC"},
{in: "Mon, 1 Dec 2008 14:48:22 GMT-07:00", out: "2008-12-01 21:48:22 +0000 UTC"},
}
func TestParse(t *testing.T) {
@ -425,52 +428,70 @@ func TestParse(t *testing.T) {
time.Local = time.UTC
zeroTime := time.Time{}.Unix()
ts, err := ParseAny("INVALID")
assert.Equal(t, zeroTime, ts.Unix())
assert.NotEqual(t, nil, err)
t.Run("Invalid", func(t *testing.T) {
ts, err := ParseAny("INVALID")
assert.Equal(t, zeroTime, ts.Unix())
assert.NotEqual(t, nil, err)
assert.Equal(t, true, testDidPanic("NOT GONNA HAPPEN"))
// https://github.com/golang/go/issues/5294
_, err = ParseAny(time.RFC3339)
assert.NotEqual(t, nil, err)
assert.Equal(t, true, testDidPanic("NOT GONNA HAPPEN"))
// https://github.com/golang/go/issues/5294
_, err = ParseAny(time.RFC3339)
assert.NotEqual(t, nil, err)
})
for _, th := range testInputs {
if len(th.loc) > 0 {
loc, err := time.LoadLocation(th.loc)
if err != nil {
t.Fatalf("Expected to load location %q but got %v", th.loc, err)
t.Run(th.in, func(t *testing.T) {
var ts time.Time
defer func() {
if r := recover(); r != nil {
t.Fatalf("error: %s", r)
}
}()
if len(th.loc) > 0 {
loc, err := time.LoadLocation(th.loc)
if err != nil {
t.Fatalf("Expected to load location %q but got %v", th.loc, err)
}
ts, err = ParseIn(th.in, loc)
if err != nil {
t.Fatalf("expected to parse %q but got %v", th.in, err)
}
got := fmt.Sprintf("%v", ts.In(time.UTC))
assert.Equal(t, th.out, got, "Expected %q but got %q from %q", th.out, got, th.in)
if th.out != got {
t.Fatalf("whoops, got %s, expected %s", got, th.out)
}
} else {
ts = MustParse(th.in)
got := fmt.Sprintf("%v", ts.In(time.UTC))
assert.Equal(t, th.out, got, "Expected %q but got %q from %q", th.out, got, th.in)
if th.out != got {
t.Fatalf("whoops, got %s, expected %s", got, th.out)
}
}
ts, err = ParseIn(th.in, loc)
if err != nil {
t.Fatalf("expected to parse %q but got %v", th.in, err)
}
got := fmt.Sprintf("%v", ts.In(time.UTC))
assert.Equal(t, th.out, got, "Expected %q but got %q from %q", th.out, got, th.in)
if th.out != got {
panic("whoops")
}
} else {
ts = MustParse(th.in)
got := fmt.Sprintf("%v", ts.In(time.UTC))
assert.Equal(t, th.out, got, "Expected %q but got %q from %q", th.out, got, th.in)
if th.out != got {
panic("whoops")
}
}
})
}
// some errors
assert.Equal(t, true, testDidPanic(`{"ts":"now"}`))
t.Run("", func(t *testing.T) {
assert.Equal(t, true, testDidPanic(`{"ts":"now"}`))
})
_, err = ParseAny("138421636711122233311111") // too many digits
assert.NotEqual(t, nil, err)
t.Run("too many digits", func(t *testing.T) {
_, err := ParseAny("138421636711122233311111") // too many digits
assert.NotEqual(t, nil, err)
})
_, err = ParseAny("-1314")
assert.NotEqual(t, nil, err)
t.Run("negative number", func(t *testing.T) {
_, err := ParseAny("-1314")
assert.NotEqual(t, nil, err)
})
_, err = ParseAny("2014-13-13 08:20:13,787") // month 13 doesn't exist so error
assert.NotEqual(t, nil, err)
t.Run("month doesn't exist", func(t *testing.T) {
_, err := ParseAny("2014-13-13 08:20:13,787") // month 13 doesn't exist so error
assert.NotEqual(t, nil, err)
})
}
func testDidPanic(datestr string) (paniced bool) {
@ -488,7 +509,10 @@ func TestPStruct(t *testing.T) {
denverLoc, err := time.LoadLocation("America/Denver")
assert.Equal(t, nil, err)
p := newParser("08.21.71", denverLoc)
p, err := newParser("08.21.71", denverLoc)
if err != nil {
t.Fatalf("Parser build error: %s", err)
}
p.setMonth()
assert.Equal(t, 0, p.moi)