doing 2.1.11 → 2.1.15

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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/.irbrc +1 -0
  3. data/.yardoc/checksums +15 -13
  4. data/.yardoc/object_types +0 -0
  5. data/.yardoc/objects/root.dat +0 -0
  6. data/CHANGELOG.md +44 -1
  7. data/Gemfile.lock +9 -2
  8. data/README.md +56 -19
  9. data/bin/doing +215 -76
  10. data/docs/doc/Array.html +117 -3
  11. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  12. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  13. data/docs/doc/BooleanTermParser/Query.html +1 -1
  14. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  15. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  16. data/docs/doc/BooleanTermParser.html +1 -1
  17. data/docs/doc/Doing/Color.html +1 -1
  18. data/docs/doc/Doing/Completion.html +1 -1
  19. data/docs/doc/Doing/Configuration.html +7 -4
  20. data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  21. data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  22. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  23. data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
  24. data/docs/doc/Doing/Errors/NoResults.html +1 -1
  25. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  26. data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
  27. data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
  28. data/docs/doc/Doing/Errors.html +1 -1
  29. data/docs/doc/Doing/Hooks.html +1 -1
  30. data/docs/doc/Doing/Item.html +337 -14
  31. data/docs/doc/Doing/Items.html +66 -2
  32. data/docs/doc/Doing/LogAdapter.html +1 -1
  33. data/docs/doc/Doing/Note.html +2 -2
  34. data/docs/doc/Doing/Pager.html +1 -1
  35. data/docs/doc/Doing/Plugins.html +1 -1
  36. data/docs/doc/Doing/Prompt.html +35 -1
  37. data/docs/doc/Doing/Section.html +1 -1
  38. data/docs/doc/Doing/TemplateString.html +2 -2
  39. data/docs/doc/Doing/Util/Backup.html +84 -1
  40. data/docs/doc/Doing/Util.html +1 -1
  41. data/docs/doc/Doing/WWID.html +180 -35
  42. data/docs/doc/Doing.html +3 -3
  43. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  44. data/docs/doc/GLI/Commands.html +1 -1
  45. data/docs/doc/GLI.html +1 -1
  46. data/docs/doc/Hash.html +1 -1
  47. data/docs/doc/Numeric.html +279 -0
  48. data/docs/doc/PhraseParser/Operator.html +1 -1
  49. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  50. data/docs/doc/PhraseParser/Query.html +1 -1
  51. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  52. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  53. data/docs/doc/PhraseParser/TermClause.html +1 -1
  54. data/docs/doc/PhraseParser.html +1 -1
  55. data/docs/doc/Status.html +1 -1
  56. data/docs/doc/String.html +767 -115
  57. data/docs/doc/Symbol.html +1 -1
  58. data/docs/doc/Time.html +1 -1
  59. data/docs/doc/_index.html +14 -9
  60. data/docs/doc/class_list.html +1 -1
  61. data/docs/doc/file.README.html +41 -15
  62. data/docs/doc/index.html +41 -15
  63. data/docs/doc/method_list.html +407 -279
  64. data/docs/doc/top-level-namespace.html +2 -2
  65. data/docs/index.md +56 -19
  66. data/doing.gemspec +2 -0
  67. data/doing.rdoc +244 -45
  68. data/example_plugin.rb +2 -4
  69. data/lib/completion/_doing.zsh +31 -27
  70. data/lib/completion/doing.bash +50 -39
  71. data/lib/completion/doing.fish +35 -6
  72. data/lib/doing/array_chronify.rb +57 -0
  73. data/lib/doing/configuration.rb +4 -1
  74. data/lib/doing/item.rb +176 -0
  75. data/lib/doing/log_adapter.rb +1 -1
  76. data/lib/doing/numeric_chronify.rb +40 -0
  77. data/lib/doing/plugins/export/dayone_export.rb +1 -1
  78. data/lib/doing/plugins/export/json_export.rb +2 -2
  79. data/lib/doing/plugins/export/template_export.rb +47 -90
  80. data/lib/doing/plugins/import/calendar_import.rb +13 -1
  81. data/lib/doing/plugins/import/doing_import.rb +12 -1
  82. data/lib/doing/plugins/import/timing_import.rb +13 -1
  83. data/lib/doing/prompt.rb +13 -1
  84. data/lib/doing/string.rb +97 -33
  85. data/lib/doing/string_chronify.rb +83 -13
  86. data/lib/doing/time.rb +6 -6
  87. data/lib/doing/util_backup.rb +1 -1
  88. data/lib/doing/version.rb +1 -1
  89. data/lib/doing/wwid.rb +88 -93
  90. data/lib/doing.rb +31 -27
  91. data/lib/examples/plugins/say_export.rb +1 -4
  92. metadata +46 -2
