timely 0.0.2 → 0.1.0
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 +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
|
-
|