timely 0.4.2 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +6 -0
  3. data/.github/workflows/release.yml +59 -0
  4. data/.github/workflows/ruby.yml +19 -0
  5. data/.rubocop.yml +22 -2
  6. data/.ruby-version +1 -1
  7. data/CHANGELOG.md +23 -0
  8. data/Gemfile +2 -0
  9. data/README.md +13 -2
  10. data/Rakefile +6 -4
  11. data/gemfiles/rails60.gemfile +8 -0
  12. data/gemfiles/rails61.gemfile +8 -0
  13. data/lib/timely.rb +2 -0
  14. data/lib/timely/date.rb +6 -2
  15. data/lib/timely/date_chooser.rb +29 -30
  16. data/lib/timely/date_range.rb +17 -19
  17. data/lib/timely/date_time.rb +3 -1
  18. data/lib/timely/rails.rb +2 -0
  19. data/lib/timely/rails/calendar_tag.rb +3 -3
  20. data/lib/timely/rails/date.rb +3 -1
  21. data/lib/timely/rails/date_group.rb +42 -18
  22. data/lib/timely/rails/date_range_validity_module.rb +8 -6
  23. data/lib/timely/rails/date_time.rb +5 -3
  24. data/lib/timely/rails/extensions.rb +12 -37
  25. data/lib/timely/rails/period.rb +14 -12
  26. data/lib/timely/rails/season.rb +14 -33
  27. data/lib/timely/rails/time.rb +7 -3
  28. data/lib/timely/railtie.rb +2 -0
  29. data/lib/timely/range.rb +4 -2
  30. data/lib/timely/string.rb +4 -2
  31. data/lib/timely/time.rb +7 -3
  32. data/lib/timely/time_since.rb +5 -3
  33. data/lib/timely/trackable_date_set.rb +21 -21
  34. data/lib/timely/version.rb +3 -1
  35. data/lib/timely/week_days.rb +22 -14
  36. data/rails/init.rb +2 -0
  37. data/spec/calendar_tag_spec.rb +11 -10
  38. data/spec/date_chooser_spec.rb +67 -62
  39. data/spec/date_group_spec.rb +103 -5
  40. data/spec/date_range_spec.rb +29 -19
  41. data/spec/date_spec.rb +3 -1
  42. data/spec/extensions_spec.rb +5 -9
  43. data/spec/rails/date_spec.rb +5 -3
  44. data/spec/rails/date_time_spec.rb +9 -7
  45. data/spec/rails/period_spec.rb +2 -0
  46. data/spec/rails/time_spec.rb +6 -4
  47. data/spec/schema.rb +4 -3
  48. data/spec/season_spec.rb +23 -26
  49. data/spec/spec_helper.rb +16 -1
  50. data/spec/support/coverage_loader.rb +3 -1
  51. data/spec/temporal_patterns_spec.rb +5 -5
  52. data/spec/time_since_spec.rb +6 -4
  53. data/spec/time_spec.rb +6 -4
  54. data/spec/trackable_date_set_spec.rb +20 -18
  55. data/spec/week_days_spec.rb +41 -16
  56. data/timely.gemspec +23 -19
  57. metadata +54 -26
  58. data/.travis.yml +0 -22
  59. data/gemfiles/rails5.gemfile +0 -6
  60. data/gemfiles/rails6.gemfile +0 -6
  61. data/spec/string_spec.rb +0 -14
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'timely/rails/extensions'
2
4
  if defined?(ActiveRecord)
3
5
  ActiveRecord::Base.extend Timely::Extensions
@@ -1,10 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Timely
2
4
  # Uses Date.current to be more accurate for Rails applications
3
5
  def self.current_date
4
6
  ::Date.respond_to?(:current) ? ::Date.current : ::Date.today
5
7
  end
6
8
 
7
-
8
9
  module ActionViewHelpers
9
10
  module FormTagHelper
10
11
  def calendar_tag(name, value = Timely.current_date, *args)
@@ -24,11 +25,10 @@ module Timely
24
25
  options[:size] ||= 10
25
26
  options[:maxlength] ||= 10
26
27
 
27
- tag(:input, options.merge(:name => name, :type => 'text', :value => value)).html_safe
28
+ tag(:input, options.merge(name: name, type: 'text', value: value)).html_safe
28
29
  end
29
30
  end
30
31
 
31
-
32
32
  module DateHelper
