doing 1.0.64 → 1.0.68

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e6ebff7b78368227b7642297b5b88c8f018f0a9049f42478fc070f545421b1c
4
- data.tar.gz: bff5c3852e60db87cacaba489f30fcf93fbdb01a8b7e4cba5fd06b6fc23db936
3
+ metadata.gz: acf8a6f7fcc6e38684720c4a4f28c00b03ad5e7d0d51a92c734f84254eeb6c1a
4
+ data.tar.gz: 4cf29822709042f2744718ce5e525582408295f2300df8f962e32c9a788c339e
5
5
  SHA512:
6
- metadata.gz: 3c987d7c4a54bb331a3d9db464c5e35b86b1bcc777418547067b8f3bf609756663990c42b53b12d0f36a536f86b3041943eacd539d4d0cf8d1610ff7f3333801
7
- data.tar.gz: 514fa3c1e8acefc9ebfde3e71e517918648b57c6b52c6d2c86bc9fe79d555d158edae4a9a2dc54c91bbfc32962280e328d97e26875114d4e1b0c98f693923b04
6
+ metadata.gz: 9a76904bdca84e0bbc77694d0864d01a419ae6cd5c13aa42404b199bcb60d7421fcc58b02dd1373f1a81c8645815dbf43f958f43d52510f574fa9147c04cbfb3
7
+ data.tar.gz: 1edaa81995c4b1ab77f5a0b4413b7961d47e67974a7201b623eb7d1727b274076e26a8a86a6ad1811ee6c2f8c9d3ac0786579097783eb5069feb045097527da2
data/README.md CHANGED
@@ -29,7 +29,7 @@ _Side note:_ I actually use the library behind this utility as part of another s
29
29
 
30
30
  ## Installation
31
31
 
32
- The current version of `doing` is <!--VER-->1.0.63<!--END VER-->.
32
+ The current version of `doing` is <!--VER-->1.0.67<!--END VER-->.
33
33
 
34
34
  $ [sudo] gem install doing
35
35
 
@@ -595,6 +595,29 @@ Example: Archive all Currently items for _@client_ that are marked _@done_
595
595
 
596
596
  doing archive @client @done
597
597
 
598
+ ##### Importing
599
+
600
+ Doing can currently only import tasks from Timing.app reports. If you want to sync up your Doing file with Timing's tracking:
601
+
602
+ 1. Open Timing and go to Reports
603
+ 2. Set the date span you want to import into doing
604
+ 3. Group by Project, Then by None
605
+ 4. Include Tasks with Title, (not as subgroup), Timespan, and Notes
606
+ 5. Uncheck App Usage
607
+ 6. Set File Format to JSON and Duration format to "XX:YY:ZZ"
608
+ 7. Include short entries if desired
609
+ 8. Export the report to a new file
610
+
611
+ Now you can run `doing import --type timing -s SECTION PATH`, where SECTION is the name of the section you want to import the entries to (defauts to Currently), and PATH is the path to the JSON file. You can also add a tag (or tags) to all entries, or a custom prefix.
612
+
613
+ (`--type timing` is the only option right now, so it doesn't need to be included)
614
+
615
+ # Import entries to Projects section and add @timing to all new entries
616
+ doing import -s Projects --tag=timing "~/Desktop/All Activities.json"
617
+
618
+ # Import to default section (Currently) and prefix entries with '[Imported]'
619
+ doing import --prefix="[Imported]" "~/Desktop/All Activities.json"
620
+
598
621
  ---
599
622
 
600
623
  ## Extras
data/bin/doing CHANGED
@@ -52,7 +52,7 @@ flag %i[f doing_file]
52
52
 
53
53
  desc 'Add an entry'
54
54
  arg_name 'ENTRY'
55
- command [:now, :next] do |c|
55
+ command %i[now next] do |c|
56
56
  c.desc 'Section'
57
57
  c.arg_name 'NAME'
58
58
  c.flag %i[s section], default_value: wwid.current_section
@@ -105,7 +105,8 @@ command [:now, :next] do |c|
105
105
  wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:f] })
106
106
  wwid.write(wwid.doing_file)
107
107
  elsif $stdin.stat.size.positive?
108
- title, note = wwid.format_input($stdin.read)
108
+ input = $stdin.read
109
+ title, note = wwid.format_input(input)
109
110
  note.push(options[:n]) if options[:n]
110
111
  wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:f] })
111
112
  wwid.write(wwid.doing_file)
@@ -239,7 +240,7 @@ long_desc %(
239
240
 
240
241
  Example `doing template HAML > ~/styles/my_doing.haml`
241
242
  )