@@ -5,6 +5,7 @@
5
5
  # author: Brett Terpstra
6
6
  # url: https://brettterpstra.com
7
7
  module Doing
8
+ # Template Export
8
9
  class TemplateExport
9
10
  include Doing::Color
10
11
  include Doing::Util
@@ -32,7 +33,7 @@ module Doing
32
33
 
33
34
  placeholders = {}
34
35
 
35
- if (!item.note.empty?) && wwid.config['include_notes']
36
+ if !item.note.empty? && wwid.config['include_notes']
36
37
  note = item.note.map(&:strip).delete_if(&:empty?)
37
38
  note.map! { |line| "#{line.sub(/^\t*/, '')} " }
38
39
 
@@ -42,122 +43,74 @@ module Doing
42
43
  line.simple_wrap(width)
43
44
  # line.chomp.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n")
44
45
  end
45
- note = note.delete_if(&:empty?)
46
+ note.delete_if(&:empty?)
46
47
  end
47
48
  else
48
49
  note = []
49
50
  end
50
51
 
51
- # output.sub!(/%(\d+)?date/) do
52
- # pad = Regexp.last_match(1).to_i
53
- # format("%#{pad}s", item.date.strftime(opt[:format]))
54
- # end
55
52
  placeholders['date'] = item.date.strftime(opt[:format])
56
53
 
57
54
  interval = wwid.get_interval(item, record: true, formatted: false) if opt[:times]
58
55
  if interval
59
- case opt[:interval_format].to_sym
60
- when :human
61
- _d, h, m = wwid.format_time(interval, human: true)
62
- interval = format('%<h> 4dh %<m>02dm', h: h, m: m)
63
- else
64
- d, h, m = wwid.format_time(interval)
65
- interval = format('%<d>02d:%<h>02d:%<m>02d', d: d, h: h, m: m)
66
- end
56
+ interval = case opt[:interval_format].to_sym
57
+ when :human
58
+ interval.time_string(format: :hm)
59
+ else
60
+ interval.time_string(format: :clock)
61
+ end
67
62
  end
68
63
 
69
64
  interval ||= ''
70
- # output.sub!(/%interval/, interval)
71
65
  placeholders['interval'] = interval
72
66
 
73
67
  duration = item.duration if opt[:duration]
74
68
  if duration
75
- case opt[:interval_format].to_sym
76
- when :human
77
- _d, h, m = wwid.format_time(duration, human: true)
78
- duration = format('%<h> 4dh %<m>02dm', h: h, m: m)
79
- else
80
- d, h, m = wwid.format_time(duration)
81
- duration = format('%<d>02d:%<h>02d:%<m>02d', d: d, h: h, m: m)
82
- end
69
+ duration = case opt[:interval_format].to_sym
70
+ when :human
71
+ duration.time_string(format: :hm)
72
+ else
73
+ duration.time_string(format: :clock)
74
+ end
83
75
  end
84
76
  duration ||= ''
85
- # output.sub!(/%duration/, duration)
86
77
  placeholders['duration'] = duration
87
78
 
88
- # output.sub!(/%(\d+)?shortdate/) do
89
- # pad = Regexp.last_match(1) || 13
90
- # format("%#{pad}s", item.date.relative_date)
91
- # end
92
- placeholders['shortdate'] = format("%13s", item.date.relative_date)
93
- # output.sub!(/%section/, item.section) if item.section
79
+ placeholders['shortdate'] = format('%13s', item.date.relative_date)
94
80
  placeholders['section'] = item.section || ''
