rufus-scheduler 3.1.4 → 3.8.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +410 -0
  3. data/CREDITS.md +142 -0
  4. data/LICENSE.txt +1 -1
  5. data/Makefile +27 -0
  6. data/README.md +407 -143
  7. data/lib/rufus/scheduler/job_array.rb +36 -66
  8. data/lib/rufus/scheduler/jobs_core.rb +369 -0
  9. data/lib/rufus/scheduler/jobs_one_time.rb +53 -0
  10. data/lib/rufus/scheduler/jobs_repeat.rb +335 -0
  11. data/lib/rufus/scheduler/locks.rb +41 -67
  12. data/lib/rufus/scheduler/util.rb +89 -179
  13. data/lib/rufus/scheduler.rb +545 -453
  14. data/rufus-scheduler.gemspec +22 -11
  15. metadata +44 -85
  16. data/CHANGELOG.txt +0 -243
  17. data/CREDITS.txt +0 -88
  18. data/Rakefile +0 -83
  19. data/TODO.txt +0 -151
  20. data/lib/rufus/scheduler/cronline.rb +0 -470
  21. data/lib/rufus/scheduler/jobs.rb +0 -633
  22. data/lib/rufus/scheduler/zones.rb +0 -174
  23. data/lib/rufus/scheduler/zotime.rb +0 -155
  24. data/spec/basics_spec.rb +0 -54
  25. data/spec/cronline_spec.rb +0 -915
  26. data/spec/error_spec.rb +0 -139
  27. data/spec/job_array_spec.rb +0 -39
  28. data/spec/job_at_spec.rb +0 -58
  29. data/spec/job_cron_spec.rb +0 -128
  30. data/spec/job_every_spec.rb +0 -104
  31. data/spec/job_in_spec.rb +0 -20
  32. data/spec/job_interval_spec.rb +0 -68
  33. data/spec/job_repeat_spec.rb +0 -357
  34. data/spec/job_spec.rb +0 -631
  35. data/spec/lock_custom_spec.rb +0 -47
  36. data/spec/lock_flock_spec.rb +0 -47
  37. data/spec/lock_lockfile_spec.rb +0 -61
  38. data/spec/lock_spec.rb +0 -59
  39. data/spec/parse_spec.rb +0 -263
  40. data/spec/schedule_at_spec.rb +0 -158
  41. data/spec/schedule_cron_spec.rb +0 -66
  42. data/spec/schedule_every_spec.rb +0 -109
  43. data/spec/schedule_in_spec.rb +0 -80
  44. data/spec/schedule_interval_spec.rb +0 -128
  45. data/spec/scheduler_spec.rb +0 -1067
  46. data/spec/spec_helper.rb +0 -126
  47. data/spec/threads_spec.rb +0 -96
  48. data/spec/zotime_spec.rb +0 -396
