icalPal 3.3.0 → 3.5.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',
@@ -26,6 +27,7 @@ $defaults = {
26
27
  is: [],
27
28
  it: [],
28
29
  li: 0,
30
+ norc: false,
29
31
  output: 'default',
30
32
  ps: [ "\n " ],
31
33
  r: false,
@@ -37,32 +39,38 @@ $defaults = {
37
39
  sp: false,
38
40
  tf: '%-I:%M %p',
39
41
  },
42
+
40
43
  tasks: {
41
44
  dated: 0,
42
45
  db: [ ICalPal::Reminder::DB_PATH ],
43
46
  iep: %w[ title notes due priority ],
44
47
  sort: 'prio',
45
48
  },
49
+
46
50
  undatedTasks: {
47
51
  dated: 1,
48
52
  db: [ ICalPal::Reminder::DB_PATH ],
49
53
  iep: %w[ title notes due priority ],
50
54
  sort: 'prio',
51
55
  },
56
+
52
57
  datedTasks: {
53
58
  dated: 2,
54
59
  db: [ ICalPal::Reminder::DB_PATH ],
55
60
  iep: %w[ title notes due priority ],
56
61
  sort: 'prio',
57
62
  },
63
+
58
64
  stores: {
59
65
  iep: %w[ account type ],
60
66
  sort: 'account',
61
67
  },
68
+
62
69
  calendars: {
63
70
  iep: %w[ calendar type UUID ],
64
71
  sort: 'calendar',
65
72
  },
73
+
66
74
  events: {
67
75
  days: nil,
68
76
  ea: false,
@@ -77,7 +85,7 @@ $defaults = {
77
85
  ps: [ "\n " ],
78
86
  sa: false,
79
87
  sed: false,
80
- sort: 'sdate',
88
+ sort: 'sctime',
81
89
  ss: "\n------------------------",
82
90
  to: nil,
83
91
  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'], self['edate'])
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
+ codate = Time.at(change['orig_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 codate == 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
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
@@ -113,13 +111,14 @@ module ICalPal
113
111
  # Get the +n+'th +dow+ in month +m+
114
112
  #
115
113
  # @param n [Integer] Integer between -4 and +4
116
- # @param dow [Array] Days of the week
114
+ # @param dow [Integer] Day of the week
117
115
  # @param m [RDT] The RDT with the year and month we're searching
118
116
  # @return [RDT] The resulting day
119
117
  def self.nth(n, dow, m)
120
- # Get the number of days in the month
121
- a = [ ICalPal::RDT.new(m.year, m.month, 1) ] # First of this month
122
- 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
123
122
 
124
123
  # Reverse it if going backwards
125
124
  a.reverse! if n.negative?
@@ -127,7 +126,7 @@ module ICalPal
127
126
 
128
127
  j = 0
129
128
  a[0].step(a[1], step) do |i|
130
- j += step if dow.any?(i.wday)
129
+ j += step if dow == i.wday
131
130
  return i if j == n
132
131
  end
133
132
  end
@@ -138,7 +137,7 @@ module ICalPal
138
137
  # Days of the week abbreviations used in recurrence rules
139
138
  #
140
139
  # <tt><i>SU, MO, TU, WE, TH, FR, SA</i></tt>
141
- 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
142
141
 
143
142
  # @!group Accessors
144
143
  def [](k)
@@ -156,5 +155,13 @@ module ICalPal
156
155
  def values
157
156
  @self.values
158
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
+
159
166
  # @!endgroup
160
167
  end