95
81
  placeholders['title'] = item.title
96
-
97
- # title_rx = /(?mi)%(?<width>-?\d+)?(?:(?<ichar>[ _t])(?<icount>\d+))?(?<prefix>.[ _t]?)?title(?<after>.*?)$/
98
- # title_color = Doing::Color.reset + output.match(/(?mi)^(.*?)(%.*?title)/)[1].last_color
99
-
100
- # title_offset = Doing::Color.uncolor(output).match(title_rx).begin(0)
101
-
102
- # output.sub!(title_rx) do
103
- # m = Regexp.last_match
104
-
105
- # after = m['after']
106
- # pad = m['width'].to_i
107
- # indent = ''
108
- # if m['ichar']
109
- # char = m['ichar'] =~ /t/ ? "\t" : ' '
110
- # indent = char * m['icount'].to_i
111
- # end
112
- # prefix = m['prefix']
113
- # if opt[:wrap_width]&.positive? || pad.positive?
114
- # width = pad.positive? ? pad : opt[:wrap_width]
115
- # item.title.wrap(width, pad: pad, indent: indent, offset: title_offset, prefix: prefix, color: title_color, after: after, reset: reset)
116
- # # flag + item.title.gsub(/(.{#{opt[:wrap_width]}})(?=\s+|\Z)/, "\\1\n ").sub(/\s*$/, '') + reset
117
- # else
118
- # format("%s%#{pad}s%s", prefix, item.title.sub(/\s*$/, ''), after)
119
- # end
120
- # end
121
-
122
-
123
-
124
82
  placeholders['note'] = note
125
83
  placeholders['idnote'] = note.empty? ? '' : "\n#{note.map { |l| "\t\t#{l.strip} " }.join("\n")}"
126
84
  placeholders['odnote'] = note.empty? ? '' : "\n#{note.map { |l| "#{l.strip} " }.join("\n")}"
127
- placeholders['chompnote'] = note.empty? ? '' : note.map { |l| l.gsub(/\n+/, ' ').gsub(/(^\s*|\s*$)/, '').gsub(/\s+/, ' ') }.join(' ')
128
-
129
- # if note.empty?
130
- # output.gsub!(/%(chomp|[io]d|(\^.)?(([ _t]|[^a-z0-9])?\d+)?(.[ _t]?)?)?note/, '')
131
- # else
132
- # output.sub!(/%note/, "\n#{note.map { |l| "\t#{l.strip} " }.join("\n")}")
133
- # output.sub!(/%idnote/, "\n#{note.map { |l| "\t\t#{l.strip} " }.join("\n")}")
134
- # output.sub!(/%odnote/, "\n#{note.map { |l| "#{l.strip} " }.join("\n")}")
135
- # output.sub!(/(?mi)%(?:\^(?<mchar>.))?(?:(?<ichar>[ _t]|[^a-z0-9])?(?<icount>\d+))?(?<prefix>.[ _t]?)?note/) do
136
- # m = Regexp.last_match
137
- # mark = m['mchar'] || ''
138
- # indent = if m['ichar']
139
- # char = m['ichar'] =~ /t/ ? "\t" : ' '
140
- # char * m['icount'].to_i
141
- # else
142
- # ''
143
- # end
144
- # prefix = m['prefix'] || ''
145
- # "\n#{note.map { |l| "#{mark}#{indent}#{prefix}#{l.strip} " }.join("\n")}"
146
- # end
147
-
148
- # output.sub!(/%chompnote/) do
149
- # note.map { |l| l.gsub(/\n+/, ' ').gsub(/(^\s*|\s*$)/, '').gsub(/\s+/, ' ') }.join(' ')
150
- # end
151
- # end
152
85
 
153
- template = opt[:template].dup
154
- template.sub!(/(?i-m)^([\s\S]*?)(%(?:[io]d|(?:\^[\s\S])?(?:(?:[ _t]|[^a-z0-9])?\d+)?(?:[\s\S][ _t]?)?)?note)([\s\S]*?)$/, '\1\3\2')
155
- output = Doing::TemplateString.new(template, placeholders: placeholders, wrap_width: opt[:wrap_width], color: flag, tags_color: opt[:tags_color], reset: reset).colored
86
+ chompnote = []
87
+ unless note.empty?
88
+ chompnote = note.map do |l|
89
+ l.gsub(/\n+/, ' ').gsub(/(^\s*|\s*$)/, '').gsub(/\s+/, ' ')
90
+ end
91
+ end
92
+ placeholders['chompnote'] = chompnote.join(' ')
156
93
 