242
- arg_name 'TYPE', must_match: /^(html|haml|css)/i
243
+ arg_name 'TYPE', must_match: /^(?:html|haml|css)/i
243
244
  command :template do |c|
244
245
  c.action do |_global_options, options, args|
245
246
  raise 'No type specified, use `doing template [HAML|CSS]`' if args.empty?
@@ -443,7 +444,7 @@ command :cancel do |c|
443
444
 
444
445
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
445
446
  c.arg_name 'BOOLEAN'
446
- c.flag [:bool], must_match: /^(and|or|not)$/i, default_value: 'AND'
447
+ c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
447
448
 
448
449
  c.action do |_global_options, options, args|
449
450
  section = wwid.guess_section(options[:s]) || options[:s].cap_first
@@ -452,7 +453,16 @@ command :cancel do |c|
452
453
  tags = []
453
454
  else
454
455
  tags = options[:tag].split(/ *, */).map { |t| t.strip.sub(/^@/, '') }
455
- options[:bool] = options[:bool] =~ /^(and|or|not)$/i ? options[:bool].upcase : 'AND'
456
+ options[:bool] = case options[:bool]
457
+ when /(and|all)/i
458
+ 'AND'
459
+ when /(any|or)/i
460
+ 'OR'
461
+ when /(not|none)/i
462
+ 'NOT'
463
+ else
464
+ 'AND'
465
+ end
456
466
  end
457
467
 
458
468
  raise 'Only one argument allowed' if args.length > 1
@@ -500,7 +510,7 @@ command :finish do |c|
500
510
 
501
511
  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
502
512
  c.arg_name 'BOOLEAN'
503
- c.flag [:bool], must_match: /^(and|or|not)$/i, default_value: 'AND'
513
+ c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
504
514
 
