flexitime 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7761e5fbc7cf6ddf2a963ae8a38f8ade7b38db26eb2835b23def41df09599ad9
4
+ data.tar.gz: d7e5366d7d36c665f17d9695b563c894451538925efd016d5e4f0ed5ae695cab
5
+ SHA512:
6
+ metadata.gz: fe193a2473e99b72abd0d94751a491f9f5971c8861a671053afc3151c7eea04588fb20a6279d7bd2fa3d33e4b5ec8e92f82f418f4bf4ad3d2e807daa8da8415a
7
+ data.tar.gz: 617842e3fd380181f280fe8ffdff7fea5bb52165596816b6ad0be9d7190bae37e3487840a28d7144f614c4d0f660ad652108d4a5cc9e8231e094cf90e2670d56
@@ -0,0 +1,49 @@
1
+ # GitHub actions workflow using a matrix to test all stable releases of Ruby
2
+ # with the different releases of ActiveSupport using the Appraisals gemfiles
3
+ # References
4
+ # https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-ruby
5
+ # https://github.com/ruby/setup-ruby#matrix-of-gemfiles
6
+ # https://technology.customink.com/blog/2019/09/02/from-travis-ci-to-github-actions/
7
+ # https://bibwild.wordpress.com/2020/11/12/deep-dive-moving-ruby-projects-from-travis-to-github-actions-for-ci/
8
+ # https://github.com/jrochkind/attr_json/blob/a3f38f01dc641bc0486442ec6ff4351e18ab4a03/.github/workflows/ci.yml
9
+ name: CI
10
+
11
+ on: [push,pull_request]
12
+
13
+ jobs:
14
+ test:
15
+ runs-on: ubuntu-latest
16
+
17
+ strategy:
18
+ fail-fast: false # should GitHub cancel all in-progress jobs if any matrix job fails
19
+ matrix:
20
+ ruby-version:
21
+ - '2.5'
22
+ - '2.6'
23
+ - '2.7'
24
+ - '3.0'
25
+ gemfile:
26
+ - 'activesupport_4_0'
27
+ - 'activesupport_4_1'
28
+ - 'activesupport_4_2'
29
+ - 'activesupport_5_0'
30
+ - 'activesupport_5_1'
31
+ - 'activesupport_5_2'
32
+ - 'activesupport_6_0'
33
+ - 'activesupport_6_1'
34
+
35
+ env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
36
+ BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile
37
+
38
+ steps:
39
+ - name: Checkout
40
+ uses: actions/checkout@v2
41
+
42
+ - name: Set up Ruby ${{ matrix.ruby-version }} and bundle ${{ matrix.gemfile }}
43
+ uses: ruby/setup-ruby@v1
44
+ with:
45
+ ruby-version: ${{ matrix.ruby-version }}
46
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
47
+
48
+ - name: Run the tests
49
+ run: bundle exec rspec
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+
13
+ # appraisals gemfile lockfiles
14
+ /gemfiles/*.gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.standard.yml ADDED
@@ -0,0 +1,10 @@
1
+ # Standard ruby style guide, linter, and formatter https://github.com/testdouble/standard
2
+ # to check the style of files run:-
3
+ # $ bundle exec standardrb
4
+ ignore:
5
+ - 'flexitime.gemspec'
6
+
7
+ # Standard will default to telling RuboCop to target the currently running version of Ruby
8
+ # instead set this to minimum version of Ruby supported by Flexitime
9
+ # which prevents an offence being flagged for the Style/SlicingWithRange cop that is releveant to Ruby 2.6+
10
+ ruby_version: 2.5
data/Appraisals ADDED
@@ -0,0 +1,44 @@
1
+ # Appraisal integrates with bundler and rake to test your library
2
+ # against different versions of dependencies in repeatable scenarios called "appraisals"
3
+ # https://github.com/thoughtbot/appraisal
4
+ #
5
+ # The dependencies in your Appraisals file are combined with dependencies in your Gemfile
6
+ #
7
+ # Install the dependencies for each appraisal
8
+ # $ bundle exec appraisal install
9
+ # which generates a Gemfile for each appraisal in the gemfiles directory
10
+ #
11
+ # Run each appraisal in turn or a single appraisal:-
12
+ # $ bundle exec appraisal rspec
13
+ # $ bundle exec appraisal activesupport-6-1 rspec
14
+ appraise "activesupport-6-1" do
15
+ gem "activesupport", "~> 6.1"
16
+ end
17
+
18
+ appraise "activesupport-6-0" do
19
+ gem "activesupport", "~> 6.0.1"
20
+ end
21
+
22
+ appraise "activesupport-5-2" do
23
+ gem "activesupport", "~> 5.2"
24
+ end
25
+
26
+ appraise "activesupport-5-1" do
27
+ gem "activesupport", "~> 5.1.0"
28
+ end
29
+
30
+ appraise "activesupport-5-0" do
31
+ gem "activesupport", "~> 5.0.1"
32
+ end
33
+
34
+ appraise "activesupport-4-2" do
35
+ gem "activesupport", "~> 4.2"
36
+ end
37
+
38
+ appraise "activesupport-4-1" do
39
+ gem "activesupport", "~> 4.1.0"
40
+ end
41
+
42
+ appraise "activesupport-4-0" do
43
+ gem "activesupport", "~> 4.0.1"
44
+ end
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2021-11-22
4
+
5
+ - Initial release
@@ -0,0 +1,84 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
+
9
+ ## Our Standards
10
+
11
+ Examples of behavior that contributes to a positive environment for our community include:
12
+
13
+ * Demonstrating empathy and kindness toward other people
14
+ * Being respectful of differing opinions, viewpoints, and experiences
15
+ * Giving and gracefully accepting constructive feedback
16
+ * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
+ * Focusing on what is best not just for us as individuals, but for the overall community
18
+
19
+ Examples of unacceptable behavior include:
20
+
21
+ * The use of sexualized language or imagery, and sexual attention or
22
+ advances of any kind
23
+ * Trolling, insulting or derogatory comments, and personal or political attacks
24
+ * Public or private harassment
25
+ * Publishing others' private information, such as a physical or email
26
+ address, without their explicit permission
27
+ * Other conduct which could reasonably be considered inappropriate in a
28
+ professional setting
29
+
30
+ ## Enforcement Responsibilities
31
+
32
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33
+
34
+ Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35
+
36
+ ## Scope
37
+
38
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39
+
40
+ ## Enforcement
41
+
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at 138595+chrisbranson@users.noreply.github.com. All complaints will be reviewed and investigated promptly and fairly.
43
+
44
+ All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
+
46
+ ## Enforcement Guidelines
47
+
48
+ Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49
+
50
+ ### 1. Correction
51
+
52
+ **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53
+
54
+ **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55
+
56
+ ### 2. Warning
57
+
58
+ **Community Impact**: A violation through a single incident or series of actions.
59
+
60
+ **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61
+
62
+ ### 3. Temporary Ban
63
+
64
+ **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65
+
66
+ **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67
+
68
+ ### 4. Permanent Ban
69
+
70
+ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71
+
72
+ **Consequence**: A permanent ban from any sort of public interaction within the community.
73
+
74
+ ## Attribution
75
+
76
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77
+ available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78
+
79
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80
+
81
+ [homepage]: https://www.contributor-covenant.org
82
+
83
+ For answers to common questions about this code of conduct, see the FAQ at
84
+ https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in flexitime.gemspec
6
+ gemspec
7
+
8
+ gem "standard"
data/Gemfile.lock ADDED
@@ -0,0 +1,81 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ flexitime (0.1.0)
5
+ activesupport (>= 4.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activesupport (6.1.4.1)
11
+ concurrent-ruby (~> 1.0, >= 1.0.2)
12
+ i18n (>= 1.6, < 2)
13
+ minitest (>= 5.1)
14
+ tzinfo (~> 2.0)
15
+ zeitwerk (~> 2.3)
16
+ appraisal (2.4.1)
17
+ bundler
18
+ rake
19
+ thor (>= 0.14.0)
20
+ ast (2.4.2)
21
+ concurrent-ruby (1.1.9)
22
+ diff-lcs (1.4.4)
23
+ i18n (1.8.11)
24
+ concurrent-ruby (~> 1.0)
25
+ minitest (5.14.4)
26
+ parallel (1.21.0)
27
+ parser (3.0.2.0)
28
+ ast (~> 2.4.1)
29
+ rainbow (3.0.0)
30
+ rake (13.0.6)
31
+ regexp_parser (2.1.1)
32
+ rexml (3.2.5)
33
+ rspec (3.10.0)
34
+ rspec-core (~> 3.10.0)
35
+ rspec-expectations (~> 3.10.0)
36
+ rspec-mocks (~> 3.10.0)
37
+ rspec-core (3.10.1)
38
+ rspec-support (~> 3.10.0)
39
+ rspec-expectations (3.10.1)
40
+ diff-lcs (>= 1.2.0, < 2.0)
41
+ rspec-support (~> 3.10.0)
42
+ rspec-mocks (3.10.2)
43
+ diff-lcs (>= 1.2.0, < 2.0)
44
+ rspec-support (~> 3.10.0)
45
+ rspec-support (3.10.3)
46
+ rubocop (1.22.3)
47
+ parallel (~> 1.10)
48
+ parser (>= 3.0.0.0)
49
+ rainbow (>= 2.2.2, < 4.0)
50
+ regexp_parser (>= 1.8, < 3.0)
51
+ rexml
52
+ rubocop-ast (>= 1.12.0, < 2.0)
53
+ ruby-progressbar (~> 1.7)
54
+ unicode-display_width (>= 1.4.0, < 3.0)
55
+ rubocop-ast (1.13.0)
56
+ parser (>= 3.0.1.1)
57
+ rubocop-performance (1.11.5)
58
+ rubocop (>= 1.7.0, < 2.0)
59
+ rubocop-ast (>= 0.4.0)
60
+ ruby-progressbar (1.11.0)
61
+ standard (1.4.0)
62
+ rubocop (= 1.22.3)
63
+ rubocop-performance (= 1.11.5)
64
+ thor (1.1.0)
65
+ tzinfo (2.0.4)
66
+ concurrent-ruby (~> 1.0)
67
+ unicode-display_width (2.1.0)
68
+ zeitwerk (2.5.1)
69
+
70
+ PLATFORMS
71
+ arm64-darwin-21
72
+
73
+ DEPENDENCIES
74
+ appraisal
75
+ flexitime!
76
+ rake (~> 13.0)
77
+ rspec (~> 3.0)
78
+ standard
79
+
80
+ BUNDLED WITH
81
+ 2.2.22
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Circle Software & Design Ltd.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,203 @@
1
+ # Flexitime
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 a Time 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) or set this via a configuration option; configure the parser to return Time objects either in the system local time or for a specified [time zone](https://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html); and set a desired precision for the Time object.
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 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
+
7
+ ![Build Status](https://github.com/CircleSD/flexitime/actions/workflows/ci.yml/badge.svg?branch=main)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ ```ruby
15
+ gem 'flexitime'
16
+ ```
17
+
18
+ And then execute:
19
+
20
+ ```ruby
21
+ bundle install
22
+ ```
23
+
24
+ Or install it yourself as:
25
+
26
+ ```ruby
27
+ gem install flexitime
28
+ ```
29
+
30
+ ## Usage
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 a Time object otherwise it returns `nil` if any of the date/time parts are invalid or the string does not match a recognised format.
33
+
34
+ ```ruby
35
+ Flexitime.parse("23/08/2021") # => 2021-08-23 00:00:00 +0100
36
+ Flexitime.parse("23/08/2021 08:00") # => 2021-08-23 08:00:00 +0100
37
+ Flexitime.parse("08:00") # => 2021-11-21 08:00:00 +0000
38
+ ```
39
+
40
+ ```ruby
41
+ Flexitime.parse("31/02/2021 08:00") # => nil
42
+ Flexitime.parse("computer says no") # => nil
43
+ ```
44
+
45
+ ## Formats
46
+
47
+ 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
+
49
+ 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
+
51
+ ```text
52
+ 01/08/2021
53
+ 01-08-2021
54
+ 01.08.2021
55
+ 1/8/2021
56
+ 1-8-2021
57
+ 1.8.2021
58
+ 01/08/21
59
+ 01-08-21
60
+ 01.08.21
61
+ 1/8/21
62
+ 1-8-21
63
+ 1.8.21
64
+ 2021/08/01
65
+ 2021-08-01
66
+ 2021.08.01
67
+ 2021/8/1
68
+ 2021-8-1
69
+ 2021.8.1
70
+ ```
71
+
72
+ Times with hours, minutes & seconds separated by colons `:`; with hours & minutes separated by either colons `:` or periods `.`; with 1 or 2 digit hours; and with case-insensitive AM or PM.
73
+
74
+ ```text
75
+ 08:15:30
76
+ 08:15
77
+ 08.15
78
+ 8:15:30
79
+ 8.15
80
+ 08:15:30 PM
81
+ 08:15 am
82
+ ```
83
+
84
+ Also times in the ISO 8601 Zulu format with between 1 and 6 digit milliseconds
85
+
86
+ ```text
87
+ 2021-08-01T08:15:30.144515Z
88
+ ```
89
+
90
+ If the string does not match any of the regular expressions then Flexitime will attempt to parse the string using the time 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
+
92
+ ```ruby
93
+ Flexitime.parse("2021-01-02T09:00:00+02:00") # => 2021-01-02 09:00:00 +0200
94
+ Flexitime.parse("2nd January 2021") # => 2021-01-02 00:00:00 +0000
95
+ ```
96
+
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
+ ## Development
174
+
175
+ 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.
176
+
177
+ After making changes run `rake spec` to run the tests and `bundle exec appraisal rspec` to run the tests against different versions of activesupport; and run `bundle exec standardrb` to check the style of files.
178
+
179
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
180
+
181
+ ## Contributing
182
+
183
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/CircleSD/flexitime). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/CircleSD/flexitime/blob/main/CODE_OF_CONDUCT.md).
184
+
185
+ 1. Fork it
186
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
187
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
188
+ 4. Push to the branch (`git push origin my-new-feature`)
189
+ 5. Create new Pull Request
190
+
191
+ ## Credits
192
+
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 `time_class` and `ambiguous_year_future_bias` configuration options.
194
+
195
+ 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
+
197
+ ## License
198
+
199
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
200
+
201
+ ## Code of Conduct
202
+
203
+ Everyone interacting in the Flexitime project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/CircleSD/flexitime/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/benchmark.rb ADDED
@@ -0,0 +1,148 @@
1
+ # Benchmark file to compare Flexitime with Time and Time.zone parsing
2
+ # to ensure Flexitime performs as well as possible
3
+ # To perform benchmark comparison run:-
4
+ # $ ruby benchmark.rb
5
+ $:.unshift(File.expand_path("lib"))
6
+
7
+ require "benchmark"
8
+ require "flexitime"
9
+ require "active_support/time"
10
+
11
+ Time.zone = "Europe/London"
12
+
13
+ n = 10_000
14
+
15
+ Benchmark.bm(50) do |benchmark|
16
+ benchmark.report("Time.parse datetime YMD") do
17
+ n.times do
18
+ Time.parse("2021-08-12 12:30")
19
+ end
20
+ end
21
+
22
+ benchmark.report("Time.zone.parse datetime YMD") do
23
+ n.times do
24
+ Time.zone.parse("2021-08-12 12:30")
25
+ end
26
+ end
27
+
28
+ benchmark.report("Flexitime.parse (with Time) datetime YMD") do
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
36
+ n.times do
37
+ Flexitime.parse("2021-08-12 12:30")
38
+ end
39
+ end
40
+ end
41
+
42
+ Benchmark.bm(50) do |benchmark|
43
+ benchmark.report("Time.parse datetime DMY HM") do
44
+ n.times do
45
+ Time.parse("23/08/2021 12:30")
46
+ end
47
+ end
48
+
49
+ benchmark.report("Time.zone.parse datetime DMY HM") do
50
+ n.times do
51
+ Time.zone.parse("23/08/2021 12:30")
52
+ end
53
+ end
54
+
55
+ benchmark.report("Flexitime.parse (with Time) datetime DMY HM") do
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
63
+ n.times do
64
+ Flexitime.parse("23/08/2021 12:30")
65
+ end
66
+ end
67
+ end
68
+
69
+ Benchmark.bm(50) do |benchmark|
70
+ benchmark.report("Time.parse datetime DMY HMS") do
71
+ n.times do
72
+ Time.parse("23/08/2021 12:30:45")
73
+ end
74
+ end
75
+
76
+ benchmark.report("Time.zone.parse datetime DMY HMS") do
77
+ n.times do
78
+ Time.zone.parse("23/08/2021 12:30:45")
79
+ end
80
+ end
81
+
82
+ benchmark.report("Flexitime.parse (with Time) datetime DMY HMS") do
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
90
+ n.times do
91
+ Flexitime.parse("23/08/2021 12:30:45")
92
+ end
93
+ end
94
+ end
95
+
96
+ Benchmark.bm(50) do |benchmark|
97
+ benchmark.report("Time.parse time HM") do
98
+ n.times do
99
+ Time.parse("12:30")
100
+ end
101
+ end
102
+
103
+ benchmark.report("Time.zone.parse time HM") do
104
+ n.times do
105
+ Time.zone.parse("12:30")
106
+ end
107
+ end
108
+
109
+ benchmark.report("Flexitime.parse (with Time) time HM") do
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
117
+ n.times do
118
+ Flexitime.parse("12:30")
119
+ end
120
+ end
121
+ end
122
+
123
+ Benchmark.bm(50) do |benchmark|
124
+ benchmark.report("Time.parse time HMS") do
125
+ n.times do
126
+ Time.parse("12:30:45")
127
+ end
128
+ end
129
+
130
+ benchmark.report("Time.zone.parse time HMS") do
131
+ n.times do
132
+ Time.zone.parse("12:30:45")
133
+ end
134
+ end
135
+
136
+ benchmark.report("Flexitime.parse (with Time) time HMS") do
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
144
+ n.times do
145
+ Flexitime.parse("12:30:45")
146
+ end
147
+ end
148
+ end
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "flexitime"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/flexitime.gemspec ADDED
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # load a file relative to the current location
4
+ require_relative "lib/flexitime/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "flexitime"
8
+ spec.version = Flexitime::VERSION
9
+ spec.authors = ["Chris Hilton"]
10
+ spec.email = ["449774+chrismhilton@users.noreply.github.com"]
11
+
12
+ spec.summary = "Ruby date/time string parser"
13
+ spec.description = "Ruby date/time string parser for common formats and different date orders"
14
+ spec.homepage = "https://github.com/CircleSD/flexitime"
15
+ spec.license = "MIT"
16
+
17
+ # Minimum version of Ruby that the gem works with
18
+ spec.required_ruby_version = ">= 2.5.0"
19
+
20
+ # Metadata used on gem’s profile page on rubygems.org
21
+ spec.metadata["homepage_uri"] = spec.homepage
22
+ spec.metadata["source_code_uri"] = spec.homepage
23
+ spec.metadata["changelog_uri"] = "https://github.com/CircleSD/flexitime/blob/main/CHANGELOG.md"
24
+ spec.metadata["bug_tracker_uri"] = "https://github.com/CircleSD/flexitime/issues"
25
+ spec.metadata["documentation_uri"] = spec.homepage
26
+
27
+ # Specify which files should be added to the gem when it is released
28
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
29
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
30
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
31
+ end
32
+
33
+ # Binary folder where the gem’s executables are located
34
+ spec.bindir = "exe"
35
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
36
+
37
+ # Add lib directory to $LOAD_PATH to make code available via the require statement
38
+ spec.require_paths = ["lib"]
39
+
40
+ # Register runtime and development dependencies
41
+ # including gems that are essential to test and build this gem
42
+ # whereas gems like rubocop or standard are not essential are included in the Gemfile
43
+ spec.add_dependency "activesupport", ">= 4.0"
44
+ spec.add_development_dependency "rake", "~> 13.0"
45
+ spec.add_development_dependency "rspec", "~> 3.0"
46
+ spec.add_development_dependency "appraisal"
47
+
48
+ # For more information and examples about making a new gem, checkout our
49
+ # guide at: https://bundler.io/guides/creating_gem.html
50
+ end
@@ -0,0 +1,2 @@
1
+ ---
2
+ BUNDLE_RETRY: "1"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "standard"
6
+ gem "activesupport", "~> 4.0.1"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "standard"
6
+ gem "activesupport", "~> 4.1.0"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "standard"
6
+ gem "activesupport", "~> 4.2"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "standard"
6
+ gem "activesupport", "~> 5.0.1"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "standard"
6
+ gem "activesupport", "~> 5.1.0"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "standard"
6
+ gem "activesupport", "~> 5.2"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "standard"
6
+ gem "activesupport", "~> 6.0.1"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "standard"
6
+ gem "activesupport", "~> 6.1"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexitime
4
+ PRECISIONS = [:day, :hour, :min, :sec, :usec].freeze
5
+ DATE_PARTS = [:day, :month].freeze
6
+
7
+ class Configuration
8
+ # = Configuration options
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
+ # == first_date_part
21
+ # The first part of the date within the string, either :day or :month
22
+ # This can be set manually otherwise when using the rails-I18n gem if the first element
23
+ # of the "date.order" translation is :day or :month that will be used
24
+ # otherwise the default of :day will be used
25
+ #
26
+ # Flexitime.first_date_part = :day
27
+ # Flexitime.parse("01/06/2021") # => 2021-06-01 00:00:00 +0100
28
+ #
29
+ # Flexitime.first_date_part = :month
30
+ # Flexitime.parse("01/06/2021") # => 2021-01-06 00:00:00 +0100
31
+ #
32
+ # Regardless of the option value, the gem will always attempt to parse
33
+ # a string starting with a 4 digit year, deciphering the format as year/month/day
34
+ #
35
+ # Flexitime.parse("2021-11-12 08:15") # => 2021-11-12 08:15:00 +0000
36
+ #
37
+ # == precision
38
+ # The desired precision for the returned Time object, defaulting to minute (:min)
39
+ #
40
+ # Flexitime.parse("01/06/2021 18:30:45.036711") # => 2021-06-01 18:30:00 +0100
41
+ # Flexitime.precision = :sec
42
+ # Flexitime.parse("01/06/2021 18:30:45.036711") # => 2021-06-01 18:30:45 +0100
43
+ # Flexitime.precision = :usec
44
+ # Flexitime.parse("01/06/2021 18:30:45.036711") # => 2021-06-01 18:30:45.036711 +0100
45
+ #
46
+ # == ambiguous_year_future_bias
47
+ # The option used to determine the century when parsing a string containing a 2 digit year
48
+ # with the default value of 50 and the current year of 2020
49
+ # the year is set from within a range of >= 1970 and <= 2069
50
+ # whereas with a value of 20 and the current year of 2020
51
+ # the year is set from within a range of >= 2000 and <= 2099
52
+ #
53
+ # Flexitime.parse("01/08/00").year # => 2000
54
+ # Flexitime.parse("01/08/69").year # => 2069
55
+ # Flexitime.parse("01/08/70").year # => 1970
56
+ # Flexitime.parse("01/08/99").year # => 1999
57
+ #
58
+ attr_accessor :time_class
59
+ attr_reader :precision
60
+ attr_accessor :ambiguous_year_future_bias
61
+
62
+ def initialize
63
+ @time_class = ::Time
64
+ @first_date_part = nil
65
+ @precision = :min
66
+ @ambiguous_year_future_bias = 50
67
+ end
68
+
69
+ def first_date_part=(first_date_part)
70
+ raise ArgumentError.new("Invalid first date part") unless first_date_part_valid?(first_date_part)
71
+ @first_date_part = first_date_part.to_sym
72
+ end
73
+
74
+ def first_date_part
75
+ first_date_part = @first_date_part || i18n_first_date_part
76
+ first_date_part_valid?(first_date_part) ? first_date_part : DATE_PARTS.first
77
+ end
78
+
79
+ def precision=(precision)
80
+ raise ArgumentError.new("Invalid precision") unless precision_valid?(precision)
81
+ @precision = precision.to_sym
82
+ end
83
+
84
+ private
85
+
86
+ def i18n_first_date_part
87
+ Array(I18n.t("date.order")).map(&:to_sym).first if defined?(I18n)
88
+ end
89
+
90
+ def first_date_part_valid?(first_date_part)
91
+ first_date_part.present? && DATE_PARTS.include?(first_date_part.to_s.to_sym)
92
+ end
93
+
94
+ def precision_valid?(precision)
95
+ precision.present? && PRECISIONS.include?(precision.to_s.to_sym)
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexitime
4
+ module Parser
5
+ # date regex to match year/month/day eg. "2021-10-27"
6
+ # with 4 digit year; 1 or 2 digit month & day; and slash, hyphen or period separator
7
+ ISO8601_DATE_REGEX = %r{(?<!\d)(\d{4})[-./](\d{1,2})[-./](\d{1,2})(?!\d)}
8
+ # date regex to match day/month/year and month/day/year eg. "27/10/2021" and "10/27/2021"
9
+ # with 1 or 2 digit day & month; 2 or 4 digit year; and slash, hyphen or period separator
10
+ LOCAL_DATE_REGEX = %r{(?<!\d)(\d{1,2})[-./](\d{1,2})[-./](\d{4}|\d{2})(?!\d)}
11
+ # time regex to match a zulu time "11:14:30.000Z"
12
+ # with 2 digit hour, minute & second; 1 to 6 digit milliseconds; and Z character
13
+ ISO8601_TIME_REGEX = %r{(\d{2}):(\d{2}):(\d{2})\.(\d{1,6})(Z{1})$}
14
+ # time regex to match hour/min/sec eg. "09:15:30", "09:15:30 AM" and "09:15:30AM"
15
+ # with 1 or 2 digit hour; 2 digit minute & second; optional case-insensitive meridiem;
16
+ # and colon separator but not a period separator as there are no rails-i18n locales
17
+ # using that separator with hours, minutes & seconds and it would clash with the local date pattern
18
+ HOUR_MINUTE_SECOND_REGEX = %r{^(\d{1,2}):(\d{2}):(\d{2})\s?([aApP][mM])?\.?$}
19
+ # time regex to match hour/min eg. "09:15", "09:15 AM" and "09:15AM"
20
+ # with 1 or 2 digit hour; 2 digit minute; optional case-insensitive meridiem;
21
+ # and colon or period separator as in rails-i18n there a few locales (such as Danish)
22
+ # that use a period with hours & minutes
23
+ HOUR_MINUTE_REGEX = %r{^(\d{1,2})[:.](\d{2})\s?([aApP][mM])?\.?$}
24
+
25
+ # Parse a String argument and create a Time object
26
+ # using the configuration time class and desired precision.
27
+ #
28
+ # When the String contains a date or date/time or time that matches the regular expressions
29
+ # the time class local method is used to create a Time object
30
+ # otherwise the time class parse method is used to create a Time object
31
+ # and for an invalid date, date/time or time nil is returned.
32
+ def parse(str)
33
+ str = str.is_a?(String) ? str : str.try(:to_str)
34
+ return nil if str.blank?
35
+
36
+ parts = extract_parts(str)
37
+
38
+ if parts.present?
39
+ create_time_from_parts(parts) if valid_date_parts?(parts)
40
+ else
41
+ time_class_parse(str)
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ # Extract date and time parts and return a Hash containing the parts
48
+ # or nil if either the date or time string does not match a regex
49
+ def extract_parts(str)
50
+ date_str, time_str = separate_date_and_time(str)
51
+
52
+ date_parts = extract_date_parts(date_str)
53
+
54
+ if date_parts.blank?
55
+ now = Flexitime.configuration.time_class.now
56
+ date_parts = {year: now.year, month: now.month, day: now.day}
57
+ time_str = str.strip
58
+ end
59
+
60
+ if time_str.present?
61
+ time_parts = extract_time_parts(time_str)
62
+ return nil if time_parts.blank?
63
+ date_parts.merge(time_parts)
64
+ else
65
+ date_parts
66
+ end
67
+ end
68
+
69
+ def separate_date_and_time(str)
70
+ parts = str.index(" ").present? ? str.split(" ") : str.split("T")
71
+ [parts.shift, parts.join(" ")]
72
+ end
73
+
74
+ def extract_date_parts(str)
75
+ extract_iso_date_parts(str) || extract_local_date_parts(str)
76
+ end
77
+
78
+ def extract_iso_date_parts(str)
79
+ # match array returns ["1973-08-23", "1973", "08", "23"]
80
+ parts = str.match(ISO8601_DATE_REGEX).to_a.pop(3).map(&:to_i)
81
+ if parts.present?
82
+ {year: make_year(parts.first), month: parts.second, day: parts.third}
83
+ end
84
+ end
85
+
86
+ def extract_local_date_parts(str)
87
+ # match array returns ["23-08-1973", "23", "08", "1973"]
88
+ parts = str.match(LOCAL_DATE_REGEX).to_a.pop(3).map(&:to_i)
89
+ if parts.present?
90
+ day, month = Flexitime.configuration.first_date_part == :day ? [parts.first, parts.second] : [parts.second, parts.first]
91
+ {year: make_year(parts.third), month: month, day: day}
92
+ end
93
+ end
94
+
95
+ def extract_time_parts(str)
96
+ extract_iso_time_parts(str) || extract_hour_minute_second_parts(str) || extract_hour_minute_parts(str)
97
+ end
98
+
99
+ def extract_iso_time_parts(str)
100
+ # match array returns ["11:14:30.999Z", "11", "14", "30", "999", "Z"]
101
+ parts = str.match(ISO8601_TIME_REGEX).to_a.pop(5).map(&:to_i)
102
+ if parts.present?
103
+ {hour: parts.first, min: parts.second, sec: parts.third, usec: parts.fourth, utc: true}
104
+ end
105
+ end
106
+
107
+ def extract_hour_minute_second_parts(str)
108
+ # match array returns ["12:35:20 AM", "12", "35", "20", "AM"]
109
+ parts = str.match(HOUR_MINUTE_SECOND_REGEX).to_a.pop(4)
110
+ if parts.present?
111
+ {hour: make_hour(parts.first.to_i, parts.fourth), min: parts.second.to_i, sec: parts.third.to_i}
112
+ end
113
+ end
114
+
115
+ def extract_hour_minute_parts(str)
116
+ # match array returns ["12:35 AM", "12", "35", "AM"]
117
+ parts = str.match(HOUR_MINUTE_REGEX).to_a.pop(3)
118
+ if parts.present?
119
+ {hour: make_hour(parts.first.to_i, parts.third), min: parts.second.to_i}
120
+ end
121
+ end
122
+
123
+ # Convert 2 digit years into 4
124
+ def make_year(year)
125
+ return year if year.to_s.size > 2
126
+
127
+ start_year = Flexitime.configuration.time_class.now.year - Flexitime.configuration.ambiguous_year_future_bias
128
+ century = (start_year / 100) * 100
129
+ full_year = century + year
130
+ full_year < start_year ? full_year + 100 : full_year
131
+ end
132
+
133
+ # Convert hour depending on presence of am/pm
134
+ def make_hour(hour, meridiem)
135
+ meridiem.to_s.downcase == "pm" ? hour + 12 : hour
136
+ end
137
+
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"
140
+ def valid_date_parts?(parts)
141
+ parts[:month] >= 1 && parts[:month] <= 12 && parts[:day] >= 1 &&
142
+ parts[:day] <= Time.days_in_month(parts[:month], parts[:year])
143
+ end
144
+
145
+ # Create a Time object using only those parts required for the configuration precision
146
+ def create_time_from_parts(parts)
147
+ time = Flexitime.configuration.time_class.local(*local_args_for_precision(parts))
148
+ parts[:utc] ? (time + time.utc_offset) : time
149
+ rescue
150
+ end
151
+
152
+ # Returns the date/time parts required for the configuration precision
153
+ def local_args_for_precision(parts)
154
+ keys = [:year, :month, :day, :hour, :min, :sec, :usec]
155
+ index = keys.index(Flexitime.configuration.precision)
156
+ keys[0..index].map { |key| parts[key] }
157
+ end
158
+
159
+ # Parse the string using the time class and set the configuration precision
160
+ def time_class_parse(str)
161
+ time = Flexitime.configuration.time_class.parse(str)
162
+ set_precision(time)
163
+ rescue
164
+ end
165
+
166
+ # Set the precision, first checking if this is necessary
167
+ # to avoid the overhead of calling the change method
168
+ def set_precision(time)
169
+ index = PRECISIONS.index(Flexitime.configuration.precision)
170
+ dismiss_part = PRECISIONS[index + 1]
171
+ excess = PRECISIONS[(index + 1)..-1].sum { |key| time.send(key) }
172
+ excess > 0 ? time.change(dismiss_part => 0) : time
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexitime
4
+ VERSION = "0.1.0"
5
+ end
data/lib/flexitime.rb ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+ require "forwardable"
5
+
6
+ # Active Support Core Extensions
7
+ # https://guides.rubyonrails.org/active_support_core_extensions.html
8
+ require "active_support/core_ext/object/blank" # blank? and present?
9
+ require "active_support/core_ext/array/access" # array second/third/fourth
10
+ require "active_support/core_ext/time/calculations" # Time.days_in_month
11
+
12
+ require_relative "flexitime/version"
13
+ require_relative "flexitime/configuration"
14
+ require_relative "flexitime/parser"
15
+
16
+ module Flexitime
17
+ class << self
18
+ extend Forwardable
19
+ def_delegators :configuration, :time_class, :first_date_part, :precision, :ambiguous_year_future_bias
20
+ def_delegators :configuration, :time_class=, :first_date_part=, :precision=, :ambiguous_year_future_bias=
21
+
22
+ include Flexitime::Parser
23
+
24
+ def configuration
25
+ Thread.current["Flexitime.configuration"] ||= Configuration.new
26
+ end
27
+ end
28
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: flexitime
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Chris Hilton
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-11-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '4.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '13.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '13.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: appraisal
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Ruby date/time string parser for common formats and different date orders
70
+ email:
71
+ - 449774+chrismhilton@users.noreply.github.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".github/workflows/ci.yml"
77
+ - ".gitignore"
78
+ - ".rspec"
79
+ - ".standard.yml"
80
+ - Appraisals
81
+ - CHANGELOG.md
82
+ - CODE_OF_CONDUCT.md
83
+ - Gemfile
84
+ - Gemfile.lock
85
+ - LICENSE.txt
86
+ - README.md
87
+ - Rakefile
88
+ - benchmark.rb
89
+ - bin/console
90
+ - bin/setup
91
+ - flexitime.gemspec
92
+ - gemfiles/.bundle/config
93
+ - gemfiles/activesupport_4_0.gemfile
94
+ - gemfiles/activesupport_4_1.gemfile
95
+ - gemfiles/activesupport_4_2.gemfile
96
+ - gemfiles/activesupport_5_0.gemfile
97
+ - gemfiles/activesupport_5_1.gemfile
98
+ - gemfiles/activesupport_5_2.gemfile
99
+ - gemfiles/activesupport_6_0.gemfile
100
+ - gemfiles/activesupport_6_1.gemfile
101
+ - lib/flexitime.rb
102
+ - lib/flexitime/configuration.rb
103
+ - lib/flexitime/parser.rb
104
+ - lib/flexitime/version.rb
105
+ homepage: https://github.com/CircleSD/flexitime
106
+ licenses:
107
+ - MIT
108
+ metadata:
109
+ homepage_uri: https://github.com/CircleSD/flexitime
110
+ source_code_uri: https://github.com/CircleSD/flexitime
111
+ changelog_uri: https://github.com/CircleSD/flexitime/blob/main/CHANGELOG.md
112
+ bug_tracker_uri: https://github.com/CircleSD/flexitime/issues
113
+ documentation_uri: https://github.com/CircleSD/flexitime
114
+ post_install_message:
115
+ rdoc_options: []
116
+ require_paths:
117
+ - lib
118
+ required_ruby_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: 2.5.0
123
+ required_rubygems_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ requirements: []
129
+ rubygems_version: 3.2.22
130
+ signing_key:
131
+ specification_version: 4
132
+ summary: Ruby date/time string parser
133
+ test_files: []