plansheet 0.17.1 → 0.23.2

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: 4c094d7236b6603dcab810d785eab766754e0cd9f161a2cb2daf280962b41026
4
- data.tar.gz: 2686468ff7aa21915ff73ff27c49990cd05c2dc4e75c139837a69c1580a49dd5
3
+ metadata.gz: e479f020bfa87192523fd231ec0704d07a2e0e92805eaf76720bf3ee62469c4a
4
+ data.tar.gz: c197a3c2730522f7f70aa4793ff50670e91e04f57f15874bef174c0f94b44f83
5
5
  SHA512:
6
- metadata.gz: 646ede4ae64522cfc33912eaefae96083de293800647700ec04b2569fe305d40bf7855a0780c63262db5fdaa5adf8472dba4790bd5f0b60228acc7a921b84417
7
- data.tar.gz: 0c56e57fa79c225d9c3edf055e5c033e9bb9c5b63a49d2d1e7ae1bc17e3c1298e086af4c06d88d270fe9a6e83f3d6f67bba6c4f65f09afbcaf80ed46570f4a3f
6
+ metadata.gz: 77d8895595f834e4bf6f844aabd20178b3f50172071a2713e81f50e9e0e21f4c9cc29df0550cfba085419f7508eeb188417610ce8364b18fdcdd8a6012290eed
7
+ data.tar.gz: 1da8aab0fe36a1775584afbdd140c3b0e0b6a6ed43202b2987311aa258bc3a2d59ce6109cb88c11c6a8a9335bd5770c9c2e66c1ff3f74c3f26cd4c0c07120733
data/Gemfile CHANGED
@@ -7,7 +7,7 @@ gemspec
7
7
 
8
8
  group :development, optional: true do
9
9
  gem "guard", "~> 2.18"
10
- gem "guard-minitest", "~> 2.4"
10
+ gem "guard-rake", "~> 1.0"
11
11
  gem "minitest", "~> 5.0"
12
12
  gem "rake", "~> 13.0"
13
13
  gem "rdoc"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- plansheet (0.17.1)
4
+ plansheet (0.23.2)
5
5
  dc-kwalify (~> 1.0)
6
6
  rgl (= 0.5.8)
7
7
 
@@ -23,10 +23,9 @@ GEM
23
23
  pry (>= 0.13.0)
24
24
  shellany (~> 0.0)
25
25
  thor (>= 0.18.1)
26
- guard-compat (1.2.1)
27
- guard-minitest (2.4.6)
28
- guard-compat (~> 1.2)
29
- minitest (>= 3.0)
26
+ guard-rake (1.0.0)
27
+ guard
28
+ rake
30
29
  lazy_priority_queue (0.1.1)
31
30
  listen (3.7.1)
32
31
  rb-fsevent (~> 0.10, >= 0.10.3)
@@ -87,7 +86,7 @@ PLATFORMS
87
86
 
88
87
  DEPENDENCIES
89
88
  guard (~> 2.18)
90
- guard-minitest (~> 2.4)
89
+ guard-rake (~> 1.0)
91
90
  minitest (~> 5.0)
92
91
  plansheet!
93
92
  rake (~> 13.0)
data/Guardfile CHANGED
@@ -1,9 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- directories(%w[lib test].select { |d| Dir.exist?(d) ? d : UI.warning("Directory #{d} does not exist") })
3
+ directories(%w[. exe bin lib test].select { |d| Dir.exist?(d) ? d : UI.warning("Directory #{d} does not exist") })
4
4
 
5
- guard :minitest do
6
- watch(%r{^test/test_(.*)\.rb$}) { "test" }
7
- watch(%r{^lib/plansheet/(.*)\.rb$}) { "test" }
8
- watch(%r{^lib/plansheet\.rb$}) { "test" }
5
+ guard :rake, task: "default" do
6
+ watch("Gemfile")
7
+ watch("Rakefile")
8
+ watch("Guardfile")
9
+ watch(%r{^test/test_(.*)\.rb$})
10
+ watch("exe/plansheet")
11
+ watch("bin/console")
12
+ watch(%r{^test/test_(.*)\.rb$})
13
+ watch(%r{^lib/plansheet/(.*)\.rb$})
14
+ watch(%r{^lib/plansheet/project/(.*)\.rb$})
15
+ watch(%r{^lib/plansheet\.rb$})
9
16
  end
data/exe/plansheet CHANGED
@@ -13,10 +13,22 @@ parser.on(
13
13
  "--sort",
14
14
  "Sort project files"
15
15
  )
