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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b27f01102a6bdb5c8a7aa11844f3a9fd568c9d09
4
- data.tar.gz: a8fc64785938d22e1751314441936f17e5b52b9b
3
+ metadata.gz: 8eaf871af31feb52dcf53dcf209011faa140849a
4
+ data.tar.gz: ea9daa4546d229bdd17b454d2ef51bac5e33bd1c
5
5
  SHA512:
6
- metadata.gz: 691256a8b27a1dadb57fbac13b44855e5c65b7c891c554792c3cf99b16066a292043a0611be829ed2df4c1e5f846117615ca1afd2b6d78942a2e531c630a5e43
7
- data.tar.gz: 324414f8022f9a464274d95b43788c9d523d449b86467b74bbc9c4b82b0894656d8294237aa37d0006b97862bb8c057faf83d6f498a89b1ff5e22257bebc3af2
6
+ metadata.gz: cac1e269b14a79478b15db2c34c11cb2166762c3c50b4a6dec66953e574d3500461f0fb5353ab14a6a6717c1197f258b0ba5cff07f3fd3da9d36ce9ab75e03fe
7
+ data.tar.gz: b9cfe1fc23f4b068a9244d65ef5f2c7543bbd17732429a856b8eeaa3e83008438a70cac2dc0b36cf21750b3af4ef9acc81c9edda7b098c7a7441037e841c3afc
data/AUTHORS ADDED
@@ -0,0 +1,2 @@
1
+ * Jason Rogers <jacaetevha@gmail.com>
2
+ * Devin McCabe <devin.mccabe@gmail.com>
data/CREDITS CHANGED
@@ -1 +1,2 @@
1
1
  * Jason Rogers <jacaetevha@gmail.com>
2
+ * Devin McCabe <devin.mccabe@gmail.com>
data/README.md CHANGED
@@ -1,7 +1,9 @@
1
- # availability - easily and quickly calculate schedule availability
1
+ # availability
2
+ (easily and quickly calculate schedule availability)
2
3
 
3
4
  [![Build Status](https://travis-ci.org/upper-hand/availability.svg?branch=master)](https://travis-ci.org/upper-hand/availability)
4
5
  [![Gem Version](https://badge.fury.io/rb/availability.svg)](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
@@ -0,0 +1,2 @@
1
+ #!/bin/bash
2
+ bundle exec rake release
@@ -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| e.respond_to? :corresponds_to? }
75
+ return list if list.all? { |e| Availability.availability?(e) }
76
76
  raise ArgumentError, "expected a list of availabilities"
77
77
  end
78
78
 
@@ -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 frequency' do
98
- expect(bobs_schedule.allow? availability_request: one_hour_slot_per_week(start_time: T(2016, 4, 18, 9), frequency: 4)).to be_falsey
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 frequency' do
102
- expect(bobs_schedule.allow? availability_request: one_hour_slot_per_week(start_time: T(2016, 4, 4, 9), frequency: 4)).to be_falsey
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 frequency' do
106
- expect(bobs_schedule.allow? availability_request: one_hour_slot_per_week(start_time: T(2016, 4, 4, 9), frequency: 5)).to be_falsey
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, :frequency, :stops_by
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
- # interval: an integer that is the interval of occurrences per frequency
11
- # start_time: a Time, Date, or DateTime that indicates when the availability begins
12
- # duration: an integer indicating how long the availability lasts in seconds
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
- # frequency: a symbol, one of [:once, :daily, :monthly, :yearly]; defaults to :daily
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, frequency: :daily, stops_by: nil, duration: , interval: , start_time: )
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
- def end_time
59
- start_time + duration
60
- end
61
-
62
- def exclusions=(exclusions)
63
- @exclusions = Array(exclusions).flatten.compact + [
64
- Exclusion.before_day(start_time),
65
- Exclusion.before_time(start_time)
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
- def interval=(interval)
82
- @interval = interval
83
- compute_residue
84
- self
85
- end
77
+ # @!endgroup
86
78
 
87
- def interval_difference(first, second)
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
- def move_by(time, amount)
102
- raise 'subclass responsibility'
103
- end
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
- # Returns an array of occurrences for n <= 1000, otherwise it returns a lazy enumerator
120
+ # Calculates occurrences for n <= 1000; where n > 1000, it returns a lazy enumerator
122
121
  #
123
- # n: Fixnum, how many occurrences to get
124
- # from_date: a Date, Time, or DateTime from which to start calculating
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
- def occurs_at?(time)
135
- residue_for(time) == @residue && time_overlaps?(time, start_time, start_time + duration)
135
+ # @!endgroup
136
+
137
+ # @!group Accessors
138
+
139
+ def end_time
140
+ start_time + duration
136
141
  end
137
142
 
138
- def residue_for(time)
139
- raise 'subclass responsibility'
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
@@ -6,7 +6,7 @@ module Availability
6
6
 
7
7
  def create(**args)
8
8
  frequency = name.split(':').last.downcase.to_sym
9
- super **args.merge(frequency: frequency, event_class: self)
9
+ super **args.merge(event_class: self)
10
10
  end
11
11
  end
12
12
  end
@@ -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[:frequency] ||= :daily)
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
@@ -14,8 +14,6 @@ module Availability
14
14
  end
15
15
 
16
16
  def residue_for(time)
17
- # date = time.to_date
18
- # ((date.year - beginning.year) * 12 + (date.month - beginning.month)).modulo(@interval)
19
17
  interval_difference(beginning, time).modulo(@interval)
20
18
  end
21
19
  end
@@ -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, frequency: :once, interval: 0, stops_by: args[:start_time] + args[:duration]
10
+ super **args, interval: 0, stops_by: args[:start_time] + args[:duration]
11
11
  end
12
12
 
13
13
  def interval_difference(this, that)
@@ -1,3 +1,3 @@
1
1
  module Availability
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
@@ -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 #/ interval
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.to_date - beginning.to_date).to_i.modulo(interval)
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.2
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-02 00:00:00.000000000 Z
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