weesked 0.0.2

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: d713ba18b65104cb45d0b4b44940399164fa1b31
4
+ data.tar.gz: 11d2fa26b7744f569caeb78f00f341d31d51d0fe
5
+ SHA512:
6
+ metadata.gz: 2ebfcab19081a08c2f85dba70d7c7abff2db357fb7d94fed6d7eb7b948a9073b30860428e788960504aadea0046c5aa1df085fa5fb05b1938f9052615e4d0cd5
7
+ data.tar.gz: ed4c59d6b925a01285a9b79c4eebe48601fedc46e3d9f8ee15ba87f54128a7003aedb9b148d0499ec24b43639065446930e5fcd3adcf72db9ff17ce23f065efb
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/.travis.yml ADDED
@@ -0,0 +1,2 @@
1
+ rvm:
2
+ - 2.1.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in weesked.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Igor Davydov
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
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
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,137 @@
1
+ [![Build Status](https://travis-ci.org/div/weesked.svg?branch=master)](https://travis-ci.org/div/weesked)
2
+
3
+ # Weesked
4
+
5
+ Simple weekely schedule based on redis sets
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'weesked'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install weesked
22
+
23
+ ## Setup
24
+
25
+ Weesked need Redis.current to return your redis instance
26
+
27
+ ## Usage
28
+
29
+ Let's say that we run some small buisness, we've got several employees which provide some service to our customers. Customers book appointments with our employees. But each employee has her personal week schedule. Some work in the morning, some on weekends etc. So you want to know if this employee is availiable on tuesday at 18.00. We need to check his appointments if he is free at that time, and also we need to check his schedule if he is even availiable on that day. Figuring out appointments is easy - we've got postgres ranges for that. But the weekly schedule is a bit trickier. We could use the same ranges approach to make availibility schedule for the comming weeks, but we'll have to maintain some process to keep this shcedule updated and so on. Other option is to use some scheduling lib like ice_cube. But you wont be able to query all your employees availiability easely.
30
+
31
+ Here comes weesked - redis weekly scheduler. Schedule is an array of redis sets, each list corresponds to time period in some incremets let's say an hour for now. So our week consists of 24*7=168 sets. We add our employee to a list to mark that she's not availiable at that time.
32
+
33
+ ```ruby
34
+ class Employee
35
+ include Weesked::Schedule
36
+
37
+ def self.weesked_schedule_key
38
+ "weesked:#{class_name.downcase}"
39
+ # by default each class has it's own schedule
40
+ # you can set same key for different classes to share same schedule
41
+ end
42
+
43
+ def id
44
+ # rails got that for you
45
+ end
46
+
47
+ def weesked_key
48
+ "#{class.class_name.downcase}:#{id}"
49
+ # that's defefault - fill free to override
50
+ end
51
+
52
+ end
53
+ ```
54
+
55
+ Including Weesked::Schedule module gives you the folowing:
56
+
57
+ ```ruby
58
+ employee = Employee.new
59
+
60
+ employee.schedule
61
+ => {monday: [1, 2, 5, 6, 9], tuesday: [0, 18, 25], ...}
62
+
63
+ employee.schedule = {monday: [1, 2, 5, 6, 9], tuesday: [0, 18, 25], ...}
64
+ => {monday: [1, 2, 5, 6, 9], tuesday: [0, 18, 25], ...}
65
+
66
+ employee.availiable? Time.now
67
+ => true
68
+
69
+ Employee.availiable Time.now
70
+ => [1, 3, 18]
71
+ # employee ids
72
+
73
+ Employee.availiable 10.hours.from_now..12.hours.from_now
74
+ => [3, 12]
75
+ ```
76
+
77
+ So if we're using rails with postgres and want to get the employees availiable and without appointments, you might do something like that:
78
+
79
+ ```ruby
80
+ time_range = 10.hours.from_now..12.hours.from_now
81
+ booked_employees = Employee.joins(:appointments).merge(Appointment.booked_on(time_range))
82
+ availiable_employees = Eployee.where id: Employee.availiable(time_range)
83
+ ```
84
+ Now you'll have to extract the first from the second to get those which are free at this time range.
85
+ Or you can do combine it in a single query (not sure how to do it in AR) and assign to a scope:
86
+
87
+ ```ruby
88
+ scope :free, -> (range) {
89
+ from("
90
+ (
91
+ (
92
+ #{Employee.availiable(range).to_sql}
93
+ )
94
+ except
95
+ (
96
+ #{Employee.booked_on(range).to_sql}
97
+ )
98
+ ) #{Employee.table_name}
99
+ ")
100
+ }
101
+ ```
102
+
103
+ ## Config
104
+
105
+ You might want to customize method names and/or time step if you need more granularity in your schedule.
106
+ You coluld throw this in an initializer or just config where ever suits your needs.
107
+
108
+ ```ruby
109
+ Weesked.setup do |config|
110
+ # ...
111
+ # Configures the methods needed by weesked
112
+ config.schedule_method = :schedule
113
+ config.availiable_method = :availiable
114
+
115
+ # Configures the default stuff about days and steps
116
+ config.time_step = 1.hour
117
+ config.availiable_days = %w(sunday monday tuesday wednesday thursday friday saturday)
118
+ config.availiable_steps = (9..18).to_a # if your step is 1.hour i'ts typical workours
119
+ config.steps_day_shift = 3 # number of steps if you want 2am on tuesday still be 'on monday'
120
+ # it's only needed if in you UI you want users to pick availiability on monday: from 18pm to 2am
121
+ # we make it easier to do so with day_shift
122
+ # ...
123
+ end
124
+
125
+ Redis.current = Redis.new(url: '//127.0.0.1:6379/1')
126
+ # but it's better to put redis initialization in it's own initializer
127
+ ```
128
+ But keep in mind that .schedule will return indexes of your new steps within a day. So if you set step to 15.minutes you'll have 96 periods in a day [0,95] and so on.
129
+
130
+
131
+ ## Contributing
132
+
133
+ 1. Fork it ( https://github.com/div/weesked/fork )
134
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
135
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
136
+ 4. Push to the branch (`git push origin my-new-feature`)
137
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake/testtask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task default: :test
7
+
8
+ desc 'Test the weesked gem.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.libs << 'test'
12
+ t.pattern = 'test/**/*_test.rb'
13
+ t.verbose = true
14
+ end
@@ -0,0 +1,41 @@
1
+ module Weesked
2
+ module Availiability
3
+
4
+ class << self
5
+ def included(klass)
6
+ klass.send :include, InstanceMethods
7
+ klass.extend ClassMethods
8
+ end
9
+ end
10
+
11
+ module ClassMethods
12
+
13
+ def availiability range
14
+ keys = range_keys range
15
+ if keys.empty?
16
+ []
17
+ else
18
+ redis.sinter *keys
19
+ end
20
+ end
21
+
22
+ def range_keys range
23
+ keys = []
24
+ day = Day.build range
25
+ return keys unless day.steps
26
+ day.steps.each do |step|
27
+ keys << self.weesked_schedule_key(day.day, step)
28
+ end
29
+ keys
30
+ end
31
+
32
+ end
33
+
34
+ module InstanceMethods
35
+ def availiable? range
36
+ self.class.availiability(range).include?(id.to_s)
37
+ end
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,59 @@
1
+ require File.expand_path('../version', __FILE__)
2
+
3
+ module Weesked
4
+ MINUTES_IN_HOUR = 60
5
+ SECONDS_IN_MINUTE = 60
6
+ SECONDS_IN_HOUR = MINUTES_IN_HOUR * SECONDS_IN_MINUTE
7
+ SUNDAY = 0
8
+ SATURDAY = 6
9
+
10
+ module Configuration
11
+ VALID_OPTIONS_KEYS = [
12
+ :time_step,
13
+ :availiable_days,
14
+ :availiable_steps,
15
+ :steps_day_shift,
16
+ # :redis
17
+ ].freeze
18
+
19
+
20
+ # By default, 1.hour
21
+ DEFAULT_TIME_STEP = SECONDS_IN_HOUR # 1.hour
22
+
23
+ # By default, the whole week
24
+ DEFAULT_AVAILIABLE_DAYS = %w(sunday monday tuesday wednesday thursday friday saturday)
25
+
26
+ # By default, whole day in hours
27
+ DEFAULT_AVAILIABLE_STEPS = (0..23).to_a
28
+
29
+ # By default, we use astonomical day
30
+ DEFAULT_STEPS_DAY_SHIFT = 0
31
+
32
+ attr_accessor *VALID_OPTIONS_KEYS
33
+
34
+ # When this module is extended, set all configuration options to their default values
35
+ def self.extended(base)
36
+ base.reset
37
+ end
38
+
39
+ # Convenience method to allow configuration options to be set in a block
40
+ def configure
41
+ yield self
42
+ end
43
+
44
+ # Create a hash of options and their values
45
+ def options
46
+ VALID_OPTIONS_KEYS.inject({}) do |option, key|
47
+ option.merge!(key => send(key))
48
+ end
49
+ end
50
+
51
+ # Reset all configuration options to defaults
52
+ def reset
53
+ self.time_step = DEFAULT_TIME_STEP
54
+ self.availiable_days = DEFAULT_AVAILIABLE_DAYS
55
+ self.availiable_steps = DEFAULT_AVAILIABLE_STEPS
56
+ self.steps_day_shift = DEFAULT_STEPS_DAY_SHIFT
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,25 @@
1
+ module Weesked
2
+ class WrongDay < StandardError; end
3
+ class Day
4
+ attr_reader :day
5
+
6
+ def self.build date
7
+ DayBuilder.new(date).run
8
+ end
9
+
10
+ def initialize(day, steps=[])
11
+ @steps = steps
12
+ @day = if day.kind_of?(Integer)
13
+ Weesked.availiable_days.fetch(day.to_i).to_sym
14
+ else
15
+ raise WrongDay unless Weesked.availiable_days.include?(day.to_s)
16
+ day.to_sym
17
+ end
18
+ end
19
+
20
+ def steps
21
+ steps = (Array(@steps)- ['', nil]).map(&:to_i)
22
+ Weesked.availiable_steps&steps
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,68 @@
1
+ module Weesked
2
+
3
+ class DateNotRecognized < StandardError; end
4
+ class NotAvailiable < StandardError; end
5
+
6
+ class DayBuilder
7
+
8
+ def initialize dates
9
+ @dates = dates
10
+ end
11
+
12
+ def run
13
+ if dates.kind_of? Range
14
+ build_from_date_range
15
+ elsif dates.kind_of? Time
16
+ build_from_single_date
17
+ else
18
+ raise DateNotRecognized
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :dates
25
+
26
+ def build_from_single_date date=dates
27
+ wd = date.wday
28
+ time = beginning_of_step date
29
+ step = step_index time
30
+ raise NotAvailiable unless Weesked.availiable_steps.include? step
31
+ if step < Weesked.steps_day_shift
32
+ if wd == SUNDAY
33
+ wd = SATURDAY
34
+ else
35
+ wd -= 1
36
+ end
37
+ end
38
+ Day.new(wd, step)
39
+ end
40
+
41
+ def build_from_date_range
42
+ start = build_from_single_date dates.begin
43
+ ending = build_from_single_date dates.end
44
+ raise NotAvailiable unless start.day == ending.day
45
+ i_start = Weesked.availiable_steps.index start.steps.first
46
+ i_end = Weesked.availiable_steps.index ending.steps.first
47
+ array = if Weesked.steps_day_shift > 0 && i_start > i_end
48
+ Weesked.availiable_steps.slice(i_start..-1) + Weesked.availiable_steps.slice(0..i_end)
49
+ else
50
+ Weesked.availiable_steps.slice(i_start..i_end)
51
+ end
52
+ Day.new start.day, array
53
+ end
54
+
55
+ def beginning_of_step time
56
+ (seconds_since_midnight(time) / Weesked.time_step) * Weesked.time_step
57
+ end
58
+
59
+ def step_index seconds
60
+ seconds / Weesked.time_step
61
+ end
62
+
63
+ def seconds_since_midnight time
64
+ time.hour * SECONDS_IN_HOUR + time.min * SECONDS_IN_MINUTE + time.sec
65
+ end
66
+ end
67
+
68
+ end
@@ -0,0 +1,53 @@
1
+ module Weesked
2
+ class OffsetHandler
3
+
4
+ def initialize(ary = [], offset = 0, length = 24)
5
+ @ary = ary.sort
6
+ @offset = offset
7
+ @length = length
8
+ end
9
+
10
+ def to_a
11
+ return ary if ary.length == 0 || offset == 0
12
+ ary.push(ary.shift(offset)).flatten
13
+ end
14
+
15
+ def to_range
16
+ to_a.inject([]) do |spans, n|
17
+ if start_new_range? spans, n
18
+ spans + [n..n]
19
+ else
20
+ spans[0..-2] + [spans.last.first..n]
21
+ end
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :ary, :offset, :length
28
+
29
+ def start_new_range? spans, current
30
+ return true if spans.empty?
31
+ previous = spans.last.last
32
+ return true unless consecutive?(previous, current) || consecutive_with_offset?(previous, current)
33
+ end
34
+
35
+ def max? n
36
+ n == max
37
+ end
38
+
39
+ def max
40
+ length - 1
41
+ end
42
+
43
+ def consecutive_with_offset? x, y
44
+ x % max == y && max?(x)
45
+ end
46
+
47
+ def consecutive? x, y
48
+ x == y - 1
49
+ end
50
+
51
+ end
52
+
53
+ end
@@ -0,0 +1,122 @@
1
+ module Weesked
2
+ module Schedule
3
+
4
+ class NotConnected < StandardError; end
5
+ class NilObjectId < StandardError; end
6
+
7
+ class << self
8
+ def redis=(conn)
9
+ @redis = conn
10
+ end
11
+
12
+ def redis
13
+ @redis || $redis || Redis.current ||
14
+ raise(NotConnected, "Redis not set to a Redis.new connection")
15
+ end
16
+
17
+ def included(klass)
18
+ klass.instance_variable_set('@redis', nil)
19
+ klass.send :include, InstanceMethods
20
+ klass.extend ClassMethods
21
+ end
22
+ end
23
+
24
+ module ClassMethods
25
+ attr_writer :redis
26
+ def redis
27
+ @redis || Schedule.redis
28
+ end
29
+
30
+ def redis_prefix=(redis_prefix) @redis_prefix = redis_prefix end
31
+ def redis_prefix(klass = self)
32
+ @redis_prefix ||= klass.name.to_s.
33
+ sub(%r{(.*::)}, '').
34
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
35
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
36
+ downcase
37
+ end
38
+
39
+ def weesked_schedule_key(day, step)
40
+ "weesked:availiability:#{self.name.downcase}:#{day}:#{step}"
41
+ end
42
+
43
+ def availiable date
44
+ end
45
+
46
+ def reset_schedule
47
+ redis.multi do
48
+ Weesked.availiable_days.each do |day|
49
+ Weesked.availiable_steps.each do |step|
50
+ redis.del weesked_schedule_key(day, step)
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ end
57
+
58
+ module InstanceMethods
59
+ def redis
60
+ self.class.redis
61
+ end
62
+
63
+ def schedule=(availiability_hash)
64
+ update_schedule_for_instance availiability_hash
65
+ update_schedule_for_class
66
+ end
67
+
68
+ def schedule(range=false)
69
+ return get_schedule unless range
70
+ Hash[get_schedule.map {|k,v| [k, OffsetHandler.new(v, Weesked.steps_day_shift, 24).to_range] }]
71
+ end
72
+
73
+ def availiable? date
74
+ end
75
+
76
+ def weesked_key(day)
77
+ raise NilDay unless day
78
+ if id.nil?
79
+ raise NilObjectId,
80
+ "Weesked schedule on class #{self.class.name} with nil id (unsaved record?) [object_id=#{object_id}]"
81
+ end
82
+ day = Day.new(day).day
83
+ "weesked:#{self.class.name.downcase}:#{id}:#{day}"
84
+ end
85
+
86
+ private
87
+
88
+ def update_schedule_for_class
89
+ sch = schedule
90
+ redis.multi do
91
+ Weesked.availiable_days.each do |day|
92
+ Weesked.availiable_steps.each do |step|
93
+ if sch[day.to_sym].include?(step)
94
+ redis.sadd self.class.weesked_schedule_key(day, step), id
95
+ else
96
+ redis.srem self.class.weesked_schedule_key(day, step), id
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ def update_schedule_for_instance hash
104
+ redis.multi do
105
+ Weesked.availiable_days.each do |d|
106
+ redis.del weesked_key(d)
107
+ steps = hash.fetch d.to_sym
108
+ day = Day.new d, steps
109
+ redis.sadd(weesked_key(d), day.steps) if day.steps.any?
110
+ end
111
+ end
112
+ end
113
+
114
+ def get_schedule
115
+ Weesked.availiable_days.each_with_object(Hash.new) do |day, h|
116
+ h[day.to_sym] = redis.smembers(weesked_key(day)).map(&:to_i)
117
+ end
118
+ end
119
+
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,3 @@
1
+ module Weesked
2
+ VERSION = "0.0.2"
3
+ end
data/lib/weesked.rb ADDED
@@ -0,0 +1,10 @@
1
+ require File.expand_path('../weesked/configuration', __FILE__)
2
+ require File.expand_path('../weesked/schedule', __FILE__)
3
+ require File.expand_path('../weesked/day', __FILE__)
4
+ require File.expand_path('../weesked/day_builder', __FILE__)
5
+ require File.expand_path('../weesked/availiability', __FILE__)
6
+ require File.expand_path('../weesked/offset_handler', __FILE__)
7
+
8
+ module Weesked
9
+ extend Configuration
10
+ end
@@ -0,0 +1,46 @@
1
+ require 'weesked'
2
+ require 'minitest'
3
+ require 'minitest/autorun'
4
+ require 'minitest/spec'
5
+ require 'fakeredis'
6
+
7
+
8
+ MONDAY_SUNDAY_12_14 = {
9
+ sunday: [ '12', '13', '14' ],
10
+ monday: [ '12', '13', '14' ],
11
+ tuesday: [ '12', '13', '14' ],
12
+ wednesday: [ '12', '13', '14' ],
13
+ thursday: [ '12', '13', '14' ],
14
+ friday: [ '12', '13', '14' ],
15
+ saturday: [ '12', '13', '14' ],
16
+ }.freeze
17
+
18
+ EMPTY_AVAIL = {
19
+ monday: [ ],
20
+ tuesday: [ ],
21
+ wednesday: [ ],
22
+ thursday: [ ],
23
+ friday: [ ],
24
+ saturday: [ ],
25
+ sunday: [ ]
26
+ }
27
+
28
+ MONDAY_TUESDAY_12_14 = {
29
+ monday: [ '12', '13', '14' ],
30
+ tuesday: [ '12', '13', '14' ],
31
+ wednesday: [ ],
32
+ thursday: [ ],
33
+ friday: [ ],
34
+ saturday: [ ],
35
+ sunday: [ ]
36
+ }
37
+
38
+ TUESDAY_12_14 = {
39
+ monday: [ ],
40
+ tuesday: [ '12', '13', '14' ],
41
+ wednesday: [ ],
42
+ thursday: [ ],
43
+ friday: [ ],
44
+ saturday: [ ],
45
+ sunday: [ ]
46
+ }
@@ -0,0 +1,155 @@
1
+ require File.expand_path("../../test_helper", __FILE__)
2
+
3
+ class Myclass
4
+ attr_accessor :id
5
+ include Weesked::Schedule
6
+ include Weesked::Availiability
7
+ def initialize(id) @id=id; end
8
+ end
9
+
10
+ module Weesked
11
+ describe Availiability do
12
+
13
+ let(:sitter1) { Myclass.new 13}
14
+ let(:sitter2) { Myclass.new 18}
15
+
16
+ let(:s1) {
17
+ MONDAY_TUESDAY_12_14.dup
18
+ }
19
+
20
+ let(:s2) {
21
+ TUESDAY_12_14.dup
22
+ }
23
+
24
+ let(:empty) {
25
+ EMPTY_AVAIL.dup
26
+ }
27
+
28
+ subject { Myclass.availiability(date_range) }
29
+ let(:start_at) { 11 }
30
+ let(:end_at) { 15 }
31
+ let(:monday_range) { Time.local(2020, 'jan', 6, start_at)..Time.local(2020, 'jan', 6, end_at, 34) }
32
+ let(:tuesday_range) { Time.local(2020, 'jan', 7, start_at)..Time.local(2020, 'jan', 7, end_at, 34) }
33
+ let(:date_range) { monday_range }
34
+
35
+ before do
36
+ Myclass.reset_schedule
37
+ sitter1.schedule = s1
38
+ sitter2.schedule = s2
39
+ end
40
+
41
+ describe '.availibility' do
42
+
43
+ describe 'single sitter' do
44
+
45
+ describe 'no intersection' do
46
+ let(:start_at) { 15 }
47
+ let(:end_at) { 18 }
48
+ it 'empty array' do
49
+ subject.must_equal Set.new
50
+ end
51
+ end
52
+
53
+ describe 'partial intersection' do
54
+ let(:start_at) { 14 }
55
+ let(:end_at) { 15 }
56
+ it 'empty array' do
57
+ subject.must_equal Set.new
58
+ end
59
+ end
60
+
61
+ describe 'date_range broader than availibility' do
62
+ let(:start_at) { 10 }
63
+ let(:end_at) { 22 }
64
+ it 'empty array' do
65
+ subject.must_equal Set.new
66
+ end
67
+ end
68
+
69
+ describe 'exact intersection' do
70
+ let(:start_at) { 12 }
71
+ let(:end_at) { 14 }
72
+ it 'sitter id' do
73
+ subject.must_equal [ sitter1.id.to_s ]
74
+ end
75
+ end
76
+
77
+ describe 'date_range narrower than availibility' do
78
+ let(:start_at) { 12 }
79
+ let(:end_at) { 13 }
80
+ it 'sitter id' do
81
+ subject.must_equal [ sitter1.id.to_s ]
82
+ end
83
+ end
84
+
85
+ end
86
+
87
+ describe 'multiple sitters' do
88
+
89
+ let(:date_range) { tuesday_range }
90
+
91
+ describe 'no intersection' do
92
+ let(:start_at) { 15 }
93
+ let(:end_at) { 18 }
94
+ it 'empty array' do
95
+ subject.must_equal Set.new
96
+ end
97
+ end
98
+
99
+ describe 'partial intersection' do
100
+ let(:start_at) { 14 }
101
+ let(:end_at) { 15 }
102
+ it 'empty array' do
103
+ subject.must_equal Set.new
104
+ end
105
+ end
106
+
107
+ describe 'date_range broader than availibility' do
108
+ let(:start_at) { 10 }
109
+ let(:end_at) { 22 }
110
+ it 'empty array' do
111
+ subject.must_equal Set.new
112
+ end
113
+ end
114
+
115
+ describe 'exact intersection' do
116
+ let(:start_at) { 12 }
117
+ let(:end_at) { 14 }
118
+ it 'sitter id' do
119
+ subject.must_equal [ sitter1.id.to_s, sitter2.id.to_s ]
120
+ end
121
+ end
122
+
123
+ describe 'date_range narrower than availibility' do
124
+ let(:start_at) { 12 }
125
+ let(:end_at) { 13 }
126
+ it 'sitter id' do
127
+ subject.must_equal [ sitter1.id.to_s, sitter2.id.to_s ]
128
+ end
129
+ end
130
+
131
+ end
132
+
133
+ describe '.availiable?' do
134
+ subject { sitter1.availiable?(date_range) }
135
+
136
+ describe 'for availiable' do
137
+ let(:start_at) { 12 }
138
+ let(:end_at) { 13 }
139
+ it 'true' do
140
+ subject.must_equal true
141
+ end
142
+ end
143
+
144
+ describe 'for unavailiable' do
145
+ let(:start_at) { 18 }
146
+ let(:end_at) { 19 }
147
+ it 'false' do
148
+ subject.must_equal false
149
+ end
150
+ end
151
+ end
152
+
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,13 @@
1
+ require File.expand_path("../../test_helper", __FILE__)
2
+
3
+ module Weesked
4
+ describe Configuration do
5
+ Configuration::VALID_OPTIONS_KEYS.each do |key|
6
+ describe ".#{key}" do
7
+ it 'returns default value' do
8
+ Weesked.send(key).must_equal Configuration.const_get("DEFAULT_#{key.upcase}")
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,98 @@
1
+ require File.expand_path("../../test_helper", __FILE__)
2
+
3
+ module Weesked
4
+ describe DayBuilder do
5
+
6
+
7
+ describe '#run' do
8
+ describe 'converts single datetime to Day' do
9
+
10
+
11
+ let(:dates) {
12
+ {
13
+ sunday: Time.local(2020, 'jan', 5, hour, 17),
14
+ monday: Time.local(2020, 'jan', 6, hour, 12),
15
+ wednesday: Time.local(2020, 'jan', 8, hour, 13),
16
+ }
17
+ }
18
+
19
+ describe 'with day hours' do
20
+ let(:hour) { 14 }
21
+
22
+ it 'returns correct day' do
23
+ dates.each_pair do |day, date|
24
+ subject = DayBuilder.new(date).run
25
+ date.wday.must_equal Weesked.availiable_days.index(day.to_s)
26
+ subject.must_be_instance_of Day
27
+ subject.day.must_equal day
28
+ subject.steps.must_equal [ hour ]
29
+ end
30
+ end
31
+
32
+ end
33
+
34
+ describe 'handles unavailiable times' do
35
+ let(:hour) { 2 }
36
+ it "works" do
37
+ Weesked.availiable_steps = [0,1]
38
+ dates.each_value do |date|
39
+ -> { DayBuilder.new(date).run }.must_raise NotAvailiable
40
+ end
41
+ Weesked.reset
42
+ end
43
+ end
44
+ end
45
+
46
+ describe 'converts date range to window' do
47
+
48
+ describe 'with day hours' do
49
+ let(:monday_day_range) { Time.local(2020, 'jan', 6, start_at)..Time.local(2020, 'jan', 6, end_at, 34) }
50
+ let(:start_at) { 11 }
51
+ let(:end_at) { 15 }
52
+ subject { DayBuilder.new(monday_day_range).run }
53
+ it 'works for monday' do
54
+ subject.day.must_equal :monday
55
+ subject.steps.must_equal (start_at..end_at).to_a
56
+ end
57
+ end
58
+
59
+ describe 'with night hours' do
60
+ let(:monday_night_range) { Time.local(2020, 'jan', 6, start_at, 12)..Time.local(2020, 'jan', 7, end_at, 34) }
61
+ let(:start_at) { 22 }
62
+ let(:end_at) { 1 }
63
+ subject { DayBuilder.new(monday_night_range).run }
64
+ before do
65
+ Weesked.steps_day_shift = 2
66
+ end
67
+ it 'works for night' do
68
+ subject.day.must_equal :monday
69
+ subject.steps.must_equal [0, 1, 22, 23]
70
+ end
71
+ after do
72
+ Weesked.reset
73
+ end
74
+ end
75
+
76
+ describe 'with unavailiable hours' do
77
+ let(:monday_night_range) { Time.local(2020, 'jan', 6, start_at, 12)..Time.local(2020, 'jan', 7, end_at, 34) }
78
+ let(:start_at) { 1 }
79
+ let(:end_at) { 4 }
80
+ subject { DayBuilder.new(monday_night_range).run }
81
+ it 'works for night' do
82
+ -> { subject }.must_raise NotAvailiable
83
+ end
84
+ end
85
+
86
+ describe 'with too long range' do
87
+ let(:monday_night_range) { Time.local(2020, 'jan', 6, start_at, 12)..Time.local(2020, 'jan', 10, end_at, 34) }
88
+ let(:start_at) { 22 }
89
+ let(:end_at) { 1 }
90
+ subject { DayBuilder.new(monday_night_range).run }
91
+ it 'works for night' do
92
+ -> { subject }.must_raise NotAvailiable
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,86 @@
1
+ require File.expand_path("../../test_helper", __FILE__)
2
+
3
+ module Weesked
4
+ describe Day do
5
+
6
+ subject { Day.new day, steps}
7
+ let(:steps) { [18, 19] }
8
+ let(:day) { :monday }
9
+
10
+ describe '.steps' do
11
+
12
+ describe 'can be initialized with' do
13
+ describe 'single int' do
14
+ let(:steps) { 22 }
15
+ it 'works' do
16
+ subject.steps.must_equal [22]
17
+ end
18
+ end
19
+
20
+ describe'single literal' do
21
+ let(:steps) { '22' }
22
+ it 'works' do
23
+ subject.steps.must_equal [22]
24
+ end
25
+ end
26
+
27
+ describe'array of ints' do
28
+ let(:steps) { [22, 23] }
29
+ it 'works' do
30
+ subject.steps.must_equal [22, 23]
31
+ end
32
+ end
33
+
34
+ describe'array of literals' do
35
+ let(:steps) { ['22', '23'] }
36
+ it 'works' do
37
+ subject.steps.must_equal [22, 23]
38
+ end
39
+ end
40
+ end
41
+
42
+ describe 'unavailiable steps' do
43
+ let(:steps) { [22, 23, 24, 25, 26] }
44
+ it 'skips' do
45
+ subject.steps.must_equal [22, 23]
46
+ end
47
+ end
48
+
49
+ end
50
+
51
+ describe '.day' do
52
+
53
+ describe 'can be initialized with' do
54
+ describe 'single int' do
55
+ let(:day) { 1 }
56
+ it 'works' do
57
+ subject.day.must_equal :monday
58
+ end
59
+ end
60
+
61
+ describe 'day name as string' do
62
+ let(:day) { 'monday' }
63
+ it 'works' do
64
+ subject.day.must_equal :monday
65
+ end
66
+ end
67
+
68
+ describe 'day name as symbol' do
69
+ let(:day) { :monday }
70
+ it 'works' do
71
+ subject.day.must_equal :monday
72
+ end
73
+ end
74
+ end
75
+
76
+ describe 'unavailiable day' do
77
+ let(:day) { :monday123 }
78
+ it 'raises' do
79
+ -> { subject }.must_raise WrongDay
80
+ end
81
+ end
82
+ end
83
+
84
+
85
+ end
86
+ end
@@ -0,0 +1,39 @@
1
+ require File.expand_path("../../test_helper", __FILE__)
2
+
3
+ module Weesked
4
+ describe OffsetHandler do
5
+
6
+ let(:input) { [0, 1, 13, 14, 15, 21, 22, 23] }
7
+ let(:input2) { [1, 2, 3, 13, 14, 15, 21, 22, 23] }
8
+ let(:offset) { 2 }
9
+ let(:offset2) { 3 }
10
+
11
+ describe '#offset' do
12
+ let(:output) { [13, 14, 15, 21, 22, 23, 0, 1] }
13
+ let(:output2) { [13, 14, 15, 21, 22, 23, 1, 2, 3] }
14
+ subject { OffsetHandler.new(input, offset).to_a }
15
+ it 'returns same array if input empty or offset in zero' do
16
+ OffsetHandler.new.to_a.must_equal []
17
+ OffsetHandler.new(input).to_a.must_equal input
18
+ end
19
+ it 'returns array with offset' do
20
+ subject.must_equal output
21
+ end
22
+ it 'returns array with offset' do
23
+ OffsetHandler.new(input2, offset2).to_a.must_equal output2
24
+ end
25
+ end
26
+
27
+ describe '#to_range' do
28
+ let(:output) { [13..15, 21..1] }
29
+ let(:output2) { [13..15, 21..23, 1..3] }
30
+ subject { OffsetHandler.new(input, offset).to_range }
31
+ it 'returns array with ranges' do
32
+ subject.must_equal output
33
+ end
34
+ it 'returns array with ranges' do
35
+ (OffsetHandler.new(input2, offset2).to_range).must_equal output2
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,87 @@
1
+ require File.expand_path("../../test_helper", __FILE__)
2
+
3
+ class MyClass
4
+ include Weesked::Schedule
5
+ def id
6
+ 1
7
+ end
8
+ end
9
+
10
+ module Weesked
11
+ describe Schedule do
12
+
13
+ before do
14
+ MyClass.redis = Redis.new
15
+ end
16
+
17
+ subject { MyClass.new }
18
+
19
+ let(:availiability) {
20
+ MONDAY_SUNDAY_12_14.dup
21
+ }
22
+
23
+ let(:availiability_int) {
24
+ availiability.each_with_object(Hash.new) do |k, h|
25
+ h[k.first] = Array(k.last).map(&:to_i).sort.reverse
26
+ end
27
+ }
28
+
29
+ it '#redis' do
30
+ MyClass.redis = '123'
31
+ subject.redis.must_equal '123'
32
+ end
33
+
34
+ it '#weesked_schedule_key' do
35
+ MyClass.weesked_schedule_key(:monday, 10).must_equal 'weesked:availiability:myclass:monday:10'
36
+ end
37
+
38
+ it 'has key' do
39
+ subject.weesked_key(:monday).must_equal 'weesked:myclass:1:monday'
40
+ end
41
+
42
+ describe '.schedule=' do
43
+
44
+ describe 'with date hash' do
45
+
46
+ it 'saves to redis with strings' do
47
+ subject.schedule = availiability
48
+ Redis.current.smembers(subject.weesked_key(:monday)).sort.must_equal availiability[:monday].map(&:to_s)
49
+ end
50
+
51
+ it 'saves to redis with empty array' do
52
+ availiability[:monday] = []
53
+ subject.schedule = availiability
54
+ Redis.current.smembers(subject.weesked_key(:monday)).sort.must_equal availiability[:monday].map(&:to_s)
55
+ end
56
+
57
+ it 'clears before save' do
58
+ subject.schedule = availiability
59
+ availiability[:monday] = [10]
60
+ subject.schedule = availiability
61
+ Redis.current.smembers(subject.weesked_key(:monday)).sort.must_equal availiability[:monday].map(&:to_s)
62
+ end
63
+
64
+ it 'handles empty string' do
65
+ availiability[:monday] = ''
66
+ subject.schedule = availiability
67
+ Redis.current.smembers(subject.weesked_key(:monday)).sort.must_equal []
68
+ end
69
+
70
+ it 'saves to redis with ints' do
71
+ subject.schedule = availiability_int
72
+ Redis.current.smembers(subject.weesked_key(:monday)).sort.must_equal availiability[:monday].map(&:to_s)
73
+ end
74
+ end
75
+ end
76
+
77
+ describe '.schedule' do
78
+
79
+ it 'gets hash' do
80
+ subject.schedule = availiability
81
+ subject.schedule.must_equal availiability_int
82
+ end
83
+
84
+ end
85
+
86
+ end
87
+ end
@@ -0,0 +1,7 @@
1
+ require File.expand_path("../test_helper", __FILE__)
2
+
3
+ describe Weesked do
4
+ it "is valid" do
5
+ Weesked.must_be_kind_of Module
6
+ end
7
+ end
data/weesked.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'weesked/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "weesked"
8
+ spec.version = Weesked::VERSION
9
+ spec.authors = ["Igor Davydov"]
10
+ spec.email = ["iskiche@gmail.com"]
11
+ spec.summary = %q{Simple availialibility schedlule based on Redis lists}
12
+ spec.description = %q{Each time step has its redis list of booked objects}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.6"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "minitest", "~> 5"
24
+ spec.add_development_dependency "fakeredis", "~> 0.3"
25
+ end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: weesked
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Igor Davydov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-12-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: fakeredis
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.3'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.3'
69
+ description: Each time step has its redis list of booked objects
70
+ email:
71
+ - iskiche@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".travis.yml"
78
+ - Gemfile
79
+ - LICENSE.txt
80
+ - README.md
81
+ - Rakefile
82
+ - lib/weesked.rb
83
+ - lib/weesked/availiability.rb
84
+ - lib/weesked/configuration.rb
85
+ - lib/weesked/day.rb
86
+ - lib/weesked/day_builder.rb
87
+ - lib/weesked/offset_handler.rb
88
+ - lib/weesked/schedule.rb
89
+ - lib/weesked/version.rb
90
+ - test/test_helper.rb
91
+ - test/weesked/availiability_test.rb
92
+ - test/weesked/configuration_test.rb
93
+ - test/weesked/day_bulder_test.rb
94
+ - test/weesked/day_test.rb
95
+ - test/weesked/offset_handler_spec.rb
96
+ - test/weesked/schedule_test.rb
97
+ - test/weesked_test.rb
98
+ - weesked.gemspec
99
+ homepage: ''
100
+ licenses:
101
+ - MIT
102
+ metadata: {}
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubyforge_project:
119
+ rubygems_version: 2.4.4
120
+ signing_key:
121
+ specification_version: 4
122
+ summary: Simple availialibility schedlule based on Redis lists
123
+ test_files:
124
+ - test/test_helper.rb
125
+ - test/weesked/availiability_test.rb
126
+ - test/weesked/configuration_test.rb
127
+ - test/weesked/day_bulder_test.rb
128
+ - test/weesked/day_test.rb
129
+ - test/weesked/offset_handler_spec.rb
130
+ - test/weesked/schedule_test.rb
131
+ - test/weesked_test.rb