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.
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
- '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,
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
- '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',
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(opts)
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(/\n/, "\n ")
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 a [Array] Ignored
200
- def accept_list_end(a) end
196
+ # @param _a [Array] Ignored
197
+ def accept_list_end(_a) end
201
198
 
202
- # @param a [Array] Ignored
203
- def accept_list_item_end(a) 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
- $now = ICalPal::RDT.now
3
- $today = ICalPal::RDT.new(*$now.to_a[0..2] + [ 0, 0, 0, $now.zone ])
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: "#{ENV['HOME']}/.icalpal",
12
+ cf: "#{Dir.home}/.icalpal",
12
13
  color: false,
13
14
  db: [
14
- "#{ENV['HOME']}/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb",
15
- "#{ENV['HOME']}/Library/Calendars/Calendar.sqlitedb",
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: 'sdate',
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 'time'
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
- # +sdate+ will also set +sday+.
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
- @self['sday'] = ICalPal::RDT.new(*self['sdate'].to_a[0..2]) if k == 'sdate'
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['sdate'].strftime($opts[:tf])}" if @self['sdate']
40
- t += " - #{@self['edate'].strftime($opts[:tf])}" unless $opts[:eed] || !@self['edate']
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(/\n/, $opts[:nnr]) : nil
50
+ (@self['notes'])? @self['notes'].strip.gsub("\n", $opts[:nnr]) : nil
49
51
 
50
52
  when 'sday' # pseudo-property
51
- ICalPal::RDT.new(*@self['sdate'].to_a[0..2])
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
- k = RDT.new(*Time.at(k + ITIME).to_a.reverse[4..]) if k
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
- t = Time.at(obj[k] + ITIME) if obj[k]
95
- @self["#{k[0]}date"] = RDT.new(*t.to_a.reverse[4..], t.zone) if t
96
- end
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
- if @self['start_tz'] == '_float'
99
- tzoffset = Time.zone_offset($now.zone)
102
+ ctime = obj[k] + ITIME
100
103
 
101
- @self['sdate'] = RDT.new(*(@self['sdate'].to_time - tzoffset).to_a.reverse[4..], $now.zone)
102
- @self['edate'] = RDT.new(*(@self['edate'].to_time - tzoffset).to_a.reverse[4..], $now.zone)
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
- $log.debug("multi-day event #{i + 1}") if (i.positive?)
131
-
132
- self['daynum'] = i + 1
133
- events.push(clone) if in_window?(self['sdate'])
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
- if stop < $opts[:from]
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 = [ { 'orig_date' => -1 } ]
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 or clone self
169
- if self['specifier'] && self['specifier'].length.positive?
170
- occurrences = get_occurrences(changes)
171
- else
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
- occurrences.each do |occurrence|
177
- changes.each do |change|
178
- next if change['orig_date'] == self['sdate'].to_i - ITIME
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
- events.push(occurrence) if in_window?(occurrence['sdate'], occurrence['edate'])
183
+ skip = true if cdate == odate
181
184
  end
182
- end
183
185
 
184
- break if self['specifier']
186
+ events.push(clone(occurrence)) if in_window?(occurrence['sdate'], occurrence['edate']) && !skip
187
+ end
185
188
 
186
- apply_frequency!
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
- # @return a deep clone of self
200
- def clone
201
- Marshal.load(Marshal.dump(self))
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 occurences of a recurring event from a specifier
211
+ # Get next occurrences of a recurring event given a specifier
205
212
  #
206
- # @param _changes [Array] Recurrence changes for the event
207
- # @return [Array<IcalPal::Event>]
208
- def get_occurrences(_changes)
209
- occurrences = []
213
+ # @return [Array<ICalPal::Event>]
214
+ def occurrences
215
+ o = []
210
216
 
211
217
  dow = DOW.keys
212
- dom = [ nil ]
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 = [ nil ]
234
- dow.each { |d| dows.push(DOW[d[-2..].to_sym]) }
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 |m|
238
- next unless m
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
- nsdate = RDT.new(self['sdate'].year, m.to_i, 1)
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 |x|
245
- next unless x
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
- self['sdate'] = RDT.new(nsdate.year, nsdate.month, x.to_i)
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
- if nth
254
- self['sdate'] = ICalPal.nth(nth, dows, nsdate)
255
- self['edate'] = ICalPal.nth(nth, dows, nedate)
256
- occurrences.push(clone)
257
- elsif dows[0]
258
- self['sdate'] = RDT.new(nsdate.year, m.to_i, nsdate.wday)
259
- self['edate'] = RDT.new(nedate.year, m.to_i, nedate.wday)
260
- occurrences.push(clone)
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
- occurrences
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 if self['frequency'] && self['interval']
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 = s)
291
- if $opts[:n]
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(/\n/, ' '))
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 [Array] Days of the week
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
- a = [ ICalPal::RDT.new(m.year, m.month, 1) ] # First of this month
154
- a[1] = (a[0] >> 1) - 1 # First of next month, minus 1 day
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.any?(i.wday)
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 = { 'SU': 0, 'MO': 1, 'TU': 2, 'WE': 3, 'TH': 4, 'FR': 5, 'SA': 6 }.freeze
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 = ICalPal::VERSION
30
+ @op.version = VERSION
31
31
 
32
- @op.accept(ICalPal::RDT) { |s| ICalPal::RDT.conv(s) }
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', ICalPal::RDT, 'List events starting on or after DATE')
87
- @op.on('--to=DATE', ICalPal::RDT, 'List events starting on or before 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, environment, configuration file
195
+ # Load from CLI
188
196
  @op.parse!(into: cli)
189
- @op.parse!(ENV['ICALPAL'].split, into: env) rescue nil
190
- cli[:cf] ||= ENV['ICALPAL_CONFIG'] || $defaults[:common][:cf]
191
- @op.parse!(File.read(File.expand_path(cli[:cf])).split, into: cf) rescue nil
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[:n] = true if m[1] == 'Now'
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