minutely 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +11 -0
- data/.gitlab-ci.yml +23 -0
- data/.rspec +4 -0
- data/.rubocop.yml +14 -0
- data/.tool-versions +1 -0
- data/.travis.yml +8 -0
- data/.vscode/extensions.json +5 -0
- data/.vscode/settings.json +17 -0
- data/.vscode/tasks.json +43 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +78 -0
- data/LICENSE +21 -0
- data/README.md +134 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/minutely.rb +44 -0
- data/lib/minutely/parser.rb +30 -0
- data/lib/minutely/string_as_json.rb +16 -0
- data/lib/minutely/time.rb +102 -0
- data/lib/minutely/time/parser.rb +42 -0
- data/lib/minutely/time_range.rb +137 -0
- data/lib/minutely/time_range/parser.rb +56 -0
- data/lib/minutely/version.rb +5 -0
- data/minutely.gemspec +34 -0
- metadata +70 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 720e333d931b75150f1e2b9f9854f293e26b7164f2ba6fa483fcab646d2a0aa0
|
4
|
+
data.tar.gz: 557aecaa956d88c516d29e2e964314e3ca4600d5b5e5600b9a800e56be3a7c26
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6afee9948aa220063711a1710d51acd05efbcfd0cea087f43143e5707972486f76b62b02fe2e34666924d188773e9d6575371fb9e366252250f299b276deec18
|
7
|
+
data.tar.gz: 2cb497a1ea2d104945886383800657c15db66bc9859771f4d32adbd2c967855ebdac754b6b4ba9c86732659eeb1d6ef0528a1930efcc09d65b8bca714542bb6e
|
data/.coveralls.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
service_name: travis-ci
|
data/.gitignore
ADDED
data/.gitlab-ci.yml
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
image: gitlab.i22.de:5001/docker/i22-ubuntu:20.04
|
2
|
+
|
3
|
+
before_script:
|
4
|
+
- gem install bundler -v 2.2.15
|
5
|
+
- bundle install -j $(nproc) --path vendor/ruby
|
6
|
+
|
7
|
+
cache:
|
8
|
+
key: ${CI_COMMIT_REF_SLUG}
|
9
|
+
paths:
|
10
|
+
- vendor/ruby
|
11
|
+
|
12
|
+
lint:
|
13
|
+
script:
|
14
|
+
- bundle exec rubocop
|
15
|
+
|
16
|
+
test:
|
17
|
+
script:
|
18
|
+
- bundle exec rspec
|
19
|
+
coverage: /\(\d+.\d+%\)/
|
20
|
+
artifacts:
|
21
|
+
reports:
|
22
|
+
cobertura: coverage/coverage.xml
|
23
|
+
expire_in: 1 day
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/.tool-versions
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby 2.7.0
|
data/.travis.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
{
|
2
|
+
"editor.tabSize": 2,
|
3
|
+
"files.insertFinalNewline": true,
|
4
|
+
"files.trimFinalNewlines": true,
|
5
|
+
"files.trimTrailingWhitespace": true,
|
6
|
+
"ruby.format": "rubocop",
|
7
|
+
"ruby.lint": {
|
8
|
+
"reek": {
|
9
|
+
"useBundler": true
|
10
|
+
},
|
11
|
+
"rubocop": {
|
12
|
+
"useBundler": true
|
13
|
+
}
|
14
|
+
},
|
15
|
+
"ruby.useBundler": true,
|
16
|
+
"ruby.useLanguageServer": true
|
17
|
+
}
|
data/.vscode/tasks.json
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
{
|
2
|
+
"version": "2.0.0",
|
3
|
+
"tasks": [{
|
4
|
+
"label": "RSpec Test",
|
5
|
+
"type": "shell",
|
6
|
+
"command": "bundle exec rspec",
|
7
|
+
"group": "test",
|
8
|
+
"presentation": {
|
9
|
+
"echo": true,
|
10
|
+
"reveal": "always",
|
11
|
+
"focus": false,
|
12
|
+
"panel": "shared",
|
13
|
+
"showReuseMessage": true,
|
14
|
+
"clear": true
|
15
|
+
}
|
16
|
+
}, {
|
17
|
+
"label": "RSpec Test File",
|
18
|
+
"type": "shell",
|
19
|
+
"command": "bundle exec rspec ${relativeFile}",
|
20
|
+
"group": "test",
|
21
|
+
"presentation": {
|
22
|
+
"echo": true,
|
23
|
+
"reveal": "always",
|
24
|
+
"focus": false,
|
25
|
+
"panel": "shared",
|
26
|
+
"showReuseMessage": true,
|
27
|
+
"clear": true
|
28
|
+
}
|
29
|
+
}, {
|
30
|
+
"label": "RSpec Test Line",
|
31
|
+
"type": "shell",
|
32
|
+
"command": "bundle exec rspec ${relativeFile}:${lineNumber}",
|
33
|
+
"group": "test",
|
34
|
+
"presentation": {
|
35
|
+
"echo": true,
|
36
|
+
"reveal": "always",
|
37
|
+
"focus": false,
|
38
|
+
"panel": "shared",
|
39
|
+
"showReuseMessage": true,
|
40
|
+
"clear": true
|
41
|
+
}
|
42
|
+
}]
|
43
|
+
}
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
minutely (3.0.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
ast (2.4.2)
|
10
|
+
coveralls (0.8.23)
|
11
|
+
json (>= 1.8, < 3)
|
12
|
+
simplecov (~> 0.16.1)
|
13
|
+
term-ansicolor (~> 1.3)
|
14
|
+
thor (>= 0.19.4, < 2.0)
|
15
|
+
tins (~> 1.6)
|
16
|
+
diff-lcs (1.4.4)
|
17
|
+
docile (1.3.5)
|
18
|
+
json (2.5.1)
|
19
|
+
parallel (1.20.1)
|
20
|
+
parser (3.0.1.0)
|
21
|
+
ast (~> 2.4.1)
|
22
|
+
rainbow (3.0.0)
|
23
|
+
rake (13.0.3)
|
24
|
+
regexp_parser (2.1.1)
|
25
|
+
rexml (3.2.5)
|
26
|
+
rspec (3.10.0)
|
27
|
+
rspec-core (~> 3.10.0)
|
28
|
+
rspec-expectations (~> 3.10.0)
|
29
|
+
rspec-mocks (~> 3.10.0)
|
30
|
+
rspec-core (3.10.1)
|
31
|
+
rspec-support (~> 3.10.0)
|
32
|
+
rspec-expectations (3.10.1)
|
33
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
34
|
+
rspec-support (~> 3.10.0)
|
35
|
+
rspec-mocks (3.10.2)
|
36
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
37
|
+
rspec-support (~> 3.10.0)
|
38
|
+
rspec-support (3.10.2)
|
39
|
+
rubocop (1.12.1)
|
40
|
+
parallel (~> 1.10)
|
41
|
+
parser (>= 3.0.0.0)
|
42
|
+
rainbow (>= 2.2.2, < 4.0)
|
43
|
+
regexp_parser (>= 1.8, < 3.0)
|
44
|
+
rexml
|
45
|
+
rubocop-ast (>= 1.2.0, < 2.0)
|
46
|
+
ruby-progressbar (~> 1.7)
|
47
|
+
unicode-display_width (>= 1.4.0, < 3.0)
|
48
|
+
rubocop-ast (1.4.1)
|
49
|
+
parser (>= 2.7.1.5)
|
50
|
+
ruby-progressbar (1.11.0)
|
51
|
+
simplecov (0.16.1)
|
52
|
+
docile (~> 1.1)
|
53
|
+
json (>= 1.8, < 3)
|
54
|
+
simplecov-html (~> 0.10.0)
|
55
|
+
simplecov-html (0.10.2)
|
56
|
+
sync (0.5.0)
|
57
|
+
term-ansicolor (1.7.1)
|
58
|
+
tins (~> 1.0)
|
59
|
+
thor (1.1.0)
|
60
|
+
tins (1.28.0)
|
61
|
+
sync
|
62
|
+
unicode-display_width (2.0.0)
|
63
|
+
yard (0.9.26)
|
64
|
+
|
65
|
+
PLATFORMS
|
66
|
+
ruby
|
67
|
+
x86_64-linux
|
68
|
+
|
69
|
+
DEPENDENCIES
|
70
|
+
coveralls
|
71
|
+
minutely!
|
72
|
+
rake (~> 13.0)
|
73
|
+
rspec (~> 3.0)
|
74
|
+
rubocop
|
75
|
+
yard
|
76
|
+
|
77
|
+
BUNDLED WITH
|
78
|
+
2.2.15
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2021 Tobias Casper
|
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 all
|
13
|
+
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 THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
# Minutely
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.org/tlux/minutely.svg?branch=master)](https://travis-ci.org/tlux/minutely)
|
4
|
+
[![Coverage Status](https://coveralls.io/repos/github/tlux/minutely/badge.svg?branch=master)](https://coveralls.io/github/tlux/minutely?branch=master)
|
5
|
+
[![Gem Version](https://badge.fury.io/rb/minutely.svg)](https://badge.fury.io/rb/minutely)
|
6
|
+
|
7
|
+
Classes for representing the time of a day by using only hours and minutes.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'minutely'
|
15
|
+
```
|
16
|
+
|
17
|
+
Then execute:
|
18
|
+
|
19
|
+
```sh
|
20
|
+
bundle install
|
21
|
+
```
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
### Time
|
26
|
+
|
27
|
+
Create a new time:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
time1 = Minutely::Time.new(21, 42)
|
31
|
+
time1.to_s # => "21:42"
|
32
|
+
|
33
|
+
time2 = Minutely::Time.new(9, 3)
|
34
|
+
time2.to_s # => "09:03"
|
35
|
+
```
|
36
|
+
|
37
|
+
Parse time using `DateTime`, `Time`, `String` or `Integer`:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
Minutely::Time.parse(DateTime.now)
|
41
|
+
Minutely::Time.parse(Time.now)
|
42
|
+
Minutely::Time.parse('9:03').to_s # => "09:03"
|
43
|
+
Minutely::Time.parse(903).to_s # => "09:03"
|
44
|
+
```
|
45
|
+
|
46
|
+
Get the next minute:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
time = Minutely::Time.parse('9:03')
|
50
|
+
time.succ.to_s # => "9:04"
|
51
|
+
```
|
52
|
+
|
53
|
+
Compare and sort by times:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
time1 = Minutely::Time.parse('9:03')
|
57
|
+
time2 = Minutely::Time.parse('21:42')
|
58
|
+
|
59
|
+
time1 == time2 # => false
|
60
|
+
time1 < time2 # => true
|
61
|
+
time1 <= time2 # => true
|
62
|
+
time1 >= time2 # => false
|
63
|
+
time1 > time2 # => false
|
64
|
+
|
65
|
+
[
|
66
|
+
Minutely::Time.parse('21:42'),
|
67
|
+
Minutely::Time.parse('9:03'),
|
68
|
+
Minutely::Time.parse('15:00')
|
69
|
+
].sort.map(&:to_s) # => ["09:03", "15:00", "21:42"]
|
70
|
+
```
|
71
|
+
|
72
|
+
Native Range support:
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
(Minutely::Time.parse('9:57')..Minutely::Time.parse('10:10'))
|
76
|
+
|
77
|
+
```
|
78
|
+
|
79
|
+
### Time Range
|
80
|
+
|
81
|
+
A special type of time range, that also allows defining ranges spanning over 12
|
82
|
+
am (0:00).
|
83
|
+
|
84
|
+
Create a new time range:
|
85
|
+
|
86
|
+
time1 = Minutely::Time.new(9, 3)
|
87
|
+
```ruby
|
88
|
+
time2 = Minutely::Time.new(21, 42)
|
89
|
+
|
90
|
+
Minutely::TimeRange.new(time1, time2).to_s # => "09:03-21:42"
|
91
|
+
Minutely::TimeRange.new(time2, time1).to_s # => "21:42-09:03"
|
92
|
+
```
|
93
|
+
|
94
|
+
Parse time range using `String`:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
Minutely::TimeRange.parse('9:03-21:42')
|
98
|
+
```
|
99
|
+
|
100
|
+
Parse time range using `Hash`:
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
Minutely::TimeRange.parse(from: '9:03', to: '21:42')
|
104
|
+
```
|
105
|
+
|
106
|
+
Check whether time range includes time:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
range = Minutely::TimeRange.new('9:03', '21:42')
|
110
|
+
|
111
|
+
range.include?('10:00') # => true
|
112
|
+
range.include?('22:00') # => false
|
113
|
+
```
|
114
|
+
|
115
|
+
Convert to Ruby `Range`:
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
Minutely::TimeRange.new('9:03', '21:42').to_r
|
119
|
+
# => #<Minutely::Time @hour=9, @minute=3>..#<Minutely::Time @hour=21, @minute=42>
|
120
|
+
```
|
121
|
+
|
122
|
+
Convert to Array of `Minutely::Time`s:
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
Minutely::TimeRange.new('23:57', '0:03').to_a.map(&:to_s)
|
126
|
+
# => ["23:57", "23:58", "23:59", "00:00", "00:01", "00:02", "00:03"]
|
127
|
+
```
|
128
|
+
|
129
|
+
Note this is only possible with ranges not spanning midnight.
|
130
|
+
|
131
|
+
## Contributing
|
132
|
+
|
133
|
+
Bug reports and pull requests are welcome on [GitHub
|
134
|
+
Issues](https://github.com/tlux/minutely/issues)
|
data/Rakefile
ADDED
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 'minutely'
|
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
data/lib/minutely.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'minutely/version'
|
4
|
+
|
5
|
+
##
|
6
|
+
# A library that provides classes for representing the time of a day by using
|
7
|
+
# only hours and minutes.
|
8
|
+
module Minutely
|
9
|
+
autoload :Parser, 'minutely/parser'
|
10
|
+
autoload :StringAsJson, 'minutely/string_as_json'
|
11
|
+
autoload :Time, 'minutely/time'
|
12
|
+
autoload :TimeRange, 'minutely/time_range'
|
13
|
+
|
14
|
+
module_function
|
15
|
+
|
16
|
+
##
|
17
|
+
# Parses the given input and returns a `Minutely::Time` or `nil`,
|
18
|
+
# respectively.
|
19
|
+
#
|
20
|
+
# @param obj [Minutely::Time, #hour, #min, Integer, String, nil]
|
21
|
+
#
|
22
|
+
# @return [Minutely::Time, nil]
|
23
|
+
#
|
24
|
+
# @raise [ArgumentError] when the object does not represent a valid time
|
25
|
+
def parse(*args)
|
26
|
+
Minutely::Time.parse(*args)
|
27
|
+
end
|
28
|
+
|
29
|
+
##
|
30
|
+
# Parses the given input and returns a `Minutely::TimeRange` or `nil`,
|
31
|
+
# respectively.
|
32
|
+
#
|
33
|
+
# @param obj [Minutely::TimeRange, Array, Hash, String, nil]
|
34
|
+
#
|
35
|
+
# @return [Minutely::TimeRange, nil]
|
36
|
+
#
|
37
|
+
# @raise [ArgumentError] when the object does not represent a valid time range
|
38
|
+
#
|
39
|
+
# @raise [KeyError] when the given Hash does not contain the required keys
|
40
|
+
# (`:from` and `:to`)
|
41
|
+
def parse_range(*args)
|
42
|
+
Minutely::TimeRange.parse(*args)
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Minutely
|
4
|
+
##
|
5
|
+
# An abstract class for defining custom parsers.
|
6
|
+
#
|
7
|
+
# @!attribute [r] value
|
8
|
+
# @return [Object]
|
9
|
+
class Parser
|
10
|
+
attr_reader :value
|
11
|
+
|
12
|
+
##
|
13
|
+
# Initialize the parser.
|
14
|
+
#
|
15
|
+
# @param value [Object]
|
16
|
+
def initialize(value)
|
17
|
+
@value = value
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# Parse the specified value.
|
22
|
+
#
|
23
|
+
# @param value [Object]
|
24
|
+
# @return [Object]
|
25
|
+
# @raise [ArgumentError]
|
26
|
+
def self.parse(value)
|
27
|
+
new(value).parse
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Minutely
|
4
|
+
##
|
5
|
+
# A mixin to use #to_s as JSON representation of the including object.
|
6
|
+
module StringAsJson
|
7
|
+
##
|
8
|
+
# The JSON representation of the object as Hash. Conventionally used by
|
9
|
+
# common serializers.
|
10
|
+
#
|
11
|
+
# @return [Hash]
|
12
|
+
def as_json(_options = nil)
|
13
|
+
to_s
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Minutely
|
4
|
+
##
|
5
|
+
# A class that represents a day time by using only hours and minutes.
|
6
|
+
#
|
7
|
+
# @!attribute [r] hour
|
8
|
+
# @return [Integer]
|
9
|
+
#
|
10
|
+
# @!attribute [r] minute
|
11
|
+
# @return [Integer]
|
12
|
+
class Time
|
13
|
+
autoload :Parser, 'minutely/time/parser'
|
14
|
+
|
15
|
+
include Comparable
|
16
|
+
include StringAsJson
|
17
|
+
|
18
|
+
attr_reader :hour, :minute
|
19
|
+
alias min minute
|
20
|
+
|
21
|
+
##
|
22
|
+
# Builds a new `Minutely::Time`.
|
23
|
+
#
|
24
|
+
# @param hour [Integer] a number between 0 and 23
|
25
|
+
#
|
26
|
+
# @param minute [Integer] a number between 0 and 59
|
27
|
+
#
|
28
|
+
# @raise [ArgumentError] when arguments are not within the specified ranges
|
29
|
+
def initialize(hour, minute)
|
30
|
+
raise ArgumentError, 'invalid hour' unless (0..24).include?(hour)
|
31
|
+
raise ArgumentError, 'invalid minute' unless (0..59).include?(minute)
|
32
|
+
|
33
|
+
@hour = hour == 24 ? 0 : hour
|
34
|
+
@minute = minute
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Returns the begining of day.
|
39
|
+
#
|
40
|
+
# @return [Minutely::Time]
|
41
|
+
def self.beginning_of_day
|
42
|
+
new(0, 0)
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Returns the end of day.
|
47
|
+
#
|
48
|
+
# @return [Minutely::Time]
|
49
|
+
def self.end_of_day
|
50
|
+
new(23, 59)
|
51
|
+
end
|
52
|
+
|
53
|
+
##
|
54
|
+
# Parses the given input and returns a `Minutely::Time` or `nil`,
|
55
|
+
# respectively.
|
56
|
+
#
|
57
|
+
# @param value [Minutely::Time, #hour, #min, Integer, String, nil]
|
58
|
+
#
|
59
|
+
# @return [Minutely::Time, nil]
|
60
|
+
#
|
61
|
+
# @raise [ArgumentError] when the object does not represent a valid time
|
62
|
+
def self.parse(value)
|
63
|
+
Time::Parser.parse(value)
|
64
|
+
end
|
65
|
+
|
66
|
+
##
|
67
|
+
# Compares the time to another one.
|
68
|
+
#
|
69
|
+
# @return [Integer]
|
70
|
+
def <=>(other)
|
71
|
+
return nil unless other.is_a?(self.class)
|
72
|
+
|
73
|
+
[hour, minute] <=> [other.hour, other.minute]
|
74
|
+
end
|
75
|
+
|
76
|
+
##
|
77
|
+
# Gets the next minute as new `Minutely::Time`.
|
78
|
+
#
|
79
|
+
# @return [Minutely::Time]
|
80
|
+
def succ
|
81
|
+
return self.class.new((hour + 1) % 24, 0) if minute == 59
|
82
|
+
|
83
|
+
self.class.new(hour, minute + 1)
|
84
|
+
end
|
85
|
+
|
86
|
+
##
|
87
|
+
# Converts the time to an integer representation of the value.
|
88
|
+
#
|
89
|
+
# @return [Integer]
|
90
|
+
def to_i
|
91
|
+
100 * hour + minute
|
92
|
+
end
|
93
|
+
|
94
|
+
##
|
95
|
+
# Converts the time to an string representation of the value.
|
96
|
+
#
|
97
|
+
# @return [String]
|
98
|
+
def to_s
|
99
|
+
[hour, minute].map { |v| v.to_s.rjust(2, '0') }.join(':')
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Minutely
|
4
|
+
class Time
|
5
|
+
##
|
6
|
+
# A parser that tries to convert the input value to `Minutely::Time`.
|
7
|
+
class Parser < Minutely::Parser
|
8
|
+
def parse
|
9
|
+
return value if value.is_a?(Time)
|
10
|
+
return Time.new(value.hour, value.min) if like_time?
|
11
|
+
|
12
|
+
case value
|
13
|
+
when nil then nil
|
14
|
+
when Integer then parse_integer
|
15
|
+
when String then parse_string
|
16
|
+
else raise ArgumentError, 'invalid time'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def like_time?
|
23
|
+
value.respond_to?(:hour) && value.respond_to?(:min)
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse_integer
|
27
|
+
hour = value / 100
|
28
|
+
minute = value % 100
|
29
|
+
Time.new(hour, minute)
|
30
|
+
end
|
31
|
+
|
32
|
+
def parse_string
|
33
|
+
return nil if value.empty?
|
34
|
+
|
35
|
+
matches = value.match(/\A(?<hour>\d{1,2}):(?<minute>\d{2})\z/)
|
36
|
+
raise ArgumentError, 'invalid time string' unless matches
|
37
|
+
|
38
|
+
Time.new(matches[:hour].to_i, matches[:minute].to_i)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Minutely
|
4
|
+
##
|
5
|
+
# A class that represents a range of day times that is only using hours
|
6
|
+
# and minutes.
|
7
|
+
#
|
8
|
+
# @!attribute [r] from
|
9
|
+
# @return [Minutely::Time]
|
10
|
+
#
|
11
|
+
# @!attribute [r] to
|
12
|
+
# @return [Minutely::Time]
|
13
|
+
class TimeRange
|
14
|
+
autoload :Parser, 'minutely/time_range/parser'
|
15
|
+
|
16
|
+
include Comparable
|
17
|
+
include Enumerable
|
18
|
+
include StringAsJson
|
19
|
+
|
20
|
+
attr_reader :from, :to
|
21
|
+
|
22
|
+
##
|
23
|
+
# Builds a new `Minutely::TimeRange`.
|
24
|
+
#
|
25
|
+
# @param from [Minutely::Time, String, Integer]
|
26
|
+
#
|
27
|
+
# @param to [Minutely::Time, String, Integer]
|
28
|
+
#
|
29
|
+
# @raise [ArgumentError] when first or second argument evaluates to `nil`.
|
30
|
+
def initialize(from, to, exclude_end: false)
|
31
|
+
@from = Minutely::Time.parse(from)
|
32
|
+
@to = Minutely::Time.parse(to)
|
33
|
+
|
34
|
+
raise ArgumentError, 'invalid time range' if @from.nil? || @to.nil?
|
35
|
+
|
36
|
+
@exclude_end = exclude_end
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Indicates whether the range excludes the last item.
|
41
|
+
#
|
42
|
+
# @return [Boolean]
|
43
|
+
def exclude_end?
|
44
|
+
@exclude_end
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# Parses the given input and returns a `Minutely::TimeRange` or `nil`,
|
49
|
+
# respectively.
|
50
|
+
#
|
51
|
+
# @param value [Minutely::TimeRange, Array, Hash, String, nil]
|
52
|
+
#
|
53
|
+
# @return [Minutely::TimeRange, nil]
|
54
|
+
#
|
55
|
+
# @raise [ArgumentError] when the object does not represent a valid time
|
56
|
+
# range
|
57
|
+
#
|
58
|
+
# @raise [KeyError] when the given Hash does not contain the required keys
|
59
|
+
# (`:from` and `:to`)
|
60
|
+
def self.parse(value)
|
61
|
+
TimeRange::Parser.parse(value)
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Compares the time range to another one.
|
66
|
+
#
|
67
|
+
# @return [Integer]
|
68
|
+
def <=>(other)
|
69
|
+
return nil unless other.is_a?(self.class)
|
70
|
+
return nil if exclude_end? != other.exclude_end?
|
71
|
+
|
72
|
+
[from, to] <=> [other.from, other.to]
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# Determines whether the specified time is in the range.
|
77
|
+
#
|
78
|
+
# @return [Boolean]
|
79
|
+
def include?(time)
|
80
|
+
time = Minutely::Time.parse(time)
|
81
|
+
end_predicate = exclude_end? ? time < to : time <= to
|
82
|
+
from <= time && end_predicate
|
83
|
+
end
|
84
|
+
|
85
|
+
##
|
86
|
+
# Iterates over all range elements and calls the given block for each
|
87
|
+
# element or returns a lazy enumerator when called without block.
|
88
|
+
#
|
89
|
+
# @yield [time] A block called for every range element.
|
90
|
+
#
|
91
|
+
# @yieldparam time [Minutely::Time] The range element.
|
92
|
+
#
|
93
|
+
# @return [Enumerator, void]
|
94
|
+
def each
|
95
|
+
return to_enum(:each) unless block_given?
|
96
|
+
|
97
|
+
val = from
|
98
|
+
|
99
|
+
loop do
|
100
|
+
yield(val) if !exclude_end? || val != to
|
101
|
+
break if val == to
|
102
|
+
|
103
|
+
val = val.succ
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
##
|
108
|
+
# Indicates whether the range spans midnight.
|
109
|
+
#
|
110
|
+
# @return [Boolean]
|
111
|
+
def spanning_midnight?
|
112
|
+
from > to
|
113
|
+
end
|
114
|
+
|
115
|
+
##
|
116
|
+
# Converts the time range into a Range. This does only work when the range
|
117
|
+
# spans midnight due to the way Ranges are based on value comparison.
|
118
|
+
#
|
119
|
+
# @raise [RuntimeError] when the time range spans midnight.
|
120
|
+
# @return [Range]
|
121
|
+
def to_r
|
122
|
+
raise 'Unable to convert ranges spanning midnight' if spanning_midnight?
|
123
|
+
|
124
|
+
return from...to if exclude_end?
|
125
|
+
|
126
|
+
from..to
|
127
|
+
end
|
128
|
+
|
129
|
+
##
|
130
|
+
# Converts the time range into a string.
|
131
|
+
#
|
132
|
+
# @return [String]
|
133
|
+
def to_s
|
134
|
+
[from, to].join('-')
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Minutely
|
4
|
+
class TimeRange
|
5
|
+
##
|
6
|
+
# A parser that tries to convert the input value to `Minutely::TimeRange`.
|
7
|
+
class Parser < Minutely::Parser
|
8
|
+
def parse
|
9
|
+
case value
|
10
|
+
when nil then nil
|
11
|
+
when Array then parse_array(value)
|
12
|
+
when Hash then parse_hash
|
13
|
+
when Range then parse_range
|
14
|
+
when String then parse_string
|
15
|
+
when TimeRange then value
|
16
|
+
else invalid_time_range!
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def invalid_time_range!
|
23
|
+
raise ArgumentError, 'invalid time range'
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse_array(items)
|
27
|
+
invalid_time_range! if items.length != 2 || include_empty_item?(items)
|
28
|
+
|
29
|
+
TimeRange.new(*items)
|
30
|
+
end
|
31
|
+
|
32
|
+
def parse_hash
|
33
|
+
return nil if value.empty?
|
34
|
+
|
35
|
+
parse_array(value.fetch_values(:from, :to))
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse_range
|
39
|
+
TimeRange.new(value.begin, value.end, exclude_end: value.exclude_end?)
|
40
|
+
end
|
41
|
+
|
42
|
+
def parse_string
|
43
|
+
return nil if value.empty?
|
44
|
+
|
45
|
+
items = value.split('-').map(&:strip)
|
46
|
+
parse_array(items)
|
47
|
+
end
|
48
|
+
|
49
|
+
def include_empty_item?(items)
|
50
|
+
items.any? do |item|
|
51
|
+
item.nil? || (item.respond_to?(:empty?) && item.empty?)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/minutely.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/minutely/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'minutely'
|
7
|
+
spec.version = Minutely::VERSION
|
8
|
+
spec.authors = ['Tobias Casper']
|
9
|
+
spec.email = ['tobias.casper@gmail.com']
|
10
|
+
spec.licenses = ['MIT']
|
11
|
+
|
12
|
+
spec.summary = 'Classes for representing the time of a day by using only hours and minutes'
|
13
|
+
spec.homepage = 'https://github.com/tlux/minutely'
|
14
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.7.0')
|
15
|
+
|
16
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
17
|
+
|
18
|
+
# Specify which files should be added to the gem when it is released.
|
19
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
20
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
21
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
22
|
+
f.match(%r{\A(?:test|spec|features)/})
|
23
|
+
end
|
24
|
+
end
|
25
|
+
spec.bindir = 'exe'
|
26
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
27
|
+
spec.require_paths = ['lib']
|
28
|
+
|
29
|
+
# Uncomment to register a new dependency of your gem
|
30
|
+
# spec.add_dependency "example-gem", "~> 1.0"
|
31
|
+
|
32
|
+
# For more information and examples about making a new gem, checkout our
|
33
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: minutely
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 3.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tobias Casper
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-04-09 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email:
|
15
|
+
- tobias.casper@gmail.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- ".coveralls.yml"
|
21
|
+
- ".gitignore"
|
22
|
+
- ".gitlab-ci.yml"
|
23
|
+
- ".rspec"
|
24
|
+
- ".rubocop.yml"
|
25
|
+
- ".tool-versions"
|
26
|
+
- ".travis.yml"
|
27
|
+
- ".vscode/extensions.json"
|
28
|
+
- ".vscode/settings.json"
|
29
|
+
- ".vscode/tasks.json"
|
30
|
+
- Gemfile
|
31
|
+
- Gemfile.lock
|
32
|
+
- LICENSE
|
33
|
+
- README.md
|
34
|
+
- Rakefile
|
35
|
+
- bin/console
|
36
|
+
- bin/setup
|
37
|
+
- lib/minutely.rb
|
38
|
+
- lib/minutely/parser.rb
|
39
|
+
- lib/minutely/string_as_json.rb
|
40
|
+
- lib/minutely/time.rb
|
41
|
+
- lib/minutely/time/parser.rb
|
42
|
+
- lib/minutely/time_range.rb
|
43
|
+
- lib/minutely/time_range/parser.rb
|
44
|
+
- lib/minutely/version.rb
|
45
|
+
- minutely.gemspec
|
46
|
+
homepage: https://github.com/tlux/minutely
|
47
|
+
licenses:
|
48
|
+
- MIT
|
49
|
+
metadata:
|
50
|
+
homepage_uri: https://github.com/tlux/minutely
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options: []
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: 2.7.0
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '0'
|
65
|
+
requirements: []
|
66
|
+
rubygems_version: 3.1.2
|
67
|
+
signing_key:
|
68
|
+
specification_version: 4
|
69
|
+
summary: Classes for representing the time of a day by using only hours and minutes
|
70
|
+
test_files: []
|