timely 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
-