plansheet 0.13.2 → 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: 76e5120430c540166b342a58cf0a0c07dfaf773d0a46b1dcd05614be677fe1a6
4
- data.tar.gz: 83eb24c7811f8e1e49d8f7f79a059c1451673907460203d95a5c28e98fbc74c2
3
+ metadata.gz: 209e9e64f6e8f0ceba2e1182e60106ead59d0943331c940a9a38ff788ab2cd4f
4
+ data.tar.gz: 559296847d7777c6d339fd941f43dc8b106b00b75efa354a95ff78adcfc715b3
5
5
  SHA512:
6
- metadata.gz: 71d3b9dad2c16601ef4472805c282cbfced288e0fc1cee42b0b2e6cb508c5aa255a688c3301fbdee38ae5188f36a2d65cbee2ebc9aa3290771a8f2a2f84854ac
7
- data.tar.gz: bc7d994b5d320c0c9f7ed53ca625afb6f586f228a48f16e2e9f1b2f392cae4cb1e1cef45ddc305069fb06a59a8e1fa9cf2324c1dc4a322e411f801d5802c41fe
6
+ metadata.gz: d883a334412977859444a6db5c33885ced07fabcfa8665aa0331be6609fc94bb7c3d15f4e5fa6a8b4c01bf7e537b4c281b88c90ac30337fcc3c326ef828dfb29
7
+ data.tar.gz: 4033cae733fd661360022a705b9d3098d07431db2d2377406af95eeb52fc60ace9ba881803557e8146c8bd48524e3f07cd0ab5eff9859b83633c51fc41115306
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.13.2)
4
+ plansheet (0.23.0)
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,18 +37,36 @@ options = {}
25
37
  parser.parse!(into: options)
26
38
 
27
39
  config = Plansheet.load_config
40
+ pool = Plansheet::Pool.new({ projects_dir: config["projects_dir"],
41
+ sort_order: config["sort_order"] })
28
42
 
29
43
  if options[:sheet] || options.empty?
30
- project_arr = Plansheet.load_projects_dir config["projects_dir"]
31
-
44
+ require "plansheet/sheet"
32
45
  Dir.mkdir config["output_dir"] unless Dir.exist? config["output_dir"]
33
-
34
- Plansheet::Sheet.new("#{config["output_dir"]}/projects.md", project_arr)
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"
35
54
  elsif options[:sort]
36
- Plansheet.resort_projects_in_dir config["projects_dir"]
55
+ # Pool sorts projects, this now just matches old behaviour
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
37
67
  elsif options[:cli]
38
- project_arr = Plansheet.load_projects_dir config["projects_dir"]
39
- project_arr.sort!
68
+ # TODO: add a project filter method
69
+ project_arr = pool.projects
40
70
  project_arr.delete_if { |x| x.status == "dropped" || x.status == "done" }
41
71
  project_arr.select! { |x| x.location == options[:location_filter] } if options[:location_filter]
42
72
  project_arr.each do |proj|
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rgl/adjacency"
4
+ require "rgl/topsort"
5
+
6
+ module Plansheet
7
+ # The "pool" is the aggregated collection of projects, calendar events, etc.
8
+ class Pool
9
+ attr_accessor :projects
10
+
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)
22
+ @projects_dir = config[:projects_dir]
23
+ @sort_order = config[:sort_order]
24
+ # @completed_projects_dir = config(:completed_projects_dir)
25
+
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
39
+ end
40
+
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.
75
+ @projects.sort!
76
+ end
77
+
78
+ def project_namespaces
79
+ @projects.collect(&:namespace).uniq.sort
80
+ end
81
+
82
+ def projects_in_namespace(namespace)
83
+ @projects.select { |x| x.namespace == namespace }
84
+ end
85
+
86
+ def write_projects
87
+ # TODO: This leaves potential for duplicate projects where empty files
88
+ # are involved once completed project directories are a thing - will need
89
+ # to keep a list of project files to delete
90
+ project_namespaces.each do |ns|
91
+ pyf = ProjectYAMLFile.new "#{@projects_dir}/#{ns}.yml"
92
+ pyf.projects = projects_in_namespace(ns)
93
+ pyf.write
94
+ end
95
+ end
96
+
97
+ def load_projects_dir(dir)
98
+ project_arr = []
99
+ projects = Dir.glob("*yml", base: dir)
100
+ projects.each do |l|
101
+ project_arr << ProjectYAMLFile.new(File.join(dir, l)).load_file
102
+ end
103
+
104
+ @projects = project_arr.flatten!
105
+ end
106
+ end
107
+ end
@@ -4,7 +4,9 @@ module Plansheet
4
4
  class Project