157
- output.gsub!(/(?<!\\)%hr(_under)?/) do
94
+ template = opt[:template].dup
95
+ note_rx = /(?i-m)(?x:^([\s\S]*?)
96
+ (%(?:[io]d|(?:\^[\s\S])?
97
+ (?:(?:[ _t]|[^a-z0-9])?\d+)?
98
+ (?:[\s\S][ _t]?)?)?note)
99
+ ([\s\S]*?)$)/
100
+ template.sub!(note_rx, '\1\3\2')
101
+ output = Doing::TemplateString.new(template,
102
+ color: flag,
103
+ placeholders: placeholders,
104
+ reset: reset,
105
+ tags_color: opt[:tags_color],
106
+ wrap_width: opt[:wrap_width]).colored
107
+
108
+ output.gsub!(/(?<!\\)%(\S)?hr(_under)?/) do
158
109
  o = ''
159
- `tput cols`.to_i.times do
160
- o += Regexp.last_match(1).nil? ? '-' : '_'
110
+ TTY::Screen.columns.to_i.times do
111
+ char = Regexp.last_match(2).nil? ? '-' : '_'
112
+ char = Regexp.last_match(1).nil? ? char : Regexp.last_match(1)
113
+ o += char
161
114
  end
162
115
  o
163
116
  end
@@ -170,7 +123,11 @@ module Doing
170
123
  end
171
124
 
172
125
  # Doing.logger.debug('Template Export:', "#{items.count} items output to template #{opt[:template]}")
173
- out += wwid.tag_times(format: wwid.config['timer_format'].to_sym, sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order]) if opt[:totals]
126
+ if opt[:totals]
127
+ out += wwid.tag_times(format: wwid.config['timer_format'].to_sym,
128
+ sort_by_name: opt[:sort_tags],
129
+ sort_order: opt[:tag_order])
130
+ end
174
131
  out
175
132
  end
176
133
 
@@ -61,7 +61,19 @@ module Doing
61
61
  title.strip!
62
62
  new_entry = Item.new(start_time, title, section)
63
63
  new_entry.note = entry['notes'].split(/\n/).map(&:chomp) if entry.key?('notes')
64
- new_items.push(new_entry)
64
+
65
+ is_match = true
66
+
67
+ if options[:search]
68
+ is_match = new_entry.search(options[:search], case_type: options[:date], negate: options[:not])
69
+ end
70
+
71
+ if is_match && options[:date_filter]
72
+ is_match = start_time > options[:date_filter][0] && start_time < options[:date_filter][1]
73
+ is_match = options[:not] ? !is_match : is_match
74
+ end
75
+
76
+ new_items.push(new_entry) if is_match
65
77
  end
66
78
  total = new_items.count
67
79
 
@@ -68,7 +68,18 @@ module Doing
68
68
  new_item = Item.new(item.date, title, section)
69
69
  new_item.note = item.note
70
70
 
71
- imported.push(new_item)
71
+ is_match = true
72
+
73
+ if options[:search]
74
+ is_match = new_item.search(options[:search], case_type: options[:case], negate: options[:not])
75
+ end
76
+
77
+ if is_match && options[:date_filter]
78
+ is_match = new_item.date > options[:date_filter][0] && new_item.date < options[:date_filter][1]
79
+ is_match = options[:not] ? !is_match : is_match
80
+ end
81
+
82
+ imported.push(new_item) if is_match
72
83
  end
73
84
 
74
85
  dups = new_items.count - imported.count
@@ -63,7 +63,19 @@ module Doing
63
63
  title.strip!
64
64
  new_item = Item.new(start_time, title, section)
65
65
  new_item.note.add(entry['notes']) if entry.key?('notes')
