doing 2.1.14 → 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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/.irbrc +1 -0
  3. data/.yardoc/checksums +11 -9
  4. data/.yardoc/object_types +0 -0
  5. data/.yardoc/objects/root.dat +0 -0
  6. data/CHANGELOG.md +18 -0
  7. data/Gemfile.lock +3 -2
  8. data/README.md +56 -19
  9. data/bin/doing +4 -3
  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 +5 -2
  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 +106 -2
  31. data/docs/doc/Doing/Items.html +2 -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 +1 -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 +1 -1
  40. data/docs/doc/Doing/Util.html +1 -1
  41. data/docs/doc/Doing/WWID.html +35 -65
  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 +357 -293
  64. data/docs/doc/top-level-namespace.html +2 -2
  65. data/docs/index.md +56 -19
  66. data/doing.gemspec +1 -0
  67. data/doing.rdoc +3 -3
  68. data/example_plugin.rb +2 -4
  69. data/lib/completion/_doing.zsh +3 -3
  70. data/lib/completion/doing.bash +4 -4
  71. data/lib/completion/doing.fish +2 -2
  72. data/lib/doing/array_chronify.rb +57 -0
  73. data/lib/doing/configuration.rb +4 -1
  74. data/lib/doing/item.rb +32 -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/string.rb +97 -33
  81. data/lib/doing/string_chronify.rb +83 -13
  82. data/lib/doing/time.rb +4 -4
  83. data/lib/doing/util_backup.rb +1 -1
  84. data/lib/doing/version.rb +1 -1
  85. data/lib/doing/wwid.rb +58 -83
  86. data/lib/doing.rb +30 -27
  87. data/lib/examples/plugins/say_export.rb +1 -4
  88. metadata +26 -2
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
@@ -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.14'
2
+ VERSION = '2.1.15'
3
3
  end
data/lib/doing/wwid.rb CHANGED
@@ -185,34 +185,9 @@ module Doing
185
185
 
186
186
  date = nil
187
187
  iso_rx = /\d{4}-\d\d-\d\d \d\d:\d\d/
188
- watch_tags = [
189
- 'start(?:ed)?',
190
- 'beg[ia]n',
191
- 'done',
192
- 'finished',
193
- 'completed?',
194
- 'waiting',
195
- 'defer(?:red)?'
196
- ]
197
- if @config['date_tags']
198
- date_tags = @config['date_tags']
199
- date_tags = date_tags.split(/ *, */) if date_tags.is_a?(String)
200
- date_tags.map! do |tag|
201
- tag.sub(/^@/, '').gsub(/\((?!\?:)(.*?)\)/, '(?:\1)').strip
202
- end
203
- watch_tags.concat(date_tags).uniq!
204
- end
205
-
206
- done_rx = /(?<=^| )@(?<tag>#{watch_tags.join('|')})\((?<date>.*?)\)/i
207
188
  date_rx = /^(?:\s*- )?(?<date>.*?) \| (?=\S)/
208
189
 
209
- title.gsub!(done_rx) do
210
- m = Regexp.last_match
211
- t = m['tag']
212
- d = m['date']
213
- parsed_date = d =~ date_rx ? Time.parse(d) : d.chronify(guess: :begin)
214
- parsed_date.nil? ? m[0] : "@#{t}(#{parsed_date.strftime('%F %R')})"
215
- end
190
+ title.expand_date_tags(@config['date_tags'])
216
191
 
217
192
  if title =~ date_rx
218
193
  m = title.match(date_rx)
@@ -369,7 +344,8 @@ module Doing
369
344
  items.each_with_index do |i, x|
370
345
  next if i.title =~ / @done/
371
346
 
372
- items[x].title = "#{i.title} @done(#{opt[:back].strftime('%F %R')})"
347
+ finish_date = verify_duration(i.date, opt[:back], title: i.title)
348
+ items[x].tag('done', value: finish_date.strftime('%F %R'))
373
349
  break
374
350
  end
375
351
  end
@@ -1001,7 +977,7 @@ module Doing
1001
977
  '--no-sort',
1002
978
  '--info=hidden'
1003
979
  ])
1004
- next if tag =~ /^ *$/
980
+ next if output_format =~ /^ *$/
1005
981
 
1006
982
  raise UserCancelled unless output_format
1007
983
 
@@ -1089,6 +1065,7 @@ module Doing
1089
1065
  tag = opt[:tag]
1090
1066
  items.map! do |i|
1091
1067
  i.tag(tag, date: false, remove: opt[:remove], single: single)
1068
+ i.expand_date_tags(@config['date_tags'])
1092
1069
  Hooks.trigger :post_entry_updated, self, i
1093
1070
  end
1094
1071
  end
@@ -1148,6 +1125,26 @@ module Doing
1148
1125
  end
1149
1126
  end
1150
1127
 
