availabiliter 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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