on_calendar 0.1.6 → 0.1.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a48fcb9c6e6836fd84408fcd7e650a355b0bd75b66e191ed9a3496ddb2ba63c5
4
- data.tar.gz: 66abbea8914bedb82b8d8dc2e93cd768b2cc8dcc9880289159d83d060d9edc9a
3
+ metadata.gz: f0f1bc76c2e4b7726e431fd1966f3eeb249ac1d1a37a922d8d7faf146609e569
4
+ data.tar.gz: 80a788fc4eff681edca8761d07753d5a88a2254639a97682ab1cea150ca68b90
5
5
  SHA512:
6
- metadata.gz: 36c626a4db17a5fba1af85e0065b419c8811bfe0415c416f8bef9f10ddf3f389f7ccc198090be4d060b5b90e38a6b522b1b1e2497b21fb0cc5ac600bff1c936d
7
- data.tar.gz: 10319d969d600e2e1d8a2188e4cbbe641276a684180a76cc7d1cce81e79e0947bc9e56a3c8e3473a018a003a82bbc4593b0d57d496719846c098a7495ad97e26
6
+ metadata.gz: 8016805896b6eb2f46aae718954a1c5e7506e0473f6b34221b6f654434fdf3df6fec342a0c4167507da38a72679df756621a897ef44896657227252866544a38
7
+ data.tar.gz: e1b1b69f687f36f354ca69102ed2d4f8aba996ac8272bd850a03216c906e03d0caaa157b9b74730a87e80f70132043610e29cb4fca98cb8d78486f3c6054522b
@@ -19,10 +19,15 @@ module OnCalendar
19
19
  end
20
20
 
21
21
  # Some subclasses need more context for RANGE
22
- def range
22
+ def range(clamp: nil)
23
23
  self.class::RANGE
24
24
  end
25
25
 
26
+ # Provide min / max of range for validation
27
+ def self.range_bounds
28
+ self.const_get(:RANGE).minmax
29
+ end
30
+
26
31
  # Match this condition
27
32
  # - If wild card return true
28
33
  # No step:
@@ -70,7 +75,7 @@ module OnCalendar
70
75
  return 1 if wildcard
71
76
 
72
77
  # Build array to find needle_index
73
- arr = range_args.nil? ? range.to_a : range(**range_args).to_a
78
+ arr = range(clamp: range_args).to_a
74
79
  needle_index = arr.index(current)
75
80
 
76
81
  return nil if needle_index.nil?
@@ -119,6 +124,13 @@ module OnCalendar
119
124
  end
120
125
  distance
121
126
  end
127
+
128
+ private
129
+
130
+ # Determine if change in DST between start/end of day
131
+ def dst_day?(clamp:)
132
+ clamp.beginning_of_day.dst? != clamp.end_of_day.dst?
133
+ end
122
134
  end
123
135
  end
124
136
  end
@@ -7,11 +7,11 @@ module OnCalendar
7
7
 
8
8
  # NOTE: by default we validate number in default range but this needs to be context aware
9
9
  # because not all months have the same number of days
10
- def range(year: nil, month: nil)
11
- if year.nil? || month.nil?
12
- RANGE
10
+ def range(clamp: nil)
11
+ if clamp.present?
12
+ (RANGE.min..Time.days_in_month(clamp.month, clamp.year))
13
13
  else
14
- (RANGE.min..Time.days_in_month(month, year))
14
+ RANGE
15
15
  end
16
16
  end
17
17
  end
@@ -4,12 +4,6 @@ module OnCalendar
4
4
  module Condition
5
5
  class DayOfWeek < Base
6
6
  RANGE = (0..6)
7
-
8
- # Utility function to pass our min,max range to the segment parser
9
- # this helps dealing with when the parser comes back with 6..0
10
- def self.range_bounds
11
- RANGE.minmax
12
- end
13
7
  end
14
8
  end
15
9
  end
@@ -4,6 +4,27 @@ module OnCalendar
4
4
  module Condition
