availabiliter 0.2.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e4398863d21393841d616f4fd47c7cfd3d5c7c3c04839735e4508cae695f07f1
4
- data.tar.gz: dc7736e7c8a5ee4498719a8976e73b5923b35b0f32154bac151009db6eeb98b7
3
+ metadata.gz: acbff7e9ed2d5615c2298a6b8802fe20920b594a19292fdff70d933100610a4d
4
+ data.tar.gz: 71b01103cb94ebfd73733135d95758bbc4fbd0c4936d2199f7917d3f9be06904
5
5
  SHA512:
6
- metadata.gz: 60eb5709da8cd9dedee6e7246c517f27117e23aa04cfeb69e8945e67acdaada2e5b86e1b0c5ba67a94dcd2e88b06841b8881b5a9bd179e8d76f090cb09c66240
7
- data.tar.gz: cf00b270e3afbef8253100d3784ce435ada6f2ef69a00ba6af41e01e26a6473295f339dc6f9ceaaa1f21ec9136c806c1d7bccb8c06e54bc6233b9773d40a89d5
6
+ metadata.gz: b5658fb8a5aae7db823d0767b6eff6ecd65fc305258e2e79ec483df63d8bed008fdca0a54ec1ccdb1a0079fabdc1887262f130401e77a716f297d959ef5fbe64
7
+ data.tar.gz: 5c0bf3d49fd0064b597cbfd5d83eccbebe6bfebf2a78c265904bb9e9ea044d00fb027e6588e38337bf61c6884faa41340851185089b426c3d3897cddcecae0a4
data/.gitignore CHANGED
@@ -12,4 +12,10 @@
12
12
  .rspec_status
13
13
 
14
14
  # byebug historic
15
- .byebug_history
15
+ .byebug_history
16
+
17
+ #irb history
18
+ .irb_history
19
+
20
+ # rbenv local setting
21
+ .ruby-version
data/.rubocop.yml CHANGED
@@ -1,5 +1,6 @@
1
1
  AllCops:
2
2
  TargetRubyVersion: 2.7.2
3
+ NewCops: enable
3
4
 
4
5
  Style/StringLiterals:
5
6
  Enabled: true
@@ -11,7 +12,13 @@ Style/StringLiteralsInInterpolation:
11
12
 
12
13
  Layout/LineLength:
13
14
  Max: 120
15
+ Exclude:
16
+ - 'spec/**/*'
17
+ - 'rakelib/*'
14
18
 
15
19
  Metrics/BlockLength:
16
20
  Exclude:
17
- - 'spec/**/*'
21
+ - 'spec/**/*'
22
+
23
+ Style/FrozenStringLiteralComment:
24
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## Future releases
2
- - DateTime calculation
2
+ - Easy integration in Rails
3
+ - Allow to pick your own infinity format
4
+
5
+ ## 1.0.0
6
+
7
+ - Adds support for Time and Unix timestamps
8
+ - Allows to pick preferred format for the output: Time or Unix timestamps.
9
+ - Allow to pick your timezone when choosing time format
10
+ - Ruby ranges are not accepted anymore as input
11
+ - Float::INFINITY replaces Nil class to represent time infinity
12
+ - Major architectural internal changes to improve maintainability
3
13
 
4
14
  ## 0.2.0
5
15
  - Active Support dependency removal
data/README.md CHANGED
@@ -1,54 +1,177 @@
1
1
  # Availabiliter
2
2
 
3
- Availabiliter is a Ruby availability calculator for date ranges. It handles the following edge cases:
3
+ Calulating gaps/availabilities between two time slots should be easy right ? Well very often it is not because of various edge cases like:
4
+ - endless or beginless time slots
5
+ - overlapping time slots
6
+ - consecutive time slots
7
+ - one second time slots
8
+ - different time zones
4
9
 
5
- - endless date ranges
6
- - overlapping date ranges
7
- - consecutive date ranges
8
- - one day date ranges
10
+ Availabiter is a tested and documented Ruby libary which provides an easy way of performing this calculation while handling all edge cases above.
9
11
 
10
- It also allows to calculate availabilities from a dedicated date in time.
12
+ ## Basic example
11
13
 
