doing 2.1.39 → 2.1.40

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.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +1 -1
  5. data/bin/commands/config.rb +43 -34
  6. data/bin/commands/done.rb +1 -18
  7. data/bin/commands/finish.rb +30 -25
  8. data/bin/commands/grep.rb +3 -14
  9. data/bin/commands/last.rb +2 -8
  10. data/bin/commands/meanwhile.rb +13 -6
  11. data/bin/commands/on.rb +3 -16
  12. data/bin/commands/recent.rb +2 -8
  13. data/bin/commands/reset.rb +24 -1
  14. data/bin/commands/select.rb +1 -1
  15. data/bin/commands/show.rb +6 -17
  16. data/bin/commands/since.rb +1 -12
  17. data/bin/commands/today.rb +2 -13
  18. data/bin/commands/view.rb +1 -1
  19. data/bin/commands/yesterday.rb +2 -13
  20. data/bin/doing +15 -8
  21. data/docs/doc/Array.html +1 -1
  22. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  23. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  24. data/docs/doc/BooleanTermParser/Query.html +1 -1
  25. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  26. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  27. data/docs/doc/BooleanTermParser.html +1 -1
  28. data/docs/doc/Doing/Color.html +166 -20
  29. data/docs/doc/Doing/Completion.html +1 -1
  30. data/docs/doc/Doing/Configuration.html +1 -1
  31. data/docs/doc/Doing/Errors/DoingNoTraceError.html +7 -3
  32. data/docs/doc/Doing/Errors/DoingRuntimeError.html +7 -3
  33. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  34. data/docs/doc/Doing/Errors/EmptyInput.html +10 -2
  35. data/docs/doc/Doing/Errors/HistoryLimitError.html +194 -0
  36. data/docs/doc/Doing/Errors/InvalidPlugin.html +194 -0
  37. data/docs/doc/Doing/Errors/MissingBackupFile.html +194 -0
  38. data/docs/doc/Doing/Errors/NoResults.html +10 -2
  39. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  40. data/docs/doc/Doing/Errors/UserCancelled.html +10 -2
  41. data/docs/doc/Doing/Errors/WrongCommand.html +10 -2
  42. data/docs/doc/Doing/Errors.html +9 -9
  43. data/docs/doc/Doing/Hooks.html +1 -1
  44. data/docs/doc/Doing/Item.html +90 -1615
  45. data/docs/doc/Doing/Items.html +121 -5
  46. data/docs/doc/Doing/Logger.html +1 -1
  47. data/docs/doc/Doing/Note.html +1 -1
  48. data/docs/doc/Doing/Pager.html +1 -1
  49. data/docs/doc/Doing/Plugins.html +1 -1
  50. data/docs/doc/Doing/Prompt.html +2 -2
  51. data/docs/doc/Doing/Section.html +1 -1
  52. data/docs/doc/Doing/TemplateString.html +2 -2
  53. data/docs/doc/Doing/Types.html +1 -1
  54. data/docs/doc/Doing/Util/Backup.html +5 -5
  55. data/docs/doc/Doing/Util.html +1 -1
  56. data/docs/doc/Doing/WWID.html +197 -4033
  57. data/docs/doc/Doing.html +2 -2
  58. data/docs/doc/FalseClass.html +1 -1
  59. data/docs/doc/GLI/Commands/Help.html +1 -1
  60. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  61. data/docs/doc/GLI/Commands.html +1 -1
  62. data/docs/doc/GLI.html +1 -1
  63. data/docs/doc/Hash.html +1 -1
  64. data/docs/doc/Object.html +1 -1
  65. data/docs/doc/PhraseParser/Operator.html +1 -1
  66. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  67. data/docs/doc/PhraseParser/Query.html +1 -1
  68. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  69. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  70. data/docs/doc/PhraseParser/TermClause.html +1 -1
  71. data/docs/doc/PhraseParser.html +1 -1
  72. data/docs/doc/Status.html +1 -1
  73. data/docs/doc/String.html +1 -1
  74. data/docs/doc/Symbol.html +1 -1
  75. data/docs/doc/Time.html +1 -1
  76. data/docs/doc/TrueClass.html +1 -1
  77. data/docs/doc/_index.html +26 -5
  78. data/docs/doc/class_list.html +1 -1
  79. data/docs/doc/file.README.html +2 -2
  80. data/docs/doc/index.html +2 -2
  81. data/docs/doc/method_list.html +293 -773
  82. data/docs/doc/top-level-namespace.html +3 -3
  83. data/docs/index.md +1 -1
  84. data/doing.rdoc +49 -7
  85. data/lib/completion/_doing.zsh +5 -5
  86. data/lib/completion/doing.bash +8 -8
  87. data/lib/completion/doing.fish +7 -2
  88. data/lib/doing/add_options.rb +31 -1
  89. data/lib/doing/chronify/array.rb +64 -22
  90. data/lib/doing/colors.rb +77 -30
  91. data/lib/doing/completion.rb +4 -5
  92. data/lib/doing/errors.rb +51 -35
  93. data/lib/doing/hooks.rb +3 -3
  94. data/lib/doing/item/dates.rb +112 -0
  95. data/lib/doing/item/query.rb +433 -0
  96. data/lib/doing/item/state.rb +59 -0
  97. data/lib/doing/item/tags.rb +87 -0
  98. data/lib/doing/item.rb +6 -667
  99. data/lib/doing/items.rb +38 -13
  100. data/lib/doing/plugin_manager.rb +3 -3
  101. data/lib/doing/plugins/export/template_export.rb +4 -4
  102. data/lib/doing/plugins/import/cal_to_json.scpt +0 -0
  103. data/lib/doing/util_backup.rb +6 -8
  104. data/lib/doing/version.rb +1 -1
  105. data/lib/doing/wwid/display.rb +399 -0
  106. data/lib/doing/wwid/editor.rb +214 -0
  107. data/lib/doing/wwid/filetools.rb +186 -0
  108. data/lib/doing/wwid/filter.rb +218 -0
  109. data/lib/doing/wwid/guess.rb +87 -0
  110. data/lib/doing/wwid/interactive.rb +385 -0
  111. data/lib/doing/wwid/modify.rb +618 -0
  112. data/lib/doing/wwid/tags.rb +54 -0
  113. data/lib/doing/wwid/timers.rb +345 -0
  114. data/lib/doing/wwid/wwidutil.rb +104 -0
  115. data/lib/doing/wwid.rb +31 -2317
  116. metadata +19 -2
