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