1128
+ def verify_duration(date, finish_date, title: nil)
1129
+ max_elapsed = @config.dig('interaction', 'confirm_longer_than') || 0
1130
+ max_elapsed = max_elapsed.chronify_qty if max_elapsed.is_a?(String)
1131
+ elapsed = finish_date - date
1132
+
1133
+ if max_elapsed.positive? && (elapsed > max_elapsed)
1134
+ puts boldwhite(title) if title
1135
+ human = elapsed.time_string(format: :natural)
1136
+ res = Prompt.yn(yellow("Did this entry actually take #{human}"), default_response: true)
1137
+ unless res
1138
+ new_elapsed = Prompt.enter_text('How long did it take?').chronify_qty
1139
+ raise InvalidTimeExpression, 'Unrecognized time span entry' unless new_elapsed.positive?
1140
+
1141
+ finish_date = date + new_elapsed if new_elapsed
1142
+ end
1143
+ end
1144
+
1145
+ finish_date
1146
+ end
1147
+
1151
1148
  ##
1152
1149
  ## Tag the last entry or X entries
1153
1150
  ##
@@ -1209,21 +1206,8 @@ module Doing
1209
1206
  else
1210
1207
  next_entry.date - 60
1211
1208
  end
1212
- elsif opt[:took]
1213
- if item.date + opt[:took] > Time.now
1214
- item.date = Time.now - opt[:took]
1215
- done_date = Time.now
1216
- else
1217
- done_date = item.date + opt[:took]
1218
- end
1219
- elsif opt[:back]
1220
- done_date = if opt[:back].is_a? Integer
1221
- item.date + opt[:back]
1222
- else
1223
- item.date + (opt[:back] - item.date)
1224
- end
1225
1209
  else
1226
- done_date = Time.now
1210
+ done_date = item.calculate_end_date(opt)
1227
1211
  end
1228
1212
 
1229
1213
  opt[:tags].each do |tag|
@@ -1234,7 +1218,28 @@ module Doing
1234
1218
  next
1235
1219
  end
1236
1220
 
1221
+
1237
1222
  tag = tag.strip
1223
+
1224
+ if tag =~ /^done$/
1225
+ max_elapsed = @config.dig('interaction', 'confirm_longer_than') || 0
1226
+ max_elapsed = max_elapsed.chronify_qty if max_elapsed.is_a?(String)
1227
+ elapsed = done_date - item.date
1228
+
1229
+ if max_elapsed.positive? && (elapsed > max_elapsed) && !opt[:took]
1230
+ puts boldwhite(item.title)
1231
+ human = elapsed.time_string(format: :natural)
1232
+ res = Prompt.yn(yellow("Did this actually take #{human}"), default_response: true)
1233
+ unless res
1234
+ new_elapsed = Prompt.enter_text('How long did it take?').chronify_qty
1235
+ raise InvalidTimeExpression, 'Unrecognized time span entry' unless new_elapsed > 0
1236
+
1237
+ opt[:took] = new_elapsed
1238
+ done_date = item.calculate_end_date(opt) if opt[:took]
1239
+ end
1240
+ end
1241
+ end
1242
+
1238
1243
  if opt[:remove] || opt[:rename] || opt[:value]
1239
1244
  rename_to = nil
1240
1245
  if opt[:value]
@@ -1272,6 +1277,7 @@ module Doing
1272
1277
  logger.warn('Skipped:', 'Archiving is skipped when operating on all entries')
1273
1278
  end
1274
1279
 
1280
+ item.expand_date_tags(@config['date_tags'])
1275
1281
  Hooks.trigger :post_entry_updated, self, item
1276
1282
  end
1277
1283
 
@@ -2005,7 +2011,7 @@ module Doing
2005
2011
  EOS
2006
2012
  sorted_tags_data.reverse.each do |k, v|
2007
2013
  if v > 0
2008
- output += "<tr><td style='text-align:left;'>#{k}</td><td style='text-align:left;'>#{'%02d:%02d:%02d' % format_time(v)}</td></tr>\n"
2014
+ output += "<tr><td style='text-align:left;'>#{k}</td><td style='text-align:left;'>#{v.time_string(format: :clock)}</td></tr>\n"
2009
2015
  end
2010
2016
  end
2011
2017
  tail = <<EOS
@@ -2016,7 +2022,7 @@ EOS
2016
2022
  <tfoot>
2017
2023
  <tr>
2018
2024
  <td style="text-align:left;"><strong>Total</strong></td>
2019
- <td style="text-align:left;">#{'%02d:%02d:%02d' % format_time(total)}</td>
2025
+ <td style="text-align:left;">#{total.time_string(format: :clock)}</td>
2020
2026
  </tr>
2021
2027
  </tfoot>
2022
2028
  </table>
@@ -2031,7 +2037,7 @@ EOS
2031
2037
  EOS
2032
2038
  sorted_tags_data.reverse.each do |k, v|
2033
2039
  if v > 0