66
- new_items.push(new_item)
66
+
67
+ is_match = true
68
+
69
+ if options[:search]
70
+ is_match = new_item.search(options[:search], case_type: options[:case], negate: options[:not])
71
+ end
72
+
73
+ if is_match && options[:date_filter]
74
+ is_match = start_time > options[:date_filter][0] && start_time < options[:date_filter][1]
75
+ is_match = options[:not] ? !is_match : is_match
76
+ end
77
+
78
+ new_items.push(new_item) if is_match
67
79
  end
68
80
  total = new_items.count
69
81
  skipped = data.count - total
data/lib/doing/prompt.rb CHANGED
@@ -23,6 +23,18 @@ module Doing
23
23
  $stdin.gets.strip
24
24
  end
25
25
 
26
+ def request_lines(prompt: 'Enter text')
27
+ ask_note = []
28
+ reader = TTY::Reader.new(interrupt: -> { raise Errors::UserCancelled }, track_history: false)
29
+ puts "#{boldgreen(prompt.sub(/:?$/, ':'))} #{yellow('Hit return for a new line, ')}#{boldwhite('enter a blank line (')}#{boldyellow('return twice')}#{boldwhite(') to end editing')}"
30
+ loop do
31
+ res = reader.read_line(green('> '))
32
+ break if res.strip.empty?
33
+
34
+ ask_note.push(res)
35
+ end
36
+ ask_note.join("\n").strip
37
+ end
26
38
 
27
39
  ##
28
40
  ## Ask a yes or no question in the terminal
@@ -205,7 +217,7 @@ module Doing
205
217
  out = [
206
218
  format("%#{pad}d", i),
207
219
  ') ',
208
- format('%13s', item.date.relative_date),
220
+ format('%16s', item.date.strftime('%Y-%m-%d %H:%M')),
209
221
  ' | ',
210
222
  item.title
211
223
  ]
data/lib/doing/string.rb CHANGED
@@ -259,7 +259,13 @@ module Doing
259
259
  end
260
260
  end
261
261
 
262
- def pluralize(number)
262
+ ##
263
+ ## Pluralize a string based on quantity
264
+ ##
265
+ ## @param number [Integer] the quantity of the
266
+ ## object the string represents
267
+ ##
268
+ def to_p(number)
263
269
  number == 1 ? self : "#{self}s"
264
270
  end
265
271
 
@@ -268,10 +274,6 @@ module Doing
268
274
  ##
269
275
  ## @return [Symbol] :oldest or :newest
270
276
  ##
271
- def normalize_age!(default = :newest)
272
- replace normalize_age(default)
273
- end
274
-
275
277
  def normalize_age(default = :newest)
276
278
  case self
277
279
  when /^o/i
@@ -283,6 +285,11 @@ module Doing
283
285
  end
284
286
  end
285
287
 
288
+ ## @see #normalize_age
289
+ def normalize_age!(default = :newest)
290
+ replace normalize_age(default)
291
+ end
292
+
286
293
  ##
287
294
  ## Convert a sort order string to a qualified type
288
295
  ##
@@ -308,10 +315,6 @@ module Doing
308
315
  ##
309
316
  ## @return Symbol :smart, :sensitive, :ignore
310
317
  ##
311
- def normalize_case!
312
- replace normalize_case
313
- end
314
-
315
318
  def normalize_case(default = :smart)
316
319
  case self
317
320
  when /^(c|sens)/i
@@ -325,15 +328,16 @@ module Doing
325
328
  end
326
329
  end
327
330
 
331
+ ## @see #normalize_case
332
+ def normalize_case!
333
+ replace normalize_case
334
+ end
335
+
328
336
  ##
329
337
  ## Convert a boolean string to a symbol
330
338
  ##
331
339
  ## @return Symbol :and, :or, or :not
332
340
  ##
333
- def normalize_bool!(default = :and)
334
- replace normalize_bool(default)
335
- end
336
-
337
341
  def normalize_bool(default = :and)
338
342
  case self
339
343
  when /(and|all)/i
@@ -349,15 +353,19 @@ module Doing
349
353
  end
350
354
  end
351
355
 
356
+ ## @see #normalize_bool
357
+ def normalize_bool!(default = :and)
358
+ replace normalize_bool(default)
359
+ end
360
+
352
361
  ##