505
515
  c.desc %(Auto-generate finish dates from next entry's start time.
506
516
  Automatically generate completion dates 1 minute before next start date.
@@ -537,7 +547,16 @@ command :finish do |c|
537
547
  tags = []
538
548
  else
539
549
  tags = options[:tag].split(/ *, */).map { |t| t.strip.sub(/^@/, '') }
540
- options[:bool] = options[:bool] =~ /^(and|or|not)$/i ? options[:bool].upcase : 'AND'
550
+ options[:bool] = case options[:bool]
551
+ when /(and|all)/i
552
+ 'AND'
553
+ when /(any|or)/i
554
+ 'OR'
555
+ when /(not|none)/i
556
+ 'NOT'
557
+ else
558
+ 'AND'
559
+ end
541
560
  end
542
561
 
543
562
  raise 'Only one argument allowed' if args.length > 1
@@ -582,7 +601,7 @@ command [:again, :resume] do |c|
582
601
 
583
602
  c.desc 'Boolean used to combine multiple tags'
584
603
  c.arg_name 'BOOLEAN'
585
- c.flag [:bool], must_match: /^(and|or|not)$/i, default_value: 'AND'
604
+ c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
586
605
 
587
606
  c.desc 'Note'
588
607
  c.arg_name 'TEXT'
@@ -590,6 +609,16 @@ command [:again, :resume] do |c|
590
609
 
591
610
  c.action do |_global_options, options, _args|
592
611
  tags = options[:tag].nil? ? [] : options[:tag].split(/ *, */).map { |t| t.sub(/^@/, '').strip }
612
+ options[:bool] = case options[:bool]
613
+ when /(and|all)/i
614
+ 'AND'
615
+ when /(any|or)/i
616
+ 'OR'
617
+ when /(not|none)/i
618
+ 'NOT'
619
+ else
620
+ 'AND'
621
+ end
593
622
  opts = {
594
623
  in: options[:in],
595
624
  note: options[:n],
@@ -694,9 +723,9 @@ command :show do |c|
694
723
  c.arg_name 'TAG'
695
724
  c.flag [:tag]
696
725
 
697
- c.desc 'Tag boolean (AND,OR,NONE)'
726
+ c.desc 'Tag boolean (AND,OR,NOT)'
698
727
  c.arg_name 'BOOLEAN'
699
- c.flag %i[b bool], must_match: /^(and|or|not)$/i, default_value: 'OR'
728
+ c.flag %i[b bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'OR'
700
729
 
701
730
  c.desc 'Max count to show'
702
731
  c.arg_name 'MAX'
@@ -708,7 +737,7 @@ command :show do |c|
708
737
 
709
738
  c.desc 'Sort order (asc/desc)'
710
739
  c.arg_name 'ORDER'
711
- c.flag %i[s sort], must_match: /^(a|d)/i, default_value: 'ASC'
740
+ c.flag %i[s sort], must_match: /^[ad].*/i, default_value: 'ASC'
712
741
 
713
742
  c.desc %(
714
743
  Date range to show, or a single day to filter date on.
@@ -728,14 +757,14 @@ command :show do |c|
728
757
  default = 'time'
729
758
  default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
730
759
  c.arg_name 'KEY'
731
- c.flag [:tag_sort], must_match: /^(name|time)/i, default_value: default
760
+ c.flag [:tag_sort], must_match: /^(?:name|time)/i, default_value: default
732
761
 
733
762
  c.desc 'Only show items with recorded time intervals'
734
763
  c.switch [:only_timed], default_value: false, negatable: false
735
764
 
736
765
  c.desc 'Output to export format (csv|html|json|template|timeline)'
737
766
  c.arg_name 'FORMAT'
738
- c.flag %i[o output], must_match: /^(template|html|csv|json|timeline)$/i
767
+ c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
739
768
  c.action do |_global_options, options, args|
740
769
  tag_filter = false
741
770
  tags = []
@@ -757,13 +786,9 @@ command :show do |c|
757
786
  end
758
787
  if args.length.positive?
759
788
  args.each do |arg|
760
- if arg =~ /,/
761
- arg.split(/,/).each do |tag|
762
- tags.push(tag.strip.sub(/^@/, ''))
763
- end
764
- else
765
- tags.push(arg.strip.sub(/^@/, ''))
766
- end
789
+ arg.split(/,/).each do |tag|
790
+ tags.push(tag.strip.sub(/^@/, ''))
791
+ end
767
792
  end
768
793
  end
769
794
  else
@@ -771,11 +796,21 @@ command :show do |c|
771
796
  end
772
797
 
773
798
  tags.concat(options[:tag].split(/ *, */).map { |t| t.sub(/^@/, '').strip }) if options[:tag]
799
+ options[:bool] = case options[:bool]
800
+ when /(and|all)/i
801
+ 'AND'
802
+ when /(any|or)/i
803
+ 'OR'
804
+ when /(not|none)/i
805
+ 'NOT'
806
+ else
807
+ 'AND'
808
+ end
774
809
 
775
810
  unless tags.empty?
776
811
  tag_filter = {
777
812
  'tags' => tags,
778
- 'bool' => options[:b]
813
+ 'bool' => options[:bool]
779
814
  }
780
815
  end
781
816
 
@@ -832,7 +867,7 @@ command [:grep, :search] do |c|
832
867
 
833
868
  c.desc 'Output to export format (csv|html|json|template|timeline)'
834
869
  c.arg_name 'FORMAT'
835
- c.flag %i[o output], must_match: /^(template|html|csv|json|timeline)$/i
870
+ c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
836
871
 
837
872
  c.desc 'Show time intervals on @done tasks'
838
873
  c.switch %i[t times], default_value: true
@@ -844,7 +879,7 @@ command [:grep, :search] do |c|
844
879
  default = 'time'
845
880
  default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
846
881
  c.arg_name 'KEY'
847
- c.flag [:tag_sort], must_match: /^(name|time)$/i, default_value: default
882
+ c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
848
883
 
849
884
  c.desc 'Only show items with recorded time intervals'
850
885
  c.switch [:only_timed], default_value: false, negatable: false
@@ -891,7 +926,7 @@ command :recent do |c|
891
926
  default = 'time'
892
927
  default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
893
928
  c.arg_name 'KEY'
894
- c.flag [:tag_sort], must_match: /^(name|time)$/i, default_value: default
929
+ c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
895
930
 
896
931
  c.action do |global_options, options, args|
897
932
  section = wwid.guess_section(options[:s]) || options[:s].cap_first
@@ -931,11 +966,11 @@ command :today do |c|
931
966
  default = 'time'
932
967
  default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
933
968
  c.arg_name 'KEY'
934
- c.flag [:tag_sort], must_match: /^(name|time)$/i, default_value: default
969
+ c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
935
970
 
936
971
  c.desc 'Output to export format (csv|html|json|template|timeline)'
937
972
  c.arg_name 'FORMAT'
938
- c.flag %i[o output], must_match: /^(template|html|csv|json|timeline)$/i
973
+ c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
939
974
 
940
975
  c.action do |_global_options, options, _args|
941
976
  options[:t] = true if options[:totals]
@@ -966,11 +1001,11 @@ command :on do |c|
966
1001
  default = 'time'
967
1002
  default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
968
1003
  c.arg_name 'KEY'
969
- c.flag [:tag_sort], must_match: /^(name|time)$/i, default_value: default
1004
+ c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
970
1005
 
971
1006
  c.desc 'Output to export format (csv|html|json|template|timeline)'
972
1007
  c.arg_name 'FORMAT'
973
- c.flag %i[o output], must_match: /^(template|html|csv|json|timeline)$/i
1008
+ c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
974
1009
 
975
1010
  c.action do |_global_options, options, args|
976
1011
  exit_now! 'Missing date argument' if args.empty?
@@ -1008,7 +1043,7 @@ command :yesterday do |c|
1008
1043
 
1009
1044
  c.desc 'Output to export format (csv|html|json|template|timeline)'
1010
1045
  c.arg_name 'FORMAT'
1011
- c.flag %i[o output], must_match: /^(template|html|csv|json|timeline)$/i
1046
+ c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
1012
1047
 
1013
1048
  c.desc 'Show time intervals on @done tasks'
1014
1049
  c.switch %i[t times], default_value: true
@@ -1020,7 +1055,7 @@ command :yesterday do |c|
1020
1055
  default = 'time'
1021
1056
  default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1022
1057
  c.arg_name 'KEY'
1023
- c.flag [:tag_sort], must_match: /^(name|time)$/i, default_value: default
1058
+ c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1024
1059
 
1025
1060
  c.action do |_global_options, options, _args|
1026
1061
  options[:sort_tags] = options[:tag_sort] =~ /^n/i
@@ -1044,7 +1079,7 @@ command :last do |c|
1044
1079
 
1045
1080
  c.desc 'Tag boolean'
1046
1081
  c.arg_name 'BOOLEAN'
1047
- c.flag [:bool], must_match: /(and|or|not)/i, default_value: 'AND'
1082
+ c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
1048
1083
 
1049
1084
  c.desc 'Search filter, surround with slashes for regex (/query/)'
1050
1085
  c.arg_name 'QUERY'
@@ -1057,7 +1092,15 @@ command :last do |c|
1057
1092
  tags = []
1058
1093
  else
1059
1094
  tags = options[:tag].split(/ *, */).map { |t| t.strip.sub(/^@/, '') }
1060
- options[:bool] = options[:bool] =~ /^(and|or|not)$/i ? options[:bool].upcase : 'AND'
1095
+ options[:bool] = case options[:bool]
1096
+ when /(any|or)/i
1097
+ :or
1098
+ when /(not|none)/i
1099
+ :not
1100
+ else
1101
+ :and
1102
+ end
1103
+
1061
1104
  end
1062
1105
 
1063
1106
  if options[:e]
@@ -1130,7 +1173,7 @@ command :view do |c|
1130
1173
 
1131
1174
  c.desc 'Output to export format (csv|html|json|template|timeline)'
1132
1175
  c.arg_name 'FORMAT'
1133
- c.flag %i[o output], must_match: /^(template|html|csv|json|timeline)$/i
1176
+ c.flag %i[o output], must_match: /^(?:template|html|csv|json|timeline)$/i
1134
1177
 
1135
1178
  c.desc 'Show time intervals on @done tasks'
1136
1179
  c.switch %i[t times], default_value: true
@@ -1145,7 +1188,7 @@ command :view do |c|
1145
1188
  default = 'time'
1146
1189
  default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
1147
1190
  c.arg_name 'KEY'
1148
- c.flag [:tag_sort], must_match: /^(name|time)$/i, default_value: default
1191
+ c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
1149
1192
 
1150
1193
  c.desc 'Only show items with recorded time intervals'
1151
1194
  c.switch [:only_timed], default_value: false, negatable: true
@@ -1178,7 +1221,7 @@ command :view do |c|
1178
1221
  else
1179
1222
  view['tags'].gsub(/[, ]+/, ' ').split(' ').map(&:strip)
1180
1223
  end
1181
- tag_filter['bool'] = view.key?('tags_bool') && !view['tags_bool'].nil? ? view['tags_bool'].upcase : 'OR'
1224
+ tag_filter['bool'] = view.key?('tags_bool') && !view['tags_bool'].nil? ? view['tags_bool'].normalize_bool : :or
1182
1225
  end
1183
1226
 
1184
1227
  # If the -o/--output flag was specified, override any default in the view template
@@ -1257,7 +1300,7 @@ command :archive do |c|
1257
1300
 
1258
1301
  c.desc 'Tag boolean (AND|OR|NOT)'
1259
1302
  c.arg_name 'BOOLEAN'
1260
- c.flag [:bool], must_match: /(and|or|not)/i, default_value: 'AND'
1303
+ c.flag [:bool], must_match: /(?:and|all|any|or|not|none)/i, default_value: 'AND'
1261
1304
 
1262
1305
  c.desc 'Search filter'
1263
1306
  c.arg_name 'QUERY'
@@ -1281,6 +1324,16 @@ command :archive do |c|
1281
1324
 
1282
1325
  tags.concat(options[:tag].split(/ *, */).map { |t| t.sub(/^@/, '').strip }) if options[:tag]
1283
1326
 
1327
+ options[:bool] = case options[:bool]
1328
+ when /(and|all)/i
1329
+ 'AND'
1330
+ when /(any|or)/i
1331
+ 'OR'
1332
+ when /(not|none)/i
1333
+ 'NOT'
1334
+ else
1335
+ 'AND'
1336
+ end
1284
1337
  opts = {
1285
1338
  bool: options[:bool],
1286
1339
  destination: options[:to],
@@ -1394,10 +1447,10 @@ end
1394
1447
  desc 'Import entries from an external source'
1395
1448
  long_desc 'Imports entries from other sources. Currently only handles JSON reports exported from Timing.app.'
1396
1449
  arg_name 'PATH'
1397
- command [:import] do |c|
1450
+ command :import do |c|
1398
1451
  c.desc 'Import type'
1399
1452
  c.arg_name 'TYPE'
1400
- c.flag [:type], default_value: 'timing'
1453
+ c.flag :type, default_value: 'timing'
1401
1454
 
1402
1455
  c.desc 'Target section'
1403
1456
  c.arg_name 'NAME'
@@ -1405,11 +1458,14 @@ command [:import] do |c|
1405
1458
 
1406
1459
  c.desc 'Tag all imported entries'
1407
1460
  c.arg_name 'TAGS'
1408
- c.flag [:tag]
1461
+ c.flag :tag
1409
1462
 
1410
1463
  c.desc 'Prefix entries with'
1411
1464
  c.arg_name 'PREFIX'
1412
- c.flag [:prefix]
1465
+ c.flag :prefix
1466
+
1467
+ c.desc 'Allow entries that overlap existing times'
1468
+ c.switch [:overlap], negatable: true
1413
1469
 
1414
1470
  c.action do |_global_options, options, args|
1415
1471
 
@@ -1417,7 +1473,7 @@ command [:import] do |c|
1417
1473
 
1418
1474
  if options[:type] =~ /^tim/i
1419
1475
  args.each do |path|
1420
- wwid.import_timing(path, { section: section, tag: options[:tag], prefix: options[:prefix] })
1476
+ wwid.import_timing(path, { section: section, tag: options[:tag], prefix: options[:prefix], no_overlap: !options[:overlap] })
1421
1477
  wwid.write(wwid.doing_file)
1422
1478
  end
1423
1479
  else
data/lib/doing.rb CHANGED
@@ -8,4 +8,5 @@ require 'tempfile'
8
8
  require 'chronic'
9
9
  require 'haml'
10
10
  require 'json'
11
+ require 'doing/helpers.rb'
11
12
  require 'doing/wwid.rb'
@@ -0,0 +1,120 @@
1
+ ##
2
+ ## @brief Hash helpers
3
+ ##
4
+ class ::Hash
5
+ def has_tags?(tags, bool = :and)
6
+ tags = tags.split(/ *, */) if tags.is_a? String
7
+ bool = bool.normalize_bool if bool.is_a? String
8
+ item = self
9
+ case bool
10
+ when :and
11
+ result = true
12
+ tags.each do |tag|
13
+ unless item['title'] =~ /@#{tag}/
14
+ result = false
15
+ break
16
+ end
17
+ end
18
+ result
19
+ when :not
20
+ result = true
21
+ tags.each do |tag|
22
+ if item['title'] =~ /@#{tag}/
23
+ result = false
24
+ break
25
+ end
26
+ end
27
+ result
28
+ else
29
+ result = false
30
+ tags.each do |tag|
31
+ if item['title'] =~ /@#{tag}/
32
+ result = true
33
+ break
34
+ end
35
+ end
36
+ result
37
+ end
38
+ end
39
+
40
+ def matches_search?(search)
41
+ item = self
42
+ text = item['note'] ? item['title'] + item['note'].join(' ') : item['title']
43
+ pattern = if search.strip =~ %r{^/.*?/$}
44
+ search.sub(%r{/(.*?)/}, '\1')
45
+ else
46
+ search.split('').join('.{0,3}')
47
+ end
48
+ text =~ /#{pattern}/i ? true : false
49
+ end
50
+ end
51
+
52
+ ##
53
+ ## @brief String helpers
54
+ ##
55
+ class ::String
56
+ def cap_first
57
+ sub(/^\w/) do |m|
58
+ m.upcase
59
+ end
60
+ end
61
+
62
+
63
+ ##
64
+ ## @brief Convert a boolean string to a symbol
65
+ ##
66
+ ## @return Symbol :and, :or, or :not
67
+ ##
68
+ def normalize_bool!
69
+ replace normalize_bool
70
+ end
71
+
72
+ def normalize_bool(default = :and)
73
+ case self
74
+ when /(and|all)/i
75
+ :and
76
+ when /(any|or)/i
77
+ :or
78
+ when /(not|none)/i
79
+ :not
80
+ else
81
+ default.is_a?(Symbol) ? default : default.normalize_bool
82
+ end
83
+ end
84
+
85
+
86
+ ##
87
+ ## @brief Turn raw urls into HTML links
88
+ ##
89
+ ## @param opt (Hash) Additional Options
90
+ ##
91
+ def link_urls(opt = {})
92
+ opt[:format] ||= :html
93
+ if opt[:format] == :html
94
+ gsub(%r{(?mi)((http|https)://)?([\w\-_]+(\.[\w\-_]+)+)([\w\-.,@?^=%&amp;:/~+#]*[\w\-@^=%&amp;/~+#])?}) do |_match|
95
+ m = Regexp.last_match
96
+ proto = m[1].nil? ? 'http://' : ''
97
+ %(<a href="#{proto}#{m[0]}" title="Link to #{m[0]}">[#{m[3]}]</a>)
98
+ end.gsub(/<(\w+:.*?)>/) do |match|
99
+ m = Regexp.last_match
100
+ if m[1] =~ /<a href/
101
+ match
102
+ else
103
+ %(<a href="#{m[1]}" title="Link to #{m[1]}">[link]</a>)
104
+ end
105
+ end
106
+ else
107
+ self
108
+ end
109
+ end
110
+ end
111
+
112
+ class ::Symbol
113
+ def normalize_bool!
114
+ replace normalize_bool
115
+ end
116
+
117
+ def normalize_bool
118
+ to_s.normalize_bool
119
+ end
120
+ end
data/lib/doing/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '1.0.64'
2
+ VERSION = '1.0.68'
3
3
  end
data/lib/doing/wwid.rb CHANGED
@@ -4,92 +4,6 @@ require 'deep_merge'
4
4
  require 'open3'
5
5
  require 'pp'
6
6
 
7
- ##
8
- ## @brief Hash helpers
9
- ##
10
- class Hash
11
- def has_tags?(tags, bool = 'AND')
12
- tags = tags.split(/ *, */) if tags.is_a? String
13
- item = self
14
- case bool
15
- when 'AND'
16
- result = true
17
- tags.each do |tag|
18
- unless item['title'] =~ /@#{tag}/
19
- result = false
20
- break
21
- end
22
- end
23
- result
24
- when 'NOT'
25
- result = true
26
- tags.each do |tag|
27
- if item['title'] =~ /@#{tag}/
28
- result = false
29
- break
30
- end
31
- end
32
- result
33
- else
34
- result = false
35
- tags.each do |tag|
36
- if item['title'] =~ /@#{tag}/
37
- result = true
38
- break
39
- end
40
- end
41
- result
42
- end
43
- end
44
-
45
- def matches_search?(search)
46
- item = self
47
- text = item['note'] ? item['title'] + item['note'].join(' ') : item['title']
48
- pattern = if search.strip =~ %r{^/.*?/$}
49
- search.sub(%r{/(.*?)/}, '\1')
50
- else
51
- search.split('').join('.{0,3}')
52
- end
53
- text =~ /#{pattern}/i ? true : false
54
- end
55
- end
56
-
57
- ##
58
- ## @brief String helpers
59
- ##
60
- class String
61
- def cap_first
62
- sub(/^\w/) do |m|
63
- m.upcase
64
- end
65
- end
66
-
67
- ##
68
- ## @brief Turn raw urls into HTML links
69
- ##
70
- ## @param opt (Hash) Additional Options
71
- ##
72
- def link_urls(opt = {})
73
- opt[:format] ||= :html
74
- if opt[:format] == :html
75
- gsub(%r{(?mi)((http|https)://)?([\w\-_]+(\.[\w\-_]+)+)([\w\-.,@?^=%&amp;:/~+#]*[\w\-@^=%&amp;/~+#])?}) do |_match|
76
- m = Regexp.last_match
77
- proto = m[1].nil? ? 'http://' : ''
78
- %(<a href="#{proto}#{m[0]}" title="Link to #{m[0]}">[#{m[3]}]</a>)
79
- end.gsub(/<(\w+:.*?)>/) do |match|
80
- m = Regexp.last_match
81
- if m[1] =~ /<a href/
82
- match
83
- else
84
- %(<a href="#{m[1]}" title="Link to #{m[1]}">[link]</a>)
85
- end
86
- end
87
- else
88
- self
89
- end
90
- end
91
- end
92
-
93
7
  ##
94
8
  ## @brief Main "What Was I Doing" methods
95
9
  ##
@@ -667,6 +581,40 @@ class WWID
667
581
  @results.push(%(Added "#{entry['title']}" to #{section}))
668
582
  end
669
583
 
584
+ def same_time?(item_a, item_b)
585
+ item_a['date'] == item_b['date'] ? get_interval(item_a, false) == get_interval(item_b, false) : false
586
+ end
587
+
588
+ def overlapping_time?(item_a, item_b)
589
+ return true if same_time?(item_a, item_b)
590
+
591
+ start_a = item_a['date']
592
+ interval = get_interval(item_a, false)
593
+ end_a = interval ? start_a + interval.to_i : start_a
594
+ start_b = item_b['date']
595
+ interval = get_interval(item_b, false)
596
+ end_b = interval ? start_b + interval.to_i : start_b
597
+ (start_a >= start_b && start_a <= end_b) || (end_a >= start_b && end_a <= end_b) || (start_a < start_b && end_a > end_b)
598
+ end
599
+
600
+ def dedup(items, no_overlap = false)
601
+
602
+ combined = []
603
+ @content.each do |_k, v|
604
+ combined += v['items']
605
+ end
606
+
607
+ items.delete_if do |item|
608
+ duped = false
609
+ combined.each do |comp|
610
+ duped = no_overlap ? overlapping_time?(item, comp) : same_time?(item, comp)
611
+ break if duped
612
+ end
613
+ # warn "Skipping overlapping entry: #{item['title']}" if duped
614
+ duped
615
+ end
616
+ end
617
+
670
618
  ##
671
619
  ## @brief Imports a Timing report
672
620
  ##
@@ -676,6 +624,8 @@ class WWID
676
624
  ##
677
625
  def import_timing(path, opt = {})
678
626
  section = opt[:section] || @current_section
627
+ opt[:no_overlap] ||= false
628
+
679
629
  add_section(section) unless @content.has_key?(section)
680
630
 
681
631
  add_tags = opt[:tag] ? opt[:tag].split(/[ ,]+/).map { |t| t.sub(/^@?/, '@') }.join(' ') : ''
@@ -690,10 +640,12 @@ class WWID
690
640
  # Only process entries with a start and end date
691
641
  next unless entry.key?('startDate') && entry.key?('endDate')
692
642
 
693
- start_time = Time.parse(entry['startDate'])
694
- end_time = Time.parse(entry['endDate'])
643
+ # Round down seconds and convert UTC to local time
644
+ start_time = Time.parse(entry['startDate'].sub(/:\d\dZ$/, ':00Z')).getlocal
645
+ end_time = Time.parse(entry['endDate'].sub(/:\d\dZ$/, ':00Z')).getlocal
695
646
  next unless start_time && end_time
696
- tags = entry['project'].split(/ ▸ /).map {|proj| proj.gsub(/ +/, '').downcase }
647
+
648
+ tags = entry['project'].split(/ ▸ /).map {|proj| proj.gsub(/[^a-z0-9]+/i, '').downcase }
697
649
  title = "#{prefix} "
698
650
  title += entry.key?('activityTitle') && entry['activityTitle'] != '(Untitled Task)' ? entry['activityTitle'] : 'Working on'
699
651
  tags.each do |tag|
@@ -711,7 +663,10 @@ class WWID
711
663
  new_entry['note'] = entry['notes'].split(/\n/).map(&:chomp) if entry.key?('notes')
712
664
  new_items.push(new_entry)
713
665
  end
714
-
666
+ total = new_items.count
667
+ new_items = dedup(new_items, opt[:no_overlap])
668
+ dups = total - new_items.count
669
+ @results.push(%(Skipped #{dups} items with overlapping times)) if dups > 0
715
670
  @content[section]['items'].concat(new_items)
716
671
  @results.push(%(Imported #{new_items.count} items to #{section}))
717
672
  end
@@ -750,7 +705,7 @@ class WWID
750
705
  opt[:section] ||= 'all'
751
706
  opt[:note] ||= []
752
707
  opt[:tag] ||= []
753
- opt[:tag_bool] ||= 'AND'
708
+ opt[:tag_bool] ||= :and
754
709
 
755
710
  last = last_entry(opt)
756
711
  if last.nil?
@@ -776,7 +731,7 @@ class WWID
776
731
  ## @param opt (Hash) Additional Options
777
732
  ##
778
733
  def last_entry(opt = {})
779
- opt[:tag_bool] ||= 'AND'
734
+ opt[:tag_bool] ||= :and
780
735
  opt[:section] ||= @current_section
781
736
 
782
737
  sec_arr = []
@@ -1267,7 +1222,7 @@ class WWID
1267
1222
  @content.each do |_k, v|
1268
1223
  combined['items'] += v['items']
1269
1224
  end
1270
- section = if opt[:tag_filter] && opt[:tag_filter]['bool'] != 'NONE'
1225
+ section = if opt[:tag_filter] && opt[:tag_filter]['bool'].normalize_bool != :not
1271
1226
  opt[:tag_filter]['tags'].map do |tag|
1272
1227
  "@#{tag}"
1273
1228
  end.join(' + ')
@@ -1298,31 +1253,7 @@ class WWID
1298
1253
  end
1299
1254
 
1300
1255
  if opt[:tag_filter] && !opt[:tag_filter]['tags'].empty?
1301
- items.delete_if do |item|
1302
- case opt[:tag_filter]['bool']
1303
- when /(AND|ALL)/
1304
- del = false
1305
- opt[:tag_filter]['tags'].each do |tag|
1306
- unless item['title'] =~ /@#{tag}/
1307
- del = true
1308
- break
1309
- end
1310
- end
1311
- del
1312
- when /NONE/
1313
- del = false
1314
- opt[:tag_filter]['tags'].each do |tag|
1315
- del = true if item['title'] =~ /@#{tag}/
1316
- end
1317
- del
1318
- when /(OR|ANY)/
1319
- del = true
1320
- opt[:tag_filter]['tags'].each do |tag|
1321
- del = false if item['title'] =~ /@#{tag}/
1322
- end
1323
- del
1324
- end
1325
- end
1256
+ items.select! { |item| item.has_tags?(opt[:tag_filter]['tags'], opt[:tag_filter]['bool']) }
1326
1257
  end
1327
1258
 
1328
1259
  if opt[:search]
@@ -1614,7 +1545,7 @@ class WWID
1614
1545
  count = options[:keep] || 0
1615
1546
  destination = options[:destination] || 'Archive'
1616
1547
  tags = options[:tags] || []
1617
- bool = options[:bool] || 'AND'
1548
+ bool = options[:bool] || :and
1618
1549
 
1619
1550
  section = choose_section if section.nil? || section =~ /choose/i
1620
1551
  archive_all = section =~ /^all$/i # && !(tags.nil? || tags.empty?)
@@ -1642,7 +1573,7 @@ class WWID
1642
1573
  def do_archive(sect, destination, opt = {})
1643
1574
  count = opt[:count] || 0
1644
1575
  tags = opt[:tags] || []
1645
- bool = opt[:bool] || 'AND'
1576
+ bool = opt[:bool] || :and
1646
1577
  label = opt[:label] || true
1647
1578
 
1648
1579
  if sect =~ /^all$/i
@@ -1655,7 +1586,7 @@ class WWID
1655
1586
  counter = 0
1656
1587
 
1657
1588
  all_sections.each do |section|
1658
- items = @content[section]['items']
1589
+ items = @content[section]['items'].dup
1659
1590
 
1660
1591
  moved_items = []
1661
1592
  if !tags.empty? || opt[:search]
@@ -1679,13 +1610,7 @@ class WWID
1679
1610
  @content[destination]['items'].concat(moved_items)
1680
1611
  @results.push("Archived #{moved_items.length} items from #{section} to #{destination}")
1681
1612
  else
1682
- count = items.length if count == 0 || items.length < count
1683
-
1684
- @content[section]['items'] = if count.zero?
1685
- []
1686
- else
1687
- items[0..count - 1]
1688
- end
1613
+ count = items.length if items.length < count
1689
1614
 
1690
1615
  items.map! do |item|
1691
1616
  if label && section != 'Currently'
@@ -1694,12 +1619,19 @@ class WWID
1694
1619
  end
1695
1620
  item
1696
1621
  end
1622
+
1697
1623
  if items.count > count
1698
1624
  @content[destination]['items'].concat(items[count..-1])
1699
1625
  else
1700
1626
  @content[destination]['items'].concat(items)
1701
1627
  end
1702
1628
 
1629
+ @content[section]['items'] = if count.zero?
1630
+ []
1631
+ else
1632
+ items[0..count - 1]
1633
+ end
1634
+
1703
1635
  @results.push("Archived #{items.length - count} items from #{section} to #{destination}")
1704
1636
  end
1705
1637
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: doing
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.64
4
+ version: 1.0.68
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-07-22 00:00:00.000000000 Z
11
+ date: 2021-08-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -165,6 +165,7 @@ files:
165
165
  - README.md
166
166
  - bin/doing
167
167
  - lib/doing.rb
168
+ - lib/doing/helpers.rb
168
169
  - lib/doing/version.rb
169
170
  - lib/doing/wwid.rb
170
171
  - lib/templates/doing.css