data/lib/doing/item.rb CHANGED
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'item/dates'
4
+ require_relative 'item/tags'
5
+ require_relative 'item/state'
6
+ require_relative 'item/query'
7
+
3
8
  module Doing
4
9
  ##
5
10
  ## This class describes a single WWID item
@@ -29,57 +34,6 @@ module Doing
29
34
  @note = Note.new(note)
30
35
  end
31
36
 
32
- # def date=(new_date)
33
- # @date = new_date.is_a?(Time) ? new_date : Time.parse(new_date)
34
- # end
35
-
36
- ## If the entry doesn't have a @done date, return the elapsed time
37
- def duration
38
- return nil unless should_time? && should_finish?
39
-
40
- return nil if @title =~ /(?<=^| )@done\b/
41
-
42
- return Time.now - @date
43
- end
44
-
45
- ##
46
- ## Get the difference between the item's start date and
47
- ## the value of its @done tag (if present)
48
- ##
49
- ## @return Interval in seconds
50
- ##
51
- def interval
52
- @interval ||= calc_interval
53
- end
54
-
55
- ##
56
- ## Get the value of the item's @done tag
57
- ##
58
- ## @return [Time] @done value
59
- ##
60
- def end_date
61
- @end_date ||= Time.parse(Regexp.last_match(1)) if @title =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/
62
- end
63
-
64
- def calculate_end_date(opt)
65
- if opt[:took]
66
- if @date + opt[:took] > Time.now
67
- @date = Time.now - opt[:took]
68
- Time.now
69
- else
70
- @date + opt[:took]
71
- end
72
- elsif opt[:back]
73
- if opt[:back].is_a? Integer
74
- @date + opt[:back]
75
- else
76
- @date + (opt[:back] - @date)
77
- end
78
- else
79
- Time.now
80
- end
81
- end
82
-
83
37
  # Generate a hash that represents the entry
84
38
  #
85
39
  # @return [String] entry hash