12
- ## Installation
14
+ For this given array of time slots...
13
15
 
14
- Add this line to your application's Gemfile:
16
+ ```ruby
17
+ shift_1 = [Time.new(2021, 1, 1, 8), Time.new(2021, 1, 1, 12)]
18
+ shift_2 = [Time.new(2021, 1, 1, 14), Time.new(2021, 1, 1, 18)]
19
+ working_hours = [shift_1, shift_2]
20
+ ```
21
+
22
+ ...you can calculate the gaps between those time slots by doing the following.
15
23
 
16
24
  ```ruby
17
- gem 'availabiliter'
25
+ Availabiliter.call(working_hours, format: :time)
26
+ # returns:
27
+ # [
28
+ # [-Infinity, 2021-01-01 07:59:59 +0100],
29
+ # [2021-01-01 12:00:01 +0100, 2021-01-01 13:59:59 +0100],
30
+ # [2021-01-01 18:00:01 +0100, Infinity]
31
+ # ]
18
32
  ```
19
33
 
20
- And then execute:
21
34
 
22
- $ bundle install
35
+ ## Features
23
36
 
24
- Or install it yourself as:
37
+ ### Overlapping time slots
25
38
 
26
- $ gem install availabiliter
39
+ ```ruby
40
+ shift_1 = [Time.new(2021, 1, 1, 12), Time.new(2021, 1, 1, 20)]
41
+ shift_2 = [Time.new(2021, 1, 1, 10), Time.new(2021, 1, 1, 22)]
42
+ available_hours = [shift_1, shift_2]
43
+
44
+ Availabiliter.call(working_hours, format: :time)
45
+ # => [[-Infinity, 2021-01-01 09:59:59 +0100], [2021-01-01 22:00:01 +0100, Infinity]]
46
+ ```
47
+
48
+ ## Consecutive time slots
49
+
50
+ ```ruby
51
+ shift_1 = [Time.new(2021, 1, 1, 12), Time.new(2021, 1, 1, 20, 0, 1)]
52
+ shift_2 = [Time.new(2021, 1, 1, 20, 0, 1), Time.new(2021, 1, 1, 22)]
53
+ working_hours = [shift_1, shift_2]
54
+
55
+ Availabiliter.call(working_hours, format: :time)
56
+ # => [[-Infinity, 2021-01-01 11:59:59 +0100], [2021-01-01 22:00:01 +0100, Infinity]]
57
+ ```
58
+
59
+ ### Endless or beginless time slots
60
+
61
+ ```ruby
62
+ shift_1 = [-Float::INFINITY, Time.new(2021, 1, 1, 12)]
63
+ shift_2 = [Time.new(2021, 1, 2, 8), Float::INFINITY]
64
+ working_hours = [shift_1, shift_2]
65
+
66
+ Availabiliter.call(working_hours, format: :time)
67
+ # => [[2021-01-01 12:00:01 +0100, 2021-01-02 07:59:59 +0100]]
68
+ ```
69
+
70
+ ### Boundaries
71
+
72
+ ```ruby
73
+ minimum_availability_start = Time.new(2021, 1, 1, 6)
74
+ maximum_availability_end = Time.new(2021, 1, 1, 11)
75
+
76
+ shift_1 = [Time.new(2021, 1, 1, 8), Time.new(2021, 1, 1, 12)]
77
+ working_hours = [shift_1]
78
+
79
+ Availabiliter.call(
80
+ working_hours,
81
+ minimum_availability_start: minimum_availability_start,
82
+ maximum_availability_end: maximum_availability_end,
83
+ format: :time
84
+ )
85
+ # => [[2021-01-01 06:00:00 +0100, 2021-01-01 07:59:59 +0100]]
86
+ ```
87
+
88
+ ### Format
89
+
90
+ ```ruby
91
+ shift = [Time.new(2021, 1, 1, 8), Time.new(2021, 1, 1, 12)]
92
+ working_hours = [shift]
93
+
94
+ Availabiliter.call(working_hours)
95
+ # => [[-Infinity, 1609484399], [1609498801, Infinity]]
96
+
97
+ Availabiliter.call(working_hours, format: :time)
98
+ # => [[-Infinity, 2021-01-01 07:59:59 +0100], [2021-01-01 12:00:01 +0100, Infinity]]
99
+ ```
100
+
101
+ ### Timezone
102
+
103
+ ```ruby
104
+ time_zone = "+00:00"
105
+ shift = [Time.new(2021, 1, 1, 8, 0, 0, time_zone), Time.new(2021, 1, 1, 12, 0, 0, time_zone)]
106
+ working_hours = [shift]
107
+
108
+ Availabiliter.call(working_hours, time_zone: "+10:00", format: :time)
109
+ # => [[-Infinity, 2021-01-01 17:59:59 +1000], [2021-01-01 22:00:01 +1000, Infinity]]
110
+ ```
111
+
112
+ When a time zone is not specifed by default Ruby determines the time zone according to the current system time.
27
113
 
