diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..ff43abf --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,28 @@ +name: golangci-lint +on: + push: + tags: + - v* + branches: + - main + pull_request: +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Cache-Go + uses: actions/cache@v4 + 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@v7 + with: + version: latest diff --git a/.github/workflows/releaser.yml b/.github/workflows/releaser.yml new file mode 100644 index 0000000..fc1008a --- /dev/null +++ b/.github/workflows/releaser.yml @@ -0,0 +1,39 @@ +name: releaser + +on: + push: + tags: + - 'v*.*.*' + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.20.x + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Cache-Go + uses: actions/cache@v4 + 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@v6 + with: + distribution: goreleaser + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..892cdf0 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,28 @@ +on: [push, pull_request] +name: Test +jobs: + test: + strategy: + matrix: + go-version: [1.20.x] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + - name: Checkout code + uses: actions/checkout@v4 + - name: Cache-Go + uses: actions/cache@v4 + 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 ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..05f3653 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.pprof +*.test +dist +vendor +dateparse/dateparse +example/example diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..4826342 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,41 @@ +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: + use: github-native + sort: asc + filters: + include: + - "^feat:" + exclude: + - '^docs:' + - '^test:' +nfpms: + - + vendor: dateparse + homepage: https://github.com/itlightning/dateparse + maintainer: IT Lightning, LLC + description: NA + formats: + - apk + - deb + - rpm + release: 1 + section: default + priority: extra diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 3b4b177..0000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: go - -go: - - 1.13.x - -before_install: - - go get -t -v ./... - -script: - - go test -race -coverprofile=coverage.txt -covermode=atomic - -after_success: - - bash <(curl -s https://codecov.io/bash) diff --git a/LICENSE b/LICENSE index f675ed3..f1cf09a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ The MIT License (MIT) Copyright (c) 2015-2017 Aaron Raddon +Copyright (c) 2023-2024 IT Lightning, LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index fe682dd..0704587 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,29 @@ Go Date Parser --------------------------- +[![GoDoc](https://godoc.org/github.com/itlightning/dateparse?status.svg)](http://godoc.org/github.com/itlightning/dateparse) +![Test Status](https://github.com/itlightning/dateparse/actions/workflows/test.yaml/badge.svg) +[![Go ReportCard](https://goreportcard.com/badge/itlightning/dateparse)](https://goreportcard.com/report/itlightning/dateparse) -Parse many date strings without knowing format in advance. Uses a scanner to read bytes and use a state machine to find format. Much faster than shotgun based parse methods. See [bench_test.go](https://github.com/araddon/dateparse/blob/master/bench_test.go) for performance comparison. +Parse date/time strings without knowing the format in advance. Supports 100+ formats. Validates comprehensively to avoid false positives. Very fast (~single-pass state-machine based approach). See [bench_test.go](https://github.com/itlightning/dateparse/blob/main/bench_test.go) for performance comparison. See the critical note below about timezones. + +⚡ Maintained by [SparkLogs](https://sparklogs.com/), a cloud-first logging platform that's uniquely powerful, super-easy (schemaless, point-and-shoot ingestion), and affordable. It automatically extracts and classifies structured data out of your unstructured log messages. Enjoy visual pattern-analysis and robust SQL-like search. It's unique architecture means you can log more and pay less. Check it out and give us feedback! SparkLogs is developed by [IT Lightning](https://itlightning.com/). ⚡ + +🐛💡 Find a bug or have an idea with this package? [Issues](https://github.com/itlightning/dateparse/issues) and pull requests are welcome. -[![Code Coverage](https://codecov.io/gh/araddon/dateparse/branch/master/graph/badge.svg)](https://codecov.io/gh/araddon/dateparse) -[![GoDoc](https://godoc.org/github.com/araddon/dateparse?status.svg)](http://godoc.org/github.com/araddon/dateparse) -[![Build Status](https://travis-ci.org/araddon/dateparse.svg?branch=master)](https://travis-ci.org/araddon/dateparse) -[![Go ReportCard](https://goreportcard.com/badge/araddon/dateparse)](https://goreportcard.com/report/araddon/dateparse) +History and Contributors +---------------------------------- -**MM/DD/YYYY VS DD/MM/YYYY** Right now this uses mm/dd/yyyy WHEN ambiguous if this is not desired behavior, use `ParseStrict` which will fail on ambiguous date strings. +This is an actively maintained fork of the excellent [original dateparse package](https://github.com/araddon/dateparse) by [@araddon](https://github.com/araddon). +This fork [incorporates](https://github.com/araddon/dateparse/pull/159) many bugfixes from the community, and adds comprehensive validation and extensive performance optimizations. +A special thanks to [@araddon](https://github.com/araddon), other contributors to the original project, as well as those who contributed fixes that got incorporated into this version: +[@arran4](https://github.com/arran4), [@bizy01](https://github.com/bizy01), [@BrianLeishman](https://github.com/BrianLeishman), [@dferstay](https://github.com/dferstay), [@jiangxin](https://github.com/jiangxin), [@jmdacruz](https://github.com/jmdacruz), [@krhubert](https://github.com/krhubert), [@mehanizm](https://github.com/mehanizm), [@xwjdsh](https://github.com/xwjdsh), and [@zifengyu](https://github.com/zifengyu). -**Timezones** The location your server is configured affects the results! See example or https://play.golang.org/p/IDHRalIyXh and last paragraph here https://golang.org/pkg/time/#Parse. +Ambiguous Date Formats +---------------------------------- + +**MM/DD/YYYY VS DD/MM/YYYY** Right now this uses mm/dd/yyyy *when* ambiguous. If this is not desired behavior, use `ParseStrict` which will fail on ambiguous date strings. This behavior can be adjusted using the `PreferMonthFirst` parser option. Some ambiguous formats can fail (e.g., trying to parse 31/03/2023 as the default month-first format `MM/DD/YYYY`), but can be automatically retried with `RetryAmbiguousDateWithSwap`. ```go @@ -24,21 +35,59 @@ t, err := dateparse.ParseStrict("3/1/2014") > returns error // Return a string that represents the layout to parse the given date-time. +// For certain highly complex date formats, ParseFormat's return value may +// not be accurate (if this is the case, the returned format string will be a +// different length, than the input). In these cases, ParseAny will still be +// able to successfully parse the format, but this return value will fail to +// parse. For example, anything that starts with a full weekday will fail. layout, err := dateparse.ParseFormat("May 8, 2009 5:57:51 PM") > "Jan 2, 2006 3:04:05 PM" ``` + +Performance Considerations +---------------------------------- + +Internally a memory pool is used to minimize allocation overhead. If you could +be frequently parsing text that does not match any format, consider turning on +the the `SimpleErrorMessages` option. This will make error messages have no +contextual details, but will reduce allocation overhead 13x and will be 4x +faster (most of the time is spent in generating a complex error message if the +option is off (default)). + +Timezone Considerations +---------------------------------- + +The location that your server is configured to affects the results! See example or https://play.golang.org/p/IDHRalIyXh and last paragraph here https://golang.org/pkg/time/#Parse. + +Important points to understand: +* If you are parsing a date string that does *not* reference a timezone, if you use `Parse` it will assume UTC, or for `ParseIn` it will use the specified location. +* If you are parsing a date string that *does* reference a timezone and *does* specify an explicit offset (e.g., `2012-08-03 13:31:59 -0600 MST`), then it will return a time object with a location that represents a fixed timezone that has the given offset and name (it will not validate that the timezone abbreviation specified in the date string is a potential valid match for the given offset). + * This can lead to some potentially unexpected results, for example consider the date string `2012-08-03 18:31:59.000+00:00 PST` -- this string has an explicit offset of `+00:00` (UTC), and so the returned time will have a location with a zero offset (18:31:59.000 UTC) even though the name of the fixed time zone associated with the returned time is `PST`. Essentially, it will always prioritize an explicit offset as accurate over an explicit +* If you are parsing a date string that *does* reference a timezone but *without* an explicit offset (e.g., `2012-08-03 14:32:59 MST`), then it will only recognize and map the timezone name and add an offset if you are using `ParseIn` and specify a location that knows about the given time zone abbreviation (e.g., in this example, you would need to pass the `America/Denver` location and it will recognize the `MST` and `MDT` time zone names) + * If a time zone abbreviation is recognized based on the passed location, then it will use the appropriate offset, and make any appropriate adjustment for daylight saving time (e.g., in the above example, the parsed time would actually contain a zone name of `MDT` because the date is within the range when daylight savings time is active). + * If a time zone abbreviation is *not* recognized for the passed location, then it will create a fake time zone with a *zero* offset but with the specified name. This requires further processing if you are trying to actually get the correct absolute time in the UTC time zone. + * If you receive a parsed time that has a zero offset but a non-UTC timezone name, then you should use a method to map the (sometimes ambiguous) timezone name (e.g., `"EEG"`) into a location name (e.g., `"Africa/Cairo"` or `"Europe/Bucharest"`), and then reconstruct a new time object with the same date/time/nanosecond but with the properly mapped location. (Do not use the `time.In` method to convert it to the new location, as this will treat the original time as if it was in UTC with a zero offset -- you need to reconstruct the time as if it was constructed with the proper location in the first place.) + cli tool for testing dateformats ---------------------------------- -[Date Parse CLI](https://github.com/araddon/dateparse/blob/master/dateparse) +[Date Parse CLI](https://github.com/itlightning/dateparse/tree/main/dateparse) +Running the tests +---------------------------------- + +Make sure for your Linux distribution you've installed the relevant package that includes older timezone name links (e.g., `US/Pacific`). For example, on Ubuntu: + +```bash +sudo apt install tzdata-legacy +``` Extended example ------------------- -https://github.com/araddon/dateparse/blob/master/example/main.go +https://github.com/itlightning/dateparse/blob/main/example/main.go ```go package main @@ -48,61 +97,108 @@ import ( "fmt" "time" + "github.com/itlightning/dateparse" "github.com/scylladb/termtables" - "github.com/araddon/dateparse" ) var examples = []string{ + // mon day year (time) "May 8, 2009 5:57:51 PM", "oct 7, 1970", "oct 7, '70", "oct. 7, 1970", "oct. 7, 70", - "Mon Jan 2 15:04:05 2006", - "Mon Jan 2 15:04:05 MST 2006", - "Mon Jan 02 15:04:05 -0700 2006", - "Monday, 02-Jan-06 15:04:05 MST", - "Mon, 02 Jan 2006 15:04:05 MST", - "Tue, 11 Jul 2017 16:28:13 +0200 (CEST)", - "Mon, 02 Jan 2006 15:04:05 -0700", - "Mon 30 Sep 2018 09:09:09 PM UTC", - "Mon Aug 10 15:44:11 UTC+0100 2015", - "Thu, 4 Jan 2018 17:53:36 +0000", - "Fri Jul 03 2015 18:04:07 GMT+0100 (GMT Daylight Time)", - "Sun, 3 Jan 2021 00:12:23 +0800 (GMT+08:00)", - "September 17, 2012 10:09am", - "September 17, 2012 at 10:09am PST-08", - "September 17, 2012, 10:10:09", "October 7, 1970", "October 7th, 1970", + "Sept. 7, 1970 11:15:26pm", + "Sep 7 2009 11:15:26.123 PM PST", + "September 3rd, 2009 11:15:26.123456789pm", + "September 17 2012 10:09am", + "September 17, 2012, 10:10:09", + "Sep 17, 2012 at 10:02am (EST)", + // (PST-08 will have an offset of -0800, and a zone name of "PST") + "September 17, 2012 at 10:09am PST-08", + // (UTC-0700 has the same offset as -0700, and the returned zone name will be empty) + "September 17 2012 5:00pm UTC-0700", + "September 17 2012 5:00pm GMT-0700", + // (weekday) day mon year (time) + "7 oct 70", + "7 Oct 1970", + "7 September 1970 23:15", + "7 September 1970 11:15:26pm", + "03 February 2013", "12 Feb 2006, 19:17", "12 Feb 2006 19:17", "14 May 2019 19:11:40.164", - "7 oct 70", - "7 oct 1970", - "03 February 2013", - "1 July 2013", - "2013-Feb-03", - // dd/Mon/yyy alpha Months - "06/Jan/2008:15:04:05 -0700", + "4th Sep 2012", + "1st February 2018 13:58:24", + "Mon, 02 Jan 2006 15:04:05 MST", // RFC1123 + "Mon, 02 Jan 2006 15:04:05 -0700", + "Tue, 11 Jul 2017 16:28:13 +0200 (CEST)", + "Mon 30 Sep 2018 09:09:09 PM UTC", + "Sun, 07 Jun 2020 00:00:00 +0100", + "Wed, 8 Feb 2023 19:00:46 +1100 (AEDT)", + // ANSIC and UnixDate - weekday month day time year + "Mon Jan 2 15:04:05 2006", + "Mon Jan 2 15:04:05 MST 2006", + "Monday Jan 02 15:04:05 -0700 2006", + "Mon Jan 2 15:04:05.103786 2006", + // RubyDate - weekday month day time offset year + "Mon Jan 02 15:04:05 -0700 2006", + // ANSIC_GLIBC - weekday day month year time + "Mon 02 Jan 2006 03:04:05 PM UTC", + "Monday 02 Jan 2006 03:04:05 PM MST", + // weekday month day time timezone-offset year + "Mon Aug 10 15:44:11 UTC+0000 2015", + // git log default date format + "Thu Apr 7 15:13:13 2005 -0700", + // variants of git log default date format + "Thu Apr 7 15:13:13 2005 -07:00", + "Thu Apr 7 15:13:13 2005 -07:00 PST", + "Thu Apr 7 15:13:13 2005 -07:00 PST (Pacific Standard Time)", + "Thu Apr 7 15:13:13 -0700 2005", + "Thu Apr 7 15:13:13 -07:00 2005", + "Thu Apr 7 15:13:13 -0700 PST 2005", + "Thu Apr 7 15:13:13 -07:00 PST 2005", + "Thu Apr 7 15:13:13 PST 2005", + // Variants of the above with a (full time zone description) + "Fri Jul 3 2015 06:04:07 PST-0700 (Pacific Daylight Time)", + "Fri Jul 03 2015 18:04:07 GMT+0100 (GMT Daylight Time)", + "Sun, 3 Jan 2021 00:12:23 +0800 (GMT+08:00)", + // year month day + "2013 May 2", + "2013 May 02 11:37:55", + // dd/Mon/year alpha Months "06/Jan/2008 15:04:05 -0700", - // mm/dd/yy + "06/January/2008 15:04:05 -0700", + "06/Jan/2008:15:04:05 -0700", // ngnix-log + "06/January/2008:08:11:17 -0700", + // mm/dd/year (see also PreferMonthFirst and RetryAmbiguousDateWithSwap options) "3/31/2014", "03/31/2014", "08/21/71", "8/1/71", "4/8/2014 22:05", "04/08/2014 22:05", + "04/08/2014, 22:05", "4/8/14 22:05", "04/2/2014 03:00:51", - "8/8/1965 12:00:00 AM", - "8/8/1965 01:00:01 PM", - "8/8/1965 01:00 PM", "8/8/1965 1:00 PM", + "8/8/1965 01:00 PM", "8/8/1965 12:00 AM", + "8/8/1965 12:00:00AM", + "8/8/1965 01:00:01 PM", + "8/8/1965 01:00:01PM -0700", + "8/8/1965 13:00:01 -0700 PST", + "8/8/1965 01:00:01 PM -0700 PST", + "8/8/1965 01:00:01 PM -07:00 PST (Pacific Standard Time)", "4/02/2014 03:00:51", "03/19/2012 10:11:59", "03/19/2012 10:11:59.3186369", + // mon/dd/year + "Oct/ 7/1970", + "Oct/03/1970 22:33:44", + "February/03/1970 11:33:44.555 PM PST", // yyyy/mm/dd "2014/3/31", "2014/03/31", @@ -112,6 +208,78 @@ var examples = []string{ "2014/4/02 03:00:51", "2012/03/19 10:11:59", "2012/03/19 10:11:59.3186369", + // weekday, day-mon-yy time + "Fri, 03-Jul-15 08:08:08 CEST", + "Monday, 02-Jan-06 15:04:05 MST", // RFC850 + "Monday, 02 Jan 2006 15:04:05 -0600", + "02-Jan-06 15:04:05 MST", + // RFC3339 - yyyy-mm-ddThh + "2006-01-02T15:04:05+0000", + "2009-08-12T22:15:09-07:00", + "2009-08-12T22:15:09", + "2009-08-12T22:15:09.988", + "2009-08-12T22:15:09Z", + "2009-08-12T22:15:09.52Z", + "2017-07-19T03:21:51:897+0100", + "2019-05-29T08:41-04", // no seconds, 2 digit TZ offset + // yyyy-mm-dd hh:mm:ss + "2014-04-26 17:24:37.3186369", + "2012-08-03 18:31:59.257000000", + "2014-04-26 17:24:37.123", + "2014-04-01 12:01am", + "2014-04-01 12:01:59.765 AM", + "2014-04-01 12:01:59,765", + "2014-04-01 22:43", + "2014-04-01 22:43:22", + "2014-12-16 06:20:00 UTC", + "2014-12-16 06:20:00 GMT", + "2014-04-26 05:24:37 PM", + "2014-04-26 13:13:43 +0800", + "2014-04-26 13:13:43 +0800 +08", + "2014-04-26 13:13:44 +09:00", + "2012-08-03 18:31:59.257000000 +0000 UTC", + "2015-09-30 18:48:56.35272715 +0000 UTC", + "2015-02-18 00:12:00 +0000 GMT", // golang native format + "2015-02-18 00:12:00 +0000 UTC", + "2015-02-08 03:02:00 +0300 MSK m=+0.000000001", + "2015-02-08 03:02:00.001 +0300 MSK m=+0.000000001", + "2017-07-19 03:21:51+00:00", + "2017-04-03 22:32:14.322 CET", + "2017-04-03 22:32:14,322 CET", + "2017-04-03 22:32:14:322 CET", + "2018-09-30 08:09:13.123PM PMDT", // PMDT time zone + "2018-09-30 08:09:13.123 am AMT", // AMT time zone + "2014-04-26", + "2014-04", + "2014", + // yyyy-mm-dd(offset) + "2020-07-20+08:00", + "2020-07-20+0800", + // year-mon-dd + "2013-Feb-03", + "2013-February-03 09:07:08.123", + // dd-mon-year + "03-Feb-13", + "03-Feb-2013", + "07-Feb-2004 09:07:07 +0200", + "07-February-2004 09:07:07 +0200", + // dd-mm-year (this format (common in Europe) always puts the day first, regardless of PreferMonthFirst) + "28-02-02", + "28-02-02 15:16:17", + "28-02-2002", + "28-02-2002 15:16:17", + // mm.dd.yy (see also PreferMonthFirst and RetryAmbiguousDateWithSwap options) + "3.31.2014", + "03.31.14", + "03.31.2014", + "03.31.2014 10:11:59 MST", + "03.31.2014 10:11:59.3186369Z", + // year.mm.dd + "2014.03", + "2014.03.30", + "2014.03.30 08:33pm", + "2014.03.30T08:33:44.555 PM -0700 MST", + "2014.03.30-0600", // yyyy:mm:dd "2014:3:31", "2014:03:31", @@ -121,58 +289,53 @@ var examples = []string{ "2014:4:02 03:00:51", "2012:03:19 10:11:59", "2012:03:19 10:11:59.3186369", - // Chinese - "2014年04月08日", - // yyyy-mm-ddThh - "2006-01-02T15:04:05+0000", - "2009-08-12T22:15:09-07:00", - "2009-08-12T22:15:09", - "2009-08-12T22:15:09.988", - "2009-08-12T22:15:09Z", - "2017-07-19T03:21:51:897+0100", - "2019-05-29T08:41-04", // no seconds, 2 digit TZ offset - // yyyy-mm-dd hh:mm:ss - "2014-04-26 17:24:37.3186369", - "2012-08-03 18:31:59.257000000", - "2014-04-26 17:24:37.123", - "2013-04-01 22:43", - "2013-04-01 22:43:22", - "2014-12-16 06:20:00 UTC", - "2014-12-16 06:20:00 GMT", - "2014-04-26 05:24:37 PM", - "2014-04-26 13:13:43 +0800", - "2014-04-26 13:13:43 +0800 +08", - "2014-04-26 13:13:44 +09:00", - "2012-08-03 18:31:59.257000000 +0000 UTC", - "2015-09-30 18:48:56.35272715 +0000 UTC", - "2015-02-18 00:12:00 +0000 GMT", - "2015-02-18 00:12:00 +0000 UTC", - "2015-02-08 03:02:00 +0300 MSK m=+0.000000001", - "2015-02-08 03:02:00.001 +0300 MSK m=+0.000000001", - "2017-07-19 03:21:51+00:00", - "2014-04-26", - "2014-04", - "2014", - "2014-05-11 08:20:13,787", - // yyyy-mm-dd-07:00 - "2020-07-20+08:00", - // mm.dd.yy - "3.31.2014", - "03.31.2014", - "08.21.71", - "2014.03", - "2014.03.30", - // yyyymmdd and similar + // mm:dd:yyyy (see also PreferMonthFirst and RetryAmbiguousDateWithSwap options) + "08:03:2012", + "08:04:2012 18:31:59+00:00", + // yyyymmdd and similar "20140601", "20140722105203", - // yymmdd hh:mm:yy mysql log + "20140722105203.364", + // Chinese + "2014年4月25日", + "2014年04月08日", + "2014年04月08日 19:17:22 -0700", + // RabbitMQ log format + "8-Mar-2018::14:09:27", + "08-03-2018::02:09:29 PM", + // yymmdd hh:mm:yy mysql log // 080313 05:21:55 mysqld started "171113 14:14:20", + "190910 11:51:49", // unix seconds, ms, micro, nano "1332151919", "1384216367189", "1384216367111222", "1384216367111222333", + // syslog RFC3164 (and non-conformant variants) + "Apr 9 12:37:24", + "Apr 9 12:37:24-10", + "Apr 9 12:37:24-1000", + "Apr 9 12:37:24 UTC-10", + "Apr 9 12:37:24 MST", + "Apr 9 12:37:24 MST-07:00", + "Apr 9 12:37:24 TZ-10", + "Apr 9 12:37:24 TZ+02:00", + "Apr 9 12:37:24+10", + "Apr 9 12:37:24+10:00", + "Apr 9 12:37:24 CEST", + "Apr 9 12:37:24 CEST+0200", + "Apr 9 12:37:24 2025", + "Apr 9 12:37:24 2025 +02:00", + "Apr 9 2025 12:37:24", + "Apr 9 2025 12:37:24 -0700", + // syslog RFC5424 (and non-conformant variants) + "2025-04-09T12:37:24Z", + "2025-04-09T12:37:24.123Z", + "2025-04-09T12:37:24.123456Z", + "2025-04-09T12:37:24-10:00", + "2025-04-09T12:37:24.123 +0200", + "2025-04-09T12:37:24.123456 -0700 MDT", } var ( @@ -207,117 +370,209 @@ func main() { } /* -+-------------------------------------------------------+-----------------------------------------+ -| Input | Parsed, and Output as %v | -+-------------------------------------------------------+-----------------------------------------+ -| May 8, 2009 5:57:51 PM | 2009-05-08 17:57:51 +0000 UTC | -| oct 7, 1970 | 1970-10-07 00:00:00 +0000 UTC | -| oct 7, '70 | 1970-10-07 00:00:00 +0000 UTC | -| oct. 7, 1970 | 1970-10-07 00:00:00 +0000 UTC | -| oct. 7, 70 | 1970-10-07 00:00:00 +0000 UTC | -| Mon Jan 2 15:04:05 2006 | 2006-01-02 15:04:05 +0000 UTC | -| Mon Jan 2 15:04:05 MST 2006 | 2006-01-02 15:04:05 +0000 MST | -| Mon Jan 02 15:04:05 -0700 2006 | 2006-01-02 15:04:05 -0700 -0700 | -| Monday, 02-Jan-06 15:04:05 MST | 2006-01-02 15:04:05 +0000 MST | -| Mon, 02 Jan 2006 15:04:05 MST | 2006-01-02 15:04:05 +0000 MST | -| Tue, 11 Jul 2017 16:28:13 +0200 (CEST) | 2017-07-11 16:28:13 +0200 +0200 | -| Mon, 02 Jan 2006 15:04:05 -0700 | 2006-01-02 15:04:05 -0700 -0700 | -| Mon 30 Sep 2018 09:09:09 PM UTC | 2018-09-30 21:09:09 +0000 UTC | -| Mon Aug 10 15:44:11 UTC+0100 2015 | 2015-08-10 15:44:11 +0000 UTC | -| Thu, 4 Jan 2018 17:53:36 +0000 | 2018-01-04 17:53:36 +0000 UTC | -| Fri Jul 03 2015 18:04:07 GMT+0100 (GMT Daylight Time) | 2015-07-03 18:04:07 +0100 GMT | -| Sun, 3 Jan 2021 00:12:23 +0800 (GMT+08:00) | 2021-01-03 00:12:23 +0800 +0800 | -| September 17, 2012 10:09am | 2012-09-17 10:09:00 +0000 UTC | -| September 17, 2012 at 10:09am PST-08 | 2012-09-17 10:09:00 -0800 PST | -| September 17, 2012, 10:10:09 | 2012-09-17 10:10:09 +0000 UTC | -| October 7, 1970 | 1970-10-07 00:00:00 +0000 UTC | -| October 7th, 1970 | 1970-10-07 00:00:00 +0000 UTC | -| 12 Feb 2006, 19:17 | 2006-02-12 19:17:00 +0000 UTC | -| 12 Feb 2006 19:17 | 2006-02-12 19:17:00 +0000 UTC | -| 14 May 2019 19:11:40.164 | 2019-05-14 19:11:40.164 +0000 UTC | -| 7 oct 70 | 1970-10-07 00:00:00 +0000 UTC | -| 7 oct 1970 | 1970-10-07 00:00:00 +0000 UTC | -| 03 February 2013 | 2013-02-03 00:00:00 +0000 UTC | -| 1 July 2013 | 2013-07-01 00:00:00 +0000 UTC | -| 2013-Feb-03 | 2013-02-03 00:00:00 +0000 UTC | -| 06/Jan/2008:15:04:05 -0700 | 2008-01-06 15:04:05 -0700 -0700 | -| 06/Jan/2008 15:04:05 -0700 | 2008-01-06 15:04:05 -0700 -0700 | -| 3/31/2014 | 2014-03-31 00:00:00 +0000 UTC | -| 03/31/2014 | 2014-03-31 00:00:00 +0000 UTC | -| 08/21/71 | 1971-08-21 00:00:00 +0000 UTC | -| 8/1/71 | 1971-08-01 00:00:00 +0000 UTC | -| 4/8/2014 22:05 | 2014-04-08 22:05:00 +0000 UTC | -| 04/08/2014 22:05 | 2014-04-08 22:05:00 +0000 UTC | -| 4/8/14 22:05 | 2014-04-08 22:05:00 +0000 UTC | -| 04/2/2014 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | -| 8/8/1965 12:00:00 AM | 1965-08-08 00:00:00 +0000 UTC | -| 8/8/1965 01:00:01 PM | 1965-08-08 13:00:01 +0000 UTC | -| 8/8/1965 01:00 PM | 1965-08-08 13:00:00 +0000 UTC | -| 8/8/1965 1:00 PM | 1965-08-08 13:00:00 +0000 UTC | -| 8/8/1965 12:00 AM | 1965-08-08 00:00:00 +0000 UTC | -| 4/02/2014 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | -| 03/19/2012 10:11:59 | 2012-03-19 10:11:59 +0000 UTC | -| 03/19/2012 10:11:59.3186369 | 2012-03-19 10:11:59.3186369 +0000 UTC | -| 2014/3/31 | 2014-03-31 00:00:00 +0000 UTC | -| 2014/03/31 | 2014-03-31 00:00:00 +0000 UTC | -| 2014/4/8 22:05 | 2014-04-08 22:05:00 +0000 UTC | -| 2014/04/08 22:05 | 2014-04-08 22:05:00 +0000 UTC | -| 2014/04/2 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | -| 2014/4/02 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | -| 2012/03/19 10:11:59 | 2012-03-19 10:11:59 +0000 UTC | -| 2012/03/19 10:11:59.3186369 | 2012-03-19 10:11:59.3186369 +0000 UTC | -| 2014:3:31 | 2014-03-31 00:00:00 +0000 UTC | -| 2014:03:31 | 2014-03-31 00:00:00 +0000 UTC | -| 2014:4:8 22:05 | 2014-04-08 22:05:00 +0000 UTC | -| 2014:04:08 22:05 | 2014-04-08 22:05:00 +0000 UTC | -| 2014:04:2 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | -| 2014:4:02 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | -| 2012:03:19 10:11:59 | 2012-03-19 10:11:59 +0000 UTC | -| 2012:03:19 10:11:59.3186369 | 2012-03-19 10:11:59.3186369 +0000 UTC | -| 2014年04月08日 | 2014-04-08 00:00:00 +0000 UTC | -| 2006-01-02T15:04:05+0000 | 2006-01-02 15:04:05 +0000 UTC | -| 2009-08-12T22:15:09-07:00 | 2009-08-12 22:15:09 -0700 -0700 | -| 2009-08-12T22:15:09 | 2009-08-12 22:15:09 +0000 UTC | -| 2009-08-12T22:15:09.988 | 2009-08-12 22:15:09.988 +0000 UTC | -| 2009-08-12T22:15:09Z | 2009-08-12 22:15:09 +0000 UTC | -| 2017-07-19T03:21:51:897+0100 | 2017-07-19 03:21:51.897 +0100 +0100 | -| 2019-05-29T08:41-04 | 2019-05-29 08:41:00 -0400 -0400 | -| 2014-04-26 17:24:37.3186369 | 2014-04-26 17:24:37.3186369 +0000 UTC | -| 2012-08-03 18:31:59.257000000 | 2012-08-03 18:31:59.257 +0000 UTC | -| 2014-04-26 17:24:37.123 | 2014-04-26 17:24:37.123 +0000 UTC | -| 2013-04-01 22:43 | 2013-04-01 22:43:00 +0000 UTC | -| 2013-04-01 22:43:22 | 2013-04-01 22:43:22 +0000 UTC | -| 2014-12-16 06:20:00 UTC | 2014-12-16 06:20:00 +0000 UTC | -| 2014-12-16 06:20:00 GMT | 2014-12-16 06:20:00 +0000 UTC | -| 2014-04-26 05:24:37 PM | 2014-04-26 17:24:37 +0000 UTC | -| 2014-04-26 13:13:43 +0800 | 2014-04-26 13:13:43 +0800 +0800 | -| 2014-04-26 13:13:43 +0800 +08 | 2014-04-26 13:13:43 +0800 +0800 | -| 2014-04-26 13:13:44 +09:00 | 2014-04-26 13:13:44 +0900 +0900 | -| 2012-08-03 18:31:59.257000000 +0000 UTC | 2012-08-03 18:31:59.257 +0000 UTC | -| 2015-09-30 18:48:56.35272715 +0000 UTC | 2015-09-30 18:48:56.35272715 +0000 UTC | -| 2015-02-18 00:12:00 +0000 GMT | 2015-02-18 00:12:00 +0000 UTC | -| 2015-02-18 00:12:00 +0000 UTC | 2015-02-18 00:12:00 +0000 UTC | -| 2015-02-08 03:02:00 +0300 MSK m=+0.000000001 | 2015-02-08 03:02:00 +0300 +0300 | -| 2015-02-08 03:02:00.001 +0300 MSK m=+0.000000001 | 2015-02-08 03:02:00.001 +0300 +0300 | -| 2017-07-19 03:21:51+00:00 | 2017-07-19 03:21:51 +0000 UTC | -| 2014-04-26 | 2014-04-26 00:00:00 +0000 UTC | -| 2014-04 | 2014-04-01 00:00:00 +0000 UTC | -| 2014 | 2014-01-01 00:00:00 +0000 UTC | -| 2014-05-11 08:20:13,787 | 2014-05-11 08:20:13.787 +0000 UTC | -| 2020-07-20+08:00 | 2020-07-20 00:00:00 +0800 +0800 | -| 3.31.2014 | 2014-03-31 00:00:00 +0000 UTC | -| 03.31.2014 | 2014-03-31 00:00:00 +0000 UTC | -| 08.21.71 | 1971-08-21 00:00:00 +0000 UTC | -| 2014.03 | 2014-03-01 00:00:00 +0000 UTC | -| 2014.03.30 | 2014-03-30 00:00:00 +0000 UTC | -| 20140601 | 2014-06-01 00:00:00 +0000 UTC | -| 20140722105203 | 2014-07-22 10:52:03 +0000 UTC | -| 171113 14:14:20 | 2017-11-13 14:14:20 +0000 UTC | -| 1332151919 | 2012-03-19 10:11:59 +0000 UTC | -| 1384216367189 | 2013-11-12 00:32:47.189 +0000 UTC | -| 1384216367111222 | 2013-11-12 00:32:47.111222 +0000 UTC | -| 1384216367111222333 | 2013-11-12 00:32:47.111222333 +0000 UTC | -+-------------------------------------------------------+-----------------------------------------+ ++------------------------------------------------------------+-----------------------------------------+ +| Input | Parsed, and Output as %v | ++------------------------------------------------------------+-----------------------------------------+ +| May 8, 2009 5:57:51 PM | 2009-05-08 17:57:51 +0000 UTC | +| oct 7, 1970 | 1970-10-07 00:00:00 +0000 UTC | +| oct 7, '70 | 1970-10-07 00:00:00 +0000 UTC | +| oct. 7, 1970 | 1970-10-07 00:00:00 +0000 UTC | +| oct. 7, 70 | 1970-10-07 00:00:00 +0000 UTC | +| October 7, 1970 | 1970-10-07 00:00:00 +0000 UTC | +| October 7th, 1970 | 1970-10-07 00:00:00 +0000 UTC | +| Sept. 7, 1970 11:15:26pm | 1970-09-07 23:15:26 +0000 UTC | +| Sep 7 2009 11:15:26.123 PM PST | 2009-09-07 23:15:26.123 +0000 PST | +| September 3rd, 2009 11:15:26.123456789pm | 2009-09-03 23:15:26.123456789 +0000 UTC | +| September 17 2012 10:09am | 2012-09-17 10:09:00 +0000 UTC | +| September 17, 2012, 10:10:09 | 2012-09-17 10:10:09 +0000 UTC | +| Sep 17, 2012 at 10:02am (EST) | 2012-09-17 10:02:00 +0000 EST | +| September 17, 2012 at 10:09am PST-08 | 2012-09-17 10:09:00 -0800 PST | +| September 17 2012 5:00pm UTC-0700 | 2012-09-17 17:00:00 -0700 -0700 | +| September 17 2012 5:00pm GMT-0700 | 2012-09-17 17:00:00 -0700 -0700 | +| 7 oct 70 | 1970-10-07 00:00:00 +0000 UTC | +| 7 Oct 1970 | 1970-10-07 00:00:00 +0000 UTC | +| 7 September 1970 23:15 | 1970-09-07 23:15:00 +0000 UTC | +| 7 September 1970 11:15:26pm | 1970-09-07 23:15:26 +0000 UTC | +| 03 February 2013 | 2013-02-03 00:00:00 +0000 UTC | +| 12 Feb 2006, 19:17 | 2006-02-12 19:17:00 +0000 UTC | +| 12 Feb 2006 19:17 | 2006-02-12 19:17:00 +0000 UTC | +| 14 May 2019 19:11:40.164 | 2019-05-14 19:11:40.164 +0000 UTC | +| 4th Sep 2012 | 2012-09-04 00:00:00 +0000 UTC | +| 1st February 2018 13:58:24 | 2018-02-01 13:58:24 +0000 UTC | +| Mon, 02 Jan 2006 15:04:05 MST | 2006-01-02 15:04:05 +0000 MST | +| Mon, 02 Jan 2006 15:04:05 -0700 | 2006-01-02 15:04:05 -0700 -0700 | +| Tue, 11 Jul 2017 16:28:13 +0200 (CEST) | 2017-07-11 16:28:13 +0200 CEST | +| Mon 30 Sep 2018 09:09:09 PM UTC | 2018-09-30 21:09:09 +0000 UTC | +| Sun, 07 Jun 2020 00:00:00 +0100 | 2020-06-07 00:00:00 +0100 +0100 | +| Wed, 8 Feb 2023 19:00:46 +1100 (AEDT) | 2023-02-08 19:00:46 +1100 AEDT | +| Mon Jan 2 15:04:05 2006 | 2006-01-02 15:04:05 +0000 UTC | +| Mon Jan 2 15:04:05 MST 2006 | 2006-01-02 15:04:05 +0000 MST | +| Monday Jan 02 15:04:05 -0700 2006 | 2006-01-02 15:04:05 -0700 -0700 | +| Mon Jan 2 15:04:05.103786 2006 | 2006-01-02 15:04:05.103786 +0000 UTC | +| Mon Jan 02 15:04:05 -0700 2006 | 2006-01-02 15:04:05 -0700 -0700 | +| Mon 02 Jan 2006 03:04:05 PM UTC | 2006-01-02 15:04:05 +0000 UTC | +| Monday 02 Jan 2006 03:04:05 PM MST | 2006-01-02 15:04:05 +0000 MST | +| Mon Aug 10 15:44:11 UTC+0000 2015 | 2015-08-10 15:44:11 +0000 UTC | +| Thu Apr 7 15:13:13 2005 -0700 | 2005-04-07 15:13:13 -0700 -0700 | +| Thu Apr 7 15:13:13 2005 -07:00 | 2005-04-07 15:13:13 -0700 -0700 | +| Thu Apr 7 15:13:13 2005 -07:00 PST | 2005-04-07 15:13:13 -0700 PST | +| Thu Apr 7 15:13:13 2005 -07:00 PST (Pacific Standard Time) | 2005-04-07 15:13:13 -0700 PST | +| Thu Apr 7 15:13:13 -0700 2005 | 2005-04-07 15:13:13 -0700 -0700 | +| Thu Apr 7 15:13:13 -07:00 2005 | 2005-04-07 15:13:13 -0700 -0700 | +| Thu Apr 7 15:13:13 -0700 PST 2005 | 2005-04-07 15:13:13 -0700 PST | +| Thu Apr 7 15:13:13 -07:00 PST 2005 | 2005-04-07 15:13:13 -0700 PST | +| Thu Apr 7 15:13:13 PST 2005 | 2005-04-07 15:13:13 +0000 PST | +| Fri Jul 3 2015 06:04:07 PST-0700 (Pacific Daylight Time) | 2015-07-03 06:04:07 -0700 PST | +| Fri Jul 03 2015 18:04:07 GMT+0100 (GMT Daylight Time) | 2015-07-03 18:04:07 +0100 +0100 | +| Sun, 3 Jan 2021 00:12:23 +0800 (GMT+08:00) | 2021-01-03 00:12:23 +0800 +0800 | +| 2013 May 2 | 2013-05-02 00:00:00 +0000 UTC | +| 2013 May 02 11:37:55 | 2013-05-02 11:37:55 +0000 UTC | +| 06/Jan/2008 15:04:05 -0700 | 2008-01-06 15:04:05 -0700 -0700 | +| 06/January/2008 15:04:05 -0700 | 2008-01-06 15:04:05 -0700 -0700 | +| 06/Jan/2008:15:04:05 -0700 | 2008-01-06 15:04:05 -0700 -0700 | +| 06/January/2008:08:11:17 -0700 | 2008-01-06 08:11:17 -0700 -0700 | +| 3/31/2014 | 2014-03-31 00:00:00 +0000 UTC | +| 03/31/2014 | 2014-03-31 00:00:00 +0000 UTC | +| 08/21/71 | 1971-08-21 00:00:00 +0000 UTC | +| 8/1/71 | 1971-08-01 00:00:00 +0000 UTC | +| 4/8/2014 22:05 | 2014-04-08 22:05:00 +0000 UTC | +| 04/08/2014 22:05 | 2014-04-08 22:05:00 +0000 UTC | +| 04/08/2014, 22:05 | 2014-04-08 22:05:00 +0000 UTC | +| 4/8/14 22:05 | 2014-04-08 22:05:00 +0000 UTC | +| 04/2/2014 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | +| 8/8/1965 1:00 PM | 1965-08-08 13:00:00 +0000 UTC | +| 8/8/1965 01:00 PM | 1965-08-08 13:00:00 +0000 UTC | +| 8/8/1965 12:00 AM | 1965-08-08 00:00:00 +0000 UTC | +| 8/8/1965 12:00:00AM | 1965-08-08 00:00:00 +0000 UTC | +| 8/8/1965 01:00:01 PM | 1965-08-08 13:00:01 +0000 UTC | +| 8/8/1965 01:00:01PM -0700 | 1965-08-08 13:00:01 -0700 -0700 | +| 8/8/1965 13:00:01 -0700 PST | 1965-08-08 13:00:01 -0700 PST | +| 8/8/1965 01:00:01 PM -0700 PST | 1965-08-08 13:00:01 -0700 PST | +| 8/8/1965 01:00:01 PM -07:00 PST (Pacific Standard Time) | 1965-08-08 13:00:01 -0700 PST | +| 4/02/2014 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | +| 03/19/2012 10:11:59 | 2012-03-19 10:11:59 +0000 UTC | +| 03/19/2012 10:11:59.3186369 | 2012-03-19 10:11:59.3186369 +0000 UTC | +| Oct/ 7/1970 | 1970-10-07 00:00:00 +0000 UTC | +| Oct/03/1970 22:33:44 | 1970-10-03 22:33:44 +0000 UTC | +| February/03/1970 11:33:44.555 PM PST | 1970-02-03 23:33:44.555 +0000 PST | +| 2014/3/31 | 2014-03-31 00:00:00 +0000 UTC | +| 2014/03/31 | 2014-03-31 00:00:00 +0000 UTC | +| 2014/4/8 22:05 | 2014-04-08 22:05:00 +0000 UTC | +| 2014/04/08 22:05 | 2014-04-08 22:05:00 +0000 UTC | +| 2014/04/2 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | +| 2014/4/02 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | +| 2012/03/19 10:11:59 | 2012-03-19 10:11:59 +0000 UTC | +| 2012/03/19 10:11:59.3186369 | 2012-03-19 10:11:59.3186369 +0000 UTC | +| Fri, 03-Jul-15 08:08:08 CEST | 2015-07-03 08:08:08 +0000 CEST | +| Monday, 02-Jan-06 15:04:05 MST | 2006-01-02 15:04:05 +0000 MST | +| Monday, 02 Jan 2006 15:04:05 -0600 | 2006-01-02 15:04:05 -0600 -0600 | +| 02-Jan-06 15:04:05 MST | 2006-01-02 15:04:05 +0000 MST | +| 2006-01-02T15:04:05+0000 | 2006-01-02 15:04:05 +0000 UTC | +| 2009-08-12T22:15:09-07:00 | 2009-08-12 22:15:09 -0700 -0700 | +| 2009-08-12T22:15:09 | 2009-08-12 22:15:09 +0000 UTC | +| 2009-08-12T22:15:09.988 | 2009-08-12 22:15:09.988 +0000 UTC | +| 2009-08-12T22:15:09Z | 2009-08-12 22:15:09 +0000 UTC | +| 2009-08-12T22:15:09.52Z | 2009-08-12 22:15:09.52 +0000 UTC | +| 2017-07-19T03:21:51:897+0100 | 2017-07-19 03:21:51.897 +0100 +0100 | +| 2019-05-29T08:41-04 | 2019-05-29 08:41:00 -0400 -0400 | +| 2014-04-26 17:24:37.3186369 | 2014-04-26 17:24:37.3186369 +0000 UTC | +| 2012-08-03 18:31:59.257000000 | 2012-08-03 18:31:59.257 +0000 UTC | +| 2014-04-26 17:24:37.123 | 2014-04-26 17:24:37.123 +0000 UTC | +| 2014-04-01 12:01am | 2014-04-01 00:01:00 +0000 UTC | +| 2014-04-01 12:01:59.765 AM | 2014-04-01 00:01:59.765 +0000 UTC | +| 2014-04-01 12:01:59,765 | 2014-04-01 12:01:59.765 +0000 UTC | +| 2014-04-01 22:43 | 2014-04-01 22:43:00 +0000 UTC | +| 2014-04-01 22:43:22 | 2014-04-01 22:43:22 +0000 UTC | +| 2014-12-16 06:20:00 UTC | 2014-12-16 06:20:00 +0000 UTC | +| 2014-12-16 06:20:00 GMT | 2014-12-16 06:20:00 +0000 GMT | +| 2014-04-26 05:24:37 PM | 2014-04-26 17:24:37 +0000 UTC | +| 2014-04-26 13:13:43 +0800 | 2014-04-26 13:13:43 +0800 +0800 | +| 2014-04-26 13:13:43 +0800 +08 | 2014-04-26 13:13:43 +0800 +0800 | +| 2014-04-26 13:13:44 +09:00 | 2014-04-26 13:13:44 +0900 +0900 | +| 2012-08-03 18:31:59.257000000 +0000 UTC | 2012-08-03 18:31:59.257 +0000 UTC | +| 2015-09-30 18:48:56.35272715 +0000 UTC | 2015-09-30 18:48:56.35272715 +0000 UTC | +| 2015-02-18 00:12:00 +0000 GMT | 2015-02-18 00:12:00 +0000 GMT | +| 2015-02-18 00:12:00 +0000 UTC | 2015-02-18 00:12:00 +0000 UTC | +| 2015-02-08 03:02:00 +0300 MSK m=+0.000000001 | 2015-02-08 03:02:00 +0300 MSK | +| 2015-02-08 03:02:00.001 +0300 MSK m=+0.000000001 | 2015-02-08 03:02:00.001 +0300 MSK | +| 2017-07-19 03:21:51+00:00 | 2017-07-19 03:21:51 +0000 UTC | +| 2017-04-03 22:32:14.322 CET | 2017-04-03 22:32:14.322 +0000 CET | +| 2017-04-03 22:32:14,322 CET | 2017-04-03 22:32:14.322 +0000 CET | +| 2017-04-03 22:32:14:322 CET | 2017-04-03 22:32:14.322 +0000 CET | +| 2018-09-30 08:09:13.123PM PMDT | 2018-09-30 20:09:13.123 +0000 PMDT | +| 2018-09-30 08:09:13.123 am AMT | 2018-09-30 08:09:13.123 +0000 AMT | +| 2014-04-26 | 2014-04-26 00:00:00 +0000 UTC | +| 2014-04 | 2014-04-01 00:00:00 +0000 UTC | +| 2014 | 2014-01-01 00:00:00 +0000 UTC | +| 2020-07-20+08:00 | 2020-07-20 00:00:00 +0800 +0800 | +| 2020-07-20+0800 | 2020-07-20 00:00:00 +0800 +0800 | +| 2013-Feb-03 | 2013-02-03 00:00:00 +0000 UTC | +| 2013-February-03 09:07:08.123 | 2013-02-03 09:07:08.123 +0000 UTC | +| 03-Feb-13 | 2013-02-03 00:00:00 +0000 UTC | +| 03-Feb-2013 | 2013-02-03 00:00:00 +0000 UTC | +| 07-Feb-2004 09:07:07 +0200 | 2004-02-07 09:07:07 +0200 +0200 | +| 07-February-2004 09:07:07 +0200 | 2004-02-07 09:07:07 +0200 +0200 | +| 28-02-02 | 2002-02-28 00:00:00 +0000 UTC | +| 28-02-02 15:16:17 | 2002-02-28 15:16:17 +0000 UTC | +| 28-02-2002 | 2002-02-28 00:00:00 +0000 UTC | +| 28-02-2002 15:16:17 | 2002-02-28 15:16:17 +0000 UTC | +| 3.31.2014 | 2014-03-31 00:00:00 +0000 UTC | +| 03.31.14 | 2014-03-31 00:00:00 +0000 UTC | +| 03.31.2014 | 2014-03-31 00:00:00 +0000 UTC | +| 03.31.2014 10:11:59 MST | 2014-03-31 10:11:59 +0000 MST | +| 03.31.2014 10:11:59.3186369Z | 2014-03-31 10:11:59.3186369 +0000 UTC | +| 2014.03 | 2014-03-01 00:00:00 +0000 UTC | +| 2014.03.30 | 2014-03-30 00:00:00 +0000 UTC | +| 2014.03.30 08:33pm | 2014-03-30 20:33:00 +0000 UTC | +| 2014.03.30T08:33:44.555 PM -0700 MST | 2014-03-30 20:33:44.555 -0700 MST | +| 2014.03.30-0600 | 2014-03-30 00:00:00 -0600 -0600 | +| 2014:3:31 | 2014-03-31 00:00:00 +0000 UTC | +| 2014:03:31 | 2014-03-31 00:00:00 +0000 UTC | +| 2014:4:8 22:05 | 2014-04-08 22:05:00 +0000 UTC | +| 2014:04:08 22:05 | 2014-04-08 22:05:00 +0000 UTC | +| 2014:04:2 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | +| 2014:4:02 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | +| 2012:03:19 10:11:59 | 2012-03-19 10:11:59 +0000 UTC | +| 2012:03:19 10:11:59.3186369 | 2012-03-19 10:11:59.3186369 +0000 UTC | +| 08:03:2012 | 2012-08-03 00:00:00 +0000 UTC | +| 08:04:2012 18:31:59+00:00 | 2012-08-04 18:31:59 +0000 UTC | +| 20140601 | 2014-06-01 00:00:00 +0000 UTC | +| 20140722105203 | 2014-07-22 10:52:03 +0000 UTC | +| 20140722105203.364 | 2014-07-22 10:52:03.364 +0000 UTC | +| 2014年4月25日 | 2014-04-25 00:00:00 +0000 UTC | +| 2014年04月08日 | 2014-04-08 00:00:00 +0000 UTC | +| 2014年04月08日 19:17:22 -0700 | 2014-04-08 19:17:22 -0700 -0700 | +| 8-Mar-2018::14:09:27 | 2018-03-08 14:09:27 +0000 UTC | +| 08-03-2018::02:09:29 PM | 2018-03-08 14:09:29 +0000 UTC | +| 171113 14:14:20 | 2017-11-13 14:14:20 +0000 UTC | +| 190910 11:51:49 | 2019-09-10 11:51:49 +0000 UTC | +| 1332151919 | 2012-03-19 10:11:59 +0000 UTC | +| 1384216367189 | 2013-11-12 00:32:47.189 +0000 UTC | +| 1384216367111222 | 2013-11-12 00:32:47.111222 +0000 UTC | +| 1384216367111222333 | 2013-11-12 00:32:47.111222333 +0000 UTC | +| Apr 9 12:37:24 | 0000-04-09 12:37:24 +0000 UTC | +| Apr 9 12:37:24-10 | 0000-04-09 12:37:24 -1000 -1000 | +| Apr 9 12:37:24-1000 | 0000-04-09 12:37:24 -1000 -1000 | +| Apr 9 12:37:24 UTC-10 | 0000-04-09 12:37:24 -1000 -1000 | +| Apr 9 12:37:24 MST | 0000-04-09 12:37:24 +0000 MST | +| Apr 9 12:37:24 MST-07:00 | 0000-04-09 12:37:24 -0700 MST | +| Apr 9 12:37:24 TZ-10 | 0000-04-09 12:37:24 -1000 -1000 | +| Apr 9 12:37:24 TZ+02:00 | 0000-04-09 12:37:24 +0200 +0200 | +| Apr 9 12:37:24+10 | 0000-04-09 12:37:24 +1000 +1000 | +| Apr 9 12:37:24+10:00 | 0000-04-09 12:37:24 +1000 +1000 | +| Apr 9 12:37:24 CEST | 0000-04-09 12:37:24 +0000 CEST | +| Apr 9 12:37:24 CEST+0200 | 0000-04-09 12:37:24 +0200 CEST | +| Apr 9 12:37:24 2025 | 2025-04-09 12:37:24 +0000 UTC | +| Apr 9 12:37:24 2025 +02:00 | 2025-04-09 12:37:24 +0200 +0200 | +| Apr 9 2025 12:37:24 | 2025-04-09 12:37:24 +0000 UTC | +| Apr 9 2025 12:37:24 -0700 | 2025-04-09 12:37:24 -0700 -0700 | +| 2025-04-09T12:37:24Z | 2025-04-09 12:37:24 +0000 UTC | +| 2025-04-09T12:37:24.123Z | 2025-04-09 12:37:24.123 +0000 UTC | +| 2025-04-09T12:37:24.123456Z | 2025-04-09 12:37:24.123456 +0000 UTC | +| 2025-04-09T12:37:24-10:00 | 2025-04-09 12:37:24 -1000 -1000 | +| 2025-04-09T12:37:24.123 +0200 | 2025-04-09 12:37:24.123 +0200 +0200 | +| 2025-04-09T12:37:24.123456 -0700 MDT | 2025-04-09 12:37:24.123456 -0700 MDT | ++------------------------------------------------------------+-----------------------------------------+ */ ``` diff --git a/bench_test.go b/bench_test.go index 0c6739a..b5b1f7a 100644 --- a/bench_test.go +++ b/bench_test.go @@ -7,12 +7,8 @@ import ( ) /* - go test -bench Parse -BenchmarkShotgunParse 50000 37588 ns/op 13258 B/op 167 allocs/op -BenchmarkDateparseParseAny 500000 5752 ns/op 0 B/op 0 allocs/op - // Aarons Laptop Lenovo 900 Feb 2018 BenchmarkShotgunParse-4 50000 30045 ns/op 13136 B/op 169 allocs/op BenchmarkParseAny-4 200000 8627 ns/op 144 B/op 3 allocs/op @@ -22,13 +18,26 @@ BenchmarkShotgunParse-8 50000 33940 ns/op 13136 B/op 169 allo BenchmarkParseAny-8 200000 10146 ns/op 912 B/op 29 allocs/op BenchmarkParseDateString-8 10000 123077 ns/op 208 B/op 13 allocs/op +// Klondike Dragon Dec 2023 +cpu: 12th Gen Intel(R) Core(TM) i7-1255U +BenchmarkShotgunParse-12 62788 18113 ns/op 19448 B/op 474 allocs/op +BenchmarkParseAny-12 347020 3455 ns/op 48 B/op 2 allocs/op +BenchmarkBigShotgunParse-12 1226 951271 ns/op 1214937 B/op 27245 allocs/op +BenchmarkBigParseAny-12 4234 267893 ns/op 27492 B/op 961 allocs/op +BenchmarkBigParseIn-12 4032 280900 ns/op 30422 B/op 1033 allocs/op +BenchmarkBigParseRetryAmbiguous-12 4453 282475 ns/op 29558 B/op 1030 allocs/op +BenchmarkShotgunParseErrors-12 19240 62641 ns/op 67080 B/op 1679 allocs/op +BenchmarkParseAnyErrors-12 185677 6179 ns/op 752 B/op 23 allocs/op +BenchmarkBigParseAnyErrors-12 26688 44885 ns/op 480 B/op 94 allocs/op +BenchmarkParseAmbiguous-12 1590302 752.9 ns/op 296 B/op 7 allocs/op +BenchmarkParseWeekdayAndFullMonth-12 2141109 555.0 ns/op 16 B/op 2 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,11 +46,115 @@ func BenchmarkParseAny(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { for _, dateStr := range testDates { - ParseAny(dateStr) + _, _ = ParseAny(dateStr) } } } +func BenchmarkBigShotgunParse(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + for _, t := range testInputs { + // This is the non dateparse traditional approach + _, _ = parseShotgunStyle(t.in) + } + } +} + +func BenchmarkBigParseAny(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + for _, t := range testInputs { + _, _ = ParseAny(t.in) + } + } +} + +func BenchmarkBigParseIn(b *testing.B) { + b.ReportAllocs() + loc, _ := time.LoadLocation("America/New_York") + for i := 0; i < b.N; i++ { + for _, t := range testInputs { + _, _ = ParseIn(t.in, loc) + } + } +} + +func BenchmarkBigParseRetryAmbiguous(b *testing.B) { + b.ReportAllocs() + opts := []ParserOption{RetryAmbiguousDateWithSwap(true)} + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, t := range testInputs { + _, _ = ParseAny(t.in, opts...) + } + } +} + +func BenchmarkShotgunParseErrors(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + for _, t := range testParseErrors { + // This is the non dateparse traditional approach + _, _ = parseShotgunStyle(t.in) + } + } +} + +func BenchmarkParseAnyErrors(b *testing.B) { + b.ReportAllocs() + opts := []ParserOption{SimpleErrorMessages(true)} + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, t := range testParseErrors { + _, _ = ParseAny(t.in, opts...) + } + } +} + +func BenchmarkBigParseAnyErrors(b *testing.B) { + b.ReportAllocs() + + opts := []ParserOption{SimpleErrorMessages(true)} + // manufacture a bunch of different tests with random errors put in them + var testBigErrorInputs []string + for index, t := range testInputs { + b := []byte(t.in) + spread := 4 + (index % 4) + startingIndex := spread % len(b) + for i := startingIndex; i < len(b); i += spread { + b[i] = '?' + } + testBigErrorInputs = append(testBigErrorInputs, string(b)) + } + b.ResetTimer() + + for i := 0; i < b.N; i++ { + for _, in := range testBigErrorInputs { + _, err := ParseAny(in, opts...) + if err == nil { + panic(fmt.Sprintf("expected parsing to fail: %s", in)) + } + } + } +} + +func BenchmarkParseAmbiguous(b *testing.B) { + b.ReportAllocs() + opts := []ParserOption{RetryAmbiguousDateWithSwap(true)} + b.ResetTimer() + for i := 0; i < b.N; i++ { + MustParse("13/02/2014 04:08:09 +0000 UTC", opts...) + } +} + +func BenchmarkParseWeekdayAndFullMonth(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + MustParse("Monday 02 December 2006 03:04:05 PM UTC") + } +} + /* func BenchmarkParseDateString(b *testing.B) { b.ReportAllocs() @@ -70,7 +183,7 @@ var ( "2014-04-26", } - ErrDateFormat = fmt.Errorf("Invalid Date Format") + ErrDateFormat = fmt.Errorf("invalid date format") timeFormats = []string{ // ISO 8601ish formats diff --git a/dateparse/README.md b/dateparse/README.md index cfc001c..128fa11 100644 --- a/dateparse/README.md +++ b/dateparse/README.md @@ -6,7 +6,7 @@ Simple CLI to test out dateparse. ```sh -# Since this date string has no timezone/offset so is more effected by +# Since this date string has no timezone/offset it's more affected by # which method you use to parse $ dateparse --timezone="America/Denver" "2017-07-19 03:21:00" @@ -169,4 +169,44 @@ Your Using time.Local set to location=America/New_York EDT | ParseAny | time.Local = time.UTC | 2017-03-03 00:00:00 +0000 UTC | 2017-03-03 00:00:00 +0000 UTC day=5 | +-------------+---------------------------+----------------------------------------------------+----------------------------------------------------+ +# Automatically retry date formats that are ambiguous mm/dd vs dd/mm +$ ./dateparse --retry-ambiguous "28.09.2024" + +Your Current time.Local zone is MDT + +Layout String: dateparse.ParseFormat() => 02.01.2006 + ++-------------+-----------------------+----------------------------------------------------+----------------------------------------------------+ +| method | Zone Source | Parsed | Parsed: t.In(time.UTC) | ++-------------+-----------------------+----------------------------------------------------+----------------------------------------------------+ +| ParseIn | time.Local = nil | 2024-09-28 00:00:00 +0000 UTC | 2024-09-28 00:00:00 +0000 UTC | +| ParseIn | time.Local = time.UTC | 2024-09-28 00:00:00 +0000 UTC | 2024-09-28 00:00:00 +0000 UTC | +| ParseLocal | time.Local = nil | 2024-09-28 00:00:00 +0000 UTC | 2024-09-28 00:00:00 +0000 UTC | +| ParseLocal | time.Local = time.UTC | 2024-09-28 00:00:00 +0000 UTC | 2024-09-28 00:00:00 +0000 UTC | +| ParseStrict | time.Local = nil | this date has ambiguous mm/dd vs dd/mm type format | this date has ambiguous mm/dd vs dd/mm type format | +| ParseStrict | time.Local = time.UTC | this date has ambiguous mm/dd vs dd/mm type format | this date has ambiguous mm/dd vs dd/mm type format | +| ParseAny | time.Local = nil | 2024-09-28 00:00:00 +0000 UTC | 2024-09-28 00:00:00 +0000 UTC day=6 | +| ParseAny | time.Local = time.UTC | 2024-09-28 00:00:00 +0000 UTC | 2024-09-28 00:00:00 +0000 UTC day=6 | ++-------------+-----------------------+----------------------------------------------------+----------------------------------------------------+ + +# Force dates to be interpreted as day-first instead of month-first +$ ./dateparse --prefer-day-first "28.09.2024" + +Your Current time.Local zone is MDT + +Layout String: dateparse.ParseFormat() => 02.01.2006 + ++-------------+-----------------------+----------------------------------------------------+----------------------------------------------------+ +| method | Zone Source | Parsed | Parsed: t.In(time.UTC) | ++-------------+-----------------------+----------------------------------------------------+----------------------------------------------------+ +| ParseAny | time.Local = nil | 2024-09-28 00:00:00 +0000 UTC | 2024-09-28 00:00:00 +0000 UTC day=6 | +| ParseAny | time.Local = time.UTC | 2024-09-28 00:00:00 +0000 UTC | 2024-09-28 00:00:00 +0000 UTC day=6 | +| ParseIn | time.Local = nil | 2024-09-28 00:00:00 +0000 UTC | 2024-09-28 00:00:00 +0000 UTC | +| ParseIn | time.Local = time.UTC | 2024-09-28 00:00:00 +0000 UTC | 2024-09-28 00:00:00 +0000 UTC | +| ParseLocal | time.Local = nil | 2024-09-28 00:00:00 +0000 UTC | 2024-09-28 00:00:00 +0000 UTC | +| ParseLocal | time.Local = time.UTC | 2024-09-28 00:00:00 +0000 UTC | 2024-09-28 00:00:00 +0000 UTC | +| ParseStrict | time.Local = nil | this date has ambiguous mm/dd vs dd/mm type format | this date has ambiguous mm/dd vs dd/mm type format | +| ParseStrict | time.Local = time.UTC | this date has ambiguous mm/dd vs dd/mm type format | this date has ambiguous mm/dd vs dd/mm type format | ++-------------+-----------------------+----------------------------------------------------+----------------------------------------------------+ + ``` \ No newline at end of file diff --git a/dateparse/main.go b/dateparse/main.go index 6c3c20a..9ec8bcb 100644 --- a/dateparse/main.go +++ b/dateparse/main.go @@ -6,17 +6,32 @@ import ( "os" "time" + "github.com/itlightning/dateparse" "github.com/scylladb/termtables" - "github.com/araddon/dateparse" ) var ( - timezone = "" - datestr = "" + timezone = "" + datestr = "" + retryAmbiguousDateWithSwap = false + preferDayFirst = false + parserOptions = []dateparse.ParserOption{} ) +func buildParserOptions() { + parserOptions = []dateparse.ParserOption{} + if retryAmbiguousDateWithSwap { + parserOptions = append(parserOptions, dateparse.RetryAmbiguousDateWithSwap(true)) + } + if preferDayFirst { + parserOptions = append(parserOptions, dateparse.PreferMonthFirst(false)) + } +} + func main() { flag.StringVar(&timezone, "timezone", "", "Timezone aka `America/Los_Angeles` formatted time-zone") + flag.BoolVar(&retryAmbiguousDateWithSwap, "retry-ambiguous", false, "Retry ambiguous date/time formats (day-first vs month-first)") + flag.BoolVar(&preferDayFirst, "prefer-day-first", false, "Prefer day-first date format") flag.Parse() if len(flag.Args()) == 0 { @@ -25,13 +40,17 @@ func main() { ./dateparse "2009-08-12T22:15:09.99Z" ./dateparse --timezone="America/Denver" "2017-07-19 03:21:51+00:00" + ./dateparse --prefer-day-first "28.09.2024" + ./dateparse --retry-ambiguous "28.09.2024" `) return } + buildParserOptions() + datestr = flag.Args()[0] - layout, err := dateparse.ParseFormat(datestr) + layout, err := dateparse.ParseFormat(datestr, parserOptions...) if err != nil { fatal(err) } @@ -82,7 +101,7 @@ type parser func(datestr string, loc *time.Location, utc bool) string func parseLocal(datestr string, loc *time.Location, utc bool) string { time.Local = loc - t, err := dateparse.ParseLocal(datestr) + t, err := dateparse.ParseLocal(datestr, parserOptions...) if err != nil { return err.Error() } @@ -93,7 +112,7 @@ func parseLocal(datestr string, loc *time.Location, utc bool) string { } func parseIn(datestr string, loc *time.Location, utc bool) string { - t, err := dateparse.ParseIn(datestr, loc) + t, err := dateparse.ParseIn(datestr, loc, parserOptions...) if err != nil { return err.Error() } @@ -104,7 +123,7 @@ func parseIn(datestr string, loc *time.Location, utc bool) string { } func parseAny(datestr string, loc *time.Location, utc bool) string { - t, err := dateparse.ParseAny(datestr) + t, err := dateparse.ParseAny(datestr, parserOptions...) if err != nil { return err.Error() } @@ -115,7 +134,7 @@ func parseAny(datestr string, loc *time.Location, utc bool) string { } func parseStrict(datestr string, loc *time.Location, utc bool) string { - t, err := dateparse.ParseStrict(datestr) + t, err := dateparse.ParseStrict(datestr, parserOptions...) if err != nil { return err.Error() } diff --git a/example/main.go b/example/main.go index 9896f04..1d9494c 100644 --- a/example/main.go +++ b/example/main.go @@ -5,61 +5,108 @@ import ( "fmt" "time" - "github.com/araddon/dateparse" + "github.com/itlightning/dateparse" "github.com/scylladb/termtables" ) var examples = []string{ + // mon day year (time) "May 8, 2009 5:57:51 PM", "oct 7, 1970", "oct 7, '70", "oct. 7, 1970", "oct. 7, 70", - "Mon Jan 2 15:04:05 2006", - "Mon Jan 2 15:04:05 MST 2006", - "Mon Jan 02 15:04:05 -0700 2006", - "Monday, 02-Jan-06 15:04:05 MST", - "Mon, 02 Jan 2006 15:04:05 MST", - "Tue, 11 Jul 2017 16:28:13 +0200 (CEST)", - "Mon, 02 Jan 2006 15:04:05 -0700", - "Mon 30 Sep 2018 09:09:09 PM UTC", - "Mon Aug 10 15:44:11 UTC+0100 2015", - "Thu, 4 Jan 2018 17:53:36 +0000", - "Fri Jul 03 2015 18:04:07 GMT+0100 (GMT Daylight Time)", - "Sun, 3 Jan 2021 00:12:23 +0800 (GMT+08:00)", - "September 17, 2012 10:09am", - "September 17, 2012 at 10:09am PST-08", - "September 17, 2012, 10:10:09", "October 7, 1970", "October 7th, 1970", + "Sept. 7, 1970 11:15:26pm", + "Sep 7 2009 11:15:26.123 PM PST", + "September 3rd, 2009 11:15:26.123456789pm", + "September 17 2012 10:09am", + "September 17, 2012, 10:10:09", + "Sep 17, 2012 at 10:02am (EST)", + // (PST-08 will have an offset of -0800, and a zone name of "PST") + "September 17, 2012 at 10:09am PST-08", + // (UTC-0700 has the same offset as -0700, and the returned zone name will be empty) + "September 17 2012 5:00pm UTC-0700", + "September 17 2012 5:00pm GMT-0700", + // (weekday) day mon year (time) + "7 oct 70", + "7 Oct 1970", + "7 September 1970 23:15", + "7 September 1970 11:15:26pm", + "03 February 2013", "12 Feb 2006, 19:17", "12 Feb 2006 19:17", "14 May 2019 19:11:40.164", - "7 oct 70", - "7 oct 1970", - "03 February 2013", - "1 July 2013", - "2013-Feb-03", - // dd/Mon/yyy alpha Months - "06/Jan/2008:15:04:05 -0700", + "4th Sep 2012", + "1st February 2018 13:58:24", + "Mon, 02 Jan 2006 15:04:05 MST", // RFC1123 + "Mon, 02 Jan 2006 15:04:05 -0700", + "Tue, 11 Jul 2017 16:28:13 +0200 (CEST)", + "Mon 30 Sep 2018 09:09:09 PM UTC", + "Sun, 07 Jun 2020 00:00:00 +0100", + "Wed, 8 Feb 2023 19:00:46 +1100 (AEDT)", + // ANSIC and UnixDate - weekday month day time year + "Mon Jan 2 15:04:05 2006", + "Mon Jan 2 15:04:05 MST 2006", + "Monday Jan 02 15:04:05 -0700 2006", + "Mon Jan 2 15:04:05.103786 2006", + // RubyDate - weekday month day time offset year + "Mon Jan 02 15:04:05 -0700 2006", + // ANSIC_GLIBC - weekday day month year time + "Mon 02 Jan 2006 03:04:05 PM UTC", + "Monday 02 Jan 2006 03:04:05 PM MST", + // weekday month day time timezone-offset year + "Mon Aug 10 15:44:11 UTC+0000 2015", + // git log default date format + "Thu Apr 7 15:13:13 2005 -0700", + // variants of git log default date format + "Thu Apr 7 15:13:13 2005 -07:00", + "Thu Apr 7 15:13:13 2005 -07:00 PST", + "Thu Apr 7 15:13:13 2005 -07:00 PST (Pacific Standard Time)", + "Thu Apr 7 15:13:13 -0700 2005", + "Thu Apr 7 15:13:13 -07:00 2005", + "Thu Apr 7 15:13:13 -0700 PST 2005", + "Thu Apr 7 15:13:13 -07:00 PST 2005", + "Thu Apr 7 15:13:13 PST 2005", + // Variants of the above with a (full time zone description) + "Fri Jul 3 2015 06:04:07 PST-0700 (Pacific Daylight Time)", + "Fri Jul 03 2015 18:04:07 GMT+0100 (GMT Daylight Time)", + "Sun, 3 Jan 2021 00:12:23 +0800 (GMT+08:00)", + // year month day + "2013 May 2", + "2013 May 02 11:37:55", + // dd/Mon/year alpha Months "06/Jan/2008 15:04:05 -0700", - // mm/dd/yy + "06/January/2008 15:04:05 -0700", + "06/Jan/2008:15:04:05 -0700", // ngnix-log + "06/January/2008:08:11:17 -0700", + // mm/dd/year (see also PreferMonthFirst and RetryAmbiguousDateWithSwap options) "3/31/2014", "03/31/2014", "08/21/71", "8/1/71", "4/8/2014 22:05", "04/08/2014 22:05", + "04/08/2014, 22:05", "4/8/14 22:05", "04/2/2014 03:00:51", - "8/8/1965 12:00:00 AM", - "8/8/1965 01:00:01 PM", - "8/8/1965 01:00 PM", "8/8/1965 1:00 PM", + "8/8/1965 01:00 PM", "8/8/1965 12:00 AM", + "8/8/1965 12:00:00AM", + "8/8/1965 01:00:01 PM", + "8/8/1965 01:00:01PM -0700", + "8/8/1965 13:00:01 -0700 PST", + "8/8/1965 01:00:01 PM -0700 PST", + "8/8/1965 01:00:01 PM -07:00 PST (Pacific Standard Time)", "4/02/2014 03:00:51", "03/19/2012 10:11:59", "03/19/2012 10:11:59.3186369", + // mon/dd/year + "Oct/ 7/1970", + "Oct/03/1970 22:33:44", + "February/03/1970 11:33:44.555 PM PST", // yyyy/mm/dd "2014/3/31", "2014/03/31", @@ -69,6 +116,78 @@ var examples = []string{ "2014/4/02 03:00:51", "2012/03/19 10:11:59", "2012/03/19 10:11:59.3186369", + // weekday, day-mon-yy time + "Fri, 03-Jul-15 08:08:08 CEST", + "Monday, 02-Jan-06 15:04:05 MST", // RFC850 + "Monday, 02 Jan 2006 15:04:05 -0600", + "02-Jan-06 15:04:05 MST", + // RFC3339 - yyyy-mm-ddThh + "2006-01-02T15:04:05+0000", + "2009-08-12T22:15:09-07:00", + "2009-08-12T22:15:09", + "2009-08-12T22:15:09.988", + "2009-08-12T22:15:09Z", + "2009-08-12T22:15:09.52Z", + "2017-07-19T03:21:51:897+0100", + "2019-05-29T08:41-04", // no seconds, 2 digit TZ offset + // yyyy-mm-dd hh:mm:ss + "2014-04-26 17:24:37.3186369", + "2012-08-03 18:31:59.257000000", + "2014-04-26 17:24:37.123", + "2014-04-01 12:01am", + "2014-04-01 12:01:59.765 AM", + "2014-04-01 12:01:59,765", + "2014-04-01 22:43", + "2014-04-01 22:43:22", + "2014-12-16 06:20:00 UTC", + "2014-12-16 06:20:00 GMT", + "2014-04-26 05:24:37 PM", + "2014-04-26 13:13:43 +0800", + "2014-04-26 13:13:43 +0800 +08", + "2014-04-26 13:13:44 +09:00", + "2012-08-03 18:31:59.257000000 +0000 UTC", + "2015-09-30 18:48:56.35272715 +0000 UTC", + "2015-02-18 00:12:00 +0000 GMT", // golang native format + "2015-02-18 00:12:00 +0000 UTC", + "2015-02-08 03:02:00 +0300 MSK m=+0.000000001", + "2015-02-08 03:02:00.001 +0300 MSK m=+0.000000001", + "2017-07-19 03:21:51+00:00", + "2017-04-03 22:32:14.322 CET", + "2017-04-03 22:32:14,322 CET", + "2017-04-03 22:32:14:322 CET", + "2018-09-30 08:09:13.123PM PMDT", // PMDT time zone + "2018-09-30 08:09:13.123 am AMT", // AMT time zone + "2014-04-26", + "2014-04", + "2014", + // yyyy-mm-dd(offset) + "2020-07-20+08:00", + "2020-07-20+0800", + // year-mon-dd + "2013-Feb-03", + "2013-February-03 09:07:08.123", + // dd-mon-year + "03-Feb-13", + "03-Feb-2013", + "07-Feb-2004 09:07:07 +0200", + "07-February-2004 09:07:07 +0200", + // dd-mm-year (this format (common in Europe) always puts the day first, regardless of PreferMonthFirst) + "28-02-02", + "28-02-02 15:16:17", + "28-02-2002", + "28-02-2002 15:16:17", + // mm.dd.yy (see also PreferMonthFirst and RetryAmbiguousDateWithSwap options) + "3.31.2014", + "03.31.14", + "03.31.2014", + "03.31.2014 10:11:59 MST", + "03.31.2014 10:11:59.3186369Z", + // year.mm.dd + "2014.03", + "2014.03.30", + "2014.03.30 08:33pm", + "2014.03.30T08:33:44.555 PM -0700 MST", + "2014.03.30-0600", // yyyy:mm:dd "2014:3:31", "2014:03:31", @@ -78,58 +197,53 @@ var examples = []string{ "2014:4:02 03:00:51", "2012:03:19 10:11:59", "2012:03:19 10:11:59.3186369", - // Chinese - "2014年04月08日", - // yyyy-mm-ddThh - "2006-01-02T15:04:05+0000", - "2009-08-12T22:15:09-07:00", - "2009-08-12T22:15:09", - "2009-08-12T22:15:09.988", - "2009-08-12T22:15:09Z", - "2017-07-19T03:21:51:897+0100", - "2019-05-29T08:41-04", // no seconds, 2 digit TZ offset - // yyyy-mm-dd hh:mm:ss - "2014-04-26 17:24:37.3186369", - "2012-08-03 18:31:59.257000000", - "2014-04-26 17:24:37.123", - "2013-04-01 22:43", - "2013-04-01 22:43:22", - "2014-12-16 06:20:00 UTC", - "2014-12-16 06:20:00 GMT", - "2014-04-26 05:24:37 PM", - "2014-04-26 13:13:43 +0800", - "2014-04-26 13:13:43 +0800 +08", - "2014-04-26 13:13:44 +09:00", - "2012-08-03 18:31:59.257000000 +0000 UTC", - "2015-09-30 18:48:56.35272715 +0000 UTC", - "2015-02-18 00:12:00 +0000 GMT", - "2015-02-18 00:12:00 +0000 UTC", - "2015-02-08 03:02:00 +0300 MSK m=+0.000000001", - "2015-02-08 03:02:00.001 +0300 MSK m=+0.000000001", - "2017-07-19 03:21:51+00:00", - "2014-04-26", - "2014-04", - "2014", - "2014-05-11 08:20:13,787", - // yyyy-mm-dd-07:00 - "2020-07-20+08:00", - // mm.dd.yy - "3.31.2014", - "03.31.2014", - "08.21.71", - "2014.03", - "2014.03.30", - // yyyymmdd and similar + // mm:dd:yyyy (see also PreferMonthFirst and RetryAmbiguousDateWithSwap options) + "08:03:2012", + "08:04:2012 18:31:59+00:00", + // yyyymmdd and similar "20140601", "20140722105203", - // yymmdd hh:mm:yy mysql log + "20140722105203.364", + // Chinese + "2014年4月25日", + "2014年04月08日", + "2014年04月08日 19:17:22 -0700", + // RabbitMQ log format + "8-Mar-2018::14:09:27", + "08-03-2018::02:09:29 PM", + // yymmdd hh:mm:yy mysql log // 080313 05:21:55 mysqld started "171113 14:14:20", + "190910 11:51:49", // unix seconds, ms, micro, nano "1332151919", "1384216367189", "1384216367111222", "1384216367111222333", + // syslog RFC3164 (and non-conformant variants) + "Apr 9 12:37:24", + "Apr 9 12:37:24-10", + "Apr 9 12:37:24-1000", + "Apr 9 12:37:24 UTC-10", + "Apr 9 12:37:24 MST", + "Apr 9 12:37:24 MST-07:00", + "Apr 9 12:37:24 TZ-10", + "Apr 9 12:37:24 TZ+02:00", + "Apr 9 12:37:24+10", + "Apr 9 12:37:24+10:00", + "Apr 9 12:37:24 CEST", + "Apr 9 12:37:24 CEST+0200", + "Apr 9 12:37:24 2025", + "Apr 9 12:37:24 2025 +02:00", + "Apr 9 2025 12:37:24", + "Apr 9 2025 12:37:24 -0700", + // syslog RFC5424 (and non-conformant variants) + "2025-04-09T12:37:24Z", + "2025-04-09T12:37:24.123Z", + "2025-04-09T12:37:24.123456Z", + "2025-04-09T12:37:24-10:00", + "2025-04-09T12:37:24.123 +0200", + "2025-04-09T12:37:24.123456 -0700 MDT", } var ( @@ -164,115 +278,185 @@ func main() { } /* -+-------------------------------------------------------+-----------------------------------------+ -| Input | Parsed, and Output as %v | -+-------------------------------------------------------+-----------------------------------------+ -| May 8, 2009 5:57:51 PM | 2009-05-08 17:57:51 +0000 UTC | -| oct 7, 1970 | 1970-10-07 00:00:00 +0000 UTC | -| oct 7, '70 | 1970-10-07 00:00:00 +0000 UTC | -| oct. 7, 1970 | 1970-10-07 00:00:00 +0000 UTC | -| oct. 7, 70 | 1970-10-07 00:00:00 +0000 UTC | -| Mon Jan 2 15:04:05 2006 | 2006-01-02 15:04:05 +0000 UTC | -| Mon Jan 2 15:04:05 MST 2006 | 2006-01-02 15:04:05 +0000 MST | -| Mon Jan 02 15:04:05 -0700 2006 | 2006-01-02 15:04:05 -0700 -0700 | -| Monday, 02-Jan-06 15:04:05 MST | 2006-01-02 15:04:05 +0000 MST | -| Mon, 02 Jan 2006 15:04:05 MST | 2006-01-02 15:04:05 +0000 MST | -| Tue, 11 Jul 2017 16:28:13 +0200 (CEST) | 2017-07-11 16:28:13 +0200 +0200 | -| Mon, 02 Jan 2006 15:04:05 -0700 | 2006-01-02 15:04:05 -0700 -0700 | -| Mon 30 Sep 2018 09:09:09 PM UTC | 2018-09-30 21:09:09 +0000 UTC | -| Mon Aug 10 15:44:11 UTC+0100 2015 | 2015-08-10 15:44:11 +0000 UTC | -| Thu, 4 Jan 2018 17:53:36 +0000 | 2018-01-04 17:53:36 +0000 UTC | -| Fri Jul 03 2015 18:04:07 GMT+0100 (GMT Daylight Time) | 2015-07-03 18:04:07 +0100 GMT | -| Sun, 3 Jan 2021 00:12:23 +0800 (GMT+08:00) | 2021-01-03 00:12:23 +0800 +0800 | -| September 17, 2012 10:09am | 2012-09-17 10:09:00 +0000 UTC | -| September 17, 2012 at 10:09am PST-08 | 2012-09-17 10:09:00 -0800 PST | -| September 17, 2012, 10:10:09 | 2012-09-17 10:10:09 +0000 UTC | -| October 7, 1970 | 1970-10-07 00:00:00 +0000 UTC | -| October 7th, 1970 | 1970-10-07 00:00:00 +0000 UTC | -| 12 Feb 2006, 19:17 | 2006-02-12 19:17:00 +0000 UTC | -| 12 Feb 2006 19:17 | 2006-02-12 19:17:00 +0000 UTC | -| 14 May 2019 19:11:40.164 | 2019-05-14 19:11:40.164 +0000 UTC | -| 7 oct 70 | 1970-10-07 00:00:00 +0000 UTC | -| 7 oct 1970 | 1970-10-07 00:00:00 +0000 UTC | -| 03 February 2013 | 2013-02-03 00:00:00 +0000 UTC | -| 1 July 2013 | 2013-07-01 00:00:00 +0000 UTC | -| 2013-Feb-03 | 2013-02-03 00:00:00 +0000 UTC | -| 06/Jan/2008:15:04:05 -0700 | 2008-01-06 15:04:05 -0700 -0700 | -| 06/Jan/2008 15:04:05 -0700 | 2008-01-06 15:04:05 -0700 -0700 | -| 3/31/2014 | 2014-03-31 00:00:00 +0000 UTC | -| 03/31/2014 | 2014-03-31 00:00:00 +0000 UTC | -| 08/21/71 | 1971-08-21 00:00:00 +0000 UTC | -| 8/1/71 | 1971-08-01 00:00:00 +0000 UTC | -| 4/8/2014 22:05 | 2014-04-08 22:05:00 +0000 UTC | -| 04/08/2014 22:05 | 2014-04-08 22:05:00 +0000 UTC | -| 4/8/14 22:05 | 2014-04-08 22:05:00 +0000 UTC | -| 04/2/2014 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | -| 8/8/1965 12:00:00 AM | 1965-08-08 00:00:00 +0000 UTC | -| 8/8/1965 01:00:01 PM | 1965-08-08 13:00:01 +0000 UTC | -| 8/8/1965 01:00 PM | 1965-08-08 13:00:00 +0000 UTC | -| 8/8/1965 1:00 PM | 1965-08-08 13:00:00 +0000 UTC | -| 8/8/1965 12:00 AM | 1965-08-08 00:00:00 +0000 UTC | -| 4/02/2014 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | -| 03/19/2012 10:11:59 | 2012-03-19 10:11:59 +0000 UTC | -| 03/19/2012 10:11:59.3186369 | 2012-03-19 10:11:59.3186369 +0000 UTC | -| 2014/3/31 | 2014-03-31 00:00:00 +0000 UTC | -| 2014/03/31 | 2014-03-31 00:00:00 +0000 UTC | -| 2014/4/8 22:05 | 2014-04-08 22:05:00 +0000 UTC | -| 2014/04/08 22:05 | 2014-04-08 22:05:00 +0000 UTC | -| 2014/04/2 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | -| 2014/4/02 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | -| 2012/03/19 10:11:59 | 2012-03-19 10:11:59 +0000 UTC | -| 2012/03/19 10:11:59.3186369 | 2012-03-19 10:11:59.3186369 +0000 UTC | -| 2014:3:31 | 2014-03-31 00:00:00 +0000 UTC | -| 2014:03:31 | 2014-03-31 00:00:00 +0000 UTC | -| 2014:4:8 22:05 | 2014-04-08 22:05:00 +0000 UTC | -| 2014:04:08 22:05 | 2014-04-08 22:05:00 +0000 UTC | -| 2014:04:2 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | -| 2014:4:02 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | -| 2012:03:19 10:11:59 | 2012-03-19 10:11:59 +0000 UTC | -| 2012:03:19 10:11:59.3186369 | 2012-03-19 10:11:59.3186369 +0000 UTC | -| 2014年04月08日 | 2014-04-08 00:00:00 +0000 UTC | -| 2006-01-02T15:04:05+0000 | 2006-01-02 15:04:05 +0000 UTC | -| 2009-08-12T22:15:09-07:00 | 2009-08-12 22:15:09 -0700 -0700 | -| 2009-08-12T22:15:09 | 2009-08-12 22:15:09 +0000 UTC | -| 2009-08-12T22:15:09.988 | 2009-08-12 22:15:09.988 +0000 UTC | -| 2009-08-12T22:15:09Z | 2009-08-12 22:15:09 +0000 UTC | -| 2017-07-19T03:21:51:897+0100 | 2017-07-19 03:21:51.897 +0100 +0100 | -| 2019-05-29T08:41-04 | 2019-05-29 08:41:00 -0400 -0400 | -| 2014-04-26 17:24:37.3186369 | 2014-04-26 17:24:37.3186369 +0000 UTC | -| 2012-08-03 18:31:59.257000000 | 2012-08-03 18:31:59.257 +0000 UTC | -| 2014-04-26 17:24:37.123 | 2014-04-26 17:24:37.123 +0000 UTC | -| 2013-04-01 22:43 | 2013-04-01 22:43:00 +0000 UTC | -| 2013-04-01 22:43:22 | 2013-04-01 22:43:22 +0000 UTC | -| 2014-12-16 06:20:00 UTC | 2014-12-16 06:20:00 +0000 UTC | -| 2014-12-16 06:20:00 GMT | 2014-12-16 06:20:00 +0000 UTC | -| 2014-04-26 05:24:37 PM | 2014-04-26 17:24:37 +0000 UTC | -| 2014-04-26 13:13:43 +0800 | 2014-04-26 13:13:43 +0800 +0800 | -| 2014-04-26 13:13:43 +0800 +08 | 2014-04-26 13:13:43 +0800 +0800 | -| 2014-04-26 13:13:44 +09:00 | 2014-04-26 13:13:44 +0900 +0900 | -| 2012-08-03 18:31:59.257000000 +0000 UTC | 2012-08-03 18:31:59.257 +0000 UTC | -| 2015-09-30 18:48:56.35272715 +0000 UTC | 2015-09-30 18:48:56.35272715 +0000 UTC | -| 2015-02-18 00:12:00 +0000 GMT | 2015-02-18 00:12:00 +0000 UTC | -| 2015-02-18 00:12:00 +0000 UTC | 2015-02-18 00:12:00 +0000 UTC | -| 2015-02-08 03:02:00 +0300 MSK m=+0.000000001 | 2015-02-08 03:02:00 +0300 +0300 | -| 2015-02-08 03:02:00.001 +0300 MSK m=+0.000000001 | 2015-02-08 03:02:00.001 +0300 +0300 | -| 2017-07-19 03:21:51+00:00 | 2017-07-19 03:21:51 +0000 UTC | -| 2014-04-26 | 2014-04-26 00:00:00 +0000 UTC | -| 2014-04 | 2014-04-01 00:00:00 +0000 UTC | -| 2014 | 2014-01-01 00:00:00 +0000 UTC | -| 2014-05-11 08:20:13,787 | 2014-05-11 08:20:13.787 +0000 UTC | -| 2020-07-20+08:00 | 2020-07-20 00:00:00 +0800 +0800 | -| 3.31.2014 | 2014-03-31 00:00:00 +0000 UTC | -| 03.31.2014 | 2014-03-31 00:00:00 +0000 UTC | -| 08.21.71 | 1971-08-21 00:00:00 +0000 UTC | -| 2014.03 | 2014-03-01 00:00:00 +0000 UTC | -| 2014.03.30 | 2014-03-30 00:00:00 +0000 UTC | -| 20140601 | 2014-06-01 00:00:00 +0000 UTC | -| 20140722105203 | 2014-07-22 10:52:03 +0000 UTC | -| 171113 14:14:20 | 2017-11-13 14:14:20 +0000 UTC | -| 1332151919 | 2012-03-19 10:11:59 +0000 UTC | -| 1384216367189 | 2013-11-12 00:32:47.189 +0000 UTC | -| 1384216367111222 | 2013-11-12 00:32:47.111222 +0000 UTC | -| 1384216367111222333 | 2013-11-12 00:32:47.111222333 +0000 UTC | -+-------------------------------------------------------+-----------------------------------------+ ++------------------------------------------------------------+-----------------------------------------+ +| Input | Parsed, and Output as %v | ++------------------------------------------------------------+-----------------------------------------+ +| May 8, 2009 5:57:51 PM | 2009-05-08 17:57:51 +0000 UTC | +| oct 7, 1970 | 1970-10-07 00:00:00 +0000 UTC | +| oct 7, '70 | 1970-10-07 00:00:00 +0000 UTC | +| oct. 7, 1970 | 1970-10-07 00:00:00 +0000 UTC | +| oct. 7, 70 | 1970-10-07 00:00:00 +0000 UTC | +| October 7, 1970 | 1970-10-07 00:00:00 +0000 UTC | +| October 7th, 1970 | 1970-10-07 00:00:00 +0000 UTC | +| Sept. 7, 1970 11:15:26pm | 1970-09-07 23:15:26 +0000 UTC | +| Sep 7 2009 11:15:26.123 PM PST | 2009-09-07 23:15:26.123 +0000 PST | +| September 3rd, 2009 11:15:26.123456789pm | 2009-09-03 23:15:26.123456789 +0000 UTC | +| September 17 2012 10:09am | 2012-09-17 10:09:00 +0000 UTC | +| September 17, 2012, 10:10:09 | 2012-09-17 10:10:09 +0000 UTC | +| Sep 17, 2012 at 10:02am (EST) | 2012-09-17 10:02:00 +0000 EST | +| September 17, 2012 at 10:09am PST-08 | 2012-09-17 10:09:00 -0800 PST | +| September 17 2012 5:00pm UTC-0700 | 2012-09-17 17:00:00 -0700 -0700 | +| September 17 2012 5:00pm GMT-0700 | 2012-09-17 17:00:00 -0700 -0700 | +| 7 oct 70 | 1970-10-07 00:00:00 +0000 UTC | +| 7 Oct 1970 | 1970-10-07 00:00:00 +0000 UTC | +| 7 September 1970 23:15 | 1970-09-07 23:15:00 +0000 UTC | +| 7 September 1970 11:15:26pm | 1970-09-07 23:15:26 +0000 UTC | +| 03 February 2013 | 2013-02-03 00:00:00 +0000 UTC | +| 12 Feb 2006, 19:17 | 2006-02-12 19:17:00 +0000 UTC | +| 12 Feb 2006 19:17 | 2006-02-12 19:17:00 +0000 UTC | +| 14 May 2019 19:11:40.164 | 2019-05-14 19:11:40.164 +0000 UTC | +| 4th Sep 2012 | 2012-09-04 00:00:00 +0000 UTC | +| 1st February 2018 13:58:24 | 2018-02-01 13:58:24 +0000 UTC | +| Mon, 02 Jan 2006 15:04:05 MST | 2006-01-02 15:04:05 +0000 MST | +| Mon, 02 Jan 2006 15:04:05 -0700 | 2006-01-02 15:04:05 -0700 -0700 | +| Tue, 11 Jul 2017 16:28:13 +0200 (CEST) | 2017-07-11 16:28:13 +0200 +0200 | +| Mon 30 Sep 2018 09:09:09 PM UTC | 2018-09-30 21:09:09 +0000 UTC | +| Sun, 07 Jun 2020 00:00:00 +0100 | 2020-06-07 00:00:00 +0100 +0100 | +| Wed, 8 Feb 2023 19:00:46 +1100 (AEDT) | 2023-02-08 19:00:46 +1100 +1100 | +| Mon Jan 2 15:04:05 2006 | 2006-01-02 15:04:05 +0000 UTC | +| Mon Jan 2 15:04:05 MST 2006 | 2006-01-02 15:04:05 +0000 MST | +| Monday Jan 02 15:04:05 -0700 2006 | 2006-01-02 15:04:05 -0700 -0700 | +| Mon Jan 2 15:04:05.103786 2006 | 2006-01-02 15:04:05.103786 +0000 UTC | +| Mon Jan 02 15:04:05 -0700 2006 | 2006-01-02 15:04:05 -0700 -0700 | +| Mon 02 Jan 2006 03:04:05 PM UTC | 2006-01-02 15:04:05 +0000 UTC | +| Monday 02 Jan 2006 03:04:05 PM MST | 2006-01-02 15:04:05 +0000 MST | +| Mon Aug 10 15:44:11 UTC+0000 2015 | 2015-08-10 15:44:11 +0000 UTC | +| Thu Apr 7 15:13:13 2005 -0700 | 2005-04-07 15:13:13 -0700 -0700 | +| Thu Apr 7 15:13:13 2005 -07:00 | 2005-04-07 15:13:13 -0700 -0700 | +| Thu Apr 7 15:13:13 2005 -07:00 PST | 2005-04-07 15:13:13 -0700 PST | +| Thu Apr 7 15:13:13 2005 -07:00 PST (Pacific Standard Time) | 2005-04-07 15:13:13 -0700 PST | +| Thu Apr 7 15:13:13 -0700 2005 | 2005-04-07 15:13:13 -0700 -0700 | +| Thu Apr 7 15:13:13 -07:00 2005 | 2005-04-07 15:13:13 -0700 -0700 | +| Thu Apr 7 15:13:13 -0700 PST 2005 | 2005-04-07 15:13:13 -0700 PST | +| Thu Apr 7 15:13:13 -07:00 PST 2005 | 2005-04-07 15:13:13 -0700 PST | +| Thu Apr 7 15:13:13 PST 2005 | 2005-04-07 15:13:13 +0000 PST | +| Fri Jul 3 2015 06:04:07 PST-0700 (Pacific Daylight Time) | 2015-07-03 06:04:07 -0700 PST | +| Fri Jul 03 2015 18:04:07 GMT+0100 (GMT Daylight Time) | 2015-07-03 18:04:07 +0100 +0100 | +| Sun, 3 Jan 2021 00:12:23 +0800 (GMT+08:00) | 2021-01-03 00:12:23 +0800 +0800 | +| 2013 May 2 | 2013-05-02 00:00:00 +0000 UTC | +| 2013 May 02 11:37:55 | 2013-05-02 11:37:55 +0000 UTC | +| 06/Jan/2008 15:04:05 -0700 | 2008-01-06 15:04:05 -0700 -0700 | +| 06/January/2008 15:04:05 -0700 | 2008-01-06 15:04:05 -0700 -0700 | +| 06/Jan/2008:15:04:05 -0700 | 2008-01-06 15:04:05 -0700 -0700 | +| 06/January/2008:08:11:17 -0700 | 2008-01-06 08:11:17 -0700 -0700 | +| 3/31/2014 | 2014-03-31 00:00:00 +0000 UTC | +| 03/31/2014 | 2014-03-31 00:00:00 +0000 UTC | +| 08/21/71 | 1971-08-21 00:00:00 +0000 UTC | +| 8/1/71 | 1971-08-01 00:00:00 +0000 UTC | +| 4/8/2014 22:05 | 2014-04-08 22:05:00 +0000 UTC | +| 04/08/2014 22:05 | 2014-04-08 22:05:00 +0000 UTC | +| 04/08/2014, 22:05 | 2014-04-08 22:05:00 +0000 UTC | +| 4/8/14 22:05 | 2014-04-08 22:05:00 +0000 UTC | +| 04/2/2014 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | +| 8/8/1965 1:00 PM | 1965-08-08 13:00:00 +0000 UTC | +| 8/8/1965 01:00 PM | 1965-08-08 13:00:00 +0000 UTC | +| 8/8/1965 12:00 AM | 1965-08-08 00:00:00 +0000 UTC | +| 8/8/1965 12:00:00AM | 1965-08-08 00:00:00 +0000 UTC | +| 8/8/1965 01:00:01 PM | 1965-08-08 13:00:01 +0000 UTC | +| 8/8/1965 01:00:01PM -0700 | 1965-08-08 13:00:01 -0700 -0700 | +| 8/8/1965 13:00:01 -0700 PST | 1965-08-08 13:00:01 -0700 PST | +| 8/8/1965 01:00:01 PM -0700 PST | 1965-08-08 13:00:01 -0700 PST | +| 8/8/1965 01:00:01 PM -07:00 PST (Pacific Standard Time) | 1965-08-08 13:00:01 -0700 PST | +| 4/02/2014 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | +| 03/19/2012 10:11:59 | 2012-03-19 10:11:59 +0000 UTC | +| 03/19/2012 10:11:59.3186369 | 2012-03-19 10:11:59.3186369 +0000 UTC | +| Oct/ 7/1970 | 1970-10-07 00:00:00 +0000 UTC | +| Oct/03/1970 22:33:44 | 1970-10-03 22:33:44 +0000 UTC | +| February/03/1970 11:33:44.555 PM PST | 1970-02-03 23:33:44.555 +0000 PST | +| 2014/3/31 | 2014-03-31 00:00:00 +0000 UTC | +| 2014/03/31 | 2014-03-31 00:00:00 +0000 UTC | +| 2014/4/8 22:05 | 2014-04-08 22:05:00 +0000 UTC | +| 2014/04/08 22:05 | 2014-04-08 22:05:00 +0000 UTC | +| 2014/04/2 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | +| 2014/4/02 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | +| 2012/03/19 10:11:59 | 2012-03-19 10:11:59 +0000 UTC | +| 2012/03/19 10:11:59.3186369 | 2012-03-19 10:11:59.3186369 +0000 UTC | +| Fri, 03-Jul-15 08:08:08 CEST | 2015-07-03 08:08:08 +0000 CEST | +| Monday, 02-Jan-06 15:04:05 MST | 2006-01-02 15:04:05 +0000 MST | +| Monday, 02 Jan 2006 15:04:05 -0600 | 2006-01-02 15:04:05 -0600 -0600 | +| 02-Jan-06 15:04:05 MST | 2006-01-02 15:04:05 +0000 MST | +| 2006-01-02T15:04:05+0000 | 2006-01-02 15:04:05 +0000 UTC | +| 2009-08-12T22:15:09-07:00 | 2009-08-12 22:15:09 -0700 -0700 | +| 2009-08-12T22:15:09 | 2009-08-12 22:15:09 +0000 UTC | +| 2009-08-12T22:15:09.988 | 2009-08-12 22:15:09.988 +0000 UTC | +| 2009-08-12T22:15:09Z | 2009-08-12 22:15:09 +0000 UTC | +| 2009-08-12T22:15:09.52Z | 2009-08-12 22:15:09.52 +0000 UTC | +| 2017-07-19T03:21:51:897+0100 | 2017-07-19 03:21:51.897 +0100 +0100 | +| 2019-05-29T08:41-04 | 2019-05-29 08:41:00 -0400 -0400 | +| 2014-04-26 17:24:37.3186369 | 2014-04-26 17:24:37.3186369 +0000 UTC | +| 2012-08-03 18:31:59.257000000 | 2012-08-03 18:31:59.257 +0000 UTC | +| 2014-04-26 17:24:37.123 | 2014-04-26 17:24:37.123 +0000 UTC | +| 2014-04-01 12:01am | 2014-04-01 00:01:00 +0000 UTC | +| 2014-04-01 12:01:59.765 AM | 2014-04-01 00:01:59.765 +0000 UTC | +| 2014-04-01 12:01:59,765 | 2014-04-01 12:01:59.765 +0000 UTC | +| 2014-04-01 22:43 | 2014-04-01 22:43:00 +0000 UTC | +| 2014-04-01 22:43:22 | 2014-04-01 22:43:22 +0000 UTC | +| 2014-12-16 06:20:00 UTC | 2014-12-16 06:20:00 +0000 UTC | +| 2014-12-16 06:20:00 GMT | 2014-12-16 06:20:00 +0000 GMT | +| 2014-04-26 05:24:37 PM | 2014-04-26 17:24:37 +0000 UTC | +| 2014-04-26 13:13:43 +0800 | 2014-04-26 13:13:43 +0800 +0800 | +| 2014-04-26 13:13:43 +0800 +08 | 2014-04-26 13:13:43 +0800 +0800 | +| 2014-04-26 13:13:44 +09:00 | 2014-04-26 13:13:44 +0900 +0900 | +| 2012-08-03 18:31:59.257000000 +0000 UTC | 2012-08-03 18:31:59.257 +0000 UTC | +| 2015-09-30 18:48:56.35272715 +0000 UTC | 2015-09-30 18:48:56.35272715 +0000 UTC | +| 2015-02-18 00:12:00 +0000 GMT | 2015-02-18 00:12:00 +0000 GMT | +| 2015-02-18 00:12:00 +0000 UTC | 2015-02-18 00:12:00 +0000 UTC | +| 2015-02-08 03:02:00 +0300 MSK m=+0.000000001 | 2015-02-08 03:02:00 +0300 MSK | +| 2015-02-08 03:02:00.001 +0300 MSK m=+0.000000001 | 2015-02-08 03:02:00.001 +0300 MSK | +| 2017-07-19 03:21:51+00:00 | 2017-07-19 03:21:51 +0000 UTC | +| 2017-04-03 22:32:14.322 CET | 2017-04-03 22:32:14.322 +0000 CET | +| 2017-04-03 22:32:14,322 CET | 2017-04-03 22:32:14.322 +0000 CET | +| 2017-04-03 22:32:14:322 CET | 2017-04-03 22:32:14.322 +0000 CET | +| 2018-09-30 08:09:13.123PM PMDT | 2018-09-30 20:09:13.123 +0000 PMDT | +| 2018-09-30 08:09:13.123 am AMT | 2018-09-30 08:09:13.123 +0000 AMT | +| 2014-04-26 | 2014-04-26 00:00:00 +0000 UTC | +| 2014-04 | 2014-04-01 00:00:00 +0000 UTC | +| 2014 | 2014-01-01 00:00:00 +0000 UTC | +| 2020-07-20+08:00 | 2020-07-20 00:00:00 +0800 +0800 | +| 2020-07-20+0800 | 2020-07-20 00:00:00 +0800 +0800 | +| 2013-Feb-03 | 2013-02-03 00:00:00 +0000 UTC | +| 2013-February-03 09:07:08.123 | 2013-02-03 09:07:08.123 +0000 UTC | +| 03-Feb-13 | 2013-02-03 00:00:00 +0000 UTC | +| 03-Feb-2013 | 2013-02-03 00:00:00 +0000 UTC | +| 07-Feb-2004 09:07:07 +0200 | 2004-02-07 09:07:07 +0200 +0200 | +| 07-February-2004 09:07:07 +0200 | 2004-02-07 09:07:07 +0200 +0200 | +| 28-02-02 | 2002-02-28 00:00:00 +0000 UTC | +| 28-02-02 15:16:17 | 2002-02-28 15:16:17 +0000 UTC | +| 28-02-2002 | 2002-02-28 00:00:00 +0000 UTC | +| 28-02-2002 15:16:17 | 2002-02-28 15:16:17 +0000 UTC | +| 3.31.2014 | 2014-03-31 00:00:00 +0000 UTC | +| 03.31.14 | 2014-03-31 00:00:00 +0000 UTC | +| 03.31.2014 | 2014-03-31 00:00:00 +0000 UTC | +| 03.31.2014 10:11:59 MST | 2014-03-31 10:11:59 +0000 MST | +| 03.31.2014 10:11:59.3186369Z | 2014-03-31 10:11:59.3186369 +0000 UTC | +| 2014.03 | 2014-03-01 00:00:00 +0000 UTC | +| 2014.03.30 | 2014-03-30 00:00:00 +0000 UTC | +| 2014.03.30 08:33pm | 2014-03-30 20:33:00 +0000 UTC | +| 2014.03.30T08:33:44.555 PM -0700 MST | 2014-03-30 20:33:44.555 -0700 MST | +| 2014.03.30-0600 | 2014-03-30 00:00:00 -0600 -0600 | +| 2014:3:31 | 2014-03-31 00:00:00 +0000 UTC | +| 2014:03:31 | 2014-03-31 00:00:00 +0000 UTC | +| 2014:4:8 22:05 | 2014-04-08 22:05:00 +0000 UTC | +| 2014:04:08 22:05 | 2014-04-08 22:05:00 +0000 UTC | +| 2014:04:2 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | +| 2014:4:02 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | +| 2012:03:19 10:11:59 | 2012-03-19 10:11:59 +0000 UTC | +| 2012:03:19 10:11:59.3186369 | 2012-03-19 10:11:59.3186369 +0000 UTC | +| 08:03:2012 | 2012-08-03 00:00:00 +0000 UTC | +| 08:04:2012 18:31:59+00:00 | 2012-08-04 18:31:59 +0000 UTC | +| 20140601 | 2014-06-01 00:00:00 +0000 UTC | +| 20140722105203 | 2014-07-22 10:52:03 +0000 UTC | +| 20140722105203.364 | 2014-07-22 10:52:03.364 +0000 UTC | +| 2014年4月25日 | 2014-04-25 00:00:00 +0000 UTC | +| 2014年04月08日 | 2014-04-08 00:00:00 +0000 UTC | +| 2014年04月08日 19:17:22 -0700 | 2014-04-08 19:17:22 -0700 -0700 | +| 8-Mar-2018::14:09:27 | 2018-03-08 14:09:27 +0000 UTC | +| 08-03-2018::02:09:29 PM | 2018-03-08 14:09:29 +0000 UTC | +| 171113 14:14:20 | 2017-11-13 14:14:20 +0000 UTC | +| 190910 11:51:49 | 2019-09-10 11:51:49 +0000 UTC | +| 1332151919 | 2012-03-19 10:11:59 +0000 UTC | +| 1384216367189 | 2013-11-12 00:32:47.189 +0000 UTC | +| 1384216367111222 | 2013-11-12 00:32:47.111222 +0000 UTC | +| 1384216367111222333 | 2013-11-12 00:32:47.111222333 +0000 UTC | ++------------------------------------------------------------+-----------------------------------------+ */ diff --git a/go.mod b/go.mod index 071cd5e..6cbdb32 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,16 @@ -module github.com/araddon/dateparse +module github.com/itlightning/dateparse -go 1.12 +go 1.20 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 + github.com/stretchr/testify v1.10.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 40bf744..808b30d 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,17 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= -github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 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= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/parseany.go b/parseany.go index b9668b2..2297079 100644 --- a/parseany.go +++ b/parseany.go @@ -1,15 +1,31 @@ // Package dateparse parses date-strings without knowing the format // in advance, using a fast lex based approach to eliminate shotgun -// attempts. It leans towards US style dates when there is a conflict. +// attempts. Validates comprehensively to avoid false positives. +// +// By default it leans towards US style dates when there is a +// conflict. This can be adjusted using the `PreferMonthFirst` +// parser option. Some ambiguous formats can fail (e.g., trying to +// parse `31/03/2023“ as the default month-first format +// `MM/DD/YYYY`), but can be automatically retried with +// `RetryAmbiguousDateWithSwap`. +// +// Consider turning on the the `SimpleErrorMessages` option if you +// will be attempting to parse many strings that do not match any +// known format and you need to maximize performance. +// +// See README.md for key points on how timezone/location parsing +// works in go, as this can be counterintuitive initially. package dateparse import ( "fmt" "strconv" "strings" + "sync" "time" "unicode" "unicode/utf8" + "unsafe" ) // func init() { @@ -17,36 +33,36 @@ import ( // gou.SetColorOutput() // } -var days = []string{ - "mon", - "tue", - "wed", - "thu", - "fri", - "sat", - "sun", - "monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - "sunday", +var knownDays = map[string]struct{}{ + "mon": {}, + "tue": {}, + "wed": {}, + "thu": {}, + "fri": {}, + "sat": {}, + "sun": {}, + "monday": {}, + "tuesday": {}, + "wednesday": {}, + "thursday": {}, + "friday": {}, + "saturday": {}, + "sunday": {}, } -var months = []string{ - "january", - "february", - "march", - "april", - "may", - "june", - "july", - "august", - "september", - "october", - "november", - "december", +var knownMonths = map[string]struct{}{ + "january": {}, + "february": {}, + "march": {}, + "april": {}, + "may": {}, + "june": {}, + "july": {}, + "august": {}, + "september": {}, + "october": {}, + "november": {}, + "december": {}, } type dateState uint8 @@ -57,42 +73,47 @@ const ( dateDigit dateDigitSt dateYearDash - dateYearDashAlphaDash + dateYearDashAlpha dateYearDashDash - dateYearDashDashWs // 5 + dateYearDashDashWs // 6 dateYearDashDashT dateYearDashDashOffset dateDigitDash dateDigitDashAlpha - dateDigitDashAlphaDash // 10 + dateDigitDashAlphaDash // 11 + dateDigitDashDigit + dateDigitDashDigitDash dateDigitDot dateDigitDotDot + dateDigitDotDotWs + dateDigitDotDotT + dateDigitDotDotOffset dateDigitSlash dateDigitYearSlash - dateDigitSlashAlpha // 15 + dateDigitSlashAlpha // 21 + dateDigitSlashAlphaSlash dateDigitColon dateDigitChineseYear dateDigitChineseYearWs dateDigitWs - dateDigitWsMoYear // 20 - dateDigitWsMolong + dateDigitWsMoYear // 27 dateAlpha dateAlphaWs dateAlphaWsDigit - dateAlphaWsDigitMore // 25 + dateAlphaWsDigitMore // 31 dateAlphaWsDigitMoreWs dateAlphaWsDigitMoreWsYear - dateAlphaWsMonth - dateAlphaWsDigitYearmaybe - dateAlphaWsMonthMore - dateAlphaWsMonthSuffix - dateAlphaWsMore - dateAlphaWsAtTime + dateAlphaWsDigitYearMaybe + dateVariousDaySuffix + dateAlphaFullMonthWs + dateAlphaFullMonthWsDayWs dateAlphaWsAlpha - dateAlphaWsAlphaYearmaybe // 35 dateAlphaPeriodWsDigit - dateWeekdayComma - dateWeekdayAbbrevComma + dateAlphaSlash + dateAlphaSlashDigit + dateAlphaSlashDigitSlash + dateYearWs + dateYearWsMonthWs ) const ( // Time state @@ -100,44 +121,61 @@ const ( timeStart timeWs timeWsAlpha + timeWsAlphaRParen timeWsAlphaWs - timeWsAlphaZoneOffset // 5 + timeWsAlphaWsYear + timeWsAlphaZoneOffset // 7 timeWsAlphaZoneOffsetWs timeWsAlphaZoneOffsetWsYear - timeWsAlphaZoneOffsetWsExtra + timeWsOffsetWsTZDescInParen // overloaded, can come from timeWsAlphaWs, timeWsAlphaZoneOffsetWs, timeWsOffsetWs, timeWsOffsetWsAlphaZoneWs timeWsAMPMMaybe - timeWsAMPM // 10 - timeWsOffset - timeWsOffsetWs // 12 - timeWsOffsetColonAlpha - timeWsOffsetColon - timeWsYear // 15 - timeOffset - timeOffsetColon - timeAlpha + timeWsAMPM // 12 + timeWsOffset // overloaded, can come from timeWs or timeWsYear + timeWsOffsetWs // 14 + timeWsOffsetWsYear // overloaded, can come from timeWsOffsetWs or timeWsOffsetWsAlphaZoneWs (ensures year is only set once) + timeWsOffsetWsAlphaZone + timeWsOffsetWsAlphaZoneWs + timeWsYear timePeriod - timePeriodOffset // 20 - timePeriodOffsetColon - timePeriodOffsetColonWs - timePeriodWs - timePeriodWsAlpha - timePeriodWsOffset // 25 - timePeriodWsOffsetWs - timePeriodWsOffsetWsAlpha - timePeriodWsOffsetColon - timePeriodWsOffsetColonAlpha + timePeriodAMPM timeZ - timeZDigit ) 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") + ErrUnexpectedTail = fmt.Errorf("unexpected content after date/time: ") + ErrUnknownTZOffset = fmt.Errorf("TZ offset not recognized") + ErrUnknownTimeZone = fmt.Errorf("timezone not recognized") + ErrFracSecTooLong = fmt.Errorf("fractional seconds too long") ) -func unknownErr(datestr string) error { - return fmt.Errorf("Could not find format for %q", datestr) +func (p *parser) unknownErr(datestr string) error { + if p == nil || !p.simpleErrorMessages { + return fmt.Errorf("%w %q", ErrCouldntFindFormat, datestr) + } else { + return ErrCouldntFindFormat + } +} + +func (p *parser) unexpectedTail(tailStart int) error { + if p != nil && !p.simpleErrorMessages { + return fmt.Errorf("%w %q", ErrUnexpectedTail, p.datestr[tailStart:]) + } else { + return ErrUnexpectedTail + } +} + +// go 1.20 allows us to convert a byte slice to a string without a memory allocation. +// See https://github.com/golang/go/issues/53003#issuecomment-1140276077. +func bytesToString(b []byte) string { + if len(b) <= 0 { + return "" + } else { + return unsafe.String(&b[0], len(b)) + } } // ParseAny parse an unknown date format, detect the layout. @@ -145,23 +183,25 @@ func unknownErr(datestr string) error { // NOTE: please see readme on mmdd vs ddmm ambiguous dates. func ParseAny(datestr string, opts ...ParserOption) (time.Time, error) { p, err := parseTime(datestr, nil, opts...) + defer putBackParser(p) if err != nil { return time.Time{}, err } - return p.parse() + return p.parse(nil, opts...) } // ParseIn with Location, equivalent to time.ParseInLocation() timezone/offset // rules. Using location arg, if timezone/offset info exists in the // datestring, it uses the given location rules for any zone interpretation. // That is, MST means one thing when using America/Denver and something else -// in other locations. +// in other locations. See README for a more detailed explanation. func ParseIn(datestr string, loc *time.Location, opts ...ParserOption) (time.Time, error) { p, err := parseTime(datestr, loc, opts...) + defer putBackParser(p) if err != nil { return time.Time{}, err } - return p.parse() + return p.parse(loc, opts...) } // ParseLocal Given an unknown date format, detect the layout, @@ -170,49 +210,54 @@ 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...) + defer putBackParser(p) if err != nil { return time.Time{}, err } - return p.parse() + return p.parse(time.Local, opts...) } // MustParse parse a date, and panic if it can't be parsed. Used for testing. // Not recommended for most use-cases. func MustParse(datestr string, opts ...ParserOption) time.Time { p, err := parseTime(datestr, nil, opts...) + defer putBackParser(p) if err != nil { panic(err.Error()) } - t, err := p.parse() + t, err := p.parse(nil, opts...) if err != nil { panic(err.Error()) } return t } -// ParseFormat parse's an unknown date-time string and returns a layout +// ParseFormat parses 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" +// In certain edge cases, this may produce a format string of a different +// length than the input string. If this happens, it's an edge case that +// requires individually parsing each time. // +// 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...) + defer putBackParser(p) if err != nil { return "", err } - _, err = p.parse() + _, err = p.parse(nil, opts...) if err != nil { return "", err } @@ -223,40 +268,30 @@ func ParseFormat(datestr string, opts ...ParserOption) (string, error) { // mm/dd vs dd/mm then return an error. These return errors: 3.3.2014 , 8/8/71 etc func ParseStrict(datestr string, opts ...ParserOption) (time.Time, error) { p, err := parseTime(datestr, nil, opts...) + defer putBackParser(p) if err != nil { return time.Time{}, err } if p.ambiguousMD { return time.Time{}, ErrAmbiguousMMDD } - return p.parse() + return p.parse(nil, opts...) } +// Creates a new parser and parses the given datestr in the given loc with the given options. +// The caller must call putBackParser on the returned parser when done with it. func parseTime(datestr string, loc *time.Location, opts ...ParserOption) (p *parser, err error) { - p = newParser(datestr, loc, opts...) - 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 - // by retrying in this case, we can fix a common situation with no assumptions - defer func() { - if p != nil && p.ambiguousMD { - // if it errors out with the following error, swap before we - // get out of this function to reduce scope it needs to be applied on - _, err := p.parse() - if err != nil && strings.Contains(err.Error(), "month out of range") { - // create the option to reverse the preference - preferMonthFirst := PreferMonthFirst(!p.preferMonthFirst) - // turn off the retry to avoid endless recursion - retryAmbiguousDateWithSwap := RetryAmbiguousDateWithSwap(false) - modifiedOpts := append(opts, preferMonthFirst, retryAmbiguousDateWithSwap) - p, err = parseTime(datestr, time.Local, modifiedOpts...) - } - } - - }() + p, err = newParser(datestr, loc, opts...) + if err != nil { + return } + // IMPORTANT: we may need to modify the datestr while we are parsing (e.g., to + // remove pieces of the string that should be ignored during golang parsing). + // We will iterate over the modified datestr, and whenever we update datestr, + // we need to make sure that i is adjusted accordingly to resume parsing in + // the correct place. In error messages though we'll use the original datestr. i := 0 // General strategy is to read rune by rune through the date looking for @@ -264,22 +299,31 @@ func parseTime(datestr string, loc *time.Location, opts ...ParserOption) (p *par // Hopefully we only need to read about 5 or 6 bytes before // we figure it out and then attempt a parse iterRunes: - for ; i < len(datestr); i++ { - //r := rune(datestr[i]) - r, bytesConsumed := utf8.DecodeRuneInString(datestr[i:]) + for ; i < len(p.datestr); i++ { + r, bytesConsumed := utf8.DecodeRuneInString(p.datestr[i:]) if bytesConsumed > 1 { - i += (bytesConsumed - 1) + i += bytesConsumed - 1 } + adjustedI := i - p.skip - // gou.Debugf("i=%d r=%s state=%d %s", i, string(r), p.stateDate, datestr) + // gou.Debugf("i=%d r=%s state=%d %s", i, string(r), p.stateDate, p.datestr) switch p.stateDate { case dateStart: - if unicode.IsDigit(r) { + // Note that we can reach this state either at the very start of the string, + // or after skipping something (like a weekday, etc). + // NOTE: don't use unicode.IsDigit and unicode.IsLetter here because + // we don't expect non-ANSI chars to start a valid date/time format. + // This will let us quickly reject strings that begin with any non-ANSI char. + if '0' <= r && r <= '9' { p.stateDate = dateDigit - } else if unicode.IsLetter(r) { + } else if ('a' <= r && r <= 'z') || ('A' <= r && r <= 'Z') { p.stateDate = dateAlpha + } else if r == ' ' { + // we can safely ignore whitespace at the start of strings (helps with + // situations where we skipped a weekday and came back to this state) + p.skip = i + 1 } else { - return nil, unknownErr(datestr) + return p, p.unknownErr(datestr) } case dateDigit: @@ -289,12 +333,12 @@ iterRunes: // 2013-Feb-03 // 13-Feb-03 // 29-Jun-2016 - if i == 4 { + if adjustedI == 4 { p.stateDate = dateYearDash - p.yeari = 0 - p.yearlen = i + p.yeari = p.skip + p.yearlen = i - p.skip p.moi = i + 1 - p.set(0, "2006") + p.set(p.skip, "2006") } else { p.stateDate = dateDigitDash } @@ -303,62 +347,97 @@ iterRunes: // 03/31/2005 // 2014/02/24 p.stateDate = dateDigitSlash - if i == 4 { + if adjustedI == 4 { // 2014/02/24 - Year first / - p.yearlen = i // since it was start of datestr, i=len + p.yeari = p.skip + p.yearlen = i - p.skip p.moi = i + 1 - p.setYear() + if !p.setYear() { + return p, p.unknownErr(datestr) + } p.stateDate = dateDigitYearSlash } else { // Either Ambiguous dd/mm vs mm/dd OR dd/month/yy // 08/May/2005 // 03/31/2005 // 31/03/2005 - if i+2 < len(p.datestr) && unicode.IsLetter(rune(datestr[i+1])) { + if i+2 < len(p.datestr) && unicode.IsLetter(rune(p.datestr[i+1])) { // 08/May/2005 p.stateDate = dateDigitSlashAlpha p.moi = i + 1 p.daylen = 2 - p.dayi = 0 - p.setDay() + p.dayi = p.skip + if !p.setDay() { + return p, p.unknownErr(datestr) + } continue } // Ambiguous dd/mm vs mm/dd the bane of date-parsing // 03/31/2005 // 31/03/2005 p.ambiguousMD = true + p.ambiguousRetryable = true if p.preferMonthFirst { if p.molen == 0 { // 03/31/2005 - p.molen = i - p.setMonth() + p.moi = p.skip + p.molen = i - p.skip + if !p.setMonth() { + return p, p.unknownErr(datestr) + } p.dayi = i + 1 + } else { + return p, p.unknownErr(datestr) } } else { if p.daylen == 0 { - p.daylen = i - p.setDay() + p.dayi = p.skip + p.daylen = i - p.skip + if !p.setDay() { + return p, p.unknownErr(datestr) + } p.moi = i + 1 + } else { + return p, p.unknownErr(datestr) } } - } case ':': - // 03/31/2005 - // 2014/02/24 + // 03:31:2005 + // 2014:02:24 p.stateDate = dateDigitColon - if i == 4 { - p.yearlen = i + if adjustedI == 4 { + p.yeari = p.skip + p.yearlen = i - p.skip p.moi = i + 1 - p.setYear() + if !p.setYear() { + return p, p.unknownErr(datestr) + } } else { p.ambiguousMD = true + p.ambiguousRetryable = true if p.preferMonthFirst { if p.molen == 0 { - p.molen = i - p.setMonth() + p.moi = p.skip + p.molen = i - p.skip + if !p.setMonth() { + return p, p.unknownErr(datestr) + } p.dayi = i + 1 + } else { + return p, p.unknownErr(datestr) + } + } else { + if p.daylen == 0 { + p.dayi = p.skip + p.daylen = i - p.skip + if !p.setDay() { + return p, p.unknownErr(datestr) + } + p.moi = i + 1 + } else { + return p, p.unknownErr(datestr) } } } @@ -368,17 +447,43 @@ iterRunes: // 08.21.71 // 2014.05 p.stateDate = dateDigitDot - if i == 4 { - p.yearlen = i + if adjustedI == 4 { + p.yeari = p.skip + p.yearlen = i - p.skip p.moi = i + 1 - p.setYear() - } else { + if !p.setYear() { + return p, p.unknownErr(datestr) + } + } else if adjustedI <= 2 { p.ambiguousMD = true - p.moi = 0 - p.molen = i - p.setMonth() - p.dayi = i + 1 + p.ambiguousRetryable = true + if p.preferMonthFirst { + if p.molen == 0 { + // 03.31.2005 + p.moi = p.skip + p.molen = i - p.skip + if !p.setMonth() { + return p, p.unknownErr(datestr) + } + p.dayi = i + 1 + } else { + return p, p.unknownErr(datestr) + } + } else { + if p.daylen == 0 { + p.dayi = p.skip + p.daylen = i - p.skip + if !p.setDay() { + return p, p.unknownErr(datestr) + } + p.moi = i + 1 + } else { + return p, p.unknownErr(datestr) + } + } } + // else this might be a unixy combined datetime of the form: + // yyyyMMddhhmmss.SSS case ' ': // 18 January 2018 @@ -388,25 +493,50 @@ iterRunes: // 02 Jan 2018 23:59:34 // 12 Feb 2006, 19:17 // 12 Feb 2006, 19:17:22 - if i == 6 { + // 2013 Jan 06 15:04:05 + switch adjustedI { + case 4: + p.yeari = p.skip + p.yearlen = i - p.skip + p.moi = i + 1 + if !p.setYear() { + return p, p.unknownErr(datestr) + } + p.stateDate = dateYearWs + case 6: p.stateDate = dateDigitSt - } else { + default: p.stateDate = dateDigitWs - p.dayi = 0 - p.daylen = i + p.dayi = p.skip + p.daylen = i - p.skip } case '年': // Chinese Year p.stateDate = dateDigitChineseYear + p.yeari = p.skip + p.yearlen = i - 2 - p.skip + p.moi = i + 1 + if !p.setYear() { + return p, p.unknownErr(datestr) + } case ',': - return nil, unknownErr(datestr) + return p, p.unknownErr(datestr) + case 's', 'S', 'r', 'R', 't', 'T', 'n', 'N': + // 1st January 2018 + // 2nd Jan 2018 23:59 + // st, rd, nd, th + p.stateDate = dateVariousDaySuffix + i-- default: + if !unicode.IsDigit(r) { + return p, p.unknownErr(datestr) + } continue } - p.part1Len = i + p.part1Len = i - p.skip case dateDigitSt: - p.set(0, "060102") + p.set(p.skip, "060102") i = i - 1 p.stateTime = timeStart break iterRunes @@ -416,17 +546,22 @@ iterRunes: // 2020-08-17T17:00:00:000+0100 // dateYearDashDashWs // 2013-04-01 22:43:22 - // dateYearDashAlphaDash - // 2013-Feb-03 + // dateYearDashAlpha + // 2013-Feb-03 + // 2013-February-03 switch r { - case '-': + case '-', '\u2212': p.molen = i - p.moi p.dayi = i + 1 p.stateDate = dateYearDashDash - p.setMonth() + if !p.setMonth() { + return p, p.unknownErr(datestr) + } default: if unicode.IsLetter(r) { - p.stateDate = dateYearDashAlphaDash + p.stateDate = dateYearDashAlpha + } else if !unicode.IsDigit(r) { + return p, p.unknownErr(datestr) } } @@ -437,100 +572,215 @@ iterRunes: // 2013-04-01 22:43:22 // dateYearDashDashOffset // 2020-07-20+00:00 + // (these states are also reused after dateYearDashAlpha, like 2020-July-20...) switch r { case '+', '-': p.offseti = i p.daylen = i - p.dayi p.stateDate = dateYearDashDashOffset - p.setDay() + if !p.setDay() { + return p, p.unknownErr(datestr) + } case ' ': p.daylen = i - p.dayi p.stateDate = dateYearDashDashWs p.stateTime = timeStart - p.setDay() + if !p.setDay() { + return p, p.unknownErr(datestr) + } break iterRunes case 'T': p.daylen = i - p.dayi p.stateDate = dateYearDashDashT p.stateTime = timeStart - p.setDay() + if !p.setDay() { + return p, p.unknownErr(datestr) + } break iterRunes + default: + if !unicode.IsDigit(r) { + return p, p.unknownErr(datestr) + } } case dateYearDashDashT: // dateYearDashDashT // 2006-01-02T15:04:05Z07:00 // 2020-08-17T17:00:00:000+0100 + // (this state should never be reached, we break out when in this state) + return p, p.unknownErr(datestr) case dateYearDashDashOffset: // 2020-07-20+00:00 - switch r { - case ':': - p.set(p.offseti, "-07:00") - // case ' ': - // return nil, unknownErr(datestr) + if r != ':' && !unicode.IsDigit(r) { + return p, p.unknownErr(datestr) } - case dateYearDashAlphaDash: - // 2013-Feb-03 + case dateYearDashAlpha: + // dateYearDashAlpha + // 2013-Feb-03 + // 2013-February-03 switch r { - case '-': + case '-', '\u2212': p.molen = i - p.moi - p.set(p.moi, "Jan") - p.dayi = i + 1 + // Must be a valid short or long month + if p.molen == 3 { + p.set(p.moi, "Jan") + p.dayi = i + 1 + p.stateDate = dateYearDashDash + } else { + possibleFullMonth := strings.ToLower(p.datestr[p.moi:(p.moi + p.molen)]) + if i > 3 && isMonthFull(possibleFullMonth) { + p.fullMonth = possibleFullMonth + p.dayi = i + 1 + p.stateDate = dateYearDashDash + } else { + return p, p.unknownErr(datestr) + } + } + default: + if !unicode.IsLetter(r) { + return p, p.unknownErr(datestr) + } } + case dateDigitDash: // 13-Feb-03 // 29-Jun-2016 if unicode.IsLetter(r) { p.stateDate = dateDigitDashAlpha p.moi = i + } else if unicode.IsDigit(r) { + p.stateDate = dateDigitDashDigit + p.moi = i } else { - return nil, unknownErr(datestr) + return p, p.unknownErr(datestr) } case dateDigitDashAlpha: // 13-Feb-03 // 28-Feb-03 // 29-Jun-2016 switch r { - case '-': + case '-', '\u2212': p.molen = i - p.moi - p.set(p.moi, "Jan") - p.yeari = i + 1 - p.stateDate = dateDigitDashAlphaDash + + // Must be a valid short or long month + if p.molen == 3 { + p.set(p.moi, "Jan") + p.yeari = i + 1 + p.stateDate = dateDigitDashAlphaDash + } else { + possibleFullMonth := strings.ToLower(p.datestr[p.moi:(p.moi + p.molen)]) + if i > 3 && isMonthFull(possibleFullMonth) { + p.fullMonth = possibleFullMonth + p.yeari = i + 1 + p.stateDate = dateDigitDashAlphaDash + } else { + return p, p.unknownErr(datestr) + } + } + default: + if !unicode.IsLetter(r) { + return p, p.unknownErr(datestr) + } } - case dateDigitDashAlphaDash: - // 13-Feb-03 ambiguous - // 28-Feb-03 ambiguous - // 29-Jun-2016 dd-month(alpha)-yyyy + case dateDigitDashDigit: + // 29-06-2026 switch r { - case ' ': - // we need to find if this was 4 digits, aka year - // or 2 digits which makes it ambiguous year/day - length := i - (p.moi + p.molen + 1) - if length == 4 { - p.yearlen = 4 - p.set(p.yeari, "2006") - // We now also know that part1 was the day - p.dayi = 0 - p.daylen = p.part1Len - p.setDay() - } else if length == 2 { - // We have no idea if this is - // yy-mon-dd OR dd-mon-yy - // - // We are going to ASSUME (bad, bad) that it is dd-mon-yy which is a horible assumption - p.ambiguousMD = true - p.yearlen = 2 - p.set(p.yeari, "06") - // We now also know that part1 was the day - p.dayi = 0 - p.daylen = p.part1Len - p.setDay() + case '-', '\u2212': + // X + // 29-06-2026 + p.molen = i - p.moi + if p.molen == 2 { + p.set(p.moi, "01") + p.yeari = i + 1 + p.stateDate = dateDigitDashDigitDash + } else { + return p, p.unknownErr(datestr) + } + default: + if !unicode.IsDigit(r) { + return p, p.unknownErr(datestr) + } + } + + case dateDigitDashAlphaDash, dateDigitDashDigitDash: + // dateDigitDashAlphaDash: + // 13-Feb-03 ambiguous + // 28-Feb-03 ambiguous + // 29-Jun-2016 dd-month(alpha)-yyyy + // 8-Mar-2018:: + // dateDigitDashDigitDash: + // 29-06-2026 + // 08-03-18:: ambiguous (dd-mm-yy or yy-mm-dd) + switch r { + case ' ', ':': + doubleColonTimeConnector := false + if r == ':' { + p.link++ + if p.link == 2 { + if i+1 < len(p.datestr) { + // only legitimate content to follow "::" is the start of the time + nextChar, _ := utf8.DecodeRuneInString(p.datestr[i+1:]) + if unicode.IsDigit(nextChar) { + doubleColonTimeConnector = true + } + } + if !doubleColonTimeConnector { + return p, p.unknownErr(datestr) + } + } + } else if p.link > 0 { + return p, p.unknownErr(datestr) + } + if r == ' ' || doubleColonTimeConnector { + // we need to find if this was 4 digits, aka year + // or 2 digits which makes it ambiguous year/day + var sepLen int + if doubleColonTimeConnector { + sepLen = 2 + } else { + sepLen = 1 + } + length := i - (p.moi + p.molen + sepLen) + switch length { + case 4: + p.yearlen = 4 + p.set(p.yeari, "2006") + // We now also know that part1 was the day + p.dayi = p.skip + p.daylen = p.part1Len + if !p.setDay() { + return p, p.unknownErr(datestr) + } + case 2: + // We have no idea if this is + // yy-mon-dd OR dd-mon-yy + // (or for dateDigitDashDigitDash, yy-mm-dd OR dd-mm-yy) + // + // We are going to ASSUME (bad, bad) that it is dd-mon-yy (dd-mm-yy), + // which is a horrible assumption, but seems to be the convention for + // dates that are formatted in this way. + p.ambiguousMD = true // not retryable + p.yearlen = 2 + p.set(p.yeari, "06") + // We now also know that part1 was the day + p.dayi = p.skip + p.daylen = p.part1Len + if !p.setDay() { + return p, p.unknownErr(datestr) + } + default: + return p, p.unknownErr(datestr) + } + p.stateTime = timeStart + break iterRunes + } + default: + if !unicode.IsDigit(r) { + return p, p.unknownErr(datestr) } - p.stateTime = timeStart - break iterRunes } case dateDigitYearSlash: @@ -538,46 +788,87 @@ 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 - p.setDay() + if !p.setDay() { + return p, p.unknownErr(datestr) + } } break iterRunes case '/': if p.molen == 0 { p.molen = i - p.moi - p.setMonth() + if !p.setMonth() { + return p, p.unknownErr(datestr) + } p.dayi = i + 1 } + default: + if !unicode.IsDigit(r) { + return p, p.unknownErr(datestr) + } } case dateDigitSlashAlpha: // 06/May/2008 + // 06/September/2008 switch r { case '/': // | // 06/May/2008 if p.molen == 0 { - p.set(p.moi, "Jan") - p.yeari = i + 1 + p.molen = i - p.moi + if p.molen == 3 { + p.set(p.moi, "Jan") + p.yeari = i + 1 + p.stateDate = dateDigitSlashAlphaSlash + } else { + possibleFullMonth := strings.ToLower(p.datestr[p.moi:(p.moi + p.molen)]) + if i > 3 && isMonthFull(possibleFullMonth) { + p.fullMonth = possibleFullMonth + p.yeari = i + 1 + p.stateDate = dateDigitSlashAlphaSlash + } else { + return p, p.unknownErr(datestr) + } + } + } else { + return p, p.unknownErr(datestr) } - // We aren't breaking because we are going to re-use this case - // to find where the date starts, and possible time begins - case ' ', ':': + default: + if !unicode.IsLetter(r) { + return p, p.unknownErr(datestr) + } + } + + case dateDigitSlashAlphaSlash: + switch r { + case ' ': + fallthrough + case ':': p.stateTime = timeStart if p.yearlen == 0 { p.yearlen = i - p.yeari - p.setYear() + if !p.setYear() { + return p, p.unknownErr(datestr) + } } break iterRunes + default: + if !unicode.IsDigit(r) { + return p, p.unknownErr(datestr) + } } case dateDigitSlash: // 03/19/2012 10:11:59 // 04/2/2014 03:00:37 + // 04/2/2014, 03:00:37 // 3/1/2012 10:11:59 // 4/8/2014 22:05 // 3/1/2014 @@ -591,25 +882,39 @@ iterRunes: if p.preferMonthFirst { if p.daylen == 0 { p.daylen = i - p.dayi - p.setDay() + if !p.setDay() { + return p, p.unknownErr(datestr) + } p.yeari = i + 1 } } else { if p.molen == 0 { p.molen = i - p.moi - p.setMonth() + if !p.setMonth() { + return p, p.unknownErr(datestr) + } p.yeari = i + 1 } } // Note no break, we are going to pass by and re-enter this dateDigitSlash // and look for ending (space) or not (just date) - case ' ': + case ' ', ',': p.stateTime = timeStart if p.yearlen == 0 { p.yearlen = i - p.yeari - p.setYear() + if r == ',' { + // skip the comma + i++ + } + if !p.setYear() { + return p, p.unknownErr(datestr) + } } break iterRunes + default: + if !unicode.IsDigit(r) { + return p, p.unknownErr(datestr) + } } case dateDigitColon: @@ -628,10 +933,19 @@ iterRunes: p.stateTime = timeStart if p.yearlen == 0 { p.yearlen = i - p.yeari - p.setYear() + if !p.setYear() { + return p, p.unknownErr(datestr) + } } else if p.daylen == 0 { p.daylen = i - p.dayi - p.setDay() + if !p.setDay() { + return p, p.unknownErr(datestr) + } + } else if p.molen == 0 { + p.molen = i - p.moi + if !p.setMonth() { + return p, p.unknownErr(datestr) + } } break iterRunes case ':': @@ -639,15 +953,31 @@ iterRunes: // 2014:07:10 06:55:38.156283 if p.molen == 0 { p.molen = i - p.moi - p.setMonth() + if !p.setMonth() { + return p, p.unknownErr(datestr) + } p.dayi = i + 1 } } else if p.preferMonthFirst { if p.daylen == 0 { p.daylen = i - p.dayi - p.setDay() + if !p.setDay() { + return p, p.unknownErr(datestr) + } p.yeari = i + 1 } + } else { + if p.molen == 0 { + p.molen = i - p.moi + if !p.setMonth() { + return p, p.unknownErr(datestr) + } + p.yeari = i + 1 + } + } + default: + if !unicode.IsDigit(r) { + return p, p.unknownErr(datestr) } } @@ -664,23 +994,40 @@ iterRunes: case ' ': p.yeari = i + 1 //p.yearlen = 4 - p.dayi = 0 + p.dayi = p.skip p.daylen = p.part1Len - p.setDay() + if !p.setDay() { + return p, p.unknownErr(datestr) + } p.stateTime = timeStart - if i > p.daylen+len(" Sep") { // November etc - // If len greather than space + 3 it must be full month - p.stateDate = dateDigitWsMolong + if adjustedI > p.daylen+len(" Sep") { // November etc + // If this is a legit full month, then change the string we're parsing + // to compensate for the longest month, and do the same with the format string. We + // must maintain a corresponding length/content and this is the easiest + // way to do this. + possibleFullMonth := strings.ToLower(p.datestr[(p.dayi + (p.daylen + 1)):i]) + if isMonthFull(possibleFullMonth) { + p.moi = p.dayi + p.daylen + 1 + p.molen = i - p.moi + p.fullMonth = possibleFullMonth + p.stateDate = dateDigitWsMoYear + } else { + return p, p.unknownErr(datestr) + } } else { // If len=3, the might be Feb or May? Ie ambigous abbreviated but // we can parse may with either. BUT, that means the // format may not be correct? - // mo := strings.ToLower(datestr[p.daylen+1 : i]) - p.moi = p.daylen + 1 + // mo := strings.ToLower(p.datestr[p.daylen+1 : i]) + p.moi = p.dayi + p.daylen + 1 p.molen = i - p.moi p.set(p.moi, "Jan") p.stateDate = dateDigitWsMoYear } + default: + if !unicode.IsLetter(r) { + return p, p.unknownErr(datestr) + } } case dateDigitWsMoYear: @@ -692,148 +1039,323 @@ iterRunes: switch r { case ',': p.yearlen = i - p.yeari - p.setYear() + if !p.setYear() { + return p, p.unknownErr(datestr) + } i++ break iterRunes case ' ': p.yearlen = i - p.yeari - p.setYear() + if !p.setYear() { + return p, p.unknownErr(datestr) + } break iterRunes + default: + if !unicode.IsDigit(r) { + return p, p.unknownErr(datestr) + } + } + + case dateYearWs: + // 2013 Jan 06 15:04:05 + // 2013 January 06 15:04:05 + if r == ' ' { + p.molen = i - p.moi + // Must be a valid short or long month + if p.molen == 3 { + p.set(p.moi, "Jan") + p.dayi = i + 1 + p.stateDate = dateYearWsMonthWs + } else { + possibleFullMonth := strings.ToLower(p.datestr[p.moi:(p.moi + p.molen)]) + if i > 3 && isMonthFull(possibleFullMonth) { + p.fullMonth = possibleFullMonth + p.dayi = i + 1 + p.stateDate = dateYearWsMonthWs + } else { + return p, p.unknownErr(datestr) + } + } + } else if !unicode.IsLetter(r) { + return p, p.unknownErr(datestr) + } + case dateYearWsMonthWs: + // 2013 Jan 06 15:04:05 + // 2013 January 06 15:04:05 + switch r { + case ',': + p.daylen = i - p.dayi + p.setDay() + i++ + p.stateTime = timeStart + break iterRunes + case ' ': + p.daylen = i - p.dayi + p.setDay() + p.stateTime = timeStart + break iterRunes + default: + if !unicode.IsDigit(r) { + return p, p.unknownErr(datestr) + } } - case dateDigitWsMolong: - // 18 January 2018 - // 8 January 2018 case dateDigitChineseYear: // dateDigitChineseYear // 2014年04月08日 // weekday %Y年%m月%e日 %A %I:%M %p // 2013年07月18日 星期四 10:27 上午 - if r == ' ' { + switch r { + case '月': + // month + p.molen = i - p.moi - 2 + p.dayi = i + 1 + if !p.setMonth() { + return p, p.unknownErr(datestr) + } + case '日': + // day + p.daylen = i - p.dayi - 2 + if !p.setDay() { + return p, p.unknownErr(datestr) + } + case ' ': + if p.daylen <= 0 { + return p, p.unknownErr(datestr) + } p.stateDate = dateDigitChineseYearWs - break + p.stateTime = timeStart + break iterRunes + default: + if !unicode.IsDigit(r) { + return p, p.unknownErr(datestr) + } } case dateDigitDot: - // This is the 2nd period // 3.31.2014 // 08.21.71 // 2014.05 // 2018.09.30 + + // This is the 2nd period if r == '.' { if p.moi == 0 { // 3.31.2014 p.daylen = i - p.dayi p.yeari = i + 1 - p.setDay() + if !p.setDay() { + return p, p.unknownErr(datestr) + } + p.stateDate = dateDigitDotDot + } else if p.dayi == 0 && p.yearlen == 0 { + // 23.07.2002 + p.molen = i - p.moi + p.yeari = i + 1 + if !p.setMonth() { + return p, p.unknownErr(datestr) + } p.stateDate = dateDigitDotDot } else { // 2018.09.30 - //p.molen = 2 + // p.molen = 2 p.molen = i - p.moi p.dayi = i + 1 - p.setMonth() + if !p.setMonth() { + return p, p.unknownErr(datestr) + } p.stateDate = dateDigitDotDot } + } else if !unicode.IsDigit(r) { + return p, p.unknownErr(datestr) } + case dateDigitDotDot: - // iterate all the way through + // dateDigitDotDotT + // 2006.01.02T15:04:05Z07:00 + // dateDigitDotDotWs + // 2013.04.01 22:43:22 + // dateDigitDotDotOffset + // 2020.07.20+00:00 + switch r { + case '+', '-': + p.offseti = i + p.daylen = i - p.dayi + p.stateDate = dateDigitDotDotOffset + if !p.setDay() { + return p, p.unknownErr(datestr) + } + case ' ': + if p.daylen == 0 && p.molen > 0 && p.yearlen > 0 { + p.daylen = i - p.dayi + if !p.setDay() { + return p, p.unknownErr(datestr) + } + } else if p.molen == 0 && p.daylen > 0 && p.yearlen > 0 { + p.molen = i - p.moi + if !p.setMonth() { + return p, p.unknownErr(datestr) + } + } else if p.yearlen == 0 && p.daylen > 0 && p.molen > 0 { + p.yearlen = i - p.yeari + if !p.setYear() { + return p, p.unknownErr(datestr) + } + } else { + return p, p.unknownErr(datestr) + } + p.stateDate = dateDigitDotDotWs + p.stateTime = timeStart + break iterRunes + case 'T': + p.daylen = i - p.dayi + p.stateDate = dateDigitDotDotT + p.stateTime = timeStart + if !p.setDay() { + return p, p.unknownErr(datestr) + } + break iterRunes + default: + if !unicode.IsDigit(r) { + return p, p.unknownErr(datestr) + } + } + + case dateDigitDotDotT: + // dateDigitDotDotT + // 2006-01-02T15:04:05Z07:00 + // 2020-08-17T17:00:00:000+0100 + // (should be unreachable, we break in this state) + return p, p.unknownErr(datestr) + + case dateDigitDotDotOffset: + // 2020-07-20+00:00 + if r != ':' && !unicode.IsDigit(r) { + return p, p.unknownErr(datestr) + } + case dateAlpha: - // dateAlphaWS + // dateAlphaWs // Mon Jan _2 15:04:05 2006 // Mon Jan _2 15:04:05 MST 2006 // Mon Jan 02 15:04:05 -0700 2006 + // Mon Jan 02 15:04:05 2006 -0700 // Mon Aug 10 15:44:11 UTC+0100 2015 // Fri Jul 03 2015 18:04:07 GMT+0100 (GMT Daylight Time) - // dateAlphaWSDigit + // Fri Jul 03 2015 18:04:07 GMT+01:00 (GMT Daylight Time) + // dateAlphaWsDigit // May 8, 2009 5:57:51 PM // oct 1, 1970 - // dateAlphaWsMonth - // April 8, 2009 - // dateAlphaWsMore - // dateAlphaWsAtTime + // dateAlphaFullMonthWs + // January 02, 2006 3:04pm + // January 02, 2006 3:04pm MST-07 // January 02, 2006 at 3:04pm MST-07 // // dateAlphaPeriodWsDigit // oct. 1, 1970 - // dateWeekdayComma - // Monday, 02 Jan 2006 15:04:05 MST - // Monday, 02-Jan-06 15:04:05 MST - // Monday, 02 Jan 2006 15:04:05 -0700 - // Monday, 02 Jan 2006 15:04:05 +0100 - // dateWeekdayAbbrevComma - // Mon, 02 Jan 2006 15:04:05 MST - // Mon, 02 Jan 2006 15:04:05 -0700 - // Thu, 13 Jul 2017 08:58:40 +0100 - // Tue, 11 Jul 2017 16:28:13 +0200 (CEST) - // Mon, 02-Jan-06 15:04:05 MST - switch { - case r == ' ': - // X - // April 8, 2009 - if i > 3 { - // Check to see if the alpha is name of month? or Day? - month := strings.ToLower(datestr[0:i]) - if isMonthFull(month) { - p.fullMonth = month - // len(" 31, 2018") = 9 - if len(datestr[i:]) < 10 { - // April 8, 2009 - p.stateDate = dateAlphaWsMonth - } else { - p.stateDate = dateAlphaWsMore - } + // dateAlphaSlash + // dateAlphaSlashDigit + // dateAlphaSlashDigitSlash + // Oct/ 7/1970 + // Oct/07/1970 + // February/ 7/1970 + // February/07/1970 + switch r { + case ' ': + // This could be a weekday or a month, detect and parse both cases. + // skip & return to dateStart + // Tue 05 May 2020, 05:05:05 + // Tuesday 05 May 2020, 05:05:05 + // Mon Jan 2 15:04:05 2006 + // Monday Jan 2 15:04:05 2006 + maybeDayOrMonth := strings.ToLower(p.datestr[p.skip:i]) + if isDay(maybeDayOrMonth) { + p.skip = i + 1 + p.stateDate = dateStart + } else if adjustedI > 3 { + // X + // April 8, 2009 + // Expecting a full month name at this point + if isMonthFull(maybeDayOrMonth) { + p.moi = p.skip + p.molen = i - p.skip + p.fullMonth = maybeDayOrMonth + p.stateDate = dateAlphaFullMonthWs p.dayi = i + 1 break + } else { + return p, p.unknownErr(datestr) } - } else { - // This is possibly ambiguous? May will parse as either though. - // So, it could return in-correct format. + } else if adjustedI == 3 { // dateAlphaWs // May 05, 2005, 05:05:05 // May 05 2005, 05:05:05 // Jul 05, 2005, 05:05:05 // May 8 17:57:51 2009 // May 8 17:57:51 2009 - // skip & return to dateStart - // Tue 05 May 2020, 05:05:05 - // Mon Jan 2 15:04:05 2006 - - maybeDay := strings.ToLower(datestr[0:i]) - if isDay(maybeDay) { - // using skip throws off indices used by other code; saner to restart - return parseTime(datestr[i+1:], loc) - } p.stateDate = dateAlphaWs - } - - case r == ',': - // Mon, 02 Jan 2006 - - if i == 3 { - p.stateDate = dateWeekdayAbbrevComma - p.set(0, "Mon") } else { - p.stateDate = dateWeekdayComma - p.skip = i + 2 - i++ - // TODO: lets just make this "skip" as we don't need - // the mon, monday, they are all superfelous and not needed - // just lay down the skip, no need to fill and then skip + return p, p.unknownErr(datestr) } - case r == '.': + + case ',': + // Mon, 02 Jan 2006 + // Monday, 02 Jan 2006 + if adjustedI >= 3 && p.nextIs(i, ' ') { + maybeDay := strings.ToLower(p.datestr[p.skip:i]) + if isDay(maybeDay) { + p.stateDate = dateStart + // Just skip past the weekday, it contains no valuable info + p.skip = i + 2 + i++ + } else { + return p, p.unknownErr(datestr) + } + } + case '.': // sept. 28, 2017 // jan. 28, 2017 p.stateDate = dateAlphaPeriodWsDigit - if i == 3 { - p.molen = i - p.set(0, "Jan") - } else if i == 4 { + switch adjustedI { + case 3: + p.moi = p.skip + p.molen = i - p.skip + p.set(p.skip, "Jan") + case 4: // gross - datestr = datestr[0:i-1] + datestr[i:] - return parseTime(datestr, loc, opts...) + newDateStr := p.datestr[p.skip:i-1] + p.datestr[i:] + putBackParser(p) + return parseTime(newDateStr, loc, opts...) + default: + return p, p.unknownErr(datestr) + } + case '/': + // X + // Oct/ 7/1970 + // Oct/07/1970 + // X + // February/ 7/1970 + // February/07/1970 + // Must be a valid short or long month + if adjustedI == 3 { + p.moi = p.skip + p.molen = i - p.moi + p.set(p.moi, "Jan") + p.stateDate = dateAlphaSlash } else { - return nil, unknownErr(datestr) + possibleFullMonth := strings.ToLower(p.datestr[p.skip:i]) + if adjustedI > 3 && isMonthFull(possibleFullMonth) { + p.moi = p.skip + p.molen = i - p.moi + p.fullMonth = possibleFullMonth + p.stateDate = dateAlphaSlash + } else { + return p, p.unknownErr(datestr) + } + } + default: + if !unicode.IsLetter(r) { + return p, p.unknownErr(datestr) } } @@ -842,7 +1364,9 @@ iterRunes: // Mon Jan _2 15:04:05 2006 // Mon Jan _2 15:04:05 MST 2006 // Mon Jan 02 15:04:05 -0700 2006 + // Mon Jan 02 15:04:05 2006 -0700 // Fri Jul 03 2015 18:04:07 GMT+0100 (GMT Daylight Time) + // Fri Jul 03 2015 18:04:07 GMT+01:00 (GMT Daylight Time) // Mon Aug 10 15:44:11 UTC+0100 2015 // dateAlphaWsDigit // May 8, 2009 5:57:51 PM @@ -852,15 +1376,30 @@ iterRunes: // May 08 17:57:51 2009 // oct 1, 1970 // oct 7, '70 + // (this state is only entered if the skip-adjusted length is 3) switch { case unicode.IsLetter(r): - p.set(0, "Mon") - p.stateDate = dateAlphaWsAlpha - p.set(i, "Jan") + // have to have a day of week and then at least a 3 digit month to follow + if adjustedI >= 3 && (i+3) < len(p.datestr) { + maybeDay := strings.ToLower(p.datestr[p.skip:i]) + if isDay(maybeDay) { + p.skip = i + p.stateDate = dateAlphaWsAlpha + p.set(i, "Jan") + } else { + return p, p.unknownErr(datestr) + } + } else { + return p, p.unknownErr(datestr) + } case unicode.IsDigit(r): - p.set(0, "Jan") + p.set(p.skip, "Jan") p.stateDate = dateAlphaWsDigit p.dayi = i + case r == ' ': + // continue + default: + return p, p.unknownErr(datestr) } case dateAlphaWsDigit: @@ -874,36 +1413,47 @@ iterRunes: // May 08 17:57:51 2009 if r == ',' { p.daylen = i - p.dayi - p.setDay() + if !p.setDay() { + return p, p.unknownErr(datestr) + } p.stateDate = dateAlphaWsDigitMore } else if r == ' ' { p.daylen = i - p.dayi - p.setDay() + if !p.setDay() { + return p, p.unknownErr(datestr) + } p.yeari = i + 1 - p.stateDate = dateAlphaWsDigitYearmaybe + p.stateDate = dateAlphaWsDigitYearMaybe p.stateTime = timeStart } else if unicode.IsLetter(r) { - p.stateDate = dateAlphaWsMonthSuffix + p.stateDate = dateVariousDaySuffix i-- + } else if !unicode.IsDigit(r) { + return p, p.unknownErr(datestr) } - case dateAlphaWsDigitYearmaybe: + case dateAlphaWsDigitYearMaybe: // x // May 8 2009 5:57:51 PM // May 8 17:57:51 2009 // May 8 17:57:51 2009 // May 08 17:57:51 2009 // Jul 03 2015 18:04:07 GMT+0100 (GMT Daylight Time) + // Jul 03 2015 18:04:07 GMT+01:00 (GMT Daylight Time) if r == ':' { // Guessed wrong; was not a year + p.yeari = 0 i = i - 3 p.stateDate = dateAlphaWsDigit - p.yeari = 0 break iterRunes } else if r == ' ' { // must be year format, not 15:04 p.yearlen = i - p.yeari - p.setYear() + if !p.setYear() { + return p, p.unknownErr(datestr) + } break iterRunes + } else if !unicode.IsDigit(r) { + return p, p.unknownErr(datestr) } case dateAlphaWsDigitMore: // x @@ -915,6 +1465,8 @@ iterRunes: if r == ' ' { p.yeari = i + 1 p.stateDate = dateAlphaWsDigitMoreWs + } else { + return p, p.unknownErr(datestr) } case dateAlphaWsDigitMoreWs: // x @@ -925,90 +1477,72 @@ iterRunes: switch r { case '\'': p.yeari = i + 1 - case ' ', ',': + case ' ': + fallthrough + case ',': // x // May 8, 2009 5:57:51 PM // x // May 8, 2009, 5:57:51 PM p.stateDate = dateAlphaWsDigitMoreWsYear p.yearlen = i - p.yeari - p.setYear() + if !p.setYear() { + return p, p.unknownErr(datestr) + } p.stateTime = timeStart break iterRunes + default: + if r != '\'' && !unicode.IsDigit(r) { + return p, p.unknownErr(datestr) + } } - case dateAlphaWsMonth: - // April 8, 2009 - // April 8 2009 - switch r { - case ' ', ',': - // x - // June 8, 2009 - // x - // June 8 2009 - if p.daylen == 0 { - p.daylen = i - p.dayi - p.setDay() - } - case 's', 'S', 'r', 'R', 't', 'T', 'n', 'N': - // st, rd, nd, st - i-- - p.stateDate = dateAlphaWsMonthSuffix - default: - if p.daylen > 0 && p.yeari == 0 { - p.yeari = i - } - } - case dateAlphaWsMonthMore: - // X - // January 02, 2006, 15:04:05 - // January 02 2006, 15:04:05 - // January 02, 2006 15:04:05 - // January 02 2006 15:04:05 - switch r { - case ',': - p.yearlen = i - p.yeari - p.setYear() - p.stateTime = timeStart - i++ - break iterRunes - case ' ': - p.yearlen = i - p.yeari - p.setYear() - p.stateTime = timeStart - break iterRunes - } - case dateAlphaWsMonthSuffix: + case dateVariousDaySuffix: // x // April 8th, 2009 // April 8th 2009 switch r { case 't', 'T': if p.nextIs(i, 'h') || p.nextIs(i, 'H') { - if len(datestr) > i+2 { - return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc, opts...) + if len(p.datestr) > i+2 { + newDateStr := p.datestr[p.skip:i] + p.datestr[i+2:] + putBackParser(p) + return parseTime(newDateStr, loc, opts...) } } + return p, p.unknownErr(datestr) case 'n', 'N': if p.nextIs(i, 'd') || p.nextIs(i, 'D') { - if len(datestr) > i+2 { - return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc, opts...) + if len(p.datestr) > i+2 { + newDateStr := p.datestr[p.skip:i] + p.datestr[i+2:] + putBackParser(p) + return parseTime(newDateStr, loc, opts...) } } + return p, p.unknownErr(datestr) case 's', 'S': if p.nextIs(i, 't') || p.nextIs(i, 'T') { - if len(datestr) > i+2 { - return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc, opts...) + if len(p.datestr) > i+2 { + newDateStr := p.datestr[p.skip:i] + p.datestr[i+2:] + putBackParser(p) + return parseTime(newDateStr, loc, opts...) } } + return p, p.unknownErr(datestr) case 'r', 'R': if p.nextIs(i, 'd') || p.nextIs(i, 'D') { - if len(datestr) > i+2 { - return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc, opts...) + if len(p.datestr) > i+2 { + newDateStr := p.datestr[p.skip:i] + p.datestr[i+2:] + putBackParser(p) + return parseTime(newDateStr, loc, opts...) } } + return p, p.unknownErr(datestr) + default: + return p, p.unknownErr(datestr) } - case dateAlphaWsMore: + + case dateAlphaFullMonthWs: // January 02, 2006, 15:04:05 // January 02 2006, 15:04:05 // January 2nd, 2006, 15:04:05 @@ -1020,19 +1554,25 @@ iterRunes: // January 02, 2006, 15:04:05 if p.nextIs(i, ' ') { p.daylen = i - p.dayi - p.setDay() + if !p.setDay() { + return p, p.unknownErr(datestr) + } p.yeari = i + 2 - p.stateDate = dateAlphaWsMonthMore + p.stateDate = dateAlphaFullMonthWsDayWs i++ + } else { + return p, p.unknownErr(datestr) } case r == ' ': // x // January 02 2006, 15:04:05 p.daylen = i - p.dayi - p.setDay() + if !p.setDay() { + return p, p.unknownErr(datestr) + } p.yeari = i + 1 - p.stateDate = dateAlphaWsMonthMore + p.stateDate = dateAlphaFullMonthWsDayWs case unicode.IsDigit(r): // XX // January 02, 2006, 15:04:05 @@ -1041,9 +1581,40 @@ iterRunes: // X // January 2nd, 2006, 15:04:05 p.daylen = i - p.dayi - p.setDay() - p.stateDate = dateAlphaWsMonthSuffix + if !p.setDay() { + return p, p.unknownErr(datestr) + } + p.stateDate = dateVariousDaySuffix i-- + default: + return p, p.unknownErr(datestr) + } + case dateAlphaFullMonthWsDayWs: + // X + // January 02, 2006, 15:04:05 + // January 02 2006, 15:04:05 + // January 02, 2006 15:04:05 + // January 02 2006 15:04:05 + switch r { + case ',': + p.yearlen = i - p.yeari + if !p.setYear() { + return p, p.unknownErr(datestr) + } + p.stateTime = timeStart + i++ + break iterRunes + case ' ': + p.yearlen = i - p.yeari + if !p.setYear() { + return p, p.unknownErr(datestr) + } + p.stateTime = timeStart + break iterRunes + default: + if !unicode.IsDigit(r) { + return p, p.unknownErr(datestr) + } } case dateAlphaPeriodWsDigit: @@ -1055,79 +1626,78 @@ iterRunes: p.stateDate = dateAlphaWsDigit p.dayi = i default: - return p, unknownErr(datestr) + return p, p.unknownErr(datestr) } - case dateWeekdayComma: - // Monday, 02 Jan 2006 15:04:05 MST - // Monday, 02 Jan 2006 15:04:05 -0700 - // Monday, 02 Jan 2006 15:04:05 +0100 - // Monday, 02-Jan-06 15:04:05 MST - if p.dayi == 0 { + + case dateAlphaSlash: + // Oct/ 7/1970 + // February/07/1970 + switch { + case r == ' ': + // continue + case unicode.IsDigit(r): + p.stateDate = dateAlphaSlashDigit p.dayi = i + default: + return p, p.unknownErr(datestr) } - switch r { - case ' ', '-': - if p.moi == 0 { - p.moi = i + 1 - p.daylen = i - p.dayi - p.setDay() - } else if p.yeari == 0 { - p.yeari = i + 1 - p.molen = i - p.moi - p.set(p.moi, "Jan") - } else { - p.stateTime = timeStart - break iterRunes + + case dateAlphaSlashDigit: + // dateAlphaSlash: + // dateAlphaSlashDigit: + // dateAlphaSlashDigitSlash: + // Oct/ 7/1970 + // Oct/07/1970 + // February/ 7/1970 + // February/07/1970 + switch { + case r == '/': + p.yeari = i + 1 + p.daylen = i - p.dayi + if !p.setDay() { + return p, p.unknownErr(datestr) } + p.stateDate = dateAlphaSlashDigitSlash + case unicode.IsDigit(r): + // continue + default: + return p, p.unknownErr(datestr) } - case dateWeekdayAbbrevComma: - // Mon, 02 Jan 2006 15:04:05 MST - // Mon, 02 Jan 2006 15:04:05 -0700 - // Thu, 13 Jul 2017 08:58:40 +0100 - // 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 - switch r { - case ' ', '-': - if p.dayi == 0 { - p.dayi = i + 1 - } else if p.moi == 0 { - p.daylen = i - p.dayi - p.setDay() - p.moi = i + 1 - } else if p.yeari == 0 { - p.molen = i - p.moi - p.set(p.moi, "Jan") - p.yeari = i + 1 - } else { - p.yearlen = i - p.yeari - p.setYear() - p.stateTime = timeStart - break iterRunes - } + + case dateAlphaSlashDigitSlash: + switch { + case unicode.IsDigit(r): + // continue + case r == ' ': + p.stateTime = timeStart + break iterRunes + default: + return p, p.unknownErr(datestr) } default: - break iterRunes + // Reaching an unhandled state unexpectedly should always fail parsing + return p, p.unknownErr(datestr) } } - p.coalesceDate(i) + if !p.coalesceDate(i) { + return p, p.unknownErr(datestr) + } if p.stateTime == timeStart { // increment first one, since the i++ occurs at end of loop if i < len(p.datestr) { i++ } // ensure we skip any whitespace prefix - for ; i < len(datestr); i++ { - r := rune(datestr[i]) + for ; i < len(p.datestr); i++ { + r := rune(p.datestr[i]) if r != ' ' { break } } - iterTimeRunes: - for ; i < len(datestr); i++ { - r := rune(datestr[i]) + for ; i < len(p.datestr); i++ { + r := rune(p.datestr[i]) // gou.Debugf("i=%d r=%s state=%d iterTimeRunes %s %s", i, string(r), p.stateTime, p.ds(), p.ts()) @@ -1141,60 +1711,43 @@ iterRunes: // 05:24:37 PM // 06:20:00 UTC // 06:20:00 UTC-05 - // 00:12:00 +0000 UTC - // 22:18:00 +0000 UTC m=+0.000000001 - // 15:04:05 -0700 - // 15:04:05 -07:00 - // 15:04:05 2008 - // timeOffset - // 03:21:51+00:00 - // 19:55:00+0100 + // timeWsYear + // 15:04:05 2008 + // timeWsOffset + // 00:12:00 +0000 UTC + // 22:18:00 +0000 UTC m=+0.000000001 + // 03:21:51+00:00 + // 19:55:00+0100 + // 15:04:05 -0700 + // 15:04:05 -07:00 // timePeriod // 17:24:37.3186369 // 00:07:31.945167 // 18:31:59.257000000 // 00:00:00.000 - // timePeriodOffset - // 19:55:00.799+0100 - // timePeriodOffsetColon - // 15:04:05.999-07:00 - // timePeriodWs - // timePeriodWsOffset - // 00:07:31.945167 +0000 - // 00:00:00.000 +0000 - // timePeriodWsOffsetAlpha - // 00:07:31.945167 +0000 UTC - // 22:18:00.001 +0000 UTC m=+0.000000001 - // 00:00:00.000 +0000 UTC - // timePeriodWsAlpha - // 06:20:00.000 UTC + // (and all variants that can follow the seconds portion of a time format, same as above) if p.houri == 0 { p.houri = i } switch r { - case ',': - // hm, lets just swap out comma for period. for some reason go - // won't parse it. - // 2014-05-11 08:20:13,787 - ds := []byte(p.datestr) - ds[i] = '.' - return parseTime(string(ds), loc, opts...) case '-', '+': // 03:21:51+00:00 - p.stateTime = timeOffset + p.offseti = i + p.stateTime = timeWsOffset if p.seci == 0 { // 22:18+0530 p.minlen = i - p.mini + } else if p.seclen == 0 { + p.seclen = i - p.seci + } else if p.msi > 0 && p.mslen == 0 { + p.mslen = i - p.msi + } else if p.parsedAMPM { + // time fully parsed, plus AM/PM indicator, this is OK } else { - if p.seclen == 0 { - p.seclen = i - p.seci - } - if p.msi > 0 && p.mslen == 0 { - p.mslen = i - p.msi - } + return p, p.unknownErr(datestr) } - p.offseti = i - case '.': + case '.', ',': + // NOTE: go 1.20 can now parse a string that has a comma delimiter properly p.stateTime = timePeriod p.seclen = i - p.seci p.msi = i + 1 @@ -1207,40 +1760,52 @@ iterRunes: } // (Z)ulu time p.loc = time.UTC - case 'a', 'A': - if p.nextIs(i, 't') || p.nextIs(i, 'T') { + endPos := i + 1 + if endPos > p.formatSetLen { + p.formatSetLen = endPos + } + case 'a', 'A', 'p', 'P': + if (r == 'a' || r == 'A') && (p.nextIs(i, 't') || p.nextIs(i, 'T')) { // x // September 17, 2012 at 5:00pm UTC-05 - i++ // skip t + i++ // skip 't' if p.nextIs(i, ' ') { // x // September 17, 2012 at 5:00pm UTC-05 - i++ // skip ' + i++ // skip ' ' p.houri = 0 // reset hour + } else { + return p, p.unknownErr(datestr) } } else { + // Could be AM/PM (followed by whitespace or an offset) + isLower := r == 'a' || r == 'p' + isTwoLetterWord := ((i+2) == len(p.datestr) || (len(p.datestr) > i+2 && (p.datestr[i+2] == ' ' || p.datestr[i+2] == '+' || p.datestr[i+2] == '-'))) switch { - case r == 'a' && p.nextIs(i, 'm'): - p.coalesceTime(i) - p.set(i, "am") - case r == 'A' && p.nextIs(i, 'M'): - p.coalesceTime(i) + case isLower && p.nextIs(i, 'm') && isTwoLetterWord && !p.parsedAMPM: + if !p.coalesceTime(i) { + return p, p.unknownErr(datestr) + } + p.set(i, "pm") + p.parsedAMPM = true + // skip 'm' + i++ + case !isLower && p.nextIs(i, 'M') && isTwoLetterWord && !p.parsedAMPM: + if !p.coalesceTime(i) { + return p, p.unknownErr(datestr) + } p.set(i, "PM") + p.parsedAMPM = true + // skip 'M' + i++ + default: + return p, p.unexpectedTail(i) } } - - case 'p', 'P': - // Could be AM/PM - switch { - case r == 'p' && p.nextIs(i, 'm'): - p.coalesceTime(i) - p.set(i, "pm") - case r == 'P' && p.nextIs(i, 'M'): - p.coalesceTime(i) - p.set(i, "PM") - } case ' ': - p.coalesceTime(i) + if !p.coalesceTime(i) { + return p, p.unknownErr(datestr) + } p.stateTime = timeWs case ':': if p.mini == 0 { @@ -1252,7 +1817,11 @@ iterRunes: } else if p.seci > 0 { // 18:31:59:257 ms uses colon, wtf p.seclen = i - p.seci - p.set(p.seci, "05") + if p.seclen == 2 { + p.set(p.seci, "05") + } else { + return p, p.unknownErr(datestr) + } p.msi = i + 1 // gross, gross, gross. manipulating the datestr is horrible. @@ -1260,41 +1829,36 @@ iterRunes: // Could not get the parsing to work using golang time.Parse() without // replacing that colon with period. p.set(i, ".") - datestr = datestr[0:i] + "." + datestr[i+1:] - p.datestr = datestr + newDatestr := p.datestr[0:i] + "." + p.datestr[i+1:] + p.datestr = newDatestr + p.stateTime = timePeriod } } - case timeOffset: - // 19:55:00+0100 - // timeOffsetColon - // 15:04:05+07:00 - // 15:04:05-07:00 - if r == ':' { - p.stateTime = timeOffsetColon - } case timeWs: // timeWsAlpha // 06:20:00 UTC // 06:20:00 UTC-05 // 15:44:11 UTC+0100 2015 // 18:04:07 GMT+0100 (GMT Daylight Time) + // 18:04:07 GMT+01:00 (GMT Daylight Time) // 17:57:51 MST 2009 // timeWsAMPMMaybe // 05:24:37 PM // timeWsOffset // 15:04:05 -0700 // 00:12:00 +0000 UTC - // timeWsOffsetColon - // 15:04:05 -07:00 - // 17:57:51 -0700 2009 - // timeWsOffsetColonAlpha - // 00:12:00 +00:00 UTC + // 15:04:05 -07:00 + // 17:57:51 -0700 2009 + // 00:12:00 +00:00 UTC // timeWsYear - // 00:12:00 2008 + // 00:12:00 2008 + // merge to state timeWsOffset + // 00:12:00 2008 -0700 + // 00:12:00 2008 -07:00 // timeZ // 15:04:05.99Z switch r { - case 'A', 'P': + case 'a', 'p', 'A', 'P': // Could be AM/PM or could be PST or similar p.tzi = i p.stateTime = timeWsAMPMMaybe @@ -1311,45 +1875,120 @@ iterRunes: p.stateTime = timeWsAlpha } else if unicode.IsDigit(r) { // 00:12:00 2008 - p.stateTime = timeWsYear - p.yeari = i + if p.yeari == 0 { + p.stateTime = timeWsYear + p.yeari = i + } else { + return p, p.unknownErr(datestr) + } + } else if r == '(' { + // (start of time zone description, ignore) + } else { + return p, p.unknownErr(datestr) + } + } + case timeWsYear: + // merge to state timeWsOffset + // 00:12:00 2008 -0700 + // 00:12:00 2008 -07:00 + switch r { + case ' ': + if p.yearlen == 0 { + p.yearlen = i - p.yeari + if !p.setYear() { + return p, p.unknownErr(datestr) + } + } + // else allow multiple trailing whitespace + case '+', '-': + // The year must be followed by a space before an offset! + if p.yearlen > 0 { + p.offseti = i + p.stateTime = timeWsOffset + } else { + return p, p.unknownErr(datestr) + } + default: + if unicode.IsDigit(r) { + if p.yearlen > 0 { + return p, p.unknownErr(datestr) + } + } else { + return p, p.unknownErr(datestr) } } case timeWsAlpha: // 06:20:00 UTC // 06:20:00 UTC-05 + // 06:20:00 (EST) // timeWsAlphaWs - // 17:57:51 MST 2009 + // timeWsAlphaWsYear + // 17:57:51 MST 2009 // timeWsAlphaZoneOffset // timeWsAlphaZoneOffsetWs // timeWsAlphaZoneOffsetWsExtra // 18:04:07 GMT+0100 (GMT Daylight Time) + // 18:04:07 GMT+01:00 (GMT Daylight Time) // timeWsAlphaZoneOffsetWsYear // 15:44:11 UTC+0100 2015 switch r { case '+', '-': - p.tzlen = i - p.tzi - if p.tzlen == 4 { - p.set(p.tzi, " MST") - } else if p.tzlen == 3 { - p.set(p.tzi, "MST") + tzNameLower := strings.ToLower(p.datestr[p.tzi:i]) + if tzNameLower == "gmt" || tzNameLower == "utc" || tzNameLower == "tz" { + // This is a special form where the actual timezone isn't UTC, but is rather + // specifying that the correct offset is a specified numeric offset from UTC: + // 06:20:00 UTC-05 + // 06:20:00 GMT+02 + p.tzi = 0 + p.tzlen = 0 + } else { + p.tzlen = i - p.tzi + } + if p.tzlen > 0 { + if err := p.setTZName(datestr); err != nil { + return p, err + } } p.stateTime = timeWsAlphaZoneOffset p.offseti = i - case ' ': + case ' ', ')': // 17:57:51 MST 2009 // 17:57:51 MST + // 06:20:00 (EST) p.tzlen = i - p.tzi - if p.tzlen == 4 { - p.set(p.tzi, " MST") - } else if p.tzlen == 3 { - p.set(p.tzi, "MST") + if err := p.setTZName(datestr); err != nil { + return p, err + } + if r == ' ' { + p.stateTime = timeWsAlphaWs + } else { + // 06:20:00 (EST) + // This must be the end of the datetime or the format is unknown + if i+1 == len(p.datestr) { + p.stateTime = timeWsAlphaRParen + } else { + return p, p.unknownErr(datestr) + } } - p.stateTime = timeWsAlphaWs - p.yeari = i + 1 } case timeWsAlphaWs: + // timeWsAlphaWsYear // 17:57:51 MST 2009 + if unicode.IsDigit(r) { + if p.yeari == 0 { + p.yeari = i + } else { + return p, p.unknownErr(datestr) + } + p.stateTime = timeWsAlphaWsYear + } else if r == '(' { + p.extra = i - 1 + p.stateTime = timeWsOffsetWsTZDescInParen + } + case timeWsAlphaWsYear: + if !unicode.IsDigit(r) { + return p, p.unknownErr(datestr) + } case timeWsAlphaZoneOffset: // 06:20:00 UTC-05 @@ -1357,35 +1996,68 @@ iterRunes: // timeWsAlphaZoneOffsetWs // timeWsAlphaZoneOffsetWsExtra // 18:04:07 GMT+0100 (GMT Daylight Time) + // 18:04:07 GMT+01:00 (GMT Daylight Time) // timeWsAlphaZoneOffsetWsYear // 15:44:11 UTC+0100 2015 switch r { case ' ': - p.set(p.offseti, "-0700") - if p.yeari == 0 { - p.yeari = i + 1 + if err := p.setTZOffset(i, datestr); err != nil { + return p, err } p.stateTime = timeWsAlphaZoneOffsetWs + default: + if r != ':' && !unicode.IsDigit(r) { + return p, p.unknownErr(datestr) + } } case timeWsAlphaZoneOffsetWs: // timeWsAlphaZoneOffsetWs // timeWsAlphaZoneOffsetWsExtra // 18:04:07 GMT+0100 (GMT Daylight Time) + // 18:04:07 GMT+01:00 (GMT Daylight Time) // timeWsAlphaZoneOffsetWsYear // 15:44:11 UTC+0100 2015 if unicode.IsDigit(r) { - p.stateTime = timeWsAlphaZoneOffsetWsYear - } else { + if p.yeari == 0 { + p.yeari = i + p.stateTime = timeWsAlphaZoneOffsetWsYear + } else { + return p, p.unknownErr(datestr) + } + } else if r == '(' { p.extra = i - 1 - p.stateTime = timeWsAlphaZoneOffsetWsExtra + p.stateTime = timeWsOffsetWsTZDescInParen + } else { + return p, p.unknownErr(datestr) } + case timeWsOffsetWsTZDescInParen: + // timeWsAlphaZoneOffsetWs + // timeWsAlphaZoneOffsetWsExtra + // 18:04:07 GMT+0100 (GMT Daylight Time) + // 18:04:07 GMT+01:00 (GMT Daylight Time) + switch r { + case '(': + return p, p.unknownErr(datestr) + case ')': + // must be the end + if i != len(p.datestr)-1 { + return p, p.unknownErr(datestr) + } + } + // any other char is OK case timeWsAlphaZoneOffsetWsYear: // 15:44:11 UTC+0100 2015 if unicode.IsDigit(r) { p.yearlen = i - p.yeari + 1 if p.yearlen == 4 { - p.setYear() + if !p.setYear() { + return p, p.unknownErr(datestr) + } + } else if p.yearlen > 4 { + return p, p.unknownErr(datestr) } + } else { + return p, p.unknownErr(datestr) } case timeWsAMPMMaybe: // timeWsAMPMMaybe @@ -1394,337 +2066,363 @@ iterRunes: // timeWsAlpha // 00:12:00 PST // 15:44:11 UTC+0100 2015 - if r == 'M' { - //return parse("2006-01-02 03:04:05 PM", datestr, loc) + isTwoLetterWord := ((i+1) == len(p.datestr) || p.nextIs(i, ' ')) + if (r == 'm' || r == 'M') && isTwoLetterWord { + if p.parsedAMPM { + return p, p.unexpectedTail(i) + } + // This isn't a time zone after all... + p.tzi = 0 p.stateTime = timeWsAMPM - p.set(i-1, "PM") - if p.hourlen == 2 { + if r == 'm' { + p.set(i-1, "pm") + } else { + p.set(i-1, "PM") + } + p.parsedAMPM = true + switch p.hourlen { + case 2: p.set(p.houri, "03") - } else if p.hourlen == 1 { + case 1: p.set(p.houri, "3") + default: + return p, p.unknownErr(datestr) } } else { p.stateTime = timeWsAlpha } + case timeWsAMPM: + // If we have a continuation after AM/PM indicator, reset parse state back to ws + if r == ' ' { + p.stateTime = timeWs + } else { + // unexpected garbage after AM/PM indicator, fail + return p, p.unexpectedTail(i) + } + case timeWsOffset: // timeWsOffset // 15:04:05 -0700 // timeWsOffsetWsOffset // 17:57:51 -0700 -07 // timeWsOffsetWs - // 17:57:51 -0700 2009 - // 00:12:00 +0000 UTC - // timeWsOffsetColon // 15:04:05 -07:00 - // timeWsOffsetColonAlpha + // timeWsOffsetWsYear + // 17:57:51 -0700 2009 + // timeWsOffsetWsAlphaZone + // 00:12:00 +0000 UTC // 00:12:00 +00:00 UTC + // timeWsOffsetWsAlphaZoneWs --> timeWsOffsetWsYear (overloaded) + // 00:12:00 +00:00 UTC 2009 + // timeWsOffsetWsTZDescInParen + // 00:12:00 +00:00 UTC (Universal Coordinated Time) switch r { case ':': - p.stateTime = timeWsOffsetColon + // Parse the case where an offset has a colon the same as timeWsOffset! + // continue case ' ': - p.set(p.offseti, "-0700") - p.yeari = i + 1 + if err := p.setTZOffset(i, datestr); err != nil { + return p, err + } p.stateTime = timeWsOffsetWs + default: + if !unicode.IsDigit(r) { + return p, p.unknownErr(datestr) + } } case timeWsOffsetWs: - // 17:57:51 -0700 2009 - // 00:12:00 +0000 UTC - // 22:18:00.001 +0000 UTC m=+0.000000001 + // timeWsOffsetWs + // timeWsOffsetWsYear + // 17:57:51 -0700 2009 + // 17:57:51 -07:00 2009 + // timeWsOffsetWsAlphaZone + // 00:12:00 +0000 UTC + // 00:12:00 +00:00 UTC + // 22:18:00.001 +0000 UTC m=+0.000000001 + // 22:18:00.001 +00:00 UTC m=+0.000000001 // w Extra // 17:57:51 -0700 -07 + // 17:57:51 -07:00 -07 + // 22:18:00.001 +0000 m=+0.000000001 + // 00:00:00 +0300 (European Daylight Time) + // 00:00:00 +03:00 (European Daylight Time) switch r { - case '=': - // eff you golang - if datestr[i-1] == 'm' { - p.extra = i - 2 - p.trimExtra() - break - } - case '+', '-', '(': + case '+', '-': // This really doesn't seem valid, but for some reason when round-tripping a go date // their is an extra +03 printed out. seems like go bug to me, but, parsing anyway. // 00:00:00 +0300 +03 // 00:00:00 +0300 +0300 + // 00:00:00 +03:00 +03 + // 00:00:00 +03:00 +0300 p.extra = i - 1 + p.trimExtra(false) p.stateTime = timeWsOffset - p.trimExtra() - break + case '(': + // 00:00:00 +0300 (European Daylight Time) + // 00:00:00 +03:00 (European Daylight Time) + p.extra = i - 1 + p.stateTime = timeWsOffsetWsTZDescInParen + case ' ': + // continue default: switch { case unicode.IsDigit(r): - p.yearlen = i - p.yeari + 1 - if p.yearlen == 4 { - p.setYear() + if p.yeari == 0 { + p.yeari = i + } else { + return p, p.unknownErr(datestr) } + p.stateTime = timeWsOffsetWsYear case unicode.IsLetter(r): - // 15:04:05 -0700 MST - if p.tzi == 0 { - p.tzi = i + if r == 'm' && p.nextIs(i, '=') { + // 22:18:00.001 +0000 UTC m=+0.000000001 + // 22:18:00.001 +00:00 UTC m=+0.000000001 + // very strange syntax! + p.extra = i - 1 + p.trimExtra(false) + } else { + // 15:04:05 -0700 MST + // 15:04:05 -07:00 MST + // 15:04:05 -07:00 MST (Mountain Standard Time) + // 15:04:05 -07:00 MST 2006 + if p.tzi == 0 { + p.tzi = i + } else { + return p, p.unknownErr(datestr) + } + p.stateTime = timeWsOffsetWsAlphaZone } + default: + return p, p.unknownErr(datestr) } } - case timeWsOffsetColon: - // timeWsOffsetColon - // 15:04:05 -07:00 - // timeWsOffsetColonAlpha - // 2015-02-18 00:12:00 +00:00 UTC - if unicode.IsLetter(r) { - // 2015-02-18 00:12:00 +00:00 UTC - p.stateTime = timeWsOffsetColonAlpha - break iterTimeRunes + case timeWsOffsetWsAlphaZone: + switch { + case r == ' ': + if p.tzi > 0 { + p.tzlen = i - p.tzi + if err := p.setTZName(datestr); err != nil { + return p, err + } + } else { + return p, p.unknownErr(datestr) + } + p.stateTime = timeWsOffsetWsAlphaZoneWs + case unicode.IsLetter(r): + // continue } + + case timeWsOffsetWsAlphaZoneWs: + switch r { + case '=': + // 22:18:00.001 +0000 UTC m=+0.000000001 + // very strange syntax! + if p.datestr[i-1] == 'm' { + p.extra = i - 2 + p.trimExtra(false) + } else { + return p, p.unknownErr(datestr) + } + case '(': + // 00:00:00 -0600 MDT (Mountain Daylight Time) + // 00:00:00 -06:00 MDT (Mountain Daylight Time) + p.extra = i - 1 + p.stateTime = timeWsOffsetWsTZDescInParen + case ' ': + // continue (extra whitespace) + case 'm': + if !p.nextIs(i, '=') { + return p, p.unknownErr(datestr) + } + default: + if unicode.IsDigit(r) { + if p.yeari == 0 { + p.yeari = i + } else { + return p, p.unknownErr(datestr) + } + p.stateTime = timeWsOffsetWsYear + } else { + return p, p.unknownErr(datestr) + } + } + + case timeWsOffsetWsYear: + if !unicode.IsDigit(r) { + return p, p.unknownErr(datestr) + } + case timePeriod: - // 15:04:05.999999999+07:00 - // 15:04:05.999999999-07:00 - // 15:04:05.999999+07:00 - // 15:04:05.999999-07:00 - // 15:04:05.999+07:00 - // 15:04:05.999-07:00 + // 15:04:05.999999999 + // 15:04:05.999999999 + // 15:04:05.999999 + // 15:04:05.999999 + // 15:04:05.999 + // 15:04:05.999 // timePeriod // 17:24:37.3186369 // 00:07:31.945167 // 18:31:59.257000000 // 00:00:00.000 - // timePeriodOffset - // 19:55:00.799+0100 - // timePeriodOffsetColon - // 15:04:05.999-07:00 - // timePeriodWs - // timePeriodWsOffset - // 00:07:31.945167 +0000 - // 00:00:00.000 +0000 - // With Extra - // 00:00:00.000 +0300 +03 - // timePeriodWsOffsetAlpha - // 00:07:31.945167 +0000 UTC - // 00:00:00.000 +0000 UTC - // 22:18:00.001 +0000 UTC m=+0.000000001 - // timePeriodWsAlpha - // 06:20:00.000 UTC + // (note: if we have an offset (+/-) or whitespace (Ws) after this state, re-enter the timeWs or timeWsOffset + // state above so that we do not have to duplicate all of the logic again for this parsing just because we + // have parsed a fractional second...) switch r { case ' ': p.mslen = i - p.msi - p.stateTime = timePeriodWs + if !p.coalesceTime(i) { + return p, p.unknownErr(datestr) + } + p.stateTime = timeWs case '+', '-': - // This really shouldn't happen p.mslen = i - p.msi p.offseti = i - p.stateTime = timePeriodOffset - default: - if unicode.IsLetter(r) { - // 06:20:00.000 UTC + p.stateTime = timeWsOffset + case 'Z': + p.stateTime = timeZ + p.mslen = i - p.msi + // (Z)ulu time + p.loc = time.UTC + endPos := i + 1 + if endPos > p.formatSetLen { + p.formatSetLen = endPos + } + case 'a', 'A', 'p', 'P': + // Could be AM/PM + isLower := r == 'a' || r == 'p' + isTwoLetterWord := ((i+2) == len(p.datestr) || p.nextIs(i+1, ' ')) + switch { + case isLower && p.nextIs(i, 'm') && isTwoLetterWord && !p.parsedAMPM: p.mslen = i - p.msi - p.stateTime = timePeriodWsAlpha + if !p.coalesceTime(i) { + return p, p.unknownErr(datestr) + } + p.set(i, "pm") + p.parsedAMPM = true + // skip 'm' + i++ + p.stateTime = timePeriodAMPM + case !isLower && p.nextIs(i, 'M') && isTwoLetterWord && !p.parsedAMPM: + p.mslen = i - p.msi + if !p.coalesceTime(i) { + return p, p.unknownErr(datestr) + } + p.set(i, "PM") + p.parsedAMPM = true + // skip 'M' + i++ + p.stateTime = timePeriodAMPM + default: + return p, p.unexpectedTail(i) + } + default: + if !unicode.IsDigit(r) { + return p, p.unexpectedTail(i) } } - case timePeriodOffset: - // timePeriodOffset - // 19:55:00.799+0100 - // timePeriodOffsetColon - // 15:04:05.999-07:00 - // 13:31:51.999-07:00 MST - if r == ':' { - p.stateTime = timePeriodOffsetColon - } - case timePeriodOffsetColon: - // timePeriodOffset - // timePeriodOffsetColon - // 15:04:05.999-07:00 - // 13:31:51.999 -07:00 MST + case timePeriodAMPM: switch r { case ' ': - p.set(p.offseti, "-07:00") - p.stateTime = timePeriodOffsetColonWs - p.tzi = i + 1 - } - case timePeriodOffsetColonWs: - // continue - case timePeriodWs: - // timePeriodWs - // timePeriodWsOffset - // 00:07:31.945167 +0000 - // 00:00:00.000 +0000 - // timePeriodWsOffsetAlpha - // 00:07:31.945167 +0000 UTC - // 00:00:00.000 +0000 UTC - // timePeriodWsOffsetColon - // 13:31:51.999 -07:00 MST - // timePeriodWsAlpha - // 06:20:00.000 UTC - if p.offseti == 0 { + p.stateTime = timeWs + case '+', '-': p.offseti = i - } - switch r { - case '+', '-': - p.mslen = i - p.msi - 1 - p.stateTime = timePeriodWsOffset + p.stateTime = timeWsOffset default: - if unicode.IsLetter(r) { - // 00:07:31.945167 +0000 UTC - // 00:00:00.000 +0000 UTC - p.stateTime = timePeriodWsOffsetWsAlpha - break iterTimeRunes - } + return p, p.unexpectedTail(i) } - - case timePeriodWsOffset: - // timePeriodWs - // timePeriodWsOffset - // 00:07:31.945167 +0000 - // 00:00:00.000 +0000 - // With Extra - // 00:00:00.000 +0300 +03 - // timePeriodWsOffsetAlpha - // 00:07:31.945167 +0000 UTC - // 00:00:00.000 +0000 UTC - // 03:02:00.001 +0300 MSK m=+0.000000001 - // timePeriodWsOffsetColon - // 13:31:51.999 -07:00 MST - // timePeriodWsAlpha - // 06:20:00.000 UTC - switch r { - case ':': - p.stateTime = timePeriodWsOffsetColon - case ' ': - p.set(p.offseti, "-0700") - case '+', '-': - // This really doesn't seem valid, but for some reason when round-tripping a go date - // their is an extra +03 printed out. seems like go bug to me, but, parsing anyway. - // 00:00:00.000 +0300 +03 - // 00:00:00.000 +0300 +0300 - p.extra = i - 1 - p.trimExtra() - break - default: - if unicode.IsLetter(r) { - // 00:07:31.945167 +0000 UTC - // 00:00:00.000 +0000 UTC - // 03:02:00.001 +0300 MSK m=+0.000000001 - p.stateTime = timePeriodWsOffsetWsAlpha - } - } - case timePeriodWsOffsetWsAlpha: - // 03:02:00.001 +0300 MSK m=+0.000000001 - // eff you golang - if r == '=' && datestr[i-1] == 'm' { - p.extra = i - 2 - p.trimExtra() - break - } - - case timePeriodWsOffsetColon: - // 13:31:51.999 -07:00 MST - switch r { - case ' ': - p.set(p.offseti, "-07:00") - default: - if unicode.IsLetter(r) { - // 13:31:51.999 -07:00 MST - p.tzi = i - p.stateTime = timePeriodWsOffsetColonAlpha - } - } - case timePeriodWsOffsetColonAlpha: - // continue case timeZ: - // timeZ - // 15:04:05.99Z - // With a time-zone at end after Z - // 2006-01-02T15:04:05.999999999Z07:00 - // 2006-01-02T15:04:05Z07:00 - // RFC3339 = "2006-01-02T15:04:05Z07:00" - // RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00" - if unicode.IsDigit(r) { - p.stateTime = timeZDigit - } - + // nothing expected can come after Z + return p, p.unexpectedTail(i) } } switch p.stateTime { case timeWsAlpha: - switch len(p.datestr) - p.tzi { - case 3: - // 13:31:51.999 +01:00 CET - p.set(p.tzi, "MST") - case 4: - p.set(p.tzi, "MST") - p.extra = len(p.datestr) - 1 - p.trimExtra() + p.tzlen = i - p.tzi + if err := p.setTZName(datestr); err != nil { + return p, err } - case timeWsAlphaWs: + case timeWsAlphaRParen: + // nothing extra to do + + case timeWsYear, timeWsAlphaWsYear: p.yearlen = i - p.yeari - p.setYear() - case timeWsYear: - p.yearlen = i - p.yeari - p.setYear() - case timeWsAlphaZoneOffsetWsExtra: - p.trimExtra() + if !p.setYear() { + return p, p.unknownErr(datestr) + } + case timeWsOffsetWsTZDescInParen: + // The last character must be a closing ')' + if i <= 0 || p.datestr[i-1] != ')' { + return p, p.unknownErr(datestr) + } + // As a special case, if we don't yet have a timezone name, + // and the content in the paren is 3-4 characters, then treat + // this as a time zone name instead + if len(p.datestr) >= p.extra+1+3+1 { + parenContentsLen := (i - 1) - (p.extra + 2) + if p.tzi == 0 && (parenContentsLen >= 3 && parenContentsLen <= 4) { + p.tzi = p.extra + 2 + p.tzlen = parenContentsLen + if err := p.setTZName(datestr); err != nil { + return p, err + } + p.extra = 0 + } + } + if p.extra > 0 { + p.trimExtra(false) + } case timeWsAlphaZoneOffset: // 06:20:00 UTC-05 - if i-p.offseti < 4 { - p.set(p.offseti, "-07") - } else { - p.set(p.offseti, "-0700") + if err := p.setTZOffset(i, datestr); err != nil { + return p, err } case timePeriod: p.mslen = i - p.msi - case timeOffset: - - switch len(p.datestr) - p.offseti { - case 0, 1, 2, 4: - return p, fmt.Errorf("TZ offset not recognized %q near %q (must be 2 or 4 digits optional colon)", datestr, string(datestr[p.offseti:])) - case 3: - // 19:55:00+01 - p.set(p.offseti, "-07") - case 5: - // 19:55:00+0100 - p.set(p.offseti, "-0700") + if p.mslen >= 10 { + if p.simpleErrorMessages { + return p, ErrFracSecTooLong + } else { + return p, fmt.Errorf("%w in %q near %q", ErrFracSecTooLong, datestr, p.datestr[p.msi:p.mslen]) + } + } + case timeWsOffset: + // 17:57:51 -07:00 (or 19:55:00.799 +01:00) + // 15:04:05+07:00 (or 19:55:00.799+01:00) + // 17:57:51 2006 -07:00 (or 19:55:00.799 +01:00) + if err := p.setTZOffset(len(p.datestr), datestr); err != nil { + return p, err } - case timeWsOffset: - p.set(p.offseti, "-0700") - case timeWsOffsetWs: + case timeWsOffsetWsYear: // 17:57:51 -0700 2009 + p.yearlen = len(p.datestr) - p.yeari + if p.yearlen == 4 { + if !p.setYear() { + return p, p.unknownErr(datestr) + } + } else if p.yearlen > 4 { + return p, p.unknownErr(datestr) + } + + case timeWsOffsetWsAlphaZone: // 00:12:00 +0000 UTC if p.tzi > 0 { - switch len(p.datestr) - p.tzi { - case 3: - // 13:31:51.999 +01:00 CET - p.set(p.tzi, "MST") - case 4: - // 13:31:51.999 +01:00 CEST - p.set(p.tzi, "MST ") + p.tzlen = i - p.tzi + if err := p.setTZName(datestr); err != nil { + return p, err } - + } else { + return p, p.unknownErr(datestr) } - case timeWsOffsetColon: - // 17:57:51 -07:00 - p.set(p.offseti, "-07:00") - case timeOffsetColon: - // 15:04:05+07:00 - p.set(p.offseti, "-07:00") - case timePeriodOffset: - // 19:55:00.799+0100 - p.set(p.offseti, "-0700") - case timePeriodOffsetColon: - p.set(p.offseti, "-07:00") - case timePeriodWsOffsetColonAlpha: - p.tzlen = i - p.tzi - switch p.tzlen { - case 3: - p.set(p.tzi, "MST") - case 4: - p.set(p.tzi, "MST ") - } - case timePeriodWsOffset: - p.set(p.offseti, "-0700") } - p.coalesceTime(i) + if !p.coalesceTime(i) { + return p, p.unknownErr(datestr) + } } switch p.stateDate { @@ -1739,36 +2437,36 @@ iterRunes: // 20140601 8 yyyymmdd // 2014 4 yyyy t := time.Time{} - if len(datestr) == len("1499979655583057426") { // 19 + if len(p.datestr) == len("1499979655583057426") { // 19 // nano-seconds - if nanoSecs, err := strconv.ParseInt(datestr, 10, 64); err == nil { + if nanoSecs, err := strconv.ParseInt(p.datestr, 10, 64); err == nil { t = time.Unix(0, nanoSecs) } - } else if len(datestr) == len("1499979795437000") { // 16 + } else if len(p.datestr) == len("1499979795437000") { // 16 // micro-seconds - if microSecs, err := strconv.ParseInt(datestr, 10, 64); err == nil { + if microSecs, err := strconv.ParseInt(p.datestr, 10, 64); err == nil { t = time.Unix(0, microSecs*1000) } - } else if len(datestr) == len("yyyyMMddhhmmss") { // 14 + } else if len(p.datestr) == len("yyyyMMddhhmmss") { // 14 // yyyyMMddhhmmss - p.format = []byte("20060102150405") + p.setEntireFormat([]byte("20060102150405")) return p, nil - } else if len(datestr) == len("1332151919000") { // 13 - if miliSecs, err := strconv.ParseInt(datestr, 10, 64); err == nil { + } else if len(p.datestr) == len("1332151919000") { // 13 + if miliSecs, err := strconv.ParseInt(p.datestr, 10, 64); err == nil { t = time.Unix(0, miliSecs*1000*1000) } - } else if len(datestr) == len("1332151919") { //10 - if secs, err := strconv.ParseInt(datestr, 10, 64); err == nil { + } else if len(p.datestr) == len("1332151919") { //10 + if secs, err := strconv.ParseInt(p.datestr, 10, 64); err == nil { t = time.Unix(secs, 0) } - } else if len(datestr) == len("20140601") { - p.format = []byte("20060102") + } else if len(p.datestr) == len("20140601") { + p.setEntireFormat([]byte("20060102")) return p, nil - } else if len(datestr) == len("2014") { - p.format = []byte("2006") + } else if len(p.datestr) == len("2014") { + p.setEntireFormat([]byte("2006")) return p, nil - } else if len(datestr) < 4 { - return nil, fmt.Errorf("unrecognized format, too short %v", datestr) + } else { + return p, p.unknownErr(datestr) } if !t.IsZero() { if loc == nil { @@ -1778,6 +2476,8 @@ iterRunes: t = t.In(loc) p.t = &t return p, nil + } else { + return p, p.unknownErr(datestr) } case dateDigitSt: // 171113 14:14:20 @@ -1796,19 +2496,15 @@ iterRunes: case dateYearDashDashOffset: /// 2020-07-20+00:00 - switch len(p.datestr) - p.offseti { - case 5: - p.set(p.offseti, "-0700") - case 6: - p.set(p.offseti, "-07:00") + if err := p.setTZOffset(len(p.datestr), datestr); err != nil { + return p, err } return p, nil - case dateYearDashAlphaDash: + case dateYearDashAlpha: // 2013-Feb-03 // 2013-Feb-3 - p.daylen = i - p.dayi - p.setDay() + // 2013-February-3 return p, nil case dateYearDashDashWs: @@ -1818,39 +2514,62 @@ iterRunes: case dateYearDashDashT: return p, nil - case dateDigitDashAlphaDash: - // 13-Feb-03 ambiguous - // 28-Feb-03 ambiguous - // 29-Jun-2016 - length := len(datestr) - (p.moi + p.molen + 1) - if length == 4 { - p.yearlen = 4 - p.set(p.yeari, "2006") - // We now also know that part1 was the day - p.dayi = 0 - p.daylen = p.part1Len - p.setDay() - } else if length == 2 { - // We have no idea if this is - // yy-mon-dd OR dd-mon-yy - // - // We are going to ASSUME (bad, bad) that it is dd-mon-yy which is a horible assumption - p.ambiguousMD = true - p.yearlen = 2 - p.set(p.yeari, "06") - // We now also know that part1 was the day - p.dayi = 0 - p.daylen = p.part1Len - p.setDay() + case dateDigitDashAlphaDash, dateDigitDashDigitDash: + // This has already been done if we parsed the time already + if p.stateTime == timeIgnore { + // dateDigitDashAlphaDash: + // 13-Feb-03 ambiguous + // 28-Feb-03 ambiguous + // 29-Jun-2016 + // dateDigitDashDigitDash: + // 29-06-2026 + length := len(p.datestr) - (p.moi + p.molen + 1) + switch length { + case 4: + p.yearlen = 4 + p.set(p.yeari, "2006") + // We now also know that part1 was the day + p.dayi = p.skip + p.daylen = p.part1Len + if !p.setDay() { + return p, p.unknownErr(datestr) + } + case 2: + // We have no idea if this is + // yy-mon-dd OR dd-mon-yy + // (or for dateDigitDashDigitDash, yy-mm-dd OR dd-mm-yy) + // + // We are going to ASSUME (bad, bad) that it is dd-mon-yy (dd-mm-yy), + // which is a horrible assumption, but seems to be the convention for + // dates that are formatted in this way. + p.ambiguousMD = true // not retryable + p.yearlen = 2 + p.set(p.yeari, "06") + // We now also know that part1 was the day + p.dayi = p.skip + p.daylen = p.part1Len + if !p.setDay() { + return p, p.unknownErr(datestr) + } + default: + return p, p.unknownErr(datestr) + } } return p, nil case dateDigitDot: - // 2014.05 - p.molen = i - p.moi - p.setMonth() - return p, nil + if len(datestr) == len("yyyyMMddhhmmss.SSS") { // 18 + p.setEntireFormat([]byte("20060102150405.000")) + return p, nil + } else { + // 2014.05 + p.molen = i - p.moi + if !p.setMonth() { + return p, p.unknownErr(datestr) + } + return p, nil + } case dateDigitDotDot: // 03.31.1981 @@ -1861,6 +2580,20 @@ iterRunes: // 2018.09.30 return p, nil + case dateDigitDotDotWs: + // 2013.04.01 + return p, nil + + case dateDigitDotDotT: + return p, nil + + case dateDigitDotDotOffset: + // 2020.07.20+00:00 + if err := p.setTZOffset(len(p.datestr), datestr); err != nil { + return p, err + } + return p, nil + case dateDigitWsMoYear: // 2 Jan 2018 // 2 Jan 18 @@ -1869,28 +2602,24 @@ iterRunes: // 12 Feb 2006, 19:17 return p, nil - case dateDigitWsMolong: - // 18 January 2018 - // 8 January 2018 - if p.daylen == 2 { - p.format = []byte("02 January 2006") - return p, nil + case dateAlphaFullMonthWs: + if p.stateTime == timeIgnore && p.yearlen == 0 { + p.yearlen = i - p.yeari + if !p.setYear() { + return p, p.unknownErr(datestr) + } } - p.format = []byte("2 January 2006") - return p, nil // parse("2 January 2006", datestr, loc) - - case dateAlphaWsMonth: - p.yearlen = i - p.yeari - p.setYear() return p, nil - case dateAlphaWsMonthMore: + case dateAlphaFullMonthWsDayWs: return p, nil case dateAlphaWsDigitMoreWs: // oct 1, 1970 p.yearlen = i - p.yeari - p.setYear() + if !p.setYear() { + return p, p.unknownErr(datestr) + } return p, nil case dateAlphaWsDigitMoreWsYear: @@ -1904,7 +2633,7 @@ iterRunes: case dateAlphaWsDigit: return p, nil - case dateAlphaWsDigitYearmaybe: + case dateAlphaWsDigitYearMaybe: return p, nil case dateDigitSlash: @@ -1913,7 +2642,7 @@ iterRunes: // 01/02/2006 return p, nil - case dateDigitSlashAlpha: + case dateDigitSlashAlphaSlash: // 03/Jun/2014 return p, nil @@ -1931,27 +2660,26 @@ iterRunes: case dateDigitChineseYear: // dateDigitChineseYear // 2014年04月08日 - p.format = []byte("2006年01月02日") + // 2014年4月12日 return p, nil case dateDigitChineseYearWs: - p.format = []byte("2006年01月02日 15:04:05") + // 2014年04月08日 00:00:00 ... return p, nil - case dateWeekdayComma: - // Monday, 02 Jan 2006 15:04:05 -0700 - // Monday, 02 Jan 2006 15:04:05 +0100 - // Monday, 02-Jan-06 15:04:05 MST + case dateAlphaSlashDigitSlash: + // Oct/ 7/1970 + // February/07/1970 return p, nil - case dateWeekdayAbbrevComma: - // Mon, 02-Jan-06 15:04:05 MST - // Mon, 02 Jan 2006 15:04:05 MST + case dateYearWsMonthWs: + // 2013 May 02 11:37:55 + // 2013 December 02 11:37:55 return p, nil } - return nil, unknownErr(datestr) + return p, p.unknownErr(datestr) } type parser struct { @@ -1959,12 +2687,18 @@ type parser struct { preferMonthFirst bool retryAmbiguousDateWithSwap bool ambiguousMD bool + ambiguousRetryable bool + allowPartialStringMatch bool + simpleErrorMessages bool stateDate dateState stateTime timeState format []byte + formatSetLen int datestr string fullMonth string + parsedAMPM bool skip int + link int extra int part1Len int yeari int @@ -1982,12 +2716,51 @@ type parser struct { msi int mslen int offseti int - offsetlen int tzi int tzlen int t *time.Time } +// something like: "Wednesday, 8 February 2023 19:00:46.999999999 +11:00 (AEDT) m=+0.000000001" +const longestPossibleDateStr = 78 + +// the format byte slice is always a little larger, in case we need to expand it to contain a full month +const formatExtraBufferBytes = 16 +const formatBufferCapacity = longestPossibleDateStr + formatExtraBufferBytes + +var parserPool = sync.Pool{ + New: func() interface{} { + // allocate a max-sized fixed-capacity format byte slice + // that will be re-used with this parser struct + return &parser{ + format: make([]byte, 0, formatBufferCapacity), + } + }, +} + +var emptyString = "" + +// Use to put a parser back into the pool in the right way +func putBackParser(p *parser) { + if p == nil { + return + } + // we'll be reusing the backing memory for the format byte slice, put it back + // to maximum capacity + if cap(p.format) == formatBufferCapacity { + p.format = p.format[:formatBufferCapacity] + } else { + // the parsing improperly process replaced this, get back a new one with the right cap + p.format = make([]byte, 0, formatBufferCapacity) + } + // clear out pointers so we don't leak memory we don't need any longer + p.loc = nil + p.datestr = emptyString + p.fullMonth = emptyString + p.t = nil + parserPool.Put(p) +} + // ParserOption defines a function signature implemented by options // Options defined like this accept the parser and operate on the data within type ParserOption func(*parser) error @@ -2008,22 +2781,58 @@ func RetryAmbiguousDateWithSwap(retryAmbiguousDateWithSwap bool) ParserOption { } } -func newParser(dateStr string, loc *time.Location, opts ...ParserOption) *parser { - p := &parser{ +// AllowPartialStringMatch is an option that allows allowPartialStringMatch to be changed from its default. +// If true, then strings can be attempted to be parsed / matched even if the end of the string might contain +// more than a date/time. This defaults to false. +func AllowPartialStringMatch(allowPartialStringMatch bool) ParserOption { + return func(p *parser) error { + p.allowPartialStringMatch = allowPartialStringMatch + return nil + } +} + +// SimpleErrorMessages is an option that will cause returned error messages to contain less detail, +// but it will avoid allocating any memory for the custom error message. If you expect to attempt +// to parse a lot of text that is not valid, this could help reduce GC pressure. +func SimpleErrorMessages(simpleErrorMessages bool) ParserOption { + return func(p *parser) error { + p.simpleErrorMessages = simpleErrorMessages + return nil + } +} + +// Creates a new parser. The caller must call putBackParser on the returned parser when done with it. +func newParser(dateStr string, loc *time.Location, opts ...ParserOption) (*parser, error) { + dateStrLen := len(dateStr) + if dateStrLen > longestPossibleDateStr { + var nilParser *parser + return nil, nilParser.unknownErr(dateStr) + } + + // Make sure to re-use the format byte slice from the pooled parser struct + p := parserPool.Get().(*parser) + // This re-slicing is guaranteed to work because of the length check above + startingFormat := p.format[:dateStrLen] + copy(startingFormat, dateStr) + *p = parser{ stateDate: dateStart, stateTime: timeIgnore, datestr: dateStr, loc: loc, preferMonthFirst: true, retryAmbiguousDateWithSwap: false, + format: startingFormat, + // this tracks how much of the format string has been set, to make sure all of it is set + formatSetLen: 0, } - p.format = []byte(dateStr) // 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 { @@ -2033,6 +2842,18 @@ func (p *parser) nextIs(i int, b byte) bool { return false } +func (p *parser) setEntireFormat(format []byte) { + // Copy so that we don't lose this pooled format byte slice + oldLen := len(p.format) + newLen := len(format) + if oldLen != newLen { + // guaranteed to work because of the allocated capacity for format buffers + p.format = p.format[:newLen] + } + copy(p.format, format) + p.formatSetLen = len(format) +} + func (p *parser) set(start int, val string) { if start < 0 { return @@ -2043,44 +2864,153 @@ func (p *parser) set(start int, val string) { for i, r := range val { p.format[start+i] = byte(r) } -} -func (p *parser) setMonth() { - if p.molen == 2 { - p.set(p.moi, "01") - } else if p.molen == 1 { - p.set(p.moi, "1") + endingPos := start + len(val) + if endingPos > p.formatSetLen { + p.formatSetLen = endingPos } } -func (p *parser) setDay() { - if p.daylen == 2 { +func (p *parser) setMonth() bool { + switch p.molen { + case 2: + p.set(p.moi, "01") + return true + case 1: + p.set(p.moi, "1") + return true + default: + return false + } +} + +func (p *parser) setDay() bool { + switch p.daylen { + case 2: p.set(p.dayi, "02") - } else if p.daylen == 1 { + return true + case 1: p.set(p.dayi, "2") + return true + default: + return false } } -func (p *parser) setYear() { - if p.yearlen == 2 { + +func (p *parser) setYear() bool { + switch p.yearlen { + case 2: p.set(p.yeari, "06") - } else if p.yearlen == 4 { + return true + case 4: p.set(p.yeari, "2006") + return true + default: + return false } } -func (p *parser) coalesceDate(end int) { + +func (p *parser) setTZOffset(i int, datestr string) error { + offsetlen := i - p.offseti + switch offsetlen { + case 3: + p.set(p.offseti, "-07") + case 5: + p.set(p.offseti, "-0700") + case 6: + p.set(p.offseti, "-07:00") + default: + if p.simpleErrorMessages { + return ErrUnknownTZOffset + } else { + return fmt.Errorf("%w %q near %q (must be 2 or 4 digits optional colon)", ErrUnknownTZOffset, datestr, p.datestr[p.offseti:i]) + } + } + return nil +} + +func (p *parser) setTZName(datestr string) error { + switch p.tzlen { + case 3: + p.set(p.tzi, "MST") + case 4: + p.set(p.tzi, "MST ") + default: + if p.simpleErrorMessages { + return ErrUnknownTimeZone + } else { + return fmt.Errorf("%w %q near %q (must be 3 or 4 characters)", ErrUnknownTimeZone, datestr, p.datestr[p.tzi:p.tzi+p.tzlen]) + } + } + return nil +} + +// Removes the characters at the given range from the format string. +// Fills the end of the format string with spaces rather than shortening it. +func (p *parser) removeRangeFromFormat(i, numBytes int) { + if i < 0 || i >= len(p.format) { + return + } + var startErase int + afterRemovedRange := i + numBytes + bytesToCopy := len(p.format) - afterRemovedRange + if bytesToCopy <= 0 { + // nothing to copy, erase everything from the removal point + startErase = i + } else { + copy(p.format[i:], p.format[afterRemovedRange:]) + startErase = i + bytesToCopy + } + // fill in spaces to erase the moved content in its old location + for index := startErase; index < len(p.format); index++ { + p.format[index] = ' ' + } +} + +// Find the proper end of the current component (scanning chars starting from start and going +// up until the end, and either returning at end or returning the first character that is +// not allowed, as determined by allowNumeric, allowAlpha, and allowOther) +func findProperEnd(s string, start, end int, allowNumeric bool, allowAlpha bool, allowOther bool) int { + for i := start; i < end; i++ { + c := s[i] + if c >= '0' && c <= '9' { + if !allowNumeric { + return i + } + } else if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') { + if !allowAlpha { + return i + } + } else { + if !allowOther { + return i + } + } + } + return end +} + +func (p *parser) coalesceDate(end int) bool { if p.yeari > 0 { if p.yearlen == 0 { - p.yearlen = end - p.yeari + p.yearlen = findProperEnd(p.datestr, p.yeari, end, true, false, false) - p.yeari + } + if !p.setYear() { + return false } - p.setYear() } if p.moi > 0 && p.molen == 0 { - p.molen = end - p.moi + p.molen = findProperEnd(p.datestr, p.moi, end, true, true, false) - p.moi + // The month may be the name of the month, so don't treat as invalid in this case. + // We can ignore the return value here. p.setMonth() } if p.dayi > 0 && p.daylen == 0 { - p.daylen = end - p.dayi - p.setDay() + p.daylen = findProperEnd(p.datestr, p.dayi, end, true, false, false) - p.dayi + if !p.setDay() { + return false + } } + return true } func (p *parser) ts() string { return fmt.Sprintf("h:(%d:%d) m:(%d:%d) s:(%d:%d)", p.houri, p.hourlen, p.mini, p.minlen, p.seci, p.seclen) @@ -2088,37 +3018,46 @@ func (p *parser) ts() string { func (p *parser) ds() string { return fmt.Sprintf("%s d:(%d:%d) m:(%d:%d) y:(%d:%d)", p.datestr, p.dayi, p.daylen, p.moi, p.molen, p.yeari, p.yearlen) } -func (p *parser) coalesceTime(end int) { +func (p *parser) coalesceTime(end int) bool { // 03:04:05 // 15:04:05 // 3:04:05 // 3:4:5 // 15:04:05.00 if p.houri > 0 { - if p.hourlen == 2 { + switch p.hourlen { + case 2: p.set(p.houri, "15") - } else if p.hourlen == 1 { + case 1: p.set(p.houri, "3") + default: + return false } } if p.mini > 0 { if p.minlen == 0 { p.minlen = end - p.mini } - if p.minlen == 2 { + switch p.minlen { + case 2: p.set(p.mini, "04") - } else { + case 1: p.set(p.mini, "4") + default: + return false } } if p.seci > 0 { if p.seclen == 0 { p.seclen = end - p.seci } - if p.seclen == 2 { + switch p.seclen { + case 2: p.set(p.seci, "05") - } else { + case 1: p.set(p.seci, "5") + default: + return false } } @@ -2126,64 +3065,161 @@ func (p *parser) coalesceTime(end int) { for i := 0; i < p.mslen; i++ { p.format[p.msi+i] = '0' } + endPos := p.msi + p.mslen + if endPos > p.formatSetLen { + p.formatSetLen = endPos + } } + return true } func (p *parser) setFullMonth(month string) { - if p.moi == 0 { - p.format = []byte(fmt.Sprintf("%s%s", "January", p.format[len(month):])) + oldLen := len(p.format) + const fullMonth = "January" + // Do an overlapping copy so we don't lose the pooled format buffer + part1Len := p.moi + part3 := p.format[p.moi+len(month):] + newLen := part1Len + len(fullMonth) + len(part3) + if newLen > oldLen { + // We can re-slice this, because the capacity is guaranteed to be a little longer than any possible datestr + p.format = p.format[:newLen] + } + // first part will not change, we need to shift the third part + copy(p.format[part1Len+len(fullMonth):], part3) + copy(p.format[part1Len:], fullMonth) + // shorten the format slice now if needed + if newLen < oldLen { + p.format = p.format[:newLen] + } + + if newLen > oldLen && p.formatSetLen >= p.moi { + p.formatSetLen += newLen - oldLen + } else if newLen < oldLen && p.formatSetLen >= p.moi { + p.formatSetLen -= oldLen - newLen + } + + if p.formatSetLen > len(p.format) { + p.formatSetLen = len(p.format) + } else if p.formatSetLen < len(fullMonth) { + p.formatSetLen = len(fullMonth) + } else if p.formatSetLen < 0 { + p.formatSetLen = 0 } } -func (p *parser) trimExtra() { +func (p *parser) trimExtra(onlyTrimFormat bool) { if p.extra > 0 && len(p.format) > p.extra { p.format = p.format[0:p.extra] - p.datestr = p.datestr[0:p.extra] + if p.formatSetLen > len(p.format) { + p.formatSetLen = len(p.format) + } + if !onlyTrimFormat { + p.datestr = p.datestr[0:p.extra] + } } } -// 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) { +func (p *parser) parse(originalLoc *time.Location, originalOpts ...ParserOption) (t time.Time, err error) { + if p == nil { + var nilParser *parser + return time.Time{}, nilParser.unknownErr("") + } if p.t != nil { return *p.t, nil } + + // Make sure that the entire string matched to a known format that was detected + if !p.allowPartialStringMatch && p.formatSetLen < len(p.format) { + // We can always ignore punctuation at the end of a date/time, but do not allow + // any numbers or letters in the format string. + validFormatTo := findProperEnd(bytesToString(p.format), p.formatSetLen, len(p.format), false, false, true) + if validFormatTo < len(p.format) { + return time.Time{}, p.unexpectedTail(p.formatSetLen) + } + } + + // Special case where the TZ name is 4 characters long and followed by punctuation, will cause parsing problems + // with the format 'MST ' (will expect a whitespace that isn't there after 4 char timezone). Most robust + // solution is to remove the extra whitespace. Even though it will cause offsets after this point to not match + // between the datestr and format string, it's not an issue at this point. + if p.tzlen == 4 && p.tzi+4 < len(p.format) && p.format[p.tzi+3] == ' ' && p.format[p.tzi+4] != ' ' { + p.removeRangeFromFormat(p.tzi+3, 1) + } + + // If we have a full month name, update the format string to use it (can change length of format string) if len(p.fullMonth) > 0 { p.setFullMonth(p.fullMonth) } - if p.skip > 0 && len(p.format) > p.skip { - p.format = p.format[p.skip:] + + if p.retryAmbiguousDateWithSwap && p.ambiguousMD && p.ambiguousRetryable { + // 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 + // by retrying in this case, we can fix a common situation with no assumptions + defer func() { + // if actual time parsing errors out with the following error, swap before we + // get out of this function to reduce scope it needs to be applied on + if err != nil && strings.Contains(err.Error(), "month out of range") { + // simple optimized case where mm and dd can be swapped directly + if p.molen == 2 && p.daylen == 2 && len(p.fullMonth) <= 0 && (p.tzi == 0 || (p.moi < p.tzi && p.dayi < p.tzi)) { + // skipped bytes have already been removed, so compensate for that + moi := p.moi - p.skip + p.moi = p.dayi - p.skip + p.dayi = moi + if !p.setDay() || !p.setMonth() { + err = p.unknownErr(p.datestr) + } else { + if p.loc == nil { + t, err = time.Parse(bytesToString(p.format), p.datestr) + } else { + t, err = time.ParseInLocation(bytesToString(p.format), p.datestr, p.loc) + } + } + } else { + // create the option to reverse the preference + preferMonthFirst := PreferMonthFirst(!p.preferMonthFirst) + // turn off the retry to avoid endless recursion + retryAmbiguousDateWithSwap := RetryAmbiguousDateWithSwap(false) + modifiedOpts := append(originalOpts, preferMonthFirst, retryAmbiguousDateWithSwap) + var newParser *parser + newParser, err = parseTime(p.datestr, originalLoc, modifiedOpts...) + defer putBackParser(newParser) + if err == nil { + t, err = newParser.parse(originalLoc, modifiedOpts...) + // The caller might use the format and datestr, so copy that back to the original parser + p.setEntireFormat(newParser.format) + p.datestr = newParser.datestr + } + } + } + }() + } + + if p.skip > len(p.format) { + p.skip = len(p.format) + } + if p.skip > 0 { + // copy and then re-slice to shorten to avoid losing the header of the pooled format string + copy(p.format, p.format[p.skip:]) + p.format = p.format[:len(p.format)-p.skip] + p.formatSetLen -= p.skip + if p.formatSetLen < 0 { + p.formatSetLen = 0 + } p.datestr = p.datestr[p.skip:] } if p.loc == nil { // gou.Debugf("parse layout=%q input=%q \ntx, err := time.Parse(%q, %q)", string(p.format), p.datestr, string(p.format), p.datestr) - return time.Parse(string(p.format), p.datestr) + return time.Parse(bytesToString(p.format), p.datestr) + } else { + //gou.Debugf("parse layout=%q input=%q \ntx, err := time.ParseInLocation(%q, %q, %v)", string(p.format), p.datestr, string(p.format), p.datestr, p.loc) + return time.ParseInLocation(bytesToString(p.format), p.datestr, p.loc) } - //gou.Debugf("parse layout=%q input=%q \ntx, err := time.ParseInLocation(%q, %q, %v)", string(p.format), p.datestr, string(p.format), p.datestr, p.loc) - return time.ParseInLocation(string(p.format), p.datestr, p.loc) } func isDay(alpha string) bool { - for _, day := range days { - if alpha == day { - return true - } - } - return false + _, ok := knownDays[alpha] + return ok } func isMonthFull(alpha string) bool { - for _, month := range months { - if alpha == month { - return true - } - } - return false + _, ok := knownMonths[alpha] + return ok } diff --git a/parseany_test.go b/parseany_test.go index 7fea1e6..c158a2a 100644 --- a/parseany_test.go +++ b/parseany_test.go @@ -9,83 +9,158 @@ import ( ) func TestOne(t *testing.T) { - time.Local = time.UTC - var ts time.Time - ts = MustParse("2020-07-20+08:00") + 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))) } type dateTest struct { - in, out, loc string - err bool + in, out, loc, zname string + err bool + preferDayFirst bool + retryAmbiguous bool + expectAmbiguous bool + allowWeekdayPrefix bool } var testInputs = []dateTest{ {in: "oct 7, 1970", out: "1970-10-07 00:00:00 +0000 UTC"}, + {in: "oct 7, 1970 11:15:26pm", out: "1970-10-07 23:15:26 +0000 UTC"}, {in: "oct 7, '70", out: "1970-10-07 00:00:00 +0000 UTC"}, + {in: "oct 7, '70 11:15:26pm", out: "1970-10-07 23:15:26 +0000 UTC"}, {in: "Oct 7, '70", out: "1970-10-07 00:00:00 +0000 UTC"}, + {in: "Oct 7, '70 11:15:26pm", out: "1970-10-07 23:15:26 +0000 UTC"}, {in: "Oct. 7, '70", out: "1970-10-07 00:00:00 +0000 UTC"}, + {in: "Oct. 7, '70 11:15:26pm", out: "1970-10-07 23:15:26 +0000 UTC"}, {in: "oct. 7, '70", out: "1970-10-07 00:00:00 +0000 UTC"}, + {in: "oct. 7, '70 11:15:26pm", out: "1970-10-07 23:15:26 +0000 UTC"}, {in: "oct. 7, 1970", out: "1970-10-07 00:00:00 +0000 UTC"}, + {in: "oct. 7, 1970 11:15:26pm", out: "1970-10-07 23:15:26 +0000 UTC"}, {in: "Sept. 7, '70", out: "1970-09-07 00:00:00 +0000 UTC"}, + {in: "Sept. 7, '70 11:15:26pm", out: "1970-09-07 23:15:26 +0000 UTC"}, {in: "sept. 7, 1970", out: "1970-09-07 00:00:00 +0000 UTC"}, + {in: "sept. 7, 1970 11:15:26pm", out: "1970-09-07 23:15:26 +0000 UTC"}, {in: "Feb 8, 2009 5:57:51 AM", out: "2009-02-08 05:57:51 +0000 UTC"}, {in: "May 8, 2009 5:57:51 PM", out: "2009-05-08 17:57:51 +0000 UTC"}, {in: "May 8, 2009 5:57:1 PM", out: "2009-05-08 17:57:01 +0000 UTC"}, {in: "May 8, 2009 5:7:51 PM", out: "2009-05-08 17:07:51 +0000 UTC"}, {in: "May 8, 2009, 5:7:51 PM", out: "2009-05-08 17:07:51 +0000 UTC"}, + {in: "June 8 2009", out: "2009-06-08 00:00:00 +0000 UTC"}, + {in: "June 8, 2009", out: "2009-06-08 00:00:00 +0000 UTC"}, + {in: "February 8th 2009", out: "2009-02-08 00:00:00 +0000 UTC"}, + {in: "February 8th, 2009", out: "2009-02-08 00:00:00 +0000 UTC"}, + {in: "September 3rd 2009", out: "2009-09-03 00:00:00 +0000 UTC"}, + {in: "September 3rd, 2009", out: "2009-09-03 00:00:00 +0000 UTC"}, + {in: "June 8 2009 11:15:26pm", out: "2009-06-08 23:15:26 +0000 UTC"}, + {in: "June 8, 2009 11:15:26pm", out: "2009-06-08 23:15:26 +0000 UTC"}, + {in: "February 8th 2009 11:15:26pm", out: "2009-02-08 23:15:26 +0000 UTC"}, + {in: "February 8th, 2009 11:15:26pm", out: "2009-02-08 23:15:26 +0000 UTC"}, + {in: "September 3rd 2009 11:15:26pm", out: "2009-09-03 23:15:26 +0000 UTC"}, + {in: "September 3rd, 2009 11:15:26pm", out: "2009-09-03 23:15:26 +0000 UTC"}, {in: "7 oct 70", out: "1970-10-07 00:00:00 +0000 UTC"}, + {in: "7 oct 70 11:15:26pm", out: "1970-10-07 23:15:26 +0000 UTC"}, {in: "7 oct 1970", out: "1970-10-07 00:00:00 +0000 UTC"}, + {in: "7 oct 1970 11:15:26pm", out: "1970-10-07 23:15:26 +0000 UTC"}, {in: "7 May 1970", out: "1970-05-07 00:00:00 +0000 UTC"}, + {in: "7 May 1970 11:15:26pm", out: "1970-05-07 23:15:26 +0000 UTC"}, {in: "7 Sep 1970", out: "1970-09-07 00:00:00 +0000 UTC"}, + {in: "7 Sep 1970 11:15:26pm", out: "1970-09-07 23:15:26 +0000 UTC"}, {in: "7 June 1970", out: "1970-06-07 00:00:00 +0000 UTC"}, + {in: "7 June 1970 11:15:26pm", out: "1970-06-07 23:15:26 +0000 UTC"}, {in: "7 September 1970", out: "1970-09-07 00:00:00 +0000 UTC"}, + {in: "7 September 1970 11:15:26pm", out: "1970-09-07 23:15:26 +0000 UTC"}, // ANSIC = "Mon Jan _2 15:04:05 2006" {in: "Mon Jan 2 15:04:05 2006", out: "2006-01-02 15:04:05 +0000 UTC"}, {in: "Thu May 8 17:57:51 2009", out: "2009-05-08 17:57:51 +0000 UTC"}, {in: "Thu May 8 17:57:51 2009", out: "2009-05-08 17:57:51 +0000 UTC"}, + {in: "Monday Jan 2 15:04:05 2006", out: "2006-01-02 15:04:05 +0000 UTC"}, + {in: "Thursday May 8 17:57:51 2009", out: "2009-05-08 17:57:51 +0000 UTC"}, + {in: "Thursday May 8 17:57:51 2009", out: "2009-05-08 17:57:51 +0000 UTC"}, // ANSIC_GLIBC = "Mon 02 Jan 2006 03:04:05 PM UTC" - {in: "Mon 02 Jan 2006 03:04:05 PM UTC", out: "2006-01-02 15:04:05 +0000 UTC"}, - {in: "Mon 30 Sep 2018 09:09:09 PM UTC", out: "2018-09-30 21:09:09 +0000 UTC"}, + {in: "Mon 02 Jan 2006 03:04:05 PM UTC", out: "2006-01-02 15:04:05 +0000 UTC", zname: "UTC"}, + {in: "Mon 02 Jan 2006 03:04:05 PM CEST", out: "2006-01-02 15:04:05 +0000 UTC", zname: "CEST"}, + {in: "Mon 30 Sep 2018 09:09:09 PM UTC", out: "2018-09-30 21:09:09 +0000 UTC", zname: "UTC"}, + {in: "Mon 30 Sep 2018 09:09:09 PM CEST", out: "2018-09-30 21:09:09 +0000 UTC", zname: "CEST"}, + {in: "Mon 02 Jan 2006", out: "2006-01-02 00:00:00 +0000 UTC"}, + {in: "Monday 02 Jan 2006 03:04:05 PM UTC", out: "2006-01-02 15:04:05 +0000 UTC", zname: "UTC"}, + {in: "SUNDAY, July 05 2015", out: "2015-07-05 00:00:00 +0000 UTC", zname: "UTC"}, // RubyDate = "Mon Jan 02 15:04:05 -0700 2006" {in: "Mon Jan 02 15:04:05 -0700 2006", out: "2006-01-02 22:04:05 +0000 UTC"}, {in: "Thu May 08 11:57:51 -0700 2009", out: "2009-05-08 18:57:51 +0000 UTC"}, + {in: "Thursday May 08 11:57:51 -0700 2009", out: "2009-05-08 18:57:51 +0000 UTC"}, // UnixDate = "Mon Jan _2 15:04:05 MST 2006" - {in: "Mon Jan 2 15:04:05 MST 2006", out: "2006-01-02 15:04:05 +0000 UTC"}, - {in: "Thu May 8 17:57:51 MST 2009", out: "2009-05-08 17:57:51 +0000 UTC"}, - {in: "Thu May 8 17:57:51 PST 2009", out: "2009-05-08 17:57:51 +0000 UTC"}, - {in: "Thu May 08 17:57:51 PST 2009", out: "2009-05-08 17:57:51 +0000 UTC"}, - {in: "Thu May 08 17:57:51 CEST 2009", out: "2009-05-08 17:57:51 +0000 UTC"}, - {in: "Thu May 08 05:05:07 PST 2009", out: "2009-05-08 05:05:07 +0000 UTC"}, - {in: "Thu May 08 5:5:7 PST 2009", out: "2009-05-08 05:05:07 +0000 UTC"}, + {in: "Mon Jan 2 15:04:05 MST 2006", out: "2006-01-02 15:04:05 +0000 UTC", zname: "MST"}, + {in: "Thu May 8 17:57:51 MST 2009", out: "2009-05-08 17:57:51 +0000 UTC", zname: "MST"}, + {in: "Thu May 8 17:57:51 PST 2009", out: "2009-05-08 17:57:51 +0000 UTC", zname: "PST"}, + {in: "Thu May 08 17:57:51 PST 2009", out: "2009-05-08 17:57:51 +0000 UTC", zname: "PST"}, + {in: "Thu May 08 17:57:51 CEST 2009", out: "2009-05-08 17:57:51 +0000 UTC", zname: "CEST"}, + {in: "Thu May 08 17:57:51 CEST 2009", out: "2009-05-08 15:57:51 +0000 UTC", loc: "Europe/Berlin", zname: "CEST"}, + {in: "Thu May 08 05:05:07 PST 2009", out: "2009-05-08 05:05:07 +0000 UTC", zname: "PST"}, + {in: "Thu May 08 5:5:7 PST 2009", out: "2009-05-08 05:05:07 +0000 UTC", zname: "PST"}, + {in: "Thursday May 08 05:05:07 PST 2009", out: "2009-05-08 05:05:07 +0000 UTC", zname: "PST"}, // Day Month dd time - {in: "Mon Aug 10 15:44:11 UTC+0000 2015", out: "2015-08-10 15:44:11 +0000 UTC"}, - {in: "Mon Aug 10 15:44:11 PST-0700 2015", out: "2015-08-10 22:44:11 +0000 UTC"}, - {in: "Mon Aug 10 15:44:11 CEST+0200 2015", out: "2015-08-10 13:44:11 +0000 UTC"}, - {in: "Mon Aug 1 15:44:11 CEST+0200 2015", out: "2015-08-01 13:44:11 +0000 UTC"}, - {in: "Mon Aug 1 5:44:11 CEST+0200 2015", out: "2015-08-01 03:44:11 +0000 UTC"}, + {in: "Mon Aug 10 15:44:11 UTC+0000 2015", out: "2015-08-10 15:44:11 +0000 UTC", zname: "UTC"}, + {in: "Mon Aug 10 15:44:11 PST-0700 2015", out: "2015-08-10 22:44:11 +0000 UTC", zname: "PST"}, + {in: "Mon Aug 10 15:44:11 CEST+0200 2015", out: "2015-08-10 13:44:11 +0000 UTC", zname: "CEST"}, + {in: "Mon Aug 1 15:44:11 CEST+0200 2015", out: "2015-08-01 13:44:11 +0000 UTC", zname: "CEST"}, + {in: "Mon Aug 1 5:44:11 CEST+0200 2015", out: "2015-08-01 03:44:11 +0000 UTC", zname: "CEST"}, // ?? {in: "Fri Jul 03 2015 18:04:07 GMT+0100 (GMT Daylight Time)", out: "2015-07-03 17:04:07 +0000 UTC"}, + {in: "Fri Jul 03 2015 18:04:07 GMT+01:00 (GMT Daylight Time)", out: "2015-07-03 17:04:07 +0000 UTC"}, {in: "Fri Jul 3 2015 06:04:07 GMT+0100 (GMT Daylight Time)", out: "2015-07-03 05:04:07 +0000 UTC"}, - {in: "Fri Jul 3 2015 06:04:07 PST-0700 (Pacific Daylight Time)", out: "2015-07-03 13:04:07 +0000 UTC"}, + {in: "Fri Jul 03 2015 18:04:07 UTC+0100 (GMT Daylight Time)", out: "2015-07-03 17:04:07 +0000 UTC"}, + {in: "Fri Jul 3 2015 06:04:07 UTC+0100 (GMT Daylight Time)", out: "2015-07-03 05:04:07 +0000 UTC"}, + {in: "Fri Jul 3 2015 06:04:07 PST-0700 (Pacific Daylight Time)", out: "2015-07-03 13:04:07 +0000 UTC", zname: "PST"}, + {in: "Fri Jul 3 2015 06:04:07 PST-07:00 (Pacific Daylight Time)", out: "2015-07-03 13:04:07 +0000 UTC", zname: "PST"}, + {in: "Fri Jul 3 2015 06:04:07 CEST-0700 (Central European Summer Time)", out: "2015-07-03 13:04:07 +0000 UTC", zname: "CEST"}, + {in: "Fri Jul 03 2015 18:04:07 GMT (GMT Daylight Time)", out: "2015-07-03 18:04:07 +0000 UTC", zname: "GMT"}, + {in: "Fri Jul 3 2015 06:04:07 +0100 (GMT Daylight Time)", out: "2015-07-03 05:04:07 +0000 UTC"}, + {in: "Fri Jul 03 2015 18:04:07 UTC (GMT Daylight Time)", out: "2015-07-03 18:04:07 +0000 UTC"}, + {in: "Fri Jul 3 2015 06:04:07 +01:00 (GMT Daylight Time)", out: "2015-07-03 05:04:07 +0000 UTC"}, + {in: "Fri Jul 3 2015 06:04:07 PST (Pacific Daylight Time)", out: "2015-07-03 06:04:07 +0000 UTC", zname: "PST"}, + {in: "Fri Jul 3 2015 06:04:07 -07:00 (Pacific Daylight Time)", out: "2015-07-03 13:04:07 +0000 UTC"}, + {in: "Fri Jul 3 2015", out: "2015-07-03 00:00:00 +0000 UTC"}, + {in: "Fri Jul 3 2015 11:15:26pm", out: "2015-07-03 23:15:26 +0000 UTC"}, // Month dd, yyyy at time - {in: "September 17, 2012 at 5:00pm UTC-05", out: "2012-09-17 17:00:00 +0000 UTC"}, - {in: "September 17, 2012 at 10:09am PST-08", out: "2012-09-17 18:09:00 +0000 UTC"}, + {in: "January 17, 2012 at 18:17:16", out: "2012-01-17 18:17:16 +0000 UTC"}, + {in: "February 17, 2012 at 18:17:16", out: "2012-02-17 18:17:16 +0000 UTC"}, + {in: "march 17, 2012 at 18:17:16", out: "2012-03-17 18:17:16 +0000 UTC"}, + {in: "APRIL 17, 2012 at 18:17:16", out: "2012-04-17 18:17:16 +0000 UTC"}, + {in: "May 17, 2012 at 18:17:16", out: "2012-05-17 18:17:16 +0000 UTC"}, + {in: "June 17, 2012 at 18:17:16", out: "2012-06-17 18:17:16 +0000 UTC"}, + {in: "July 17, 2012 at 18:17:16", out: "2012-07-17 18:17:16 +0000 UTC"}, + {in: "august 17, 2012 at 18:17:16", out: "2012-08-17 18:17:16 +0000 UTC"}, + {in: "September 17, 2012 at 18:17:16", out: "2012-09-17 18:17:16 +0000 UTC"}, + {in: "OCTober 17, 2012 at 18:17:16", out: "2012-10-17 18:17:16 +0000 UTC"}, + {in: "noVEMBER 17, 2012 at 18:17:16", out: "2012-11-17 18:17:16 +0000 UTC"}, + {in: "December 17, 2012 at 18:17:16", out: "2012-12-17 18:17:16 +0000 UTC"}, + {in: "September 17 2012 at 5:00pm UTC-05", out: "2012-09-17 22:00:00 +0000 UTC", zname: ""}, // empty zone name, special case of UTC+NNNN + {in: "September 17, 2012 at 5:00pm UTC-05", out: "2012-09-17 22:00:00 +0000 UTC", zname: ""}, // empty zone name, special case of UTC+NNNN + {in: "September 17, 2012 at 10:09am PST-08", out: "2012-09-17 18:09:00 +0000 UTC", zname: "PST"}, + {in: "September 17, 2012 at 10:09am CEST+02", out: "2012-09-17 08:09:00 +0000 UTC", zname: "CEST"}, {in: "September 17, 2012, 10:10:09", out: "2012-09-17 10:10:09 +0000 UTC"}, - {in: "May 17, 2012 at 10:09am PST-08", out: "2012-05-17 18:09:00 +0000 UTC"}, - {in: "May 17, 2012 AT 10:09am PST-08", out: "2012-05-17 18:09:00 +0000 UTC"}, + {in: "May 17 2012 at 10:09am PST-08", out: "2012-05-17 18:09:00 +0000 UTC", zname: "PST"}, + {in: "May 17, 2012 at 10:09am PST-08", out: "2012-05-17 18:09:00 +0000 UTC", zname: "PST"}, + {in: "May 17, 2012 AT 10:09am PST-08", out: "2012-05-17 18:09:00 +0000 UTC", zname: "PST"}, + {in: "May 17, 2012 AT 10:09am CEST+02", out: "2012-05-17 08:09:00 +0000 UTC", zname: "CEST"}, // Month dd, yyyy time - {in: "September 17, 2012 5:00pm UTC-05", out: "2012-09-17 17:00:00 +0000 UTC"}, - {in: "September 17, 2012 10:09am PST-08", out: "2012-09-17 18:09:00 +0000 UTC"}, + {in: "September 17, 2012 5:00pm UTC-05", out: "2012-09-17 22:00:00 +0000 UTC", zname: ""}, + {in: "September 17, 2012 10:09am PST-08", out: "2012-09-17 18:09:00 +0000 UTC", zname: "PST"}, + {in: "September 17, 2012 10:09am CEST+02", out: "2012-09-17 08:09:00 +0000 UTC", zname: "CEST"}, {in: "September 17, 2012 09:01:00", out: "2012-09-17 09:01:00 +0000 UTC"}, // Month dd yyyy time - {in: "September 17 2012 5:00pm UTC-05", out: "2012-09-17 17:00:00 +0000 UTC"}, - {in: "September 17 2012 5:00pm UTC-0500", out: "2012-09-17 17:00:00 +0000 UTC"}, - {in: "September 17 2012 10:09am PST-08", out: "2012-09-17 18:09:00 +0000 UTC"}, - {in: "September 17 2012 5:00PM UTC-05", out: "2012-09-17 17:00:00 +0000 UTC"}, - {in: "September 17 2012 10:09AM PST-08", out: "2012-09-17 18:09:00 +0000 UTC"}, + {in: "September 17 2012 5:00pm UTC-05", out: "2012-09-17 22:00:00 +0000 UTC", zname: ""}, + {in: "September 17 2012 5:00pm UTC-0500", out: "2012-09-17 22:00:00 +0000 UTC", zname: ""}, + {in: "September 17 2012 10:09am PST-08", out: "2012-09-17 18:09:00 +0000 UTC", zname: "PST"}, + {in: "September 17 2012 10:09am CEST+02", out: "2012-09-17 08:09:00 +0000 UTC", zname: "CEST"}, + {in: "September 17 2012 5:00PM UTC-05", out: "2012-09-17 22:00:00 +0000 UTC", zname: ""}, + {in: "September 17 2012 10:09AM PST-08", out: "2012-09-17 18:09:00 +0000 UTC", zname: "PST"}, + {in: "September 17 2012 10:09AM CEST+02", out: "2012-09-17 08:09:00 +0000 UTC", zname: "CEST"}, {in: "September 17 2012 09:01:00", out: "2012-09-17 09:01:00 +0000 UTC"}, {in: "May 17, 2012 10:10:09", out: "2012-05-17 10:10:09 +0000 UTC"}, + {in: "July 30 2022 08:33:53 AM PST", out: "2022-07-30 08:33:53 +0000 UTC", zname: "PST"}, + {in: "July 30 2022 08:33:53 AM CEST", out: "2022-07-30 08:33:53 +0000 UTC", zname: "CEST"}, + {in: "July 30 2022 08:33:53 PM PST", out: "2022-07-30 20:33:53 +0000 UTC", zname: "PST"}, + {in: "July 30 2022 08:33:53 PM CEST", out: "2022-07-30 20:33:53 +0000 UTC", zname: "CEST"}, // Month dd, yyyy {in: "September 17, 2012", out: "2012-09-17 00:00:00 +0000 UTC"}, {in: "May 7, 2012", out: "2012-05-07 00:00:00 +0000 UTC"}, @@ -107,14 +182,42 @@ var testInputs = []dateTest{ {in: "June 2nd 2012", out: "2012-06-02 00:00:00 +0000 UTC"}, {in: "June 22nd, 2012", out: "2012-06-22 00:00:00 +0000 UTC"}, {in: "June 22nd 2012", out: "2012-06-22 00:00:00 +0000 UTC"}, + {in: "September 17th, 2012 11:15:26pm", out: "2012-09-17 23:15:26 +0000 UTC"}, + {in: "September 17th 2012 11:15:26pm", out: "2012-09-17 23:15:26 +0000 UTC"}, + {in: "September 7th, 2012 11:15:26pm", out: "2012-09-07 23:15:26 +0000 UTC"}, + {in: "September 7th 2012 11:15:26pm", out: "2012-09-07 23:15:26 +0000 UTC"}, + {in: "September 7tH 2012 11:15:26pm", out: "2012-09-07 23:15:26 +0000 UTC"}, + {in: "May 1st 2012 11:15:26pm", out: "2012-05-01 23:15:26 +0000 UTC"}, + {in: "May 1st, 2012 11:15:26pm", out: "2012-05-01 23:15:26 +0000 UTC"}, + {in: "May 21st 2012 11:15:26pm", out: "2012-05-21 23:15:26 +0000 UTC"}, + {in: "May 21st, 2012 11:15:26pm", out: "2012-05-21 23:15:26 +0000 UTC"}, + {in: "May 23rd 2012 11:15:26pm", out: "2012-05-23 23:15:26 +0000 UTC"}, + {in: "May 23rd, 2012 11:15:26pm", out: "2012-05-23 23:15:26 +0000 UTC"}, + {in: "June 2nd, 2012 11:15:26pm", out: "2012-06-02 23:15:26 +0000 UTC"}, + {in: "June 2nd 2012 11:15:26pm", out: "2012-06-02 23:15:26 +0000 UTC"}, + {in: "June 22nd, 2012 11:15:26pm", out: "2012-06-22 23:15:26 +0000 UTC"}, + {in: "June 22nd 2012 11:15:26pm", out: "2012-06-22 23:15:26 +0000 UTC"}, + // Incorporate PR https://github.com/araddon/dateparse/pull/128 to fix https://github.com/araddon/dateparse/issues/127 + // dd[th,nd,st,rd] Month yyyy + {in: "1st September 2012", out: "2012-09-01 00:00:00 +0000 UTC"}, + {in: "2nd September 2012", out: "2012-09-02 00:00:00 +0000 UTC"}, + {in: "3rd September 2012", out: "2012-09-03 00:00:00 +0000 UTC"}, + {in: "4th Sep 2012", out: "2012-09-04 00:00:00 +0000 UTC"}, + {in: "2nd January 2018", out: "2018-01-02 00:00:00 +0000 UTC"}, + {in: "3rd Feb 2018 13:58:24", out: "2018-02-03 13:58:24 +0000 UTC"}, + {in: "1st February 2018 13:58:24", out: "2018-02-01 13:58:24 +0000 UTC"}, // RFC1123 = "Mon, 02 Jan 2006 15:04:05 MST" - {in: "Fri, 03 Jul 2015 08:08:08 MST", out: "2015-07-03 08:08:08 +0000 UTC"}, - //{in: "Fri, 03 Jul 2015 08:08:08 CET", out: "2015-07-03 08:08:08 +0000 UTC"}, - {in: "Fri, 03 Jul 2015 08:08:08 PST", out: "2015-07-03 16:08:08 +0000 UTC", loc: "America/Los_Angeles"}, - {in: "Fri, 03 Jul 2015 08:08:08 PST", out: "2015-07-03 08:08:08 +0000 UTC"}, - {in: "Fri, 3 Jul 2015 08:08:08 MST", out: "2015-07-03 08:08:08 +0000 UTC"}, - {in: "Fri, 03 Jul 2015 8:08:08 MST", out: "2015-07-03 08:08:08 +0000 UTC"}, - {in: "Fri, 03 Jul 2015 8:8:8 MST", out: "2015-07-03 08:08:08 +0000 UTC"}, + {in: "Fri, 03 Jul 2015 08:08:08 MST", out: "2015-07-03 08:08:08 +0000 UTC", zname: "MST"}, + {in: "Fri, 03 Jul 2015 08:08:08 CET", out: "2015-07-03 08:08:08 +0000 UTC", zname: "CET"}, + {in: "Fri, 03 Jul 2015 08:08:08 PST", out: "2015-07-03 16:08:08 +0000 UTC", loc: "America/Los_Angeles", zname: "PDT"}, + {in: "Fri, 03 Jul 2015 08:08:08 PST", out: "2015-07-03 08:08:08 +0000 UTC", zname: "PST"}, + {in: "Fri, 03 Jul 2015 08:08:08 CEST", out: "2015-07-03 08:08:08 +0000 UTC", zname: "CEST"}, + {in: "Fri, 3 Jul 2015 08:08:08 MST", out: "2015-07-03 08:08:08 +0000 UTC", zname: "MST"}, + {in: "Fri, 3 Jul 2015 08:08:08 CEST", out: "2015-07-03 08:08:08 +0000 UTC", zname: "CEST"}, + {in: "Fri, 03 Jul 2015 8:08:08 MST", out: "2015-07-03 08:08:08 +0000 UTC", zname: "MST"}, + {in: "Fri, 03 Jul 2015 8:08:08 CEST", out: "2015-07-03 08:08:08 +0000 UTC", zname: "CEST"}, + {in: "Fri, 03 Jul 2015 8:8:8 MST", out: "2015-07-03 08:08:08 +0000 UTC", zname: "MST"}, + {in: "Fri, 03 Jul 2015 8:8:8 CEST", out: "2015-07-03 08:08:08 +0000 UTC", zname: "CEST"}, // ? {in: "Thu, 03 Jul 2017 08:08:04 +0100", out: "2017-07-03 07:08:04 +0000 UTC"}, {in: "Thu, 03 Jul 2017 08:08:04 -0100", out: "2017-07-03 09:08:04 +0000 UTC"}, @@ -122,22 +225,33 @@ var testInputs = []dateTest{ {in: "Thu, 03 Jul 2017 8:08:04 +0100", out: "2017-07-03 07:08:04 +0000 UTC"}, {in: "Thu, 03 Jul 2017 8:8:4 +0100", out: "2017-07-03 07:08:04 +0000 UTC"}, // - {in: "Tue, 11 Jul 2017 04:08:03 +0200 (CEST)", out: "2017-07-11 02:08:03 +0000 UTC"}, - {in: "Tue, 5 Jul 2017 04:08:03 -0700 (CEST)", out: "2017-07-05 11:08:03 +0000 UTC"}, - {in: "Tue, 11 Jul 2017 04:08:03 +0200 (CEST)", out: "2017-07-11 02:08:03 +0000 UTC", loc: "Europe/Berlin"}, + {in: "Tue, 11 Jul 2017 04:08:03 +0200 (CEST)", out: "2017-07-11 02:08:03 +0000 UTC", zname: "CEST"}, + {in: "Tue, 5 Jul 2017 04:08:03 -0700 (MST)", out: "2017-07-05 11:08:03 +0000 UTC", zname: "MST"}, + {in: "Tue, 11 Jul 2017 04:08:03 +0200 (CEST)", out: "2017-07-11 02:08:03 +0000 UTC", loc: "Europe/Berlin", zname: "CEST"}, + {in: "Tue, 11 Jul 2017 04:08:03 (CEST)", out: "2017-07-11 04:08:03 +0000 UTC", zname: "CEST"}, + {in: "Tue, 5 Jul 2017 04:08:03 (MST)", out: "2017-07-05 04:08:03 +0000 UTC", zname: "MST"}, // day, dd-Mon-yy hh:mm:zz TZ - {in: "Fri, 03-Jul-15 08:08:08 MST", out: "2015-07-03 08:08:08 +0000 UTC"}, - {in: "Fri, 03-Jul-15 08:08:08 PST", out: "2015-07-03 16:08:08 +0000 UTC", loc: "America/Los_Angeles"}, - {in: "Fri, 03-Jul 2015 08:08:08 PST", out: "2015-07-03 08:08:08 +0000 UTC"}, - {in: "Fri, 3-Jul-15 08:08:08 MST", out: "2015-07-03 08:08:08 +0000 UTC"}, - {in: "Fri, 03-Jul-15 8:08:08 MST", out: "2015-07-03 08:08:08 +0000 UTC"}, - {in: "Fri, 03-Jul-15 8:8:8 MST", out: "2015-07-03 08:08:08 +0000 UTC"}, + {in: "Fri, 03-Jul-15 08:08:08 MST", out: "2015-07-03 08:08:08 +0000 UTC", zname: "MST"}, + {in: "Fri, 03-Jul-15 08:08:08 CEST", out: "2015-07-03 08:08:08 +0000 UTC", zname: "CEST"}, + {in: "Fri, 03-Jul-15 08:08:08 PST", out: "2015-07-03 16:08:08 +0000 UTC", loc: "America/Los_Angeles", zname: "PDT"}, + {in: "Fri, 03-Jul-2015", out: "2015-07-03 00:00:00 +0000 UTC"}, + {in: "Fri, 03-Jul-2015 08:08:08 PST", out: "2015-07-03 08:08:08 +0000 UTC", zname: "PST"}, + {in: "Fri, 03-Jul-2015 08:08:08 CEST", out: "2015-07-03 08:08:08 +0000 UTC", zname: "CEST"}, + {in: "Fri, 3-Jul-15 08:08:08 MST", out: "2015-07-03 08:08:08 +0000 UTC", zname: "MST"}, + {in: "Fri, 3-Jul-15 08:08:08 CEST", out: "2015-07-03 08:08:08 +0000 UTC", zname: "CEST"}, + {in: "Fri, 03-Jul-15 8:08:08 MST", out: "2015-07-03 08:08:08 +0000 UTC", zname: "MST"}, + {in: "Fri, 03-Jul-15 8:08:08 CEST", out: "2015-07-03 08:08:08 +0000 UTC", zname: "CEST"}, + {in: "Fri, 03-Jul-15 8:8:8 MST", out: "2015-07-03 08:08:08 +0000 UTC", zname: "MST"}, + {in: "Fri, 03-Jul-15 8:8:8 CEST", out: "2015-07-03 08:08:08 +0000 UTC", zname: "CEST"}, // day, dd-Mon-yy hh:mm:zz TZ (text) https://github.com/araddon/dateparse/issues/116 {in: "Sun, 3 Jan 2021 00:12:23 +0800 (GMT+08:00)", out: "2021-01-02 16:12:23 +0000 UTC"}, + {in: "Sun, 3 Jan 2021 00:12:23 +0800 (UTC+08:00)", out: "2021-01-02 16:12:23 +0000 UTC"}, // RFC850 = "Monday, 02-Jan-06 15:04:05 MST" - {in: "Wednesday, 07-May-09 08:00:43 MST", out: "2009-05-07 08:00:43 +0000 UTC"}, - {in: "Wednesday, 28-Feb-18 09:01:00 MST", out: "2018-02-28 09:01:00 +0000 UTC"}, - {in: "Wednesday, 28-Feb-18 09:01:00 MST", out: "2018-02-28 16:01:00 +0000 UTC", loc: "America/Denver"}, + {in: "Wednesday, 07-May-09 08:00:43 MST", out: "2009-05-07 08:00:43 +0000 UTC", zname: "MST"}, + {in: "Wednesday, 07-May-09 08:00:43 CEST", out: "2009-05-07 08:00:43 +0000 UTC", zname: "CEST"}, + {in: "Wednesday, 28-Feb-18 09:01:00 MST", out: "2018-02-28 09:01:00 +0000 UTC", zname: "MST"}, + {in: "Wednesday, 28-Feb-18 09:01:00 MST", out: "2018-02-28 16:01:00 +0000 UTC", loc: "America/Denver", zname: "MST"}, + {in: "Wednesday, 28-Feb-18 09:01:00 CEST", out: "2018-02-28 09:01:00 +0000 UTC", zname: "CEST"}, // with offset then with variations on non-zero filled stuff {in: "Monday, 02 Jan 2006 15:04:05 +0100", out: "2006-01-02 14:04:05 +0000 UTC"}, {in: "Wednesday, 28 Feb 2018 09:01:00 -0300", out: "2018-02-28 12:01:00 +0000 UTC"}, @@ -155,7 +269,8 @@ var testInputs = []dateTest{ {in: "7 Feb 2004 9:7:8", out: "2004-02-07 09:07:08 +0000 UTC"}, {in: "07 Feb 2004 09:07:08.123", out: "2004-02-07 09:07:08.123 +0000 UTC"}, // dd-mon-yyyy 12 Feb 2006, 19:17:08 GMT - {in: "07 Feb 2004, 09:07:07 GMT", out: "2004-02-07 09:07:07 +0000 UTC"}, + {in: "07 Feb 2004, 09:07:07 GMT", out: "2004-02-07 09:07:07 +0000 UTC", zname: "GMT"}, + {in: "07 Feb 2004, 09:07:07 CEST", out: "2004-02-07 09:07:07 +0000 UTC", zname: "CEST"}, // dd-mon-yyyy 12 Feb 2006, 19:17:08 +0100 {in: "07 Feb 2004, 09:07:07 +0100", out: "2004-02-07 08:07:07 +0000 UTC"}, // dd-mon-yyyy 12-Feb-2006 19:17:08 @@ -164,12 +279,26 @@ var testInputs = []dateTest{ {in: "07-Feb-04 09:07:07 +0100", out: "2004-02-07 08:07:07 +0000 UTC"}, // yyyy-mon-dd 2013-Feb-03 {in: "2013-Feb-03", out: "2013-02-03 00:00:00 +0000 UTC"}, + {in: "2013-Feb-03 09:07:08pm", out: "2013-02-03 21:07:08 +0000 UTC"}, + {in: "2013-February-03", out: "2013-02-03 00:00:00 +0000 UTC"}, + {in: "2013-February-03 09:07:08.123", out: "2013-02-03 09:07:08.123 +0000 UTC"}, // 03 February 2013 + {in: "13 Feb 2013", out: "2013-02-13 00:00:00 +0000 UTC"}, {in: "03 February 2013", out: "2013-02-03 00:00:00 +0000 UTC"}, + {in: "03 February 2013 09:07:08pm", out: "2013-02-03 21:07:08 +0000 UTC"}, {in: "3 February 2013", out: "2013-02-03 00:00:00 +0000 UTC"}, - // Chinese 2014年04月18日 + {in: "3 February 2013 09:07:08pm", out: "2013-02-03 21:07:08 +0000 UTC"}, + // Chinese 2014年04月18日 - https://github.com/araddon/dateparse/pull/132 {in: "2014年04月08日", out: "2014-04-08 00:00:00 +0000 UTC"}, + {in: "2014年4月8日", out: "2014-04-08 00:00:00 +0000 UTC"}, {in: "2014年04月08日 19:17:22", out: "2014-04-08 19:17:22 +0000 UTC"}, + {in: "2014年04月08日 19:17:22 MDT", out: "2014-04-08 19:17:22 +0000 UTC", zname: "MDT"}, + {in: "2014年04月08日 19:17:22 -0700", out: "2014-04-09 02:17:22 +0000 UTC"}, + {in: "2014年4月8日 19:17:22", out: "2014-04-08 19:17:22 +0000 UTC"}, + {in: "2014年4月8日 19:17:22 MDT", out: "2014-04-08 19:17:22 +0000 UTC", zname: "MDT"}, + {in: "2014年4月8日 19:17:22 MDT-0700", out: "2014-04-09 02:17:22 +0000 UTC", zname: "MDT"}, + {in: "2014年4月8日 10:17pm", out: "2014-04-08 22:17:00 +0000 UTC"}, + // TODO: support Chinese AM (上午) and PM (下午) indicators // mm/dd/yyyy {in: "03/31/2014", out: "2014-03-31 00:00:00 +0000 UTC"}, {in: "3/31/2014", out: "2014-03-31 00:00:00 +0000 UTC"}, @@ -197,15 +326,44 @@ var testInputs = []dateTest{ {in: "04:02:2014 04:08:09.123", out: "2014-04-02 04:08:09.123 +0000 UTC"}, {in: "04:02:2014 04:08:09.12312", out: "2014-04-02 04:08:09.12312 +0000 UTC"}, {in: "04:02:2014 04:08:09.123123", out: "2014-04-02 04:08:09.123123 +0000 UTC"}, + {in: "04:01:2014 04:08:09", out: "2014-01-04 04:08:09 +0000 UTC", preferDayFirst: true}, // mm/dd/yy hh:mm:ss AM + {in: "04/02/2014 04:08:09am", out: "2014-04-02 04:08:09 +0000 UTC"}, {in: "04/02/2014 04:08:09 AM", out: "2014-04-02 04:08:09 +0000 UTC"}, + {in: "04/02/2014 04:08:09AM PST", out: "2014-04-02 04:08:09 +0000 UTC", zname: "PST"}, + {in: "04/02/2014 04:08:09 AM PST", out: "2014-04-02 04:08:09 +0000 UTC", zname: "PST"}, + {in: "04/02/2014 04:08:09 AM (PST)", out: "2014-04-02 04:08:09 +0000 UTC", zname: "PST"}, + {in: "04/02/2014 04:08:09AM CEST", out: "2014-04-02 04:08:09 +0000 UTC", zname: "CEST"}, + {in: "04/02/2014 04:08:09 AM CEST", out: "2014-04-02 04:08:09 +0000 UTC", zname: "CEST"}, + {in: "04/02/2014 04:08:09 AM (CEST)", out: "2014-04-02 04:08:09 +0000 UTC", zname: "CEST"}, + {in: "04/02/2014 04:08:09pm", out: "2014-04-02 16:08:09 +0000 UTC"}, {in: "04/02/2014 04:08:09 PM", out: "2014-04-02 16:08:09 +0000 UTC"}, + {in: "04/02/2014 04:08:09PM PST", out: "2014-04-02 16:08:09 +0000 UTC", zname: "PST"}, + {in: "04/02/2014 04:08:09 PM PST", out: "2014-04-02 16:08:09 +0000 UTC", zname: "PST"}, + {in: "04/02/2014 04:08:09 PM (PST)", out: "2014-04-02 16:08:09 +0000 UTC", zname: "PST"}, + {in: "04/02/2014 04:08:09pm CEST", out: "2014-04-02 16:08:09 +0000 UTC", zname: "CEST"}, + {in: "04/02/2014 04:08:09 PM CEST", out: "2014-04-02 16:08:09 +0000 UTC", zname: "CEST"}, + {in: "04/02/2014 04:08:09 PM (CEST)", out: "2014-04-02 16:08:09 +0000 UTC", zname: "CEST"}, + {in: "04/02/2014 04:08am", out: "2014-04-02 04:08:00 +0000 UTC"}, {in: "04/02/2014 04:08 AM", out: "2014-04-02 04:08:00 +0000 UTC"}, + {in: "04/02/2014 04:08pm", out: "2014-04-02 16:08:00 +0000 UTC"}, {in: "04/02/2014 04:08 PM", out: "2014-04-02 16:08:00 +0000 UTC"}, + {in: "04/02/2014 4:8AM", out: "2014-04-02 04:08:00 +0000 UTC"}, {in: "04/02/2014 4:8 AM", out: "2014-04-02 04:08:00 +0000 UTC"}, + {in: "04/02/2014 4:8pm", out: "2014-04-02 16:08:00 +0000 UTC"}, {in: "04/02/2014 4:8 PM", out: "2014-04-02 16:08:00 +0000 UTC"}, + {in: "04/02/2014 04:08:09.123am", out: "2014-04-02 04:08:09.123 +0000 UTC"}, {in: "04/02/2014 04:08:09.123 AM", out: "2014-04-02 04:08:09.123 +0000 UTC"}, + {in: "04/02/2014 04:08:09.123PM", out: "2014-04-02 16:08:09.123 +0000 UTC"}, {in: "04/02/2014 04:08:09.123 PM", out: "2014-04-02 16:08:09.123 +0000 UTC"}, + {in: "04/02/2014 04:08:09pm-0700", out: "2014-04-02 23:08:09 +0000 UTC"}, + {in: "04/02/2014 04:08:09PM-0700 PST", out: "2014-04-02 23:08:09 +0000 UTC", zname: "PST"}, + {in: "04/02/2014 04:08:09pm-0700 PST (Pacific Standard Time)", out: "2014-04-02 23:08:09 +0000 UTC", zname: "PST"}, + {in: "04/02/2014 04:08:09pm-0700 (Pacific Standard Time)", out: "2014-04-02 23:08:09 +0000 UTC"}, + {in: "04/02/2014 04:08:09am+02:00", out: "2014-04-02 02:08:09 +0000 UTC"}, + {in: "04/02/2014 04:08:09AM+02:00 CET", out: "2014-04-02 02:08:09 +0000 UTC", zname: "CET"}, + {in: "04/02/2014 04:08:09am+02:00 CET (Central European Time)", out: "2014-04-02 02:08:09 +0000 UTC", zname: "CET"}, + {in: "04/02/2014 04:08:09am+02:00 (Central European Time)", out: "2014-04-02 02:08:09 +0000 UTC"}, // yyyy/mm/dd {in: "2014/04/02", out: "2014-04-02 00:00:00 +0000 UTC"}, {in: "2014/03/31", out: "2014-03-31 00:00:00 +0000 UTC"}, @@ -220,18 +378,44 @@ var testInputs = []dateTest{ {in: "2014/4/2 04:08:09", out: "2014-04-02 04:08:09 +0000 UTC"}, {in: "2014/04/02 04:08:09.123", out: "2014-04-02 04:08:09.123 +0000 UTC"}, {in: "2014/04/02 04:08:09.123123", out: "2014-04-02 04:08:09.123123 +0000 UTC"}, + {in: "2014/04/02 04:08:09am", out: "2014-04-02 04:08:09 +0000 UTC"}, {in: "2014/04/02 04:08:09 AM", out: "2014-04-02 04:08:09 +0000 UTC"}, + {in: "2014/03/31 04:08:09am", out: "2014-03-31 04:08:09 +0000 UTC"}, {in: "2014/03/31 04:08:09 AM", out: "2014-03-31 04:08:09 +0000 UTC"}, + {in: "2014/4/2 04:08:09AM", out: "2014-04-02 04:08:09 +0000 UTC"}, {in: "2014/4/2 04:08:09 AM", out: "2014-04-02 04:08:09 +0000 UTC"}, + {in: "2014/04/02 04:08:09.123am", out: "2014-04-02 04:08:09.123 +0000 UTC"}, {in: "2014/04/02 04:08:09.123 AM", out: "2014-04-02 04:08:09.123 +0000 UTC"}, + {in: "2014/04/02 04:08:09.123am PST", out: "2014-04-02 04:08:09.123 +0000 UTC", zname: "PST"}, + {in: "2014/04/02 04:08:09.123 AM PST", out: "2014-04-02 04:08:09.123 +0000 UTC", zname: "PST"}, + {in: "2014/04/02 04:08:09.123AM CEST", out: "2014-04-02 04:08:09.123 +0000 UTC", zname: "CEST"}, + {in: "2014/04/02 04:08:09.123 AM CEST", out: "2014-04-02 04:08:09.123 +0000 UTC", zname: "CEST"}, + {in: "2014/04/02 04:08:09.123pm", out: "2014-04-02 16:08:09.123 +0000 UTC"}, {in: "2014/04/02 04:08:09.123 PM", out: "2014-04-02 16:08:09.123 +0000 UTC"}, + {in: "2014/04/02 04:08:09.123PM PST", out: "2014-04-02 16:08:09.123 +0000 UTC", zname: "PST"}, + {in: "2014/04/02 04:08:09.123 PM PST", out: "2014-04-02 16:08:09.123 +0000 UTC", zname: "PST"}, + {in: "2014/04/02 04:08:09.123PM CEST", out: "2014-04-02 16:08:09.123 +0000 UTC", zname: "CEST"}, + {in: "2014/04/02 04:08:09.123 PM CEST", out: "2014-04-02 16:08:09.123 +0000 UTC", zname: "CEST"}, // dd/mon/yyyy:hh:mm:ss tz nginx-log? https://github.com/araddon/dateparse/issues/118 // 112.195.209.90 - - [20/Feb/2018:12:12:14 +0800] "GET / HTTP/1.1" 200 190 "-" "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Mobile Safari/537.36" "-" {in: "06/May/2008:08:11:17 -0700", out: "2008-05-06 15:11:17 +0000 UTC"}, {in: "30/May/2008:08:11:17 -0700", out: "2008-05-30 15:11:17 +0000 UTC"}, - // dd/mon/yyy hh:mm:ss tz - {in: "06/May/2008:08:11:17 -0700", out: "2008-05-06 15:11:17 +0000 UTC"}, - {in: "30/May/2008:08:11:17 -0700", out: "2008-05-30 15:11:17 +0000 UTC"}, + {in: "30/October/2008:08:11:17 -0700", out: "2008-10-30 15:11:17 +0000 UTC"}, + // dd/mon/yyyy hh:mm:ss tz + {in: "06/May/2008", out: "2008-05-06 00:00:00 +0000 UTC"}, + {in: "06/May/2008 08:11:17 -0700", out: "2008-05-06 15:11:17 +0000 UTC"}, + {in: "30/May/2008 08:11:17 -0700", out: "2008-05-30 15:11:17 +0000 UTC"}, + {in: "30/September/2008 08:11:17 -0700", out: "2008-09-30 15:11:17 +0000 UTC"}, + // mon/dd/yyyy + {in: "Oct/ 7/1970", out: "1970-10-07 00:00:00 +0000 UTC"}, + {in: "Oct/31/1970", out: "1970-10-31 00:00:00 +0000 UTC"}, + {in: "Oct/03/1970", out: "1970-10-03 00:00:00 +0000 UTC"}, + {in: "Oct/03/1970 22:33:44", out: "1970-10-03 22:33:44 +0000 UTC"}, + {in: "February/ 7/1970", out: "1970-02-07 00:00:00 +0000 UTC"}, + {in: "February/27/1970", out: "1970-02-27 00:00:00 +0000 UTC"}, + {in: "February/03/1970", out: "1970-02-03 00:00:00 +0000 UTC"}, + {in: "February/03/1970 22:33:44.555", out: "1970-02-03 22:33:44.555 +0000 UTC"}, + {in: "February/03/1970 11:33:44.555 PM PST", out: "1970-02-03 23:33:44.555 +0000 UTC", zname: "PST"}, // yyyy-mm-dd {in: "2014-04-02", out: "2014-04-02 00:00:00 +0000 UTC"}, {in: "2014-03-31", out: "2014-03-31 00:00:00 +0000 UTC"}, @@ -239,10 +423,20 @@ var testInputs = []dateTest{ // yyyy-mm-dd-07:00 {in: "2020-07-20+08:00", out: "2020-07-19 16:00:00 +0000 UTC"}, {in: "2020-07-20+0800", out: "2020-07-19 16:00:00 +0000 UTC"}, - // dd-mmm-yy + // dd-mmm-yy (alpha month) {in: "28-Feb-02", out: "2002-02-28 00:00:00 +0000 UTC"}, {in: "15-Jan-18", out: "2018-01-15 00:00:00 +0000 UTC"}, {in: "15-Jan-2017", out: "2017-01-15 00:00:00 +0000 UTC"}, + {in: "28-Feb-02 15:16:17", out: "2002-02-28 15:16:17 +0000 UTC"}, + {in: "15-Jan-18 15:16:17", out: "2018-01-15 15:16:17 +0000 UTC"}, + {in: "15-September-2017 15:16:17", out: "2017-09-15 15:16:17 +0000 UTC"}, + // dd-mm-yy (digit month - potentially ambiguous) - https://github.com/araddon/dateparse/issues/139 + {in: "28-02-02", out: "2002-02-28 00:00:00 +0000 UTC"}, + {in: "15-01-18", out: "2018-01-15 00:00:00 +0000 UTC"}, + {in: "15-01-2017", out: "2017-01-15 00:00:00 +0000 UTC"}, + {in: "28-02-02 15:16:17", out: "2002-02-28 15:16:17 +0000 UTC"}, + {in: "15-01-18 15:16:17", out: "2018-01-15 15:16:17 +0000 UTC"}, + {in: "15-01-2017 15:16:17", out: "2017-01-15 15:16:17 +0000 UTC"}, // yyyy-mm {in: "2014-04", out: "2014-04-01 00:00:00 +0000 UTC"}, // yyyy-mm-dd hh:mm:ss AM @@ -257,13 +451,105 @@ var testInputs = []dateTest{ {in: "2014-04-02 04:08:09.123123", out: "2014-04-02 04:08:09.123123 +0000 UTC"}, {in: "2014-04-02 04:08:09.12312312", out: "2014-04-02 04:08:09.12312312 +0000 UTC"}, {in: "2014-04-02 04:08:09 AM", out: "2014-04-02 04:08:09 +0000 UTC"}, + {in: "2014-04-02 04:08:09 AM PST", out: "2014-04-02 04:08:09 +0000 UTC", zname: "PST"}, + {in: "2014-04-02 04:08:09 AM CEST", out: "2014-04-02 04:08:09 +0000 UTC", zname: "CEST"}, {in: "2014-03-31 04:08:09 AM", out: "2014-03-31 04:08:09 +0000 UTC"}, + {in: "2014-03-31 04:08:09 AM PST", out: "2014-03-31 04:08:09 +0000 UTC", zname: "PST"}, + {in: "2014-03-31 04:08:09 AM CEST", out: "2014-03-31 04:08:09 +0000 UTC", zname: "CEST"}, {in: "2014-04-26 05:24:37 PM", out: "2014-04-26 17:24:37 +0000 UTC"}, + {in: "2014-04-26 05:24:37 PM PST", out: "2014-04-26 17:24:37 +0000 UTC", zname: "PST"}, + {in: "2014-04-26 05:24:37 PM CEST", out: "2014-04-26 17:24:37 +0000 UTC", zname: "CEST"}, {in: "2014-4-2 04:08:09 AM", out: "2014-04-02 04:08:09 +0000 UTC"}, + {in: "2014-4-2 04:08:09 AM PST", out: "2014-04-02 04:08:09 +0000 UTC", zname: "PST"}, + {in: "2014-4-2 04:08:09 AM CEST", out: "2014-04-02 04:08:09 +0000 UTC", zname: "CEST"}, {in: "2014-04-02 04:08:09.123 AM", out: "2014-04-02 04:08:09.123 +0000 UTC"}, + {in: "2014-04-02 04:08:09.123 AM PST", out: "2014-04-02 04:08:09.123 +0000 UTC", zname: "PST"}, + {in: "2014-04-02 04:08:09.123 AM CEST", out: "2014-04-02 04:08:09.123 +0000 UTC", zname: "CEST"}, {in: "2014-04-02 04:08:09.123 PM", out: "2014-04-02 16:08:09.123 +0000 UTC"}, + {in: "2014-04-02 04:08:09.123 PM PST", out: "2014-04-02 16:08:09.123 +0000 UTC", zname: "PST"}, + {in: "2014-04-02 04:08:09.123 PM CEST", out: "2014-04-02 16:08:09.123 +0000 UTC", zname: "CEST"}, + // https://github.com/araddon/dateparse/issues/150 + {in: "2023-01-04 12:01am", out: "2023-01-04 00:01:00 +0000 UTC"}, + {in: "2023-01-04 12:01 AM", out: "2023-01-04 00:01:00 +0000 UTC"}, + {in: "2023-01-04 12:01:59 AM", out: "2023-01-04 00:01:59 +0000 UTC"}, + {in: "2023-01-04 12:01:59.765 AM", out: "2023-01-04 00:01:59.765 +0000 UTC"}, + // https://github.com/araddon/dateparse/issues/157 + {in: "Thu Jan 28 2021", out: "2021-01-28 00:00:00 +0000 UTC"}, + {in: "Thu Jan 28 2021 15:28:21 GMT+0000 (Coordinated Universal Time)", out: "2021-01-28 15:28:21 +0000 UTC"}, + {in: "Thu Jan 28 2021 15:28:21 GMT+0100 (Coordinated Universal Time)", out: "2021-01-28 14:28:21 +0000 UTC"}, + {in: "Thu Jan 28 2021 15:28:21 UTC+0000 (Coordinated Universal Time)", out: "2021-01-28 15:28:21 +0000 UTC"}, + {in: "Thu Jan 28 2021 15:28:21 UTC+0100 (Coordinated Universal Time)", out: "2021-01-28 14:28:21 +0000 UTC"}, + // https://github.com/araddon/dateparse/issues/130 + {in: "1985-04-12T23:20:50Z", out: "1985-04-12 23:20:50 +0000 UTC"}, + {in: "1985-04-12T23:20:50.52Z", out: "1985-04-12 23:20:50.52 +0000 UTC"}, + // https://github.com/araddon/dateparse/issues/123 + {in: "2017-04-03 22:32:14.322 CET", out: "2017-04-03 22:32:14.322 +0000 UTC", zname: "CET"}, + {in: "2017-04-03 22:32:14 CET", out: "2017-04-03 22:32:14 +0000 UTC", zname: "CET"}, + {in: "Mon Dec 26 16:22:08 2016", out: "2016-12-26 16:22:08 +0000 UTC"}, + {in: "Mon Dec 26 16:15:55.103786 2016", out: "2016-12-26 16:15:55.103786 +0000 UTC"}, + // https://github.com/araddon/dateparse/issues/109 + {in: "Sun, 07 Jun 2020 00:00:00 +0100", out: "2020-06-06 23:00:00 +0000 UTC"}, + {in: "Sun, 07 Jun 2020", out: "2020-06-07 00:00:00 +0000 UTC"}, + // https://github.com/araddon/dateparse/issues/100#issuecomment-1118868154 + {in: "1 Apr 2022 23:59", out: "2022-04-01 23:59:00 +0000 UTC"}, + {in: "1 JANuary 2022 23:59", out: "2022-01-01 23:59:00 +0000 UTC"}, + {in: "1 february 2022 23:59", out: "2022-02-01 23:59:00 +0000 UTC"}, + {in: "1 marCH 2022 23:59", out: "2022-03-01 23:59:00 +0000 UTC"}, + {in: "1 April 2022 23:59", out: "2022-04-01 23:59:00 +0000 UTC"}, + {in: "1 May 2022 23:59", out: "2022-05-01 23:59:00 +0000 UTC"}, + {in: "1 JuNe 2022 23:59", out: "2022-06-01 23:59:00 +0000 UTC"}, + {in: "1 JULY 2022 23:59", out: "2022-07-01 23:59:00 +0000 UTC"}, + {in: "1 august 2022 23:59", out: "2022-08-01 23:59:00 +0000 UTC"}, + {in: "1 September 2022 23:59", out: "2022-09-01 23:59:00 +0000 UTC"}, + {in: "1 October 2022 23:59", out: "2022-10-01 23:59:00 +0000 UTC"}, + {in: "1 November 2022 23:59", out: "2022-11-01 23:59:00 +0000 UTC"}, + {in: "1 December 2022 23:59", out: "2022-12-01 23:59:00 +0000 UTC"}, + // https://github.com/araddon/dateparse/issues/149 + {in: "2018-09-30 21:09:13 PMDT", out: "2018-09-30 21:09:13 +0000 UTC", zname: "PMDT"}, + {in: "2018-09-30 08:09:13 PM PMDT", out: "2018-09-30 20:09:13 +0000 UTC", zname: "PMDT"}, + {in: "2018-09-30 08:09:13pm PMDT", out: "2018-09-30 20:09:13 +0000 UTC", zname: "PMDT"}, + {in: "2018-09-30 21:09:13.123 PMDT", out: "2018-09-30 21:09:13.123 +0000 UTC", zname: "PMDT"}, + {in: "2018-09-30 08:09:13.123 PM PMDT", out: "2018-09-30 20:09:13.123 +0000 UTC", zname: "PMDT"}, + {in: "2018-09-30 08:09:13.123pm PMDT", out: "2018-09-30 20:09:13.123 +0000 UTC", zname: "PMDT"}, + {in: "2018-09-30 21:09:13 AMT", out: "2018-09-30 21:09:13 +0000 UTC", zname: "AMT"}, + {in: "2018-09-30 08:09:13 AM AMT", out: "2018-09-30 08:09:13 +0000 UTC", zname: "AMT"}, + {in: "2018-09-30 08:09:13am AMT", out: "2018-09-30 08:09:13 +0000 UTC", zname: "AMT"}, + {in: "2018-09-30 21:09:13.123 AMT", out: "2018-09-30 21:09:13.123 +0000 UTC", zname: "AMT"}, + {in: "2018-09-30 08:09:13.123 am AMT", out: "2018-09-30 08:09:13.123 +0000 UTC", zname: "AMT"}, + {in: "2018-09-30 08:09:13.123am AMT", out: "2018-09-30 08:09:13.123 +0000 UTC", zname: "AMT"}, + /// yyyy mmm dd https://github.com/araddon/dateparse/issues/141 + {in: "2013 May 2", out: "2013-05-02 00:00:00 +0000 UTC"}, + {in: "2013 May 02 11:37:55", out: "2013-05-02 11:37:55 +0000 UTC"}, + {in: "2013 June 02 11:37:55", out: "2013-06-02 11:37:55 +0000 UTC"}, + {in: "2013 December 02 11:37:55", out: "2013-12-02 11:37:55 +0000 UTC"}, + // https://github.com/araddon/dateparse/issues/71 and https://github.com/araddon/dateparse/issues/72 + {in: "2017-12-31T16:00:00Z", out: "2017-12-31 16:00:00 +0000 UTC", loc: "America/Denver", zname: "UTC"}, + {in: "Jul 9, 2012 at 5:02am (EST)", out: "2012-07-09 05:02:00 +0000 UTC", zname: "EST"}, + {in: "Jul 9, 2012 at 5:02am (EST)", out: "2012-07-09 05:02:00 +0000 UTC", loc: "US/Pacific", zname: "EST"}, + {in: "Jul 9, 2012 at 5:02am (EST)", out: "2012-07-09 10:02:00 +0000 UTC", loc: "America/New_York", zname: "EDT"}, + // https://github.com/araddon/dateparse/pull/156 + {in: "04/02/2014, 04:08:09", out: "2014-04-02 04:08:09 +0000 UTC"}, + {in: "4/2/2014, 04:08:09", out: "2014-04-02 04:08:09 +0000 UTC"}, + {in: "04/02/2014, 04:08 AM", out: "2014-04-02 04:08:00 +0000 UTC"}, + {in: "04/02/2014, 04:08 PM", out: "2014-04-02 16:08:00 +0000 UTC"}, + // Git log default date format - https://github.com/araddon/dateparse/pull/92 + {in: "Thu Apr 7 15:13:13 2005 -0700", out: "2005-04-07 22:13:13 +0000 UTC"}, + {in: "Tue Dec 12 23:07:11 2023 -0700", out: "2023-12-13 06:07:11 +0000 UTC"}, + // Variants with different offset formats, or that place the year after the offset and/or timezone + {in: "Thu Apr 7 15:13:13 2005 -07:00", out: "2005-04-07 22:13:13 +0000 UTC"}, + {in: "Thu Apr 7 15:13:13 2005 -07:00 PST", out: "2005-04-07 22:13:13 +0000 UTC", zname: "PST"}, + {in: "Thu Apr 7 15:13:13 2005 -07:00 PST (Pacific Standard Time)", out: "2005-04-07 22:13:13 +0000 UTC", zname: "PST"}, + {in: "Thu Apr 7 15:13:13 -0700 2005", out: "2005-04-07 22:13:13 +0000 UTC"}, + {in: "Thu Apr 7 15:13:13 -07:00 2005", out: "2005-04-07 22:13:13 +0000 UTC"}, + {in: "Thu Apr 7 15:13:13 -0700 PST 2005", out: "2005-04-07 22:13:13 +0000 UTC", zname: "PST"}, + {in: "Thu Apr 7 15:13:13 -07:00 PST 2005", out: "2005-04-07 22:13:13 +0000 UTC", zname: "PST"}, + {in: "Thu Apr 7 15:13:13 PST 2005", out: "2005-04-07 15:13:13 +0000 UTC", zname: "PST"}, + // RabbitMQ log format - https://github.com/araddon/dateparse/pull/122 + {in: "8-Mar-2018::14:09:27", out: "2018-03-08 14:09:27 +0000 UTC"}, + {in: "08-03-2018::02:09:29 PM", out: "2018-03-08 14:09:29 +0000 UTC"}, // yyyy-mm-dd hh:mm:ss,000 {in: "2014-05-11 08:20:13,787", out: "2014-05-11 08:20:13.787 +0000 UTC"}, + {in: "2014-05-11 08:20:13:787", out: "2014-05-11 08:20:13.787 +0000 UTC"}, // yyyy-mm-dd hh:mm:ss +0000 {in: "2012-08-03 18:31:59 +0000", out: "2012-08-03 18:31:59 +0000 UTC"}, {in: "2012-08-03 13:31:59 -0600", out: "2012-08-03 19:31:59 +0000 UTC"}, @@ -276,13 +562,13 @@ var testInputs = []dateTest{ {in: "2014-04-26 17:24:37.1 +0000", out: "2014-04-26 17:24:37.1 +0000 UTC"}, {in: "2014-05-11 08:20:13 +0000", out: "2014-05-11 08:20:13 +0000 UTC"}, {in: "2014-05-11 08:20:13 +0530", out: "2014-05-11 02:50:13 +0000 UTC"}, + {in: "2014-05-11 08:20:13 +0530 m=+0.000000001", out: "2014-05-11 02:50:13 +0000 UTC"}, + {in: "2014-05-11 08:20:13.123456 +0530 m=+0.000000001", out: "2014-05-11 02:50:13.123456 +0000 UTC"}, // yyyy-mm-dd hh:mm:ss +0300 +03 ?? issue author said this is from golang? {in: "2018-06-29 19:09:57.77297118 +0300 +03", out: "2018-06-29 16:09:57.77297118 +0000 UTC"}, {in: "2018-06-29 19:09:57.77297118 +0300 +0300", out: "2018-06-29 16:09:57.77297118 +0000 UTC"}, {in: "2018-06-29 19:09:57 +0300 +03", out: "2018-06-29 16:09:57 +0000 UTC"}, {in: "2018-06-29 19:09:57 +0300 +0300", out: "2018-06-29 16:09:57 +0000 UTC"}, - - // 13:31:51.999 -07:00 MST // yyyy-mm-dd hh:mm:ss +00:00 {in: "2012-08-03 18:31:59 +00:00", out: "2012-08-03 18:31:59 +0000 UTC"}, {in: "2014-05-01 08:02:13 +00:00", out: "2014-05-01 08:02:13 +0000 UTC"}, @@ -296,62 +582,92 @@ var testInputs = []dateTest{ {in: "2014-04-26 17:24:37.123456 +00:00", out: "2014-04-26 17:24:37.123456 +0000 UTC"}, {in: "2014-04-26 17:24:37.12 +00:00", out: "2014-04-26 17:24:37.12 +0000 UTC"}, {in: "2014-04-26 17:24:37.1 +00:00", out: "2014-04-26 17:24:37.1 +0000 UTC"}, + {in: "2014-04-26 17:24:37 +00:00 m=+0.000000001", out: "2014-04-26 17:24:37 +0000 UTC"}, + {in: "2014-04-26 17:24:37.123456 +00:00 m=+0.000000001", out: "2014-04-26 17:24:37.123456 +0000 UTC"}, // yyyy-mm-dd hh:mm:ss +0000 TZ // Golang Native Format - {in: "2012-08-03 18:31:59 +0000 UTC", out: "2012-08-03 18:31:59 +0000 UTC"}, - {in: "2012-08-03 13:31:59 -0600 MST", out: "2012-08-03 19:31:59 +0000 UTC", loc: "America/Denver"}, - {in: "2015-02-18 00:12:00 +0000 UTC", out: "2015-02-18 00:12:00 +0000 UTC"}, - {in: "2015-02-18 00:12:00 +0000 GMT", out: "2015-02-18 00:12:00 +0000 UTC"}, - {in: "2015-02-08 03:02:00 +0200 CEST", out: "2015-02-08 01:02:00 +0000 UTC", loc: "Europe/Berlin"}, - {in: "2015-02-08 03:02:00 +0300 MSK", out: "2015-02-08 00:02:00 +0000 UTC"}, - {in: "2015-2-08 03:02:00 +0300 MSK", out: "2015-02-08 00:02:00 +0000 UTC"}, - {in: "2015-02-8 03:02:00 +0300 MSK", out: "2015-02-08 00:02:00 +0000 UTC"}, - {in: "2015-2-8 03:02:00 +0300 MSK", out: "2015-02-08 00:02:00 +0000 UTC"}, - {in: "2012-08-03 18:31:59.257000000 +0000 UTC", out: "2012-08-03 18:31:59.257 +0000 UTC"}, - {in: "2012-08-03 8:1:59.257000000 +0000 UTC", out: "2012-08-03 08:01:59.257 +0000 UTC"}, - {in: "2012-8-03 18:31:59.257000000 +0000 UTC", out: "2012-08-03 18:31:59.257 +0000 UTC"}, - {in: "2012-8-3 18:31:59.257000000 +0000 UTC", out: "2012-08-03 18:31:59.257 +0000 UTC"}, - {in: "2014-04-26 17:24:37.123456 +0000 UTC", out: "2014-04-26 17:24:37.123456 +0000 UTC"}, - {in: "2014-04-26 17:24:37.12 +0000 UTC", out: "2014-04-26 17:24:37.12 +0000 UTC"}, - {in: "2014-04-26 17:24:37.1 +0000 UTC", out: "2014-04-26 17:24:37.1 +0000 UTC"}, - {in: "2015-02-08 03:02:00 +0200 CEST m=+0.000000001", out: "2015-02-08 01:02:00 +0000 UTC", loc: "Europe/Berlin"}, - {in: "2015-02-08 03:02:00 +0300 MSK m=+0.000000001", out: "2015-02-08 00:02:00 +0000 UTC"}, - {in: "2015-02-08 03:02:00.001 +0300 MSK m=+0.000000001", out: "2015-02-08 00:02:00.001 +0000 UTC"}, + {in: "2012-08-03 18:31:59 +0000 UTC", out: "2012-08-03 18:31:59 +0000 UTC", zname: "UTC"}, + {in: "2012-08-03 13:31:59 -0600 MST", out: "2012-08-03 19:31:59 +0000 UTC", loc: "America/Denver", zname: "MST"}, + {in: "2015-02-18 00:12:00 +0000 UTC", out: "2015-02-18 00:12:00 +0000 UTC", zname: "UTC"}, + {in: "2015-02-18 00:12:00 +0000 GMT", out: "2015-02-18 00:12:00 +0000 UTC", zname: "GMT"}, + {in: "2015-02-08 03:02:00 +0200 CEST", out: "2015-02-08 01:02:00 +0000 UTC", loc: "Europe/Berlin", zname: "CEST"}, + {in: "2015-02-08 03:02:00 +0300 MSK", out: "2015-02-08 00:02:00 +0000 UTC", zname: "MSK"}, + {in: "2015-2-08 03:02:00 +0300 MSK", out: "2015-02-08 00:02:00 +0000 UTC", zname: "MSK"}, + {in: "2015-02-8 03:02:00 +0300 MSK", out: "2015-02-08 00:02:00 +0000 UTC", zname: "MSK"}, + {in: "2015-2-8 03:02:00 +0300 MSK", out: "2015-02-08 00:02:00 +0000 UTC", zname: "MSK"}, + {in: "2012-08-03 18:31:59.257000000 +0000 UTC", out: "2012-08-03 18:31:59.257 +0000 UTC", zname: "UTC"}, + {in: "2012-08-03 8:1:59.257000000 +0000 UTC", out: "2012-08-03 08:01:59.257 +0000 UTC", zname: "UTC"}, + {in: "2012-8-03 18:31:59.257000000 +0000 UTC", out: "2012-08-03 18:31:59.257 +0000 UTC", zname: "UTC"}, + {in: "2012-8-3 18:31:59.257000000 +0000 UTC", out: "2012-08-03 18:31:59.257 +0000 UTC", zname: "UTC"}, + {in: "2014-04-26 17:24:37.123456 +0000 UTC", out: "2014-04-26 17:24:37.123456 +0000 UTC", zname: "UTC"}, + {in: "2014-04-26 17:24:37.12 +0000 UTC", out: "2014-04-26 17:24:37.12 +0000 UTC", zname: "UTC"}, + {in: "2014-04-26 17:24:37.1 +0000 UTC", out: "2014-04-26 17:24:37.1 +0000 UTC", zname: "UTC"}, + {in: "2015-02-08 03:02:00 +0200 CEST m=+0.000000001", out: "2015-02-08 01:02:00 +0000 UTC", loc: "Europe/Berlin", zname: "CEST"}, + {in: "2015-02-08 03:02:00 +0300 MSK m=+0.000000001", out: "2015-02-08 00:02:00 +0000 UTC", zname: "MSK"}, + {in: "2015-02-08 03:02:00.001 +0300 MSK m=+0.000000001", out: "2015-02-08 00:02:00.001 +0000 UTC", zname: "MSK"}, + // Variant with colon in offset + {in: "2015-02-08 03:02:00 +02:00 CEST", out: "2015-02-08 01:02:00 +0000 UTC", loc: "Europe/Berlin", zname: "CEST"}, + {in: "2015-02-08 03:02:00 +02:00 CEST (Central European Standard Time)", out: "2015-02-08 01:02:00 +0000 UTC", loc: "Europe/Berlin", zname: "CEST"}, // yyyy-mm-dd hh:mm:ss TZ - {in: "2012-08-03 18:31:59 UTC", out: "2012-08-03 18:31:59 +0000 UTC"}, - {in: "2014-12-16 06:20:00 GMT", out: "2014-12-16 06:20:00 +0000 UTC"}, - {in: "2012-08-03 13:31:59 MST", out: "2012-08-03 20:31:59 +0000 UTC", loc: "America/Denver"}, - {in: "2012-08-03 18:31:59.257000000 UTC", out: "2012-08-03 18:31:59.257 +0000 UTC"}, - {in: "2012-08-03 8:1:59.257000000 UTC", out: "2012-08-03 08:01:59.257 +0000 UTC"}, - {in: "2012-8-03 18:31:59.257000000 UTC", out: "2012-08-03 18:31:59.257 +0000 UTC"}, - {in: "2012-8-3 18:31:59.257000000 UTC", out: "2012-08-03 18:31:59.257 +0000 UTC"}, - {in: "2014-04-26 17:24:37.123456 UTC", out: "2014-04-26 17:24:37.123456 +0000 UTC"}, - {in: "2014-04-26 17:24:37.12 UTC", out: "2014-04-26 17:24:37.12 +0000 UTC"}, - {in: "2014-04-26 17:24:37.1 UTC", out: "2014-04-26 17:24:37.1 +0000 UTC"}, + {in: "2012-08-03 18:31:59 UTC", out: "2012-08-03 18:31:59 +0000 UTC", zname: "UTC"}, + {in: "2012-08-03 18:31:59 CEST", out: "2012-08-03 18:31:59 +0000 UTC", zname: "CEST"}, + {in: "2014-12-16 06:20:00 GMT", out: "2014-12-16 06:20:00 +0000 UTC", zname: "GMT"}, + {in: "2012-08-03 13:31:58 MST", out: "2012-08-03 13:31:58 +0000 UTC", zname: "MST"}, + {in: "2012-08-03 13:31:59 MST", out: "2012-08-03 20:31:59 +0000 UTC", loc: "America/Denver", zname: "MDT"}, + {in: "2012-01-03 13:31:59 MST", out: "2012-01-03 20:31:59 +0000 UTC", loc: "America/Denver", zname: "MST"}, + {in: "2012-08-03 18:31:59.257000000 UTC", out: "2012-08-03 18:31:59.257 +0000 UTC", zname: "UTC"}, + {in: "2012-08-03 8:1:59.257000000 UTC", out: "2012-08-03 08:01:59.257 +0000 UTC", zname: "UTC"}, + {in: "2012-8-03 18:31:59.257000000 UTC", out: "2012-08-03 18:31:59.257 +0000 UTC", zname: "UTC"}, + {in: "2012-8-3 18:31:59.257000000 UTC", out: "2012-08-03 18:31:59.257 +0000 UTC", zname: "UTC"}, + {in: "2012-8-3 18:31:59.257000000 CEST", out: "2012-08-03 18:31:59.257 +0000 UTC", zname: "CEST"}, + {in: "2014-04-26 17:24:37.123456 UTC", out: "2014-04-26 17:24:37.123456 +0000 UTC", zname: "UTC"}, + {in: "2014-04-26 17:24:37.123456 CEST", out: "2014-04-26 17:24:37.123456 +0000 UTC", zname: "CEST"}, + {in: "2014-04-26 17:24:37.123456Z", out: "2014-04-26 17:24:37.123456 +0000 UTC"}, + {in: "2014-04-26 17:24:37.12 UTC", out: "2014-04-26 17:24:37.12 +0000 UTC", zname: "UTC"}, + {in: "2014-04-26 17:24:37.12 CEST", out: "2014-04-26 17:24:37.12 +0000 UTC", zname: "CEST"}, + {in: "2014-04-26 17:24:37.1 UTC", out: "2014-04-26 17:24:37.1 +0000 UTC", zname: "UTC"}, + {in: "2014-04-26 17:24:37.1 CEST", out: "2014-04-26 17:24:37.1 +0000 UTC", zname: "CEST"}, + // Test the capturing of arbitrary time zone names even if we use a different specific location (offset will be zero, but name will be filled in) + {in: "2012-08-03 19:32:59 UTC", out: "2012-08-03 19:32:59 +0000 UTC", loc: "Europe/Berlin", zname: "UTC"}, + {in: "2012-08-03 19:32:59 CEST", out: "2012-08-03 19:32:59 +0000 UTC", loc: "America/Denver", zname: "CEST"}, + {in: "2014-12-16 07:22:00 GMT", out: "2014-12-16 07:22:00 +0000 UTC", loc: "America/Los_Angeles", zname: "GMT"}, + {in: "2012-08-03 14:32:59 MST", out: "2012-08-03 14:32:59 +0000 UTC", loc: "America/Los_Angeles", zname: "MST"}, // This one is pretty special, it is TIMEZONE based but starts with P to emulate collions with PM - {in: "2014-04-26 05:24:37 PST", out: "2014-04-26 05:24:37 +0000 UTC"}, - {in: "2014-04-26 05:24:37 PST", out: "2014-04-26 13:24:37 +0000 UTC", loc: "America/Los_Angeles"}, + {in: "2014-04-26 05:24:37 PST", out: "2014-04-26 05:24:37 +0000 UTC", zname: "PST"}, + {in: "2014-04-26 05:24:38 PST", out: "2014-04-26 13:24:38 +0000 UTC", loc: "America/Los_Angeles", zname: "PDT"}, + {in: "2014-01-26 05:24:39 PST", out: "2014-01-26 13:24:39 +0000 UTC", loc: "America/Los_Angeles", zname: "PST"}, // yyyy-mm-dd hh:mm:ss+00:00 {in: "2012-08-03 18:31:59+00:00", out: "2012-08-03 18:31:59 +0000 UTC"}, {in: "2017-07-19 03:21:51+00:00", out: "2017-07-19 03:21:51 +0000 UTC"}, // yyyy:mm:dd hh:mm:ss+00:00 {in: "2012:08:03 18:31:59+00:00", out: "2012-08-03 18:31:59 +0000 UTC"}, - // dd:mm:yyyy hh:mm:ss+00:00 + // mm:dd:yyyy hh:mm:ss+00:00 {in: "08:03:2012 18:31:59+00:00", out: "2012-08-03 18:31:59 +0000 UTC"}, + {in: "08:04:2012 18:31:59+00:00", out: "2012-04-08 18:31:59 +0000 UTC", preferDayFirst: true}, + {in: "24:03:2012 18:31:59+00:00", out: "2012-03-24 18:31:59 +0000 UTC", retryAmbiguous: true}, // yyyy-mm-dd hh:mm:ss.000+00:00 PST - {in: "2012-08-03 18:31:59.000+00:00 PST", out: "2012-08-03 18:31:59 +0000 UTC", loc: "America/Los_Angeles"}, + {in: "2012-08-03 18:31:59.000+00:00 PST", out: "2012-08-03 18:31:59 +0000 UTC", loc: "America/Los_Angeles", zname: "PST"}, + {in: "2012-08-03 18:31:59.000+00:00 CEST", out: "2012-08-03 18:31:59 +0000 UTC", loc: "Europe/Berlin", zname: "CEST"}, // yyyy-mm-dd hh:mm:ss +00:00 TZ - {in: "2012-08-03 18:31:59 +00:00 UTC", out: "2012-08-03 18:31:59 +0000 UTC"}, - {in: "2012-08-03 13:31:51 -07:00 MST", out: "2012-08-03 20:31:51 +0000 UTC", loc: "America/Denver"}, - {in: "2012-08-03 18:31:59.257000000 +00:00 UTC", out: "2012-08-03 18:31:59.257 +0000 UTC"}, - {in: "2012-08-03 13:31:51.123 -08:00 PST", out: "2012-08-03 21:31:51.123 +0000 UTC", loc: "America/Los_Angeles"}, - {in: "2012-08-03 13:31:51.123 +02:00 CEST", out: "2012-08-03 11:31:51.123 +0000 UTC", loc: "Europe/Berlin"}, - {in: "2012-08-03 8:1:59.257000000 +00:00 UTC", out: "2012-08-03 08:01:59.257 +0000 UTC"}, - {in: "2012-8-03 18:31:59.257000000 +00:00 UTC", out: "2012-08-03 18:31:59.257 +0000 UTC"}, - {in: "2012-8-3 18:31:59.257000000 +00:00 UTC", out: "2012-08-03 18:31:59.257 +0000 UTC"}, - {in: "2014-04-26 17:24:37.123456 +00:00 UTC", out: "2014-04-26 17:24:37.123456 +0000 UTC"}, - {in: "2014-04-26 17:24:37.12 +00:00 UTC", out: "2014-04-26 17:24:37.12 +0000 UTC"}, - {in: "2014-04-26 17:24:37.1 +00:00 UTC", out: "2014-04-26 17:24:37.1 +0000 UTC"}, + {in: "2012-08-03 18:31:59 +00:00 UTC", out: "2012-08-03 18:31:59 +0000 UTC", zname: "UTC"}, + {in: "2012-08-03 13:31:51 -07:00 MST", out: "2012-08-03 20:31:51 +0000 UTC", loc: "America/Denver", zname: "MST"}, + {in: "2012-08-03 13:31:51 +02:00 CEST", out: "2012-08-03 11:31:51 +0000 UTC", loc: "Europe/Berlin", zname: "CEST"}, + {in: "2012-08-03 18:31:59.257000000 +00:00 UTC", out: "2012-08-03 18:31:59.257 +0000 UTC", zname: "UTC"}, + {in: "2012-08-03 13:31:51.123 -08:00 PST", out: "2012-08-03 21:31:51.123 +0000 UTC", loc: "America/Los_Angeles", zname: "PST"}, + {in: "2012-08-03 13:31:51.123 +02:00 CEST", out: "2012-08-03 11:31:51.123 +0000 UTC", loc: "Europe/Berlin", zname: "CEST"}, + {in: "2012-08-03 13:31:51.123 +02:00 CEST", out: "2012-08-03 11:31:51.123 +0000 UTC", loc: "America/Los_Angeles", zname: "CEST"}, + {in: "2012-08-03 8:1:59.257000000 +00:00 UTC", out: "2012-08-03 08:01:59.257 +0000 UTC", zname: "UTC"}, + {in: "2012-08-03 8:1:59.257000000 +00:00 CEST", out: "2012-08-03 08:01:59.257 +0000 UTC", zname: "CEST"}, + {in: "2012-8-03 18:31:59.257000000 +00:00 UTC", out: "2012-08-03 18:31:59.257 +0000 UTC", zname: "UTC"}, + {in: "2012-8-03 18:31:59.257000000 +00:00 CEST", out: "2012-08-03 18:31:59.257 +0000 UTC", zname: "CEST"}, + {in: "2012-8-3 18:31:59.257000000 +00:00 UTC", out: "2012-08-03 18:31:59.257 +0000 UTC", zname: "UTC"}, + {in: "2012-8-3 18:31:59.257000000 +00:00 CEST", out: "2012-08-03 18:31:59.257 +0000 UTC", zname: "CEST"}, + {in: "2014-04-26 17:24:37.123456 +00:00 UTC", out: "2014-04-26 17:24:37.123456 +0000 UTC", zname: "UTC"}, + {in: "2014-04-26 17:24:37.123456 +00:00 CEST", out: "2014-04-26 17:24:37.123456 +0000 UTC", zname: "CEST"}, + {in: "2014-04-26 17:24:37.12 +00:00 UTC", out: "2014-04-26 17:24:37.12 +0000 UTC", zname: "UTC"}, + {in: "2014-04-26 17:24:37.12 +00:00 CEST", out: "2014-04-26 17:24:37.12 +0000 UTC", zname: "CEST"}, + {in: "2014-04-26 17:24:37.1 +00:00 UTC", out: "2014-04-26 17:24:37.1 +0000 UTC", zname: "UTC"}, + {in: "2014-04-26 17:24:37.1 +00:00 CEST", out: "2014-04-26 17:24:37.1 +0000 UTC", zname: "CEST"}, // yyyy-mm-ddThh:mm:ss {in: "2009-08-12T22:15:09", out: "2009-08-12 22:15:09 +0000 UTC"}, {in: "2009-08-08T02:08:08", out: "2009-08-08 02:08:08 +0000 UTC"}, @@ -383,8 +699,10 @@ var testInputs = []dateTest{ {in: "2016-06-21T19:55+0100", out: "2016-06-21 18:55:00 +0000 UTC"}, {in: "2016-06-21T19:55+0130", out: "2016-06-21 18:25:00 +0000 UTC"}, // yyyy-mm-ddThh:mm:ss:000+0000 - weird format with additional colon in front of milliseconds + {in: "2012-08-17T18:31:59:257", out: "2012-08-17 18:31:59.257 +0000 UTC"}, // https://github.com/araddon/dateparse/issues/137 {in: "2012-08-17T18:31:59:257+0100", out: "2012-08-17 17:31:59.257 +0000 UTC"}, // https://github.com/araddon/dateparse/issues/117 - + {in: "2012-08-17T18:31:59:257+0200 CET", out: "2012-08-17 16:31:59.257 +0000 UTC", zname: "CET"}, + {in: "2012-08-17T18:31:59:257+0200 CET (Central European Time)", out: "2012-08-17 16:31:59.257 +0000 UTC", zname: "CET"}, // yyyy-mm-ddThh:mm:ssZ {in: "2009-08-12T22:15Z", out: "2009-08-12 22:15:00 +0000 UTC"}, {in: "2009-08-12T22:15:09Z", out: "2009-08-12 22:15:09 +0000 UTC"}, @@ -393,30 +711,109 @@ var testInputs = []dateTest{ {in: "2009-08-12T22:15:09.99999999Z", out: "2009-08-12 22:15:09.99999999 +0000 UTC"}, {in: "2009-08-12T22:15:9.99999999Z", out: "2009-08-12 22:15:09.99999999 +0000 UTC"}, // yyyy.mm + {in: "2014", out: "2014-01-01 00:00:00 +0000 UTC"}, {in: "2014.05", out: "2014-05-01 00:00:00 +0000 UTC"}, {in: "2018.09.30", out: "2018-09-30 00:00:00 +0000 UTC"}, - // mm.dd.yyyy {in: "3.31.2014", out: "2014-03-31 00:00:00 +0000 UTC"}, {in: "3.3.2014", out: "2014-03-03 00:00:00 +0000 UTC"}, {in: "03.31.2014", out: "2014-03-31 00:00:00 +0000 UTC"}, + {in: "03.31.2014 10:11:59 MST", out: "2014-03-31 10:11:59 +0000 UTC", zname: "MST"}, // mm.dd.yy {in: "08.21.71", out: "1971-08-21 00:00:00 +0000 UTC"}, + // dd.mm.yyyy (see https://github.com/araddon/dateparse/issues/129 and https://github.com/araddon/dateparse/issues/28 and https://github.com/araddon/dateparse/pull/133) + {in: "23.07.1938", out: "1938-07-23 00:00:00 +0000 UTC", retryAmbiguous: true}, + {in: "23.07.1938", out: "1938-07-23 00:00:00 +0000 UTC", preferDayFirst: true}, + {in: "23/07/1938", out: "1938-07-23 00:00:00 +0000 UTC", retryAmbiguous: true}, + {in: "23/07/1938", out: "1938-07-23 00:00:00 +0000 UTC", preferDayFirst: true}, + {in: "31/3/2014", out: "2014-03-31 00:00:00 +0000 UTC", retryAmbiguous: true}, + {in: "31/3/2014", out: "2014-03-31 00:00:00 +0000 UTC", preferDayFirst: true}, + {in: "31/03/2014", out: "2014-03-31 00:00:00 +0000 UTC", retryAmbiguous: true}, + {in: "31/03/2014", out: "2014-03-31 00:00:00 +0000 UTC", preferDayFirst: true}, + {in: "21/08/71", out: "1971-08-21 00:00:00 +0000 UTC", retryAmbiguous: true}, + {in: "21/08/71", out: "1971-08-21 00:00:00 +0000 UTC", preferDayFirst: true}, + {in: "1/8/71", out: "1971-01-08 00:00:00 +0000 UTC", preferDayFirst: false}, + {in: "1/8/71", out: "1971-08-01 00:00:00 +0000 UTC", preferDayFirst: true}, + {in: "8/4/2014 22:05", out: "2014-08-04 22:05:00 +0000 UTC", preferDayFirst: false}, + {in: "8/4/2014 22:05", out: "2014-04-08 22:05:00 +0000 UTC", preferDayFirst: true}, + {in: "08/04/2014 22:05", out: "2014-08-04 22:05:00 +0000 UTC", preferDayFirst: false}, + {in: "08/04/2014 22:05", out: "2014-04-08 22:05:00 +0000 UTC", preferDayFirst: true}, + {in: "2/04/2014 03:00:51", out: "2014-02-04 03:00:51 +0000 UTC", preferDayFirst: false}, + {in: "2/04/2014 03:00:51", out: "2014-04-02 03:00:51 +0000 UTC", preferDayFirst: true}, + {in: "19/03/2012 10:11:56", out: "2012-03-19 10:11:56 +0000 UTC", retryAmbiguous: true}, + {in: "19/03/2012 10:11:57", out: "2012-03-19 10:11:57 +0000 UTC", preferDayFirst: true}, + {in: "19/03/2012 10:11:58.3186369", out: "2012-03-19 10:11:58.3186369 +0000 UTC", retryAmbiguous: true}, + {in: "19/03/2012 10:11:59.3186369", out: "2012-03-19 10:11:59.3186369 +0000 UTC", preferDayFirst: true}, + // For certain parse modes that restart parsing, make sure that parsing options are passed along! + {in: "Monday 19/03/2012 10:11:50", out: "2012-03-19 10:11:50 +0000 UTC", retryAmbiguous: true}, + {in: "Monday 19/03/2012 10:11:51", out: "2012-03-19 10:11:51 +0000 UTC", preferDayFirst: true}, + // https://github.com/araddon/dateparse/issues/105 + {in: "20/5/2006 19:51:45", out: "2006-05-20 19:51:45 +0000 UTC", retryAmbiguous: true}, + {in: "20/5/2006 19:51:45", out: "2006-05-20 19:51:45 +0000 UTC", preferDayFirst: true}, // yyyymmdd and similar - {in: "2014", out: "2014-01-01 00:00:00 +0000 UTC"}, - {in: "20140601", out: "2014-06-01 00:00:00 +0000 UTC"}, - {in: "20140722105203", out: "2014-07-22 10:52:03 +0000 UTC"}, + {in: "2014", out: "2014-01-01 00:00:00 +0000 UTC", allowWeekdayPrefix: false}, + {in: "20140601", out: "2014-06-01 00:00:00 +0000 UTC", allowWeekdayPrefix: false}, + {in: "20140722105203", out: "2014-07-22 10:52:03 +0000 UTC", allowWeekdayPrefix: false}, + // https://github.com/araddon/dateparse/issues/143 + {in: "20140722105203.364", out: "2014-07-22 10:52:03.364 +0000 UTC", allowWeekdayPrefix: false}, // yymmdd hh:mm:yy mysql log https://github.com/araddon/dateparse/issues/119 // 080313 05:21:55 mysqld started // 080313 5:21:55 InnoDB: Started; log sequence number 0 43655 {in: "171113 14:14:20", out: "2017-11-13 14:14:20 +0000 UTC"}, + // https://github.com/araddon/dateparse/issues/94 + {in: "190910 11:51:49", out: "2019-09-10 11:51:49 +0000 UTC"}, // all digits: unix secs, ms etc - {in: "1332151919", out: "2012-03-19 10:11:59 +0000 UTC"}, - {in: "1332151919", out: "2012-03-19 10:11:59 +0000 UTC", loc: "America/Denver"}, - {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: "1332151919", out: "2012-03-19 10:11:59 +0000 UTC", zname: "UTC", allowWeekdayPrefix: false}, + {in: "1332151919", out: "2012-03-19 10:11:59 +0000 UTC", loc: "America/Denver", zname: "MDT", allowWeekdayPrefix: false}, + {in: "1384216367111", out: "2013-11-12 00:32:47.111 +0000 UTC", allowWeekdayPrefix: false}, + {in: "1384216367111222", out: "2013-11-12 00:32:47.111222 +0000 UTC", allowWeekdayPrefix: false}, + {in: "1384216367111222333", out: "2013-11-12 00:32:47.111222333 +0000 UTC", allowWeekdayPrefix: false}, + + // other + {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"}, + // https://github.com/araddon/dateparse/issues/158 + {in: "Mon, 1 Dec 2008 14:48:22 GMT-07:00", out: "2008-12-01 21:48:22 +0000 UTC"}, + {in: "Mon, 1 Dec 2008 14:48:22 UTC-07:00", out: "2008-12-01 21:48:22 +0000 UTC"}, + // Fixes for bugs mentioned in https://github.com/araddon/dateparse/pull/134 + {in: "2014.02.13", out: "2014-02-13 00:00:00 +0000 UTC"}, + {in: "2014-02-13 00:00:00", out: "2014-02-13 00:00:00 +0000 UTC"}, + {in: "2014.02.13 00:00:00", out: "2014-02-13 00:00:00 +0000 UTC"}, + {in: "2014.02.13 08:33:44", out: "2014-02-13 08:33:44 +0000 UTC"}, + {in: "2014.02.13T08:33:44", out: "2014-02-13 08:33:44 +0000 UTC"}, + {in: "2014.02.13T08:33:44.555", out: "2014-02-13 08:33:44.555 +0000 UTC"}, + {in: "2014.02.13T08:33:44.555 PM -0700 MST", out: "2014-02-14 03:33:44.555 +0000 UTC", zname: "MST"}, + {in: "2014.02.13-0200", out: "2014-02-13 02:00:00 +0000 UTC"}, + // Whitespace up front is now allowed + {in: " 2018-01-02 17:08:09 -07:00", out: "2018-01-03 00:08:09 +0000 UTC"}, + {in: " 2018-01-02 17:08:09 -07:00", out: "2018-01-03 00:08:09 +0000 UTC"}, + {in: " 2018-01-02 17:08:09 -07:00", out: "2018-01-03 00:08:09 +0000 UTC"}, + + //syslog RFC3164 formats and non-conformant variants observed in the wild + {in: "Apr 9 12:37:24", out: "0000-04-09 12:37:24 +0000 UTC"}, + {in: "Apr 9 12:37:24-10", out: "0000-04-09 22:37:24 +0000 UTC"}, + {in: "Apr 9 12:37:24-1000", out: "0000-04-09 22:37:24 +0000 UTC"}, + {in: "Apr 9 12:37:24 UTC-10", out: "0000-04-09 22:37:24 +0000 UTC"}, + {in: "Apr 9 12:37:24 MST", out: "0000-04-09 12:37:24 +0000 UTC", zname: "MST"}, + {in: "Apr 9 12:37:24 MST-07:00", out: "0000-04-09 19:37:24 +0000 UTC", zname: "MST"}, + {in: "Apr 9 12:37:24 TZ-10", out: "0000-04-09 22:37:24 +0000 UTC"}, + {in: "Apr 9 12:37:24 TZ+02:00", out: "0000-04-09 10:37:24 +0000 UTC"}, + {in: "Apr 9 12:37:24+10", out: "0000-04-09 02:37:24 +0000 UTC"}, + {in: "Apr 9 12:37:24+10:00", out: "0000-04-09 02:37:24 +0000 UTC"}, + {in: "Apr 9 12:37:24 CEST", out: "0000-04-09 12:37:24 +0000 UTC", zname: "CEST"}, + {in: "Apr 9 12:37:24 CEST+0200", out: "0000-04-09 10:37:24 +0000 UTC", zname: "CEST"}, + {in: "Apr 9 12:37:24 2025", out: "2025-04-09 12:37:24 +0000 UTC"}, + {in: "Apr 9 12:37:24 2025 +02:00", out: "2025-04-09 10:37:24 +0000 UTC"}, + {in: "Apr 9 2025 12:37:24", out: "2025-04-09 12:37:24 +0000 UTC"}, + {in: "Apr 9 2025 12:37:24 -0700", out: "2025-04-09 19:37:24 +0000 UTC"}, + //syslog RFC5424 formats and non-conformant variants observed in the wild + {in: "2025-04-09T12:37:24Z", out: "2025-04-09 12:37:24 +0000 UTC"}, + {in: "2025-04-09T12:37:24.123Z", out: "2025-04-09 12:37:24.123 +0000 UTC"}, + {in: "2025-04-09T12:37:24.123456Z", out: "2025-04-09 12:37:24.123456 +0000 UTC"}, + {in: "2025-04-09T12:37:24-10:00", out: "2025-04-09 22:37:24 +0000 UTC"}, + {in: "2025-04-09T12:37:24.123 +0200", out: "2025-04-09 10:37:24.123 +0000 UTC"}, + {in: "2025-04-09T12:37:24.123456 -0700 MDT", out: "2025-04-09 19:37:24.123456 +0000 UTC", zname: "MDT"}, } func TestParse(t *testing.T) { @@ -425,52 +822,104 @@ 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) - } - 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") + allDays := make([]string, 0, len(knownDays)) + for day := range knownDays { + allDays = append(allDays, day) + } + + i := 0 + for _, simpleErrorMessage := range []bool{false, true} { + for _, addWeekday := range []bool{false, true} { + for _, th := range testInputs { + i++ + prefix := "" + if addWeekday && th.allowWeekdayPrefix { + prefix = allDays[i%len(allDays)] + if i%2 == 1 { + prefix += "," + } + prefix += " " + } + fullInput := prefix + th.in + + t.Run(fmt.Sprintf("simpleerr-%v/addweekday-%v/%s", simpleErrorMessage, addWeekday, fullInput), func(t *testing.T) { + var ts time.Time + defer func() { + if r := recover(); r != nil { + t.Fatalf("error: %s", r) + } + }() + parserOptions := []ParserOption{ + PreferMonthFirst(!th.preferDayFirst), + RetryAmbiguousDateWithSwap(th.retryAmbiguous), + SimpleErrorMessages(simpleErrorMessage), + } + 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(fullInput, loc, parserOptions...) + if err != nil { + t.Fatalf("expected to parse %q but got %v", fullInput, 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, fullInput) + if th.out != got { + t.Fatalf("whoops, got %s, expected %s", got, th.out) + } + if len(th.zname) > 0 { + gotZone, _ := ts.Zone() + assert.Equal(t, th.zname, gotZone, "Expected zname %q but got %q from %q", th.zname, gotZone, fullInput) + } + } else { + ts = MustParse(fullInput, parserOptions...) + got := fmt.Sprintf("%v", ts.In(time.UTC)) + assert.Equal(t, th.out, got, "Expected %q but got %q from %q", th.out, got, fullInput) + if th.out != got { + t.Fatalf("whoops, got %s, expected %s", got, th.out) + } + if len(th.zname) > 0 { + gotZone, _ := ts.Zone() + assert.Equal(t, th.zname, gotZone, "Expected zname %q but got %q from %q", th.zname, gotZone, fullInput) + } + } + }) } } } // 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 +937,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) @@ -507,22 +959,110 @@ var testParseErrors = []dateTest{ {in: `{"hello"}`, err: true}, {in: "2009-15-12T22:15Z", err: true}, {in: "5,000-9,999", err: true}, - {in: "xyzq-baad"}, + {in: "xyzq-baad", err: true}, {in: "oct.-7-1970", err: true}, {in: "septe. 7, 1970", err: true}, {in: "SeptemberRR 7th, 1970", err: true}, - {in: "29-06-2016", err: true}, - // this is just testing the empty space up front - {in: " 2018-01-02 17:08:09 -07:00", err: true}, + // a semantic version number should not be interpreted as a date + {in: "1.22.3-78888", err: true}, + // a semantic version number that starts with a date should not be interpreted as a date + {in: "1.22.2023-78888", err: true}, + // https://github.com/araddon/dateparse/issues/145 + {in: "dataddo, faces, bug", err: true}, + // https://github.com/araddon/dateparse/issues/108 + {in: "1.jpg", err: true}, + // https://github.com/araddon/dateparse/issues/98 + {in: "Wayne, Bruce", err: true}, + {in: "Miami, Florida", err: true}, + {in: "Doe, John", err: true}, + // https://github.com/araddon/dateparse/issues/149 + {in: "2018-09-30 21:09:13PMDT", err: true}, + {in: "2018-09-30 08:09:13pm PM", err: true}, + {in: "2018-09-30 08:09:13 PM PM", err: true}, + {in: "2018-09-30 08:09:13 PMDT PM", err: true}, + {in: "2018-09-30 21:09:13.123PMDT", err: true}, + {in: "2018-09-30 08:09:13.123PM pm", err: true}, + {in: "2018-09-30 08:09:13.123 pm PM", err: true}, + {in: "2018-09-30 08:09:13.123 PMDT pm", err: true}, + {in: "2018-09-30 21:09:13AMT", err: true}, + {in: "2018-09-30 08:09:13am AM", err: true}, + {in: "2018-09-30 08:09:13 AM AM", err: true}, + {in: "2018-09-30 08:09:13 AMT AM", err: true}, + {in: "2018-09-30 21:09:13.123AMT", err: true}, + {in: "2018-09-30 08:09:13.123AM am", err: true}, + {in: "2018-09-30 08:09:13.123 am AM", err: true}, + {in: "2018-09-30 08:09:13.123 AMDT am", err: true}, + // https://github.com/araddon/dateparse/pull/134 + {in: "2014-02-13 00:00:00 utc", err: true}, // lowercase timezones are not valid + {in: "2014-02-13t00:00:00.0z", err: true}, // lowercase 't' separator is not supported + {in: "2014-02-13T00:00:00.0z", err: true}, // lowercase 'z' zulu timezone indicator not a valid format + // Invalid variants of RabbitMQ log format + {in: "8-Mar-2018:14:09:27", err: true}, + {in: "8-Mar-2018: 14:09:27", err: true}, + {in: "8-Mar-2018:::14:09:27", err: true}, + // Invalid repeated year + {in: "Thu Apr 7 15:13:13 2005 2004", err: true}, + {in: "Thu Apr 7 15:13:13 2005 2004 ", err: true}, + {in: "Thu Apr 7 15:13:13 2005-0700", err: true}, + {in: "Thu Apr 7 15:13:13 2005-07:00", err: true}, + {in: "Thu Apr 7 15:13:13 2005 -0700 2005", err: true}, + {in: "Thu Apr 7 15:13:13 2005 -0700 PST 2005", err: true}, + {in: "Thu Apr 7 15:13:13 2005 -07:00 2005", err: true}, + {in: "Thu Apr 7 15:13:13 2005 -07:00 PST 2005", err: true}, + // Invalid offsets + {in: "Fri Jul 03 2015 18:04:07 GMT+0", err: true}, + {in: "Fri Jul 03 2015 18:04:07 GMT+000", err: true}, + {in: "Fri Jul 03 2015 18:04:07 GMT+0:100", err: true}, + {in: "Fri Jul 03 2015 18:04:07 GMT+010:0", err: true}, + {in: "Fri Jul 03 2015 18:04:07 GMT+01000", err: true}, + {in: "Fri Jul 03 2015 18:04:07 GMT+01:000", err: true}, + {in: "Fri Jul 03 2015 18:04:07 +0", err: true}, + {in: "Fri Jul 03 2015 18:04:07 +000", err: true}, + {in: "Fri Jul 03 2015 18:04:07 +0:100", err: true}, + {in: "Fri Jul 03 2015 18:04:07 +010:0", err: true}, + {in: "Fri Jul 03 2015 18:04:07 +01000", err: true}, + {in: "Fri Jul 03 2015 18:04:07 +01:000", err: true}, + // Invalid extra words on the end (or invalid time zone description) + {in: "2018-09-30 21:09:13 (Universal Coordinated Time)", err: true}, + {in: "2018-09-30 21:09:13pm (Universal Coordinated Time)", err: true}, + {in: "Fri Jul 3 2015 06:04:07 GMT+0100 blah", err: true}, + {in: "Fri Jul 3 2015 06:04:07 GMT+0100 hello world", err: true}, + {in: "Fri Jul 03 2015 18:04:07 UTC+0100 GMT Daylight Time", err: true}, + {in: "Fri Jul 3 2015 06:04:07 UTC+0100 (GMT", err: true}, + {in: "Fri Jul 3 2015 06:04:07 PST-0700 (Pacific (Daylight) Time)", err: true}, + {in: "Fri Jul 3 2015 06:04:07 CEST-0700 (Central European Summer Time) extra", err: true}, + {in: "Fri Jul 3 2015 06:04:07 +0100 blah", err: true}, + {in: "Fri Jul 3 2015 06:04:07 +0100 hello world", err: true}, + {in: "Fri Jul 03 2015 18:04:07 +0100 GMT Daylight Time", err: true}, + {in: "Fri Jul 3 2015 06:04:07 +0100 (GMT", err: true}, + {in: "Fri Jul 3 2015 06:04:07 -0700 (Pacific (Daylight) Time)", err: true}, + {in: "Fri Jul 3 2015 06:04:07 -0700 (Central European Summer Time) extra", err: true}, + {in: "Fri Jul 3 2015 06:04:07 +01:00 blah", err: true}, + {in: "Fri Jul 3 2015 06:04:07 +01:00 hello world", err: true}, + {in: "Fri Jul 03 2015 18:04:07 +01:00 GMT Daylight Time", err: true}, + {in: "Fri Jul 3 2015 06:04:07 +01:00 (GMT", err: true}, + {in: "Fri Jul 3 2015 06:04:07 -07:00 (Pacific (Daylight) Time)", err: true}, + {in: "Fri Jul 3 2015 06:04:07 -07:00 (Central European Summer Time) extra", err: true}, + {in: "Fri Jul 03 2015 18:04:07 GMT GMT", err: true}, + {in: "Fri Jul 3 2015 06:04:07 PMT blah", err: true}, + {in: "Fri Jul 3 2015 06:04:07 PMT hello world", err: true}, + {in: "Fri Jul 03 2015 18:04:07 AMT GMT Daylight Time", err: true}, + {in: "Fri Jul 3 2015 06:04:07 UTC (GMT", err: true}, + {in: "Fri Jul 3 2015 06:04:07 PST (Pacific (Daylight) Time)", err: true}, + {in: "Fri Jul 3 2015 06:04:07 CEST (Central European Summer Time) extra", err: true}, + // Special TZ indicator must be followed by an offset + {in: "Apr 9 12:37:24 TZ", err: true}, } func TestParseErrors(t *testing.T) { for _, th := range testParseErrors { - v, err := ParseAny(th.in) - assert.NotEqual(t, nil, err, "%v for %v", v, th.in) + t.Run(th.in, func(t *testing.T) { + v, err := ParseAny(th.in) + assert.NotEqual(t, nil, err, "%v for %v", v, th.in) - v, err = ParseAny(th.in, RetryAmbiguousDateWithSwap(true)) - assert.NotEqual(t, nil, err, "%v for %v", v, th.in) + v, err = ParseAny(th.in, RetryAmbiguousDateWithSwap(true)) + assert.NotEqual(t, nil, err, "%v for %v", v, th.in) + }) } } @@ -543,6 +1083,7 @@ func TestParseLayout(t *testing.T) { // {in: "06/May/2008 15:04:05 -0700", out: "02/Jan/2006 15:04:05 -0700"}, {in: "06/May/2008:15:04:05 -0700", out: "02/Jan/2006:15:04:05 -0700"}, + {in: "06/June/2008 15:04:05 -0700", out: "02/January/2006 15:04:05 -0700"}, {in: "14 May 2019 19:11:40.164", out: "02 Jan 2006 15:04:05.000"}, {in: "171113 14:14:20", out: "060102 15:04:05"}, @@ -559,7 +1100,7 @@ func TestParseLayout(t *testing.T) { {in: "2012-08-03 18:31:59 +0000 UTC", out: "2006-01-02 15:04:05 -0700 MST"}, // yyyy-mm-dd hh:mm:ss TZ {in: "2012-08-03 18:31:59 UTC", out: "2006-01-02 15:04:05 MST"}, - {in: "2012-08-03 18:31:59 CEST", out: "2006-01-02 15:04:05 MST"}, + {in: "2012-08-03 18:31:59 CEST", out: "2006-01-02 15:04:05 MST "}, // yyyy-mm-ddThh:mm:ss-07:00 {in: "2009-08-12T22:15:09-07:00", out: "2006-01-02T15:04:05-07:00"}, // yyyy-mm-ddThh:mm:ss-0700 @@ -569,45 +1110,61 @@ func TestParseLayout(t *testing.T) { } for _, th := range testParseFormat { - l, err := ParseFormat(th.in) - if th.err { - assert.NotEqual(t, nil, err) - } else { - assert.Equal(t, nil, err) - assert.Equal(t, th.out, l, "for in=%v", th.in) - } + t.Run(th.in, func(t *testing.T) { + l, err := ParseFormat(th.in) + if th.err { + assert.NotEqual(t, nil, err) + } else { + assert.Equal(t, nil, err) + assert.Equal(t, th.out, l, "for in=%v", th.in) + } + }) } } var testParseStrict = []dateTest{ // dd-mon-yy 13-Feb-03 - {in: "03-03-14"}, + {in: "03-03-14", err: true, expectAmbiguous: true}, // mm.dd.yyyy - {in: "3.3.2014"}, + {in: "3.3.2014", err: true, expectAmbiguous: true}, // mm.dd.yy - {in: "08.09.71"}, + {in: "08.09.71", err: true, expectAmbiguous: true}, // mm/dd/yyyy - {in: "3/5/2014"}, + {in: "3/5/2014", err: true, expectAmbiguous: true}, // mm/dd/yy - {in: "08/08/71"}, - {in: "8/8/71"}, + {in: "08/08/71", err: true, expectAmbiguous: true}, + {in: "8/8/71", err: true, expectAmbiguous: true}, // mm/dd/yy hh:mm:ss - {in: "04/02/2014 04:08:09"}, - {in: "4/2/2014 04:08:09"}, + {in: "04/02/2014 04:08:09", err: true, expectAmbiguous: true}, + {in: "4/2/2014 04:08:09", err: true, expectAmbiguous: true}, + {in: `{"hello"}`, err: true}, + {in: "2009-08-12T22:15Z"}, + // https://github.com/araddon/dateparse/issues/91 + {in: "3.31.2014", err: true, expectAmbiguous: true}, + {in: "3.3.2014", err: true, expectAmbiguous: true}, + {in: "03.31.2014", err: true, expectAmbiguous: true}, + {in: "08.21.71", err: true, expectAmbiguous: true}, + {in: "3/31/2014", err: true, expectAmbiguous: true}, + {in: "3/3/2014", err: true, expectAmbiguous: true}, + {in: "03/31/2014", err: true, expectAmbiguous: true}, + {in: "08/21/71", err: true, expectAmbiguous: true}, } func TestParseStrict(t *testing.T) { for _, th := range testParseStrict { - _, err := ParseStrict(th.in) - assert.NotEqual(t, nil, err) + t.Run(th.in, func(t *testing.T) { + _, err := ParseStrict(th.in) + if th.err { + assert.NotEqual(t, nil, err) + if th.expectAmbiguous { + assert.Contains(t, err.Error(), ErrAmbiguousMMDD.Error(), "expected ambiguous") + } + } else { + assert.Equal(t, nil, err) + } + }) } - - _, err := ParseStrict(`{"hello"}`) - assert.NotEqual(t, nil, err) - - _, err = ParseStrict("2009-08-12T22:15Z") - assert.Equal(t, nil, err) } // Lets test to see how this performs using different Timezones/Locations @@ -643,6 +1200,9 @@ func TestInLocation(t *testing.T) { ts = MustParse("Tue, 5 Jul 2017 16:28:13 -0700 (MST)") assert.Equal(t, "2017-07-05 23:28:13 +0000 UTC", fmt.Sprintf("%v", ts.In(time.UTC))) + ts = MustParse("Tue, 5 Jul 2017 16:28:13 +0300 (CEST)") + assert.Equal(t, "2017-07-05 13:28:13 +0000 UTC", fmt.Sprintf("%v", ts.In(time.UTC))) + // Now we are going to use ParseIn() and see that it gives different answer // with different zone, offset time.Local = nil @@ -730,32 +1290,63 @@ func TestPreferMonthFirst(t *testing.T) { ts, err := ParseAny("04/02/2014 04:08:09 +0000 UTC") assert.Equal(t, nil, err) assert.Equal(t, "2014-04-02 04:08:09 +0000 UTC", fmt.Sprintf("%v", ts.In(time.UTC))) + ts, err = ParseAny("4/02/2014 04:08:09 +0000 UTC") + assert.Equal(t, nil, err) + assert.Equal(t, "2014-04-02 04:08:09 +0000 UTC", fmt.Sprintf("%v", ts.In(time.UTC))) + ts, err = ParseAny("04/2/2014 04:08:09 +0000 UTC") + assert.Equal(t, nil, err) + assert.Equal(t, "2014-04-02 04:08:09 +0000 UTC", fmt.Sprintf("%v", ts.In(time.UTC))) preferMonthFirstTrue := PreferMonthFirst(true) ts, err = ParseAny("04/02/2014 04:08:09 +0000 UTC", preferMonthFirstTrue) assert.Equal(t, nil, err) assert.Equal(t, "2014-04-02 04:08:09 +0000 UTC", fmt.Sprintf("%v", ts.In(time.UTC))) + ts, err = ParseAny("4/02/2014 04:08:09 +0000 UTC", preferMonthFirstTrue) + assert.Equal(t, nil, err) + assert.Equal(t, "2014-04-02 04:08:09 +0000 UTC", fmt.Sprintf("%v", ts.In(time.UTC))) + ts, err = ParseAny("04/2/2014 04:08:09 +0000 UTC", preferMonthFirstTrue) + assert.Equal(t, nil, err) + assert.Equal(t, "2014-04-02 04:08:09 +0000 UTC", fmt.Sprintf("%v", ts.In(time.UTC))) // allows the day to be preferred before the month, when completely ambiguous preferMonthFirstFalse := PreferMonthFirst(false) ts, err = ParseAny("04/02/2014 04:08:09 +0000 UTC", preferMonthFirstFalse) assert.Equal(t, nil, err) assert.Equal(t, "2014-02-04 04:08:09 +0000 UTC", fmt.Sprintf("%v", ts.In(time.UTC))) + ts, err = ParseAny("4/02/2014 04:08:09 +0000 UTC", preferMonthFirstFalse) + assert.Equal(t, nil, err) + assert.Equal(t, "2014-02-04 04:08:09 +0000 UTC", fmt.Sprintf("%v", ts.In(time.UTC))) + ts, err = ParseAny("04/2/2014 04:08:09 +0000 UTC", preferMonthFirstFalse) + assert.Equal(t, nil, err) + assert.Equal(t, "2014-02-04 04:08:09 +0000 UTC", fmt.Sprintf("%v", ts.In(time.UTC))) } func TestRetryAmbiguousDateWithSwap(t *testing.T) { // default is false _, err := ParseAny("13/02/2014 04:08:09 +0000 UTC") assert.NotEqual(t, nil, err) + _, err = ParseAny("13/2/2014 04:08:09 +0000 UTC") + assert.NotEqual(t, nil, err) // will fail error if the month preference cannot work due to the value being larger than 12 retryAmbiguousDateWithSwapFalse := RetryAmbiguousDateWithSwap(false) _, err = ParseAny("13/02/2014 04:08:09 +0000 UTC", retryAmbiguousDateWithSwapFalse) assert.NotEqual(t, nil, err) + _, err = ParseAny("13/2/2014 04:08:09 +0000 UTC", retryAmbiguousDateWithSwapFalse) + assert.NotEqual(t, nil, err) // will retry with the other month preference if this error is detected retryAmbiguousDateWithSwapTrue := RetryAmbiguousDateWithSwap(true) ts, err := ParseAny("13/02/2014 04:08:09 +0000 UTC", retryAmbiguousDateWithSwapTrue) assert.Equal(t, nil, err) assert.Equal(t, "2014-02-13 04:08:09 +0000 UTC", fmt.Sprintf("%v", ts.In(time.UTC))) + ts, err = ParseAny("13/2/2014 04:08:09 +0000 UTC", retryAmbiguousDateWithSwapTrue) + assert.Equal(t, nil, err) + assert.Equal(t, "2014-02-13 04:08:09 +0000 UTC", fmt.Sprintf("%v", ts.In(time.UTC))) +} + +// Convenience function for debugging a particular broken test case +func TestDebug(t *testing.T) { + ts := MustParse("September 17, 2012 at 10:09am CEST+02", RetryAmbiguousDateWithSwap(true)) + assert.Equal(t, "2012-09-17 08:09:00 +0000 UTC", fmt.Sprintf("%v", ts.In(time.UTC))) }