16
+ parser.on(
17
+ "--irb",
18
+ "Open IRB console after loading projects"
19
+ )
16
20
  parser.on(
17
21
  "--cli",
18
22
  "CLI dump of projects (WIP)"
19
23
  )
24
+ parser.on(
25
+ "--stats",
26
+ "Various stats (WIP)"
27
+ )
28
+ parser.on(
29
+ "--time-roi",
30
+ "Show projects with a time return-on-investment"
31
+ )
20
32
  parser.on(
21
33
  "--calendar",
22
34
  "List of projects ordered by due date"
@@ -36,9 +48,23 @@ if options[:sheet] || options.empty?
36
48
  require "plansheet/sheet"
37
49
  Dir.mkdir config["output_dir"] unless Dir.exist? config["output_dir"]
38
50
  Plansheet::Sheet.new("#{config["output_dir"]}/projects.md", pool.projects)
51
+ elsif options[:irb]
52
+ binding.irb # rubocop:disable Lint/Debugger
53
+ elsif options[:stats]
54
+ puts "# of projects: #{pool.projects.count}"
55
+ puts "# of tasks: #{pool.projects.sum { |x| x&.tasks&.count || 0 }}"
56
+ puts "# of locations: #{pool.projects.collect(&:location).flatten.delete_if(&:nil?).uniq.count}"
57
+ puts "combined time estimate: #{pool.projects.sum { |x| x.time_estimate_minutes || 0 }} minutes"
39
58
  elsif options[:sort]
40
59
  # Pool sorts projects, this now just matches old behaviour
41
60
  pool.write_projects
61
+ elsif options[:"time-roi"]
62
+ project_arr = pool.projects.select {|x| x.time_roi_payoff != 0 && !x.dropped_or_done?}.sort
63
+ project_arr.each do |proj|
64
+ puts proj
65
+ puts "time ROI payoff: #{proj.time_roi_payoff}"
66
+ puts "\n"
67
+ end
42
68
  elsif options[:calendar]
43
69
  # TODO: add a project filter method
44
70
  project_arr = pool.projects
@@ -14,10 +14,11 @@ module Plansheet
14
14
  priority
15
15
  defer
16
16
  due
17
+ time_roi
17
18
  status
18
19
  ].freeze
19
20
 
20
- def initialize(config)
21
+ def initialize(config, debug: false)
21
22
  @projects_dir = config[:projects_dir]
22
23
  @sort_order = config[:sort_order]
23
24
  # @completed_projects_dir = config(:completed_projects_dir)
@@ -26,17 +27,19 @@ module Plansheet
26
27
  # until runtime. I'm sure this design decision definitely won't bite me
27
28
  # in the future ;-) Fortunately, it's also not a problem that can't be
28
29
  # walked back from.
29
- if config[:sort_order]
30
- self.class.const_set("POOL_COMPARISON_ORDER", config[:sort_order])
31
- else
32
- self.class.const_set("POOL_COMPARISON_ORDER", Plansheet::Pool::DEFAULT_COMPARISON_ORDER)
33
- end
30
+ # rubocop:disable Lint/OrAssignmentToConstant
31
+ Plansheet::Pool::POOL_COMPARISON_ORDER ||= config[:sort_order] if config[:sort_order]
32
+ puts "using config sort order" if config[:sort_order]
33
+ Plansheet::Pool::POOL_COMPARISON_ORDER ||= Plansheet::Pool::DEFAULT_COMPARISON_ORDER
34
+ # rubocop:enable Lint/OrAssignmentToConstant
34
35
  require_relative "project"
35
- load_projects_dir(@projects_dir)
36
- sort_projects
36
+
37
+ load_projects_dir(@projects_dir) unless debug
38
+ sort_projects if @projects
37
39
  end
38
40
 
39
41
  def sort_projects
42
+ @projects ||= []
40
43
  @projects.sort!
41
44
  # lookup_hash returns the index of a project
42
45
  lookup_hash = Hash.new nil
@@ -9,6 +9,8 @@ require "kwalify"
9
9
  module Plansheet
10
10
  # Once there's some stability in plansheet and dc-kwalify, will pre-load this
11
11
  # to save the later YAML.load
12
+ YAML_TIME_REGEX = "/\\d+[mh] ?\d*m?/" # TODO: regex is bad, adjust for better handling of pretty time
13
+ YAML_DATE_REGEX = "/\\d+[dw]/"
12
14
  PROJECT_YAML_SCHEMA = <<~YAML
13
15
  desc: dc-tasks project schema
