timely 0.0.1 → 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.
- 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