5
5
  class Hour < Base
6
6
  RANGE = (0..23)
7
+
8
+ # NOTE: by default we validate number in default range but this needs to be context aware
9
+ # because not all days have all hours (ie: DST changes)
10
+ def range(clamp: nil)
11
+ # If we are dealing with DST
12
+ if clamp.present? and dst_day?(clamp: clamp)
13
+ hours = []
14
+ cursor = clamp.beginning_of_day
15
+ day = clamp.day
16
+ zone = clamp.zone
17
+ # Record each hour of the day until we change day || zone
18
+ loop do
19
+ hours << cursor.hour
20
+ cursor = cursor + 1.hour
21
+ break if cursor.day != day or clamp.zone != zone
22
+ end
23
+ hours
24
+ else
25
+ RANGE
26
+ end
27
+ end
7
28
  end
8
29
  end
9
30
  end
@@ -4,6 +4,29 @@ module OnCalendar
4
4
  module Condition
5
5
  class Minute < Base
6
6
  RANGE = (0..59)
7
+
8
+ # NOTE: by default we validate number in default range but this needs to be context aware
9
+ # because not all days have all minutes (ie: DST changes with 30 mins)
10
+ # NOTE: With Condition::Hour we check dst_day? I haven't been able to work out how to check for DST change within an hour without jumping boundaries of DST. Therefore we check the entire day. This might have a small performance impact but we generally aren't looping over each hour when checking conditions.
11
+ def range(clamp: nil)
12
+ # If we are dealing with DST
13
+ if clamp.present? and dst_day?(clamp: clamp)
14
+ mins = []
15
+ # NOTE: We can't use Time.beginning_of_hour because we may jump across DST boundary
16
+ cursor = clamp.end_of_hour
17
+ hour = clamp.hour
18
+ zone = clamp.zone
19
+ # Record each minute of the hour until we change hour || zone
20
+ loop do
21
+ mins << cursor.min
22
+ cursor = cursor - 1.minute
23
+ break if cursor.hour != hour or cursor.zone != zone
24
+ end
25
+ mins.reverse
26
+ else
27
+ RANGE
28
+ end
29
+ end
7
30
  end
8
31
  end
9
32
  end
@@ -8,14 +8,30 @@ module OnCalendar
8
8
  def initialize(base: nil, step: nil, wildcard: false)
9
9
  # Translate short year to long
10
10
  unless base.nil?
11
- if (0..69).cover?(base)
12
- base += 2000
13
- elsif (70..99).cover?(base)
14
- base += 1900
11
+ if base.is_a?(Range)
12
+ values = [
13
+ translate_short_year(base.begin),
14
+ translate_short_year(base.end)
15
+ ]
16
+ base = (values.min..values.max)
17
+ else
18
+ base = translate_short_year(base)
15
19
  end
16
20
  end
17
21
  super
18
22
  end
23
+
24
+ private
25
+
26
+ def translate_short_year(base)
27
+ if (0..69).cover?(base)
28
+ base += 2000
29
+ elsif (70..99).cover?(base)
30
+ base += 1900
31
+ else
32
+ base
33
+ end
34
+ end
19
35
  end
20
36
  end
21
37
  end
@@ -28,7 +28,7 @@ module OnCalendar
28
28
  @expression = expression
29
29
  end
30
30
 
31
- def next(count=1, clamp: timezone.now)
31
+ def next(count=1, clamp: timezone.now, debug: false)
32
32
  raise OnCalendar::Parser::Error, "Clamp must be instance of Time" unless clamp.is_a?(Time)
33
33
 
34
34
  # Translate to correct timezone and add 1.second to ensure
@@ -37,7 +37,7 @@ module OnCalendar
37
37
 
38
38
  results = []
39
39
  count.times do
40
- result = iterate(clamp: clamp)
40
+ result = iterate(clamp: clamp, debug: debug)
41
41
  break if result.nil?
42
42
 
43
43
  clamp = result + 1.second
@@ -55,8 +55,9 @@ module OnCalendar
55
55
 