14
16
  type: seq
@@ -18,6 +20,7 @@ module Plansheet
18
20
  "project":
19
21
  desc: Project name
20
22
  type: str
23
+ unique: true
21
24
  "namespace":
22
25
  desc: Project name
23
26
  type: str
@@ -51,14 +54,42 @@ module Plansheet
51
54
  "time_estimate":
52
55
  desc: The estimated amount of time before a project is completed
53
56
  type: str
57
+ pattern: #{YAML_TIME_REGEX}
58
+ "daily_time_roi":
59
+ desc: The estimated amount of time saved daily by completing this project
60
+ type: str
61
+ pattern: #{YAML_TIME_REGEX}
62
+ "weekly_time_roi":
63
+ desc: The estimated amount of time saved daily by completing this project
64
+ type: str
65
+ pattern: #{YAML_TIME_REGEX}
66
+ "yearly_time_roi":
67
+ desc: The estimated amount of time saved daily by completing this project
68
+ type: str
69
+ pattern: #{YAML_TIME_REGEX}
70
+ "day_of_week":
71
+ desc: recurring day of week project
72
+ type: str
73
+ enum:
74
+ - Sunday
75
+ - Monday
76
+ - Tuesday
77
+ - Wednesday
78
+ - Thursday
79
+ - Friday
80
+ - Saturday
54
81
  "frequency":
55
- desc: The amount of time before a recurring project moves to ready status again from when it was last done (WIP)
82
+ desc: The amount of time before a recurring project moves to ready status again from when it was last done, with a set due date (eg. a bill becomes due)
83
+ type: str
84
+ pattern: #{YAML_DATE_REGEX}
85
+ "last_for":
86
+ desc: "The amount of time before a recurring project moves to ready status again from when it was last done, with a set defer date (eg. inflating a bike tire). If your project 'can't wait' a day or two, you should use frequency."
56
87
  type: str
57
- pattern: /\\d+[dwDW]/
88
+ pattern: #{YAML_DATE_REGEX}
58
89
  "lead_time":
59
90
  desc: The amount of time before a recurring project is "due" moved to ready where the project (sort of a deferral mechanism) (WIP)
60
91
  type: str
61
- pattern: /\\d+[dwDW]/
92
+ pattern: #{YAML_DATE_REGEX}
62
93
  "due":
63
94
  desc: Due date of the task
64
95
  type: date
@@ -5,6 +5,13 @@ require "date"
5
5
  require_relative "project/yaml"
6
6
  require_relative "project/stringify"
7
7
 
8
+ # Needed for Project#time_estimate, would be much happier *not* patching Array
9
+ class Array
10
+ def nil_if_empty
11
+ count.zero? ? nil : self
12
+ end
13
+ end
14
+
8
15
  module Plansheet
9
16
  PROJECT_STATUS_PRIORITY = {
10
17
  "wip" => 1,
@@ -17,6 +24,12 @@ module Plansheet
17
24
  "done" => 8
18
25
  }.freeze
19
26
 
27
+ # Pre-compute the next days-of-week
28
+ NEXT_DOW = 0.upto(6).to_h do |x|
29
+ d = Date.today + x
30
+ [d.strftime("%A"), d]
31
+ end.freeze
32
+
20
33
  def self.parse_date_duration(str)
21
34
  return Regexp.last_match(1).to_i if str.strip.match(/(\d+)[dD]/)
22
35
  return (Regexp.last_match(1).to_i * 7) if str.strip.match(/(\d+)[wW]/)
@@ -24,6 +37,29 @@ module Plansheet
24
37
  raise "Can't parse time duration string #{str}"
25
38
  end
26
39
 
40
+ def self.parse_time_duration(str)
41
+ if str.match(/(\d+h) (\d+m)/)
42
+ return (parse_time_duration(Regexp.last_match(1)) + parse_time_duration(Regexp.last_match(2)))
43
+ end
44
+
45
+ return Regexp.last_match(1).to_i if str.strip.match(/(\d+)m/)
46
+ return (Regexp.last_match(1).to_f * 60).to_i if str.strip.match(/(\d+\.?\d*)h/)
47
+
48
+ raise "Can't parse time duration string #{str}"
49
+ end
50
+
51
+ def self.build_time_duration(minutes)
52
+ if minutes > 59
53
+ if (minutes % 60).zero?
54
+ "#{minutes / 60}h"
55
+ else
56
+ "#{minutes / 60}h #{minutes % 60}m"
57
+ end
58
+ else
59
+ "#{minutes}m"
60
+ end
61
+ end
62
+
27
63
  # The use of instance_variable_set/get probably seems a bit weird, but the
