plansheet 0.15.0 → 0.23.1

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: f121593269dc11059fe547d8b72d58c2ad1a6685ae4826ac39daaff8b33f7c9a
4
- data.tar.gz: 4020dab046e837f02b64cfc975408240d472b04a26bc6f40e7e552d915d77336
3
+ metadata.gz: de86fd5b55207da728e3ed67e500a687576c6f10ce3bb9ad09df7cec56a6e7b1
4
+ data.tar.gz: 785db3bae6f43dadb9ac2bdc95b05c6fc9cc7933e199a41d6316807c3899410a
5
5
  SHA512:
6
- metadata.gz: ae82c5c2df93d55e520262754cc1c6e8b1b80a9a2fe230c09d00bd7ed4a38790c71b0dfcde5ac178e38112a26284219f21a8d99e48212601bbbcfae98607c0d1
7
- data.tar.gz: 6ca757244abd515dcd86bbedb5a31590a53fbc8720e35167549bfe326c1f96673acef49b0dd1044a0e92f43253ef1442d126872859cebfa0f1185bb06612da59
6
+ metadata.gz: e1082bae42f951c3c880247c0dbe34f24c4cfcbb3de58e8a0a804fa670d7616e077f60854db7e788cbb1b4234f17089524fd6779bf9308d3be1a32a0a2a9f7db
7
+ data.tar.gz: 138a351986a9c11c6e6814bb2279e42dd39c35476b2e254b41a74aea4770b7d8017fcb4ac8f34ced403e3788fd6821d4ac453fdf8e6e4fe4331a068e80680059
data/Gemfile CHANGED
@@ -7,9 +7,10 @@ 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
+ gem "rdoc"
13
14
 
14
15
  gem "rubocop", "~> 1.21"
15
16
  gem "rubocop-minitest"
data/Gemfile.lock CHANGED
@@ -1,8 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- plansheet (0.15.0)
4
+ plansheet (0.23.1)
5
5
  dc-kwalify (~> 1.0)
6
+ rgl (= 0.5.8)
6
7
 
7
8
  GEM
8
9
  remote: https://rubygems.org/
@@ -12,6 +13,7 @@ GEM
12
13
  dc-kwalify (1.0.0)
13
14
  ffi (1.15.5)
14
15
  formatador (1.1.0)
16
+ generator (0.0.1)
15
17
  guard (2.18.0)
16
18
  formatador (>= 0.2.4)
17
19
  listen (>= 2.7, < 4.0)
@@ -21,10 +23,10 @@ GEM
21
23
  pry (>= 0.13.0)
22
24
  shellany (~> 0.0)
23
25
  thor (>= 0.18.1)
24
- guard-compat (1.2.1)
25
- guard-minitest (2.4.6)
26
- guard-compat (~> 1.2)
27
- minitest (>= 3.0)
26
+ guard-rake (1.0.0)
27
+ guard
28
+ rake
29
+ lazy_priority_queue (0.1.1)
28
30
  listen (3.7.1)
29
31
  rb-fsevent (~> 0.10, >= 0.10.3)
30
32
  rb-inotify (~> 0.9, >= 0.9.10)
@@ -41,13 +43,21 @@ GEM
41
43
  pry (0.14.1)
42
44
  coderay (~> 1.1)
43
45
  method_source (~> 1.0)
46
+ psych (4.0.4)
47
+ stringio
44
48
  rainbow (3.1.1)
45
49
  rake (13.0.6)
46
50
  rb-fsevent (0.11.1)
47
51
  rb-inotify (0.10.1)
48
52
  ffi (~> 1.0)
53
+ rdoc (6.4.0)
54
+ psych (>= 4.0.0)
49
55
  regexp_parser (2.4.0)
50
56
  rexml (3.2.5)
57
+ rgl (0.5.8)
58
+ lazy_priority_queue (~> 0.1.0)
59
+ rexml (~> 3.2, >= 3.2.4)
60
+ stream (~> 0.5.3)
51
61
  rubocop (1.29.1)
52
62
  parallel (~> 1.10)
53
63
  parser (>= 3.1.0.0)
@@ -65,6 +75,9 @@ GEM
65
75
  rubocop (~> 1.0)
66
76
  ruby-progressbar (1.11.0)
67
77
  shellany (0.0.1)
78
+ stream (0.5.4)
79
+ generator
80
+ stringio (3.0.2)
68
81
  thor (1.2.1)
69
82
  unicode-display_width (2.1.0)
70
83
 
@@ -73,10 +86,11 @@ PLATFORMS
73
86
 
74
87
  DEPENDENCIES
75
88
  guard (~> 2.18)
76
- guard-minitest (~> 2.4)
89
+ guard-rake (~> 1.0)
77
90
  minitest (~> 5.0)
78
91
  plansheet!
79
92
  rake (~> 13.0)
93
+ rdoc
80
94
  rubocop (~> 1.21)
81
95
  rubocop-minitest
82
96
  rubocop-rake
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
+ "--calendar",
30
+ "List of projects ordered by due date"
31
+ )
20
32
  parser.on(
21
33
  "--location_filter LOCATION",
22
34
  "location filter for CLI dump (WIP)"
@@ -25,14 +37,33 @@ options = {}
25
37
  parser.parse!(into: options)
26
38
 
27
39
  config = Plansheet.load_config
28
- pool = Plansheet::Pool.new({ projects_dir: config["projects_dir"] })
40
+ pool = Plansheet::Pool.new({ projects_dir: config["projects_dir"],
41
+ sort_order: config["sort_order"] })
29
42
 
30
43
  if options[:sheet] || options.empty?
44
+ require "plansheet/sheet"
31
45
  Dir.mkdir config["output_dir"] unless Dir.exist? config["output_dir"]
32
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"
33
54
  elsif options[:sort]
34
55
  # Pool sorts projects, this now just matches old behaviour
35
56
  pool.write_projects
57
+ elsif options[:calendar]
58
+ # TODO: add a project filter method
59
+ project_arr = pool.projects
60
+ project_arr.delete_if { |x| x.status == "dropped" || x.status == "done" }
61
+ project_arr.delete_if { |x| x.due.nil? }
62
+ project_arr.sort_by!(&:due)
63
+ project_arr.each do |proj|
64
+ puts proj
65
+ puts "\n"
66
+ end
36
67
  elsif options[:cli]
37
68
  # TODO: add a project filter method
38
69
  project_arr = pool.projects
@@ -1,20 +1,77 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rgl/adjacency"
4
+ require "rgl/topsort"
5
+
3
6
  module Plansheet
4
7
  # The "pool" is the aggregated collection of projects, calendar events, etc.
5
8
  class Pool
6
9
  attr_accessor :projects
7
10
 
8
- def initialize(config)
11
+ DEFAULT_COMPARISON_ORDER = %w[
12
+ completeness
13
+ dependency
14
+ priority
15
+ defer
16
+ due
17
+ time_roi
18
+ status
19
+ ].freeze
20
+
21
+ def initialize(config, debug: false)
9
22
  @projects_dir = config[:projects_dir]
23
+ @sort_order = config[:sort_order]
10
24
  # @completed_projects_dir = config(:completed_projects_dir)
11
25
 
12
- load_projects_dir(@projects_dir)
13
- # TODO: Slurp all files
14
- sort_projects
26
+ # This bit of trickiness is because we don't know what the sort order is
27
+ # until runtime. I'm sure this design decision definitely won't bite me
28
+ # in the future ;-) Fortunately, it's also not a problem that can't be
29
+ # walked back from.
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
35
+ require_relative "project"
36
+
37
+ load_projects_dir(@projects_dir) unless debug
38
+ sort_projects if @projects
15
39
  end
16
40
 
17
41
  def sort_projects
42
+ @projects ||= []
43
+ @projects.sort!
44
+ # lookup_hash returns the index of a project
45
+ lookup_hash = Hash.new nil
46
+
47
+ # initialize the lookups
48
+ @projects.each_index do |i|
49
+ lookup_hash[@projects[i].name] = i
50
+ end
51
+
52
+ pg = RGL::DirectedAdjacencyGraph.new
53
+ pg.add_vertices @projects
54
+ @projects.each_index do |proj_index|
55
+ next if @projects[proj_index].dropped_or_done?
56
+
57
+ @projects[proj_index]&.dependencies&.each do |dep|
58
+ di = lookup_hash[dep]
59
+ if di
60
+ # Don't add edges for dropped/done projects, they'll be sorted out
61
+ # later
62
+ next if @projects[di].dropped_or_done?
63
+
64
+ pg.add_edge(@projects[di], @projects[proj_index])
65
+ end
66
+ end
67
+ end
68
+
69
+ # The topological sort of pg is the correct dependency order of the
70
+ # projects
71
+ @projects = pg.topsort_iterator.to_a.flatten.uniq
72
+
73
+ # TODO: second sort doesn't deal with problems where deferred task gets
74
+ # pushed below.
18
75
  @projects.sort!
19
76
  end
20
77
 
@@ -31,10 +88,7 @@ module Plansheet
31
88
  # are involved once completed project directories are a thing - will need
32
89
  # to keep a list of project files to delete
33
90
  project_namespaces.each do |ns|
34
- # TODO: move this to ProjectYAMLFile
35
- #
36
- f = "#{@projects_dir}/#{ns}.yml"
37
- pyf = ProjectYAMLFile.new f
91
+ pyf = ProjectYAMLFile.new "#{@projects_dir}/#{ns}.yml"
38
92
  pyf.projects = projects_in_namespace(ns)
39
93
  pyf.write
40
94
  end
@@ -4,9 +4,13 @@ require "yaml"
4
4
  require "date"
5
5
  require "pathname"
6
6
 
7
+ require "kwalify"
8
+
7
9
  module Plansheet
8
10
  # Once there's some stability in plansheet and dc-kwalify, will pre-load this
9
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]/"
10
14
  PROJECT_YAML_SCHEMA = <<~YAML
11
15
  desc: dc-tasks project schema
12
16
  type: seq
@@ -16,6 +20,7 @@ module Plansheet
16
20
  "project":
17
21
  desc: Project name
18
22
  type: str
23
+ unique: true
19
24
  "namespace":
20
25
  desc: Project name
21
26
  type: str
@@ -49,14 +54,42 @@ module Plansheet
49
54
  "time_estimate":
50
55
  desc: The estimated amount of time before a project is completed
51
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
52
81
  "frequency":
53
- 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."
54
87
  type: str
55
- pattern: /\\d+[dwDW]/
88
+ pattern: #{YAML_DATE_REGEX}
56
89
  "lead_time":
57
90
  desc: The amount of time before a recurring project is "due" moved to ready where the project (sort of a deferral mechanism) (WIP)
58
91
  type: str
59
- pattern: /\\d+[dwDW]/
92
+ pattern: #{YAML_DATE_REGEX}
60
93
  "due":
61
94
  desc: Due date of the task
62
95
  type: date
@@ -161,7 +194,7 @@ module Plansheet
161
194
  end
162
195
 
163
196
  def yaml_dump
164
- YAML.dump(@projects.map { |x| x.to_h.except("namespace") })
197
+ YAML.dump(@projects.map { |x| x.to_h.delete_if { |k, _| k == "namespace" } })
165
198
  end
166
199
  end
167
200
  end
@@ -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,11 +24,11 @@ module Plansheet
17
24
  "done" => 8
18
25
  }.freeze
19
26
 
20
- PROJECT_PRIORITY = {
21
- "high" => 1,
22
- "medium" => 2,
23
- "low" => 3
24
- }.freeze
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
25
32
 
26
33
  def self.parse_date_duration(str)
27
34
  return Regexp.last_match(1).to_i if str.strip.match(/(\d+)[dD]/)
@@ -30,6 +37,29 @@ module Plansheet
30
37
  raise "Can't parse time duration string #{str}"
31
38
  end
32
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
+
33
63
  # The use of instance_variable_set/get probably seems a bit weird, but the
34
64
  # intent is to avoid object allocation on non-existent project properties, as
35
65
  # well as avoiding a bunch of copy-paste boilerplate when adding a new
@@ -39,23 +69,26 @@ module Plansheet
39
69
  class Project
40
70
  include Comparable
41
71
 
42
- DEFAULT_COMPARISON_ORDER = %w[
43
- completeness
44
- dependency
45
- priority
46
- defer
47
- due
48
- status
49
- ].map { |x| "compare_#{x}".to_sym }.freeze
72
+ TIME_EST_REGEX = /\((\d+\.?\d*[mMhH])\)$/.freeze
73
+ TIME_EST_REGEX_NO_CAPTURE = /\(\d+\.?\d*[mMhH]\)$/.freeze
74
+
75
+ PROJECT_PRIORITY = {
76
+ "high" => 1,
77
+ "medium" => 2,
78
+ "low" => 3
79
+ }.freeze
80
+
81
+ COMPARISON_ORDER_SYMS = Plansheet::Pool::POOL_COMPARISON_ORDER.map { |x| "compare_#{x}".to_sym }.freeze
50
82
  # NOTE: The order of these affects presentation!
51
83
  # namespace is derived from file name
52
- 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
53
86
  DATE_PROPERTIES = %w[due defer completed_on created_on starts_on last_done last_reviewed].freeze
54
87
  ARRAY_PROPERTIES = %w[dependencies externals urls tasks done tags].freeze
55
88
 
56
89
  ALL_PROPERTIES = STRING_PROPERTIES + DATE_PROPERTIES + ARRAY_PROPERTIES
57
90
 
58
- attr_reader :name, *ALL_PROPERTIES
91
+ attr_reader :name, :priority_val, :time_estimate_minutes, *ALL_PROPERTIES
59
92
  attr_accessor :namespace
60
93
 
61
94
  def initialize(options)
@@ -76,12 +109,57 @@ module Plansheet
76
109
  # date/external commits/penalties for project failure, etc
77
110
  #
78
111
  # Assume all projects are low priority unless stated otherwise.
79
- @priority ||= "low"
112
+ @priority_val = if @priority
113
+ PROJECT_PRIORITY[@priority]
114
+ else
115
+ PROJECT_PRIORITY["low"]
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
80
158
  end
81
159
 
82
160
  def <=>(other)
83
161
  ret_val = 0
84
- DEFAULT_COMPARISON_ORDER.each do |method|
162
+ COMPARISON_ORDER_SYMS.each do |method|
85
163
  ret_val = send(method, other)
86
164
  break if ret_val != 0
87
165
  end
@@ -89,7 +167,15 @@ module Plansheet
89
167
  end
90
168
 
91
169
  def compare_priority(other)
92
- PROJECT_PRIORITY[@priority] <=> PROJECT_PRIORITY[other.priority]
170
+ priority_val <=> other.priority_val
171
+ end
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
93
179
  end
94
180
 
95
181
  def compare_status(other)
@@ -117,19 +203,25 @@ module Plansheet
117
203
  receiver <=> comparison
118
204
  end
119
205
 
120
- def compare_dependency(other)
121
- return 0 if @dependencies.nil? && other.dependencies.nil?
206
+ def dependency_of?(other)
207
+ other&.dependencies&.any? do |dep|
208
+ @name&.downcase == dep.downcase
209
+ end
210
+ end
122
211
 
123
- if @dependencies.nil?
124
- return -1 if other.dependencies.any? do |dep|
125
- @name.downcase == dep.downcase
126
- end
127
- elsif @dependencies.any? do |dep|
128
- other.name.downcase == dep.downcase
129
- end
130
- return 1
212
+ def dependent_on?(other)
213
+ @dependencies&.any? do |dep|
214
+ other&.name&.downcase == dep.downcase
131
215
  end
132
- 0
216
+ end
217
+
218
+ def compare_dependency(other)
219
+ # This approach might seem odd,
220
+ # but it's to handle circular dependencies
221
+ retval = 0
222
+ retval -= 1 if dependency_of?(other)
223
+ retval += 1 if dependent_on?(other)
224
+ retval
133
225
  end
134
226
 
135
227
  # Projects that are dropped or done are considered "complete", insofar as
@@ -175,7 +267,8 @@ module Plansheet
175
267
 
176
268
  def subsequent_recurring_status
177
269
  return "done" if @lead_time && defer > Date.today
