availability 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b234c8bd7ec4affc5c2021508d9d7f3875541e94
4
+ data.tar.gz: 2accd7d37b256be224033646289723257fea91ec
5
+ SHA512:
6
+ metadata.gz: 2bc9136b9b206d295305bc283bb3624499827017080981c8c3589014292f058b0a637af1a97494e8c7207eb5b1f4ddb5f74aed709bd218e056dda8ad3f033fcd
7
+ data.tar.gz: f1170cb99e6773867917b83407e55f5d001c38ea453588553633d5f9a82b589e3f39570bcae419d192d9bacffba9f50a57d1bfbbc0f3d329ed732518b4f969a2
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ spec/examples.txt
2
+ /.bundle/
3
+ /.yardoc
4
+ /Gemfile.lock
5
+ /_yardoc/
6
+ /coverage/
7
+ /doc/
8
+ /pkg/
9
+ /spec/reports/
10
+ /tmp/
11
+ /WAIVER.asc
data/.rspec ADDED
@@ -0,0 +1,5 @@
1
+ --color
2
+ --require spec_helper
3
+ -I examples
4
+ --require scheduler_spec
5
+ --format progress
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ modulo-math-poc
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.3.0
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.3.0
4
+ before_install: gem install bundler -v 1.10.5
data/CREDITS ADDED
@@ -0,0 +1 @@
1
+ * Jason Rogers <jacaetevha@gmail.com>
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in availability.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # availability - easily and quickly calculate schedule availability
2
+
3
+ [![Build Status][travis-availability-png]][travis-availability]
4
+ [![Gem Version](https://badge.fury.io/rb/availability.svg)](http://badge.fury.io/rb/availability)
5
+
6
+ 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
+
8
+ Shout out to @dpmccabe for his [original article](http://dmcca.be/2014/01/09/recurring-subscriptions-with-ruby-rspec-and-modular-arithmetic.html) and code.
9
+
10
+ ```
11
+ gem install availability
12
+ ```
13
+
14
+ ## TODO
15
+
16
+ add more documentation
17
+
18
+ ## Authors
19
+
20
+ * Jason Rogers <jacaetevha@gmail.com>
21
+
22
+ ## Contributors
23
+
24
+ * Jason Rogers <jacaetevha@gmail.com>
25
+
26
+ ## Contributing
27
+
28
+ * Do your best to adhere to the existing coding conventions and idioms.
29
+ * Don't use hard tabs, and don't leave trailing whitespace on any line.
30
+ Before committing, run `git diff --check` to make sure of this.
31
+ * Do document every method you add using [YARD][] annotations. Read the
32
+ [tutorial][YARD-GS] or just look at the existing code for examples.
33
+ * Don't touch the `availability.gemspec` or `VERSION` files. If you need
34
+ to change them, do so on your private branch only.
35
+ * Do feel free to add yourself to the `CREDITS` file and the
36
+ corresponding list in the the `README`. Alphabetical order applies.
37
+ * Don't touch the `AUTHORS` file. If your contributions are significant
38
+ enough, be assured we will eventually add you in there.
39
+ * Do note that in order for us to merge any non-trivial changes (as a rule
40
+ of thumb, additions larger than about 15 lines of code), we need an
41
+ explicit on record from you. You can submit this dedication as a GitHub
42
+ Issue in this repository. See [public domain dedication][PDD] for an example.
43
+
44
+ ## License
45
+
46
+ This is free and unencumbered public domain software. For more information,
47
+ see <http://unlicense.org/> or the accompanying [UNLICENSE]{UNLICENSE} file.
48
+
49
+ [YARD]: http://yardoc.org/
50
+ [YARD-GS]: http://rubydoc.info/docs/yard/file/docs/GettingStarted.md
51
+ [PDD]: http://lists.w3.org/Archives/Public/public-rdf-ruby/2010May/0013.html
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/UNLICENSE ADDED
@@ -0,0 +1,24 @@
1
+ This is free and unencumbered software released into the public domain.
2
+
3
+ Anyone is free to copy, modify, publish, use, compile, sell, or
4
+ distribute this software, either in source code form or as a compiled
5
+ binary, for any purpose, commercial or non-commercial, and by any
6
+ means.
7
+
8
+ In jurisdictions that recognize copyright laws, the author or authors
9
+ of this software dedicate any and all copyright interest in the
10
+ software to the public domain. We make this dedication for the benefit
11
+ of the public at large and to the detriment of our heirs and
12
+ successors. We intend this dedication to be an overt act of
13
+ relinquishment in perpetuity of all present and future rights to this
14
+ software under copyright law.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ For more information, please refer to <http://unlicense.org>
data/WAIVER ADDED
@@ -0,0 +1,17 @@
1
+ # Copyright waiver for <https://github.com/upper-hand/availability>
2
+
3
+ I dedicate any and all copyright interest in this software to the
4
+ public domain. I make this dedication for the benefit of the public at
5
+ large and to the detriment of my heirs and successors. I intend this
6
+ dedication to be an overt act of relinquishment in perpetuity of all
7
+ present and future rights to this software under copyright law.
8
+
9
+ To the best of my knowledge and belief, my contributions are either
10
+ originally authored by me or are derived from prior works which I have
11
+ verified are also in the public domain and are not subject to claims
12
+ of copyright by other parties.
13
+
14
+ To the best of my knowledge and belief, no individual, business,
15
+ organization, government, or other entity has any copyright interest
16
+ in my contributions, and I affirm that I will not make contributions
17
+ that are otherwise encumbered.
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'availability/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "availability"
8
+ spec.version = Availability::VERSION
9
+ spec.authors = ["Jason Rogers"]
10
+ spec.email = ["jacaetevha@gmail.com"]
11
+
12
+ spec.summary = %q{Calculating schedule availability}
13
+ spec.description = %q{Use modular arithmetic and residue classes to calculate schedule availability for dates (times handled separately).}
14
+ spec.homepage = "https://github.com/upper-hand/availability"
15
+ spec.license = "Unlicense"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency "activesupport"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.10"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec"
27
+ spec.add_development_dependency "rspec-its"
28
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "availability"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,88 @@
1
+ require_relative '../lib/availability'
2
+
3
+ #
4
+ # This is just an example of a scheduler that might be used with availabilities. It's a work in
5
+ # progress that's helping to flesh out the design of availabilities.
6
+ #
7
+ class Scheduler
8
+ attr_reader :availabilities, :scheduled
9
+
10
+ #
11
+ # availabilities: a list of Availability instances defining when the resource is available
12
+ #
13
+ def initialize(availabilities)
14
+ @availabilities = validate_availabilities availabilities
15
+ @scheduled = Hash.new{|h, k| h[k] = []}
16
+ end
17
+
18
+ #
19
+ # This method expects either an availability request or a start/end time pair. If the
20
+ # start/end time pair is offered, this method acts the same as if an Availability::Once
21
+ # request was offered with the given start_time and a duration going through the given
22
+ # end_time.
23
+ #
24
+ # availability_request: an Availability instance
25
+ # start_time: Time/DateTime representative of a single request starting at that time
26
+ # end_time: Time/DateTime representative of a single request ending at that time
27
+ #
28
+ # Returns the first availability that corresponds to the availability request. If no
29
+ # availability is found, returns nil.
30
+ #
31
+ def allow?(**args)
32
+ availability_request = convert **args
33
+ availability_for availability_request
34
+ end
35
+
36
+ #
37
+ # This method expects either an availability request or a start/end time pair. If the
38
+ # start/end time pair is offered, this method acts the same as if an Availability::Once
39
+ # request was offered with the given start_time and a duration going through the given
40
+ # end_time.
41
+ #
42
+ # availability_request: an Availability instance
43
+ # start_time: Time/DateTime representative of a single request starting at that time
44
+ # end_time: Time/DateTime representative of a single request ending at that time
45
+ #
46
+ # returns boolean indicating whether the availability request was scheduled.
47
+ #
48
+ def schedule(**args)
49
+ request = convert **args
50
+ availability = allow? availability_request: request
51
+ return self unless availability
52
+ scheduled[availability] << request unless scheduled[availability].size >= availability.capacity
53
+ self
54
+ end
55
+
56
+ private
57
+ def availability_for(request)
58
+ @availabilities.detect do |some_availability|
59
+ some_availability.corresponds_to? request
60
+ end
61
+ end
62
+
63
+ def convert(availability_request: nil, start_time: nil, end_time: nil)
64
+ validate_allow_params! availability_request, start_time, end_time
65
+ if availability_request.nil?
66
+ availability_request = Availability::Once.create(
67
+ start_time: start_time, duration: (end_time.to_time - start_time.to_time).to_i)
68
+ else
69
+ availability_request
70
+ end
71
+ end
72
+
73
+ def validate_availabilities(availabilities)
74
+ list = Array(availabilities).flatten.compact
75
+ return list if list.all? { |e| e.respond_to? :corresponds_to? }
76
+ raise ArgumentError, "expected a list of availabilities"
77
+ end
78
+
79
+ def validate_allow_params!(availability, start_time, end_time)
80
+ return if Availability.availability?(availability)
81
+ return if valid_time?(start_time) && valid_time?(end_time)
82
+ raise ArgumentError, "must specify either availability_request or (start_time and end_time)"
83
+ end
84
+
85
+ def valid_time?(a_time)
86
+ Time === a_time || DateTime === a_time || Date === a_time
87
+ end
88
+ end
@@ -0,0 +1,136 @@
1
+ require_relative 'scheduler'
2
+
3
+ module SchedulerSpecHelpers
4
+ def T(*args)
5
+ Time.new *args
6
+ end
7
+
8
+ def one_hour_slot_per_week(**args)
9
+ Availability.weekly **args, duration: 1.hour
10
+ end
11
+
12
+ def half_hour_slot_per_week(**args)
13
+ Availability.weekly **args, duration: 30.minutes
14
+ end
15
+ end
16
+
17
+ RSpec.describe Scheduler do
18
+ include SchedulerSpecHelpers
19
+
20
+ context '#allow?' do
21
+ let(:bob_availabilities) do
22
+ [
23
+ one_hour_slot_per_week(start_time: T(2016, 4, 11, 9), stops_by: T(2016, 5, 9, 9)),
24
+ one_hour_slot_per_week(start_time: T(2016, 4, 12, 9), stops_by: T(2016, 5, 10, 9)),
25
+ one_hour_slot_per_week(start_time: T(2016, 4, 13, 9), stops_by: T(2016, 5, 11, 9)),
26
+ one_hour_slot_per_week(start_time: T(2016, 4, 14, 9), stops_by: T(2016, 5, 12, 9)),
27
+ one_hour_slot_per_week(start_time: T(2016, 4, 15, 9), stops_by: T(2016, 5, 13, 9))
28
+ ]
29
+ end
30
+
31
+ let(:bobs_schedule) { Scheduler.new bob_availabilities }
32
+
33
+ describe '#schedule' do
34
+ let(:request) { bob_availabilities.first.dup }
35
+ let(:short_request) do
36
+ half_hour_slot_per_week start_time: T(2016, 4, 14, 9, 15), stops_by: T(2016, 5, 12, 9, 45)
37
+ end
38
+ let(:scheduled_count) { -> { bobs_schedule.scheduled.values.map(&:size).sum } }
39
+
40
+ before :each do
41
+ bob_availabilities.each { |e| e.capacity = 1 }
42
+ end
43
+
44
+ context 'at capacity' do
45
+ before :each do
46
+ bob_availabilities.each { |e| bobs_schedule.scheduled[e] = [double('a request')] }
47
+ end
48
+
49
+ context 'with a shorter slot that fits within the availability' do
50
+ it 'does not accept a request' do
51
+ expect{ bobs_schedule.schedule availability_request: short_request }.not_to change(&scheduled_count)
52
+ end
53
+ end
54
+
55
+ it 'does not accept a request' do
56
+ expect{ bobs_schedule.schedule availability_request: request }.not_to change(&scheduled_count)
57
+ end
58
+ end
59
+
60
+ context 'not at capacity' do
61
+ context 'with a shorter slot that fits within the availability' do
62
+ it 'accepts a request' do
63
+ expect{ bobs_schedule.schedule availability_request: short_request }.to change(&scheduled_count).from(0).to(1)
64
+ end
65
+ end
66
+
67
+ it 'accepts a request' do
68
+ expect{ bobs_schedule.schedule availability_request: request }.to change(&scheduled_count).from(0).to(1)
69
+ end
70
+ end
71
+ end
72
+
73
+ describe '#allow?' do
74
+ context 'enforces parameter values' do
75
+ it { expect{bobs_schedule.allow?}.to raise_error ArgumentError }
76
+ it { expect{bobs_schedule.allow? availability_request: nil}.to raise_error ArgumentError }
77
+ it { expect{bobs_schedule.allow? start_time: nil }.to raise_error ArgumentError }
78
+ it { expect{bobs_schedule.allow? start_time: Date.today }.to raise_error ArgumentError }
79
+ it { expect{bobs_schedule.allow? end_time: nil }.to raise_error ArgumentError }
80
+ it { expect{bobs_schedule.allow? end_time: Date.today }.to raise_error ArgumentError }
81
+ it { expect{bobs_schedule.allow? availability_request: one_hour_slot_per_week(start_time: T(0))}.not_to raise_error }
82
+ it { expect{bobs_schedule.allow? start_time: Date.today, end_time: Date.tomorrow}.not_to raise_error }
83
+ end
84
+
85
+ context 'with Availability objects' do
86
+ it 'allows an event requests that start in the first week and go through the end of the availability' do
87
+ bob_availabilities.each do |a|
88
+ expect(bobs_schedule.allow? availability_request: a).to be_truthy, "at #{a.start_time}"
89
+ end
90
+ end
91
+
92
+ it 'allows an event request in the second week that lasts until the end of the availability' do
93
+ ar = one_hour_slot_per_week(start_time: T(2016, 4, 18, 9), stops_by: T(2016, 5, 2, 9))
94
+ expect(bobs_schedule.allow? availability_request: ar).to be_truthy
95
+ end
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
99
+ end
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
103
+ end
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
107
+ end
108
+ end
109
+
110
+ context 'with start/end times' do
111
+ it 'allows an event requests that start in the first week and go through the end of the availability' do
112
+ bob_availabilities.each do |a|
113
+ expect(bobs_schedule.allow? start_time: a.start_time, end_time: a.end_time).to be_truthy, "at #{a.start_time}"
114
+ end
115
+ end
116
+
117
+ it 'allows an event request in the second week that lasts until the end of the availability' do
118
+ a = bob_availabilities[1]
119
+ expect(bobs_schedule.allow? start_time: a.start_time, end_time: a.end_time).to be_truthy
120
+ end
121
+
122
+ it 'does not allow an event request that is beyond the availability' do
123
+ start_time = bob_availabilities.last.start_time + 1.day
124
+ end_time = bob_availabilities.last.start_time + 2.days - 1.hour
125
+ expect(bobs_schedule.allow? start_time: start_time, end_time: end_time).to be_falsey
126
+ end
127
+
128
+ it 'does not allow an event request that starts before the availability frequency' do
129
+ start_time = bob_availabilities.first.start_time - 1.hour
130
+ end_time = bob_availabilities.first.start_time
131
+ expect(bobs_schedule.allow? start_time: start_time, end_time: end_time).to be_falsey
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,22 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext'
3
+ require_relative "availability/version"
4
+ require_relative 'availability/createable'
5
+ require_relative 'availability/exclusion'
6
+ require_relative 'availability/abstract_availability'
7
+ require_relative 'availability/daily'
8
+ require_relative 'availability/weekly'
9
+ require_relative 'availability/monthly'
10
+ require_relative 'availability/yearly'
11
+ require_relative 'availability/once'
12
+ require_relative 'availability/class_methods'
13
+ require_relative 'availability/factory_methods'
14
+
15
+ # shamelessly adapted from the following article
16
+ # http://dmcca.be/2014/01/09/recurring-subscriptions-with-ruby-rspec-and-modular-arithmetic.html
17
+ module Availability
18
+ class AbstractAvailability
19
+ extend FactoryMethods
20
+ extend ClassMethods
21
+ end
22
+ end
@@ -0,0 +1,173 @@
1
+ module Availability
2
+ class AbstractAvailability
3
+ private_class_method :new # :nodoc:
4
+
5
+ attr_accessor :capacity, :duration, :frequency, :stops_by
6
+ attr_reader :exclusions, :interval, :residue, :start_time
7
+
8
+ #
9
+ # 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
13
+ #
14
+ # 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
17
+ #
18
+ def initialize(capacity: Float::INFINITY, exclusions: nil, frequency: :daily, stops_by: nil, duration: , interval: , start_time: )
19
+ raise ArgumentError, "start_time is required" if start_time.nil?
20
+ raise ArgumentError, "duration is required" if duration.nil?
21
+ raise ArgumentError, "interval is required" if interval.nil?
22
+ @capacity = capacity
23
+ @duration = duration
24
+ @frequency = frequency
25
+ @interval = interval
26
+ @start_time = start_time.to_time
27
+ @stops_by = stops_by
28
+ self.exclusions = exclusions
29
+ compute_residue
30
+ end
31
+
32
+ #
33
+ # The copy constructor that Ruby calls when cloning or duping an object.
34
+ #
35
+ def initialize_copy(orig)
36
+ super
37
+ @exclusions = orig.exclusions
38
+ compute_residue
39
+ end
40
+
41
+ def beginning
42
+ self.class.beginning
43
+ end
44
+
45
+ def corresponds_to?(availability)
46
+ return unless occurs_at?(availability.start_time) && occurs_at?(availability.start_time + availability.duration)
47
+ if !!stops_by
48
+ that_last = availability.last_occurrence
49
+ !that_last.nil? &&
50
+ occurs_at?(that_last) &&
51
+ occurs_at?(that_last + availability.duration) &&
52
+ that_last.to_date <= self.last_occurrence.to_date
53
+ else
54
+ true
55
+ end
56
+ end
57
+
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
79
+ end
80
+
81
+ def interval=(interval)
82
+ @interval = interval
83
+ compute_residue
84
+ self
85
+ end
86
+
87
+ def interval_difference(first, second)
88
+ raise 'subclass responsibility'
89
+ end
90
+
91
+ def last_occurrence
92
+ return nil unless stops_by
93
+ unless @last_occurrence
94
+ next_date = move_by start_time, interval_difference(start_time, stops_by)
95
+ next_date = move_by next_date, -1 * interval while next_date >= stops_by && residue_for(next_date) != residue
96
+ @last_occurrence = next_occurrence next_date
97
+ end
98
+ @last_occurrence
99
+ end
100
+
101
+ def move_by(time, amount)
102
+ raise 'subclass responsibility'
103
+ end
104
+
105
+ def next_occurrence(from_date)
106
+ residue = @residue - residue_for(from_date)
107
+ date = move_by from_date, residue.modulo(interval)
108
+ time = Time.new(date.year, date.month, date.day, start_time.hour, start_time.min, start_time.sec)
109
+ if exclusions.any? {|rule| rule.violated_by? time}
110
+ if stops_by && time > stops_by
111
+ nil
112
+ else
113
+ next_occurrence(move_by time, 1)
114
+ end
115
+ else
116
+ time
117
+ end
118
+ end
119
+
120
+ #
121
+ # Returns an array of occurrences for n <= 1000, otherwise it returns a lazy enumerator
122
+ #
123
+ # n: Fixnum, how many occurrences to get
124
+ # from_date: a Date, Time, or DateTime from which to start calculating
125
+ #
126
+ def next_n_occurrences(n, from_date)
127
+ first_next_occurrence = next_occurrence(from_date)
128
+ blk = proc { |i| move_by first_next_occurrence, interval * i }
129
+ range = 0.upto(n - 1)
130
+ range = range.lazy if n > 1000
131
+ range.map &blk
132
+ end
133
+
134
+ def occurs_at?(time)
135
+ residue_for(time) == @residue && time_overlaps?(time, start_time, start_time + duration)
136
+ end
137
+
138
+ def residue_for(time)
139
+ raise 'subclass responsibility'
140
+ end
141
+
142
+ def start_time=(start_time)
143
+ @start_time = start_time
144
+ compute_residue
145
+ self
146
+ end
147
+
148
+ def time_overlaps?(time, start_time, end_time)
149
+ that_start = time.seconds_since_midnight.to_i
150
+ this_start = start_time.seconds_since_midnight.to_i
151
+ this_end = end_time.seconds_since_midnight.to_i
152
+ (this_start..this_end).include?(that_start)
153
+ end
154
+
155
+ private
156
+
157
+ def compute_residue
158
+ @residue = residue_for(@start_time)
159
+ end
160
+
161
+ def hour_offset_from_midnight(time)
162
+ time.to_time.hour * 60 * 60
163
+ end
164
+
165
+ def minute_offset_from_midnight(time)
166
+ time.to_time.min * 60
167
+ end
168
+
169
+ def second_offset_from_midnight(time)
170
+ time.to_time.sec
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,20 @@
1
+ module Availability
2
+ module ClassMethods
3
+ def availability?(thing)
4
+ AbstractAvailability === thing
5
+ end
6
+
7
+ def beginning
8
+ @@beginning ||= Date.new(1970, 1, 1)
9
+ end
10
+
11
+ def default_args
12
+ {}
13
+ end
14
+
15
+ def subclass_for(frequency)
16
+ Availability.const_get frequency.to_s.capitalize rescue nil
17
+ end
18
+ end
19
+ extend ClassMethods
20
+ end
@@ -0,0 +1,12 @@
1
+ module Availability
2
+ module Createable
3
+ def self.extended(base)
4
+ base.public_class_method :new
5
+ end
6
+
7
+ def create(**args)
8
+ frequency = name.split(':').last.downcase.to_sym
9
+ super **args.merge(frequency: frequency, event_class: self)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,20 @@
1
+ require_relative 'abstract_availability'
2
+
3
+ module Availability
4
+ class Daily < AbstractAvailability
5
+ extend Createable
6
+
7
+ def interval_difference(this, that)
8
+ first, second = [this.to_date, that.to_date].sort
9
+ (second - first).to_i
10
+ end
11
+
12
+ def move_by(time, amount)
13
+ time + amount.days
14
+ end
15
+
16
+ def residue_for(time)
17
+ interval_difference(time, beginning).modulo(@interval)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,91 @@
1
+ module Availability
2
+ class Exclusion
3
+ private_class_method :new # :nodoc:
4
+
5
+ def self.after_day(date)
6
+ raise ArgumentError, "invalid date" if date.nil?
7
+ new Rule::AfterDate.new(date.to_date)
8
+ end
9
+
10
+ def self.after_time(time)
11
+ raise ArgumentError, "invalid time" if time.nil?
12
+ new Rule::AfterTime.new(time.to_time)
13
+ end
14
+
15
+ def self.all_day(date)
16
+ raise ArgumentError, "invalid date" if date.nil?
17
+ new Rule::OnDate.new(date.to_date)
18
+ end
19
+
20
+ def self.before_day(date)
21
+ raise ArgumentError, "invalid date" if date.nil?
22
+ new Rule::BeforeDate.new(date.to_date)
23
+ end
24
+
25
+ def self.before_time(time)
26
+ raise ArgumentError, "invalid time" if time.nil?
27
+ new Rule::BeforeTime.new(time.to_time)
28
+ end
29
+
30
+ def initialize(rule)
31
+ @rule = rule
32
+ end
33
+
34
+ def violated_by?(time)
35
+ @rule.violated_by? time
36
+ end
37
+
38
+ private
39
+ module Rule
40
+ class AfterDate
41
+ def initialize(date)
42
+ @date = date
43
+ end
44
+
45
+ def violated_by?(time)
46
+ time.to_date > @date
47
+ end
48
+ end
49
+
50
+ class AfterTime
51
+ def initialize(date_or_time)
52
+ @compare_to = date_or_time.to_time
53
+ end
54
+
55
+ def violated_by?(time)
56
+ time.to_time.seconds_since_midnight > @compare_to.to_time.seconds_since_midnight
57
+ end
58
+ end
59
+
60
+ class BeforeDate
61
+ def initialize(date)
62
+ @date = date
63
+ end
64
+
65
+ def violated_by?(time)
66
+ time.to_date < @date
67
+ end
68
+ end
69
+
70
+ class BeforeTime
71
+ def initialize(date_or_time)
72
+ @compare_to = date_or_time.to_time
73
+ end
74
+
75
+ def violated_by?(time)
76
+ time.to_time.seconds_since_midnight < @compare_to.to_time.seconds_since_midnight
77
+ end
78
+ end
79
+
80
+ class OnDate
81
+ def initialize(date)
82
+ @date = date
83
+ end
84
+
85
+ def violated_by?(time)
86
+ time.to_date == @date
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,37 @@
1
+ module Availability
2
+ module FactoryMethods
3
+ def create(**args)
4
+ cls = args.delete(:event_class) || Availability::subclass_for(args[:frequency] ||= :daily)
5
+ raise ArgumentError, "undefined frequency" if cls.nil?
6
+ cls.send :new, **args
7
+ end
8
+
9
+ def once(**args)
10
+ Once.create **args
11
+ end
12
+
13
+ %w{day week month year}.each do |suffix|
14
+ frequency = suffix == 'day' ? :daily : :"#{suffix}ly"
15
+ cls = Availability::subclass_for(frequency)
16
+
17
+ define_method frequency do |**args|
18
+ args[:interval] ||= 1 unless args[:interval]
19
+ cls.create **args
20
+ end
21
+
22
+ {
23
+ :"every_#{suffix}" => 1,
24
+ :"every_other_#{suffix}" => 2,
25
+ :"every_two_#{suffix}s" => 2,
26
+ :"every_three_#{suffix}s" => 3,
27
+ :"every_four_#{suffix}s" => 4,
28
+ :"every_five_#{suffix}s" => 5
29
+ }.each do |method_name, interval|
30
+ define_method method_name do |**args|
31
+ cls.send(frequency, **args, interval: interval)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ extend FactoryMethods
37
+ end
@@ -0,0 +1,22 @@
1
+ require_relative 'abstract_availability'
2
+
3
+ module Availability
4
+ class Monthly < AbstractAvailability
5
+ extend Createable
6
+
7
+ def interval_difference(first, second)
8
+ first_date, second_date = [first.to_date, second.to_date].sort
9
+ (second_date.year - first_date.year) * 12 + (second_date.month - first_date.month)
10
+ end
11
+
12
+ def move_by(time, amount)
13
+ time + amount.months
14
+ end
15
+
16
+ def residue_for(time)
17
+ # date = time.to_date
18
+ # ((date.year - beginning.year) * 12 + (date.month - beginning.month)).modulo(@interval)
19
+ interval_difference(beginning, time).modulo(@interval)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,29 @@
1
+ require_relative 'abstract_availability'
2
+
3
+ module Availability
4
+ class Once < AbstractAvailability
5
+ extend Createable
6
+
7
+ def initialize(**args)
8
+ raise ArgumentError, "start_time is required" unless args.has_key?(:start_time)
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]
11
+ end
12
+
13
+ def interval_difference(this, that)
14
+ raise NotImplementedError.new('not supported')
15
+ end
16
+
17
+ def move_by(time, amount)
18
+ time + amount.days
19
+ end
20
+
21
+ def last_occurrence
22
+ start_time
23
+ end
24
+
25
+ def residue_for(time)
26
+ 0
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,3 @@
1
+ module Availability
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,24 @@
1
+ require_relative 'abstract_availability'
2
+
3
+ module Availability
4
+ class Weekly < AbstractAvailability
5
+ extend Createable
6
+
7
+ def interval
8
+ @interval * 7
9
+ end
10
+
11
+ def interval_difference(first, second)
12
+ first_date, second_date = [first.to_date, second.to_date].sort
13
+ (second_date - first_date).to_i #/ interval
14
+ end
15
+
16
+ def move_by(time, amount)
17
+ time + amount.days
18
+ end
19
+
20
+ def residue_for(time)
21
+ (time.to_date - beginning.to_date).to_i.modulo(interval)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ require_relative 'abstract_availability'
2
+
3
+ module Availability
4
+ class Yearly < AbstractAvailability
5
+ extend Createable
6
+
7
+ def interval_difference(first, second)
8
+ first_date, second_date = [first.to_date, second.to_date].sort
9
+ second_date.year - first_date.year
10
+ end
11
+
12
+ def move_by(time, amount)
13
+ time + amount.years
14
+ end
15
+
16
+ def residue_for(time)
17
+ # date = time.to_date
18
+ # (date.year - beginning.year).modulo(@interval)
19
+ interval_difference(beginning, time).modulo(@interval)
20
+ end
21
+ end
22
+ end
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: availability
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Jason Rogers
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-05-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.10'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.10'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec-its
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Use modular arithmetic and residue classes to calculate schedule availability
84
+ for dates (times handled separately).
85
+ email:
86
+ - jacaetevha@gmail.com
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - ".gitignore"
92
+ - ".rspec"
93
+ - ".ruby-gemset"
94
+ - ".ruby-version"
95
+ - ".travis.yml"
96
+ - CREDITS
97
+ - Gemfile
98
+ - README.md
99
+ - Rakefile
100
+ - UNLICENSE
101
+ - WAIVER
102
+ - availability.gemspec
103
+ - bin/console
104
+ - bin/setup
105
+ - examples/scheduler.rb
106
+ - examples/scheduler_spec.rb
107
+ - lib/availability.rb
108
+ - lib/availability/abstract_availability.rb
109
+ - lib/availability/class_methods.rb
110
+ - lib/availability/createable.rb
111
+ - lib/availability/daily.rb
112
+ - lib/availability/exclusion.rb
113
+ - lib/availability/factory_methods.rb
114
+ - lib/availability/monthly.rb
115
+ - lib/availability/once.rb
116
+ - lib/availability/version.rb
117
+ - lib/availability/weekly.rb
118
+ - lib/availability/yearly.rb
119
+ homepage: https://github.com/upper-hand/availability
120
+ licenses:
121
+ - Unlicense
122
+ metadata: {}
123
+ post_install_message:
124
+ rdoc_options: []
125
+ require_paths:
126
+ - lib
127
+ required_ruby_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ required_rubygems_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ requirements: []
138
+ rubyforge_project:
139
+ rubygems_version: 2.5.1
140
+ signing_key:
141
+ specification_version: 4
142
+ summary: Calculating schedule availability
143
+ test_files: []