28
64
  # intent is to avoid object allocation on non-existent project properties, as
29
65
  # well as avoiding a bunch of copy-paste boilerplate when adding a new
@@ -33,6 +69,9 @@ module Plansheet
33
69
  class Project
34
70
  include Comparable
35
71
 
72
+ TIME_EST_REGEX = /\((\d+\.?\d*[mMhH])\)$/.freeze
73
+ TIME_EST_REGEX_NO_CAPTURE = /\(\d+\.?\d*[mMhH]\)$/.freeze
74
+
36
75
  PROJECT_PRIORITY = {
37
76
  "high" => 1,
38
77
  "medium" => 2,
@@ -42,13 +81,14 @@ module Plansheet
42
81
  COMPARISON_ORDER_SYMS = Plansheet::Pool::POOL_COMPARISON_ORDER.map { |x| "compare_#{x}".to_sym }.freeze
43
82
  # NOTE: The order of these affects presentation!
44
83
  # namespace is derived from file name
45
- STRING_PROPERTIES = %w[priority status location notes time_estimate frequency lead_time].freeze
84
+ STRING_PROPERTIES = %w[priority status location notes time_estimate daily_time_roi weekly_time_roi yearly_time_roi
85
+ day_of_week frequency last_for lead_time].freeze
46
86
  DATE_PROPERTIES = %w[due defer completed_on created_on starts_on last_done last_reviewed].freeze
47
87
  ARRAY_PROPERTIES = %w[dependencies externals urls tasks done tags].freeze
48
88
 
49
89
  ALL_PROPERTIES = STRING_PROPERTIES + DATE_PROPERTIES + ARRAY_PROPERTIES
50
90
 
51
- attr_reader :name, :priority_val, *ALL_PROPERTIES
91
+ attr_reader :name, :priority_val, :time_estimate_minutes, *ALL_PROPERTIES
52
92
  attr_accessor :namespace
53
93
 
54
94
  def initialize(options)
@@ -74,6 +114,47 @@ module Plansheet
74
114
  else
75
115
  PROJECT_PRIORITY["low"]
76
116
  end
117
+
118
+ # Remove stale defer dates
119
+ remove_instance_variable("@defer") if @defer && (@defer < Date.today)
120
+
121
+ # Add a created_on field if it doesn't exist
122
+ instance_variable_set("@created_on", Date.today) unless @created_on
123
+
124
+ # Handle nil-value tasks
125
+ if @tasks
126
+ @tasks.compact!
127
+ remove_instance_variable("@tasks") if @tasks.empty?
128
+ end
129
+
130
+ # Generate time estimate from tasks if specified
131
+ # Stomps time_estimate field
132
+ if @tasks
133
+ @time_estimate_minutes = @tasks&.select do |t|
134
+ t.match? TIME_EST_REGEX_NO_CAPTURE
135
+ end&.nil_if_empty&.map { |t| Plansheet::Project.task_time_estimate(t) }&.sum
136
+ elsif @time_estimate
137
+ # No tasks with estimates, but there's an explicit time_estimate
138
+ # Convert the field to minutes
139
+ @time_estimate_minutes = Plansheet.parse_time_duration(@time_estimate)
140
+ end
141
+ if @time_estimate_minutes # rubocop:disable Style/GuardClause
142
+ # Rewrite time_estimate field
143
+ @time_estimate = Plansheet.build_time_duration(@time_estimate_minutes)
144
+
145
+ yms = yearly_minutes_saved
146
+ @time_roi_payoff = yms.to_f / @time_estimate_minutes if yms
147
+ end
148
+ end
149
+
150
+ def yearly_minutes_saved
151
+ if @daily_time_roi
152
+ Plansheet.parse_time_duration(@daily_time_roi) * 365
153
+ elsif @weekly_time_roi
154
+ Plansheet.parse_time_duration(@weekly_time_roi) * 52
155
+ elsif @yearly_time_roi
156
+ Plansheet.parse_time_duration(@yearly_time_roi)
157
+ end
77
158
  end
78
159
 
79
160
  def <=>(other)
@@ -89,6 +170,14 @@ module Plansheet
89
170
  priority_val <=> other.priority_val
90
171
  end
91
172
 
173
+ def time_roi_payoff
174
+ @time_roi_payoff || 0
175
+ end
176
+
177
+ def compare_time_roi(other)
178
+ other.time_roi_payoff <=> time_roi_payoff
179
+ end
180
+
92
181
  def compare_status(other)
93
182
  PROJECT_STATUS_PRIORITY[status] <=> PROJECT_STATUS_PRIORITY[other.status]
94
183
  end
@@ -178,7 +267,8 @@ module Plansheet
178
267
 
179
268
  def subsequent_recurring_status
180
269
  return "done" if @lead_time && defer > Date.today
181
- return "done" if due > Date.today
270
+ return "done" if @last_for && defer > Date.today
271
+ return "done" if due && due > Date.today
182
272
 
183
273
  task_based_status
184
274
  end
@@ -191,39 +281,60 @@ module Plansheet
191
281
  # Due date either explicit or recurring
192
282
  def due
193
283
  return @due if @due
194
- return recurring_due_date if recurring?
284
+ return recurring_due_date if recurring_due?
195
285
 
196
286
  nil
197
287
  end
198
288
 
199
289
  def recurring_due_date
200
290
  if @last_done
201
- @last_done + Plansheet.parse_date_duration(@frequency)
202
- else
203
- Date.today
291
+ return @last_done + Plansheet.parse_date_duration(@frequency) if @frequency
292
+
293
+ if @day_of_week
294
+ return Date.today + 7 if @last_done == Date.today
295
+ return @last_done + 7 if @last_done < Date.today - 7
296
+
297
+ return NEXT_DOW[@day_of_week]
298
+ end
204
299
  end
300
+
301
+ # Going to assume this is the first time, so due today
302
+ Date.today
205
303
  end
206
304
 
207
305
  def defer
208
306
  return @defer if @defer
209
307
  return lead_time_deferral if @lead_time && due
308
+ return last_for_deferral if @last_for
210
309
 
211
310
  nil
212
311
  end
213
312
 
313
+ def last_for_deferral
314
+ return @last_done + Plansheet.parse_date_duration(@last_for) if @last_done
315
+ end
316
+
214
317
  def lead_time_deferral
215
318
  [(due - Plansheet.parse_date_duration(@lead_time)),
216
319
  Date.today].max
217
320
  end
218
321
 
322
+ def recurring_due?
323
+ !@frequency.nil? || !@day_of_week.nil?
324
+ end
325
+
219
326
  def recurring?
220
- !@frequency.nil?
327
+ !@frequency.nil? || !@day_of_week.nil? || !@last_done.nil?
221
328
  end
222
329
 
223
330
  def dropped_or_done?
224
331
  status == "dropped" || status == "done"
225
332
  end
226
333
 
334
+ def self.task_time_estimate(str)
335
+ Plansheet.parse_time_duration(Regexp.last_match(1)) if str.match(TIME_EST_REGEX)
336
+ end
337
+
227
338
  def to_h
228
339
  h = { "project" => @name, "namespace" => @namespace }
229
340
  ALL_PROPERTIES.each do |prop|
@@ -28,20 +28,26 @@ module Plansheet
28
28
 
29
29
  def project_minipage(proj)
30
30
  str = String.new
31
- str << "\\begin{minipage}{4.5cm}\n"
31
+ str << "\\begin{minipage}{6cm}\n"
32
32
  str << project_header(proj)
33
33
  proj&.tasks&.each do |t|
34
- str << "$\\square$ #{t} \\\\\n"
34
+ str << "$\\square$ #{sanitize_string(t)} \\\\\n"
35
35
  end
36
36
  str << "\\end{minipage}\n"
37
37
  str
38
38
  end
39
39
 
40
+ def sanitize_string(str)
41
+ str.gsub("_", '\_')
42
+ end
43
+
40
44
  def project_header(proj)
41
45
  str = String.new
42
- str << "#{proj.namespace}: #{proj.name} - #{proj.status}"
43
- str << " - #{proj.location}" if proj.location
44
- str << " - due #{proj.due}" if proj.due
46
+ str << "#{sanitize_string(proj.namespace)}: #{sanitize_string(proj.name)}\\\\\n"
47
+ str << proj.status.to_s
48
+ str << " - #{sanitize_string(proj.location)}" if proj.location
49
+ str << " due: #{proj.due}" if proj.due
50
+ str << " time: #{proj.time_estimate}" if proj.time_estimate
45
51
  str << " \\\\\n"
46
52
  str
47
53
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plansheet
4
- VERSION = "0.17.1"
4
+ VERSION = "0.23.2"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plansheet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.1
4
+ version: 0.23.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Crosby
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-06-30 00:00:00.000000000 Z
11
+ date: 2022-07-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dc-kwalify