plansheet 0.17.1 → 0.23.0

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: 209e9e64f6e8f0ceba2e1182e60106ead59d0943331c940a9a38ff788ab2cd4f
4
+ data.tar.gz: 559296847d7777c6d339fd941f43dc8b106b00b75efa354a95ff78adcfc715b3
5
5
  SHA512:
6
- metadata.gz: 646ede4ae64522cfc33912eaefae96083de293800647700ec04b2569fe305d40bf7855a0780c63262db5fdaa5adf8472dba4790bd5f0b60228acc7a921b84417
7
- data.tar.gz: 0c56e57fa79c225d9c3edf055e5c033e9bb9c5b63a49d2d1e7ae1bc17e3c1298e086af4c06d88d270fe9a6e83f3d6f67bba6c4f65f09afbcaf80ed46570f4a3f
6
+ metadata.gz: d883a334412977859444a6db5c33885ced07fabcfa8665aa0331be6609fc94bb7c3d15f4e5fa6a8b4c01bf7e537b4c281b88c90ac30337fcc3c326ef828dfb29
7
+ data.tar.gz: 4033cae733fd661360022a705b9d3098d07431db2d2377406af95eeb52fc60ace9ba881803557e8146c8bd48524e3f07cd0ab5eff9859b83633c51fc41115306
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.0)
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,18 @@ 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
+ )
20
28
  parser.on(
21
29
  "--calendar",
22
30
  "List of projects ordered by due date"
@@ -36,6 +44,13 @@ if options[:sheet] || options.empty?
36
44
  require "plansheet/sheet"
37
45
  Dir.mkdir config["output_dir"] unless Dir.exist? config["output_dir"]
38
46
  Plansheet::Sheet.new("#{config["output_dir"]}/projects.md", pool.projects)
47
+ elsif options[:irb]
48
+ binding.irb # rubocop:disable Lint/Debugger
49
+ elsif options[:stats]
50
+ puts "# of projects: #{pool.projects.count}"
51
+ puts "# of tasks: #{pool.projects.sum { |x| x&.tasks&.count || 0 }}"
52
+ puts "# of locations: #{pool.projects.collect(&:location).flatten.delete_if(&:nil?).uniq.count}"
53
+ puts "combined time estimate: #{pool.projects.sum { |x| x.time_estimate_minutes || 0 }} minutes"
39
54
  elsif options[:sort]
40
55
  # Pool sorts projects, this now just matches old behaviour
41
56
  pool.write_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,41 @@ 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
+ # Generate time estimate from tasks if specified
125
+ # Stomps time_estimate field
126
+ if @tasks
127
+ @time_estimate_minutes = @tasks&.select do |t|
128
+ t.match? TIME_EST_REGEX_NO_CAPTURE
129
+ end&.nil_if_empty&.map { |t| Plansheet::Project.task_time_estimate(t) }&.sum
130
+ elsif @time_estimate
131
+ # No tasks with estimates, but there's an explicit time_estimate
132
+ # Convert the field to minutes
133
+ @time_estimate_minutes = Plansheet.parse_time_duration(@time_estimate)
134
+ end
135
+ if @time_estimate_minutes # rubocop:disable Style/GuardClause
136
+ # Rewrite time_estimate field
137
+ @time_estimate = Plansheet.build_time_duration(@time_estimate_minutes)
138
+
139
+ yms = yearly_minutes_saved
140
+ @time_roi_payoff = yms.to_f / @time_estimate_minutes if yms
141
+ end
142
+ end
143
+
144
+ def yearly_minutes_saved
145
+ if @daily_time_roi
146
+ Plansheet.parse_time_duration(@daily_time_roi) * 365
147
+ elsif @weekly_time_roi
148
+ Plansheet.parse_time_duration(@weekly_time_roi) * 52
149
+ elsif @yearly_time_roi
150
+ Plansheet.parse_time_duration(@yearly_time_roi)
151
+ end
77
152
  end
78
153
 
79
154
  def <=>(other)
@@ -89,6 +164,14 @@ module Plansheet
89
164
  priority_val <=> other.priority_val
90
165
  end
91
166
 
167
+ def time_roi_payoff
168
+ @time_roi_payoff || 0
169
+ end
170
+
171
+ def compare_time_roi(other)
172
+ other.time_roi_payoff <=> time_roi_payoff
173
+ end
174
+
92
175
  def compare_status(other)
93
176
  PROJECT_STATUS_PRIORITY[status] <=> PROJECT_STATUS_PRIORITY[other.status]
94
177
  end
@@ -178,7 +261,8 @@ module Plansheet
178
261
 
179
262
  def subsequent_recurring_status
180
263
  return "done" if @lead_time && defer > Date.today
181
- return "done" if due > Date.today
264
+ return "done" if @last_for && defer > Date.today
265
+ return "done" if due && due > Date.today
182
266
 
183
267
  task_based_status
184
268
  end
@@ -191,39 +275,60 @@ module Plansheet
191
275
  # Due date either explicit or recurring
192
276
  def due
193
277
  return @due if @due
194
- return recurring_due_date if recurring?
278
+ return recurring_due_date if recurring_due?
195
279
 
196
280
  nil
197
281
  end
198
282
 
199
283
  def recurring_due_date
200
284
  if @last_done
201
- @last_done + Plansheet.parse_date_duration(@frequency)
202
- else
203
- Date.today
285
+ return @last_done + Plansheet.parse_date_duration(@frequency) if @frequency
286
+
287
+ if @day_of_week
288
+ return Date.today + 7 if @last_done == Date.today
289
+ return @last_done + 7 if @last_done < Date.today - 7
290
+
291
+ return NEXT_DOW[@day_of_week]
292
+ end
204
293
  end
294
+
295
+ # Going to assume this is the first time, so due today
296
+ Date.today
205
297
  end
206
298
 
207
299
  def defer
208
300
  return @defer if @defer
209
301
  return lead_time_deferral if @lead_time && due
302
+ return last_for_deferral if @last_for
210
303
 
211
304
  nil
212
305
  end
213
306
 
307
+ def last_for_deferral
308
+ return @last_done + Plansheet.parse_date_duration(@last_for) if @last_done
309
+ end
310
+
214
311
  def lead_time_deferral
215
312
  [(due - Plansheet.parse_date_duration(@lead_time)),
216
313
  Date.today].max
217
314
  end
218
315
 
316
+ def recurring_due?
317
+ !@frequency.nil? || !@day_of_week.nil?
318
+ end
319
+
219
320
  def recurring?
220
- !@frequency.nil?
321
+ !@frequency.nil? || !@day_of_week.nil? || !@last_done.nil?
221
322
  end
222
323
 
223
324
  def dropped_or_done?
224
325
  status == "dropped" || status == "done"
225
326
  end
226
327
 
328
+ def self.task_time_estimate(str)
329
+ Plansheet.parse_time_duration(Regexp.last_match(1)) if str.match(TIME_EST_REGEX)
330
+ end
331
+
227
332
  def to_h
228
333
  h = { "project" => @name, "namespace" => @namespace }
229
334
  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.0"
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.0
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-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dc-kwalify