icalPal 3.2.0 → 3.4.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 +4 -4
- data/README.md +23 -12
- data/bin/icalPal +51 -90
- data/bin/icalpal +322 -0
- data/icalPal.gemspec +32 -2
- data/lib/ToICalPal.rb +24 -27
- data/lib/defaults.rb +8 -6
- data/lib/event.rb +101 -80
- data/lib/icalPal.rb +18 -43
- data/lib/options.rb +52 -9
- data/lib/rdt.rb +22 -4
- data/lib/utils.rb +38 -0
- data/lib/version.rb +1 -1
- metadata +36 -6
data/lib/ToICalPal.rb
CHANGED
@@ -6,25 +6,25 @@ class RDoc::Markup::ToICalPal < RDoc::Markup::Formatter
|
|
6
6
|
# ANSI[https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-T.416-199303-I!!PDF-E&type=items]
|
7
7
|
# colors
|
8
8
|
ANSI = {
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
9
|
+
black: 30, '#000000': '38;5;0',
|
10
|
+
red: 31, '#ff0000': '38;5;1',
|
11
|
+
green: 32, '#00ff00': '38;5;2',
|
12
|
+
yellow: 33, '#ffff00': '38;5;3',
|
13
|
+
blue: 34, '#0000ff': '38;5;4',
|
14
|
+
magenta: 35, '#ff00ff': '38;5;5',
|
15
|
+
cyan: 36, '#00ffff': '38;5;6',
|
16
|
+
white: 37, '#ffffff': '38;5;255',
|
17
|
+
default: 39, custom: nil,
|
18
18
|
|
19
19
|
# Reminders custom colors
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
20
|
+
brown: '38;2;162;132;94',
|
21
|
+
gray: '38;2;91;98;106',
|
22
|
+
indigo: '38;2;88;86;214',
|
23
|
+
lightblue: '38;2;90;200;250',
|
24
|
+
orange: '38;2;255;149;0',
|
25
|
+
pink: '38;2;255;45;85',
|
26
|
+
purple: '38;2;204;115;225',
|
27
|
+
rose: '38;2;217;166;159',
|
28
28
|
}.freeze
|
29
29
|
|
30
30
|
# Increased intensity
|
@@ -55,7 +55,7 @@ class RDoc::Markup::ToICalPal < RDoc::Markup::Formatter
|
|
55
55
|
# @option opts [Array<String>] :ps List of property separators
|
56
56
|
# @option opts [String] :ss Section separator
|
57
57
|
def initialize(opts)
|
58
|
-
super
|
58
|
+
super
|
59
59
|
@opts = opts
|
60
60
|
end
|
61
61
|
|
@@ -76,10 +76,7 @@ class RDoc::Markup::ToICalPal < RDoc::Markup::Formatter
|
|
76
76
|
def accept_list_start(_arg)
|
77
77
|
begin
|
78
78
|
return if @item['placeholder']
|
79
|
-
rescue
|
80
|
-
end
|
81
79
|
|
82
|
-
begin
|
83
80
|
if (@item['due_date'] + ICalPal::ITIME).between?(ICalPal::ITIME + 1, $now.to_i)
|
84
81
|
@res << "#{@opts[:ab]} " unless @opts[:nb]
|
85
82
|
return
|
@@ -95,7 +92,7 @@ class RDoc::Markup::ToICalPal < RDoc::Markup::Formatter
|
|
95
92
|
# @param arg [RDoc::Markup::ListItem]
|
96
93
|
# @option arg [String] .label Contains the property name
|
97
94
|
def accept_list_item_start(arg)
|
98
|
-
@res << @opts[:ps][@ps] || ' ' unless @item['placeholder']
|
95
|
+
@res << (@opts[:ps][@ps] || ' ') unless @item['placeholder']
|
99
96
|
@res << colorize(*LABEL_COLOR, arg.label) << ': ' unless @opts[:npn] || NO_LABEL.any?(arg.label)
|
100
97
|
|
101
98
|
@ps += 1 unless @ps == @opts[:ps].count - 1
|
@@ -133,7 +130,7 @@ class RDoc::Markup::ToICalPal < RDoc::Markup::Formatter
|
|
133
130
|
# @param p [RDoc::Markup::Paragraph]
|
134
131
|
# @option p [Array<String>] :parts The property's text
|
135
132
|
def accept_paragraph(p)
|
136
|
-
t = p.parts.join('; ').gsub(
|
133
|
+
t = p.parts.join('; ').gsub("\n", "\n ")
|
137
134
|
t = colorize(*DATE_COLOR, t) if @prop == 'datetime'
|
138
135
|
@res << t
|
139
136
|
end
|
@@ -196,10 +193,10 @@ class RDoc::Markup::ToICalPal < RDoc::Markup::Formatter
|
|
196
193
|
|
197
194
|
# @!visibility private
|
198
195
|
|
199
|
-
# @param
|
200
|
-
def accept_list_end(
|
196
|
+
# @param _a [Array] Ignored
|
197
|
+
def accept_list_end(_a) end
|
201
198
|
|
202
|
-
# @param
|
203
|
-
def accept_list_item_end(
|
199
|
+
# @param _a [Array] Ignored
|
200
|
+
def accept_list_item_end(_a) end
|
204
201
|
|
205
202
|
end
|
data/lib/defaults.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# Does anybody really know what time it is?
|
2
|
-
|
3
|
-
$
|
2
|
+
now = Time.now
|
3
|
+
$now = ICalPal::RDT.from_time(now)
|
4
|
+
$today = ICalPal::RDT.new(*$now.to_a[0..2] + [ 0, 0, 0 ])
|
4
5
|
|
5
6
|
# Defaults
|
6
7
|
$defaults = {
|
@@ -8,11 +9,11 @@ $defaults = {
|
|
8
9
|
ab: '!',
|
9
10
|
aep: [],
|
10
11
|
bullet: '•',
|
11
|
-
cf: "#{
|
12
|
+
cf: "#{Dir.home}/.icalpal",
|
12
13
|
color: false,
|
13
14
|
db: [
|
14
|
-
"#{
|
15
|
-
"#{
|
15
|
+
"#{Dir.home}/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb",
|
16
|
+
"#{Dir.home}/Library/Calendars/Calendar.sqlitedb",
|
16
17
|
],
|
17
18
|
debug: Logger::WARN,
|
18
19
|
df: '%b %-d, %Y',
|
@@ -73,10 +74,11 @@ $defaults = {
|
|
73
74
|
n: false,
|
74
75
|
nnr: "\n ",
|
75
76
|
nrd: false,
|
77
|
+
now: false,
|
76
78
|
ps: [ "\n " ],
|
77
79
|
sa: false,
|
78
80
|
sed: false,
|
79
|
-
sort: '
|
81
|
+
sort: 'sctime',
|
80
82
|
ss: "\n------------------------",
|
81
83
|
to: nil,
|
82
84
|
uid: false,
|
data/lib/event.rb
CHANGED
@@ -1,18 +1,20 @@
|
|
1
|
-
require '
|
1
|
+
require 'timezone'
|
2
2
|
|
3
3
|
module ICalPal
|
4
4
|
# Class representing items from the <tt>CalendarItem</tt> table
|
5
5
|
class Event
|
6
6
|
include ICalPal
|
7
7
|
|
8
|
-
# Standard accessor with special handling for +sdate+. Setting
|
9
|
-
#
|
8
|
+
# Standard accessor with special handling for +sdate+ and +edate+. Setting
|
9
|
+
# those will also set +sctime+ and +ectime+ respectively.
|
10
10
|
#
|
11
11
|
# @param k [String] Key/property name
|
12
12
|
# @param v [Object] Key/property value
|
13
13
|
def []=(k, v)
|
14
14
|
@self[k] = v
|
15
|
-
|
15
|
+
|
16
|
+
@self['sctime'] = Time.at(@self['sdate'].to_i, in: 'UTC') if k == 'sdate'
|
17
|
+
@self['ectime'] = Time.at(@self['edate'].to_i, in: 'UTC') if k == 'edate'
|
16
18
|
end
|
17
19
|
|
18
20
|
# Standard accessor with special handling for +age+,
|
@@ -34,10 +36,10 @@ module ICalPal
|
|
34
36
|
t += ' at ' unless @self['all_day'].positive?
|
35
37
|
end
|
36
38
|
|
37
|
-
unless @self['all_day'] && @self['all_day'].positive? || @self['placeholder']
|
39
|
+
unless (@self['all_day'] && @self['all_day'].positive?) || @self['placeholder']
|
38
40
|
t ||= ''
|
39
|
-
t += "#{@self['
|
40
|
-
t += " - #{@self['
|
41
|
+
t += "#{@self['sctime'].strftime($opts[:tf])}" if @self['sctime']
|
42
|
+
t += " - #{@self['ectime'].strftime($opts[:tf])}" unless $opts[:eed] || !@self['ectime'] || @self['duration'].zero?
|
41
43
|
end
|
42
44
|
t
|
43
45
|
|
@@ -45,10 +47,10 @@ module ICalPal
|
|
45
47
|
(@self['location'])? [ @self['location'], @self['address'] ].join(' ').chop : nil
|
46
48
|
|
47
49
|
when 'notes' # \n -> :nnr
|
48
|
-
(@self['notes'])? @self['notes'].strip.gsub(
|
50
|
+
(@self['notes'])? @self['notes'].strip.gsub("\n", $opts[:nnr]) : nil
|
49
51
|
|
50
52
|
when 'sday' # pseudo-property
|
51
|
-
|
53
|
+
RDT.new(*@self['sdate'].to_a[0..2])
|
52
54
|
|
53
55
|
when 'status' # Integer -> String
|
54
56
|
EventKit::EKEventStatus.select { |_k, v| v == @self['status'] }.keys[0]
|
@@ -83,23 +85,24 @@ module ICalPal
|
|
83
85
|
|
84
86
|
# Convert JSON arrays to Arrays
|
85
87
|
@self['attendees'] = JSON.parse(obj['attendees'])
|
86
|
-
# rubocop: disable Lint/UselessAssignment
|
87
88
|
@self['xdate'] = JSON.parse(obj['xdate']).map do |k|
|
88
|
-
|
89
|
+
RDT.from_itime(k) if k
|
89
90
|
end
|
90
|
-
# rubocop: enable Lint/UselessAssignment
|
91
91
|
|
92
92
|
# Convert iCal dates to normal dates
|
93
93
|
obj.keys.select { |i| i.end_with? '_date' }.each do |k|
|
94
|
-
|
95
|
-
|
96
|
-
|
94
|
+
next unless obj[k]
|
95
|
+
|
96
|
+
begin
|
97
|
+
zone = Timezone.fetch(obj['start_tz'])
|
98
|
+
rescue Timezone::Error::InvalidZone
|
99
|
+
zone = 'UTC'
|
100
|
+
end
|
97
101
|
|
98
|
-
|
99
|
-
tzoffset = Time.zone_offset($now.zone)
|
102
|
+
ctime = obj[k] + ITIME
|
100
103
|
|
101
|
-
@self[
|
102
|
-
@self[
|
104
|
+
@self["#{k[0]}ctime"] = Time.at(ctime)
|
105
|
+
@self["#{k[0]}date"] = RDT.from_time(Time.at(ctime, in: zone))
|
103
106
|
end
|
104
107
|
|
105
108
|
# Type of calendar event is from
|
@@ -123,14 +126,17 @@ module ICalPal
|
|
123
126
|
# Sanity checks
|
124
127
|
return events if nDays > 100_000
|
125
128
|
|
129
|
+
# If multi-day, each (unique) day needs to end at 23:59:59
|
130
|
+
self['edate'] = RDT.new(*@self['sdate'].to_a[0..2] + [ 23, 59, 59 ]) if nDays.positive?
|
131
|
+
|
126
132
|
# Repeat for multi-day events
|
127
133
|
(nDays + 1).times do |i|
|
128
134
|
break if self['sdate'] > $opts[:to]
|
129
135
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
136
|
+
if in_window?(self['sdate'], self['edate'])
|
137
|
+
self['daynum'] = i + 1 if nDays.positive?
|
138
|
+
events.push(clone)
|
139
|
+
end
|
134
140
|
|
135
141
|
self['sdate'] += 1
|
136
142
|
self['edate'] += 1
|
@@ -145,78 +151,76 @@ module ICalPal
|
|
145
151
|
# All occurrences of a recurring event that are within our window
|
146
152
|
def recurring
|
147
153
|
stop = [ $opts[:to], (self['rdate'] || $opts[:to]) ].min
|
154
|
+
events = []
|
155
|
+
count = 1
|
148
156
|
|
149
157
|
# See if event ends before we start
|
150
|
-
|
151
|
-
$log.debug("#{stop} < #{$opts[:from]}")
|
152
|
-
return([])
|
153
|
-
end
|
158
|
+
return events if $opts[:from] > stop
|
154
159
|
|
155
160
|
# Get changes to series
|
156
|
-
changes = [ {
|
161
|
+
changes = [ { orig_date: -1 } ]
|
157
162
|
changes += $rows.select { |r| r['orig_item_id'] == self['ROWID'] }
|
158
163
|
|
159
|
-
events = []
|
160
|
-
count = 1
|
161
|
-
|
162
164
|
while self['sdate'] <= stop
|
163
165
|
# count
|
164
166
|
break if self['count'].positive? && count > self['count']
|
165
167
|
|
166
168
|
count += 1
|
167
169
|
|
168
|
-
# Handle specifier
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
occurrences = [ clone ]
|
173
|
-
end
|
170
|
+
# Handle specifier
|
171
|
+
o = []
|
172
|
+
o.push(self) unless self['specifier'] && self['specifier'].length.positive?
|
173
|
+
o += occurrences if self['specifier'] && self['specifier'].length.positive?
|
174
174
|
|
175
175
|
# Check for changes
|
176
|
-
|
177
|
-
|
178
|
-
|
176
|
+
o.each do |occurrence|
|
177
|
+
skip = false
|
178
|
+
|
179
|
+
changes[1..].each do |change|
|
180
|
+
cdate = Time.at(change['start_date'] + ITIME).to_a[3..5].reverse
|
181
|
+
odate = occurrence['sdate'].ymd
|
179
182
|
|
180
|
-
|
183
|
+
skip = true if cdate == odate
|
181
184
|
end
|
182
|
-
end
|
183
185
|
|
184
|
-
|
186
|
+
events.push(clone(occurrence)) if in_window?(occurrence['sdate'], occurrence['edate']) && !skip
|
187
|
+
end
|
185
188
|
|
186
|
-
|
189
|
+
# Handle frequency and interval
|
190
|
+
apply_frequency! if self['frequency'] && self['interval']
|
187
191
|
end
|
188
192
|
|
189
193
|
# Remove exceptions
|
190
194
|
events.delete_if { |event| event['xdate'].any?(event['sdate']) }
|
191
195
|
|
192
|
-
events
|
196
|
+
events.uniq { |e| e['sdate'] }
|
193
197
|
end
|
194
198
|
|
195
199
|
private
|
196
200
|
|
197
201
|
# @!visibility public
|
198
202
|
|
199
|
-
#
|
200
|
-
|
201
|
-
|
203
|
+
# Deep clone an object
|
204
|
+
#
|
205
|
+
# @param obj [Object]
|
206
|
+
# @return [Object] a deep clone of obj
|
207
|
+
def clone(obj = self)
|
208
|
+
Marshal.load(Marshal.dump(obj))
|
202
209
|
end
|
203
210
|
|
204
|
-
# Get next
|
211
|
+
# Get next occurrences of a recurring event given a specifier
|
205
212
|
#
|
206
|
-
# @
|
207
|
-
|
208
|
-
|
209
|
-
occurrences = []
|
213
|
+
# @return [Array<ICalPal::Event>]
|
214
|
+
def occurrences
|
215
|
+
o = []
|
210
216
|
|
211
217
|
dow = DOW.keys
|
212
|
-
dom = [
|
218
|
+
dom = []
|
213
219
|
moy = 1..12
|
214
220
|
nth = nil
|
215
221
|
|
216
|
-
specifier = (self['specifier'])? self['specifier'] : []
|
217
|
-
|
218
222
|
# Deconstruct specifier
|
219
|
-
specifier.split(';').each do |k|
|
223
|
+
self['specifier'].split(';').each do |k|
|
220
224
|
j = k.split('=')
|
221
225
|
|
222
226
|
# D=Day of the week, M=Day of the month, O=Month of the year, S=Nth
|
@@ -230,38 +234,55 @@ module ICalPal
|
|
230
234
|
end
|
231
235
|
|
232
236
|
# Build array of DOWs
|
233
|
-
dows = [
|
234
|
-
dow.each
|
237
|
+
dows = []
|
238
|
+
dow.each do |d|
|
239
|
+
dows.push(DOW[d[-2..].to_sym])
|
240
|
+
nth = d[0..-3].to_i if [ '+', '-' ].include? d[0]
|
241
|
+
end
|
235
242
|
|
236
243
|
# Months of the year (O)
|
237
|
-
moy.each do |
|
238
|
-
|
244
|
+
moy.each do |mo|
|
245
|
+
m = mo.to_i
|
246
|
+
|
247
|
+
# Set dates to the first of <m>
|
248
|
+
nsdate = RDT.new(self['sdate'].year, m, 1, self['sdate'].hour, self['sdate'].minute, self['sdate'].second)
|
249
|
+
nedate = RDT.new(self['edate'].year, m, 1, self['edate'].hour, self['edate'].minute, self['edate'].second)
|
250
|
+
|
251
|
+
# ...but not in the past
|
252
|
+
nsdate >>= 12 if nsdate.month < m
|
253
|
+
nedate >>= 12 if nedate.month < m
|
254
|
+
|
255
|
+
next if nsdate > $opts[:to]
|
256
|
+
next if ((nedate >> 1) - 1) < $opts[:from]
|
239
257
|
|
240
|
-
|
241
|
-
nedate = RDT.new(self['edate'].year, m.to_i, 1)
|
258
|
+
c = clone
|
242
259
|
|
243
260
|
# Days of the month (M)
|
244
|
-
dom.each do |
|
245
|
-
|
261
|
+
dom.each do |day|
|
262
|
+
c['sdate'] = RDT.new(nsdate.year, nsdate.month, day.to_i)
|
263
|
+
c['edate'] = RDT.new(nedate.year, nedate.month, day.to_i)
|
246
264
|
|
247
|
-
|
248
|
-
self['edate'] = RDT.new(nedate.year, nedate.month, x.to_i)
|
249
|
-
occurrences.push(clone)
|
265
|
+
o.push(clone(c)) if in_window?(c['sdate'], c['edate'])
|
250
266
|
end
|
251
267
|
|
252
268
|
# Days of the week (D)
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
269
|
+
dows.each do |day|
|
270
|
+
if nth
|
271
|
+
c['sdate'] = ICalPal.nth(nth, day, nsdate)
|
272
|
+
c['edate'] = ICalPal.nth(nth, day, nedate)
|
273
|
+
else
|
274
|
+
diff = day - c['sdate'].wday
|
275
|
+
diff += 7 if diff.negative?
|
276
|
+
|
277
|
+
c['sdate'] += diff
|
278
|
+
c['edate'] += diff
|
279
|
+
end
|
280
|
+
|
281
|
+
o.push(clone(c)) if in_window?(c['sdate'], c['edate'])
|
261
282
|
end
|
262
283
|
end
|
263
284
|
|
264
|
-
|
285
|
+
o
|
265
286
|
end
|
266
287
|
|
267
288
|
# Apply frequency and interval
|
@@ -278,7 +299,7 @@ module ICalPal
|
|
278
299
|
when 'yearly' then self[d] >>= self['interval'] * 12
|
279
300
|
else $log.error("Unknown frequency: #{self['frequency']}")
|
280
301
|
end
|
281
|
-
end
|
302
|
+
end
|
282
303
|
end
|
283
304
|
|
284
305
|
# Check if an event starts or ends between from and to, or if it's
|
@@ -287,8 +308,8 @@ module ICalPal
|
|
287
308
|
# @param s [RDT] Event start
|
288
309
|
# @param e [RDT] Event end
|
289
310
|
# @return [Boolean]
|
290
|
-
def in_window?(s, e
|
291
|
-
if $opts[:
|
311
|
+
def in_window?(s, e)
|
312
|
+
if $opts[:now]
|
292
313
|
if ($now >= s && $now < e)
|
293
314
|
$log.debug("now: #{s} to #{e} vs. #{$now}")
|
294
315
|
true
|
data/lib/icalPal.rb
CHANGED
@@ -19,8 +19,7 @@ module ICalPal
|
|
19
19
|
# @return [Class] The subclass of ICalPal
|
20
20
|
def self.call(klass)
|
21
21
|
case klass
|
22
|
-
when 'accounts' then Store
|
23
|
-
when 'stores' then Store
|
22
|
+
when 'accounts', 'stores' then Store
|
24
23
|
when 'calendars' then Calendar
|
25
24
|
when 'events' then Event
|
26
25
|
when 'tasks' then Reminder
|
@@ -32,7 +31,7 @@ module ICalPal
|
|
32
31
|
|
33
32
|
# Load data
|
34
33
|
def self.load_data(db_file, q)
|
35
|
-
$log.debug(q.gsub(
|
34
|
+
$log.debug(q.gsub("\n", ' '))
|
36
35
|
|
37
36
|
rows = []
|
38
37
|
|
@@ -93,8 +92,7 @@ module ICalPal
|
|
93
92
|
# @param headers [Array] Key names used as the header row in a CSV::Table
|
94
93
|
# @return [CSV::Row] The +Store+, +Calendar+, or +CalendarItem+ as a CSV::Row
|
95
94
|
def to_csv(headers)
|
96
|
-
values = []
|
97
|
-
headers.each { |h| values.push((@self[h].respond_to?(:gsub))? @self[h].gsub(/\n/, '\n') : @self[h]) }
|
95
|
+
values = headers.map { |h| (@self[h].respond_to?(:gsub))? @self[h].gsub("\n", '\n') : @self[h] }
|
98
96
|
|
99
97
|
CSV::Row.new(headers, values)
|
100
98
|
end
|
@@ -110,48 +108,17 @@ module ICalPal
|
|
110
108
|
retval
|
111
109
|
end
|
112
110
|
|
113
|
-
# Convert a key/value pair to XML. The value should be +nil+, +String+,
|
114
|
-
# +Integer+, +Array+, or +ICalPal::RDT+
|
115
|
-
#
|
116
|
-
# @param key The key
|
117
|
-
# @param value The value
|
118
|
-
# @return [String] The key/value pair in a simple XML format
|
119
|
-
def xmlify(key, value)
|
120
|
-
case value
|
121
|
-
# Nil
|
122
|
-
when NilClass then "<#{key}/>"
|
123
|
-
|
124
|
-
# String, Integer
|
125
|
-
when String then "<#{key}>#{value}</#{key}>"
|
126
|
-
when Integer then "<#{key}>#{value}</#{key}>"
|
127
|
-
|
128
|
-
# Array
|
129
|
-
when Array
|
130
|
-
# Treat empty arrays as nil values
|
131
|
-
xmlify(key, nil) if value[0].nil?
|
132
|
-
|
133
|
-
retval = ''
|
134
|
-
value.each { |x| retval += xmlify("#{key}0", x) }
|
135
|
-
"<#{key}>#{retval}</#{key}>"
|
136
|
-
|
137
|
-
# RDT
|
138
|
-
when ICalPal::RDT then "<#{key}>#{value}</#{key}>"
|
139
|
-
|
140
|
-
# Unknown
|
141
|
-
else "<#{key}>#{value}</#{key}>"
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
111
|
# Get the +n+'th +dow+ in month +m+
|
146
112
|
#
|
147
113
|
# @param n [Integer] Integer between -4 and +4
|
148
|
-
# @param dow [
|
114
|
+
# @param dow [Integer] Day of the week
|
149
115
|
# @param m [RDT] The RDT with the year and month we're searching
|
150
116
|
# @return [RDT] The resulting day
|
151
117
|
def self.nth(n, dow, m)
|
152
|
-
# Get the number of days in the month
|
153
|
-
|
154
|
-
a
|
118
|
+
# Get the number of days in the month by advancing to the first of
|
119
|
+
# the next month, then going back one day
|
120
|
+
a = [ RDT.new(m.year, m.month, 1, m.hour, m.minute, m.second) ]
|
121
|
+
a[1] = (a[0] >> 1) - 1
|
155
122
|
|
156
123
|
# Reverse it if going backwards
|
157
124
|
a.reverse! if n.negative?
|
@@ -159,7 +126,7 @@ module ICalPal
|
|
159
126
|
|
160
127
|
j = 0
|
161
128
|
a[0].step(a[1], step) do |i|
|
162
|
-
j += step if dow
|
129
|
+
j += step if dow == i.wday
|
163
130
|
return i if j == n
|
164
131
|
end
|
165
132
|
end
|
@@ -170,7 +137,7 @@ module ICalPal
|
|
170
137
|
# Days of the week abbreviations used in recurrence rules
|
171
138
|
#
|
172
139
|
# <tt><i>SU, MO, TU, WE, TH, FR, SA</i></tt>
|
173
|
-
DOW = {
|
140
|
+
DOW = { SU: 0, MO: 1, TU: 2, WE: 3, TH: 4, FR: 5, SA: 6 }.freeze
|
174
141
|
|
175
142
|
# @!group Accessors
|
176
143
|
def [](k)
|
@@ -188,5 +155,13 @@ module ICalPal
|
|
188
155
|
def values
|
189
156
|
@self.values
|
190
157
|
end
|
158
|
+
|
159
|
+
# Like inspect, but easier for humans to read
|
160
|
+
#
|
161
|
+
# @return [Array<String>] @self as a key=value array, sorted by key
|
162
|
+
def dump
|
163
|
+
@self.keys.sort.map { |k| "#{k}: #{@self[k]}" }
|
164
|
+
end
|
165
|
+
|
191
166
|
# @!endgroup
|
192
167
|
end
|
data/lib/options.rb
CHANGED
@@ -27,9 +27,9 @@ module ICalPal
|
|
27
27
|
@op = OptionParser.new
|
28
28
|
@op.summary_width = 23
|
29
29
|
@op.banner += ' [-c] COMMAND'
|
30
|
-
@op.version =
|
30
|
+
@op.version = VERSION
|
31
31
|
|
32
|
-
@op.accept(
|
32
|
+
@op.accept(RDT) { |s| RDT.conv(s) }
|
33
33
|
|
34
34
|
# head
|
35
35
|
@op.on_head("\nCOMMAND must be one of the following:\n\n")
|
@@ -83,8 +83,8 @@ module ICalPal
|
|
83
83
|
# dates
|
84
84
|
@op.separator("\nChoosing dates:\n\n")
|
85
85
|
|
86
|
-
@op.on('--from=DATE',
|
87
|
-
@op.on('--to=DATE',
|
86
|
+
@op.on('--from=DATE', RDT, 'List events starting on or after DATE')
|
87
|
+
@op.on('--to=DATE', RDT, 'List events starting on or before DATE',
|
88
88
|
'DATE can be yesterday, today, tomorrow, +N, -N, or anything accepted by DateTime.parse()',
|
89
89
|
'See https://ruby-doc.org/stdlib-2.6.1/libdoc/date/rdoc/DateTime.html#method-c-parse')
|
90
90
|
@op.separator('')
|
@@ -172,6 +172,14 @@ module ICalPal
|
|
172
172
|
@op.on_tail('%s%s %sAdditional arguments' % pad('ICALPAL'))
|
173
173
|
@op.on_tail('%s%s %sAdditional arguments from a file' % pad('ICALPAL_CONFIG'))
|
174
174
|
@op.on_tail("%s%s %s(default: #{$defaults[:common][:cf]})" % pad(''))
|
175
|
+
|
176
|
+
@op.on_tail('')
|
177
|
+
|
178
|
+
note = 'Do not quote or escape values.'
|
179
|
+
note += ' Options set in ICALPAL override ICALPAL_CONFIG.'
|
180
|
+
note += ' Options on the command line override ICALPAL.'
|
181
|
+
|
182
|
+
@op.on_tail("#{@op.summary_indent}#{note}")
|
175
183
|
end
|
176
184
|
|
177
185
|
# Parse options from the CLI and merge them with other sources
|
@@ -184,11 +192,45 @@ module ICalPal
|
|
184
192
|
env = {}
|
185
193
|
cf = {}
|
186
194
|
|
187
|
-
# Load from CLI
|
195
|
+
# Load from CLI
|
188
196
|
@op.parse!(into: cli)
|
189
|
-
|
190
|
-
|
191
|
-
|
197
|
+
|
198
|
+
# Environment variable needs special parsing.
|
199
|
+
# OptionParser.parse doesn't handle whitespace in a
|
200
|
+
# comma-separated value.
|
201
|
+
begin
|
202
|
+
o = []
|
203
|
+
|
204
|
+
ENV['ICALPAL'].gsub(/^-/, ' -').split(' -').each do |e|
|
205
|
+
a = e.split(' ', 2)
|
206
|
+
|
207
|
+
if a[0]
|
208
|
+
o.push("-#{a[0]}")
|
209
|
+
o.push(a[1]) if a[1]
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
@op.parse!(o, into: env)
|
214
|
+
end if ENV['ICALPAL']
|
215
|
+
|
216
|
+
# Configuration file needs special parsing for the same reason
|
217
|
+
begin
|
218
|
+
o = []
|
219
|
+
|
220
|
+
cli[:cf] ||= ENV['ICALPAL_CONFIG'] || $defaults[:common][:cf]
|
221
|
+
|
222
|
+
File.read(File.expand_path(cli[:cf])).split("\n").each do |line|
|
223
|
+
a = line.split(' ', 2)
|
224
|
+
|
225
|
+
if a[0] && a[0][0] != '#'
|
226
|
+
o.push(a[0])
|
227
|
+
o.push(a[1]) if a[1]
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
@op.parse!(o, into: cf)
|
232
|
+
rescue StandardError
|
233
|
+
end
|
192
234
|
|
193
235
|
cli[:cmd] ||= @op.default_argv[0]
|
194
236
|
cli[:cmd] ||= env[:cmd] if env[:cmd]
|
@@ -197,7 +239,7 @@ module ICalPal
|
|
197
239
|
|
198
240
|
# Parse eventsNow and eventsToday commands
|
199
241
|
cli[:cmd].match('events(Now|Today)(\+[0-9]+)?') do |m|
|
200
|
-
cli[:
|
242
|
+
cli[:now] = true if m[1] == 'Now'
|
201
243
|
cli[:days] = (m[1] == 'Today')? m[2].to_i + 1 : 1
|
202
244
|
|
203
245
|
cli[:from] = $today
|
@@ -244,6 +286,7 @@ module ICalPal
|
|
244
286
|
opts[:to] += 1 if opts[:to]
|
245
287
|
opts[:to] ||= opts[:from] + 1 if opts[:from]
|
246
288
|
opts[:to] = opts[:from] + opts[:days] if opts[:days]
|
289
|
+
opts[:to] = RDT.new(*opts[:to].to_a[0..2] + [ 23, 59, 59 ])
|
247
290
|
opts[:days] ||= Integer(opts[:to] - opts[:from])
|
248
291
|
opts[:from] = $now if opts[:n]
|
249
292
|
end
|