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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/release.yml +59 -0
- data/.github/workflows/ruby.yml +19 -0
- data/.rubocop.yml +22 -2
- data/.ruby-version +1 -1
- data/CHANGELOG.md +23 -0
- data/Gemfile +2 -0
- data/README.md +13 -2
- data/Rakefile +6 -4
- data/gemfiles/rails60.gemfile +8 -0
- data/gemfiles/rails61.gemfile +8 -0
- data/lib/timely.rb +2 -0
- data/lib/timely/date.rb +6 -2
- data/lib/timely/date_chooser.rb +29 -30
- data/lib/timely/date_range.rb +17 -19
- data/lib/timely/date_time.rb +3 -1
- data/lib/timely/rails.rb +2 -0
- data/lib/timely/rails/calendar_tag.rb +3 -3
- data/lib/timely/rails/date.rb +3 -1
- data/lib/timely/rails/date_group.rb +42 -18
- data/lib/timely/rails/date_range_validity_module.rb +8 -6
- data/lib/timely/rails/date_time.rb +5 -3
- data/lib/timely/rails/extensions.rb +12 -37
- data/lib/timely/rails/period.rb +14 -12
- data/lib/timely/rails/season.rb +14 -33
- data/lib/timely/rails/time.rb +7 -3
- data/lib/timely/railtie.rb +2 -0
- data/lib/timely/range.rb +4 -2
- data/lib/timely/string.rb +4 -2
- data/lib/timely/time.rb +7 -3
- data/lib/timely/time_since.rb +5 -3
- data/lib/timely/trackable_date_set.rb +21 -21
- data/lib/timely/version.rb +3 -1
- data/lib/timely/week_days.rb +22 -14
- data/rails/init.rb +2 -0
- data/spec/calendar_tag_spec.rb +11 -10
- data/spec/date_chooser_spec.rb +67 -62
- data/spec/date_group_spec.rb +103 -5
- data/spec/date_range_spec.rb +29 -19
- data/spec/date_spec.rb +3 -1
- data/spec/extensions_spec.rb +5 -9
- data/spec/rails/date_spec.rb +5 -3
- data/spec/rails/date_time_spec.rb +9 -7
- data/spec/rails/period_spec.rb +2 -0
- data/spec/rails/time_spec.rb +6 -4
- data/spec/schema.rb +4 -3
- data/spec/season_spec.rb +23 -26
- data/spec/spec_helper.rb +16 -1
- data/spec/support/coverage_loader.rb +3 -1
- data/spec/temporal_patterns_spec.rb +5 -5
- data/spec/time_since_spec.rb +6 -4
- data/spec/time_spec.rb +6 -4
- data/spec/trackable_date_set_spec.rb +20 -18
- data/spec/week_days_spec.rb +41 -16
- data/timely.gemspec +23 -19
- metadata +54 -26
- data/.travis.yml +0 -22
- data/gemfiles/rails5.gemfile +0 -6
- data/gemfiles/rails6.gemfile +0 -6
- data/spec/string_spec.rb +0 -14
data/lib/timely/rails.rb
CHANGED
@@ -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(:
|
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
|
data/lib/timely/rails/date.rb
CHANGED
@@ -1,12 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Timely
|
2
4
|
class DateGroup < ActiveRecord::Base
|
3
|
-
belongs_to :season, :
|
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 }.
|
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
|
-
:
|
53
|
-
:
|
54
|
-
:
|
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
|
-
:
|
58
|
-
:
|
59
|
-
:
|
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).
|
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
|
-
:
|
69
|
-
:
|
70
|
-
:
|
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
|
-
|
82
|
-
|
83
|
-
|
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, :
|
7
|
+
validates :from, :to, presence: true
|
6
8
|
end
|
7
9
|
end
|
8
10
|
|
9
11
|
def validity_range
|
10
|
-
(from
|
12
|
+
(from..to)
|
11
13
|
end
|
12
14
|
|
13
15
|
def correctness_of_date_range
|
14
|
-
|
15
|
-
|
16
|
-
|
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(:
|
10
|
+
advance(days: num_units - 1).end_of_day
|
9
11
|
when :calendar_months
|
10
|
-
advance(:
|
12
|
+
advance(months: num_units - 1).end_of_month
|
11
13
|
when :calendar_years
|
12
|
-
advance(:
|
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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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, :
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
data/lib/timely/rails/period.rb
CHANGED
@@ -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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
data/lib/timely/rails/season.rb
CHANGED
@@ -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
|
-
|
13
|
-
|
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,
|
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).
|
37
|
+
date_groups.map(&:start_date).min
|
50
38
|
end
|
51
39
|
|
52
40
|
def boundary_end
|
53
|
-
date_groups.map(&:end_date).
|
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 =
|
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
|
-
|
62
|
+
alias audit_name to_s
|
84
63
|
|
85
64
|
def string_of_date_groups
|
86
|
-
date_groups.map
|
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
|
data/lib/timely/rails/time.rb
CHANGED
@@ -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
|
9
|
+
year = date.year
|
10
|
+
month = date.month
|
11
|
+
day = date.day
|
8
12
|
end
|
9
13
|
|
10
|
-
raise ArgumentError,
|
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
|
-
|
19
|
+
alias on on_date
|
16
20
|
end
|
17
21
|
end
|
18
22
|
end
|
data/lib/timely/railtie.rb
CHANGED
data/lib/timely/range.rb
CHANGED
@@ -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(
|
6
|
+
DateRange.new(first, last)
|
5
7
|
end
|
6
8
|
|
7
9
|
def days_from(date = Date.today)
|
8
|
-
(date +
|
10
|
+
(date + first)..(date + last)
|
9
11
|
end
|
10
12
|
end
|
11
13
|
end
|
data/lib/timely/string.rb
CHANGED
@@ -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
|
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 <
|
22
|
+
class DateFormatException < RuntimeError; end
|
21
23
|
end
|
22
24
|
|
23
25
|
class String
|