data/TODO.txt DELETED
@@ -1,151 +0,0 @@
1
-
2
- [o] merge schedule_queue and unschedule_queue (and merge [un]schedule steps)
3
- [x] OR stop using queue, since we've got the thread-safe JobArray
4
- [x] if possible, drop the mutex in JobArray
5
- NO, that mutex is necessary for Scheduler#jobs (on JRuby an co)...
6
- [o] named mutexes
7
- [o] drop the schedule queue, rely on the mutex in JobArray
8
- [o] def jobs; (@jobs.to_a + running_jobs).uniq; end
9
- [o] replace @unscheduled by @unscheduled_at
10
- [o] make sure #jobs doesn't return unscheduled jobs
11
- [o] job tags and find_by_tag(t) (as in rs 2.x)
12
- [o] require tzinfo anyway (runtime dep)
13
- [o] document frequency
14
- [o] accept :frequency => '5s'
15
- [o] timeout (as in rufus-scheduler 2.x)
16
- [o] Rufus::Scheduler#running_jobs (as in rufus-scheduler 2.x)
17
- [o] Rufus::Scheduler#terminate_all_jobs
18
- [o] Rufus::Scheduler::Job#kill
19
- [x] Rufus::Scheduler#kill_all_jobs
20
- [o] Rufus::Scheduler#shutdown(:terminate or :kill (or nothing))
21
- [o] RepeatJob #pause / #resume (think about discard past)
22
- [o] Rufus::Scheduler.start_new (backward comp) (with deprec note?)
23
- [o] pass job to scheduled block? What does rs 2.x do?
24
- [o] :first[_in|_at] for RepeatJob
25
- [o] :last[_in|_at] for RepeatJob
26
- [o] :times for RepeatJob (how many recurrences)
27
- [o] fix issue #39 (first_at parses as UTC)
28
- [o] about issue #43, raise if cron/every job frequency < scheduler frequency
29
- [o] unlock spec/parse_spec.rb:30 "parse datimes with timezones"
30
- [o] some kind of Schedulable (Xyz#call(job, time))
31
- [o] add Jruby and Rubinius to Travis
32
- [o] make Job #first_at= / #last_at= automatically parse strings?
33
- [o] bring in Kratob's spec about mutex vs timeout and adapt 3.0 to it,
34
- https://github.com/jmettraux/rufus-scheduler/pull/67
35
- [x] :unschedule_if => lambda { |job| ... }
36
- [o] OR look at how it was done in rs 2.0.x, some return value?
37
- no, pass the job as arg to the block, then let the block do job.unschedule
38
- so, document schedule.every('10d') { |j| j.unschedule if x?() }
39
- [x] remove the time in job.trigger(time)
40
- [o] add spec for job queued then unscheduled
41
- [o] add spec for Scheduler#shutdown and work threads
42
- [o] at some point, bring back rbx19 to Travis
43
- [o] move the parse/util part of scheduler.rb to util.rb
44
- [o] rescue KillSignal in job thread loop to kill just the job
45
- [o] add spec for raise if scheduling a job while scheduler is shutting down
46
- [o] schedule_in(2.days.from_now) {}
47
- at and in could understand each others time parameter, ftw...
48
- use the new #parse_to_time? no
49
- [o] do repeat jobs reschedule after timing out? yes
50
- [o] schedule_interval('20s')?
51
- [x] Scheduler#reschedule(job) (new copy of the job)
52
- [x] #free_all_work_threads is missing an implementation
53
- [x] rescue StandardError
54
- :on_error => :crash[_scheduler]
55
- :on_error => :ignore
56
- :on_error => ...
57
- [o] on_error: what about TimeoutError in that scheme?
58
- TimeoutError goes to $stderr, like a normal error
59
- [o] link to SO for support
60
- - sublink to "how to report bugs effectively"
61
- [o] link to #ruote for support
62
- [x] lockblock? pass a block to teach the scheduler how to lock?
63
- is not necessary, @scheduler = Scheduler.new if should_start?
64
- the surrounding Ruby code checks
65
- [o] introduce job "vars", as in
66
- http://stackoverflow.com/questions/18202848/how-to-have-a-variable-that-will-available-to-particular-scheduled-task-whenever
67
- or job['key'] Job #[] and #[]=, as with Thread #[] #[]=
68
- job-local variables #keys #key?
69
- [o] thread-safety for job-local variables?
70
- [x] discard past? discard_past => true or => "1d"
71
- default would be discard_past => "1m" or scheduler freq * 2 ?
72
- jobs would adjust their next_time until it fits the window...
73
- ~~ discard past by default
74
- [o] expanded block/schedulable (it's "callable")
75
- ```
76
- scheduler.every '10m' do
77
- def pre
78
- return false if Backend.down?
79
- # ...
80
- end
81
- def post
82
- # ...
83
- end
84
- def trigger
85
- puts "oh hai!"
86
- end
87
- end
88
- ```
89
- or something like that...
90
- ...
91
- OR accept a class (and instantiate it the first time)
92
- ```
93
- scheduler.every '10m', Class.new do
94
- def call(job, time)
95
- # ...
96
- end
97
- end
98
- ```
99
- the job contains the instance in its @callable
100
- [x] add spec case for corner case in Job#trigger (overlap vs reschedule) !!!
101
- [o] rethink job array vs job set for #scheduled?
102
- [x] introduce common parent class for EveryJob and IntervalJob
103
- [o] create spec/ at_job_spec.rb, repeat_job_spec.rb, cron_job_spec.rb, ...
104
- [x] ensure EveryJob do not schedule in the past (it's already like that)
105
- [o] CronLine#next_time should return a time with subseconds chopped off
106
- [o] drop min work threads setting?
107
- [o] thread pool something? Thread upper limit?
108
- [o] Rufus::Scheduler.singleton, Rufus::Scheduler.s
109
- [o] EveryJob#first_at= and IntervalJob#first_at= should alter @next_time
110
- [o] scheduler.schedule duration/time/cron ... for at/in/cron
111
- (not every, nor interval)
112
- scheduler.repeat time/cron ... for every/cron
113
-
114
- [o] :lockfile => x, timestamp, process_id, thread_id...
115
- warning: have to clean up that file on exit... or does the scheduler
116
- timestamps it?
117
- [ ] develop lockfile timestamp thinggy
118
- ~ if the timestamp is too old (twice the default frequency?) then
119
- lock [file] take over...
120
- Is that really what we want all the time?
121
-
122
- [ ] idea: :mutex => x and :skip_on_mutex => true ?
123
- would prevent blocking/waiting for the mutex to get available
124
- :mutex => [ "mutex_name", true ]
125
- :mutex => [ [ "mutex_name", true ], [ "other_mutex_name", false ] ]
126
-
127
- [ ] bring back EM (but only EM.defer ?) :defer => true (Job or Scheduler
128
- or both option?)
129
-
130
- [ ] prepare a daemon, trust daemon-kit for that
131
-
132
- [ ] :if => lambda { |job, time| ... } why not?
133
- :unless => lambda { ...
134
- :block => lambda { ...
135
- can help get the block themselves leaner
136
- #
137
- investigate guards for schedulables... def if_guard; ...; end
138
-
139
- [ ] scheduler.every '10', Class.new do
140
- def call(job, time)
141
- # might fail...
142
- end
143
- def on_error(err, job)
144
- # catches...
145
- end
146
- end
147
-
148
- ~~~
149
-
150
- [ ] scheduler.at('chronic string', chronic_options...)
151
-
@@ -1,470 +0,0 @@
1
- #--
2
- # Copyright (c) 2006-2015, John Mettraux, jmettraux@gmail.com
3
- #
4
- # Permission is hereby granted, free of charge, to any person obtaining a copy
5
- # of this software and associated documentation files (the "Software"), to deal
6
- # in the Software without restriction, including without limitation the rights
7
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
- # copies of the Software, and to permit persons to whom the Software is
9
- # furnished to do so, subject to the following conditions:
10
- #
11
- # The above copyright notice and this permission notice shall be included in
12
- # all copies or substantial portions of the Software.
13
- #
14
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
- # THE SOFTWARE.
21
- #
22
- # Made in Japan.
23
- #++
24
-
25
- require 'set'
26
-
27
-
28
- class Rufus::Scheduler
29
-
30
- #
31
- # A 'cron line' is a line in the sense of a crontab
32
- # (man 5 crontab) file line.
33
- #
34
- class CronLine
35
-
36
- # The string used for creating this cronline instance.
37
- #
38
- attr_reader :original
39
-
40
- attr_reader :seconds
41
- attr_reader :minutes
42
- attr_reader :hours
43
- attr_reader :days
44
- attr_reader :months
45
- attr_reader :weekdays
46
- attr_reader :monthdays
47
- attr_reader :timezone
48
-
49
- def initialize(line)
50
-
51
- raise ArgumentError.new(
52
- "not a string: #{line.inspect}"
53
- ) unless line.is_a?(String)
54
-
55
- @original = line
56
-
57
- items = line.split
58
-
59
- @timezone = items.pop if ZoTime.is_timezone?(items.last)
60
-
61
- raise ArgumentError.new(
62
- "not a valid cronline : '#{line}'"
63
- ) unless items.length == 5 or items.length == 6
64
-
65
- offset = items.length - 5
66
-
67
- @seconds = offset == 1 ? parse_item(items[0], 0, 59) : [ 0 ]
68
- @minutes = parse_item(items[0 + offset], 0, 59)
69
- @hours = parse_item(items[1 + offset], 0, 24)
70
- @days = parse_item(items[2 + offset], 1, 31)
71
- @months = parse_item(items[3 + offset], 1, 12)
72
- @weekdays, @monthdays = parse_weekdays(items[4 + offset])
73
-
74
- [ @seconds, @minutes, @hours, @months ].each do |es|
75
-
76
- raise ArgumentError.new(
77
- "invalid cronline: '#{line}'"
78
- ) if es && es.find { |e| ! e.is_a?(Fixnum) }
79
- end
80
- end
81
-
82
- # Returns true if the given time matches this cron line.
83
- #
84
- def matches?(time)
85
-
86
- time = ZoTime.new(time.to_f, @timezone || ENV['TZ']).time
87
-
88
- return false unless sub_match?(time, :sec, @seconds)
89
- return false unless sub_match?(time, :min, @minutes)
90
- return false unless sub_match?(time, :hour, @hours)
91
- return false unless date_match?(time)
92
- true
93
- end
94
-
95
- # Returns the next time that this cron line is supposed to 'fire'
96
- #
97
- # This is raw, 3 secs to iterate over 1 year on my macbook :( brutal.
98
- # (Well, I was wrong, takes 0.001 sec on 1.8.7 and 1.9.1)
99
- #
100
- # This method accepts an optional Time parameter. It's the starting point
101
- # for the 'search'. By default, it's Time.now
102
- #
103
- # Note that the time instance returned will be in the same time zone that
104
- # the given start point Time (thus a result in the local time zone will
105
- # be passed if no start time is specified (search start time set to
106
- # Time.now))
107
- #
108
- # Rufus::Scheduler::CronLine.new('30 7 * * *').next_time(
109
- # Time.mktime(2008, 10, 24, 7, 29))
110
- # #=> Fri Oct 24 07:30:00 -0500 2008
111
- #
112
- # Rufus::Scheduler::CronLine.new('30 7 * * *').next_time(
113
- # Time.utc(2008, 10, 24, 7, 29))
114
- # #=> Fri Oct 24 07:30:00 UTC 2008
115
- #
116
- # Rufus::Scheduler::CronLine.new('30 7 * * *').next_time(
117
- # Time.utc(2008, 10, 24, 7, 29)).localtime
118
- # #=> Fri Oct 24 02:30:00 -0500 2008
119
- #
120
- # (Thanks to K Liu for the note and the examples)
121
- #
122
- def next_time(from=Time.now)
123
-
124
- time = nil
125
- zotime = ZoTime.new(from.to_i + 1, @timezone || ENV['TZ'])
126
-
127
- loop do
128
-
129
- time = zotime.time
130
-
131
- unless date_match?(time)
132
- zotime.add((24 - time.hour) * 3600 - time.min * 60 - time.sec)
133
- next
134
- end
135
- unless sub_match?(time, :hour, @hours)
136
- zotime.add((60 - time.min) * 60 - time.sec)
137
- next
138
- end
139
- unless sub_match?(time, :min, @minutes)
140
- zotime.add(60 - time.sec)
141
- next
142
- end
143
- unless sub_match?(time, :sec, @seconds)
144
- zotime.add(next_second(time))
145
- next
146
- end
147
-
148
- break
149
- end
150
-
151
- time
152
- end
153
-
154
- # Returns the previous time the cronline matched. It's like next_time, but
155
- # for the past.
156
- #
157
- def previous_time(from=Time.now)
158
-
159
- time = nil
160
- zotime = ZoTime.new(from.to_i - 1, @timezone || ENV['TZ'])
161
-
162
- loop do
163
-
164
- time = zotime.time
165
-
166
- unless date_match?(time)
167
- zotime.substract(time.hour * 3600 + time.min * 60 + time.sec + 1)
168
- next
169
- end
170
- unless sub_match?(time, :hour, @hours)
171
- zotime.substract(time.min * 60 + time.sec + 1)
172
- next
173
- end
174
- unless sub_match?(time, :min, @minutes)
175
- zotime.substract(time.sec + 1)
176
- next
177
- end
178
- unless sub_match?(time, :sec, @seconds)
179
- zotime.substract(prev_second(time))
180
- next
181
- end
182
-
183
- break
184
- end
185
-
186
- time
187
- end
188
-
189
- # Returns an array of 6 arrays (seconds, minutes, hours, days,
190
- # months, weekdays).
191
- # This method is used by the cronline unit tests.
192
- #
193
- def to_array
194
-
195
- [
196
- toa(@seconds),
197
- toa(@minutes),
198
- toa(@hours),
199
- toa(@days),
200
- toa(@months),
201
- toa(@weekdays),
202
- toa(@monthdays),
203
- @timezone
204
- ]
205
- end
206
-
207
- # Returns a quickly computed approximation of the frequency for this
208
- # cron line.
209
- #
210
- # #brute_frequency, on the other hand, will compute the frequency by
211
- # examining a whole year, that can take more than seconds for a seconds
212
- # level cron...
213
- #
214
- def frequency
215
-
216
- return brute_frequency unless @seconds && @seconds.length > 1
217
-
218
- secs = toa(@seconds)
219
-
220
- secs[1..-1].inject([ secs[0], 60 ]) { |(prev, delta), sec|
221
- d = sec - prev
222
- [ sec, d < delta ? d : delta ]
223
- }[1]
224
- end
225
-
226
- # Returns the shortest delta between two potential occurences of the
227
- # schedule described by this cronline.
228
- #
229
- # .
230
- #
231
- # For a simple cronline like "*/5 * * * *", obviously the frequency is
232
- # five minutes. Why does this method look at a whole year of #next_time ?
233
- #
234
- # Consider "* * * * sun#2,sun#3", the computed frequency is 1 week
235
- # (the shortest delta is the one between the second sunday and the third
236
- # sunday). This method takes no chance and runs next_time for the span
237
- # of a whole year and keeps the shortest.
238
- #
239
- # Of course, this method can get VERY slow if you call on it a second-
240
- # based cronline...
241
- #
242
- # Since it's a rarely used method, I haven't taken the time to make it
243
- # smarter/faster.
244
- #
245
- # One obvious improvement would be to cache the result once computed...
246
- #
247
- # See https://github.com/jmettraux/rufus-scheduler/issues/89
248
- # for a discussion about this method.
249
- #
250
- def brute_frequency
251
-
252
- delta = 366 * DAY_S
253
-
254
- t0 = previous_time(Time.local(2000, 1, 1))
255
-
256
- loop do
257
-
258
- break if delta <= 1
259
- break if delta <= 60 && @seconds && @seconds.size == 1
260
-
261
- t1 = next_time(t0)
262
- d = t1 - t0
263
- delta = d if d < delta
264
-
265
- break if @months == nil && t1.month == 2
266
- break if t1.year >= 2001
267
-
268
- t0 = t1
269
- end
270
-
271
- delta
272
- end
273
-
274
- def next_second(time)
275
-
276
- secs = toa(@seconds)
277
-
278
- return secs.first + 60 - time.sec if time.sec > secs.last
279
-
280
- secs.shift while secs.first < time.sec
281
-
282
- secs.first - time.sec
283
- end
284
-
285
- def prev_second(time)
286
-
287
- secs = toa(@seconds)
288
-
289
- secs.pop while time.sec < secs.last
290
-
291
- time.sec - secs.last
292
- end
293
-
294
- protected
295
-
296
- def sc_sort(a)
297
-
298
- a.sort_by { |e| e.is_a?(String) ? 61 : e.to_i }
299
- end
300
-
301
- if RUBY_VERSION >= '1.9'
302
- def toa(item)
303
- item == nil ? nil : item.to_a
304
- end
305
- else
306
- def toa(item)
307
- item.is_a?(Set) ? sc_sort(item.to_a) : item
308
- end
309
- end
310
-
311
- WEEKDAYS = %w[ sun mon tue wed thu fri sat ]
312
- DAY_S = 24 * 3600
313
- WEEK_S = 7 * DAY_S
314
-
315
- def parse_weekdays(item)
316
-
317
- return nil if item == '*'
318
-
319
- items = item.downcase.split(',')
320
-
321
- weekdays = nil
322
- monthdays = nil
323
-
324
- items.each do |it|
325
-
326
- if m = it.match(/^(.+)#(l|-?[12345])$/)
327
-
328
- raise ArgumentError.new(
329
- "ranges are not supported for monthdays (#{it})"
330
- ) if m[1].index('-')
331
-
332
- expr = it.gsub(/#l/, '#-1')
333
-
334
- (monthdays ||= []) << expr
335
-
336
- else
337
-
338
- expr = it.dup
339
- WEEKDAYS.each_with_index { |a, i| expr.gsub!(/#{a}/, i.to_s) }
340
-
341
- raise ArgumentError.new(
342
- "invalid weekday expression (#{it})"
343
- ) if expr !~ /^0*[0-7](-0*[0-7])?$/
344
-
345
- its = expr.index('-') ? parse_range(expr, 0, 7) : [ Integer(expr) ]
346
- its = its.collect { |i| i == 7 ? 0 : i }
347
-
348
- (weekdays ||= []).concat(its)
349
- end
350
- end
351
-
352
- weekdays = weekdays.uniq.sort if weekdays
353
-
354
- [ weekdays, monthdays ]
355
- end
356
-
357
- def parse_item(item, min, max)
358
-
359
- return nil if item == '*'
360
-
361
- r = item.split(',').map { |i| parse_range(i.strip, min, max) }.flatten
362
-
363
- raise ArgumentError.new(
364
- "found duplicates in #{item.inspect}"
365
- ) if r.uniq.size < r.size
366
-
367
- r = sc_sort(r)
368
-
369
- Set.new(r)
370
- end
371
-
372
- RANGE_REGEX = /^(\*|\d{1,2})(?:-(\d{1,2}))?(?:\/(\d{1,2}))?$/
373
-
374
- def parse_range(item, min, max)
375
-
376
- return %w[ L ] if item == 'L'
377
-
378
- item = '*' + item if item.match(/^\//)
379
-
380
- m = item.match(RANGE_REGEX)
381
-
382
- raise ArgumentError.new(
383
- "cannot parse #{item.inspect}"
384
- ) unless m
385
-
386
- sta = m[1]
387
- sta = sta == '*' ? min : sta.to_i
388
-
389
- edn = m[2]
390
- edn = edn ? edn.to_i : sta
391
- edn = max if m[1] == '*'
392
-
393
- inc = m[3]
394
- inc = inc ? inc.to_i : 1
395
-
396
- raise ArgumentError.new(
397
- "#{item.inspect} is not in range #{min}..#{max}"
398
- ) if sta < min || edn > max
399
-
400
- r = []
401
- val = sta
402
-
403
- loop do
404
- v = val
405
- v = 0 if max == 24 && v == 24
406
- r << v
407
- break if inc == 1 && val == edn
408
- val += inc
409
- break if inc > 1 && val > edn
410
- val = min if val > max
411
- end
412
-
413
- r.uniq
414
- end
415
-
416
- def sub_match?(time, accessor, values)
417
-
418
- value = time.send(accessor)
419
-
420
- return true if values.nil?
421
- return true if values.include?('L') && (time + DAY_S).day == 1
422
-
423
- return true if value == 0 && accessor == :hour && values.include?(24)
424
-
425
- values.include?(value)
426
- end
427
-
428
- def monthday_match?(date, values)
429
-
430
- return true if values.nil?
431
-
432
- today_values = monthdays(date)
433
-
434
- (today_values & values).any?
435
- end
436
-
437
- def date_match?(date)
438
-
439
- return false unless sub_match?(date, :day, @days)
440
- return false unless sub_match?(date, :month, @months)
441
- return false unless sub_match?(date, :wday, @weekdays)
442
- return false unless monthday_match?(date, @monthdays)
443
- true
444
- end
445
-
446
- def monthdays(date)
447
-
448
- pos = 1
449
- d = date.dup
450
-
451
- loop do
452
- d = d - WEEK_S
453
- break if d.month != date.month
454
- pos = pos + 1
455
- end
456
-
457
- neg = -1
458
- d = date.dup
459
-
460
- loop do
461
- d = d + WEEK_S
462
- break if d.month != date.month
463
- neg = neg - 1
464
- end
465
-
466
- [ "#{WEEKDAYS[date.wday]}##{pos}", "#{WEEKDAYS[date.wday]}##{neg}" ]
467
- end
468
- end
469
- end
470
-