availability 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []