plansheet 0.17.1 → 0.23.0

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