availability 0.0.2 → 0.0.3
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 +4 -4
- data/AUTHORS +2 -0
- data/CREDITS +1 -0
- data/README.md +47 -2
- data/bin/release +2 -0
- data/examples/scheduler.rb +1 -1
- data/examples/scheduler_spec.rb +6 -6
- data/lib/availability/abstract_availability.rb +94 -49
- data/lib/availability/createable.rb +1 -1
- data/lib/availability/factory_methods.rb +1 -1
- data/lib/availability/monthly.rb +0 -2
- data/lib/availability/once.rb +1 -1
- data/lib/availability/version.rb +1 -1
- data/lib/availability/weekly.rb +2 -2
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8eaf871af31feb52dcf53dcf209011faa140849a
|
4
|
+
data.tar.gz: ea9daa4546d229bdd17b454d2ef51bac5e33bd1c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cac1e269b14a79478b15db2c34c11cb2166762c3c50b4a6dec66953e574d3500461f0fb5353ab14a6a6717c1197f258b0ba5cff07f3fd3da9d36ce9ab75e03fe
|
7
|
+
data.tar.gz: b9cfe1fc23f4b068a9244d65ef5f2c7543bbd17732429a856b8eeaa3e83008438a70cac2dc0b36cf21750b3af4ef9acc81c9edda7b098c7a7441037e841c3afc
|
data/AUTHORS
ADDED
data/CREDITS
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,9 @@
|
|
1
|
-
# availability
|
1
|
+
# availability
|
2
|
+
(easily and quickly calculate schedule availability)
|
2
3
|
|
3
4
|
[](https://travis-ci.org/upper-hand/availability)
|
4
5
|
[](https://badge.fury.io/rb/availability)
|
6
|
+
[read the docs][DOCS]
|
5
7
|
|
6
8
|
This library uses modular arithmetic and residue classes to calculate schedule availability for dates. Time ranges within a date are handled differently. The goal is to create an easy-to-use API for schedule availability that is very fast and lightweight that is also easy and lightweight to persist in a database.
|
7
9
|
|
@@ -11,9 +13,46 @@ Shout out to @dpmccabe for his [original article](http://dmcca.be/2014/01/09/rec
|
|
11
13
|
gem install availability
|
12
14
|
```
|
13
15
|
|
16
|
+
Creating `Availability` instances is pretty simple. There are 5 variants of availabilities: `Availability::Once`, `Availability::Daily`, `Availability::Weekly`, `Availability::Monthly`, and `Availability::Yearly`. You can instantiate either of those directly (with the `#new` method), or you can use the equivalent factory methods exposed on `Availability` (e.g. `Availability.once`, `Availability.yearly`). Most availabilities will require at least an `interval`, a `start_time`, and a `duration` (see the [RDocs](http://www.rubydoc.info/gems/availability/Availability/AbstractAvailability#initialize-instance_method) for explanations of these).
|
17
|
+
|
18
|
+
There are a few convenience factory methods beyond the main factory methods:
|
19
|
+
- `Availability.every_{day,week,month,year}` will create the appropriate availability with an interval of 1
|
20
|
+
- `Availability.every_two_{days,weeks,months,years}` and `Availability.every_other_{day,week,month,year}` will create the appropriate availability with an interval of 2
|
21
|
+
- There are other `every_*` factory methods for intervals of 3, 4, and 5 (e.g. `every_three_months`)
|
22
|
+
|
23
|
+
## Primary API
|
24
|
+
|
25
|
+
### [#occurs_at?/1][OCCURS_AT]
|
26
|
+
This method takes a date or time and responds with a boolean indicating whether or not it is covered by the receiver.
|
27
|
+
|
28
|
+
### [#corresponds_to?/1][CORRESPONDS_TO]
|
29
|
+
This method takes another availability and responds with a boolean indicating whether or not it is covered by the receiver.
|
30
|
+
|
31
|
+
### [#last_occurrence][LAST_OCCURRENCE]
|
32
|
+
This returns the last occurrence of the receiver, or `nil` if `stops_by` is not set.
|
33
|
+
|
34
|
+
### [#next_n_occurrences/2][NEXT_N_OCCURS]
|
35
|
+
This returns an enumerable object of the next `N` occurrences of this availability from the given date or time. If `N` is greater than 1,000 a lazy enumerable is returned, otherwise the enumerable is an instance of `Array` with the occurrences.
|
36
|
+
|
37
|
+
### [#next_occurrence/1][NEXT_OCCUR]
|
38
|
+
This returns a single time object for the next occurrence on or after the given date or time. If no occurrence exists, `nil` is returned.
|
39
|
+
|
40
|
+
## Examples
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
# Every Monday from 9:00 AM to 10:00 AM starting on May 2, 2016
|
44
|
+
Availability.every_other_week(start_time: Time.new(2016, 5, 2, 9), duration: 1.hour)
|
45
|
+
|
46
|
+
# A business week starting on May 2, 2016 going from 1:30 PM until 2:00 PM every day
|
47
|
+
Availability.daily(start_time: Time.new(2016, 5, 2, 13, 30), stops_by: Time.new(2016, 5, 6), duration: 30.minutes)
|
48
|
+
|
49
|
+
# A semi-monthly availability occurring all day, without an end
|
50
|
+
Availability.every_other_month(start_time: Time.new(2016, 1, 1), duration: 1.day)
|
51
|
+
```
|
52
|
+
|
14
53
|
## TODO
|
15
54
|
|
16
|
-
add more documentation
|
55
|
+
* add more documentation
|
17
56
|
|
18
57
|
## Authors
|
19
58
|
|
@@ -49,3 +88,9 @@ see <http://unlicense.org/> or the accompanying [UNLICENSE]{UNLICENSE} file.
|
|
49
88
|
[YARD]: http://yardoc.org/
|
50
89
|
[YARD-GS]: http://rubydoc.info/docs/yard/file/docs/GettingStarted.md
|
51
90
|
[PDD]: http://lists.w3.org/Archives/Public/public-rdf-ruby/2010May/0013.html
|
91
|
+
[DOCS]: http://www.rubydoc.info/gems/availability
|
92
|
+
[OCCURS_AT]: http://www.rubydoc.info/gems/availability/Availability/AbstractAvailability#occurs_at%3F-instance_method
|
93
|
+
[CORRESPONDS_TO]: http://www.rubydoc.info/gems/availability/Availability/AbstractAvailability#corresponds_to%3F-instance_method
|
94
|
+
[LAST_OCCURRENCE]: http://www.rubydoc.info/gems/availability/Availability/AbstractAvailability#last_occurrence-instance_method
|
95
|
+
[NEXT_N_OCCURS]: http://www.rubydoc.info/gems/availability/Availability/AbstractAvailability#next_n_occurrences%3F-instance_method
|
96
|
+
[NEXT_OCCUR]: http://www.rubydoc.info/gems/availability/Availability/AbstractAvailability#next_occurrence%3F-instance_method
|
data/bin/release
ADDED
data/examples/scheduler.rb
CHANGED
@@ -72,7 +72,7 @@ class Scheduler
|
|
72
72
|
|
73
73
|
def validate_availabilities(availabilities)
|
74
74
|
list = Array(availabilities).flatten.compact
|
75
|
-
return list if list.all? { |e|
|
75
|
+
return list if list.all? { |e| Availability.availability?(e) }
|
76
76
|
raise ArgumentError, "expected a list of availabilities"
|
77
77
|
end
|
78
78
|
|
data/examples/scheduler_spec.rb
CHANGED
@@ -94,16 +94,16 @@ RSpec.describe Scheduler do
|
|
94
94
|
expect(bobs_schedule.allow? availability_request: ar).to be_truthy
|
95
95
|
end
|
96
96
|
|
97
|
-
it 'does not allow an event request that is beyond the availability
|
98
|
-
expect(bobs_schedule.allow? availability_request: one_hour_slot_per_week(start_time: T(2016, 4, 18, 9),
|
97
|
+
it 'does not allow an event request that is beyond the availability interval' do
|
98
|
+
expect(bobs_schedule.allow? availability_request: one_hour_slot_per_week(start_time: T(2016, 4, 18, 9), interval: 4)).to be_falsey
|
99
99
|
end
|
100
100
|
|
101
|
-
it 'does not allow an event request that starts before the availability
|
102
|
-
expect(bobs_schedule.allow? availability_request: one_hour_slot_per_week(start_time: T(2016, 4, 4, 9),
|
101
|
+
it 'does not allow an event request that starts before the availability interval' do
|
102
|
+
expect(bobs_schedule.allow? availability_request: one_hour_slot_per_week(start_time: T(2016, 4, 4, 9), interval: 4)).to be_falsey
|
103
103
|
end
|
104
104
|
|
105
|
-
it 'does not allow an event request with a different
|
106
|
-
expect(bobs_schedule.allow? availability_request: one_hour_slot_per_week(start_time: T(2016, 4, 4, 9),
|
105
|
+
it 'does not allow an event request with a different interval' do
|
106
|
+
expect(bobs_schedule.allow? availability_request: one_hour_slot_per_week(start_time: T(2016, 4, 4, 9), interval: 5)).to be_falsey
|
107
107
|
end
|
108
108
|
end
|
109
109
|
|
@@ -1,27 +1,26 @@
|
|
1
1
|
module Availability
|
2
|
+
# @abstract see concrete classes: Once, Daily, Weekly, Monthly and Yearly
|
2
3
|
class AbstractAvailability
|
3
4
|
private_class_method :new # :nodoc:
|
4
5
|
|
5
|
-
attr_accessor :capacity, :duration, :
|
6
|
+
attr_accessor :capacity, :duration, :stops_by
|
6
7
|
attr_reader :exclusions, :interval, :residue, :start_time
|
7
8
|
|
8
9
|
#
|
9
10
|
# Required arguments:
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
11
|
+
# @param [Fixnum] interval an integer that is the interval of occurrences per frequency
|
12
|
+
# @param [Time] start_time a Time, Date, or DateTime that indicates when the availability begins
|
13
|
+
# @param [Fixnum] duration an integer indicating how long the availability lasts in seconds
|
13
14
|
#
|
14
15
|
# Optional arguements:
|
15
|
-
#
|
16
|
-
# stops_by: specific date by which the availability ends
|
16
|
+
# @param [Time] stops_by specific Date, Time, or DateTime by which the availability ends
|
17
17
|
#
|
18
|
-
def initialize(capacity: Float::INFINITY, exclusions: nil,
|
18
|
+
def initialize(capacity: Float::INFINITY, exclusions: nil, stops_by: nil, duration: , interval: , start_time: )
|
19
19
|
raise ArgumentError, "start_time is required" if start_time.nil?
|
20
20
|
raise ArgumentError, "duration is required" if duration.nil?
|
21
21
|
raise ArgumentError, "interval is required" if interval.nil?
|
22
22
|
@capacity = capacity
|
23
23
|
@duration = duration
|
24
|
-
@frequency = frequency
|
25
24
|
@interval = interval
|
26
25
|
@start_time = start_time.to_time
|
27
26
|
@stops_by = stops_by
|
@@ -42,8 +41,17 @@ module Availability
|
|
42
41
|
self.class.beginning
|
43
42
|
end
|
44
43
|
|
44
|
+
# @!group Testing
|
45
|
+
|
46
|
+
#
|
47
|
+
# Whether or not the availability is covered by the receiver
|
48
|
+
#
|
49
|
+
# @param [Availability::AbstractAvailability] availability the availability to test for coverage
|
50
|
+
#
|
51
|
+
# @return [Boolean] true or false
|
52
|
+
#
|
45
53
|
def corresponds_to?(availability)
|
46
|
-
return unless occurs_at?(availability.start_time) && occurs_at?(availability.start_time + availability.duration)
|
54
|
+
return false unless occurs_at?(availability.start_time) && occurs_at?(availability.start_time + availability.duration)
|
47
55
|
if !!stops_by
|
48
56
|
that_last = availability.last_occurrence
|
49
57
|
!that_last.nil? &&
|
@@ -55,39 +63,26 @@ module Availability
|
|
55
63
|
end
|
56
64
|
end
|
57
65
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
if stops_by
|
68
|
-
@exclusions += [
|
69
|
-
Exclusion.after_day(stops_by),
|
70
|
-
Exclusion.after_time(stops_by)
|
71
|
-
# TODO: should previous be: Exclusion.after_time(start_time + duration)
|
72
|
-
]
|
73
|
-
end
|
74
|
-
self
|
75
|
-
end
|
76
|
-
|
77
|
-
def interval
|
78
|
-
@interval
|
66
|
+
#
|
67
|
+
# Whether or not the given time is covered by the receiver
|
68
|
+
#
|
69
|
+
# @param [Time] time the Date, Time, or DateTime to test for coverage
|
70
|
+
#
|
71
|
+
# @return [Boolean] true or false
|
72
|
+
#
|
73
|
+
def occurs_at?(time)
|
74
|
+
residue_for(time) == @residue && time_overlaps?(time, start_time, start_time + duration)
|
79
75
|
end
|
80
76
|
|
81
|
-
|
82
|
-
@interval = interval
|
83
|
-
compute_residue
|
84
|
-
self
|
85
|
-
end
|
77
|
+
# @!endgroup
|
86
78
|
|
87
|
-
|
88
|
-
raise 'subclass responsibility'
|
89
|
-
end
|
79
|
+
# @!group Occurrences
|
90
80
|
|
81
|
+
#
|
82
|
+
# Calculates the last occurrence of an availability
|
83
|
+
#
|
84
|
+
# @return [Time] the last occurrence of the receiver, or nil if stops_by is not set
|
85
|
+
#
|
91
86
|
def last_occurrence
|
92
87
|
return nil unless stops_by
|
93
88
|
unless @last_occurrence
|
@@ -98,10 +93,14 @@ module Availability
|
|
98
93
|
@last_occurrence
|
99
94
|
end
|
100
95
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
96
|
+
#
|
97
|
+
# Calculates a time for the next occurrence on or after the given date or time.
|
98
|
+
# If no occurrence exists, `nil` is returned.
|
99
|
+
#
|
100
|
+
# from_date: a Date, Time, or DateTime from which to start calculating
|
101
|
+
#
|
102
|
+
# @return [Time] the next occurrence (or nil)
|
103
|
+
#
|
105
104
|
def next_occurrence(from_date)
|
106
105
|
residue = @residue - residue_for(from_date)
|
107
106
|
date = move_by from_date, residue.modulo(interval)
|
@@ -118,10 +117,12 @@ module Availability
|
|
118
117
|
end
|
119
118
|
|
120
119
|
#
|
121
|
-
#
|
120
|
+
# Calculates occurrences for n <= 1000; where n > 1000, it returns a lazy enumerator
|
122
121
|
#
|
123
|
-
#
|
124
|
-
#
|
122
|
+
# @param [Fixnum] n how many occurrences to get
|
123
|
+
# @param [Date, Time, DateTime] from_date time from which to start calculating
|
124
|
+
#
|
125
|
+
# @return [Enumerable] an array of [Time] or lazy enumeration for n > 1000
|
125
126
|
#
|
126
127
|
def next_n_occurrences(n, from_date)
|
127
128
|
first_next_occurrence = next_occurrence(from_date)
|
@@ -131,12 +132,34 @@ module Availability
|
|
131
132
|
range.map &blk
|
132
133
|
end
|
133
134
|
|
134
|
-
|
135
|
-
|
135
|
+
# @!endgroup
|
136
|
+
|
137
|
+
# @!group Accessors
|
138
|
+
|
139
|
+
def end_time
|
140
|
+
start_time + duration
|
136
141
|
end
|
137
142
|
|
138
|
-
def
|
139
|
-
|
143
|
+
def exclusions=(exclusions)
|
144
|
+
#TODO: should be a set of exclusions
|
145
|
+
@exclusions = Array(exclusions).flatten.compact + [
|
146
|
+
Exclusion.before_day(start_time),
|
147
|
+
Exclusion.before_time(start_time)
|
148
|
+
]
|
149
|
+
if stops_by
|
150
|
+
@exclusions += [
|
151
|
+
Exclusion.after_day(stops_by),
|
152
|
+
Exclusion.after_time(stops_by)
|
153
|
+
# TODO: should previous be: Exclusion.after_time(start_time + duration)
|
154
|
+
]
|
155
|
+
end
|
156
|
+
self
|
157
|
+
end
|
158
|
+
|
159
|
+
def interval=(interval)
|
160
|
+
@interval = interval
|
161
|
+
compute_residue
|
162
|
+
self
|
140
163
|
end
|
141
164
|
|
142
165
|
def start_time=(start_time)
|
@@ -145,6 +168,10 @@ module Availability
|
|
145
168
|
self
|
146
169
|
end
|
147
170
|
|
171
|
+
# @!endgroup
|
172
|
+
|
173
|
+
# @!group Helpers
|
174
|
+
|
148
175
|
def time_overlaps?(time, start_time, end_time)
|
149
176
|
that_start = time.seconds_since_midnight.to_i
|
150
177
|
this_start = start_time.seconds_since_midnight.to_i
|
@@ -152,6 +179,24 @@ module Availability
|
|
152
179
|
(this_start..this_end).include?(that_start)
|
153
180
|
end
|
154
181
|
|
182
|
+
# @!endgroup
|
183
|
+
|
184
|
+
# @!group Subclass Responsibilities
|
185
|
+
|
186
|
+
def interval_difference(first, second)
|
187
|
+
raise 'subclass responsibility'
|
188
|
+
end
|
189
|
+
|
190
|
+
def move_by(time, amount)
|
191
|
+
raise 'subclass responsibility'
|
192
|
+
end
|
193
|
+
|
194
|
+
def residue_for(time)
|
195
|
+
raise 'subclass responsibility'
|
196
|
+
end
|
197
|
+
|
198
|
+
# @!endgroup
|
199
|
+
|
155
200
|
private
|
156
201
|
|
157
202
|
def compute_residue
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module Availability
|
2
2
|
module FactoryMethods
|
3
3
|
def create(**args)
|
4
|
-
cls = args.delete(:event_class) || Availability::subclass_for(args
|
4
|
+
cls = args.delete(:event_class) || Availability::subclass_for(args.delete(:frequency) || :daily)
|
5
5
|
raise ArgumentError, "undefined frequency" if cls.nil?
|
6
6
|
cls.send :new, **args
|
7
7
|
end
|
data/lib/availability/monthly.rb
CHANGED
data/lib/availability/once.rb
CHANGED
@@ -7,7 +7,7 @@ module Availability
|
|
7
7
|
def initialize(**args)
|
8
8
|
raise ArgumentError, "start_time is required" unless args.has_key?(:start_time)
|
9
9
|
raise ArgumentError, "duration is required" unless args.has_key?(:duration)
|
10
|
-
super **args,
|
10
|
+
super **args, interval: 0, stops_by: args[:start_time] + args[:duration]
|
11
11
|
end
|
12
12
|
|
13
13
|
def interval_difference(this, that)
|
data/lib/availability/version.rb
CHANGED
data/lib/availability/weekly.rb
CHANGED
@@ -10,7 +10,7 @@ module Availability
|
|
10
10
|
|
11
11
|
def interval_difference(first, second)
|
12
12
|
first_date, second_date = [first.to_date, second.to_date].sort
|
13
|
-
(second_date - first_date).to_i
|
13
|
+
(second_date - first_date).to_i
|
14
14
|
end
|
15
15
|
|
16
16
|
def move_by(time, amount)
|
@@ -18,7 +18,7 @@ module Availability
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def residue_for(time)
|
21
|
-
(time
|
21
|
+
interval_difference(time, beginning).modulo(interval)
|
22
22
|
end
|
23
23
|
end
|
24
24
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: availability
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jason Rogers
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-05-
|
11
|
+
date: 2016-05-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -93,6 +93,7 @@ files:
|
|
93
93
|
- ".ruby-gemset"
|
94
94
|
- ".ruby-version"
|
95
95
|
- ".travis.yml"
|
96
|
+
- AUTHORS
|
96
97
|
- CREDITS
|
97
98
|
- Gemfile
|
98
99
|
- README.md
|
@@ -101,6 +102,7 @@ files:
|
|
101
102
|
- WAIVER
|
102
103
|
- availability.gemspec
|
103
104
|
- bin/console
|
105
|
+
- bin/release
|
104
106
|
- bin/setup
|
105
107
|
- examples/scheduler.rb
|
106
108
|
- examples/scheduler_spec.rb
|