28
114
  ## Usage
29
115
 
116
+ ### Expected input
117
+
118
+ Expected input is an array of arrays. Each of those subarray should represent a time slot with the first element being the time slot start and the last element being the time slot end.
119
+ A "time slot array" size should contain only 2 elements or an error will be returned.
120
+
30
121
  ```ruby
31
- # Build an array with all the date ranges you are looking an availability for
32
- holidays = [Date.new(1999, 1, 2)..Date.new(1999, 5, 1), Date.new(2000, 1, 1)..Date.new(2000, 2, 1), Date.new(2000, 3, 1)..nil]
122
+ incorrect_shift = [Time.new(2021, 1, 1, 8), Time.new(2021, 1, 1, 12), Time.new(2021, 1, 1, 14)]
123
+ working_hours = [incorrect_shift]
124
+
125
+ Availabiliter.call(working_hours)
126
+ # => Availabiliter::IncorrectInput (In the array input there is a time slot array which size is different from 2)
127
+ ```
128
+
129
+ Time slot start and end should be instances of `Date`, `Time` and `Integer` classes. `Integer` in that case represent unix timestamps.
130
+ During calculations `Date` instances will be converted to timestamp representing the beginning of the day. Be aware that if a time zone is specified Availabiliter will take it into account while doing this conversion.
131
+
132
+ ### Expected output
133
+
134
+ The expected output is an array of arrays, each representing an availability. Availabilities are unix timestamps by default but this can be overriden using the `format` option.
135
+ The supported output formats are:
136
+ - unix timestamps
137
+ - Time instances
138
+
139
+ ### Infinity
140
+
141
+ Time slots with an infinite start or an infinite end are supported. As input positive infinity should be represented with `Float::INFINITY`and negative infinity with `Float::INFINTIY`.
142
+ The same values can be expected as output.
33
143
 
34
- Availabiliter.get_availabilities(holidays)
35
- # => [Date.new(1999, 5, 2)..Date.new(1999, 12, 31), Date.new(2000, 2, 2)..Date.new(2000, 2, 29)]
144
+ ### Lowest time value accepted
36
145
 
37
- # If you want to retrieve availabilities starting from a specific date simply add it as a second argument
38
- Availabiliter.get_availabilities(holidays, Date.new(1999, 12, 15))
39
- # => [Date.new(1999, 12, 15)..Date.new(1999, 12, 31), Date.new(2000, 2, 2)..Date.new(2000, 2, 29)]
146
+ Since we rely on unix timestamps to deal with time zones, seconds are the lowest time value that can be accepted as an input.
40
147
 
41
- # If all of your date ranges have an end the last availability will be endless
42
- holidays = [Date.new(1999, 1, 2)..Date.new(1999, 5, 1)]
148
+ ### Performance
43
149
 
44
- Availabiliter.get_availabilities(holidays)
45
- # => [Date.new(1999, 5, 2)..]
150
+ Benchmark done with ruby 2.7.2p137 on MacBook Pro (13-inch, M1, 2020) with an Apple M1 processor.
46
151
 
