timely 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/.travis.yml +14 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +1 -12
- data/README.md +5 -0
- data/Rakefile +1 -139
- data/gemfiles/rails3.gemfile +8 -0
- data/gemfiles/rails4.gemfile +9 -0
- data/lib/timely.rb +7 -3
- data/lib/timely/date.rb +20 -0
- data/lib/timely/date_chooser.rb +10 -5
- data/lib/timely/date_range.rb +47 -10
- data/lib/timely/rails.rb +10 -3
- data/lib/timely/rails/calendar_tag.rb +52 -0
- data/lib/timely/rails/date.rb +5 -0
- data/lib/timely/rails/date_group.rb +68 -99
- data/lib/timely/rails/date_range_validity_module.rb +27 -0
- data/lib/timely/rails/date_time.rb +20 -0
- data/lib/timely/rails/extensions.rb +23 -11
- data/lib/timely/rails/period.rb +31 -0
- data/lib/timely/rails/season.rb +65 -75
- data/lib/timely/railtie.rb +7 -0
- data/lib/timely/temporal_patterns/finder.rb +152 -0
- data/lib/timely/temporal_patterns/frequency.rb +108 -0
- data/lib/timely/temporal_patterns/interval.rb +67 -0
- data/lib/timely/temporal_patterns/pattern.rb +160 -0
- data/lib/timely/time_since.rb +17 -0
- data/lib/timely/version.rb +3 -0
- data/spec/calendar_tag_spec.rb +29 -0
- data/spec/date_chooser_spec.rb +36 -27
- data/spec/date_group_spec.rb +9 -9
- data/spec/date_range_spec.rb +58 -20
- data/spec/date_spec.rb +20 -12
- data/spec/extensions_spec.rb +32 -0
- data/spec/rails/date_spec.rb +16 -0
- data/spec/rails/date_time_spec.rb +20 -0
- data/spec/rails/period_spec.rb +17 -0
- data/spec/schema.rb +5 -0
- data/spec/season_spec.rb +21 -24
- data/spec/spec_helper.rb +5 -20
- data/spec/string_spec.rb +4 -3
- data/spec/support/coverage.rb +26 -0
- data/spec/temporal_patterns_spec.rb +28 -0
- data/spec/time_since_spec.rb +24 -0
- data/spec/time_spec.rb +14 -14
- data/spec/trackable_date_set_spec.rb +14 -14
- data/spec/week_days_spec.rb +18 -18
- data/timely.gemspec +34 -98
- metadata +244 -21
- data/lib/timely/temporal_patterns.rb +0 -441
@@ -0,0 +1,52 @@
|
|
1
|
+
module Timely
|
2
|
+
# Uses Date.current to be more accurate for Rails applications
|
3
|
+
def self.current_date
|
4
|
+
::Date.respond_to?(:current) ? ::Date.current : ::Date.today
|
5
|
+
end
|
6
|
+
|
7
|
+
|
8
|
+
module ActionViewHelpers
|
9
|
+
module FormTagHelper
|
10
|
+
def calendar_tag(name, value = Timely.current_date, *args)
|
11
|
+
options = args.extract_options!
|
12
|
+
|
13
|
+
value = value.to_s(:calendar) if value.respond_to?(:day)
|
14
|
+
|
15
|
+
name = name.to_s if name.is_a?(Symbol)
|
16
|
+
|
17
|
+
options[:id] = options[:id] || name.gsub(/\]$/, '').gsub(/\]\[/, '[').gsub(/[\[\]]/, '_')
|
18
|
+
|
19
|
+
options[:class] = options[:class].split(' ') if options[:class].is_a?(String)
|
20
|
+
options[:class] ||= []
|
21
|
+
options[:class] += %w(datepicker input-small)
|
22
|
+
options[:class] = options[:class].join(' ') # Rails 2 requires string values
|
23
|
+
|
24
|
+
options[:size] ||= 10
|
25
|
+
options[:maxlength] ||= 10
|
26
|
+
|
27
|
+
tag(:input, options.merge(:name => name, :type => 'text', :value => value)).html_safe
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
module DateHelper
|
33
|
+
def calendar(object_name, method, options = {})
|
34
|
+
value = options[:object] || Timely.current_date
|
35
|
+
calendar_tag("#{object_name}[#{method}]", value, options)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
module FormBuilder
|
40
|
+
def calendar(method, options = {})
|
41
|
+
options[:object] = @object.send(method) unless options.key?(:object)
|
42
|
+
@template.calendar(@object_name, method, options)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
if defined?(ActionView)
|
49
|
+
ActionView::Base.send :include, Timely::ActionViewHelpers::FormTagHelper
|
50
|
+
ActionView::Base.send :include, Timely::ActionViewHelpers::DateHelper
|
51
|
+
ActionView::Helpers::FormBuilder.send :include, Timely::ActionViewHelpers::FormBuilder
|
52
|
+
end
|
@@ -1,115 +1,84 @@
|
|
1
1
|
module Timely
|
2
|
-
class DateGroup < ActiveRecord::Base
|
3
|
-
|
4
|
-
# acts_as_audited
|
2
|
+
class DateGroup < ActiveRecord::Base
|
3
|
+
belongs_to :season, :class_name => 'Timely::Season'
|
5
4
|
|
6
|
-
|
5
|
+
weekdays_field :weekdays
|
7
6
|
|
8
|
-
|
7
|
+
validates_presence_of :start_date, :end_date
|
8
|
+
validate :validate_date_range!
|
9
9
|
|
10
|
-
|
11
|
-
|
10
|
+
def includes_date?(date)
|
11
|
+
date >= start_date && date <= end_date && weekdays.applies_for_date?(date)
|
12
|
+
end
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
14
|
+
def applicable_for_duration?(date_range)
|
15
|
+
if date_range.first > end_date || date_range.last < start_date
|
16
|
+
false
|
17
|
+
elsif weekdays.all_days?
|
18
|
+
true
|
19
|
+
else
|
20
|
+
date_range.intersecting_dates(start_date..end_date).any?{|d| weekdays.applies_for_date?(d)}
|
21
|
+
end
|
22
|
+
end
|
16
23
|
|
17
|
-
|
18
|
-
|
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)}
|
24
|
+
def dates
|
25
|
+
start_date.upto(end_date).select { |d| weekdays.applies_for_date?(d) }
|
25
26
|
end
|
26
|
-
end
|
27
|
-
|
28
|
-
|
29
|
-
def dates
|
30
|
-
start_date.upto(end_date).select { |d| weekdays.applies_for_date?(d) }
|
31
|
-
end
|
32
27
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
unless weekdays.all_days?
|
38
|
-
str += " on #{weekdays}"
|
28
|
+
def to_s
|
29
|
+
str = start_date && end_date ? (start_date..end_date).to_date_range.to_s : (start_date || end_date).to_s
|
30
|
+
str += " on #{weekdays}" unless weekdays.all_days?
|
31
|
+
str
|
39
32
|
end
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
weekdays = dates.map(&:wday).inject({}) do |hash, wday|
|
79
|
-
hash[wday] = 1
|
80
|
-
hash
|
33
|
+
|
34
|
+
################################################################
|
35
|
+
#---------------- Date intervals and patterns -----------------#
|
36
|
+
################################################################
|
37
|
+
|
38
|
+
def pattern
|
39
|
+
ranges = dates.group_by(&:wday).values.map { |weekdates| (weekdates.min..weekdates.max) }
|
40
|
+
TemporalPatterns::Pattern.new(ranges, 1.week)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.from_patterns(patterns)
|
44
|
+
date_groups = []
|
45
|
+
Array.wrap(patterns).each do |pattern|
|
46
|
+
if pattern.frequency.unit == :weeks
|
47
|
+
weekdays = pattern.intervals.map { |i| i.first_datetime.wday }.inject({}) do |hash, wday|
|
48
|
+
hash[wday] = 1
|
49
|
+
hash
|
50
|
+
end
|
51
|
+
date_groups << DateGroup.new(
|
52
|
+
:start_date => pattern.first_datetime.to_date,
|
53
|
+
:end_date => pattern.last_datetime.to_date,
|
54
|
+
:weekdays => weekdays)
|
55
|
+
elsif pattern.frequency.unit == :days && pattern.frequency.duration == 1.day
|
56
|
+
date_groups << DateGroup.new(
|
57
|
+
:start_date => pattern.first_datetime.to_date,
|
58
|
+
:end_date => pattern.last_datetime.to_date,
|
59
|
+
:weekdays => 127)
|
60
|
+
else
|
61
|
+
pattern.datetimes.each do |datetimes|
|
62
|
+
datetimes.group_by(&:week).values.each do |dates|
|
63
|
+
weekdays = dates.map(&:wday).inject({}) do |hash, wday|
|
64
|
+
hash[wday] = 1
|
65
|
+
hash
|
66
|
+
end
|
67
|
+
date_groups << DateGroup.new(
|
68
|
+
:start_date => dates.min.to_date.beginning_of_week,
|
69
|
+
:end_date => dates.max.to_date.end_of_week,
|
70
|
+
:weekdays => weekdays)
|
81
71
|
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
72
|
end
|
87
73
|
end
|
88
74
|
end
|
75
|
+
date_groups
|
89
76
|
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
77
|
|
100
|
-
|
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
|
-
#
|
78
|
+
private
|
115
79
|
|
80
|
+
def validate_date_range!
|
81
|
+
raise ArgumentError, "Incorrect date range" if start_date && end_date && (start_date > end_date)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Timely
|
2
|
+
module DateRangeValidityModule
|
3
|
+
def self.included(base)
|
4
|
+
base.class_eval do
|
5
|
+
validates :from, :to, :presence => true
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def validity_range
|
10
|
+
(from .. to)
|
11
|
+
end
|
12
|
+
|
13
|
+
def correctness_of_date_range
|
14
|
+
if (from.present? && to.present?)
|
15
|
+
errors.add(:base, "Invalid Date Range. From date should be less than or equal to To date") if from > to
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def validity_range_to_s
|
20
|
+
"#{from.to_s(:short)} ~ #{to.to_s(:short)}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def valid_on?(date)
|
24
|
+
validity_range.include?(date)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module RailsCoreExtensions
|
2
|
+
module DateTime
|
3
|
+
def advance_considering_calendar(units, num_units)
|
4
|
+
case units
|
5
|
+
when :seconds, :minutes, :hours, :days, :weeks, :months, :years
|
6
|
+
advance(units => num_units)
|
7
|
+
when :calendar_days
|
8
|
+
advance(:days => num_units - 1).end_of_day
|
9
|
+
when :calendar_months
|
10
|
+
advance(:months => num_units - 1).end_of_month
|
11
|
+
when :calendar_years
|
12
|
+
advance(:years => num_units - 1).end_of_year
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class DateTime
|
19
|
+
include RailsCoreExtensions::DateTime
|
20
|
+
end
|
@@ -14,22 +14,34 @@ module Timely
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def acts_as_seasonal
|
17
|
-
belongs_to :season
|
17
|
+
belongs_to :season, :class_name => 'Timely::Season'
|
18
18
|
accepts_nested_attributes_for :season
|
19
19
|
validates_associated :season
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
:conditions => ["date_groups.start_date <= ? AND date_groups.end_date >= ?", date, date]
|
21
|
+
if ::ActiveRecord::VERSION::MAJOR >= 3
|
22
|
+
scope :season_on, lambda { |*args|
|
23
|
+
date = args.first || ::Date.current # Can't assign in block in Ruby 1.8
|
24
|
+
joins(:season => :date_groups).where("date_groups.start_date <= ? AND date_groups.end_date >= ?", date, date)
|
26
25
|
}
|
27
|
-
}
|
28
26
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
27
|
+
scope :available_from, lambda { |*args|
|
28
|
+
date = args.first || ::Date.current # Can't assign in block in Ruby 1.8
|
29
|
+
where("boundary_end >= ?", date)
|
30
|
+
}
|
31
|
+
else
|
32
|
+
named_scope :season_on, lambda { |*args|
|
33
|
+
date = args.first || ::Date.current # Can't assign in block in Ruby 1.8
|
34
|
+
{
|
35
|
+
:joins => {:season => :date_groups},
|
36
|
+
:conditions => ["date_groups.start_date <= ? AND date_groups.end_date >= ?", date, date]
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
named_scope :available_from, lambda { |*args|
|
41
|
+
date = args.first || ::Date.current # Can't assign in block in Ruby 1.8
|
42
|
+
{:conditions => ["boundary_end >= ?", date]}
|
43
|
+
}
|
44
|
+
end
|
33
45
|
|
34
46
|
before_save do |object|
|
35
47
|
if object.season
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Timely
|
2
|
+
class Period
|
3
|
+
attr_reader :number, :units
|
4
|
+
|
5
|
+
UNITS = [
|
6
|
+
:seconds,
|
7
|
+
:minutes,
|
8
|
+
:hours,
|
9
|
+
:days,
|
10
|
+
:weeks,
|
11
|
+
:months,
|
12
|
+
:years,
|
13
|
+
:calendar_days,
|
14
|
+
:calendar_months,
|
15
|
+
:calendar_years
|
16
|
+
]
|
17
|
+
|
18
|
+
def initialize(number, units)
|
19
|
+
@number = number
|
20
|
+
@units = units.to_sym
|
21
|
+
end
|
22
|
+
|
23
|
+
def after(time)
|
24
|
+
time.advance_considering_calendar(units, number)
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
"#{number} #{units.to_s.gsub('_', '')}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/timely/rails/season.rb
CHANGED
@@ -1,99 +1,89 @@
|
|
1
1
|
module Timely
|
2
|
-
class Season < ActiveRecord::Base
|
3
|
-
# acts_as_audited
|
2
|
+
class Season < ActiveRecord::Base
|
4
3
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
4
|
+
if ::ActiveRecord::VERSION::MAJOR >= 4
|
5
|
+
has_many :date_groups, lambda { order(:start_date) }, :dependent => :destroy, :class_name => 'Timely::DateGroup'
|
6
|
+
else
|
7
|
+
has_many :date_groups, :order => :start_date, :dependent => :destroy, :class_name => 'Timely::DateGroup'
|
8
|
+
end
|
18
9
|
|
19
10
|
|
20
|
-
|
21
|
-
|
22
|
-
|
11
|
+
accepts_nested_attributes_for :date_groups,
|
12
|
+
:reject_if => proc {|attributes| attributes['start_date'].blank?},
|
13
|
+
:allow_destroy => true
|
23
14
|
|
15
|
+
validate :validate_dates_specified
|
24
16
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
return true if last_date && dg.start_date != last_date + 1
|
17
|
+
def validate_dates_specified
|
18
|
+
errors.add(:base, "No dates specified") if date_groups.blank?
|
19
|
+
errors.empty?
|
29
20
|
end
|
30
|
-
false
|
31
|
-
end
|
32
21
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
end.flatten
|
37
|
-
end
|
38
|
-
|
39
|
-
def boundary_range
|
40
|
-
boundary_start..boundary_end
|
41
|
-
end
|
22
|
+
def includes_date?(date)
|
23
|
+
date_groups.any?{|dg| dg.includes_date?(date)}
|
24
|
+
end
|
42
25
|
|
26
|
+
def has_gaps?
|
27
|
+
last_date = nil
|
28
|
+
date_groups.each do |dg|
|
29
|
+
return true if last_date && dg.start_date != last_date + 1
|
30
|
+
end
|
31
|
+
false
|
32
|
+
end
|
43
33
|
|
44
|
-
|
45
|
-
|
46
|
-
|
34
|
+
def dates
|
35
|
+
date_groups.map do |date_group|
|
36
|
+
((date_group.start_date)..(date_group.end_date)).to_a
|
37
|
+
end.flatten
|
38
|
+
end
|
47
39
|
|
40
|
+
def boundary_range
|
41
|
+
boundary_start..boundary_end
|
42
|
+
end
|
48
43
|
|
49
|
-
|
50
|
-
|
51
|
-
|
44
|
+
def past?
|
45
|
+
boundary_end && boundary_end < ::Date.current
|
46
|
+
end
|
52
47
|
|
48
|
+
def boundary_start
|
49
|
+
date_groups.map(&:start_date).sort.first
|
50
|
+
end
|
53
51
|
|
54
|
-
|
55
|
-
|
56
|
-
|
52
|
+
def boundary_end
|
53
|
+
date_groups.map(&:end_date).sort.last
|
54
|
+
end
|
57
55
|
|
56
|
+
def within_boundary?(date)
|
57
|
+
boundary_start && boundary_end && boundary_start <= date && boundary_end >= date
|
58
|
+
end
|
58
59
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
cloned.
|
60
|
+
def deep_clone
|
61
|
+
# Use clone until it is removed in AR 3.1, then dup is the same
|
62
|
+
method = ActiveRecord::Base.instance_methods(false).include?(:clone) ? :clone : :dup
|
63
|
+
cloned = self.send(method)
|
64
|
+
date_groups.each do |dg|
|
65
|
+
cloned.date_groups.build(dg.send(method).attributes)
|
66
|
+
end
|
67
|
+
cloned
|
63
68
|
end
|
64
|
-
cloned
|
65
|
-
end
|
66
69
|
|
70
|
+
def self.build_season_for(dates=[])
|
71
|
+
season = Season.new
|
72
|
+
date_groups = dates.map do |date|
|
73
|
+
DateGroup.new(:start_date => date, :end_date => date)
|
74
|
+
end
|
75
|
+
season.date_groups = date_groups
|
76
|
+
season
|
77
|
+
end
|
67
78
|
|
68
|
-
|
69
|
-
|
70
|
-
date_groups = dates.map do |date|
|
71
|
-
DateGroup.new(:start_date => date, :end_date => date)
|
79
|
+
def to_s
|
80
|
+
name.presence || Timely::DateRange.to_s(boundary_start, boundary_end)
|
72
81
|
end
|
73
|
-
season.date_groups = date_groups
|
74
|
-
season
|
75
|
-
end
|
76
82
|
|
77
|
-
|
78
|
-
name
|
79
|
-
end
|
80
|
-
alias_method :audit_name, :to_s
|
83
|
+
alias_method :audit_name, :to_s
|
81
84
|
|
82
|
-
|
83
|
-
|
85
|
+
def string_of_date_groups
|
86
|
+
date_groups.map{|dg| "#{dg.start_date.to_s(:short)} - #{dg.end_date.to_s(:short)}"}.to_sentence
|
87
|
+
end
|
84
88
|
end
|
85
89
|
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
|
-
|