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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/bin/commands/config.rb +43 -34
- data/bin/commands/done.rb +1 -18
- data/bin/commands/finish.rb +30 -25
- data/bin/commands/grep.rb +3 -14
- data/bin/commands/last.rb +2 -8
- data/bin/commands/meanwhile.rb +13 -6
- data/bin/commands/on.rb +3 -16
- data/bin/commands/recent.rb +2 -8
- data/bin/commands/reset.rb +24 -1
- data/bin/commands/select.rb +1 -1
- data/bin/commands/show.rb +6 -17
- data/bin/commands/since.rb +1 -12
- data/bin/commands/today.rb +2 -13
- data/bin/commands/view.rb +1 -1
- data/bin/commands/yesterday.rb +2 -13
- data/bin/doing +15 -8
- data/docs/doc/Array.html +1 -1
- data/docs/doc/BooleanTermParser/Clause.html +1 -1
- data/docs/doc/BooleanTermParser/Operator.html +1 -1
- data/docs/doc/BooleanTermParser/Query.html +1 -1
- data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
- data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
- data/docs/doc/BooleanTermParser.html +1 -1
- data/docs/doc/Doing/Color.html +166 -20
- data/docs/doc/Doing/Completion.html +1 -1
- data/docs/doc/Doing/Configuration.html +1 -1
- data/docs/doc/Doing/Errors/DoingNoTraceError.html +7 -3
- data/docs/doc/Doing/Errors/DoingRuntimeError.html +7 -3
- data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
- data/docs/doc/Doing/Errors/EmptyInput.html +10 -2
- data/docs/doc/Doing/Errors/HistoryLimitError.html +194 -0
- data/docs/doc/Doing/Errors/InvalidPlugin.html +194 -0
- data/docs/doc/Doing/Errors/MissingBackupFile.html +194 -0
- data/docs/doc/Doing/Errors/NoResults.html +10 -2
- data/docs/doc/Doing/Errors/PluginException.html +1 -1
- data/docs/doc/Doing/Errors/UserCancelled.html +10 -2
- data/docs/doc/Doing/Errors/WrongCommand.html +10 -2
- data/docs/doc/Doing/Errors.html +9 -9
- data/docs/doc/Doing/Hooks.html +1 -1
- data/docs/doc/Doing/Item.html +90 -1615
- data/docs/doc/Doing/Items.html +121 -5
- data/docs/doc/Doing/Logger.html +1 -1
- data/docs/doc/Doing/Note.html +1 -1
- data/docs/doc/Doing/Pager.html +1 -1
- data/docs/doc/Doing/Plugins.html +1 -1
- data/docs/doc/Doing/Prompt.html +2 -2
- data/docs/doc/Doing/Section.html +1 -1
- data/docs/doc/Doing/TemplateString.html +2 -2
- data/docs/doc/Doing/Types.html +1 -1
- data/docs/doc/Doing/Util/Backup.html +5 -5
- data/docs/doc/Doing/Util.html +1 -1
- data/docs/doc/Doing/WWID.html +197 -4033
- data/docs/doc/Doing.html +2 -2
- data/docs/doc/FalseClass.html +1 -1
- data/docs/doc/GLI/Commands/Help.html +1 -1
- data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
- data/docs/doc/GLI/Commands.html +1 -1
- data/docs/doc/GLI.html +1 -1
- data/docs/doc/Hash.html +1 -1
- data/docs/doc/Object.html +1 -1
- data/docs/doc/PhraseParser/Operator.html +1 -1
- data/docs/doc/PhraseParser/PhraseClause.html +1 -1
- data/docs/doc/PhraseParser/Query.html +1 -1
- data/docs/doc/PhraseParser/QueryParser.html +1 -1
- data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
- data/docs/doc/PhraseParser/TermClause.html +1 -1
- data/docs/doc/PhraseParser.html +1 -1
- data/docs/doc/Status.html +1 -1
- data/docs/doc/String.html +1 -1
- data/docs/doc/Symbol.html +1 -1
- data/docs/doc/Time.html +1 -1
- data/docs/doc/TrueClass.html +1 -1
- data/docs/doc/_index.html +26 -5
- data/docs/doc/class_list.html +1 -1
- data/docs/doc/file.README.html +2 -2
- data/docs/doc/index.html +2 -2
- data/docs/doc/method_list.html +293 -773
- data/docs/doc/top-level-namespace.html +3 -3
- data/docs/index.md +1 -1
- data/doing.rdoc +49 -7
- data/lib/completion/_doing.zsh +5 -5
- data/lib/completion/doing.bash +8 -8
- data/lib/completion/doing.fish +7 -2
- data/lib/doing/add_options.rb +31 -1
- data/lib/doing/chronify/array.rb +64 -22
- data/lib/doing/colors.rb +77 -30
- data/lib/doing/completion.rb +4 -5
- data/lib/doing/errors.rb +51 -35
- data/lib/doing/hooks.rb +3 -3
- data/lib/doing/item/dates.rb +112 -0
- data/lib/doing/item/query.rb +433 -0
- data/lib/doing/item/state.rb +59 -0
- data/lib/doing/item/tags.rb +87 -0
- data/lib/doing/item.rb +6 -667
- data/lib/doing/items.rb +38 -13
- data/lib/doing/plugin_manager.rb +3 -3
- data/lib/doing/plugins/export/template_export.rb +4 -4
- data/lib/doing/plugins/import/cal_to_json.scpt +0 -0
- data/lib/doing/util_backup.rb +6 -8
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid/display.rb +399 -0
- data/lib/doing/wwid/editor.rb +214 -0
- data/lib/doing/wwid/filetools.rb +186 -0
- data/lib/doing/wwid/filter.rb +218 -0
- data/lib/doing/wwid/guess.rb +87 -0
- data/lib/doing/wwid/interactive.rb +385 -0
- data/lib/doing/wwid/modify.rb +618 -0
- data/lib/doing/wwid/tags.rb +54 -0
- data/lib/doing/wwid/timers.rb +345 -0
- data/lib/doing/wwid/wwidutil.rb +104 -0
- data/lib/doing/wwid.rb +31 -2317
- 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
|