5
5
  def to_s
6
6
  str = String.new
7
- str << "# #{@name}\n"
7
+ str << "# "
8
+ str << "#{@namespace} - " if @namespace
9
+ str << "#{@name}\n"
8
10
  STRING_PROPERTIES.each do |o|
9
11
  str << stringify_string_property(o)
10
12
  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,11 +20,10 @@ module Plansheet
16
20
  "project":
17
21
  desc: Project name
18
22
  type: str
19
- required: yes
23
+ unique: true
20
24
  "namespace":
21
- desc: Namespace of a group of projects (for organizing)
25
+ desc: Project name
22
26
  type: str
23
- required: yes
24
27
  "priority":
25
28
  desc: Project priority
26
29
  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
@@ -114,17 +145,19 @@ module Plansheet
114
145
  PROJECT_SCHEMA = YAML.safe_load(PROJECT_YAML_SCHEMA)
115
146
 
116
147
  class ProjectYAMLFile
117
- attr_reader :projects
148
+ attr_accessor :projects
118
149
 
119
150
  def initialize(path)
120
151
  @path = path
121
152
  # TODO: this won't GC, inline validation instead?
153
+ end
122
154
 
155
+ def load_file
123
156
  # Handle pre-Ruby 3.1 psych versions (this is brittle)
124
157
  @raw = if Psych::VERSION.split(".")[0].to_i >= 4
125
- YAML.load_file(path, permitted_classes: [Date])
158
+ YAML.load_file(@path, permitted_classes: [Date])
126
159
  else
127
- YAML.load_file(path)
160
+ YAML.load_file(@path)
128
161
  end
129
162
 
130
163
  validate_schema
@@ -133,6 +166,7 @@ module Plansheet
133
166
  proj["namespace"] = namespace
134
167
  Project.new proj
135
168
  end
169
+ @projects
136
170
  end
137
171
 
138
172
  def namespace
@@ -155,8 +189,12 @@ module Plansheet
155
189
  @projects.sort!
156
190
  end
157
191
 
192
+ def write
193
+ File.write @path, yaml_dump
194
+ end
195
+
158
196
  def yaml_dump
159
- YAML.dump(@projects.map(&:to_h))
197
+ YAML.dump(@projects.map { |x| x.to_h.delete_if { |k, _| k == "namespace" } })
160
198
  end
161
199
  end