152
+ | | Real time per second |
153
+ |---------------|----------------------|
154
+ | 1000 input | 0.004353 |
155
+ | 10 000 input | 0.038922 |
156
+ | 100 000 input | 0.401442 |
157
+
158
+ To do a benchmark on your machine git clone the repo and run `rake measure_performance`.
159
+
160
+ ## Installation
161
+
162
+ Add this line to your application's Gemfile:
163
+
164
+ ```ruby
165
+ gem 'availabiliter'
47
166
  ```
48
- ## Future improvements
49
167
 
50
- - support Datetime
51
- - support beginless ranges
168
+ And then execute:
169
+
170
+ $ bundle install
171
+
172
+ Or install it yourself as:
173
+
174
+ $ gem install availabiliter
52
175
 
53
176
  ## Development
54
177
 
@@ -28,7 +28,8 @@ Gem::Specification.new do |spec|
28
28
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
29
29
  spec.require_paths = ["lib"]
30
30
 
31
- spec.add_development_dependency "byebug", "~> 11.1.3"
31
+ spec.add_development_dependency "pry", "~> 0.13.1"
32
+ spec.add_development_dependency "pry-byebug", "~> 3.10.1"
32
33
  spec.add_development_dependency "rake", "~> 13.0.3"
33
34
  spec.add_development_dependency "rspec", "~> 3.10"
34
35
  spec.add_development_dependency "rubocop", "~> 1.16.1"
data/bin/console CHANGED
@@ -12,4 +12,10 @@ require "availabiliter"
12
12
  # Pry.start
13
13
 
14
14
  require "irb"
15
+
16
+ # http://www.seanbehan.com/ruby-reload-method-in-non-rails-irb-sessions/
17
+ def reload!
18
+ Dir["./lib/**/*.rb"].each { |f| load(f) }
19
+ end
20
+
15
21
  IRB.start(__FILE__)