2034
- output += "| #{' ' * (pad - k.length)}#{k} | #{'%02d:%02d:%02d' % format_time(v)} |\n"
2040
+ output += "| #{' ' * (pad - k.length)}#{k} | #{v.time_string(format: :clock)} |\n"
2035
2041
  end
2036
2042
  end
2037
2043
  tail = "[Tag Totals]"
@@ -2039,11 +2045,10 @@ EOS
2039
2045
  when :json
2040
2046
  output = []
2041
2047
  sorted_tags_data.reverse.each do |k, v|
2042
- d, h, m = format_time(v)
2043
2048
  output << {
2044
2049
  'tag' => k,
2045
2050
  'seconds' => v,
2046
- 'formatted' => format('%<d>02d:%<h>02d:%<m>02d', d: d, h: h, m: m)
2051
+ 'formatted' => v.time_string(format: :clock)
2047
2052
  }
2048
2053
  end
2049
2054
  output
@@ -2054,8 +2059,7 @@ EOS
2054
2059
  (max - k.length).times do
2055
2060
  spacer += ' '
2056
2061
  end
2057
- _d, h, m = format_time(v, human: true)
2058
- output.push("┃ #{spacer}#{k}:#{format('%<h> 4dh %<m>02dm', h: h, m: m)} ┃")
2062
+ output.push("┃ #{spacer}#{k}:#{v.time_string(format: :hm)} ┃")
2059
2063
  end
2060
2064
 
2061
2065
  header = '┏━━ Tag Totals '
@@ -2068,14 +2072,14 @@ EOS
2068
2072
  (max + 12).times { divider += '━' }
2069
2073
  divider += '┫'
2070
2074
  output = output.empty? ? '' : "\n#{header}\n#{output.join("\n")}"
2071
- d, h, m = format_time(total, human: true)
2072
2075
  output += "\n#{divider}"
2073
2076
  spacer = ''
2074
2077
  (max - 6).times do
2075
2078
  spacer += ' '
2076
2079
  end
2080
+ total_time = total.time_string(format: :hm)
2077
2081
  total = "┃ #{spacer}total: "
2078
- total += format('%<h> 4dh %<m>02dm', h: h, m: m)
2082
+ total += total_time
2079
2083
  total += ' ┃'
2080
2084
  output += "\n#{total}"
2081
2085
  output += "\n#{footer}"
@@ -2087,13 +2091,11 @@ EOS
2087
2091
  (max - k.length).times do
2088
2092
  spacer += ' '
2089
2093
  end
2090
- d, h, m = format_time(v)
2091
- output.push("#{k}:#{spacer}#{format('%<d>02d:%<h>02d:%<m>02d', d: d, h: h, m: m)}")
2094
+ output.push("#{k}:#{spacer}#{v.time_string(format: :clock)}")
2092
2095
  end
2093
2096
 
2094
2097
  output = output.empty? ? '' : "\n--- Tag Totals ---\n#{output.join("\n")}"
2095
- d, h, m = format_time(total)
2096
- output += "\n\nTotal tracked: #{format('%<d>02d:%<h>02d:%<m>02d', d: d, h: h, m: m)}\n"
2098
+ output += "\n\nTotal tracked: #{total.time_string(format: :clock)}\n"
2097
2099
  output
2098
2100
  end
2099
2101
  end
@@ -2118,39 +2120,12 @@ EOS
2118
2120
  record_tag_times(item, seconds) if record
2119
2121
  return seconds.positive? ? seconds : false unless formatted
2120
2122
 
2121
- return seconds.positive? ? format('%02d:%02d:%02d', *format_time(seconds)) : false
2123
+ return seconds.positive? ? seconds.time_string(format: :clock) : false
2122
2124
  end
2123
2125
 
2124
2126
  false
2125
2127
  end
2126
2128
 
2127
- ##
2128
- ## Format human readable time from seconds
2129
- ##
2130
- ## @param seconds [Integer] Seconds
2131
- ##
2132
- def format_time(seconds, human: false)
2133
- return [0, 0, 0] if seconds.nil?
2134
-
2135
- if seconds.instance_of?(String) && seconds =~ /(\d+):(\d+):(\d+)/
2136
- h = Regexp.last_match(1)
2137
- m = Regexp.last_match(2)
2138
- s = Regexp.last_match(3)
2139
- seconds = (h.to_i * 60 * 60) + (m.to_i * 60) + s.to_i
2140
- end
2141
- minutes = (seconds / 60).to_i
2142
- hours = (minutes / 60).to_i
2143
- if human
2144
- minutes = (minutes % 60).to_i
2145
- [0, hours, minutes]
2146
- else
2147
- days = (hours / 24).to_i
2148
- hours = (hours % 24).to_i
2149
- minutes = (minutes % 60).to_i
2150
- [days, hours, minutes]
2151
- end
2152
- end
2153
-
2154
2129
  def configure(filename = nil)
2155
2130
  if filename
2156
2131
  Doing.config_with(filename, { ignore_local: true })