162
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,26 +69,31 @@ 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[namespace 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
92
+ attr_accessor :namespace
59
93
 
60
94
  def initialize(options)
61
95
  @name = options["project"]
96
+ @namespace = options["namespace"]
62
97
 
63
98
  ALL_PROPERTIES.each do |o|
64
99
  instance_variable_set("@#{o}", options[o]) if options[o]
@@ -74,12 +109,51 @@ module Plansheet
74
109
  # date/external commits/penalties for project failure, etc
75
110
  #
76
111
  # Assume all projects are low priority unless stated otherwise.
77
- @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
+ # 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
78
152
  end
79
153
 
80
154
  def <=>(other)
81
155
  ret_val = 0
82
- DEFAULT_COMPARISON_ORDER.each do |method|
156
+ COMPARISON_ORDER_SYMS.each do |method|
83
157
  ret_val = send(method, other)
84
158
  break if ret_val != 0
85
159
  end
@@ -87,7 +161,15 @@ module Plansheet
87
161
  end
88
162
 
89
163
  def compare_priority(other)
90
- PROJECT_PRIORITY[@priority] <=> PROJECT_PRIORITY[other.priority]
164
+ priority_val <=> other.priority_val
165
+ end
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
91
173
  end
92
174
 
93
175
  def compare_status(other)
@@ -115,19 +197,25 @@ module Plansheet
115
197
  receiver <=> comparison
116
198
  end
117
199
 
118
- def compare_dependency(other)
119
- return 0 if @dependencies.nil? && other.dependencies.nil?
200
+ def dependency_of?(other)
201
+ other&.dependencies&.any? do |dep|
202
+ @name&.downcase == dep.downcase
203
+ end
204
+ end
120
205
 
121
- if @dependencies.nil?
122
- return -1 if other.dependencies.any? do |dep|
123
- @name.downcase == dep.downcase
124
- end
125
- elsif @dependencies.any? do |dep|
126
- other.name.downcase == dep.downcase
127
- end
128
- return 1
206
+ def dependent_on?(other)
207
+ @dependencies&.any? do |dep|
208
+ other&.name&.downcase == dep.downcase
129
209
  end
130
- 0
210
+ end
211
+
212
+ def compare_dependency(other)
213
+ # This approach might seem odd,
214
+ # but it's to handle circular dependencies
215
+ retval = 0
216
+ retval -= 1 if dependency_of?(other)
217
+ retval += 1 if dependent_on?(other)
218
+ retval
131
219
  end
132
220
 
133
221
  # Projects that are dropped or done are considered "complete", insofar as
@@ -173,7 +261,8 @@ module Plansheet
173
261
 
174
262
  def subsequent_recurring_status
175
263
  return "done" if @lead_time && defer > Date.today
176
- return "done" if due > Date.today
264
+ return "done" if @last_for && defer > Date.today
265
+ return "done" if due && due > Date.today
177
266
 
178
267
  task_based_status
179
268
  end
@@ -186,41 +275,62 @@ module Plansheet
186
275
  # Due date either explicit or recurring
187
276
  def due
188
277
  return @due if @due
189
- return recurring_due_date if recurring?
278
+ return recurring_due_date if recurring_due?
190
279
 
191
280
  nil
192
281
  end
193
282
 
194
283
  def recurring_due_date
195
284
  if @last_done
196
- @last_done + Plansheet.parse_date_duration(@frequency)
197
- else
198
- 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
199
293
  end
294
+
295
+ # Going to assume this is the first time, so due today
296
+ Date.today
200
297
  end
201
298
 
202
299
  def defer
203
300
  return @defer if @defer
204
301
  return lead_time_deferral if @lead_time && due
302
+ return last_for_deferral if @last_for
205
303
 
206
304
  nil
207
305
  end
208
306
 
307
+ def last_for_deferral
308
+ return @last_done + Plansheet.parse_date_duration(@last_for) if @last_done
309
+ end
310
+
209
311
  def lead_time_deferral
210
312
  [(due - Plansheet.parse_date_duration(@lead_time)),
211
313
  Date.today].max
212
314
  end
213
315
 
316
+ def recurring_due?
317
+ !@frequency.nil? || !@day_of_week.nil?
318
+ end
319
+
214
320
  def recurring?
215
- !@frequency.nil?
321
+ !@frequency.nil? || !@day_of_week.nil? || !@last_done.nil?
216
322
  end
217
323
 
218
324
  def dropped_or_done?
219
325
  status == "dropped" || status == "done"
220
326
  end
221
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
+
222
332
  def to_h
223
- h = { "project" => @name }
333
+ h = { "project" => @name, "namespace" => @namespace }
224
334
  ALL_PROPERTIES.each do |prop|
225
335
  h[prop] = instance_variable_get("@#{prop}") if instance_variable_defined?("@#{prop}")
226
336
  end
@@ -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.13.2"
4
+ VERSION = "0.23.0"
5
5
  end
data/lib/plansheet.rb CHANGED
@@ -1,36 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "plansheet/version"
4
- require_relative "plansheet/project"
5
- require_relative "plansheet/sheet"
4
+ require_relative "plansheet/pool"
6
5
  require "yaml"
7
- require "kwalify"
8
6
 
9
7
  module Plansheet
10
8
  class Error < StandardError; end
11
9
 
10
+ # TODO: config schema validation
12
11
  def self.load_config
13
12
  YAML.load_file "#{Dir.home}/.plansheet.yml"
14
13
  rescue StandardError
15
14
  abort "unable to load plansheet config file"
16
15
  end
17
-
18
- def self.resort_projects_in_dir(dir)
19
- project_files = Dir.glob("#{dir}/*yml")
20
- project_files.each do |f|
21
- pyf = ProjectYAMLFile.new(f)
22
- pyf.sort!
23
- File.write(f, pyf.yaml_dump)
24
- end
25
- end
26
-
27
- def self.load_projects_dir(dir)
28
- project_arr = []
29
- projects = Dir.glob("*yml", base: dir)
30
- projects.each do |l|
31
- project_arr << ProjectYAMLFile.new(File.join(dir, l)).projects
32
- end
33
-
34
- project_arr.flatten!
35
- end
36
16
  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.13.2
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-08 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
@@ -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
@@ -43,6 +57,7 @@ files:
43
57
  - examples/backpack.yml
44
58
  - exe/plansheet
45
59
  - lib/plansheet.rb
60
+ - lib/plansheet/pool.rb
46
61
  - lib/plansheet/project.rb
47
62
  - lib/plansheet/project/stringify.rb
48
63
  - lib/plansheet/project/yaml.rb