artic 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://img.shields.io/gem/v/artic.svg?maxAge=3600&style=flat-square)](https://rubygems.org/gems/artic)
|
4
|
+
[![Build Status](https://img.shields.io/travis/alessandro1997/artic.svg?maxAge=3600&style=flat-square)](https://travis-ci.org/alessandro1997/artic)
|
5
|
+
[![Dependency Status](https://img.shields.io/gemnasium/alessandro1997/artic.svg?maxAge=3600&style=flat-square)](https://gemnasium.com/github.com/alessandro1997/artic)
|
6
|
+
[![Code Climate](https://img.shields.io/codeclimate/github/alessandro1997/artic.svg?maxAge=3600&style=flat-square)](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: []
|