plansheet 0.17.1 → 0.23.2

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.
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