flexitime 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +79 -14
- data/Appraisals +4 -0
- data/CHANGELOG.md +8 -3
- data/Gemfile.lock +12 -6
- data/README.md +97 -87
- data/benchmark.rb +5 -40
- data/flexitime.gemspec +1 -0
- data/gemfiles/activesupport_7_0.gemfile +8 -0
- data/lib/flexitime/configuration.rb +0 -12
- data/lib/flexitime/parser.rb +51 -29
- data/lib/flexitime/version.rb +1 -1
- data/lib/flexitime.rb +9 -2
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6dfd029b07e75148e00109b01e4c6c17f09edfb2629dffe091a9003f2c1fb610
|
4
|
+
data.tar.gz: c4290263155f0291fcd2ca897075cb315f76f5a99fc20623042225cef6e3c3e7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c15e333e96e05f99b4dbed931f217be00a5c3832c47dc5adef7e5e76e740cf9fb2576deeece81e86e66cca10ba96c6492a97284bee49f7780c4f9a5a3bfeb039
|
7
|
+
data.tar.gz: '007581d7382a12508c861ca67b7024ce094251d33050467be371bc12ec1399a080f642a4bb0a7ba11b3ac4364a3e4eadc0f80ce0ec796e8781f9ce0f3969b0d0'
|
data/.github/workflows/ci.yml
CHANGED
@@ -17,20 +17,85 @@ jobs:
|
|
17
17
|
strategy:
|
18
18
|
fail-fast: false # should GitHub cancel all in-progress jobs if any matrix job fails
|
19
19
|
matrix:
|
20
|
-
ruby-version:
|
21
|
-
|
22
|
-
|
23
|
-
- '2.
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
- '
|
28
|
-
|
29
|
-
- '
|
30
|
-
|
31
|
-
- '
|
32
|
-
|
33
|
-
- '
|
20
|
+
ruby-version: ['2.5']
|
21
|
+
gemfile: ['activesupport_4_0']
|
22
|
+
include:
|
23
|
+
- ruby-version: '2.5'
|
24
|
+
gemfile: 'activesupport_4_1'
|
25
|
+
- ruby-version: '2.5'
|
26
|
+
gemfile: 'activesupport_4_2'
|
27
|
+
- ruby-version: '2.5'
|
28
|
+
gemfile: 'activesupport_5_0'
|
29
|
+
- ruby-version: '2.5'
|
30
|
+
gemfile: 'activesupport_5_1'
|
31
|
+
- ruby-version: '2.5'
|
32
|
+
gemfile: 'activesupport_5_2'
|
33
|
+
- ruby-version: '2.5'
|
34
|
+
gemfile: 'activesupport_6_0'
|
35
|
+
- ruby-version: '2.5'
|
36
|
+
gemfile: 'activesupport_6_1'
|
37
|
+
- ruby-version: '2.6'
|
38
|
+
gemfile: 'activesupport_4_0'
|
39
|
+
- ruby-version: '2.6'
|
40
|
+
gemfile: 'activesupport_4_1'
|
41
|
+
- ruby-version: '2.6'
|
42
|
+
gemfile: 'activesupport_4_2'
|
43
|
+
- ruby-version: '2.6'
|
44
|
+
gemfile: 'activesupport_5_0'
|
45
|
+
- ruby-version: '2.6'
|
46
|
+
gemfile: 'activesupport_5_1'
|
47
|
+
- ruby-version: '2.6'
|
48
|
+
gemfile: 'activesupport_5_2'
|
49
|
+
- ruby-version: '2.6'
|
50
|
+
gemfile: 'activesupport_6_0'
|
51
|
+
- ruby-version: '2.6'
|
52
|
+
gemfile: 'activesupport_6_1'
|
53
|
+
- ruby-version: '2.7'
|
54
|
+
gemfile: 'activesupport_4_0'
|
55
|
+
- ruby-version: '2.7'
|
56
|
+
gemfile: 'activesupport_4_1'
|
57
|
+
- ruby-version: '2.7'
|
58
|
+
gemfile: 'activesupport_4_2'
|
59
|
+
- ruby-version: '2.7'
|
60
|
+
gemfile: 'activesupport_5_0'
|
61
|
+
- ruby-version: '2.7'
|
62
|
+
gemfile: 'activesupport_5_1'
|
63
|
+
- ruby-version: '2.7'
|
64
|
+
gemfile: 'activesupport_5_2'
|
65
|
+
- ruby-version: '2.7'
|
66
|
+
gemfile: 'activesupport_6_0'
|
67
|
+
- ruby-version: '2.7'
|
68
|
+
gemfile: 'activesupport_6_1'
|
69
|
+
- ruby-version: '2.7'
|
70
|
+
gemfile: 'activesupport_7_0'
|
71
|
+
- ruby-version: '3.0'
|
72
|
+
gemfile: 'activesupport_4_2'
|
73
|
+
- ruby-version: '3.0'
|
74
|
+
gemfile: 'activesupport_5_0'
|
75
|
+
- ruby-version: '3.0'
|
76
|
+
gemfile: 'activesupport_5_1'
|
77
|
+
- ruby-version: '3.0'
|
78
|
+
gemfile: 'activesupport_5_2'
|
79
|
+
- ruby-version: '3.0'
|
80
|
+
gemfile: 'activesupport_6_0'
|
81
|
+
- ruby-version: '3.0'
|
82
|
+
gemfile: 'activesupport_6_1'
|
83
|
+
- ruby-version: '3.0'
|
84
|
+
gemfile: 'activesupport_7_0'
|
85
|
+
- ruby-version: '3.1'
|
86
|
+
gemfile: 'activesupport_4_2'
|
87
|
+
- ruby-version: '3.1'
|
88
|
+
gemfile: 'activesupport_5_0'
|
89
|
+
- ruby-version: '3.1'
|
90
|
+
gemfile: 'activesupport_5_1'
|
91
|
+
- ruby-version: '3.1'
|
92
|
+
gemfile: 'activesupport_5_2'
|
93
|
+
- ruby-version: '3.1'
|
94
|
+
gemfile: 'activesupport_6_0'
|
95
|
+
- ruby-version: '3.1'
|
96
|
+
gemfile: 'activesupport_6_1'
|
97
|
+
- ruby-version: '3.1'
|
98
|
+
gemfile: 'activesupport_7_0'
|
34
99
|
|
35
100
|
env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
|
36
101
|
BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile
|
data/Appraisals
CHANGED
@@ -11,6 +11,10 @@
|
|
11
11
|
# Run each appraisal in turn or a single appraisal:-
|
12
12
|
# $ bundle exec appraisal rspec
|
13
13
|
# $ bundle exec appraisal activesupport-6-1 rspec
|
14
|
+
appraise "activesupport-7-0" do
|
15
|
+
gem "activesupport", "~> 7.0"
|
16
|
+
end
|
17
|
+
|
14
18
|
appraise "activesupport-6-1" do
|
15
19
|
gem "activesupport", "~> 6.1"
|
16
20
|
end
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,10 @@
|
|
1
|
-
|
1
|
+
# Change Log
|
2
2
|
|
3
|
-
##
|
3
|
+
## 1.0.0 - 2022-03-17
|
4
4
|
|
5
|
-
|
5
|
+
* Perform parsing using `Time.zone` ([d30b224](https://github.com/CircleSD/flexitime/commit/d30b224f40379331473f60d11f6dcc8b1875413d))
|
6
|
+
* Add `first_date_part` and `precision` arguments to `parse` method ([18ce552](https://github.com/CircleSD/flexitime/commit/18ce5528caf2f1c6d2643b610a713e7d2cd6ce75))
|
7
|
+
|
8
|
+
## 0.1.0 - 2021-11-22
|
9
|
+
|
10
|
+
* Initial release
|
data/Gemfile.lock
CHANGED
@@ -1,18 +1,17 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
flexitime (
|
4
|
+
flexitime (1.0.0)
|
5
5
|
activesupport (>= 4.0)
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: https://rubygems.org/
|
9
9
|
specs:
|
10
|
-
activesupport (
|
10
|
+
activesupport (7.0.2.3)
|
11
11
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
12
12
|
i18n (>= 1.6, < 2)
|
13
13
|
minitest (>= 5.1)
|
14
14
|
tzinfo (~> 2.0)
|
15
|
-
zeitwerk (~> 2.3)
|
16
15
|
appraisal (2.4.1)
|
17
16
|
bundler
|
18
17
|
rake
|
@@ -20,9 +19,10 @@ GEM
|
|
20
19
|
ast (2.4.2)
|
21
20
|
concurrent-ruby (1.1.9)
|
22
21
|
diff-lcs (1.4.4)
|
23
|
-
|
22
|
+
docile (1.4.0)
|
23
|
+
i18n (1.10.0)
|
24
24
|
concurrent-ruby (~> 1.0)
|
25
|
-
minitest (5.
|
25
|
+
minitest (5.15.0)
|
26
26
|
parallel (1.21.0)
|
27
27
|
parser (3.0.2.0)
|
28
28
|
ast (~> 2.4.1)
|
@@ -58,6 +58,12 @@ GEM
|
|
58
58
|
rubocop (>= 1.7.0, < 2.0)
|
59
59
|
rubocop-ast (>= 0.4.0)
|
60
60
|
ruby-progressbar (1.11.0)
|
61
|
+
simplecov (0.21.2)
|
62
|
+
docile (~> 1.1)
|
63
|
+
simplecov-html (~> 0.11)
|
64
|
+
simplecov_json_formatter (~> 0.1)
|
65
|
+
simplecov-html (0.12.3)
|
66
|
+
simplecov_json_formatter (0.1.4)
|
61
67
|
standard (1.4.0)
|
62
68
|
rubocop (= 1.22.3)
|
63
69
|
rubocop-performance (= 1.11.5)
|
@@ -65,7 +71,6 @@ GEM
|
|
65
71
|
tzinfo (2.0.4)
|
66
72
|
concurrent-ruby (~> 1.0)
|
67
73
|
unicode-display_width (2.1.0)
|
68
|
-
zeitwerk (2.5.1)
|
69
74
|
|
70
75
|
PLATFORMS
|
71
76
|
arm64-darwin-21
|
@@ -75,6 +80,7 @@ DEPENDENCIES
|
|
75
80
|
flexitime!
|
76
81
|
rake (~> 13.0)
|
77
82
|
rspec (~> 3.0)
|
83
|
+
simplecov
|
78
84
|
standard
|
79
85
|
|
80
86
|
BUNDLED WITH
|
data/README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# Flexitime
|
2
2
|
|
3
|
-
Flexitime is a Ruby date/time string parser with the intended purpose of converting a string value received from a UI or API into
|
3
|
+
Flexitime is a Ruby date/time string parser with the intended purpose of converting a string value received from a UI or API into an [ActiveSupport::TimeWithZone](https://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html) object. It offers the flexibility of deciphering date/time strings in the most common formats with the ability to self-determine the expected order of the date parts when using [rails-i18n](https://github.com/svenfuchs/rails-i18n). The date order and a desired precision for the time object can also be set via configuration options or method arguments.
|
4
4
|
|
5
|
-
The gem was born of the need to parse date, datetime & time strings in a multi-user environment supporting different locales and time zones. Depending upon the user's locale the UI would return date/time strings in different formats and in different orders (day/month/year or month/day/year). This variation in the ordering of the day and month parts proved to be the main catalyst to finding or creating a date/time parser. The resultant
|
5
|
+
The gem was born of the need to parse date, datetime & time strings in a multi-user environment supporting different locales and time zones. Depending upon the user's locale the UI would return date/time strings in different formats and in different orders (day/month/year or month/day/year). This variation in the ordering of the day and month parts proved to be the main catalyst to finding or creating a date/time parser. The resultant time object needed to be created in the user's time zone and additionally the system stored times only to a minute precision. Flexitime was created to provide a simple yet flexible parser to meet these needs.
|
6
6
|
|
7
7
|
![Build Status](https://github.com/CircleSD/flexitime/actions/workflows/ci.yml/badge.svg?branch=main)
|
8
8
|
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
|
@@ -29,22 +29,107 @@ gem install flexitime
|
|
29
29
|
|
30
30
|
## Usage
|
31
31
|
|
32
|
-
The Flexitime `parse` method accepts a string argument or an object that implements the `to_str` method to denote that it behaves like a string. When the string is recognised as a valid date, datetime or time it returns
|
32
|
+
The Flexitime `parse` method accepts a string argument or an object that implements the `to_str` method (to denote that it behaves like a string). When the string is recognised as a valid date, datetime or time it returns an [ActiveSupport::TimeWithZone](https://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html) object. If any of the date/time parts are invalid or the string does not match a recognised format the method returns `nil`.
|
33
|
+
|
34
|
+
As Flexitime uses the [TimeZone](https://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html) class, `Time.zone` should be set before using the `parse` method. If `Time.zone` is `nil` Flexitime will set the zone to UTC.
|
33
35
|
|
34
36
|
```ruby
|
35
|
-
|
36
|
-
Flexitime.parse("23/08/2021
|
37
|
-
Flexitime.parse("08:00")
|
37
|
+
Time.zone = "London"
|
38
|
+
Flexitime.parse("23/08/2021") # => Mon, 23 Aug 2021 00:00:00.000000000 BST +01:00
|
39
|
+
Flexitime.parse("23/08/2021 08:00") # => Mon, 23 Aug 2021 08:00:00.000000000 BST +01:00
|
40
|
+
Flexitime.parse("08:00") # => Mon, 14 Mar 2022 08:00:00.000000000 GMT +00:00
|
38
41
|
```
|
39
42
|
|
40
43
|
```ruby
|
44
|
+
Time.zone = "London"
|
41
45
|
Flexitime.parse("31/02/2021 08:00") # => nil
|
42
46
|
Flexitime.parse("computer says no") # => nil
|
43
47
|
```
|
44
48
|
|
49
|
+
### First Date Part
|
50
|
+
|
51
|
+
The `first_date_part` option is used to denote the first part of the date within the string, either `:day` or `:month`. When using the [rails-I18n gem](https://github.com/svenfuchs/rails-i18n) if the first element of the "date.order" translation is :day or :month that will be used. Alternatively the option can be set manually via a configuration option or a `parse` argument. If the option has not been set then the default of `:day` is used.
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
Time.zone = "London"
|
55
|
+
I18n.locale = :"en-GB"
|
56
|
+
I18n.t("date.order") # => [:day, :month, :year]
|
57
|
+
Flexitime.parse("01/06/2021") # => Tue, 01 Jun 2021 00:00:00.000000000 BST +01:00
|
58
|
+
|
59
|
+
I18n.locale = :"en-US"
|
60
|
+
I18n.t("date.order") # => [:month, :day, :year]
|
61
|
+
Flexitime.parse("01/06/2021") # => Wed, 06 Jan 2021 00:00:00.000000000 GMT +00:00
|
62
|
+
```
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
Time.zone = "London"
|
66
|
+
Flexitime.first_date_part = :day
|
67
|
+
Flexitime.parse("01/06/2021") # => Tue, 01 Jun 2021 00:00:00.000000000 BST +01:00
|
68
|
+
|
69
|
+
Flexitime.first_date_part = :month
|
70
|
+
Flexitime.parse("01/06/2021") # => Wed, 06 Jan 2021 00:00:00.000000000 GMT +00:00
|
71
|
+
```
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
Time.zone = "London"
|
75
|
+
Flexitime.parse("01/06/2021", first_date_part: :day) # => Tue, 01 Jun 2021 00:00:00.000000000 BST +01:00
|
76
|
+
Flexitime.parse("01/06/2021", first_date_part: :month) # => Wed, 06 Jan 2021 00:00:00.000000000 GMT +00:00
|
77
|
+
```
|
78
|
+
|
79
|
+
Regardless of the `first_date_part` value, Flexitime will always attempt to parse a string starting with a 4 digit year, deciphering the format as year/month/day.
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
Time.zone = "London"
|
83
|
+
Flexitime.parse("2021-11-12 08:15") # => Fri, 12 Nov 2021 08:15:00.000000000 GMT +00:00
|
84
|
+
```
|
85
|
+
|
86
|
+
### Precision
|
87
|
+
|
88
|
+
The `precision` option denotes the desired precision for the returned time objects. This defaults to minute (`:min`) meaning that the time object will be returned without seconds. The option can be set via a configuration option or a `parse` argument and the accepted values are `:day`, `:hour`, `:min`, `:sec` or `:usec`
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
Time.zone = "London"
|
92
|
+
Flexitime.precision = :day
|
93
|
+
Flexitime.parse("2022-12-01T18:30:45.036711Z") # => Thu, 01 Dec 2022 00:00:00.000000000 GMT +00:00
|
94
|
+
Flexitime.precision = :hour
|
95
|
+
Flexitime.parse("2022-12-01T18:30:45.036711Z") # => Thu, 01 Dec 2022 18:00:00.000000000 GMT +00:00
|
96
|
+
Flexitime.precision = :min
|
97
|
+
Flexitime.parse("2022-12-01T18:30:45.036711Z") # => Thu, 01 Dec 2022 18:30:00.000000000 GMT +00:00
|
98
|
+
Flexitime.precision = :sec
|
99
|
+
Flexitime.parse("2022-12-01T18:30:45.036711Z") # => Thu, 01 Dec 2022 18:30:45.000000000 GMT +00:00
|
100
|
+
Flexitime.precision = :usec
|
101
|
+
Flexitime.parse("2022-12-01T18:30:45.036711Z") # => Thu, 01 Dec 2022 18:30:45.036711000 GMT +00:00
|
102
|
+
|
103
|
+
Flexitime.parse("2022-12-01T18:30:45.036711Z", precision: :day) # => Thu, 01 Dec 2022 00:00:00.000000000 GMT +00:00
|
104
|
+
Flexitime.parse("2022-12-01T18:30:45.036711Z", precision: :hour) # => Thu, 01 Dec 2022 18:00:00.000000000 GMT +00:00
|
105
|
+
Flexitime.parse("2022-12-01T18:30:45.036711Z", precision: :min) # => Thu, 01 Dec 2022 18:30:00.000000000 GMT +00:00
|
106
|
+
Flexitime.parse("2022-12-01T18:30:45.036711Z", precision: :sec) # => Thu, 01 Dec 2022 18:30:45.000000000 GMT +00:00
|
107
|
+
Flexitime.parse("2022-12-01T18:30:45.036711Z", precision: :usec) # => Thu, 01 Dec 2022 18:30:45.036711000 GMT +00:00
|
108
|
+
```
|
109
|
+
|
110
|
+
### Ambiguous Year Future Bias
|
111
|
+
|
112
|
+
The `ambiguous_year_future_bias` configuration option is used to determine the century when parsing a string containing a 2 digit year and defaults to 50.
|
113
|
+
|
114
|
+
With a bias of 50, when the current year is 2020 the time object's year is set from within a range of >= 1970 and <= 2069
|
115
|
+
|
116
|
+
With a bias of 20, when the current year is 2020 the time object's year is set from within a range of >= 2000 and <= 2099
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
Time.zone = "London"
|
120
|
+
Flexitime.parse("01/08/00").year # => 2000
|
121
|
+
Flexitime.parse("01/08/71").year # => 2071
|
122
|
+
Flexitime.parse("01/08/72").year # => 1972
|
123
|
+
Flexitime.parse("01/08/99").year # => 1999
|
124
|
+
```
|
125
|
+
|
126
|
+
### Thread Safety
|
127
|
+
|
128
|
+
The Flexitime `configuration` instance is stored in the currently executing thread as a thread-local variable in order to avoid conflicts with concurrent requests and make the variable threadsafe.
|
129
|
+
|
45
130
|
## Formats
|
46
131
|
|
47
|
-
Flexitime uses regular expressions to match the string to the most common date & time formats and uses the matched parts to create a
|
132
|
+
Flexitime uses regular expressions to match the string to the most common date & time formats and uses the matched parts to create a time object.
|
48
133
|
|
49
134
|
Dates separated by either forward slashes `/` hyphens `-` or periods `.`; with 1 or 2 digit days and months; with 2 or 4 digit years; and with day/month or year first (always a 4 digit year).
|
50
135
|
|
@@ -87,89 +172,14 @@ Also times in the ISO 8601 Zulu format with between 1 and 6 digit milliseconds
|
|
87
172
|
2021-08-01T08:15:30.144515Z
|
88
173
|
```
|
89
174
|
|
90
|
-
If the string does not match any of the regular expressions then Flexitime will attempt to parse the string using the
|
175
|
+
If the string does not match any of the regular expressions then Flexitime will attempt to parse the string using the TimeZone class, so you lose nothing from using Flexitime and it will still return a time object for a string containing, for example, an offset or words.
|
91
176
|
|
92
177
|
```ruby
|
93
|
-
|
94
|
-
Flexitime.parse("
|
178
|
+
Time.zone = "Kyiv"
|
179
|
+
Flexitime.parse("2022-02-24T09:00:00+02:00") # => Thu, 24 Feb 2022 09:00:00.000000000 EET +02:00
|
180
|
+
Flexitime.parse("2nd January 2021") # => Sat, 02 Jan 2021 00:00:00.000000000 EET +02:00
|
95
181
|
```
|
96
182
|
|
97
|
-
## Configuration
|
98
|
-
|
99
|
-
### Time Class
|
100
|
-
|
101
|
-
The Flexitime `time_class` configuration option is used to create Time objects and defaults to the `Time` class which will use the system local time zone. The `time_class` can be set to an ActiveSupport [TimeZone](https://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html) to create Time objects using the specified time zone.
|
102
|
-
|
103
|
-
```ruby
|
104
|
-
Time.zone = "Europe/London"
|
105
|
-
Flexitime.time_class = Time.zone
|
106
|
-
Flexitime.parse("01/06/2021 17:00") # => Tue, 01 Jun 2021 17:00:00.000000000 BST +01:00
|
107
|
-
```
|
108
|
-
|
109
|
-
### First Date Part
|
110
|
-
|
111
|
-
The Flexitime `first_date_part` configuration option is used to denote the first part of the date within the string, either `:day` or `:month`. When using the [rails-I18n gem](https://github.com/svenfuchs/rails-i18n) if the first element of the "date.order" translation is :day or :month that will be used. Alternatively the option can be set manually or finally the default of `:day` will be used.
|
112
|
-
|
113
|
-
```ruby
|
114
|
-
I18n.locale = :"en-GB"
|
115
|
-
I18n.t("date.order") # => [:day, :month, :year]
|
116
|
-
Flexitime.parse("01/06/2021") # => 2021-06-01 00:00:00 +0100
|
117
|
-
|
118
|
-
I18n.locale = :"en-US"
|
119
|
-
I18n.t("date.order") # => [:month, :day, :year]
|
120
|
-
Flexitime.parse("01/06/2021") # => 2021-01-06 00:00:00 +0100
|
121
|
-
```
|
122
|
-
|
123
|
-
```ruby
|
124
|
-
Flexitime.first_date_part = :day
|
125
|
-
Flexitime.parse("01/06/2021") # => 2021-06-01 00:00:00 +0100
|
126
|
-
|
127
|
-
Flexitime.first_date_part = :month
|
128
|
-
Flexitime.parse("01/06/2021") # => 2021-01-06 00:00:00 +0100
|
129
|
-
```
|
130
|
-
|
131
|
-
Regardless of the `first_date_part` value, Flexitime will always attempt to parse a string starting with a 4 digit year, deciphering the format as year/month/day.
|
132
|
-
|
133
|
-
```ruby
|
134
|
-
Flexitime.parse("2021-11-12 08:15") # => 2021-11-12 08:15:00 +0000
|
135
|
-
```
|
136
|
-
|
137
|
-
### Precision
|
138
|
-
|
139
|
-
The Flexitime `precision` configuration option denotes the desired precision for the returned Time objects. This defaults to minute (`:min`) meaning that the Time object will be returned without seconds. The accepted values are `:day`, `:hour`, `:min`, `:sec` or `:usec`
|
140
|
-
|
141
|
-
```ruby
|
142
|
-
Flexitime.precision = :day
|
143
|
-
Flexitime.parse("2022-12-01T18:30:45.036711Z") # => 2022-12-01 00:00:00 +0000
|
144
|
-
Flexitime.precision = :hour
|
145
|
-
Flexitime.parse("2022-12-01T18:30:45.036711Z") # => 2022-12-01 18:00:00 +0000
|
146
|
-
Flexitime.precision = :min
|
147
|
-
Flexitime.parse("2022-12-01T18:30:45.036711Z") # => 2022-12-01 18:30:00 +0000
|
148
|
-
Flexitime.precision = :sec
|
149
|
-
Flexitime.parse("2022-12-01T18:30:45.036711Z") # => 2022-12-01 18:30:45 +0000
|
150
|
-
Flexitime.precision = :usec
|
151
|
-
Flexitime.parse("2022-12-01T18:30:45.036711Z") # => 2022-12-01 18:30:45.036711 +0000
|
152
|
-
```
|
153
|
-
|
154
|
-
### Ambiguous Year Future Bias
|
155
|
-
|
156
|
-
The Flexitime `ambiguous_year_future_bias` configuration option is used to determine the century when parsing a string containing a 2 digit year and defaults to 50.
|
157
|
-
|
158
|
-
With a bias of 50, when the current year is 2020 the Time object's year is set from within a range of >= 1970 and <= 2069
|
159
|
-
|
160
|
-
With a bias of 20, when the current year is 2020 the Time object's year is set from within a range of >= 2000 and <= 2099
|
161
|
-
|
162
|
-
```ruby
|
163
|
-
Flexitime.parse("01/08/00").year # => 2000
|
164
|
-
Flexitime.parse("01/08/69").year # => 2069
|
165
|
-
Flexitime.parse("01/08/70").year # => 1970
|
166
|
-
Flexitime.parse("01/08/99").year # => 1999
|
167
|
-
```
|
168
|
-
|
169
|
-
### Thread Safety
|
170
|
-
|
171
|
-
The Flexitime `configuration` instance is stored in the currently executing thread as a thread-local variable in order to avoid conflicts with concurrent requests and make the variable threadsafe.
|
172
|
-
|
173
183
|
## Development
|
174
184
|
|
175
185
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -190,7 +200,7 @@ Bug reports and pull requests are welcome on [GitHub](https://github.com/CircleS
|
|
190
200
|
|
191
201
|
## Credits
|
192
202
|
|
193
|
-
Tom Preston-Werner and [Chronic](https://github.com/mojombo/chronic) which was our previous go-to parser of choice but is unfortunately no longer maintained and performs natural language parsing that we do not require. It was a useful reference in particular for the `
|
203
|
+
Tom Preston-Werner and [Chronic](https://github.com/mojombo/chronic) which was our previous go-to parser of choice but is unfortunately no longer maintained and performs natural language parsing that we do not require. It was a useful reference in particular for the `ambiguous_year_future_bias` configuration options.
|
194
204
|
|
195
205
|
Adam Meehan and [Timeliness](https://github.com/adzap/timeliness) which was a close match for our needs but proved to be too strict in its accepted formats particulary as we wanted to cater for a variety of date separators for both day/month/year and month/day/year date ordering. The gem provided some very useful inspiration in regards to code structure with forwarding for the configuration class and testing thread safety.
|
196
206
|
|
data/benchmark.rb
CHANGED
@@ -25,14 +25,7 @@ Benchmark.bm(50) do |benchmark|
|
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
|
-
benchmark.report("Flexitime.parse
|
29
|
-
n.times do
|
30
|
-
Flexitime.parse("2021-08-12 12:30")
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
benchmark.report("Flexitime.parse (with Time.zone) datetime YMD") do
|
35
|
-
Flexitime.time_class = Time.zone
|
28
|
+
benchmark.report("Flexitime.parse datetime YMD") do
|
36
29
|
n.times do
|
37
30
|
Flexitime.parse("2021-08-12 12:30")
|
38
31
|
end
|
@@ -52,14 +45,7 @@ Benchmark.bm(50) do |benchmark|
|
|
52
45
|
end
|
53
46
|
end
|
54
47
|
|
55
|
-
benchmark.report("Flexitime.parse
|
56
|
-
n.times do
|
57
|
-
Flexitime.parse("23/08/2021 12:30")
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
benchmark.report("Flexitime.parse (with Time.zone) datetime DMY HM") do
|
62
|
-
Flexitime.time_class = Time.zone
|
48
|
+
benchmark.report("Flexitime.parse datetime DMY HM") do
|
63
49
|
n.times do
|
64
50
|
Flexitime.parse("23/08/2021 12:30")
|
65
51
|
end
|
@@ -79,14 +65,7 @@ Benchmark.bm(50) do |benchmark|
|
|
79
65
|
end
|
80
66
|
end
|
81
67
|
|
82
|
-
benchmark.report("Flexitime.parse
|
83
|
-
n.times do
|
84
|
-
Flexitime.parse("23/08/2021 12:30:45")
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
benchmark.report("Flexitime.parse (with Time.zone) datetime DMY HMS") do
|
89
|
-
Flexitime.time_class = Time.zone
|
68
|
+
benchmark.report("Flexitime.parse datetime DMY HMS") do
|
90
69
|
n.times do
|
91
70
|
Flexitime.parse("23/08/2021 12:30:45")
|
92
71
|
end
|
@@ -106,14 +85,7 @@ Benchmark.bm(50) do |benchmark|
|
|
106
85
|
end
|
107
86
|
end
|
108
87
|
|
109
|
-
benchmark.report("Flexitime.parse
|
110
|
-
n.times do
|
111
|
-
Flexitime.parse("12:30")
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
benchmark.report("Flexitime.parse (with Time.zone) time HM") do
|
116
|
-
Flexitime.time_class = Time.zone
|
88
|
+
benchmark.report("Flexitime.parse time HM") do
|
117
89
|
n.times do
|
118
90
|
Flexitime.parse("12:30")
|
119
91
|
end
|
@@ -133,14 +105,7 @@ Benchmark.bm(50) do |benchmark|
|
|
133
105
|
end
|
134
106
|
end
|
135
107
|
|
136
|
-
benchmark.report("Flexitime.parse
|
137
|
-
n.times do
|
138
|
-
Flexitime.parse("12:30:45")
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
|
-
benchmark.report("Flexitime.parse (with Time.zone) time HMS") do
|
143
|
-
Flexitime.time_class = Time.zone
|
108
|
+
benchmark.report("Flexitime.parse time HMS") do
|
144
109
|
n.times do
|
145
110
|
Flexitime.parse("12:30:45")
|
146
111
|
end
|
data/flexitime.gemspec
CHANGED
@@ -44,6 +44,7 @@ Gem::Specification.new do |spec|
|
|
44
44
|
spec.add_development_dependency "rake", "~> 13.0"
|
45
45
|
spec.add_development_dependency "rspec", "~> 3.0"
|
46
46
|
spec.add_development_dependency "appraisal"
|
47
|
+
spec.add_development_dependency "simplecov"
|
47
48
|
|
48
49
|
# For more information and examples about making a new gem, checkout our
|
49
50
|
# guide at: https://bundler.io/guides/creating_gem.html
|
@@ -7,16 +7,6 @@ module Flexitime
|
|
7
7
|
class Configuration
|
8
8
|
# = Configuration options
|
9
9
|
#
|
10
|
-
# == time_class
|
11
|
-
# The time class used to create the Time object
|
12
|
-
# defaulting to Time which will use the system local time zone.
|
13
|
-
# This can be set to ActiveSupport::TimeZone to create a Time object
|
14
|
-
# using the specified time zone
|
15
|
-
#
|
16
|
-
# Time.zone = "Europe/London"
|
17
|
-
# Flexitime.time_class = Time.zone
|
18
|
-
# Flexitime.parse("01/06/2021 17:00") # => Tue, 01 Jun 2021 17:00:00.000000000 BST +01:00
|
19
|
-
#
|
20
10
|
# == first_date_part
|
21
11
|
# The first part of the date within the string, either :day or :month
|
22
12
|
# This can be set manually otherwise when using the rails-I18n gem if the first element
|
@@ -55,12 +45,10 @@ module Flexitime
|
|
55
45
|
# Flexitime.parse("01/08/70").year # => 1970
|
56
46
|
# Flexitime.parse("01/08/99").year # => 1999
|
57
47
|
#
|
58
|
-
attr_accessor :time_class
|
59
48
|
attr_reader :precision
|
60
49
|
attr_accessor :ambiguous_year_future_bias
|
61
50
|
|
62
51
|
def initialize
|
63
|
-
@time_class = ::Time
|
64
52
|
@first_date_part = nil
|
65
53
|
@precision = :min
|
66
54
|
@ambiguous_year_future_bias = 50
|
data/lib/flexitime/parser.rb
CHANGED
@@ -22,37 +22,56 @@ module Flexitime
|
|
22
22
|
# that use a period with hours & minutes
|
23
23
|
HOUR_MINUTE_REGEX = %r{^(\d{1,2})[:.](\d{2})\s?([aApP][mM])?\.?$}
|
24
24
|
|
25
|
-
# Parse a String argument and create
|
26
|
-
#
|
25
|
+
# Parse a String argument and create an ActiveSupport::TimeWithZone object using Time.zone
|
26
|
+
#
|
27
|
+
# The parse uses either the argument or configuration first_date_part (:day or :month)
|
28
|
+
# and either the argument or configuration precision (:day, :hour, :min, :sec or :usec).
|
27
29
|
#
|
28
30
|
# When the String contains a date or date/time or time that matches the regular expressions
|
29
|
-
# the
|
30
|
-
# otherwise the
|
31
|
+
# the Time.zone local method is used to create a TimeWithZone object
|
32
|
+
# otherwise the Time.zone parse method is used to create a TimeWithZone object
|
31
33
|
# and for an invalid date, date/time or time nil is returned.
|
32
|
-
def parse(str)
|
34
|
+
def parse(str, first_date_part: nil, precision: nil)
|
35
|
+
validate_options(first_date_part: first_date_part, precision: precision)
|
36
|
+
|
33
37
|
str = str.is_a?(String) ? str : str.try(:to_str)
|
34
38
|
return nil if str.blank?
|
35
39
|
|
36
|
-
parts = extract_parts(str)
|
40
|
+
parts = extract_parts(str, first_date_part: first_date_part)
|
37
41
|
|
38
42
|
if parts.present?
|
39
|
-
create_time_from_parts(parts) if valid_date_parts?(parts)
|
43
|
+
create_time_from_parts(parts, precision: precision) if valid_date_parts?(parts)
|
40
44
|
else
|
41
|
-
|
45
|
+
time_zone_parse(str, precision: precision)
|
42
46
|
end
|
43
47
|
end
|
44
48
|
|
45
49
|
private
|
46
50
|
|
51
|
+
# Returns an ActiveSupport::TimeZone
|
52
|
+
def time_zone
|
53
|
+
Time.zone ||= "UTC"
|
54
|
+
Time.zone
|
55
|
+
end
|
56
|
+
|
57
|
+
# Ensure the parse method options are valid
|
58
|
+
# reusing the configuration class validation which raises an exception
|
59
|
+
def validate_options(first_date_part: nil, precision: nil)
|
60
|
+
return if first_date_part.blank? && precision.blank?
|
61
|
+
config = Configuration.new
|
62
|
+
config.first_date_part = first_date_part if first_date_part.present?
|
63
|
+
config.precision = precision if precision.present?
|
64
|
+
end
|
65
|
+
|
47
66
|
# Extract date and time parts and return a Hash containing the parts
|
48
67
|
# or nil if either the date or time string does not match a regex
|
49
|
-
def extract_parts(str)
|
68
|
+
def extract_parts(str, first_date_part: nil)
|
50
69
|
date_str, time_str = separate_date_and_time(str)
|
51
70
|
|
52
|
-
date_parts = extract_date_parts(date_str)
|
71
|
+
date_parts = extract_date_parts(date_str, first_date_part: first_date_part)
|
53
72
|
|
54
73
|
if date_parts.blank?
|
55
|
-
now =
|
74
|
+
now = time_zone.now
|
56
75
|
date_parts = {year: now.year, month: now.month, day: now.day}
|
57
76
|
time_str = str.strip
|
58
77
|
end
|
@@ -71,8 +90,8 @@ module Flexitime
|
|
71
90
|
[parts.shift, parts.join(" ")]
|
72
91
|
end
|
73
92
|
|
74
|
-
def extract_date_parts(str)
|
75
|
-
extract_iso_date_parts(str) || extract_local_date_parts(str)
|
93
|
+
def extract_date_parts(str, first_date_part: nil)
|
94
|
+
extract_iso_date_parts(str) || extract_local_date_parts(str, first_date_part: first_date_part)
|
76
95
|
end
|
77
96
|
|
78
97
|
def extract_iso_date_parts(str)
|
@@ -83,11 +102,12 @@ module Flexitime
|
|
83
102
|
end
|
84
103
|
end
|
85
104
|
|
86
|
-
def extract_local_date_parts(str)
|
105
|
+
def extract_local_date_parts(str, first_date_part: nil)
|
87
106
|
# match array returns ["23-08-1973", "23", "08", "1973"]
|
88
107
|
parts = str.match(LOCAL_DATE_REGEX).to_a.pop(3).map(&:to_i)
|
89
108
|
if parts.present?
|
90
|
-
|
109
|
+
first_date_part ||= Flexitime.configuration.first_date_part
|
110
|
+
day, month = first_date_part == :day ? [parts.first, parts.second] : [parts.second, parts.first]
|
91
111
|
{year: make_year(parts.third), month: month, day: day}
|
92
112
|
end
|
93
113
|
end
|
@@ -124,7 +144,7 @@ module Flexitime
|
|
124
144
|
def make_year(year)
|
125
145
|
return year if year.to_s.size > 2
|
126
146
|
|
127
|
-
start_year =
|
147
|
+
start_year = time_zone.now.year - Flexitime.configuration.ambiguous_year_future_bias
|
128
148
|
century = (start_year / 100) * 100
|
129
149
|
full_year = century + year
|
130
150
|
full_year < start_year ? full_year + 100 : full_year
|
@@ -135,38 +155,40 @@ module Flexitime
|
|
135
155
|
meridiem.to_s.downcase == "pm" ? hour + 12 : hour
|
136
156
|
end
|
137
157
|
|
138
|
-
# Validate the day & month parts as Time#local accepts some invalid values
|
139
|
-
# such as Time.local(2021,2,30) returning "2021-03-02"
|
158
|
+
# Validate the day & month parts as Time.zone#local accepts some invalid values
|
159
|
+
# such as Time.zone.local(2021,2,30) returning "2021-03-02"
|
140
160
|
def valid_date_parts?(parts)
|
141
161
|
parts[:month] >= 1 && parts[:month] <= 12 && parts[:day] >= 1 &&
|
142
162
|
parts[:day] <= Time.days_in_month(parts[:month], parts[:year])
|
143
163
|
end
|
144
164
|
|
145
|
-
# Create a
|
146
|
-
def create_time_from_parts(parts)
|
147
|
-
time =
|
165
|
+
# Create a TimeWithZone object object using only those parts required for the configuration precision
|
166
|
+
def create_time_from_parts(parts, precision: nil)
|
167
|
+
time = time_zone.local(*local_args_for_precision(parts, precision: precision))
|
148
168
|
parts[:utc] ? (time + time.utc_offset) : time
|
149
169
|
rescue
|
150
170
|
end
|
151
171
|
|
152
172
|
# Returns the date/time parts required for the configuration precision
|
153
|
-
def local_args_for_precision(parts)
|
173
|
+
def local_args_for_precision(parts, precision: nil)
|
154
174
|
keys = [:year, :month, :day, :hour, :min, :sec, :usec]
|
155
|
-
|
175
|
+
precision ||= Flexitime.configuration.precision
|
176
|
+
index = keys.index(precision)
|
156
177
|
keys[0..index].map { |key| parts[key] }
|
157
178
|
end
|
158
179
|
|
159
|
-
# Parse the string using
|
160
|
-
def
|
161
|
-
time =
|
162
|
-
set_precision(time)
|
180
|
+
# Parse the string using Time.zone and set the configuration precision
|
181
|
+
def time_zone_parse(str, precision: nil)
|
182
|
+
time = time_zone.parse(str)
|
183
|
+
set_precision(time, precision: precision)
|
163
184
|
rescue
|
164
185
|
end
|
165
186
|
|
166
187
|
# Set the precision, first checking if this is necessary
|
167
188
|
# to avoid the overhead of calling the change method
|
168
|
-
def set_precision(time)
|
169
|
-
|
189
|
+
def set_precision(time, precision: nil)
|
190
|
+
precision ||= Flexitime.configuration.precision
|
191
|
+
index = PRECISIONS.index(precision)
|
170
192
|
dismiss_part = PRECISIONS[index + 1]
|
171
193
|
excess = PRECISIONS[(index + 1)..-1].sum { |key| time.send(key) }
|
172
194
|
excess > 0 ? time.change(dismiss_part => 0) : time
|
data/lib/flexitime/version.rb
CHANGED
data/lib/flexitime.rb
CHANGED
@@ -5,6 +5,13 @@ require "forwardable"
|
|
5
5
|
|
6
6
|
# Active Support Core Extensions
|
7
7
|
# https://guides.rubyonrails.org/active_support_core_extensions.html
|
8
|
+
begin
|
9
|
+
# workaround for activesupport 7.0
|
10
|
+
# ref: https://github.com/rails/rails/issues/43851
|
11
|
+
require "active_support/isolated_execution_state"
|
12
|
+
rescue LoadError
|
13
|
+
end
|
14
|
+
require "active_support/time" # Time.zone
|
8
15
|
require "active_support/core_ext/object/blank" # blank? and present?
|
9
16
|
require "active_support/core_ext/array/access" # array second/third/fourth
|
10
17
|
require "active_support/core_ext/time/calculations" # Time.days_in_month
|
@@ -16,8 +23,8 @@ require_relative "flexitime/parser"
|
|
16
23
|
module Flexitime
|
17
24
|
class << self
|
18
25
|
extend Forwardable
|
19
|
-
def_delegators :configuration, :
|
20
|
-
def_delegators :configuration, :
|
26
|
+
def_delegators :configuration, :first_date_part, :precision, :ambiguous_year_future_bias
|
27
|
+
def_delegators :configuration, :first_date_part=, :precision=, :ambiguous_year_future_bias=
|
21
28
|
|
22
29
|
include Flexitime::Parser
|
23
30
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: flexitime
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Hilton
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-03-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: simplecov
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
69
83
|
description: Ruby date/time string parser for common formats and different date orders
|
70
84
|
email:
|
71
85
|
- 449774+chrismhilton@users.noreply.github.com
|
@@ -98,6 +112,7 @@ files:
|
|
98
112
|
- gemfiles/activesupport_5_2.gemfile
|
99
113
|
- gemfiles/activesupport_6_0.gemfile
|
100
114
|
- gemfiles/activesupport_6_1.gemfile
|
115
|
+
- gemfiles/activesupport_7_0.gemfile
|
101
116
|
- lib/flexitime.rb
|
102
117
|
- lib/flexitime/configuration.rb
|
103
118
|
- lib/flexitime/parser.rb
|