plansheet 0.13.2 → 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: 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