33
33
  def calendar(object_name, method, options = {})
34
34
  value = options[:object] || Timely.current_date
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Date
2
4
  def to_time_in_time_zone
3
- (Time.zone || ActiveSupport::TimeZone["UTC"]).local(self.year, self.month, self.day)
5
+ (Time.zone || ActiveSupport::TimeZone['UTC']).local(year, month, day)
4
6
  end
5
7
  end
@@ -1,12 +1,35 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Timely
2
4
  class DateGroup < ActiveRecord::Base
3
- belongs_to :season, :class_name => 'Timely::Season', optional: true
5
+ belongs_to :season, class_name: 'Timely::Season', optional: true, inverse_of: :date_groups
4
6
 
5
7
  weekdays_field :weekdays
6
8
 
9
+ validates :weekdays_bit_array, presence: true
7
10
  validates_presence_of :start_date, :end_date
8
11
  validate :validate_date_range!
9
12
 
13
+ scope :covering_date, lambda { |date|
14
+ # Suitable for use with joins/merge!
15
+ where(arel_table[:start_date].lteq(date)).where(arel_table[:end_date].gteq(date))
16
+ }
17
+
18
+ scope :within_range, lambda { |date_range|
19
+ # IMPORTANT: Required for correctness in case of string param.
20
+ dates = Array(date_range)
21
+ where(arel_table[:start_date].lteq(dates.last)).where(arel_table[:end_date].gteq(dates.first))
22
+ }
23
+
24
+ scope :for_any_weekdays, lambda { |weekdays_int|
25
+ where((arel_table[:weekdays_bit_array] & weekdays_int.to_i).not_eq(0))
26
+ }
27
+
28
+ scope :applying_for_duration, lambda { |date_range|
29
+ weekdays_int = Timely::WeekDays.from_range(date_range).weekdays_int
30
+ within_range(date_range).for_any_weekdays(weekdays_int)
31
+ }
32
+
10
33
  def includes_date?(date)
11
34
  date >= start_date && date <= end_date && weekdays.applies_for_date?(date)
12
35
  end
@@ -17,7 +40,7 @@ module Timely
17
40
  elsif weekdays.all_days?
18
41
  true
19
42
  else
20
- date_range.intersecting_dates(start_date..end_date).any?{|d| weekdays.applies_for_date?(d)}
43
+ date_range.intersecting_dates(start_date..end_date).any? { |d| weekdays.applies_for_date?(d) }
21
44
  end
22
45
  end
23
46
 
@@ -44,30 +67,31 @@ module Timely
44
67
  date_groups = []
45
68
  Array.wrap(patterns).each do |pattern|
46
69
  if pattern.frequency.unit == :weeks
47
- weekdays = pattern.intervals.map { |i| i.first_datetime.wday }.inject({}) do |hash, wday|
70
+ weekdays = pattern.intervals.map { |i| i.first_datetime.wday }.each_with_object({}) do |wday, hash|
48
71
  hash[wday] = 1
49
- hash
50
72
  end
51
73
  date_groups << DateGroup.new(
52
- :start_date => pattern.first_datetime.to_date,
53
- :end_date => pattern.last_datetime.to_date,
54
- :weekdays => weekdays)
74
+ start_date: pattern.first_datetime.to_date,
75
+ end_date: pattern.last_datetime.to_date,
76
+ weekdays: weekdays
77
+ )
55
78
  elsif pattern.frequency.unit == :days && pattern.frequency.duration == 1.day
56
79
  date_groups << DateGroup.new(
57
- :start_date => pattern.first_datetime.to_date,
58
- :end_date => pattern.last_datetime.to_date,
59
- :weekdays => 127)
80
+ start_date: pattern.first_datetime.to_date,
81
+ end_date: pattern.last_datetime.to_date,
82
+ weekdays: 127
83
+ )
60
84
  else
61
85
  pattern.datetimes.each do |datetimes|
62
86
  datetimes.group_by(&:week).values.each do |dates|
63
- weekdays = dates.map(&:wday).inject({}) do |hash, wday|
87
+ weekdays = dates.map(&:wday).each_with_object({}) do |wday, hash|
64
88
  hash[wday] = 1
65
- hash
66
89
  end
67
90
  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)
91
+ start_date: dates.min.to_date.beginning_of_week,
92
+ end_date: dates.max.to_date.end_of_week,
93
+ weekdays: weekdays
94
+ )
71
95
  end
