async-cron 0.1.0

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