artic 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 677bcdd6f44140488725e808cc21352cae9a637d
4
+ data.tar.gz: f8c0969a9fe9a90483bbe8daede9739b6f59f0c2
5
+ SHA512:
6
+ metadata.gz: 4892f61298533796efdb513c5f34a1a15cea5c999a9485c8da110df61482170acbbe7cbb1f33ecb84402671279a2060b8d052337f8b349a8cfa1c47edaa32f35
7
+ data.tar.gz: 7c4199ff8a5478a62eb36a94e02a5c0a2e15c0e929c402cb0d81df5da44abffe682b3e1f3e9988b6ec22515284f0cb837c037a44fbd864ca4ec40a1d20dc715e
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /spec/examples.txt
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format Fuubar
2
+ --color
3
+ --require spec_helper
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
@@ -0,0 +1,9 @@
1
+ rvm:
2
+ - 2.3.0
3
+ before_install:
4
+ - gem update --system
5
+ - gem update bundler
6
+ - gem cleanup bundler
7
+ branches:
8
+ only:
9
+ - master
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in agenda.gemspec
4
+ gemspec
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
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/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
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module Artic
3
+ VERSION = '1.0.0'
4
+ 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: []