@@ -0,0 +1,41 @@
1
+ require_relative "output_formatter"
2
+ require_relative "input_formatter"
3
+ require_relative "time_slot_collection"
4
+
5
+ class Availabiliter
6
+ # Centralize and orchestrate all behavior for availabilities calculations
7
+ class AvailabilitiesCalculator
8
+ attr_reader :raw_time_slots, :minimum_availability_start, :maximum_availability_end, :format, :time_zone
9
+
10
+ def initialize(raw_time_slots, minimum_availability_start:, maximum_availability_end:, format:, time_zone:)
11
+ @raw_time_slots = raw_time_slots
12
+ @minimum_availability_start = minimum_availability_start
13
+ @maximum_availability_end = maximum_availability_end
14
+ @format = format
15
+ @time_zone = time_zone
16
+ end
17
+
18
+ def call
19
+ output_availabilities
20
+ end
21
+
22
+ private
23
+
24
+ def time_slots
25
+ InputFormatter.new(input_array: raw_time_slots, time_zone: time_zone).time_slots
26
+ end
27
+
28
+ def availabilities
29
+ time_slot_collection.availabilities
30
+ end
31
+
32
+ def output_availabilities
33
+ OutputFormatter.new(availabilities, format: format, time_zone: time_zone).format_availabilities
34
+ end
35
+
36
+ def time_slot_collection
37
+ TimeSlotCollection.new(time_slots: time_slots, minimum_availability_start: minimum_availability_start,
38
+ maximum_availability_end: maximum_availability_end)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,5 @@
1
+ class Availabiliter
2
+ class Error < ::StandardError; end
3
+
4
+ class IncorrectInput < Error; end
5
+ end
@@ -0,0 +1,48 @@
1
+ require_relative "time_slot"
2
+
3
+ class Availabiliter
4
+ # Convert raw time slots to TimeSlot instances
5
+ class InputFormatter
6
+ attr_reader :input_array, :time_zone
7
+
8
+ def initialize(input_array:, time_zone:)
9
+ @input_array = input_array
10
+ @time_zone = time_zone
11
+ end
12
+
13
+ def time_slots
14
+ input_array.map do |time_slot_input|
15
+ validate_time_slot_input(time_slot_input)
16
+
17
+ TimeSlot.new(starting_time: to_timestamp(time_slot_input.first),
18
+ ending_time: to_timestamp(time_slot_input.last))
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def validate_time_slot_input(time_slot_input)
25
+ return if time_slot_input.size == 2
26
+
27
+ raise IncorrectInput, "In the array input there is a time slot array which size is different from 2"
28
+ end
29
+
30
+ def to_timestamp(value)
31
+ case value.class.to_s
32
+ when "Time" then value.to_i
33
+ when "Date"
34
+ convert_date(value)
35
+ else
36
+ value
37
+ end
38
+ end
39
+
40
+ def convert_date(date)
41
+ if time_zone
42
+ Time.new(date.year, date.month, date.day, 0, 0, 0, time_zone).to_i
43
+ else
44
+ date.to_time.to_i
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,56 @@
1
+ class Availabiliter
2
+ # Verify that the options input are valid
3
+ class OptionsValidator
4
+ attr_reader :minimum_availability_start, :maximum_availability_end, :format
5
+
6
+ def initialize(minimum_availability_start:, maximum_availability_end:, format:, **_options)
7
+ @minimum_availability_start = minimum_availability_start
8
+ @maximum_availability_end = maximum_availability_end
9
+ @format = format
10
+ end
11
+
12
+ def call
13
+ validate_format
14
+ validate_boundary_class
15
+ validate_boundary_value
16
+ end
17
+
18
+ private
19
+
20
+ def validate_format
21
+ return if PROCESSABLE_FORMATS.include?(format)
22
+
23
+ raise IncorrectInput, "#{format} is not an available format"
24
+ end
25
+
26
+ def validate_boundary_class
27
+ raise_invalid_boundary_class if invalid_boundary_class?
28
+ end
29
+
30
+ def invalid_boundary_class?
31
+ [minimum_availability_start, maximum_availability_end].one? do |boundary|
32
+ PROCESSABLE_TIME_CLASSES.none? { |klass| klass == boundary.class }
33
+ end
34
+ end
35
+
36
+ def validate_boundary_value
37
+ raise_invalid_boundary_value if invalid_boundary_value?
38
+ end
39
+
40
+ def invalid_boundary_value?
41
+ minimum_availability_start > maximum_availability_end
42
+ end
43
+
44
+ def raise_invalid_boundary_class
45
+ raise IncorrectInput, invalid_boundary_class_message
46
+ end
47
+
48
+ def raise_invalid_boundary_value
49
+ raise IncorrectInput, "minimum_availability_start can't be greater than maximum_availability_end"
50
+ end
51
+
52
+ def invalid_boundary_class_message
53
+ "#{minimum_availability_start.class}, #{maximum_availability_end.class} : one of this boundary class is not valid"
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,42 @@
1
+ class Availabiliter
2
+ # Transform availabilities to the required format
3
+ class OutputFormatter
4
+ attr_reader :availabilities, :format, :time_zone
5
+
6
+ def initialize(availabilities, format:, time_zone:)
7
+ @availabilities = availabilities
8
+ @format = format
9
+ @time_zone = time_zone
10
+ end
11
+
12
+ def format_availabilities
13
+ availabilities.map do |availability|
14
+ convert_to_format(availability)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def convert_to_format(availability)
21
+ [convert_timestamp(availability.first), convert_timestamp(availability.last)]
22
+ end
23
+
24
+ def convert_timestamp(timestamp)
25
+ return timestamp if timestamp.infinite?
26
+
27
+ case format
28
+ when :time then convert_time(timestamp)
29
+ else
30
+ timestamp
31
+ end
32
+ end
33
+
34
+ def convert_time(timestamp)
35
+ if time_zone
36
+ Time.at(timestamp, in: time_zone)
37
+ else
38
+ Time.at(timestamp)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,50 @@
1
+ class Availabiliter
2
+ # A TimeSlot represent a slot in time with a start and an end that can be finite or infinite.
3
+ # It can also act as a boundary in availabilities calculation.
4
+ class TimeSlot
5
+ attr_reader :starting_time, :ending_time, :boundary
6
+
7
+ def initialize(starting_time:, ending_time:, boundary: false)
8
+ @starting_time = starting_time
9
+ @ending_time = ending_time
10
+ @boundary = boundary
11
+ validate
12
+ end
13
+
14
+ def furthest(other)
15
+ [self, other].max_by(&:ending_time)
16
+ end
17
+
18
+ def dependent?(other)
19
+ adjacent?(other) || overlaps?(other)
20
+ end
21
+
22
+ def next_second
23
+ ending_time + 1
24
+ end
25
+
26
+ def previous_second
27
+ starting_time - 1
28
+ end
29
+
30
+ def overlaps?(other)
31
+ !does_not_overlap?(other)
32
+ end
33
+
34
+ def does_not_overlap?(other)
35
+ starting_time > other.ending_time || ending_time < other.starting_time
36
+ end
37
+
38
+ def adjacent?(other)
39
+ other.ending_time == previous_second || other.starting_time == next_second
40
+ end
41
+
42
+ private
43
+
44
+ def validate
45
+ return if ending_time >= starting_time
46
+
47
+ raise IncorrectInput, "A time slot ending time must be equal or greater than its starting time"
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,58 @@
1
+ require_relative "time_slot"
2
+ require "forwardable"
3
+
4
+ class Availabiliter
5
+ # A time slot collection is a sorted collection of time slots with a start and an end boundary
6
+ class TimeSlotCollection
7
+ include Enumerable
8
+ attr_reader :collection
9
+
10
+ def initialize(time_slots:, minimum_availability_start:, maximum_availability_end:)
11
+ @minimum_availability_start = minimum_availability_start
12
+ @maximum_availability_end = maximum_availability_end
13
+ @collection = build_collection(time_slots)
14
+ end
15
+
16
+ def availabilities
17
+ furthest_time_slot = start_boundary
18
+
19
+ filter_map.with_index do |time_slot, index|
20
+ next_time_slot = collection[index + 1]
21
+ furthest_time_slot = time_slot.furthest(furthest_time_slot)
22
+
23
+ next if index == last_time_slot_index
24
+ next if furthest_time_slot.dependent?(next_time_slot)
25
+
26
+ [furthest_time_slot.next_second, next_time_slot.previous_second]
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :minimum_availability_start, :maximum_availability_end
33
+
34
+ def build_collection(time_slots)
35
+ time_slots.then do |array|
36
+ array.sort_by!(&:starting_time)
37
+ array.unshift(start_boundary)
38
+ array.push(end_boundary)
39
+ end
40
+ end
41
+
42
+ def each(&block)
43
+ collection.each(&block)
44
+ end
45
+
46
+ def start_boundary
47
+ TimeSlot.new(starting_time: -Float::INFINITY, ending_time: minimum_availability_start - 1, boundary: true)
48
+ end
49
+
50
+ def end_boundary
51
+ TimeSlot.new(starting_time: maximum_availability_end + 1, ending_time: Float::INFINITY, boundary: true)
52
+ end
53
+
54
+ def last_time_slot_index
55
+ @last_time_slot_index ||= count - 1
56
+ end
57
+ end
58
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Availabiliter
4
- VERSION = "0.2.0"
3
+ class Availabiliter
4
+ VERSION = "1.0.0"
5
5
  end
