timely 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +13 -0
- data/HISTORY.md +3 -0
- data/LICENSE +21 -0
- data/README.md +55 -0
- data/Rakefile +150 -4
- data/lib/timely/date.rb +3 -3
- data/lib/timely/date_chooser.rb +98 -0
- data/lib/timely/date_range.rb +51 -0
- data/lib/timely/date_time.rb +11 -0
- data/lib/timely/rails/date_group.rb +115 -0
- data/lib/timely/rails/extensions.rb +43 -0
- data/lib/timely/rails/season.rb +99 -0
- data/lib/timely/rails.rb +4 -0
- data/lib/timely/range.rb +15 -0
- data/lib/timely/string.rb +25 -0
- data/lib/timely/temporal_patterns.rb +441 -0
- data/lib/timely/time.rb +5 -4
- data/lib/timely/trackable_date_set.rb +148 -0
- data/lib/timely/week_days.rb +128 -0
- data/lib/timely.rb +15 -6
- data/rails/init.rb +1 -0
- data/spec/date_chooser_spec.rb +101 -0
- data/spec/date_group_spec.rb +26 -0
- data/spec/date_range_spec.rb +40 -0
- data/spec/date_spec.rb +15 -15
- data/spec/schema.rb +11 -0
- data/spec/season_spec.rb +68 -0
- data/spec/spec_helper.rb +41 -18
- data/spec/string_spec.rb +13 -0
- data/spec/time_spec.rb +28 -9
- data/spec/trackable_date_set_spec.rb +80 -0
- data/spec/week_days_spec.rb +51 -0
- data/timely.gemspec +99 -0
- metadata +61 -61
- data/History.txt +0 -4
- data/License.txt +0 -20
- data/Manifest.txt +0 -23
- data/README.txt +0 -31
- data/config/hoe.rb +0 -73
- data/config/requirements.rb +0 -15
- data/lib/timely/version.rb +0 -9
- data/script/console +0 -10
- data/script/destroy +0 -14
- data/script/generate +0 -14
- data/setup.rb +0 -1585
- data/spec/spec.opts +0 -1
- data/tasks/deployment.rake +0 -34
- data/tasks/environment.rake +0 -7
- data/tasks/rspec.rake +0 -21
- data/tasks/website.rake +0 -9
data/Gemfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
source :rubygems
|
2
|
+
gemspec
|
3
|
+
|
4
|
+
group :development, :test do
|
5
|
+
gem 'rake', '~> 0.9.2'
|
6
|
+
gem 'rdoc', '~> 3.12'
|
7
|
+
gem 'rspec'
|
8
|
+
gem 'simplecov-rcov'
|
9
|
+
gem 'simplecov'
|
10
|
+
gem 'sqlite3'
|
11
|
+
gem 'activesupport', '~> 2.3.0'
|
12
|
+
gem 'activerecord', '~> 2.3.0'
|
13
|
+
end
|
data/HISTORY.md
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) Tom Preston-Werner
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
Timely
|
2
|
+
======
|
3
|
+
|
4
|
+
# DESCRIPTION
|
5
|
+
|
6
|
+
Various helpers to work with times, dates and weekdays, etc.
|
7
|
+
|
8
|
+
It includes the following (see end for full descriptions)
|
9
|
+
* Core extensions to Date and Time
|
10
|
+
* DateChooser, a class to help select a subset of dates within any range, e.g. All 2nd Sundays, Every 15th of the month, All Tuesdays and Wednesdays
|
11
|
+
* WeekDays, a class to manage the selection of weekdays, outputs a integer representing which days as a number between 0 and 127 (e.g. a 7 bit integer)
|
12
|
+
* DateRange: A subclass of Range for dates with various helpers and aliases
|
13
|
+
* TrackableDateSet: Recording set of dates processed/processing
|
14
|
+
* TemporalPatterns: Various other classes related to time, e.g. Frequency
|
15
|
+
|
16
|
+
It includes the following rails extensions (only loaded if inside rails project):
|
17
|
+
* Date Group, a date range which can also be limited to WeekDays, e.g. all weekends between March and April
|
18
|
+
* Season, a collection of Date Groups
|
19
|
+
* weekdays_field, a way to declare an integer field to store weekdays (weekdays is stored as 7 bit integer)
|
20
|
+
* acts_as_seasonal, a way to declare a season_id foreign key as well as some helper methods
|
21
|
+
|
22
|
+
# INSTALLATION
|
23
|
+
|
24
|
+
gem install timely
|
25
|
+
|
26
|
+
or add to your Gemfile:
|
27
|
+
gem 'timely'
|
28
|
+
|
29
|
+
# SYNOPSIS
|
30
|
+
|
31
|
+
require 'timely'
|
32
|
+
|
33
|
+
For examples on most usage see the tests in the spec directory.
|
34
|
+
As these contain many basic examples with expected output.
|
35
|
+
|
36
|
+
## Core Extensions
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
some_date = Date.today - 5 # => 2008-05-03
|
40
|
+
some_date.at_time(3, 5, 13) # => Sat May 03 03:05:13 -0500 2008
|
41
|
+
|
42
|
+
# arguments are optional
|
43
|
+
some_date.at_time(13) # => Sat May 03 13:00:00 -0500 2008
|
44
|
+
|
45
|
+
some_time = Time.now - 345678 # => Sun May 04 13:40:22 -0500 2008
|
46
|
+
some_time.on_date(2001, 6, 18) # => Mon Jun 18 13:40:22 -0500 2001
|
47
|
+
|
48
|
+
# if you have objects corresponding to the times/dates you want
|
49
|
+
some_time.on_date(some_date) # => Sat May 03 13:40:22 -0500 2008
|
50
|
+
some_date.at_time(some_time) # => Sat May 03 13:40:22 -0500 2008
|
51
|
+
|
52
|
+
# if you like typing less
|
53
|
+
some_time.on(some_date) # => Sat May 03 13:40:22 -0500 2008
|
54
|
+
some_date.at(some_time) # => Sat May 03 13:40:22 -0500 2008
|
55
|
+
```
|
data/Rakefile
CHANGED
@@ -1,4 +1,150 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
3
|
-
|
4
|
-
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
#############################################################################
|
6
|
+
#
|
7
|
+
# Helper functions
|
8
|
+
#
|
9
|
+
#############################################################################
|
10
|
+
|
11
|
+
def name
|
12
|
+
@name ||= Dir['*.gemspec'].first.split('.').first
|
13
|
+
end
|
14
|
+
|
15
|
+
def version
|
16
|
+
line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
|
17
|
+
line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
|
18
|
+
end
|
19
|
+
|
20
|
+
def date
|
21
|
+
Date.today.to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
def rubyforge_project
|
25
|
+
name
|
26
|
+
end
|
27
|
+
|
28
|
+
def gemspec_file
|
29
|
+
"#{name}.gemspec"
|
30
|
+
end
|
31
|
+
|
32
|
+
def gem_file
|
33
|
+
"#{name}-#{version}.gem"
|
34
|
+
end
|
35
|
+
|
36
|
+
def replace_header(head, header_name)
|
37
|
+
head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
|
38
|
+
end
|
39
|
+
|
40
|
+
#############################################################################
|
41
|
+
#
|
42
|
+
# Standard tasks
|
43
|
+
#
|
44
|
+
#############################################################################
|
45
|
+
|
46
|
+
desc 'Default: run specs.'
|
47
|
+
task :default => :spec
|
48
|
+
|
49
|
+
require 'rspec/core/rake_task'
|
50
|
+
|
51
|
+
desc "Run specs"
|
52
|
+
RSpec::Core::RakeTask.new do |t|
|
53
|
+
t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
|
54
|
+
# Put spec opts in a file named .rspec in root
|
55
|
+
end
|
56
|
+
|
57
|
+
desc "Generate SimpleCov test coverage and open in your browser"
|
58
|
+
task :coverage do
|
59
|
+
ENV['COVERAGE'] = 'true'
|
60
|
+
Rake::Task['spec'].invoke
|
61
|
+
end
|
62
|
+
|
63
|
+
require 'rdoc/task'
|
64
|
+
RDoc::Task.new do |rdoc|
|
65
|
+
rdoc.rdoc_dir = 'rdoc'
|
66
|
+
rdoc.title = "#{name} #{version}"
|
67
|
+
rdoc.rdoc_files.include('README*')
|
68
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
69
|
+
end
|
70
|
+
|
71
|
+
desc "Open an irb session preloaded with this library"
|
72
|
+
task :console do
|
73
|
+
sh "irb -rubygems -r ./lib/#{name}.rb"
|
74
|
+
end
|
75
|
+
|
76
|
+
#############################################################################
|
77
|
+
#
|
78
|
+
# Custom tasks (add your own tasks here)
|
79
|
+
#
|
80
|
+
#############################################################################
|
81
|
+
|
82
|
+
|
83
|
+
|
84
|
+
#############################################################################
|
85
|
+
#
|
86
|
+
# Packaging tasks
|
87
|
+
#
|
88
|
+
#############################################################################
|
89
|
+
|
90
|
+
desc "Create tag v#{version} and build and push #{gem_file} to Rubygems"
|
91
|
+
task :release => :build do
|
92
|
+
unless `git branch` =~ /^\* master$/
|
93
|
+
puts "You must be on the master branch to release!"
|
94
|
+
exit!
|
95
|
+
end
|
96
|
+
sh "git commit --allow-empty -a -m 'Release #{version}'"
|
97
|
+
sh "git tag v#{version}"
|
98
|
+
sh "git push origin master"
|
99
|
+
sh "git push origin v#{version}"
|
100
|
+
sh "gem push pkg/#{name}-#{version}.gem"
|
101
|
+
end
|
102
|
+
|
103
|
+
desc "Build #{gem_file} into the pkg directory"
|
104
|
+
task :build => :gemspec do
|
105
|
+
sh "mkdir -p pkg"
|
106
|
+
sh "gem build #{gemspec_file}"
|
107
|
+
sh "mv #{gem_file} pkg"
|
108
|
+
end
|
109
|
+
|
110
|
+
desc "Generate #{gemspec_file}"
|
111
|
+
task :gemspec => :validate do
|
112
|
+
# read spec file and split out manifest section
|
113
|
+
spec = File.read(gemspec_file)
|
114
|
+
head, manifest, tail = spec.split(" # = MANIFEST =\n")
|
115
|
+
|
116
|
+
# replace name version and date
|
117
|
+
replace_header(head, :name)
|
118
|
+
replace_header(head, :version)
|
119
|
+
replace_header(head, :date)
|
120
|
+
#comment this out if your rubyforge_project has a different name
|
121
|
+
replace_header(head, :rubyforge_project)
|
122
|
+
|
123
|
+
# determine file list from git ls-files
|
124
|
+
files = `git ls-files`.
|
125
|
+
split("\n").
|
126
|
+
sort.
|
127
|
+
reject { |file| file =~ /^\./ }.
|
128
|
+
reject { |file| file =~ /^(rdoc|pkg)/ }.
|
129
|
+
map { |file| " #{file}" }.
|
130
|
+
join("\n")
|
131
|
+
|
132
|
+
# piece file back together and write
|
133
|
+
manifest = " s.files = %w[\n#{files}\n ]\n"
|
134
|
+
spec = [head, manifest, tail].join(" # = MANIFEST =\n")
|
135
|
+
File.open(gemspec_file, 'w') { |io| io.write(spec) }
|
136
|
+
puts "Updated #{gemspec_file}"
|
137
|
+
end
|
138
|
+
|
139
|
+
desc "Validate #{gemspec_file}"
|
140
|
+
task :validate do
|
141
|
+
libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"]
|
142
|
+
unless libfiles.empty?
|
143
|
+
puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir."
|
144
|
+
exit!
|
145
|
+
end
|
146
|
+
unless Dir['VERSION*'].empty?
|
147
|
+
puts "A `VERSION` file at root level violates Gem best practices."
|
148
|
+
exit!
|
149
|
+
end
|
150
|
+
end
|
data/lib/timely/date.rb
CHANGED
@@ -5,14 +5,14 @@ module Timely
|
|
5
5
|
time = hour
|
6
6
|
hour, minute, second = time.hour, time.min, time.sec
|
7
7
|
end
|
8
|
-
|
8
|
+
|
9
9
|
::Time.local(year, month, day, hour, minute, second)
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
alias_method :at, :at_time
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
16
16
|
class Date
|
17
17
|
include Timely::Date
|
18
|
-
end
|
18
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Timely
|
2
|
+
class DateChooser
|
3
|
+
# Where is this used... so far only in one place, _date_range.html.haml
|
4
|
+
# May be good to refactor this as well, after the class behaviour is refactored.
|
5
|
+
INTERVALS = [
|
6
|
+
{:code => 'w', :name => 'week(s)', :description =>
|
7
|
+
'Weekdays selected will be chosen every {{n}} weeks for the date range'},
|
8
|
+
{:code => 'wom', :name => 'week of month', :description =>
|
9
|
+
'Weekdays selected will be chosen in their {{ord}} occurance every month,
|
10
|
+
e.g. if wednesday and thursday are selected, the first wednesday and
|
11
|
+
first thursday are selected. Note: this may mean the booking is copied
|
12
|
+
to Thursday 1st and Wednesday 7th'}
|
13
|
+
]
|
14
|
+
|
15
|
+
attr_accessor :multiple_dates, :from, :to, :select, :dates, :interval, :weekdays
|
16
|
+
|
17
|
+
def initialize(options)
|
18
|
+
@multiple_dates = options[:multiple_dates] || false
|
19
|
+
@from = process_date(options[:from])
|
20
|
+
@to = process_date(options[:to])
|
21
|
+
@select = options[:select]
|
22
|
+
@dates = options[:dates]
|
23
|
+
@interval = options[:interval]
|
24
|
+
@weekdays = WeekDays.new(options[:weekdays]) if @select == 'weekdays'
|
25
|
+
validate
|
26
|
+
end
|
27
|
+
|
28
|
+
def process_date(date)
|
29
|
+
case date
|
30
|
+
when Date; date
|
31
|
+
when NilClass; nil
|
32
|
+
when String
|
33
|
+
date !~ /[^[:space:]]/ ? nil : date.to_date
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Chooses a set of dates from a date range, based on conditions.
|
38
|
+
# date_info - A hash with conditions and date information
|
39
|
+
# :from - The start of the date range
|
40
|
+
# :to - The end of the date range
|
41
|
+
#
|
42
|
+
# You can either specify specific dates to be chosen each month:
|
43
|
+
# :dates - A comma separated string of days of the month, e.g. 1,16
|
44
|
+
#
|
45
|
+
# or you can specify how to select the dates
|
46
|
+
# :day - A hash of days, the index being the wday, e.g. 0 = sunday, and the value being 1 if chosen
|
47
|
+
# :interval - A hash of information about the interval
|
48
|
+
# :level - The level/multiplier of the interval unit
|
49
|
+
# :unit - The unit of the interval, e.g. w for week, mow for month of week
|
50
|
+
# e.g. :level => 2, :unit => w would try to select the days of the week every fortnight,
|
51
|
+
# so every friday and saturday each fornight
|
52
|
+
def choose_dates
|
53
|
+
# Not multiple dates - just return the From date.
|
54
|
+
return [@from] if !@multiple_dates
|
55
|
+
|
56
|
+
# Multiple dates - return the array, adjusted as per input
|
57
|
+
all_days = (@from..@to).to_a
|
58
|
+
|
59
|
+
case @select
|
60
|
+
when 'days'
|
61
|
+
days = @dates.gsub(/\s/, '').split(',')
|
62
|
+
all_days.select { |date| days.include?(date.mday.to_s) }
|
63
|
+
when 'weekdays'
|
64
|
+
raise DateChooserException, "No days of the week selected" if @weekdays.weekdays.empty?
|
65
|
+
raise DateChooserException, "No weekly interval selected" if @interval && @interval.empty?
|
66
|
+
|
67
|
+
all_days.select do |date|
|
68
|
+
if @weekdays.has_day?(date.wday)
|
69
|
+
case @interval[:unit]
|
70
|
+
when 'w'
|
71
|
+
# 0 = first week, 1 = second week, 2 = third week, etc.
|
72
|
+
nth_week = (date - @from).to_i / 7
|
73
|
+
# true every 2nd week (0, 2, 4, 6, etc.)
|
74
|
+
(nth_week % @interval[:level].to_i).zero?
|
75
|
+
when 'wom'
|
76
|
+
week = @interval[:level].to_i
|
77
|
+
(date.mday > (week-1)*7 && date.mday <= week*7)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
else
|
82
|
+
all_days
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
def validate
|
88
|
+
if !@from
|
89
|
+
raise DateChooserException, "A Start Date is required"
|
90
|
+
elsif @multiple_dates
|
91
|
+
@to ||= @from
|
92
|
+
raise DateChooserException, "Start Date is after End Date" if @from > @to
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class DateChooserException < Exception; end
|
98
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Timely
|
2
|
+
class DateRange < ::Range
|
3
|
+
def initialize(*args)
|
4
|
+
if args.first.is_a?(Range)
|
5
|
+
super(args.first.first, args.first.last)
|
6
|
+
elsif args.size == 1 && args.first.is_a?(Date)
|
7
|
+
super(args.first, args.first)
|
8
|
+
else
|
9
|
+
super(*args)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
alias_method :start_date, :first
|
13
|
+
alias_method :end_date, :last
|
14
|
+
|
15
|
+
def self.from_params(start_date, duration = nil)
|
16
|
+
start_date = start_date.to_date
|
17
|
+
duration = [1, duration.to_i].max
|
18
|
+
|
19
|
+
new(start_date..(start_date + duration - 1))
|
20
|
+
end
|
21
|
+
|
22
|
+
def intersecting_dates(date_range)
|
23
|
+
start_of_intersection = [self.start_date, date_range.first].max
|
24
|
+
end_of_intersection = [self.end_date, date_range.last].min
|
25
|
+
intersection = if end_of_intersection >= start_of_intersection
|
26
|
+
(start_of_intersection..end_of_intersection)
|
27
|
+
else
|
28
|
+
[]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def number_of_nights
|
33
|
+
((last - first) + 1).to_i
|
34
|
+
end
|
35
|
+
alias_method :duration, :number_of_nights
|
36
|
+
|
37
|
+
def to_s(fmt = '%b %Y')
|
38
|
+
if first == last
|
39
|
+
first.to_s(:short)
|
40
|
+
elsif first == first.at_beginning_of_month && last == last.at_end_of_month
|
41
|
+
if first.month == last.month
|
42
|
+
first.strftime(fmt)
|
43
|
+
else
|
44
|
+
"#{first.strftime(fmt)} to #{last.strftime(fmt)}"
|
45
|
+
end
|
46
|
+
else
|
47
|
+
"#{first.to_s(:short)} to #{last.to_s(:short)}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module Timely
|
2
|
+
class DateGroup < ActiveRecord::Base
|
3
|
+
|
4
|
+
# acts_as_audited
|
5
|
+
|
6
|
+
belongs_to :season
|
7
|
+
|
8
|
+
weekdays_field :weekdays
|
9
|
+
|
10
|
+
validates_presence_of :start_date, :end_date
|
11
|
+
validate :validate_date_range!
|
12
|
+
|
13
|
+
def includes_date?(date)
|
14
|
+
date >= start_date && date <= end_date && weekdays.applies_for_date?(date)
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
def applicable_for_duration?(date_range)
|
19
|
+
if date_range.first > end_date || date_range.last < start_date
|
20
|
+
false
|
21
|
+
elsif weekdays.all_days?
|
22
|
+
true
|
23
|
+
else
|
24
|
+
date_range.intersecting_dates(start_date..end_date).any?{|d| weekdays.applies_for_date?(d)}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
def dates
|
30
|
+
start_date.upto(end_date).select { |d| weekdays.applies_for_date?(d) }
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
str = start_date && end_date ? (start_date..end_date).to_date_range.to_s : (start_date || end_date).to_s
|
36
|
+
|
37
|
+
unless weekdays.all_days?
|
38
|
+
str += " on #{weekdays}"
|
39
|
+
end
|
40
|
+
str
|
41
|
+
end
|
42
|
+
|
43
|
+
alias_method :audit_name, :to_s
|
44
|
+
|
45
|
+
|
46
|
+
|
47
|
+
################################################################
|
48
|
+
#---------------- Date intervals and patterns -----------------#
|
49
|
+
################################################################
|
50
|
+
|
51
|
+
|
52
|
+
def pattern
|
53
|
+
ranges = dates.group_by(&:wday).values.map { |weekdates| (weekdates.min..weekdates.max) }
|
54
|
+
TemporalPatterns::Pattern.new(ranges, 1.week)
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
def self.from_patterns(patterns)
|
59
|
+
date_groups = []
|
60
|
+
Array.wrap(patterns).each do |pattern|
|
61
|
+
if pattern.frequency.unit == :weeks
|
62
|
+
weekdays = pattern.intervals.map { |i| i.first_datetime.wday }.inject({}) do |hash, wday|
|
63
|
+
hash[wday] = 1
|
64
|
+
hash
|
65
|
+
end
|
66
|
+
date_groups << DateGroup.new(
|
67
|
+
:start_date => pattern.first_datetime.to_date,
|
68
|
+
:end_date => pattern.last_datetime.to_date,
|
69
|
+
:weekdays => weekdays)
|
70
|
+
elsif pattern.frequency.unit == :days && pattern.frequency.duration == 1.day
|
71
|
+
date_groups << DateGroup.new(
|
72
|
+
:start_date => pattern.first_datetime.to_date,
|
73
|
+
:end_date => pattern.last_datetime.to_date,
|
74
|
+
:weekdays => 127)
|
75
|
+
else
|
76
|
+
pattern.datetimes.each do |datetimes|
|
77
|
+
datetimes.group_by(&:week).values.each do |dates|
|
78
|
+
weekdays = dates.map(&:wday).inject({}) do |hash, wday|
|
79
|
+
hash[wday] = 1
|
80
|
+
hash
|
81
|
+
end
|
82
|
+
date_groups << DateGroup.new(
|
83
|
+
:start_date => dates.min.to_date.beginning_of_week,
|
84
|
+
:end_date => dates.max.to_date.end_of_week,
|
85
|
+
:weekdays => weekdays)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
date_groups
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def validate_date_range!
|
97
|
+
raise InvalidInputException, "Incorrect date range" if start_date && end_date && (start_date > end_date)
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# == Schema Information
|
104
|
+
#
|
105
|
+
# Table name: date_groups
|
106
|
+
#
|
107
|
+
# id :integer(4) not null, primary key
|
108
|
+
# season_id :integer(4)
|
109
|
+
# start_date :date
|
110
|
+
# end_date :date
|
111
|
+
# created_at :datetime
|
112
|
+
# updated_at :datetime
|
113
|
+
# weekdays_bit_array :integer(4)
|
114
|
+
#
|
115
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Timely
|
2
|
+
module Extensions
|
3
|
+
# Add a WeekDays attribute
|
4
|
+
#
|
5
|
+
# By default it will use attribute_bit_array as db field, but this can
|
6
|
+
# be overridden by specifying :db_field => 'somthing_else'
|
7
|
+
def weekdays_field(attribute, options={})
|
8
|
+
db_field = options[:db_field] || attribute.to_s + '_bit_array'
|
9
|
+
self.composed_of(attribute,
|
10
|
+
:class_name => "::Timely::WeekDays",
|
11
|
+
:mapping => [[db_field, 'weekdays_int']],
|
12
|
+
:converter => Proc.new {|field| ::Timely::WeekDays.new(field)}
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
def acts_as_seasonal
|
17
|
+
belongs_to :season
|
18
|
+
accepts_nested_attributes_for :season
|
19
|
+
validates_associated :season
|
20
|
+
|
21
|
+
named_scope :season_on, lambda { |*args|
|
22
|
+
date = args.first || ::Date.current # Can't assign in block in Ruby 1.8
|
23
|
+
{
|
24
|
+
:joins => {:season => :date_groups},
|
25
|
+
:conditions => ["date_groups.start_date <= ? AND date_groups.end_date >= ?", date, date]
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
named_scope :available_from, lambda { |*args|
|
30
|
+
date = args.first || ::Date.current # Can't assign in block in Ruby 1.8
|
31
|
+
{:conditions => ["boundary_end >= ?", date]}
|
32
|
+
}
|
33
|
+
|
34
|
+
before_save do |object|
|
35
|
+
if object.season
|
36
|
+
object.boundary_start = object.season.boundary_start
|
37
|
+
object.boundary_end = object.season.boundary_end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Timely
|
2
|
+
class Season < ActiveRecord::Base
|
3
|
+
# acts_as_audited
|
4
|
+
|
5
|
+
has_many :date_groups, :order => :start_date, :dependent => :destroy
|
6
|
+
|
7
|
+
# has_many :fare_bases
|
8
|
+
|
9
|
+
accepts_nested_attributes_for :date_groups,
|
10
|
+
:reject_if => proc {|attributes| attributes['start_date'].blank?},
|
11
|
+
:allow_destroy => true
|
12
|
+
|
13
|
+
|
14
|
+
def validate
|
15
|
+
errors.add_to_base("No dates specified") if date_groups.blank?
|
16
|
+
errors.empty?
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
def includes_date?(date)
|
21
|
+
date_groups.any?{|dg| dg.includes_date?(date)}
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def has_gaps?
|
26
|
+
last_date = nil
|
27
|
+
date_groups.each do |dg|
|
28
|
+
return true if last_date && dg.start_date != last_date + 1
|
29
|
+
end
|
30
|
+
false
|
31
|
+
end
|
32
|
+
|
33
|
+
def dates
|
34
|
+
date_groups.map do |date_group|
|
35
|
+
((date_group.start_date)..(date_group.end_date)).to_a
|
36
|
+
end.flatten
|
37
|
+
end
|
38
|
+
|
39
|
+
def boundary_range
|
40
|
+
boundary_start..boundary_end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def boundary_start
|
45
|
+
date_groups.map(&:start_date).sort.first
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
def boundary_end
|
50
|
+
date_groups.map(&:end_date).sort.last
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
def within_boundary?(date)
|
55
|
+
boundary_start && boundary_end && boundary_start <= date && boundary_end >= date
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
def deep_clone
|
60
|
+
cloned = self.clone
|
61
|
+
date_groups.each do |dg|
|
62
|
+
cloned.date_groups.build(dg.clone.attributes.except(:id))
|
63
|
+
end
|
64
|
+
cloned
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
def self.build_season_for(dates=[])
|
69
|
+
season = Season.new
|
70
|
+
date_groups = dates.map do |date|
|
71
|
+
DateGroup.new(:start_date => date, :end_date => date)
|
72
|
+
end
|
73
|
+
season.date_groups = date_groups
|
74
|
+
season
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_s
|
78
|
+
name
|
79
|
+
end
|
80
|
+
alias_method :audit_name, :to_s
|
81
|
+
|
82
|
+
def string_of_date_groups
|
83
|
+
date_groups.map{|dg| "#{dg.start_date.to_s(:short)} - #{dg.end_date.to_s(:short)}"}.to_sentence
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
|
90
|
+
# == Schema Information
|
91
|
+
#
|
92
|
+
# Table name: seasons
|
93
|
+
#
|
94
|
+
# id :integer(4) not null, primary key
|
95
|
+
# name :string(255)
|
96
|
+
# created_at :datetime
|
97
|
+
# updated_at :datetime
|
98
|
+
#
|
99
|
+
|
data/lib/timely/rails.rb
ADDED