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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +14 -0
  5. data/CHANGELOG.md +5 -0
  6. data/Gemfile +1 -12
  7. data/README.md +5 -0
  8. data/Rakefile +1 -139
  9. data/gemfiles/rails3.gemfile +8 -0
  10. data/gemfiles/rails4.gemfile +9 -0
  11. data/lib/timely.rb +7 -3
  12. data/lib/timely/date.rb +20 -0
  13. data/lib/timely/date_chooser.rb +10 -5
  14. data/lib/timely/date_range.rb +47 -10
  15. data/lib/timely/rails.rb +10 -3
  16. data/lib/timely/rails/calendar_tag.rb +52 -0
  17. data/lib/timely/rails/date.rb +5 -0
  18. data/lib/timely/rails/date_group.rb +68 -99
  19. data/lib/timely/rails/date_range_validity_module.rb +27 -0
  20. data/lib/timely/rails/date_time.rb +20 -0
  21. data/lib/timely/rails/extensions.rb +23 -11
  22. data/lib/timely/rails/period.rb +31 -0
  23. data/lib/timely/rails/season.rb +65 -75
  24. data/lib/timely/railtie.rb +7 -0
  25. data/lib/timely/temporal_patterns/finder.rb +152 -0
  26. data/lib/timely/temporal_patterns/frequency.rb +108 -0
  27. data/lib/timely/temporal_patterns/interval.rb +67 -0
  28. data/lib/timely/temporal_patterns/pattern.rb +160 -0
  29. data/lib/timely/time_since.rb +17 -0
  30. data/lib/timely/version.rb +3 -0
  31. data/spec/calendar_tag_spec.rb +29 -0
  32. data/spec/date_chooser_spec.rb +36 -27
  33. data/spec/date_group_spec.rb +9 -9
  34. data/spec/date_range_spec.rb +58 -20
  35. data/spec/date_spec.rb +20 -12
  36. data/spec/extensions_spec.rb +32 -0
  37. data/spec/rails/date_spec.rb +16 -0
  38. data/spec/rails/date_time_spec.rb +20 -0
  39. data/spec/rails/period_spec.rb +17 -0
  40. data/spec/schema.rb +5 -0
  41. data/spec/season_spec.rb +21 -24
  42. data/spec/spec_helper.rb +5 -20
  43. data/spec/string_spec.rb +4 -3
  44. data/spec/support/coverage.rb +26 -0
  45. data/spec/temporal_patterns_spec.rb +28 -0
  46. data/spec/time_since_spec.rb +24 -0
  47. data/spec/time_spec.rb +14 -14
  48. data/spec/trackable_date_set_spec.rb +14 -14
  49. data/spec/week_days_spec.rb +18 -18
  50. data/timely.gemspec +34 -98
  51. metadata +244 -21
  52. 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
@@ -0,0 +1,5 @@
1
+ class Date
2
+ def to_time_in_time_zone
3
+ (Time.zone || ActiveSupport::TimeZone["UTC"]).local(self.year, self.month, self.day)
4
+ end
5
+ 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
- belongs_to :season
5
+ weekdays_field :weekdays
7
6
 
8
- weekdays_field :weekdays
7
+ validates_presence_of :start_date, :end_date
8
+ validate :validate_date_range!
9
9
 
10
- validates_presence_of :start_date, :end_date
11
- validate :validate_date_range!
10
+ def includes_date?(date)
11
+ date >= start_date && date <= end_date && weekdays.applies_for_date?(date)
12
+ end
12
13
 
13
- def includes_date?(date)
14
- date >= start_date && date <= end_date && weekdays.applies_for_date?(date)
15
- end
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
- 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)}
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
- 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}"
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
- 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
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
- 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
- #
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
- 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]
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
- 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
- }
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
@@ -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
- 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
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
- def includes_date?(date)
21
- date_groups.any?{|dg| dg.includes_date?(date)}
22
- end
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
- 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
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
- 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
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
- def boundary_start
45
- date_groups.map(&:start_date).sort.first
46
- end
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
- def boundary_end
50
- date_groups.map(&:end_date).sort.last
51
- end
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
- def within_boundary?(date)
55
- boundary_start && boundary_end && boundary_start <= date && boundary_end >= date
56
- end
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
- def deep_clone
60
- cloned = self.clone
61
- date_groups.each do |dg|
62
- cloned.date_groups.build(dg.clone.attributes.except(:id))
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
- 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)
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
- def to_s
78
- name
79
- end
80
- alias_method :audit_name, :to_s
83
+ alias_method :audit_name, :to_s
81
84
 
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
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
-