timely 0.4.2 → 0.9.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 (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