data/lib/availabiliter.rb CHANGED
@@ -1,13 +1,46 @@
1
- # frozen_string_literal: true
2
-
3
1
  require_relative "availabiliter/version"
4
- require_relative "availabiliter/timeframe"
2
+ require_relative "availabiliter/availabilities_calculator"
3
+ require_relative "availabiliter/errors"
4
+ require_relative "availabiliter/options_validator"
5
+ require "date"
6
+
7
+ # Availabilityer main class and namespace
8
+ class Availabiliter
9
+ PROCESSABLE_FORMATS = %i[time integer].freeze
10
+ DEFAULT_FORMAT = :integer
11
+ PROCESSABLE_TIME_CLASSES = [Date, Time, Integer, Float].freeze
5
12
 
6
- # Availability calculator class
7
- module Availabiliter
8
13
  class << self
9
- def get_availabilities(array, timeframe_start = nil)
10
- TimeFrame.new(array, timeframe_start).availabilities
14
+ def call(raw_time_slots, **options)
15
+ new(raw_time_slots, options).calculate
11
16
  end
12
17
  end
18
+
19
+ def initialize(raw_time_slots, options)
20
+ @options = default_options.merge(options)
21
+ @raw_time_slots = raw_time_slots
22
+
23
+ validate_options
24
+ end
25
+
26
+ def calculate
27
+ AvailabilitiesCalculator.new(raw_time_slots, **options).call
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :raw_time_slots, :options
33
+
34
+ def default_options
35
+ {
36
+ minimum_availability_start: -Float::INFINITY,
37
+ maximum_availability_end: Float::INFINITY,
38
+ format: DEFAULT_FORMAT,
39
+ time_zone: nil
40
+ }
41
+ end
42
+
43
+ def validate_options
44
+ OptionsValidator.new(options).call
45
+ end
13
46
  end