178
- return "done" if due > Date.today
270
+ return "done" if @last_for && defer > Date.today
271
+ return "done" if due && due > Date.today
179
272
 
180
273
  task_based_status
181
274
  end
@@ -188,39 +281,60 @@ module Plansheet
188
281
  # Due date either explicit or recurring
189
282
  def due
190
283
  return @due if @due
191
- return recurring_due_date if recurring?
284
+ return recurring_due_date if recurring_due?
192
285
 
193
286
  nil
194
287
  end
195
288
 
196
289
  def recurring_due_date
197
290
  if @last_done
198
- @last_done + Plansheet.parse_date_duration(@frequency)
199
- else
200
- 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
201
299
  end
300
+
301
+ # Going to assume this is the first time, so due today
302
+ Date.today
202
303
  end
203
304
 
204
305
  def defer
205
306
  return @defer if @defer
206
307
  return lead_time_deferral if @lead_time && due
308
+ return last_for_deferral if @last_for
207
309
 
208
310
  nil
209
311
  end
210
312
 
313
+ def last_for_deferral
314
+ return @last_done + Plansheet.parse_date_duration(@last_for) if @last_done
315
+ end
316
+
211
317
  def lead_time_deferral
212
318
  [(due - Plansheet.parse_date_duration(@lead_time)),
213
319
  Date.today].max
214
320
  end
215
321
 
322
+ def recurring_due?
323
+ !@frequency.nil? || !@day_of_week.nil?
324
+ end
325
+
216
326
  def recurring?
217
- !@frequency.nil?
327
+ !@frequency.nil? || !@day_of_week.nil? || !@last_done.nil?
218
328
  end
219
329
 
220
330
  def dropped_or_done?
221
331
  status == "dropped" || status == "done"
222
332
  end
223
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
+
224
338
  def to_h
225
339
  h = { "project" => @name, "namespace" => @namespace }
226
340
  ALL_PROPERTIES.each do |prop|
@@ -5,12 +5,10 @@ module Plansheet
5
5
  # The Sheet class constructs a Markdown/LaTeX file for use with pandoc
6
6
  class Sheet
7
7
  def initialize(output_file, project_arr)
8
- sorted_arr = project_arr.sort!
9
-
10
8
  projects_str = String.new
11
9
  projects_str << sheet_header
12
10
 
13
- sorted_arr.each do |p|
11
+ project_arr.each do |p|
14
12
  projects_str << project_minipage(p)
15
13
  end
16
14
  puts "Writing to #{output_file}"
@@ -30,19 +28,26 @@ module Plansheet
30
28
 
31
29
  def project_minipage(proj)
32
30
  str = String.new
33
- str << "\\begin{minipage}{4.5cm}\n"
31
+ str << "\\begin{minipage}{6cm}\n"
34
32
  str << project_header(proj)
35
33
  proj&.tasks&.each do |t|
36
- str << "$\\square$ #{t} \\\\\n"
34
+ str << "$\\square$ #{sanitize_string(t)} \\\\\n"
37
35
  end
38
36
  str << "\\end{minipage}\n"
39
37
  str
40
38
  end
41
39
 
40
+ def sanitize_string(str)
41
+ str.gsub("_", '\_')
42
+ end
43
+
42
44
  def project_header(proj)
43
45
  str = String.new
44
- str << "#{proj.name} - #{proj.status}"
45
- str << " - #{proj.location}" if proj.location
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
46
51
  str << " \\\\\n"
47
52
  str
48
53
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plansheet
4
- VERSION = "0.15.0"
4
+ VERSION = "0.23.1"
5
5
  end
data/lib/plansheet.rb CHANGED
@@ -1,11 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "plansheet/version"
4
- require_relative "plansheet/project"
5
4
  require_relative "plansheet/pool"
6
- require_relative "plansheet/sheet"
7
5
  require "yaml"
8
- require "kwalify"
9
6
 
10
7
  module Plansheet
11
8
  class Error < StandardError; 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.15.0
4
+ version: 0.23.1
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-09 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
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rgl
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 0.5.8
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 0.5.8
27
41
  description: Convert YAML project files into a nice PDF
28
42
  email:
29
43
  - dave@dafyddcrosby.com