72
96
  end
73
97
  end
@@ -78,9 +102,9 @@ module Timely
78
102
  private
79
103
 
80
104
  def validate_date_range!
81
- if start_date && end_date && (start_date > end_date)
82
- raise ArgumentError, "Incorrect date range #{start_date} is before #{end_date}"
83
- end
105
+ return unless start_date && end_date && start_date > end_date
106
+
107
+ raise ArgumentError, "Incorrect date range #{start_date} is before #{end_date}"
84
108
  end
85
109
  end
86
110
  end
@@ -1,25 +1,27 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Timely
2
4
  module DateRangeValidityModule
3
5
  def self.included(base)
4
6
  base.class_eval do
5
- validates :from, :to, :presence => true
7
+ validates :from, :to, presence: true
6
8
  end
7
9
  end
8
10
 
9
11
  def validity_range
10
- (from .. to)
12
+ (from..to)
11
13
  end
12
14
 
13
15
  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
16
+ return unless from.present? && to.present? && from > to
17
+
18
+ errors.add(:base, 'Invalid Date Range. From date should be less than or equal to To date')
17
19
  end
18
20
 
19
21
  def validity_range_to_s
20
22
  "#{from.to_s(:short)} ~ #{to.to_s(:short)}"
21
23
  end
22
-
24
+
23
25
  def valid_on?(date)
24
26
  validity_range.include?(date)
25
27
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RailsCoreExtensions
2
4
  module DateTime
3
5
  def advance_considering_calendar(units, num_units)
@@ -5,11 +7,11 @@ module RailsCoreExtensions
5
7
  when :seconds, :minutes, :hours, :days, :weeks, :months, :years
6
8
  advance(units => num_units)
7
9
  when :calendar_days
8
- advance(:days => num_units - 1).end_of_day
10
+ advance(days: num_units - 1).end_of_day
9
11
  when :calendar_months
10
- advance(:months => num_units - 1).end_of_month
12
+ advance(months: num_units - 1).end_of_month
11
13
  when :calendar_years
12
- advance(:years => num_units - 1).end_of_year
14
+ advance(years: num_units - 1).end_of_year
13
15
  end
14
16
  end
15
17
  end
@@ -1,55 +1,30 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Timely
2
4
  module Extensions
3
5
  # Add a WeekDays attribute
4
6
  #
5
7
  # By default it will use attribute_bit_array as db field, but this can
6
8
  # be overridden by specifying :db_field => 'somthing_else'
7
- def weekdays_field(attribute, options={})
9
+ def weekdays_field(attribute, options = {})
8
10
  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
- )
11
+ composed_of(attribute,
12
+ class_name: '::Timely::WeekDays',
13
+ mapping: [[db_field, 'weekdays_int']],
14
+ converter: proc { |field| ::Timely::WeekDays.new(field) })
14
15
  end
15
16
 
16
17
  def acts_as_seasonal
17
- belongs_to :season, :class_name => 'Timely::Season', optional: true
18
+ belongs_to :season, class_name: 'Timely::Season', optional: true
18
19
  accepts_nested_attributes_for :season
19
20
  validates_associated :season
20
21
 
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)
25
- }
26
-
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
45
-
46
22
  before_save do |object|
47
- if object.season
48
- object.boundary_start = object.season.boundary_start
49
- object.boundary_end = object.season.boundary_end
50
- end
23
+ next unless object.season
24
+
25
+ object.boundary_start = object.season.boundary_start
26
+ object.boundary_end = object.season.boundary_end
51
27
  end
52
28
  end
53
29
  end
54
30
  end
55
-
@@ -1,19 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Timely
2
4
  class Period
3
5
  attr_reader :number, :units
4
6
 
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
- ]
7
+ UNITS = %i[
8
+ seconds
9
+ minutes
10
+ hours
11
+ days
12
+ weeks
13
+ months
14
+ years
15
+ calendar_days
16
+ calendar_months
17
+ calendar_years
18
+ ].freeze
17
19
 
18
20
  def initialize(number, units)
19
21
  @number = number
@@ -1,34 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Timely
2
4
  class Season < ActiveRecord::Base
3
-
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
9
-
5
+ has_many :date_groups, -> { order(:start_date) }, dependent: :destroy, class_name: 'Timely::DateGroup', inverse_of: :season
10
6
 
11
7
  accepts_nested_attributes_for :date_groups,
