availability 0.0.2 → 0.0.3

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
  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