async-cron 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bc78136142298dcbefb1b61a9673fc9ee19cdedcc1f7c49bf734efaf652370d4
4
+ data.tar.gz: 7ec5b0e30337a7299b3c6b1550d86c1cfef1fae9c811cecb21f17328ada74ccc
5
+ SHA512:
6
+ metadata.gz: 7c29dd28a35937ba33d423d60c541c1e80f54dd12415228853172697da5c58bbebcb8b85ffa959ae0bd28f5aff991900b9bb5628531430d7cc463aced867a950
7
+ data.tar.gz: 99ddc742c526e655d7afc5ef73199abb3ff32f137ca4277d5a389ec7b8fb724b91be0bc27204256600d86bc47868327070ff4ac91b1b61f826d1147353ca71b1
checksums.yaml.gz.sig ADDED
Binary file
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require_relative '../service/scheduler'
7
+ require_relative '../scheduler'
8
+
9
+ module Async
10
+ module Cron
11
+ module Environment
12
+ # Provides an environment for hosting a web application that uses a Falcon server.
13
+ module Scheduler
14
+ def name
15
+ super || "scheduler"
16
+ end
17
+
18
+ # The service class to use for the proxy.
19
+ # @returns [Class]
20
+ def service_class
21
+ Service::Scheduler
22
+ end
23
+
24
+ def scheduler
25
+ Cron::Scheduler.load(self.root)
26
+ end
27
+
28
+ def container_options
29
+ {count: 1}
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,252 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require_relative 'time'
7
+
8
+ module Async
9
+ module Cron
10
+ class Period
11
+ # Parse a string into a period.
12
+ # @parameter string [String | Nil] The string to parse.
13
+ def self.parse(string)
14
+ value, divisor = string&.split('/', 2)
15
+
16
+ if divisor
17
+ divisor = Integer(divisor)
18
+ else
19
+ divisor = 1
20
+ end
21
+
22
+ if value == '*'
23
+ value = nil
24
+ elsif value
25
+ value = value.split(',').map do |part|
26
+ if part =~ /\A(\d+)-(\d+)\z/
27
+ Range.new(Integer($1), Integer($2))
28
+ elsif part =~ /\A(-?\d+)\.\.(-?\d+)\z/
29
+ Range.new(Integer($1), Integer($2))
30
+ else
31
+ Integer(part)
32
+ end
33
+ end
34
+ end
35
+
36
+ self.new(value, divisor)
37
+ end
38
+
39
+ RANGE = nil
40
+
41
+ def initialize(value = nil, divisor = 1, range: self.class::RANGE)
42
+ @value = value
43
+ @divisor = divisor
44
+ @range = range
45
+
46
+ if @values = divide(expand(@value))
47
+ # This is an optimization to avoid recalculating the successors every time:
48
+ @successors = successors(@values)
49
+ end
50
+ end
51
+
52
+ def to_a
53
+ @values
54
+ end
55
+
56
+ # Increment the specific time unit to the next possible value.
57
+ # @parameter time [Time] The time to increment, must be normalized.
58
+ def increment(time)
59
+ time.seconds = @successors[time.seconds]
60
+ end
61
+
62
+ def step(time)
63
+ time = Time.from(time).normalize!
64
+
65
+ increment(time)
66
+
67
+ return time
68
+ end
69
+
70
+ # Reset the specific time unit to the first value.
71
+ def reset(time)
72
+ time.seconds = @values.first
73
+ end
74
+
75
+ def include?(time)
76
+ if @values
77
+ return @values.include?(value_from(time.normalize!))
78
+ else
79
+ return true
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def value_from(time)
86
+ time.seconds
87
+ end
88
+
89
+ def expand(values)
90
+ case values
91
+ when Array
92
+ flatten(values)
93
+ when Range
94
+ values.to_a
95
+ when Integer
96
+ [values]
97
+ when nil
98
+ @range&.to_a
99
+ else
100
+ raise ArgumentError, "Invalid value: #{values.inspect}"
101
+ end
102
+ end
103
+
104
+ def flatten(values)
105
+ values = values.flat_map do |value|
106
+ case value
107
+ when Array
108
+ flatten(value)
109
+ when Range
110
+ value.to_a
111
+ else
112
+ value
113
+ end
114
+ end
115
+
116
+ values.sort!
117
+ values.uniq!
118
+
119
+ return values
120
+ end
121
+
122
+ def divide(values)
123
+ return values if @divisor == 1 or values.size <= 1
124
+ raise ArgumentError, "Invalid divisor: #{@divisor}" unless @divisor > 1
125
+
126
+ offset = values.first
127
+ filtered = {}
128
+
129
+ values.each do |value|
130
+ key = ((value - offset) / @divisor).floor
131
+ filtered[key] ||= value
132
+ end
133
+
134
+ return filtered.values
135
+ end
136
+
137
+ def successors(values)
138
+ mapped = @range.to_a
139
+
140
+ current = 0
141
+
142
+ values.each do |value|
143
+ while current < value
144
+ mapped[current] = value
145
+ current += 1
146
+ end
147
+ end
148
+
149
+ while current < mapped.size
150
+ mapped[current] = values.first + mapped.size
151
+ current += 1
152
+ end
153
+
154
+ return mapped
155
+ end
156
+ end
157
+
158
+ class Seconds < Period
159
+ RANGE = 0..59
160
+ end
161
+
162
+ class Minutes < Period
163
+ RANGE = 0..59
164
+
165
+ def increment(time)
166
+ time.minutes = @successors[time.minutes]
167
+ end
168
+
169
+ def reset(time)
170
+ time.minutes = @values.first
171
+ end
172
+
173
+ def value_from(time)
174
+ time.minutes
175
+ end
176
+ end
177
+
178
+ class Hours < Period
179
+ RANGE = 0..23
180
+
181
+ def increment(time)
182
+ time.hours = @successors[time.hours]
183
+ end
184
+
185
+ def reset(time)
186
+ time.hours = @values.first
187
+ end
188
+
189
+ def value_from(time)
190
+ time.hours
191
+ end
192
+ end
193
+
194
+ class Weekday < Period
195
+ RANGE = 0..6
196
+
197
+ def increment(time)
198
+ time.weekday = @successors[time.weekday]
199
+ end
200
+
201
+ def reset(time)
202
+ time.weekday = @values.first
203
+ end
204
+
205
+ def value_from(time)
206
+ time.weekday
207
+ end
208
+ end
209
+
210
+ class Monthday < Period
211
+ def increment(time)
212
+ current_time = time.dup
213
+ monthday = @successors[time.days] || @values.first
214
+
215
+ time.monthday = monthday
216
+
217
+ while time <= current_time
218
+ time.months += 1
219
+ time.monthday = monthday
220
+ end
221
+ end
222
+
223
+ def reset(time)
224
+ if @values&.any?
225
+ time.monthday = @values.first
226
+ else
227
+ time.monthday = 0
228
+ end
229
+ end
230
+
231
+ def value_from(time)
232
+ time.days
233
+ end
234
+ end
235
+
236
+ class Month < Period
237
+ RANGE = 0..11
238
+
239
+ def increment(time)
240
+ time.months = @successors[time.months]
241
+ end
242
+
243
+ def reset(time)
244
+ time.months = @values.first
245
+ end
246
+
247
+ def value_from(time)
248
+ time.months
249
+ end
250
+ end
251
+ end
252
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require_relative 'generic'
7
+ require_relative '../period'
8
+
9
+ module Async
10
+ module Cron
11
+ module Schedule
12
+ # A schedule that runs once per day, at midnight.
13
+ class Daily < Generic
14
+ def increment(time = nil)
15
+ time = Time.from(time) || Time.now
16
+
17
+ time.seconds = 0
18
+ time.minutes = 0
19
+ time.hours = 0
20
+ time.days += 1
21
+
22
+ return time.normalize!
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ module Async
7
+ module Cron
8
+ module Schedule
9
+ class Flags
10
+ def self.parse(string)
11
+ options = {}
12
+
13
+ string&.each_char do |character|
14
+ case character
15
+ when 'D'
16
+ options[:drop] = true
17
+ when 'd'
18
+ options[:drop] = false
19
+ else
20
+ raise ArgumentError, "Invalid flag: #{character}"
21
+ end
22
+ end
23
+
24
+ self.new(**options)
25
+ end
26
+
27
+ def initialize(drop: true)
28
+ @drop = drop
29
+ end
30
+
31
+ def drop?
32
+ @drop
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require_relative 'flags'
7
+ require_relative '../time'
8
+
9
+ require 'console'
10
+
11
+ module Async
12
+ module Cron
13
+ module Schedule
14
+ class Generic
15
+ def initialize(flags = Flags.new)
16
+ @flags = flags
17
+ end
18
+
19
+ attr :flags
20
+
21
+ # Compute the next execution time after the given time.
22
+ def increment(time = nil)
23
+ Time.now
24
+ end
25
+
26
+ def run_once(time = Time.now, &block)
27
+ # The number of times the schedule has been dropped because the scheduled time was in the past:
28
+ dropped = 0
29
+
30
+ while true
31
+ # Compute the next scheduled time:
32
+ scheduled_time = increment(time)
33
+
34
+ # If the time is not advancing and we are dropping, then raise an error:
35
+ if scheduled_time <= time and dropped > 0
36
+ raise RuntimeError, "Scheduled time is not advancing: #{scheduled_time} <= #{time}"
37
+ end
38
+
39
+ # Compute the delta until the next scheduled time:
40
+ delta = scheduled_time.delta
41
+
42
+ Console.debug(self, "Next scheduled time.", delta: delta, scheduled_time: scheduled_time, block: block)
43
+
44
+ # If the time is in the past, and we are dropping, then skip this schedule and try again:
45
+ if delta < 0 and @flags.drop?
46
+ # This would normally occur if the user code took too long to execute, causing the next scheduled time to be in the past.
47
+ Console.warn(self, "Skipping schedule because it is in the past.", delta: delta, scheduled_time: scheduled_time, block: block, dropped: dropped)
48
+ dropped += 1
49
+ next
50
+ end
51
+
52
+ break
53
+ end
54
+
55
+ Console.debug(self, "Waiting for next scheduled time.", delta: delta, scheduled_time: scheduled_time, block: block)
56
+ sleep(delta) if delta > 0
57
+
58
+ begin
59
+ invoke(scheduled_time, &block)
60
+ rescue => error
61
+ Console::Event::Failure.for(error).emit(self, delta: delta, scheduled_time: scheduled_time, block: block)
62
+ end
63
+
64
+ return time
65
+ end
66
+
67
+ def run(time = Time.now, &block)
68
+ while true
69
+ time = run_once(time, &block)
70
+ end
71
+ end
72
+
73
+ def invoke(time, &block)
74
+ yield(time)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require_relative 'generic'
7
+ require_relative '../period'
8
+
9
+ module Async
10
+ module Cron
11
+ module Schedule
12
+ # A schedule that runs once per hour, on the hour.
13
+ class Hourly < Generic
14
+ def increment(time = nil)
15
+ time = Time.from(time) || Time.now
16
+
17
+ time.seconds = 0
18
+ time.minutes = 0
19
+ time.hours += 1
20
+
21
+ return time.normalize!
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require_relative 'generic'
7
+ require_relative '../period'
8
+
9
+ module Async
10
+ module Cron
11
+ module Schedule
12
+ # A schedule that runs once per day, at midnight.
13
+ class Monthly < Generic
14
+ def increment(time = nil)
15
+ time = Time.from(time) || Time.now
16
+
17
+ time.seconds = 0
18
+ time.minutes = 0
19
+ time.hours = 0
20
+ time.days = 0
21
+ time.months += 1
22
+
23
+ return time.normalize!
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require_relative 'generic'
7
+ require_relative '../period'
8
+
9
+ module Async
10
+ module Cron
11
+ module Schedule
12
+ class Periodic < Generic
13
+ def self.parse(string)
14
+ parts = string.split(/\s+/)
15
+
16
+ if parts.last =~ /[a-zA-Z]/
17
+ flags = Flags.parse(parts.pop)
18
+ else
19
+ flags = Flags.new
20
+ end
21
+
22
+ seconds = Seconds.parse(parts[0])
23
+ minutes = Minutes.parse(parts[1])
24
+ hours = Hours.parse(parts[2])
25
+ weekday = Weekday.parse(parts[3])
26
+ monthday = Monthday.parse(parts[4])
27
+ month = Month.parse(parts[5])
28
+
29
+ return self.new(seconds, minutes, hours, weekday, monthday, month, flags)
30
+ end
31
+
32
+ # Create a new schedule with the given parameters.
33
+ def initialize(seconds, minutes, hours, weekday, monthday, month, flags = Flags.new)
34
+ super(flags)
35
+
36
+ @seconds = seconds
37
+ @minutes = minutes
38
+ @hours = hours
39
+ @weekday = weekday
40
+ @monthday = monthday
41
+ @month = month
42
+ end
43
+
44
+ # Compute the next execution time after the given time.
45
+ def increment(time = nil)
46
+ time = Time.from(time) || Time.now
47
+
48
+ units = [@seconds, @minutes, @hours, @weekday, @monthday, @month]
49
+ index = 1
50
+
51
+ # Step the smallest unit first:
52
+ units[0].increment(time)
53
+
54
+ # Now try to fit the rest of the schedule:
55
+ while index < units.length
56
+ unit = units[index]
57
+
58
+ # puts "Checking #{unit} at #{index}... -> #{time}"
59
+
60
+ # If the unit is not already at the desired time, increment it:
61
+ unless unit.include?(time)
62
+ # puts "Incrementing #{unit} at #{index}..."
63
+ unit.increment(time)
64
+ # puts "-> #{time}"
65
+
66
+ # Reset all smaller units:
67
+ units[0...index].each do |unit|
68
+ # puts "Resetting #{unit}..."
69
+ unit.reset(time)
70
+ end
71
+ end
72
+
73
+ index += 1
74
+ end
75
+
76
+ return time.normalize!
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require_relative 'generic'
7
+ require_relative '../period'
8
+
9
+ module Async
10
+ module Cron
11
+ module Schedule
12
+ # A schedule that runs once per day, at midnight.
13
+ class Weekly < Generic
14
+ def increment(time = nil)
15
+ time = Time.from(time) || Time.now
16
+
17
+ time.seconds = 0
18
+ time.minutes = 0
19
+ time.hours = 0
20
+ # 0-6, Sunday is zero
21
+ time.weekday = 7
22
+
23
+ return time.normalize!
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ # These are mostly just for convenience:
7
+ require_relative 'schedule/hourly'
8
+ require_relative 'schedule/daily'
9
+ require_relative 'schedule/weekly'
10
+ require_relative 'schedule/monthly'
11
+
12
+ # This one gives you a lot of flexibility at the expense of being sgightly more verbose:
13
+ require_relative 'schedule/periodic'
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require 'async'
7
+ require 'async/barrier'
8
+
9
+ module Async
10
+ module Cron
11
+ class Scheduler
12
+ class Loader
13
+ def initialize(scheduler)
14
+ @scheduler = scheduler
15
+ end
16
+
17
+ def add(schedule, &block)
18
+ @scheduler.add(schedule, &block)
19
+ end
20
+
21
+ def hourly(&block)
22
+ add(Schedule::Hourly.new, &block)
23
+ end
24
+
25
+ def daily(&block)
26
+ add(Schedule::Daily.new, &block)
27
+ end
28
+
29
+ def weekly(&block)
30
+ add(Schedule::Weekly.new, &block)
31
+ end
32
+
33
+ def monthly(&block)
34
+ add(Schedule::Monthly.new, &block)
35
+ end
36
+
37
+ def periodic(specification, &block)
38
+ add(Schedule::Periodic.parse(specification), &block)
39
+ end
40
+ end
41
+
42
+ PATH = "config/async/cron/scheduler.rb"
43
+
44
+ def self.load(root = Dir.pwd)
45
+ path = ::File.join(root, PATH)
46
+
47
+ if ::File.exist?(path)
48
+ return self.load_file(path)
49
+ end
50
+ end
51
+
52
+ def self.load_file(path)
53
+ scheduler = self.new
54
+ loader = Loader.new(scheduler)
55
+
56
+ loader.instance_eval(::File.read(path), path)
57
+
58
+ return scheduler
59
+ end
60
+
61
+ def initialize
62
+ @schedules = Hash.new.compare_by_identity
63
+ end
64
+
65
+ attr :schedules
66
+
67
+ def add(schedule, &block)
68
+ @schedules[schedule] = block
69
+ end
70
+
71
+ def run
72
+ Sync do
73
+ barrier = Async::Barrier.new
74
+
75
+ @schedules.each do |schedule, block|
76
+ barrier.async do
77
+ schedule.run(&block)
78
+ end
79
+ end
80
+
81
+ barrier.wait
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2019-2024, by Samuel Williams.
5
+ # Copyright, 2020, by Daniel Evans.
6
+
7
+ require 'async/service/generic'
8
+
9
+ module Async
10
+ module Cron
11
+ module Service
12
+ class Scheduler < Async::Service::Generic
13
+ # Setup the container with the application instance.
14
+ # @parameter container [Async::Container::Generic]
15
+ def setup(container)
16
+ container_options = @evaluator.container_options
17
+
18
+ container.run(name: self.name, **container_options) do |instance|
19
+ evaluator = @environment.evaluator
20
+
21
+ Async do |task|
22
+ scheduler = evaluator.scheduler
23
+
24
+ task = Async do
25
+ scheduler.run
26
+ end
27
+
28
+ instance.ready!
29
+
30
+ task.wait
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require 'date'
7
+
8
+ module Async
9
+ module Cron
10
+ # A base-zero that supports assigning arbitrary values (both positive and negative) to all units. This simplifies computing relative times and dates by incrementing the relevant units and normalizing the result.
11
+ #
12
+ # **Note that base-zero means that the month and day start from zero, not one, as is the case with the standard Ruby Time and Date classes.** That is because the fields also accept negative values, which would be ambiguous if the month and day started from one.
13
+ class Time
14
+ include Comparable
15
+
16
+ def self.from(time)
17
+ case time
18
+ when ::Time
19
+ return self.new(time.year, time.month-1, time.day-1, time.hour, time.min, time.sec, time.utc_offset)
20
+ when ::DateTime
21
+ return self.new(time.year, time.month-1, time.day-1, time.hour, time.min, time.sec, time.offset)
22
+ when ::Date
23
+ return self.new(time.year, time.month-1, time.day-1, 0, 0, 0, 0)
24
+ when self
25
+ return time.dup
26
+ end
27
+ end
28
+
29
+ def self.now
30
+ return self.from(::Time.now)
31
+ end
32
+
33
+ # @parameter years [Integer] The number of years.
34
+ # @parameter months [Integer] The number of months.
35
+ # @parameter days [Integer] The number of days.
36
+ # @parameter hours [Integer] The number of hours.
37
+ # @parameter minutes [Integer] The number of minutes.
38
+ # @parameter seconds [Integer] The number of seconds.
39
+ # @parameter offset [Integer] The time zone offset.
40
+ def initialize(years, months, days, hours, minutes, seconds, offset)
41
+ @years = years
42
+ @months = months
43
+ @days = days
44
+ @hours = hours
45
+ @minutes = minutes
46
+ @seconds = seconds
47
+ @offset = offset
48
+ end
49
+
50
+ def freeze
51
+ return self if frozen?
52
+
53
+ @years.freeze
54
+ @months.freeze
55
+ @days.freeze
56
+ @hours.freeze
57
+ @minutes.freeze
58
+ @seconds.freeze
59
+ @offset.freeze
60
+
61
+ super
62
+ end
63
+
64
+ attr_accessor :years
65
+ attr_accessor :months
66
+ attr_accessor :days
67
+
68
+ attr_accessor :hours
69
+ attr_accessor :minutes
70
+ attr_accessor :seconds
71
+
72
+ attr_accessor :offset
73
+
74
+ def weekday
75
+ to_date.wday
76
+ end
77
+
78
+ def weekday=(value)
79
+ @days += value - weekday
80
+ end
81
+
82
+ def monthday=(value)
83
+ if value >= 0
84
+ @days = value
85
+ else
86
+ @days = ::Date.new(@years, @months+1, value).day
87
+ end
88
+ end
89
+
90
+ def to_a
91
+ [@years, @months, @days, @hours, @minutes, @seconds, @offset]
92
+ end
93
+
94
+ def <=> other
95
+ to_a <=> other.to_a
96
+ end
97
+
98
+ def hash
99
+ to_a.hash
100
+ end
101
+
102
+ def eql? other
103
+ to_a.eql?(other.to_a)
104
+ end
105
+
106
+ def delta
107
+ self.to_time - ::Time.now
108
+ end
109
+
110
+ def to_time
111
+ normalized = self.dup.normalize!
112
+
113
+ return ::Time.new(normalized.years, normalized.months+1, normalized.days+1, normalized.hours, normalized.minutes, normalized.seconds, normalized.offset)
114
+ end
115
+
116
+ def to_datetime
117
+ normalized = self.dup.normalize!
118
+
119
+ return ::DateTime.new(normalized.years, normalized.months+1, normalized.days+1, normalized.hours, normalized.minutes, normalized.seconds, normalized.offset)
120
+ end
121
+
122
+ def to_date
123
+ normalized = self.dup.normalize!
124
+
125
+ return ::Date.new(normalized.years, normalized.months+1, normalized.days+1)
126
+ end
127
+
128
+ def normalize!
129
+ seconds = self.seconds
130
+ delta_minutes = seconds / 60
131
+ seconds = seconds % 60
132
+
133
+ minutes = self.minutes + delta_minutes
134
+ delta_hours = minutes / 60
135
+ minutes = minutes % 60
136
+
137
+ hours = self.hours + delta_hours
138
+ delta_days = hours / 24
139
+ hours = hours % 24
140
+
141
+ days = self.days + delta_days
142
+ months = self.months
143
+ years = self.years
144
+
145
+ date = (Date.new(years, 1, 1) >> months) + days
146
+
147
+ @years = date.year
148
+ @months = date.month - 1
149
+ @days = date.day - 1
150
+ @hours = hours
151
+ @minutes = minutes
152
+ @seconds = seconds
153
+
154
+ return self
155
+ end
156
+
157
+ def to_s
158
+ sprintf("%04d+%02d+%02d %02d:%02d:%02d %d", years, months, days, hours, minutes, seconds, offset)
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ module Async
7
+ module Cron
8
+ VERSION = '0.1.0'
9
+ end
10
+ end
data/lib/async/cron.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require_relative 'cron/version'
7
+ require_relative 'cron/schedule'
data/license.md ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright, 2024, by Samuel Williams.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/readme.md ADDED
@@ -0,0 +1,23 @@
1
+ # Async::Cron
2
+
3
+ [![Development Status](https://github.com/socketry/async-cron/workflows/Test/badge.svg)](https://github.com/socketry/async-cron/actions?workflow=Test)
4
+
5
+ ## Usage
6
+
7
+ ## Contributing
8
+
9
+ We welcome contributions to this project.
10
+
11
+ 1. Fork it.
12
+ 2. Create your feature branch (`git checkout -b my-new-feature`).
13
+ 3. Commit your changes (`git commit -am 'Add some feature'`).
14
+ 4. Push to the branch (`git push origin my-new-feature`).
15
+ 5. Create new Pull Request.
16
+
17
+ ### Developer Certificate of Origin
18
+
19
+ This project uses the [Developer Certificate of Origin](https://developercertificate.org/). All contributors to this project must agree to this document to have their contributions accepted.
20
+
21
+ ### Contributor Covenant
22
+
23
+ This project is governed by the [Contributor Covenant](https://www.contributor-covenant.org/). All contributors and participants agree to abide by its terms.
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: async-cron
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Samuel Williams
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11
14
+ ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK
15
+ CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz
16
+ MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd
17
+ MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj
18
+ bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
19
+ igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2
20
+ 9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW
21
+ sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE
22
+ e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN
23
+ XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss
24
+ RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn
25
+ tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM
26
+ zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW
27
+ xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O
28
+ BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs
29
+ aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs
30
+ aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE
31
+ cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl
32
+ xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/
33
+ c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp
34
+ 8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws
35
+ JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP
36
+ eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt
37
+ Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
38
+ voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
39
+ -----END CERTIFICATE-----
40
+ date: 2024-06-08 00:00:00.000000000 Z
41
+ dependencies:
42
+ - !ruby/object:Gem::Dependency
43
+ name: async
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '2.0'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '2.0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: async-service
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ description:
71
+ email:
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - lib/async/cron.rb
77
+ - lib/async/cron/environment/scheduler.rb
78
+ - lib/async/cron/period.rb
79
+ - lib/async/cron/schedule.rb
80
+ - lib/async/cron/schedule/daily.rb
81
+ - lib/async/cron/schedule/flags.rb
82
+ - lib/async/cron/schedule/generic.rb
83
+ - lib/async/cron/schedule/hourly.rb
84
+ - lib/async/cron/schedule/monthly.rb
85
+ - lib/async/cron/schedule/periodic.rb
86
+ - lib/async/cron/schedule/weekly.rb
87
+ - lib/async/cron/scheduler.rb
88
+ - lib/async/cron/service/scheduler.rb
89
+ - lib/async/cron/time.rb
90
+ - lib/async/cron/version.rb
91
+ - license.md
92
+ - readme.md
93
+ homepage: https://github.com/socketry/async-cron
94
+ licenses:
95
+ - MIT
96
+ metadata:
97
+ documentation_uri: https://socketry.github.io/async-cron/
98
+ funding_uri: https://github.com/sponsors/ioquatix
99
+ source_code_uri: https://github.com/socketry/async-cron.git
100
+ post_install_message:
101
+ rdoc_options: []
102
+ require_paths:
103
+ - lib
104
+ required_ruby_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '3.1'
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ requirements: []
115
+ rubygems_version: 3.5.9
116
+ signing_key:
117
+ specification_version: 4
118
+ summary: A scheduling service using cron-style syntax.
119
+ test_files: []
metadata.gz.sig ADDED
Binary file