doing 2.1.14 → 2.1.15

Sign up to get free protection for your applications and to get access to all the features.
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 })