353
362
  ## Convert a matching configuration string to a symbol
354
363
  ##
364
+ ## @param default [Symbol] the default matching
365
+ ## type to return if the string
366
+ ## doesn't match a known symbol
355
367
  ## @return Symbol :fuzzy, :pattern, :exact
356
368
  ##
357
- def normalize_matching!(default = :pattern)
358
- replace normalize_bool(default)
359
- end
360
-
361
369
  def normalize_matching(default = :pattern)
362
370
  case self
363
371
  when /^f/i
@@ -371,30 +379,64 @@ module Doing
371
379
  end
372
380
  end
373
381
 
374
- def normalize_trigger!
375
- replace normalize_trigger
382
+ ## @see #normalize_matching
383
+ def normalize_matching!(default = :pattern)
384
+ replace normalize_bool(default)
376
385
  end
377
386
 
387
+ ##
388
+ ## Adds ?: to any parentheticals in a regular expression
389
+ ## to avoid match groups
390
+ ##
391
+ ## @return [String] modified regular expression
392
+ ##
378
393
  def normalize_trigger
379
394
  gsub(/\((?!\?:)/, '(?:').downcase
380
395
  end
381
396
 
397
+ ## @see #normalize_trigger
398
+ def normalize_trigger!
399
+ replace normalize_trigger
400
+ end
401
+
402
+ ##
403
+ ## Convert ? and * wildcards to regular expressions.
404
+ ## Uses \S (non-whitespace) instead of . (any character)
405
+ ##
406
+ ## @return [String] Regular expression string
407
+ ##
382
408
  def wildcard_to_rx
383
409
  gsub(/\?/, '\S').gsub(/\*/, '\S*?')
384
410
  end
385
411
 
412
+ ##
413
+ ## Add @ prefix to string if needed, maintains +/- prefix
414
+ ##
415
+ ## @return [String] @string
416
+ ##
386
417
  def add_at
387
418
  strip.sub(/^([+-]*)@/, '\1')
388
419
  end
389
420
 
421
+ ##
422
+ ## Convert a list of tags to an array. Tags can be with
423
+ ## or without @ symbols, separated by any character, and
424
+ ## can include parenthetical values (with spaces)
425
+ ##
426
+ ## @return [Array] array of tags including @ symbols
427
+ ##
390
428
  def to_tags
391
- gsub(/ *, */, ' ').gsub(/ +/, ' ').split(/ /).sort.uniq.map(&:add_at)
392
- end
393
-
394
- def add_tags!(tags, remove: false)
395
- replace add_tags(tags, remove: remove)
429
+ gsub(/ *, */, ' ').scan(/(@?(?:\S+(?:\(.+\)))|@?(?:\S+))/).map(&:first).sort.uniq.map(&:add_at)
396
430
  end
397
431
 
432
+ ##
433
+ ## @brief Adds tags to a string
434
+ ##
435
+ ## @param tags [String or Array] List of tags to add. @ symbol optional
436
+ ## @param remove [Boolean] remove tags instead of adding
437
+ ##
438
+ ## @return [String] the tagged string
439
+ ##
398
440
  def add_tags(tags, remove: false)
399
441
  title = self.dup
400
442
  tags = tags.to_tags
@@ -402,6 +444,11 @@ module Doing
402
444
  title
403
445
  end
404
446
 
447
+ ## @see #add_tags
448
+ def add_tags!(tags, remove: false)
449
+ replace add_tags(tags, remove: remove)
450
+ end
451
+
405
452
  ##
406
453
  ## Add, rename, or remove a tag in place
407
454
  ##
@@ -484,10 +531,6 @@ module Doing
484
531
  ##
485
532
  ## @return Deduplicated string
486
533
  ##
487
- def dedup_tags!
488
- replace dedup_tags
489
- end
490
-
491
534
  def dedup_tags
492
535
  title = dup
493
536
  tags = title.scan(/(?<=\A| )(@(\S+?)(\([^)]+\))?)(?= |\Z)/).uniq
@@ -505,6 +548,11 @@ module Doing
505
548
  title
506
549
  end