56
56
  private
57
57
 
58
- def iterate(clamp:)
58
+ def iterate(clamp:, debug: false)
59
59
  iterations = 0
60
+ output = [["-", clamp.to_s, "", ""]] if debug
60
61
 
61
62
  while true
62
63
  # Fail safe
@@ -84,8 +85,7 @@ module OnCalendar
84
85
  days_of_month: {
85
86
  base_method: :day,
86
87
  changes: { hour: 0, min: 0, sec: 0 },
87
- increment_method: :days,
88
- range_args: ->(clamp) { { year: clamp.year, month: clamp.month } }
88
+ increment_method: :days
89
89
  },
90
90
  days_of_week: {
91
91
  base_method: :wday,
@@ -107,14 +107,18 @@ module OnCalendar
107
107
  # Do we miss all condition matches - thus increment
108
108
  next if matches_any_conditions?(field: field, base: clamp.send(values[:base_method]))
109
109
 
110
- # Do we need any range arguments? If so calculate
111
- range_args = values[:range_args].call(clamp) if values.key?(:range_args) || nil
112
110
  # Determine distances required to jump to next match
113
111
  distances = send(field).map do |condition|
114
- condition.distance_to_next(clamp.send(values[:base_method]), range_args: range_args)
112
+ condition.distance_to_next(clamp.send(values[:base_method]), range_args: clamp)
115
113
  end.sort!
116
114
  # Check for only nil - if so impossible to compute bail
117
- return nil if distances.compact.empty?
115
+ if distances.compact.empty?
116
+ if debug
117
+ output << [iterations, clamp.to_s, "impossible", ""]
118
+ debug_table(output)
119
+ end
120
+ return nil
121
+ end
118
122
 
119
123
  # Increment by field method
120
124
  method = values[:increment_method] || field
@@ -123,6 +127,13 @@ module OnCalendar
123
127
  clamp = clamp.change(**values[:changes]) if values.key?(:changes)
124
128
  # Force re-check everything by marking manipulation
125
129
  field_manipulation = true
130
+ # Debug
131
+ output << [
132
+ iterations,
133
+ clamp.to_s,
134
+ field.to_s,
135
+ distances.min
136
+ ] if debug
126
137
  break
127
138
  end
128
139
 
@@ -131,9 +142,20 @@ module OnCalendar
131
142
  field_manipulation ? next : break
132
143
  end
133
144
 
145
+ # Output debug table
146
+ debug_table(output) if debug
147
+
134
148
  clamp
135
149
  end
136
150
 
151
+ def debug_table(rows)
152
+ table = Terminal::Table.new do |t|
153
+ t.headings = ["Iteration", "Datetime", "Function", "Distance"]
154
+ t.rows = rows
155
+ end
156
+ puts table
157
+ end
158
+
137
159
  def parse(expression)
138
160
  raise OnCalendar::Parser::Error, "Expression must be a string" unless expression.is_a?(String)
139
161
  raise OnCalendar::Parser::Error, "Expression cannot be empty" if expression.empty?
@@ -236,7 +258,7 @@ module OnCalendar
236
258
  begin
237
259
  parsed = OnCalendar::Segment.parse(segments.shift, max: max, min: min)
238
260
  if parsed.nil?
239
- # We are a wild card
261
+ # We are a wildcard
240
262
  conditions[idx] = [OnCalendar::Condition.const_get(klass).new(wildcard: true)]
241
263
  else
242
264
  # Lets build conditions with parsed
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OnCalendar
4
- VERSION = "0.1.6"
4
+ VERSION = "0.1.7"
5
5
  end
data/lib/on_calendar.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/all"
4
+ require "terminal-table"
4
5
 
5
6
  module OnCalendar
6
7
  autoload :Version, "on_calendar/version"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: on_calendar
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Passmore
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '8.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: terminal-table
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: 4.0.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 4.0.0
26
40
  email:
27
41
  - contact@passbe.com
28
42
  executables: []