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 +4 -4
- data/Gemfile +1 -1
- data/Gemfile.lock +5 -6
- data/Guardfile +12 -5
- data/exe/plansheet +15 -0
- data/lib/plansheet/pool.rb +11 -8
- data/lib/plansheet/project/yaml.rb +34 -3
- data/lib/plansheet/project.rb +113 -8
- data/lib/plansheet/sheet.rb +11 -5
- data/lib/plansheet/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 209e9e64f6e8f0ceba2e1182e60106ead59d0943331c940a9a38ff788ab2cd4f
|
4
|
+
data.tar.gz: 559296847d7777c6d339fd941f43dc8b106b00b75efa354a95ff78adcfc715b3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d883a334412977859444a6db5c33885ced07fabcfa8665aa0331be6609fc94bb7c3d15f4e5fa6a8b4c01bf7e537b4c281b88c90ac30337fcc3c326ef828dfb29
|
7
|
+
data.tar.gz: 4033cae733fd661360022a705b9d3098d07431db2d2377406af95eeb52fc60ace9ba881803557e8146c8bd48524e3f07cd0ab5eff9859b83633c51fc41115306
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
plansheet (0.
|
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-
|
27
|
-
|
28
|
-
|
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-
|
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 :
|
6
|
-
watch(
|
7
|
-
watch(
|
8
|
-
watch(
|
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
|
data/lib/plansheet/pool.rb
CHANGED
@@ -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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
36
|
-
|
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 (
|
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:
|
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:
|
92
|
+
pattern: #{YAML_DATE_REGEX}
|
62
93
|
"due":
|
63
94
|
desc: Due date of the task
|
64
95
|
type: date
|
data/lib/plansheet/project.rb
CHANGED
@@ -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
|
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
|
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
|
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
|
-
|
203
|
-
|
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|
|
data/lib/plansheet/sheet.rb
CHANGED
@@ -28,20 +28,26 @@ module Plansheet
|
|
28
28
|
|
29
29
|
def project_minipage(proj)
|
30
30
|
str = String.new
|
31
|
-
str << "\\begin{minipage}{
|
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}
|
43
|
-
str <<
|
44
|
-
str << " -
|
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
|
data/lib/plansheet/version.rb
CHANGED
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.
|
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-
|
11
|
+
date: 2022-07-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dc-kwalify
|