507
550
 
551
+ ## @see #dedup_tags
552
+ def dedup_tags!
553
+ replace dedup_tags
554
+ end
555
+
508
556
  # Returns the last escape sequence from a string.
509
557
  #
510
558
  # Actually returns all escape codes, with the assumption
@@ -525,11 +573,9 @@ module Doing
525
573
  ##
526
574
  ## @param opt [Hash] Additional Options
527
575
  ##
528
- def link_urls!(**opt)
529
- fmt = opt.fetch(:format, :html)
530
- replace link_urls(format: fmt)
531
- end
532
-
576
+ ## @option opt [Symbol] :format can be :markdown or
577
+ ## :html (default)
578
+ ##
533
579
  def link_urls(**opt)
534
580
  fmt = opt.fetch(:format, :html)
535
581
  return self unless fmt
@@ -541,6 +587,12 @@ module Doing
541
587
  str.replace_qualified_urls(format: fmt).clean_unlinked_urls
542
588
  end
543
589
 
590
+ ## @see #link_urls
591
+ def link_urls!(**opt)
592
+ fmt = opt.fetch(:format, :html)
593
+ replace link_urls(format: fmt)
594
+ end
595
+
544
596
  # Remove <self-linked> formatting
545
597
  def remove_self_links
546
598
  gsub(/<(.*?)>/) do |match|
@@ -590,6 +642,18 @@ module Doing
590
642
  end
591
643
  end
592
644
 
645
+ ##
646
+ ## Convert a string value to an appropriate type. If
647
+ ## kind is not specified, '[one, two]' becomes an Array,
648
+ ## '1' becomes Integer, '1.5' becomes Float, 'true' or
649
+ ## 'yes' becomes TrueClass, 'false' or 'no' becomes
650
+ ## FalseClass.
651
+ ##
652
+ ## @param kind [String] specify string, array,
653
+ ## integer, float, symbol, or boolean
654
+ ## (falls back to string if value is
655
+ ## not recognized)
656
+ ## @return Converted object type
593
657
  def set_type(kind = nil)
594
658
  if kind
595
659
  case kind.to_s
@@ -64,22 +64,92 @@ module Doing
64
64
  when /^(\d+):(\d\d)$/
65
65
  minutes += Regexp.last_match(1).to_i * 60
66
66
  minutes += Regexp.last_match(2).to_i
67
- when /^(\d+(?:\.\d+)?)([hmd])?$/
68
- amt = Regexp.last_match(1)
69
- type = Regexp.last_match(2).nil? ? 'm' : Regexp.last_match(2)
67
+ when /^(\d+(?:\.\d+)?)([hmd])?/
68
+ scan(/(\d+(?:\.\d+)?)([hmd])?/).each do |m|
69
+ amt = m[0]
70
+ type = m[1].nil? ? 'm' : m[1]
70
71
 
71
- minutes = case type.downcase
72
- when 'm'
73
- amt.to_i
74
- when 'h'
75
- (amt.to_f * 60).round
76
- when 'd'
77
- (amt.to_f * 60 * 24).round
78
- else
79
- minutes
80
- end
72
+ minutes += case type.downcase
73
+ when 'm'
74
+ amt.to_i
75
+ when 'h'
76
+ (amt.to_f * 60).round
77
+ when 'd'
78
+ (amt.to_f * 60 * 24).round
79
+ else
80
+ 0
81
+ end
82
+ end
81
83
  end
82
84
  minutes * 60
83
85
  end