@@ -0,0 +1,31 @@
1
+ require "benchmark"
2
+ require "date"
3
+ require_relative "../lib/availabiliter"
4
+
5
+ desc "Benchmark easily the performance of gem"
6
+ task :measure_performance do
7
+ Benchmark.bm do |benchmark|
8
+ raw_time_slots = build_raw_time_slots(1000)
9
+ benchmark.report(:one_thousand_raw_time_slots) { Availabiliter.call(raw_time_slots) }
10
+
11
+ raw_time_slots = build_raw_time_slots(10_000)
12
+ benchmark.report(:ten_thousand_raw_time_slots) { Availabiliter.call(raw_time_slots) }
13
+
14
+ raw_time_slots = build_raw_time_slots(100_000)
15
+ benchmark.report(:one_hundrer_thousand_raw_time_slots) { Availabiliter.call(raw_time_slots) }
16
+ end
17
+ end
18
+
19
+ def build_raw_time_slots(number)
20
+ date_range = Date.new(2020, 1, 1)..Date.new(2030, 1, 1)
21
+ timestamp_range = 1_577_833_200..1_893_452_400
22
+ time_range = Time.new(2020, 1, 1)..Time.new(2030, 1, 1)
23
+ rand_array = [date_range, timestamp_range, time_range]
24
+
25
+ number.times.each_with_object([]) do |_i, array|
26
+ starting_time = Random.rand(rand_array.sample)
27
+ finish_time = starting_time + Random.rand(1000)
28
+
29
+ array << [starting_time, finish_time]
30
+ end
31
+ end
@@ -0,0 +1,13 @@
1
+ require "date"
2
+ require_relative "../lib/availabiliter"
3
+
4
+ desc "Run the tests against all ruby versions"
5
+ task :test_ruby_versions do
6
+ authorized_versions = ["3.1.2", "3.0.4", "2.7.6", "2.7.2"]
7
+
8
+ authorized_versions.each do |version|
9
+ print "---------RUNNING TEST FOR RUBY #{version}---------"
10
+ system("export RBENV_VERSION=#{version} && bundle install && bundle exec rspec --fail-fast --format progress && export RBENV_VERSION=")
11
+ print "--------------------------------------------------"
12
+ end
13
+ end
metadata CHANGED
@@ -1,29 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: availabiliter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - lioneldebauge
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-09-04 00:00:00.000000000 Z
11
+ date: 2022-10-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: byebug
14
+ name: pry
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 11.1.3
19
+ version: 0.13.1
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 11.1.3
26
+ version: 0.13.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry-byebug
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 3.10.1
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 3.10.1
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: rake
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -86,9 +100,16 @@ files:
86
100
  - bin/console
87
101
  - bin/setup
88
102
  - lib/availabiliter.rb
89
- - lib/availabiliter/date_range.rb
90
- - lib/availabiliter/timeframe.rb
103
+ - lib/availabiliter/availabilities_calculator.rb
104
+ - lib/availabiliter/errors.rb
105
+ - lib/availabiliter/input_formatter.rb
106
+ - lib/availabiliter/options_validator.rb
107
+ - lib/availabiliter/output_formatter.rb
108
+ - lib/availabiliter/time_slot.rb
109
+ - lib/availabiliter/time_slot_collection.rb
91
110
  - lib/availabiliter/version.rb