@@ -107,312 +61,6 @@ module Doing
107
61
  true
108
62
  end
109
63
 
110
- ##
111
- ## Test if two items occur at the same time (same start date and equal duration)
112
- ##
113
- ## @param item_b [Item] The item to compare
114
- ##
115
- ## @return [Boolean] is equal?
116
- ##
117
- def same_time?(item_b)
118
- date == item_b.date ? interval == item_b.interval : false
119
- end
120
-
121
- ##
122
- ## Test if the interval between start date and @done
123
- ## value overlaps with another item's
124
- ##
125
- ## @param item_b [Item] The item to compare
126
- ##
127
- ## @return [Boolean] overlaps?
128
- ##
129
- def overlapping_time?(item_b)
130
- return true if same_time?(item_b)
131
-
132
- start_a = date
133
- a_interval = interval
134
- end_a = a_interval ? start_a + a_interval.to_i : start_a
135
- start_b = item_b.date
136
- b_interval = item_b.interval
137
- end_b = b_interval ? start_b + b_interval.to_i : start_b
138
- (start_a >= start_b && start_a <= end_b) || (end_a >= start_b && end_a <= end_b) || (start_a < start_b && end_a > end_b)
139
- end
140
-
141
- ##
142
- ## Updates the title of the Item by expanding natural
143
- ## language dates within configured date tags (tags
144
- ## whose value is expected to be a date)
145
- ##
146
- ## @param additional_tags An array of additional
147
- ## tag names to consider
148
- ## dates
149
- ##
150
- def expand_date_tags(additional_tags = nil)
151
- @title.expand_date_tags(additional_tags)
152
- end
153
-
154
- ##
155
- ## Add (or remove) tags from the title of the item
156
- ##
157
- ## @param tags [Array] The tags to apply
158
- ## @param options Additional options
159
- ##
160
- ## @option options :date [Boolean] Include timestamp?
161
- ## @option options :single [Boolean] Log as a single change?
162
- ## @option options :value [String] A value to include as @tag(value)
163
- ## @option options :remove [Boolean] if true remove instead of adding
164
- ## @option options :rename_to [String] if not nil, rename target tag to this tag name
165
- ## @option options :regex [Boolean] treat target tag string as regex pattern
166
- ## @option options :force [Boolean] with rename_to, add tag if it doesn't exist
167
- ##
168
- def tag(tags, **options)
169
- added = []
170
- removed = []
171
-
172
- date = options.fetch(:date, false)
173
- options[:value] ||= date ? Time.now.strftime('%F %R') : nil
174
- options.delete(:date)
175
-
176
- single = options.fetch(:single, false)
177
- options.delete(:single)
178
-
179
- tags = tags.to_tags if tags.is_a? ::String
180
-
181
- remove = options.fetch(:remove, false)
182
- tags.each do |tag|
183
- if tag =~ /^(\S+)\((.*?)\)$/
184
- m = Regexp.last_match
185
- tag = m[1]
186
- options[:value] ||= m[2]
187
- end
188
-
189
- bool = remove ? :and : :not
190
- if tags?(tag, bool) || options[:value]
191
- @title = @title.tag(tag, **options).strip
192
- remove ? removed.push(tag) : added.push(tag)
193
- end
194
- end
195
-
196
- Doing.logger.log_change(tags_added: added, tags_removed: removed, count: 1, item: self, single: single)
197
-
198
- self
199
- end
200
-
201
- ##
202
- ## Get a list of tags on the item
203
- ##
204
- ## @return [Array] array of tags (no values)
205
- ##
206
- def tags
207
- @title.scan(/(?<= |\A)@([^\s(]+)/).map { |tag| tag[0] }.sort.uniq
208
- end
209
-
210
- ##
211
- ## Return all tags including parenthetical values
212
- ##
213
- ## @return [Array<Array>] Array of array pairs,
214
- ## [[tag1, value], [tag2, value]]
215
- ##
216
- def tags_with_values
217
- @title.scan(/(?<= |\A)@([^\s(]+)(?:\((.*?)\))?/).map { |tag| [tag[0], tag[1]] }.sort.uniq
218
- end
219
-
220
- ##
221
- ## convert tags on item to an array with @ symbols removed
222
- ##
223
- ## @return [Array] array of tags
224
- ##
225
- def tag_array
226
- tags.tags_to_array
227
- end
228
-
229
- ##
230
- ## Test if item contains tag(s)
231
- ##
232
- ## @param tags (Array or String) The tags to test. Can be an array or a comma-separated string.
233
- ## @param bool (Symbol) The boolean to use for multiple tags (:and, :or, :not)
234
- ## @param negate [Boolean] negate the result?
235
- ##
236
- ## @return [Boolean] true if tag/bool combination passes
237
- ##
238
- def tags?(tags, bool = :and, negate: false)
239
- if bool == :pattern
240
- tags = tags.to_tags.tags_to_array.join(' ')
241
- matches = tag_pattern?(tags)
242
-
243
- return negate ? !matches : matches
244
- end
245
-
246
- tags = split_tags(tags)
247
- bool = bool.normalize_bool
248
-
249
- matches = case bool
250
- when :and
251
- all_tags?(tags)
252
- when :not
253
- no_tags?(tags)
254
- else
255
- any_tags?(tags)
256
- end
257
- negate ? !matches : matches
258
- end
259
-
260
- ##
261
- ## Test if item matches tag values
262
- ##
263
- ## @param queries (Array) The tag value queries to test
264
- ## @param bool (Symbol) The boolean to use for multiple tags (:and, :or, :not)
265
- ## @param negate [Boolean] negate the result?
266
- ##
267
- ## @return [Boolean] true if tag/bool combination passes
268
- ##
269
- def tag_values?(queries, bool = :and, negate: false)
270
- bool = bool.normalize_bool
271
-
272
- matches = case bool
273
- when :and
274
- all_values?(queries)
275
- when :not
276
- no_values?(queries)
277
- else
278
- any_values?(queries)
279
- end
280
- negate ? !matches : matches
281
- end
282
-
283
- ##
284
- ## Determine if case should be ignored for searches
285
- ##
286
- ## @param search [String] The search string
287
- ## @param case_type [Symbol] The case type
288
- ##
289
- ## @return [Boolean] case should be ignored
290
- ##
291
- def ignore_case(search, case_type)
292
- (case_type == :smart && search !~ /[A-Z]/) || case_type == :ignore
293
- end
294
-
295
- def highlight_search(search, distance: nil, negate: false, case_type: nil)
296
- prefs = Doing.setting('search', {})
297
- matching = prefs.fetch('matching', 'pattern').normalize_matching
298
- distance ||= prefs.fetch('distance', 3).to_i
299
- case_type ||= prefs.fetch('case', 'smart').normalize_case
300
- new_note = Note.new
301
-
302
- if search.rx? || matching == :fuzzy
303
- rx = search.to_rx(distance: distance, case_type: case_type)
304
- new_title = @title.gsub(rx) { |m| yellow(m) }
305
- new_note.add(@note.to_s.gsub(rx) { |m| yellow(m) })
306
- else
307
- query = search.strip.to_phrase_query
308
-
309
- if query[:must].nil? && query[:must_not].nil?
310
- query[:must] = query[:should]
311
- query[:should] = []
312
- end
313
- query[:must].concat(query[:should]).each do |s|
314
- rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
315
- new_title = @title.gsub(rx) { |m| yellow(m) }
316
- new_note.add(@note.to_s.gsub(rx) { |m| yellow(m) })
317
- end
318
- end
319
-
320
- Item.new(@date, new_title, @section, new_note)
321
- end
322
-
323
- ##
324
- ## Test if item matches search string
325
- ##
326
- ## @param search [String] The search string
327
- ## @param negate [Boolean] negate results
328
- ## @param case_type (Symbol) The case-sensitivity
329
- ## type (:sensitive,
330
- ## :ignore, :smart)
331
- ##
332
- ## @return [Boolean] matches search criteria
333
- ##
334
- def search(search, distance: nil, negate: false, case_type: nil)
335
- prefs = Doing.setting('search', {})
336
- matching = prefs.fetch('matching', 'pattern').normalize_matching
337
- distance ||= prefs.fetch('distance', 3).to_i
338
- case_type ||= prefs.fetch('case', 'smart').normalize_case
339
-
340
- if search.rx? || matching == :fuzzy
341
- matches = @title + @note.to_s =~ search.to_rx(distance: distance, case_type: case_type)
342
- else
343
- query = search.strip.to_phrase_query
344
-
345
- if query[:must].nil? && query[:must_not].nil?
346
- query[:must] = query[:should]
347
- query[:should] = []
348
- end
349
- matches = no_searches?(query[:must_not], case_type: case_type)
350
- matches &&= all_searches?(query[:must], case_type: case_type)
351
- matches &&= any_searches?(query[:should], case_type: case_type)
352
- end
353
- # if search =~ /(?<=\A| )[+-]\S/
354
- # else
355
- # text = @title + @note.to_s
356
- # matches = text =~ search.to_rx(distance: distance, case_type: case_type)
357
- # end
358
-
359
- # if search.rx? || !fuzzy
360
- # matches = text =~ search.to_rx(distance: distance, case_type: case_type)
361
- # else
362
- # distance = 0.25 if distance > 1
363
- # score = if (case_type == :smart && search !~ /[A-Z]/) || case_type == :ignore
364
- # text.downcase.pair_distance_similar(search.downcase)
365
- # else
366
- # score = text.pair_distance_similar(search)
367
- # end
368
-
369
- # if score >= distance
370
- # matches = true
371
- # Doing.logger.debug('Fuzzy Match:', %(#{@title}, "#{search}" #{score}))
372
- # end
373
- # end
374
-
375
- negate ? !matches : matches
376
- end
377
-
378
- ##
379
- ## Test if item has a @done tag
380
- ##
381
- ## @return [Boolean] true item has @done tag
382
- ##
383
- def finished?
384
- tags?('done')
385
- end
386
-
387
- ##
388
- ## Test if item does not contain @done tag
389
- ##
390
- ## @return [Boolean] true if item is missing @done tag
391
- ##
392
- def unfinished?
393
- tags?('done', negate: true)
394
- end
395
-
396
- ##
397
- ## Test if item is included in never_finish config and
398
- ## thus should not receive a @done tag
399
- ##
400
- ## @return [Boolean] item should receive @done tag
401
- ##
402
- def should_finish?
403
- should?('never_finish')
404
- end
405
-
406
- ##
407
- ## Test if item is included in never_time config and
408
- ## thus should not receive a date on the @done tag
409
- ##
410
- ## @return [Boolean] item should receive @done date
411
- ##
412
- def should_time?
413
- should?('never_time')
414
- end
415
-
416
64
  ##
417
65
  ## Move item from current section to destination section
418
66
  ##
@@ -465,320 +113,11 @@ module Doing
465
113
  # @private
466
114
  def inspect
467
115
  # %(<Doing::Item @date=#{@date} @title="#{@title}" @section:"#{@section}" @note:#{@note.to_s}>)
468
- %(<Doing::Item @date=#{@date}>)
116
+ %(<Doing::Item @date=#{@date.strftime('%F %T')} @section=#{@section} @title=#{@title.trunc(30)}>)
469
117
  end
470
118
 
471
119
  def clone
472
120
  Marshal.load(Marshal.dump(self))
473
121
  end
474
-
475
- private
476
-
477
- def should?(key)
478
- config = Doing.settings
479
- return true unless config[key].is_a?(Array)
480
-
481
- config[key].each do |tag|
482
- if tag =~ /^@/
483
- return false if tags?(tag.sub(/^@/, '').downcase)
484
- elsif section.downcase == tag.downcase
485
- return false
486
- end
487
- end
488
-
489
- true
490
- end
491
-
492
- def calc_interval
493
- return nil unless should_time? && should_finish?
494
-
495
- done = end_date
496
- return nil if done.nil?
497
-
498
- start = @date
499
-
500
- t = (done - start).to_i
501
- t.positive? ? t : nil
502
- end
503
-
504
- def all_searches?(searches, case_type: :smart)
505
- return true unless searches.good?
506
-
507
- text = @title + @note.to_s
508
- searches.each do |s|
509
- rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
510
- return false unless text =~ rx
511
- end
512
- true
513
- end
514
-
515
- def no_searches?(searches, case_type: :smart)
516
- return true unless searches.good?
517
-
518
- text = @title + @note.to_s
519
- searches.each do |s|
520
- rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
521
- return false if text =~ rx
522
- end
523
- true
524
- end
525
-
526
- def any_searches?(searches, case_type: :smart)
527
- return true unless searches.good?
528
-
529
- text = @title + @note.to_s
530
- searches.each do |s|
531
- rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
532
- return true if text =~ rx
533
- end
534
- false
535
- end
536
-
537
- def all_tags?(tags)
538
- return true unless tags.good?
539
-
540
- tags.each do |tag|
541
- if tag =~ /done/ && !should_finish?
542
- next
543
- else
544
- return false unless @title =~ /@#{tag.wildcard_to_rx}(?= |\(|\Z)/i
545
- end
546
- end
547
- true
548
- end
549
-
550
- def no_tags?(tags)
551
- return true unless tags.good?
552
-
553
- tags.each do |tag|
554
- if tag =~ /done/ && !should_finish?
555
- return false
556
- else
557
- return false if @title =~ /@#{tag.wildcard_to_rx}(?= |\(|\Z)/i
558
- end
559
- end
560
- true
561
- end
562
-
563
- def any_tags?(tags)
564
- return true unless tags.good?
565
-
566
- tags.each do |tag|
567
- if tag =~ /done/ && !should_finish?
568
- return true
569
- else
570
- return true if @title =~ /@#{tag.wildcard_to_rx}(?= |\(|\Z)/i
571
- end
572
- end
573
- false
574
- end
575
-
576
- def tag_pattern?(tags)
577
- query = tags.to_query
578
-
579
- no_tags?(query[:must_not]) && all_tags?(query[:must]) && any_tags?(query[:should])
580
- end
581
-
582
- def tag_value(tag)
583
- res = @title.match(/@#{tag.sub(/^@/, '').wildcard_to_rx}\((.*?)\)/)
584
- res ? res[1] : nil
585
- end
586
-
587
- def number_or_date(value)
588
- return nil unless value
589
-
590
- if value.strip =~ /^[0-9.]+%?$/
591
- value.strip.to_f
592
- else
593
- value.strip.chronify(guess: :end)
594
- end
595
- end
596
-
597
- def split_value_query(query)
598
- val_rx = /^(!)?@?(\S+) +(!?[<>=][=*]?|[$*^]=) +(.*?)$/
599
- query.match(val_rx)
600
- end
601
-
602
- def any_values?(queries)
603
- return true unless queries.good?
604
-
605
- queries.each do |q|
606
- parts = split_value_query(q)
607
- return true if tag_value_matches?(parts[2], parts[3], parts[4], parts[1])
608
- end
609
- false
610
- end
611
-
612
- def all_values?(queries)
613
- return true unless queries.good?
614
-
615
- queries.each do |q|
616
- parts = split_value_query(q)
617
-
618
- return false unless tag_value_matches?(parts[2], parts[3], parts[4], parts[1])
619
- end
620
- true
621
- end
622
-
623
- def no_values?(queries)
624
- return true unless queries.good?
625
-
626
- queries.each do |q|
627
- parts = split_value_query(q)
628
- return false if tag_value_matches?(parts[2], parts[3], parts[4], parts[1])
629
- end
630
- true
631
- end
632
-
633
- def duration_matches?(value, comp)
634
- return false if interval.nil?
635
-
636
- val = value.chronify_qty
637
- case comp
638
- when /^<$/
639
- interval < val
640
- when /^<=$/
641
- interval <= val
642
- when /^>$/
643
- interval > val
644
- when /^>=$/
645
- interval >= val
646
- when /^!=/
647
- interval != val
648
- when /^=/
649
- interval == val
650
- end
651
- end
652
-
653
- def date_matches?(value, comp)
654
- time_rx = /^(\d{1,2}+(:\d{1,2}+)?( *(am|pm))?|midnight|noon)$/i
655
- value = "#{@date.strftime('%Y-%m-%d')} #{value}" if value =~ time_rx
656
-
657
- val = value.chronify(guess: :begin)
658
- raise InvalidTimeExpression, "Unrecognized date/time expression (#{value})" if val.nil?
659
-
660
- case comp
661
- when /^<$/
662
- @date < val
663
- when /^<=$/
664
- @date <= val
665
- when /^>$/
666
- @date > val
667
- when /^>=$/
668
- @date >= val
669
- when /^!=/
670
- @date != val
671
- when /^=/
672
- @date == val
673
- end
674
- end
675
-
676
- def value_string_matches?(tag_val, comp, value)
677
- case comp
678
- when /\^=/
679
- tag_val =~ /^#{value.wildcard_to_rx}/i
680
- when /\$=/
681
- tag_val =~ /#{value.wildcard_to_rx}$/i
682
- when %r{==}
683
- tag_val =~ /^#{value.wildcard_to_rx}$/i
684
- else
685
- tag_val =~ /#{value.wildcard_to_rx}/i
686
- end
687
- end
688
-
689
- def value_number_matches?(tag_val, comp, value)
690
- case comp
691
- when /^<$/
692
- tag_val < value
693
- when /^<=$/
694
- tag_val <= value
695
- when /^>$/
696
- tag_val > value
697
- when /^>=$/
698
- tag_val >= value
699
- when /^!=/
700
- tag_val != value
701
- when /^=/
702
- tag_val == value
703
- end
704
- end
705
-
706
- ##
707
- ## Test if a tag's value matches a given value. Value
708
- ## can be a date string, a text string, or a
709
- ## number/percentage. Type of comparison is determined
710
- ## by the comparitor and the objects being compared.
711
- ##
712
- ## @param tag [String] The tag name from which
713
- ## to get the value
714
- ## @param comp [String] The comparator (e.g. >=
715
- ## or *=)
716
- ## @param value [String] The value to test
717
- ## against
718
- ## @param negate [Boolean] Negate the response
719
- ##
720
- ## @return True if tag value matches, False otherwise.
721
- ##
722
- def tag_value_matches?(tag, comp, value, negate)
723
- # If tag matches existing tag
724
- if tags?(tag, :and)
725
- tag_val = tag_value(tag)
726
-
727
- # If the tag value is not a date and contains alpha
728
- # characters and comparison is ==, or comparison is
729
- # a string comparitor (*= ^= $=)
730
- if (value.chronify.nil? && value =~ /[a-z]/i && comp =~ /^!?==?$/) || comp =~ /[$*^]=/
731
- is_match = value_string_matches?(tag_val, comp, value)
732
-
733
- comp =~ /!/ || negate ? !is_match : is_match
734
- else
735
- # Convert values to either a number or a date
736
- tag_val = number_or_date(tag_val)
737
- val = number_or_date(value)
738
-
739
- # Fail if either value is nil
740
- return false if val.nil? || tag_val.nil?
741
-
742
- # Fail unless both values are of the same class (float or date)
743
- return false unless val.class == tag_val.class
744
-
745
- is_match = value_number_matches?(tag_val, comp, val)
746
-
747
- negate.nil? ? is_match : !is_match
748
- end
749
- # If tag name matches a trigger for elapsed time test
750
- elsif tag =~ /^(elapsed|dur(ation)?|int(erval)?)$/i
751
- is_match = duration_matches?(value, comp)
752
-
753
- comp =~ /!/ || negate ? !is_match : is_match
754
- # Else if tag name matches a trigger for start date
755
- elsif tag =~ /^(d(ate)?|t(ime)?)$/i
756
- is_match = date_matches?(value, comp)
757
-
758
- comp =~ /!/ || negate ? !is_match : is_match
759
- # Else if tag name matches a trigger for all text
760
- elsif tag =~ /^text$/i
761
- is_match = value_string_matches?([@title, @note.to_s(prefix: '')].join(' '), comp, value)
762
-
763
- comp =~ /!/ || negate ? !is_match : is_match
764
- # Else if tag name matches a trigger for title
765
- elsif tag =~ /^title$/i
766
- is_match = value_string_matches?(@title, comp, value)
767
-
768
- comp =~ /!/ || negate ? !is_match : is_match
769
- # Else if tag name matches a trigger for note
770
- elsif tag =~ /^note$/i
771
- is_match = value_string_matches?(@note.to_s(prefix: ''), comp, value)
772
-
773
- comp =~ /!/ || negate ? !is_match : is_match
774
- # Else if item contains tag being tested
775
- else
776
- false
777
- end
778
- end
779
-
780
- def split_tags(tags)
781
- tags.to_tags.tags_to_array
782
- end
783
122
  end
784
123
  end