12
- :reject_if => proc {|attributes| attributes['start_date'].blank?},
13
- :allow_destroy => true
8
+ reject_if: proc { |attributes| attributes['start_date'].blank? },
9
+ allow_destroy: true
14
10
 
15
11
  validate :validate_dates_specified
16
12
 
17
13
  def validate_dates_specified
18
- errors.add(:base, "No dates specified") if date_groups.blank?
14
+ errors.add(:base, 'No dates specified') if date_groups.blank?
19
15
  errors.empty?
20
16
  end
21
17
 
22
18
  def includes_date?(date)
23
- date_groups.any?{|dg| dg.includes_date?(date)}
24
- end
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
19
+ date_groups.any? { |dg| dg.includes_date?(date) }
32
20
  end
33
21
 
34
22
  def dates
@@ -46,11 +34,11 @@ module Timely
46
34
  end
47
35
 
48
36
  def boundary_start
49
- date_groups.map(&:start_date).sort.first
37
+ date_groups.map(&:start_date).min
50
38
  end
51
39
 
52
40
  def boundary_end
53
- date_groups.map(&:end_date).sort.last
41
+ date_groups.map(&:end_date).max
54
42
  end
55
43
 
56
44
  def within_boundary?(date)
@@ -60,30 +48,23 @@ module Timely
60
48
  def deep_clone
61
49
  # Use clone until it is removed in AR 3.1, then dup is the same
62
50
  method = ActiveRecord::Base.instance_methods(false).include?(:clone) ? :clone : :dup
63
- cloned = self.send(method)
51
+ cloned = send(method)
64
52
  date_groups.each do |dg|
65
53
  cloned.date_groups.build(dg.send(method).attributes)
66
54
  end
67
55
  cloned
68
56
  end
69
57
 
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
78
-
79
58
  def to_s
80
59
  name.presence || Timely::DateRange.to_s(boundary_start, boundary_end)
81
60
  end
82
61
 
83
- alias_method :audit_name, :to_s
62
+ alias audit_name to_s
84
63
 
85
64
  def string_of_date_groups
86
- date_groups.map{|dg| "#{dg.start_date.to_s(:short)} - #{dg.end_date.to_s(:short)}"}.to_sentence
65
+ date_groups.map do |dg|
66
+ "#{dg.start_date.to_s(:short)} - #{dg.end_date.to_s(:short)}"
67
+ end.to_sentence
87
68
  end
88
69
  end
89
70
  end
@@ -1,18 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Timely
2
4
  module Rails
3
5
  module Time
4
6
  def on_date(year, month = nil, day = nil)
5
7
  if year.is_a?(Date)
6
8
  date = year
7
- year, month, day = date.year, date.month, date.day
9
+ year = date.year
10
+ month = date.month
11
+ day = date.day
8
12
  end
9
13
 
10
- raise ArgumentError, "Year, month, and day needed" unless [year, month, day].all?
14
+ raise ArgumentError, 'Year, month, and day needed' unless [year, month, day].all?
11
15
 
12
16
  ::Time.zone.local(year, month, day, hour, min, sec)
13
17
  end
14
18
 
15
- alias_method :on, :on_date
19
+ alias on on_date
16
20
  end
17
21
  end
18
22
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Railtie < Rails::Railtie
2
4
  initializer 'timely.initialize' do
3
5
  ActiveSupport.on_load(:action_view) do
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Timely
2
4
  module Range
3
5
  def to_date_range
4
- DateRange.new(self.first, self.last)
6
+ DateRange.new(first, last)
5
7
  end
6
8
 
7
9
  def days_from(date = Date.today)
8
- (date + self.first)..(date + self.last)
10
+ (date + first)..(date + last)
9
11
  end
10
12
  end
11
13
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Timely
2
4
  module String
3
5
  # fmt e.g. '%d/%m/%Y'
@@ -12,12 +14,12 @@ module Timely
12
14
  else
13
15
  ::Date.new(*::Date._parse(self, false).values_at(:year, :mon, :mday))
14
16
  end
15
- rescue NoMethodError, ArgumentError => e
17
+ rescue NoMethodError, ArgumentError
16
18
  raise DateFormatException, "Date #{self} is invalid or not formatted correctly."
17
19
  end
18
20
  end
19
21
 
20
- class DateFormatException < Exception; end
22
+ class DateFormatException < RuntimeError; end
21
23
  end
22
24
 
23
25
  class String