111
+ - rakelib/measure_performance.rake
112
+ - rakelib/test_ruby_versions.rake
92
113
  homepage: https://github.com/lioneldebauge/availabiliter.git
93
114
  licenses:
94
115
  - MIT
@@ -1,68 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "date"
4
-
5
- module Availabiliter
6
- # DateRange is a Range with only dates
7
- # DateRange start_date is always the earliest date and end_date the latest date
8
- class DateRange < Range
9
- include Comparable
10
-
11
- alias start_date begin
12
- alias end_date end
13
-
14
- def initialize(start_date, end_date)
15
- super
16
- raise ArgumentError, "bad value for DateRange" unless valid?
17
- end
18
-
19
- def independent?(other)
20
- return true if other.nil?
21
-
22
- !overlaps?(other) && !adjacent?(other)
23
- end
24
-
25
- def tomorrow
26
- end_date&.next_day
27
- end
28
-
29
- def yesterday
30
- start_date.prev_day
31
- end
32
-
33
- ## adjacent == touches but doesn't overlap other DateRange
34
- def adjacent?(other)
35
- return other.end_date == yesterday if end_date.nil?
36
-
37
- other.end_date == yesterday || other.start_date == tomorrow
38
- end
39
-
40
- def overlaps?(other)
41
- cover?(other.begin) || other.cover?(first)
42
- end
43
-
44
- def next_availability(next_date_range)
45
- return if end_date.nil?
46
- return tomorrow..nil if next_date_range.nil?
47
- return unless independent?(next_date_range)
48
-
49
- gap_start = tomorrow
50
- gap_end = next_date_range.yesterday
51
-
52
- gap_start..gap_end
53
- end
54
-
55
- def furthest(other)
56
- return self if end_date.nil? || other.nil?
57
- return other if other.end_date.nil?
58
-
59
- [self, other].max_by(&:end_date)
60
- end
61
-
62
- private
63
-
64
- def valid?
65
- start_date.instance_of?(Date) && (end_date.nil? || start_date < end_date)
66
- end
67
- end
68
- end
@@ -1,69 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "date_range"
4
-
5
- module Availabiliter
6
- # A TimeFrame is an object representing several DateRange instances. It can have a start.
7
- # It can have only one endless DateRange.
8
- class TimeFrame
9
- attr_reader :start_date, :date_ranges
10
-
11
- def initialize(array, start_date = nil)
12
- @start_date = start_date
13
- @date_ranges = build_date_ranges(array)
14
-
15
- raise ArgumentError unless valid?
16
- end
17
-
18
- def availabilities
19
- return [start_date..nil] if date_ranges.empty?
20
- return build_availabilities if start_date.nil?
21
-
22
- availabilities = build_availabilities
23
- start_date < first_date_range.start_date ? availabilities.unshift(first_availability) : availabilities
24
- end
25
-
26
- private
27
-
28
- def build_date_ranges(array)
29
- date_range_array = array.filter_map do |range|
30
- next if out_of_timeframe?(range.end)
31
-
32
- DateRange.new(range.begin, range.end)
33
- end
34
-
35
- date_range_array.sort_by(&:start_date)
36
- end
37
-
38
- def build_availabilities
39
- furthest_date_range = first_date_range
40
-
41
- date_ranges.filter_map.with_index do |date_range, index|
42
- next_date_range = date_ranges[index + 1]
43
-
44
- furthest_date_range = furthest_date_range.furthest(date_range)
45
- furthest_date_range.next_availability(next_date_range)
46
- end
47
- end
48
-
49
- def out_of_timeframe?(range_end)
50
- !start_date.nil? && (!range_end.nil? && range_end < start_date)
51
- end
52
-
53
- def first_availability
54
- start_date..first_date_range.yesterday
55
- end
56
-
57
- def first_date_range
58
- date_ranges.first
59
- end
60
-
61
- def valid?
62
- end_date_valid? && date_ranges.count { |date_range| date_range.end_date.nil? } <= 1
63
- end
64
-
65
- def end_date_valid?
66
- start_date.instance_of?(Date) || start_date.instance_of?(NilClass)
67
- end
68
- end
69
- end