86
+
87
+ ##
88
+ ## Convert DD:HH:MM to seconds
89
+ ##
90
+ ## @return [Integer] rounded number of seconds
91
+ ##
92
+ def to_seconds
93
+ mtch = match(/(\d+):(\d+):(\d+)/)
94
+
95
+ raise Errors::DoingRuntimeError, "Invalid time string: #{self}" unless mtch
96
+
97
+ h = mtch[1]
98
+ m = mtch[2]
99
+ s = mtch[3]
100
+ (h.to_i * 60 * 60) + (m.to_i * 60) + s.to_i
101
+ end
102
+
103
+ ##
104
+ ## Convert DD:HH:MM to a natural language string
105
+ ##
106
+ ## @param format [Symbol] The format to output (:dhm, :hm, :m, :clock, :natural)
107
+ ##
108
+ def time_string(format: :dhm)
109
+ to_seconds.time_string(format: format)
110
+ end
111
+
112
+ ##
113
+ ## Convert (chronify) natural language dates
114
+ ## within configured date tags (tags whose value is
115
+ ## expected to be a date). Modifies string in place.
116
+ ##
117
+ ## @param additional_tags [Array] An array of
118
+ ## additional tags to
119
+ ## consider date_tags
120
+ ##
121
+ def expand_date_tags(additional_tags = nil)
122
+ iso_rx = /\d{4}-\d\d-\d\d \d\d:\d\d/
123
+
124
+ watch_tags = [
125
+ 'start(?:ed)?',
126
+ 'beg[ia]n',
127
+ 'done',
128
+ 'finished',
129
+ 'completed?',
130
+ 'waiting',
131
+ 'defer(?:red)?'
132
+ ]
133
+
134
+ if additional_tags
135
+ date_tags = additional_tags
136
+ date_tags = date_tags.split(/ *, */) if date_tags.is_a?(String)
137
+ date_tags.map! do |tag|
138
+ tag.sub(/^@/, '').gsub(/\((?!\?:)(.*?)\)/, '(?:\1)').strip
139
+ end
140
+ watch_tags.concat(date_tags).uniq!
141
+ end
142
+
143
+ done_rx = /(?<=^| )@(?<tag>#{watch_tags.join('|')})\((?<date>.*?)\)/i
144
+
145
+ gsub!(done_rx) do
146
+ m = Regexp.last_match
147
+ t = m['tag']
148
+ d = m['date']
149
+ future = t =~ /^(done|complete)/ ? false : true
150
+ parsed_date = d =~ iso_rx ? Time.parse(d) : d.chronify(guess: :begin, future: future)
151
+ parsed_date.nil? ? m[0] : "@#{t}(#{parsed_date.strftime('%F %R')})"
152
+ end
153
+ end
84
154
  end
85
155
  end
data/lib/doing/time.rb CHANGED
@@ -8,10 +8,10 @@ module Doing
8
8
  strftime('%_I:%M%P')
9
9
  elsif self > (Date.today - 6).to_time
10
10
  strftime('%a %_I:%M%P')
11
- elsif self.year == Date.today.year
11
+ elsif self.year == Date.today.year || (self.year + 1 == Date.today.year && self.month > Date.today.month)
12
12
  strftime('%m/%d %_I:%M%P')
13
13
  else
14
- strftime('%m/%d/%Y %_I:%M%P')
14
+ strftime('%m/%d/%y %_I:%M%P')
15
15
  end
16
16
  end
17
17
 
@@ -25,10 +25,10 @@ module Doing
25
25
  h = h % 24
26
26
 
27
27
  output = []
28
- output.push("#{d} #{'day'.pluralize(d)}") if d.positive?
29
- output.push("#{h} #{'hour'.pluralize(h)}") if h.positive?
30
- output.push("#{m} #{'minute'.pluralize(m)}") if m.positive?
31
- output.push("#{s} #{'second'.pluralize(s)}") if s.positive?
28
+ output.push("#{d} #{'day'.to_p(d)}") if d.positive?
29
+ output.push("#{h} #{'hour'.to_p(h)}") if h.positive?
30
+ output.push("#{m} #{'minute'.to_p(m)}") if m.positive?
31
+ output.push("#{s} #{'second'.to_p(s)}") if s.positive?
32
32
  output.join(', ')
33
33
  end
34
34
 
@@ -79,7 +79,7 @@ module Doing
79
79
  def redo_backup(filename = nil, count: 1)
80
80
  filename ||= Doing.config.settings['doing_file']
81
81
  # redo_file = File.join(backup_dir, "undone___#{File.basename(filename)}")
82
- undones = Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).sort
82
+ undones = Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).sort.reverse
83
83
  total = undones.count
84
84
  count = total if count > total
85
85
 
data/lib/doing/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '2.1.11'
2
+ VERSION = '2.1.15'
3
3
  end