artic 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +3 -0
- data/.rubocop.yml +62 -0
- data/.travis.yml +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +141 -0
- data/Rakefile +6 -0
- data/artic.gemspec +33 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/artic.rb +23 -0
- data/lib/artic/availability.rb +110 -0
- data/lib/artic/calendar.rb +60 -0
- data/lib/artic/collection/availability_collection.rb +85 -0
- data/lib/artic/collection/occupation_collection.rb +94 -0
- data/lib/artic/occupation.rb +127 -0
- data/lib/artic/time_range.rb +132 -0
- data/lib/artic/version.rb +4 -0
- metadata +189 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 677bcdd6f44140488725e808cc21352cae9a637d
|
4
|
+
data.tar.gz: f8c0969a9fe9a90483bbe8daede9739b6f59f0c2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4892f61298533796efdb513c5f34a1a15cea5c999a9485c8da110df61482170acbbe7cbb1f33ecb84402671279a2060b8d052337f8b349a8cfa1c47edaa32f35
|
7
|
+
data.tar.gz: 7c4199ff8a5478a62eb36a94e02a5c0a2e15c0e929c402cb0d81df5da44abffe682b3e1f3e9988b6ec22515284f0cb837c037a44fbd864ca4ec40a1d20dc715e
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require: rubocop-rspec
|
2
|
+
|
3
|
+
AllCops:
|
4
|
+
TargetRubyVersion: 2.3
|
5
|
+
Include:
|
6
|
+
- '**/Gemfile'
|
7
|
+
- '**/Rakefile'
|
8
|
+
Exclude:
|
9
|
+
- 'bin/*'
|
10
|
+
- 'db/**/*'
|
11
|
+
- 'vendor/bundle/**/*'
|
12
|
+
- 'spec/spec_helper.rb'
|
13
|
+
- 'spec/rails_helper.rb'
|
14
|
+
- 'spec/support/**/*'
|
15
|
+
- 'config/**/*'
|
16
|
+
- 'Rakefile'
|
17
|
+
- 'Gemfile'
|
18
|
+
|
19
|
+
Style/BlockDelimiters:
|
20
|
+
Exclude:
|
21
|
+
- 'spec/**/*'
|
22
|
+
|
23
|
+
Style/AlignParameters:
|
24
|
+
EnforcedStyle: with_fixed_indentation
|
25
|
+
|
26
|
+
Style/ClosingParenthesisIndentation:
|
27
|
+
Enabled: false
|
28
|
+
|
29
|
+
Metrics/LineLength:
|
30
|
+
Max: 100
|
31
|
+
AllowURI: true
|
32
|
+
|
33
|
+
Style/FirstParameterIndentation:
|
34
|
+
Enabled: false
|
35
|
+
|
36
|
+
Style/MultilineMethodCallIndentation:
|
37
|
+
EnforcedStyle: indented
|
38
|
+
|
39
|
+
Style/IndentArray:
|
40
|
+
EnforcedStyle: consistent
|
41
|
+
|
42
|
+
Style/IndentHash:
|
43
|
+
EnforcedStyle: consistent
|
44
|
+
|
45
|
+
Style/SignalException:
|
46
|
+
EnforcedStyle: semantic
|
47
|
+
|
48
|
+
Style/BracesAroundHashParameters:
|
49
|
+
EnforcedStyle: context_dependent
|
50
|
+
|
51
|
+
Lint/EndAlignment:
|
52
|
+
AlignWith: variable
|
53
|
+
AutoCorrect: true
|
54
|
+
|
55
|
+
Style/AndOr:
|
56
|
+
EnforcedStyle: conditionals
|
57
|
+
|
58
|
+
Metrics/MethodLength:
|
59
|
+
Max: 15
|
60
|
+
|
61
|
+
RSpec/MessageExpectation:
|
62
|
+
Enabled: false
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Alessandro Desantis
|
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,141 @@
|
|
1
|
+
# A.R.TI.C.
|
2
|
+
|
3
|
+
[](https://rubygems.org/gems/artic)
|
4
|
+
[](https://travis-ci.org/alessandro1997/artic)
|
5
|
+
[](https://gemnasium.com/github.com/alessandro1997/artic)
|
6
|
+
[](https://codeclimate.com/github/alessandro1997/artic)
|
7
|
+
|
8
|
+
**A** **R**uby gem for **TI**me **C**omputations.
|
9
|
+
|
10
|
+
A.R.TI.C. can take questions like:
|
11
|
+
|
12
|
+
> If I'm available 9am-5pm on Mondays and I have a meeting from 10am to 1pm and another from 3pm
|
13
|
+
> to 4pm next Monday, when am I _free_ next Monday?
|
14
|
+
|
15
|
+
And give you an answer like:
|
16
|
+
|
17
|
+
> You are free in these time slots:
|
18
|
+
>
|
19
|
+
> - 9am-10am;
|
20
|
+
> - 1pm-3pm;
|
21
|
+
> - 4pm-5pm.
|
22
|
+
|
23
|
+
## Installation
|
24
|
+
|
25
|
+
Add this line to your application's Gemfile:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
gem 'artic'
|
29
|
+
```
|
30
|
+
|
31
|
+
And then execute:
|
32
|
+
|
33
|
+
$ bundle
|
34
|
+
|
35
|
+
Or install it yourself as:
|
36
|
+
|
37
|
+
$ gem install artic
|
38
|
+
|
39
|
+
## Usage
|
40
|
+
|
41
|
+
First of all, you will need to create a new calendar:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
calendar = Artic::Calendar.new
|
45
|
+
```
|
46
|
+
|
47
|
+
### Setting available times
|
48
|
+
|
49
|
+
Now you can start defining the free slots in your calendar:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
calendar.availabilities << Artic::Availability.new(:monday, '09:00'..'11:00')
|
53
|
+
calendar.availabilities << Artic::Availability.new(:monday, '11:00'..'13:00')
|
54
|
+
calendar.availabilities << Artic::Availability.new(:monday, '15:00'..'19:00')
|
55
|
+
```
|
56
|
+
|
57
|
+
If you want, you can also use specific dates in place of days of the week:
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
calendar.availabilities << Artic::Availability.new(Date.parse('2016-10-03'), '15:00'..'19:00')
|
61
|
+
```
|
62
|
+
|
63
|
+
Or you can mix the two! In this case, we won't consider the availability slots for that day of the
|
64
|
+
week when calculating availabilities:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
calendar.availabilities << Artic::Availability.new(:monday, '09:00'..'17:00')
|
68
|
+
|
69
|
+
# Only available 15-19 on Monday, October 3rd 2016.
|
70
|
+
calendar.availabilities << Artic::Availability.new(Date.parse('2016-10-03'), '15:00'..'19:00')
|
71
|
+
```
|
72
|
+
|
73
|
+
### Defining occupations
|
74
|
+
|
75
|
+
You can also define some specific slots when you will be busy with something:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
calendar.occupations << Artic::Occupation.new(Date.parse('2016-09-26'), '10:00'..'12:00'))
|
79
|
+
```
|
80
|
+
|
81
|
+
The times do not have to respect your availability slots:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
calendar.occupations << Artic::Occupation.new(Date.parse('2016-09-26'), '18:00'..'20:00'))
|
85
|
+
```
|
86
|
+
|
87
|
+
### Computing available slots
|
88
|
+
|
89
|
+
This is where the fun part begins. Suppose you want to get your work hours on Mondays:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
calendar.available_slots_on(:monday)
|
93
|
+
# => Artic::Collection::AvailabilityCollection[
|
94
|
+
# Artic::Availability<:monday, 09:00..13:00>,
|
95
|
+
# Artic::Availability<:monday, 15:00..19:00>
|
96
|
+
# ]
|
97
|
+
|
98
|
+
# We overrode this, remember?
|
99
|
+
calendar.available_slots_on(Date.parse('2016-10-03'))
|
100
|
+
# => Artic::Collection::AvailabilityCollection[
|
101
|
+
# Artic::Availability<2016-10-03 15:00..10:00>
|
102
|
+
# ]
|
103
|
+
```
|
104
|
+
|
105
|
+
### Computing free slots
|
106
|
+
|
107
|
+
Or maybe you want to see when you have time for a meeting a particular Monday?
|
108
|
+
|
109
|
+
In that case, use `#free_slots_on` and we'll take care of removing any occupied times from your
|
110
|
+
available slots:
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
calendar.free_slots_on(Date.parse('2016-09-26'))
|
114
|
+
# => Artic::Collection::AvailabilityCollection[
|
115
|
+
# Artic::Availability<2016-09-26 09:00..13:00>,
|
116
|
+
# Artic::Availability<2016-09-26 15:00..18:00>
|
117
|
+
# ]
|
118
|
+
```
|
119
|
+
|
120
|
+
## Caveats
|
121
|
+
|
122
|
+
- All times should be in the same timezone (ideally, UTC).
|
123
|
+
|
124
|
+
## Development
|
125
|
+
|
126
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run
|
127
|
+
the tests. You can also run `bin/console` for an interactive prompt that will allow you to
|
128
|
+
experiment.
|
129
|
+
|
130
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new
|
131
|
+
version, update the version number in `version.rb`, and then run `bundle exec rake release`, which
|
132
|
+
will create a git tag for the version, push git commits and tags, and push the `.gem` file to
|
133
|
+
[rubygems.org](https://rubygems.org).
|
134
|
+
|
135
|
+
## Contributing
|
136
|
+
|
137
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/alessandro1997/artic.
|
138
|
+
|
139
|
+
## License
|
140
|
+
|
141
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/artic.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'artic/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "artic"
|
8
|
+
spec.version = Artic::VERSION
|
9
|
+
spec.authors = ["Alessandro Desantis"]
|
10
|
+
spec.email = ["desa.alessandro@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{A Ruby gem for time computations.}
|
13
|
+
spec.homepage = "https://github.com/alessandro1997/artic"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
17
|
+
f.match(%r{^(test|spec|features)/})
|
18
|
+
end
|
19
|
+
spec.bindir = "exe"
|
20
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
spec.add_dependency 'tzinfo', '~> 1.2.2'
|
24
|
+
spec.add_dependency 'tzinfo-data', '~> 1.2016.7'
|
25
|
+
|
26
|
+
spec.add_development_dependency "bundler"
|
27
|
+
spec.add_development_dependency "rake"
|
28
|
+
spec.add_development_dependency "rspec"
|
29
|
+
spec.add_development_dependency "pry"
|
30
|
+
spec.add_development_dependency 'rubocop'
|
31
|
+
spec.add_development_dependency 'rubocop-rspec'
|
32
|
+
spec.add_development_dependency 'fuubar'
|
33
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "artic"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/lib/artic.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'tzinfo'
|
3
|
+
|
4
|
+
require 'artic/version'
|
5
|
+
|
6
|
+
require 'artic/time_range'
|
7
|
+
|
8
|
+
require 'artic/availability'
|
9
|
+
require 'artic/occupation'
|
10
|
+
|
11
|
+
require 'artic/collection/availability_collection'
|
12
|
+
require 'artic/collection/occupation_collection'
|
13
|
+
|
14
|
+
require 'artic/calendar'
|
15
|
+
|
16
|
+
# Artic is a Ruby gem for time computations.
|
17
|
+
#
|
18
|
+
# It can solve problems like: if I'm available 9am-5pm on Mondays and I have a meeting from 10am
|
19
|
+
# to 1pm and another from 3pm to 4pm next Monday, when am I free next Monday?
|
20
|
+
#
|
21
|
+
# @author Alessandro Desantis
|
22
|
+
module Artic
|
23
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Artic
|
3
|
+
# Availability represents a slot of time in a givn day of the week or date when you're available.
|
4
|
+
#
|
5
|
+
# @author Alessandro Desantis
|
6
|
+
class Availability
|
7
|
+
DAYS_OF_WEEK = %i(monday tuesday wednesday thursday friday saturday sunday).freeze
|
8
|
+
|
9
|
+
# @!attribute [r] day_of_week
|
10
|
+
# @todo Rename to +wday+
|
11
|
+
# @return [Symbol] the day of the week, as a lowercase symbol (e.g. +:monday+)
|
12
|
+
#
|
13
|
+
# @!attribute [r] date
|
14
|
+
# @return [Date] the date of this availability
|
15
|
+
#
|
16
|
+
# @!attribute [r] time_range
|
17
|
+
# @return [TimeRange] the time range of this availability
|
18
|
+
attr_reader :day_of_week, :date, :time_range
|
19
|
+
|
20
|
+
# Initializes the availability.
|
21
|
+
#
|
22
|
+
# @param dow_or_date [Symbol|Date] a day of the week (e.g. +:monday+) or date
|
23
|
+
# @param time_range [TimeRange|Range] a time range
|
24
|
+
#
|
25
|
+
# @raise [ArgumentError] if an invalid day of the week is passed
|
26
|
+
def initialize(dow_or_date, time_range)
|
27
|
+
@date = dow_or_date if dow_or_date.is_a?(Date)
|
28
|
+
@day_of_week = (@date ? @date.strftime('%A').downcase : dow_or_date).to_sym
|
29
|
+
@time_range = TimeRange.build(time_range)
|
30
|
+
|
31
|
+
validate_day_of_week
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns the identifier used to create this availability (i.e. either a day of the week or
|
35
|
+
# date).
|
36
|
+
#
|
37
|
+
# @return [Date|Symbol]
|
38
|
+
def identifier
|
39
|
+
date || day_of_week
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns whether the availability is appliable to the given date (i.e. the date is the same
|
43
|
+
# or the weekday is the same).
|
44
|
+
#
|
45
|
+
# @param date [Date]
|
46
|
+
#
|
47
|
+
# @return [Boolean]
|
48
|
+
def for_date?(date)
|
49
|
+
if self.date
|
50
|
+
self.date == date
|
51
|
+
else
|
52
|
+
day_of_week == date.strftime('%A').downcase.to_sym
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Determines whether this availability and the one passed as an argument represent the same
|
57
|
+
# day/time range combination, by checking for equality of both the identifier and the time
|
58
|
+
# range.
|
59
|
+
#
|
60
|
+
# @param other [Availability]
|
61
|
+
#
|
62
|
+
# @return [Boolean]
|
63
|
+
def ==(other)
|
64
|
+
identifier == other.identifier && time_range == other.time_range
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns whether this availability should be before, after or in the same position of the
|
68
|
+
# availability passed as an argument, by following these rules:
|
69
|
+
#
|
70
|
+
# * availabilities with a weekday come before those with a date;
|
71
|
+
# * if both availabilities are for a weekday, the day and time ranges are compared;
|
72
|
+
# * if both availabilities are for a date, the date and time ranges are compared.
|
73
|
+
#
|
74
|
+
# @return [Fixnum] -1 if this availability should come before the argument, 0 if it should
|
75
|
+
# be at the same position, 1 if it should come after the argument.
|
76
|
+
#
|
77
|
+
# @see TimeRange#<=>
|
78
|
+
def <=>(other)
|
79
|
+
# availabilities for weekdays come before availabilities for specific dates
|
80
|
+
return -1 if date.nil? && !other.date.nil?
|
81
|
+
return 1 if !date.nil? && other.date.nil?
|
82
|
+
|
83
|
+
if date.nil? && other.date.nil? # both availabilities are for a weekday
|
84
|
+
if day_of_week == other.day_of_week # availabilities are for the same weekday
|
85
|
+
time_range.min <=> other.time_range.min # compare times
|
86
|
+
else # availabilities are for different weekdays
|
87
|
+
index1 = DAYS_OF_WEEK.index(day_of_week)
|
88
|
+
index2 = DAYS_OF_WEEK.index(other.day_of_week)
|
89
|
+
|
90
|
+
index1 <=> index2 # compares weekdays
|
91
|
+
end
|
92
|
+
else # both availabilities are for a date
|
93
|
+
if date == other.date # both availabilities are for the same date
|
94
|
+
time_range.min <=> other.time_range.min # compare times
|
95
|
+
else # availabilities are for different dates
|
96
|
+
date <=> other.date # compare dates
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def validate_day_of_week
|
104
|
+
fail(
|
105
|
+
ArgumentError,
|
106
|
+
"#{day_of_week} is not a valid day of the week"
|
107
|
+
) unless DAYS_OF_WEEK.include?(day_of_week)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Artic
|
3
|
+
# A calendar keeps track of both your availabilities and your occupations.
|
4
|
+
#
|
5
|
+
# @author Alessandro Desantis
|
6
|
+
class Calendar
|
7
|
+
# @!attribute [r] availabilities
|
8
|
+
# @return [Collection::AvailabilityCollection]
|
9
|
+
#
|
10
|
+
# @!attribute [r] occupations
|
11
|
+
# @return [Collection::OccupationCollection]
|
12
|
+
attr_reader :availabilities, :occupations
|
13
|
+
|
14
|
+
# Initializes the calendar.
|
15
|
+
def initialize
|
16
|
+
@availabilities = Collection::AvailabilityCollection.new
|
17
|
+
@occupations = Collection::OccupationCollection.new
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns the slots available on the given day of the week or date, including any slots that
|
21
|
+
# might be occupied on the given date.
|
22
|
+
#
|
23
|
+
# If the passed argument is an instance of +Date+ but no availabilities have been defined
|
24
|
+
# for that specific date, returns any availabilities defined for that day of the week.
|
25
|
+
#
|
26
|
+
# @param dow_or_date [Date|Symbol] a day of the week or date
|
27
|
+
#
|
28
|
+
# @return Collection::AvailabilityCollection
|
29
|
+
#
|
30
|
+
# @example
|
31
|
+
# calendar.available_slots_on(:monday) # => #<Artic::Collection::AvailabilityCollection>
|
32
|
+
# @example
|
33
|
+
# calendar.available_slots_on(Date.tomorrow) # => #<Artic::Collection::AvailabilityCollection>
|
34
|
+
#
|
35
|
+
# @see Collection::AvailabilityCollection#normalize
|
36
|
+
def available_slots_on(dow_or_date)
|
37
|
+
if !availabilities.identifier?(dow_or_date) && dow_or_date.is_a?(Date) && availabilities.identifier?(dow_or_date.strftime('%A').downcase)
|
38
|
+
return available_slots_on(dow_or_date.strftime('%A').downcase)
|
39
|
+
end
|
40
|
+
|
41
|
+
availabilities.normalize dow_or_date
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns the slots free on the given date, computed by calling {Occupation#bisect} on each
|
45
|
+
# of the availability slots.
|
46
|
+
#
|
47
|
+
# @param date [Date]
|
48
|
+
#
|
49
|
+
# @return Collection::AvailabilityCollection
|
50
|
+
#
|
51
|
+
# @see Collection::OccupationCollection#bisect
|
52
|
+
def free_slots_on(date)
|
53
|
+
availabilities = available_slots_on(date).flat_map do |availability|
|
54
|
+
occupations.bisect(availability)
|
55
|
+
end
|
56
|
+
|
57
|
+
Collection::AvailabilityCollection.new availabilities
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Artic
|
3
|
+
module Collection
|
4
|
+
# Keeps a collection of availabilities and performs calculations on them.
|
5
|
+
#
|
6
|
+
# @author Alessandro Desantis
|
7
|
+
class AvailabilityCollection < Array
|
8
|
+
# Returns all the identifiers in this collection.
|
9
|
+
#
|
10
|
+
# @return [Array<Symbol|Date>]
|
11
|
+
def identifiers
|
12
|
+
map(&:identifier).uniq
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns whether an identifier exists in this collection.
|
16
|
+
#
|
17
|
+
# @param identifier [Symbol,Date]
|
18
|
+
#
|
19
|
+
# @return [Boolean]
|
20
|
+
def identifier?(identifier)
|
21
|
+
identifiers.include? cast_identifier(identifier)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns all the availabilities with the given identifier, without normalizing them.
|
25
|
+
#
|
26
|
+
# @param identifier [Symbol|Date] a weekday or a date
|
27
|
+
#
|
28
|
+
# @return AvailabilityCollection
|
29
|
+
def by_identifier(identifier)
|
30
|
+
identifier = cast_identifier identifier
|
31
|
+
availabilities = select { |availability| availability.identifier == identifier }
|
32
|
+
self.class.new availabilities
|
33
|
+
end
|
34
|
+
|
35
|
+
# Normalizes all the availabilities with the given identifier in this collection by sorting
|
36
|
+
# them and merging any contiguous availability slots.
|
37
|
+
#
|
38
|
+
# @param identifier [Symbol|Date]
|
39
|
+
#
|
40
|
+
# @return AvailabilityCollection
|
41
|
+
def normalize(identifier)
|
42
|
+
availabilities = by_identifier(identifier).sort
|
43
|
+
|
44
|
+
normalized_availabilities = availabilities.inject([]) do |accumulator, availability|
|
45
|
+
next (accumulator << availability) if accumulator.empty?
|
46
|
+
|
47
|
+
last_availability = accumulator.pop
|
48
|
+
|
49
|
+
next (
|
50
|
+
accumulator + [last_availability, availability]
|
51
|
+
) unless last_availability.time_range.overlaps?(availability.time_range)
|
52
|
+
|
53
|
+
new_time_range = Range.new(
|
54
|
+
[last_availability.time_range.min, availability.time_range.min].min,
|
55
|
+
[last_availability.time_range.max, availability.time_range.max].max
|
56
|
+
)
|
57
|
+
|
58
|
+
accumulator << Availability.new(availability.identifier, new_time_range)
|
59
|
+
end
|
60
|
+
|
61
|
+
self.class.new normalized_availabilities
|
62
|
+
end
|
63
|
+
|
64
|
+
# Normalizes all the availabilities in this collection by sorting them and merging any
|
65
|
+
# contiguous availability slots.
|
66
|
+
#
|
67
|
+
# @return AvailabilityCollection
|
68
|
+
#
|
69
|
+
# @see #normalize
|
70
|
+
def normalize_all
|
71
|
+
normalized_availabilities = identifiers.flat_map do |identifier|
|
72
|
+
normalize identifier
|
73
|
+
end
|
74
|
+
|
75
|
+
self.class.new normalized_availabilities.sort
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def cast_identifier(identifier)
|
81
|
+
identifier.is_a?(String) ? identifier.to_sym : identifier
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Artic
|
3
|
+
module Collection
|
4
|
+
# Keeps a collection of occupations.
|
5
|
+
#
|
6
|
+
# @author Alessandro Desantis
|
7
|
+
class OccupationCollection < Array
|
8
|
+
# Returns all the dates in this collection.
|
9
|
+
#
|
10
|
+
# @return [Array<Date>]
|
11
|
+
def dates
|
12
|
+
map(&:date).uniq
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns whether a date exists in this collection.
|
16
|
+
#
|
17
|
+
# @param date [Date]
|
18
|
+
#
|
19
|
+
# @return [Boolean]
|
20
|
+
def date?(date)
|
21
|
+
dates.include? date
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns all the occupations for the given date, without normalizing them.
|
25
|
+
#
|
26
|
+
# @param date [Date]
|
27
|
+
#
|
28
|
+
# @return OccupationCollection
|
29
|
+
def by_date(date)
|
30
|
+
occupations = select { |occupation| occupation.date == date }
|
31
|
+
self.class.new occupations
|
32
|
+
end
|
33
|
+
|
34
|
+
# Normalizes all the occupations with the given identifier in this collection by sorting
|
35
|
+
# them and merging any contiguous availability slots.
|
36
|
+
#
|
37
|
+
# @param date [Date]
|
38
|
+
#
|
39
|
+
# @return AvailabilityCollection
|
40
|
+
def normalize(date)
|
41
|
+
occupations = by_date(date).sort
|
42
|
+
|
43
|
+
normalized_occupations = occupations.inject([]) do |accumulator, occupation|
|
44
|
+
next (accumulator << occupation) if accumulator.empty?
|
45
|
+
|
46
|
+
last_occupation = accumulator.pop
|
47
|
+
|
48
|
+
next (
|
49
|
+
accumulator + [last_occupation, occupation]
|
50
|
+
) unless last_occupation.time_range.overlaps?(occupation.time_range)
|
51
|
+
|
52
|
+
new_time_range = Range.new(
|
53
|
+
[last_occupation.time_range.min, occupation.time_range.min].min,
|
54
|
+
[last_occupation.time_range.max, occupation.time_range.max].max
|
55
|
+
)
|
56
|
+
|
57
|
+
accumulator << Occupation.new(occupation.date, new_time_range)
|
58
|
+
end
|
59
|
+
|
60
|
+
self.class.new normalized_occupations
|
61
|
+
end
|
62
|
+
|
63
|
+
# Normalizes all the occupations in this collection by sorting them and merging any
|
64
|
+
# contiguous availability slots.
|
65
|
+
#
|
66
|
+
# @return OccupationCollection
|
67
|
+
#
|
68
|
+
# @see #normalize
|
69
|
+
def normalize_all
|
70
|
+
normalized_occupations = dates.sort.flat_map do |date|
|
71
|
+
normalize date
|
72
|
+
end
|
73
|
+
|
74
|
+
self.class.new normalized_occupations
|
75
|
+
end
|
76
|
+
|
77
|
+
# Sorts the occupations in the collection, then bisects the availability until all the
|
78
|
+
# occupations have been accounted for.
|
79
|
+
#
|
80
|
+
# @param availability [Availability]
|
81
|
+
#
|
82
|
+
# @return AvailabilityCollection
|
83
|
+
#
|
84
|
+
# @see Occupation#bisect
|
85
|
+
def bisect(availability)
|
86
|
+
availabilities = normalize_all.inject([availability]) do |accumulator, occupation|
|
87
|
+
accumulator + occupation.bisect(accumulator.pop)
|
88
|
+
end
|
89
|
+
|
90
|
+
AvailabilityCollection.new availabilities
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Artic
|
3
|
+
# An occupation represents a slot of time in a given date where you are not available.
|
4
|
+
#
|
5
|
+
# @author Alessandro Desantis
|
6
|
+
class Occupation
|
7
|
+
attr_reader :date, :time_range
|
8
|
+
|
9
|
+
# Initializes the occupation.
|
10
|
+
#
|
11
|
+
# @param date [Date] the date of the occupation
|
12
|
+
# @param time_range [TimeRange|Range] the time range of the occupation
|
13
|
+
def initialize(date, time_range)
|
14
|
+
@date = date
|
15
|
+
@time_range = TimeRange.build(time_range)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Converts the occupation to a range of +DateTime+ objects.
|
19
|
+
#
|
20
|
+
# @return [Range]
|
21
|
+
#
|
22
|
+
# @see TimeRange#with_date
|
23
|
+
def to_range
|
24
|
+
time_range.with_date(date)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns whether the occupation overlaps the availability (i.e. whether there's at least one
|
28
|
+
# moment in time shared by both the availability and the occupation).
|
29
|
+
#
|
30
|
+
# @param availability [Availability]
|
31
|
+
#
|
32
|
+
# @return [Boolean]
|
33
|
+
def overlaps?(availability)
|
34
|
+
return false unless availability.for_date?(date)
|
35
|
+
|
36
|
+
availability_range = availability.time_range.with_date(date)
|
37
|
+
(availability_range.min <= to_range.max) && (availability_range.max >= to_range.min)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns whether the occupation covers the availability (i.e. whether all moments of the
|
41
|
+
# availability are also part of the occupation).
|
42
|
+
#
|
43
|
+
# @param availability [Availability]
|
44
|
+
#
|
45
|
+
# @return [Boolean]
|
46
|
+
def covers?(availability)
|
47
|
+
return false unless availability.for_date?(date)
|
48
|
+
|
49
|
+
availability_range = availability.time_range.with_date(date)
|
50
|
+
to_range.min <= availability_range.min && to_range.max >= availability_range.max
|
51
|
+
end
|
52
|
+
|
53
|
+
# Determines whether this occupation and the one passed as an argument represent the same
|
54
|
+
# day/time range combination, by checking for equality of both the date and the time
|
55
|
+
# range.
|
56
|
+
#
|
57
|
+
# @param other [Occupation]
|
58
|
+
#
|
59
|
+
# @return [Boolean]
|
60
|
+
def ==(other)
|
61
|
+
date == other.date && time_range == other.time_range
|
62
|
+
end
|
63
|
+
|
64
|
+
# Compares this occupation with another one by comparing their ranges' start.
|
65
|
+
#
|
66
|
+
# @param other [Occupation]
|
67
|
+
#
|
68
|
+
# @return [Fixnum] -1 if this occupation should come before the other one, 0 if it should be
|
69
|
+
# in the same position, 1 if it should come after the other one
|
70
|
+
def <=>(other)
|
71
|
+
to_range.min <=> other.to_range.min
|
72
|
+
end
|
73
|
+
|
74
|
+
# Reduces the time range of the given availability or bisects it into two availabilities,
|
75
|
+
# depending on where the occupation falls within the availability.
|
76
|
+
#
|
77
|
+
# If the occupation completely covers the availability, returns an empty collection.
|
78
|
+
#
|
79
|
+
# If the occupation does not overlap the availability at all, returns a collection with
|
80
|
+
# the original availability.
|
81
|
+
#
|
82
|
+
# @param availability [Availability]
|
83
|
+
#
|
84
|
+
# @return Collection::AvailabilityCollection a collection of availabilities where the time
|
85
|
+
# slot assigned to the occupation has been removed
|
86
|
+
#
|
87
|
+
# @example
|
88
|
+
# occupation = Artic::Occupation.new(Date.today, '08:00'..'14:00')
|
89
|
+
# availability = Artic::Availability.new(Date.today, '09:00'..'18:00')
|
90
|
+
#
|
91
|
+
# occupation.bisect(availability)
|
92
|
+
# # => [
|
93
|
+
# # #<Artic::Availability 14:00..18:00>
|
94
|
+
# # ]
|
95
|
+
#
|
96
|
+
# @example
|
97
|
+
# occupation = Artic::Occupation.new(Date.today, '12:00'..'14:00')
|
98
|
+
# availability = Artic::Availability.new(Date.today, '09:00'..'18:00')
|
99
|
+
#
|
100
|
+
# occupation.bisect(availability)
|
101
|
+
# # => [
|
102
|
+
# # #<Artic::Availability 09:00..12:00>,
|
103
|
+
# # #<Artic::Availability 14:00..18:00>
|
104
|
+
# # ]
|
105
|
+
#
|
106
|
+
# @example
|
107
|
+
# occupation = Artic::Occupation.new(Date.today, '16:00'..'20:00')
|
108
|
+
# availability = Artic::Availability.new(Date.today, '09:00'..'18:00')
|
109
|
+
#
|
110
|
+
# occupation.bisect(availability)
|
111
|
+
# # => [
|
112
|
+
# # #<Artic::Availability 09:00..16:00>
|
113
|
+
# # ]
|
114
|
+
def bisect(availability)
|
115
|
+
return Collection::AvailabilityCollection.new if covers?(availability)
|
116
|
+
return Collection::AvailabilityCollection.new([availability]) unless overlaps?(availability)
|
117
|
+
|
118
|
+
bisected_ranges = time_range.bisect(availability.time_range)
|
119
|
+
|
120
|
+
availabilities = bisected_ranges.map do |bisected_range|
|
121
|
+
Availability.new(date, bisected_range)
|
122
|
+
end
|
123
|
+
|
124
|
+
Collection::AvailabilityCollection.new availabilities
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Artic
|
3
|
+
# Represents a range of two times in the same day (e.g. 09:00-17:00).
|
4
|
+
#
|
5
|
+
# @author Alessandro Desantis
|
6
|
+
class TimeRange < Range
|
7
|
+
TIME_REGEX = /\A^(0\d|1\d|2[0-3]):[0-5]\d$\z/
|
8
|
+
|
9
|
+
class << self
|
10
|
+
# Builds a time range from the provided value.
|
11
|
+
#
|
12
|
+
# @param range [Range|TimeRange] a range of times (e.g. +'09:00'..'18:00'+ or
|
13
|
+
# a +TimeRange+ object)
|
14
|
+
#
|
15
|
+
# @return [TimeRange] the passed time range or a new time range
|
16
|
+
def build(range)
|
17
|
+
range.is_a?(TimeRange) ? range : TimeRange.new(range.min, range.max)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Initializes a new range.
|
22
|
+
#
|
23
|
+
# @param min [String] the start time (in HH:MM format)
|
24
|
+
# @param max [String] the end time (in HH:MM format)
|
25
|
+
#
|
26
|
+
# @raise [ArgumentError] if the start time or the end time is invalid
|
27
|
+
# @raise [ArgumentError] if the start time is after the end time
|
28
|
+
def initialize(min, max)
|
29
|
+
super
|
30
|
+
validate_range
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns whether the two ranges overlap (i.e. whether there's at least a moment in time that
|
34
|
+
# belongs to both ranges).
|
35
|
+
#
|
36
|
+
# @param other [TimeRange]
|
37
|
+
#
|
38
|
+
# @return [Boolean]
|
39
|
+
def overlaps?(other)
|
40
|
+
min <= other.max && max >= other.min
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns whether this range completely covers the one given.
|
44
|
+
#
|
45
|
+
# @param other [TimeRange]
|
46
|
+
#
|
47
|
+
# @return [Boolean]
|
48
|
+
def covers?(other)
|
49
|
+
min <= other.min && max >= other.max
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns a range of +DateTime+ objects for this time range.
|
53
|
+
#
|
54
|
+
# @param date [Date] the date to use for the range
|
55
|
+
#
|
56
|
+
# @return [Range]
|
57
|
+
def with_date(date)
|
58
|
+
Range.new(
|
59
|
+
DateTime.parse("#{date} #{min}"),
|
60
|
+
DateTime.parse("#{date} #{max}")
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Uses this range to bisect another.
|
65
|
+
#
|
66
|
+
# If this range does not overlap the other, returns an array with the original range.
|
67
|
+
#
|
68
|
+
# If this range completely covers the other, returns an empty array.
|
69
|
+
#
|
70
|
+
# @param other [TimeRange]
|
71
|
+
#
|
72
|
+
# @return [Array<TimeRange>]
|
73
|
+
#
|
74
|
+
# @example
|
75
|
+
# range1 = TimeRange.new('10:00'..'12:00')
|
76
|
+
# range2 = TimeRange.new('09:00'..'18:00')
|
77
|
+
# range3 = TimeRange.new('08:00'..'11:00')
|
78
|
+
#
|
79
|
+
# range1.bisect(range2)
|
80
|
+
# # => [
|
81
|
+
# # #<TimeRange 09:00..10:00>,
|
82
|
+
# # #<TimeRange 12:00..18:00>
|
83
|
+
# # ]
|
84
|
+
#
|
85
|
+
# range2.bisect(range1)
|
86
|
+
# # => []
|
87
|
+
#
|
88
|
+
# range3.bisect(range2)
|
89
|
+
# # => [
|
90
|
+
# # #<TimeRange 11:00..18:00>
|
91
|
+
# # ]
|
92
|
+
#
|
93
|
+
# range2.bisect(range3)
|
94
|
+
# # => [
|
95
|
+
# # #<TimeRange 08:00..09:00>
|
96
|
+
# # ]
|
97
|
+
def bisect(other)
|
98
|
+
return [other] unless overlaps?(other)
|
99
|
+
return [] if covers?(other)
|
100
|
+
|
101
|
+
if min <= other.min && max <= other.max
|
102
|
+
[max..other.max]
|
103
|
+
elsif min >= other.min && max <= other.max
|
104
|
+
[
|
105
|
+
(other.min..min),
|
106
|
+
(max..other.max)
|
107
|
+
]
|
108
|
+
elsif min >= other.min && max >= other.max
|
109
|
+
[other.min..min]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def validate_range
|
116
|
+
fail(
|
117
|
+
ArgumentError,
|
118
|
+
"#{min} is not a valid time"
|
119
|
+
) unless min =~ TIME_REGEX
|
120
|
+
|
121
|
+
fail(
|
122
|
+
ArgumentError,
|
123
|
+
"#{max} is not a valid time"
|
124
|
+
) unless max =~ TIME_REGEX
|
125
|
+
|
126
|
+
fail(
|
127
|
+
ArgumentError,
|
128
|
+
"#{min} is greater than #{max}"
|
129
|
+
) if min > max
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
metadata
ADDED
@@ -0,0 +1,189 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: artic
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alessandro Desantis
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-10-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: tzinfo
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.2.2
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.2.2
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: tzinfo-data
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.2016.7
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.2016.7
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
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
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
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'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pry
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rubocop-rspec
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: fuubar
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
description:
|
140
|
+
email:
|
141
|
+
- desa.alessandro@gmail.com
|
142
|
+
executables: []
|
143
|
+
extensions: []
|
144
|
+
extra_rdoc_files: []
|
145
|
+
files:
|
146
|
+
- ".gitignore"
|
147
|
+
- ".rspec"
|
148
|
+
- ".rubocop.yml"
|
149
|
+
- ".travis.yml"
|
150
|
+
- Gemfile
|
151
|
+
- LICENSE.txt
|
152
|
+
- README.md
|
153
|
+
- Rakefile
|
154
|
+
- artic.gemspec
|
155
|
+
- bin/console
|
156
|
+
- bin/setup
|
157
|
+
- lib/artic.rb
|
158
|
+
- lib/artic/availability.rb
|
159
|
+
- lib/artic/calendar.rb
|
160
|
+
- lib/artic/collection/availability_collection.rb
|
161
|
+
- lib/artic/collection/occupation_collection.rb
|
162
|
+
- lib/artic/occupation.rb
|
163
|
+
- lib/artic/time_range.rb
|
164
|
+
- lib/artic/version.rb
|
165
|
+
homepage: https://github.com/alessandro1997/artic
|
166
|
+
licenses:
|
167
|
+
- MIT
|
168
|
+
metadata: {}
|
169
|
+
post_install_message:
|
170
|
+
rdoc_options: []
|
171
|
+
require_paths:
|
172
|
+
- lib
|
173
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
174
|
+
requirements:
|
175
|
+
- - ">="
|
176
|
+
- !ruby/object:Gem::Version
|
177
|
+
version: '0'
|
178
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
179
|
+
requirements:
|
180
|
+
- - ">="
|
181
|
+
- !ruby/object:Gem::Version
|
182
|
+
version: '0'
|
183
|
+
requirements: []
|
184
|
+
rubyforge_project:
|
185
|
+
rubygems_version: 2.5.1
|
186
|
+
signing_key:
|
187
|
+
specification_version: 4
|
188
|
+
summary: A Ruby gem for time computations.
|
189
|
+
test_files: []
|