plansheet 0.25.1 → 0.30.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: 1d6783f3c90e7d781de642be7c98e2a8469e48d8576405a5554cf92282632f2e
4
- data.tar.gz: 39de90d2ad1d6c371c1626b56ffba32d79e293b9adaa0c01bb3d8113cc16225d
3
+ metadata.gz: f23116f57440ffd0f1b3d290b93ab5eb71417b184464a806bd2a522963ae2ed1
4
+ data.tar.gz: f5d463a78642c72a03f736e635b422f147c22699cca5440fa16672249187d5e4
5
5
  SHA512:
6
- metadata.gz: d30350f829b3db7e0b6d1163d55b835c55bef408c1c34ffc16a209837b9f4f1573b2d51d46fe294c4e48eeb1018a11a04701b100edce353c68c39becff5c9cca
7
- data.tar.gz: fac00b40c31909b7d785e8938f49f877f02872875d72246bc8413dd84d34b857a0a3840605d0e22ac60a288091a6d28629eb915bb3ece4b2930c5e1ea8fbf841
6
+ metadata.gz: 449bde685c82452689f88fe3faf82d91a336d911ba07dcbaf9e73f421552bf62a005ffb354f21ed1dfe244b46187384166ac8856434ad1728e4336a55b5a0fba
7
+ data.tar.gz: f1366fd8b552b2800ace87bb376cf296fa00467f75c3823dd43966f9dab6fb940af2d172868a6e030afdf531039b743ff2dce9cdc0e87840b99c22a00dcc0db2
data/.rubocop.yml CHANGED
@@ -1,24 +1,5 @@
1
+ inherit_gem:
2
+ dc-rubocop: default.yml
3
+
1
4
  AllCops:
2
5
  TargetRubyVersion: 2.6
3
- NewCops: enable
4
- SuggestExtensions: false
5
-
6
- Style/StringLiterals:
7
- Enabled: true
8
- EnforcedStyle: double_quotes
9
-
10
- Style/StringLiteralsInInterpolation:
11
- Enabled: true
12
- EnforcedStyle: double_quotes
13
-
14
- Style/GuardClause:
15
- Enabled: false
16
-
17
- Layout/LineLength:
18
- Max: 120
19
-
20
- Metrics:
21
- Enabled: false
22
-
23
- Style/Documentation:
24
- Enabled: false
data/Gemfile CHANGED
@@ -6,13 +6,6 @@ source "https://rubygems.org"
6
6
  gemspec
7
7
 
8
8
  group :development, optional: true do
9
- gem "guard", "~> 2.18"
10
- gem "guard-rake", "~> 1.0"
11
- gem "minitest", "~> 5.0"
12
- gem "rake", "~> 13.0"
9
+ gem "dc-devtools"
13
10
  gem "rdoc"
14
-
15
- gem "rubocop", "~> 1.21"
16
- gem "rubocop-minitest"
17
- gem "rubocop-rake"
18
11
  end
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- plansheet (0.25.1)
4
+ plansheet (0.30.0)
5
5
  dc-kwalify (~> 1.0)
6
6
  diffy (= 3.4.2)
7
7
  rgl (= 0.5.8)
@@ -11,7 +11,18 @@ GEM
11
11
  specs:
12
12
  ast (2.4.2)
13
13
  coderay (1.1.3)
14
+ dc-devtools (0.0.202207232239)
15
+ dc-rubocop (>= 0.0.3)
16
+ guard (~> 2.18)
17
+ guard-rake (~> 1.0)
18
+ minitest
19
+ rake (~> 13.0)
20
+ rubocop (~> 1.30)
21
+ rubocop-minitest
22
+ rubocop-rake (~> 0.6)
14
23
  dc-kwalify (1.0.0)
24
+ dc-rubocop (0.0.3)
25
+ rubocop
15
26
  diffy (3.4.2)
16
27
  ffi (1.15.5)
17
28
  formatador (1.1.0)
@@ -28,13 +39,14 @@ GEM
28
39
  guard-rake (1.0.0)
29
40
  guard
30
41
  rake
42
+ json (2.6.2)
31
43
  lazy_priority_queue (0.1.1)
32
44
  listen (3.7.1)
33
45
  rb-fsevent (~> 0.10, >= 0.10.3)
34
46
  rb-inotify (~> 0.9, >= 0.9.10)
35
47
  lumberjack (1.2.8)
36
48
  method_source (1.0.0)
37
- minitest (5.15.0)
49
+ minitest (5.16.2)
38
50
  nenv (0.3.0)
39
51
  notiffany (0.1.3)
40
52
  nenv (~> 0.1)
@@ -54,24 +66,25 @@ GEM
54
66
  ffi (~> 1.0)
55
67
  rdoc (6.4.0)
56
68
  psych (>= 4.0.0)
57
- regexp_parser (2.4.0)
69
+ regexp_parser (2.5.0)
58
70
  rexml (3.2.5)
59
71
  rgl (0.5.8)
60
72
  lazy_priority_queue (~> 0.1.0)
61
73
  rexml (~> 3.2, >= 3.2.4)
62
74
  stream (~> 0.5.3)
63
- rubocop (1.29.1)
75
+ rubocop (1.32.0)
76
+ json (~> 2.3)
64
77
  parallel (~> 1.10)
65
78
  parser (>= 3.1.0.0)
66
79
  rainbow (>= 2.2.2, < 4.0)
67
80
  regexp_parser (>= 1.8, < 3.0)
68
81
  rexml (>= 3.2.5, < 4.0)
69
- rubocop-ast (>= 1.17.0, < 2.0)
82
+ rubocop-ast (>= 1.19.1, < 2.0)
70
83
  ruby-progressbar (~> 1.7)
71
84
  unicode-display_width (>= 1.4.0, < 3.0)
72
- rubocop-ast (1.18.0)
85
+ rubocop-ast (1.19.1)
73
86
  parser (>= 3.1.1.0)
74
- rubocop-minitest (0.20.0)
87
+ rubocop-minitest (0.20.1)
75
88
  rubocop (>= 0.90, < 2.0)
76
89
  rubocop-rake (0.6.0)
77
90
  rubocop (~> 1.0)
@@ -81,21 +94,15 @@ GEM
81
94
  generator
82
95
  stringio (3.0.2)
83
96
  thor (1.2.1)
84
- unicode-display_width (2.1.0)
97
+ unicode-display_width (2.2.0)
85
98
 
86
99
  PLATFORMS
87
100
  x86_64-linux
88
101
 
89
102
  DEPENDENCIES
90
- guard (~> 2.18)
91
- guard-rake (~> 1.0)
92
- minitest (~> 5.0)
103
+ dc-devtools
93
104
  plansheet!
94
- rake (~> 13.0)
95
105
  rdoc
96
- rubocop (~> 1.21)
97
- rubocop-minitest
98
- rubocop-rake
99
106
 
100
107
  BUNDLED WITH
101
108
  2.3.6
data/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # Plansheet
2
2
 
3
- Plansheet is a simple to-do list generator program that creates a
4
- structured printout (a la [pandoc](https://pandoc.org)) from a
5
- pile of formatted [YAML](https://yaml.org) files. Still very much a work-in-progress
3
+ Plansheet is a simple to-do list generator program that creates a LaTeX
4
+ printout (that can be turned into a PDF by pdflatex) from a pile of formatted
5
+ [YAML](https://yaml.org) files.
6
+
7
+ Plansheet is still a work-in-progress, and while effort is made not to break over minor/patch versions, it's best effort. If you aren't comfortable with fixing Ruby using a lot of metaprogramming, check back in a year or two
6
8
 
7
9
  ## Installation
8
10
 
data/exe/plansheet CHANGED
@@ -46,15 +46,18 @@ pool = Plansheet::Pool.new({ projects_dir: config["projects_dir"],
46
46
 
47
47
  if options[:sheet] || options.empty?
48
48
  require "plansheet/sheet"
49
- Dir.mkdir config["output_dir"] unless Dir.exist? config["output_dir"]
50
- Plansheet::Sheet.new("#{config["output_dir"]}/projects.md", pool.projects)
49
+ FileUtils.mkdir_p config["output_dir"]
50
+ Plansheet::LaTeXSheet.new("#{config["output_dir"]}/projects.tex", pool.projects)
51
51
  elsif options[:irb]
52
52
  binding.irb # rubocop:disable Lint/Debugger
53
53
  elsif options[:stats]
54
+ require "plansheet/time"
55
+ include Plansheet::TimeUtils # rubocop:disable Style/MixinUsage
54
56
  puts "# of projects: #{pool.projects.count}"
55
57
  puts "# of tasks: #{pool.projects.sum { |x| x&.tasks&.count || 0 }}"
56
58
  puts "# of locations: #{pool.projects.collect(&:location).flatten.delete_if(&:nil?).uniq.count}"
57
- puts "combined time estimate: #{pool.projects.sum { |x| x.time_estimate_minutes || 0 }} minutes"
59
+ time = Plansheet::TimeUtils.build_time_duration(pool.projects.sum { |x| x.time_estimate_minutes || 0 })
60
+ puts "combined time estimate: #{time}"
58
61
  elsif options[:sort]
59
62
  # Pool sorts projects, this now just matches old behaviour
60
63
  pool.write_projects
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "pathname"
3
4
  require "rgl/adjacency"
4
5
  require "rgl/topsort"
5
6
 
@@ -23,7 +24,6 @@ module Plansheet
23
24
  def initialize(config, debug: false)
24
25
  @projects_dir = config[:projects_dir]
25
26
  @sort_order = config[:sort_order]
26
- # @completed_projects_dir = config(:completed_projects_dir)
27
27
 
28
28
  # This bit of trickiness is because we don't know what the sort order is
29
29
  # until runtime. I'm sure this design decision definitely won't bite me
@@ -77,6 +77,25 @@ module Plansheet
77
77
  @projects.sort!
78
78
  end
79
79
 
80
+ def archive_projects
81
+ archive_dir = "#{@projects_dir}/archive/"
82
+ FileUtils.mkdir_p archive_dir
83
+
84
+ # NOTE: It would save writes if we sorted and did all month/namespaces
85
+ # writes only once, but as the normal case only sees a few projects
86
+ # archived at a time, I'll leave that for someone else to implement ;-)
87
+ projects_to_archive = @projects.select(&:archivable?)
88
+ projects_to_archive.each do |project|
89
+ path = Pathname.new "#{archive_dir}/#{project.archive_month}/#{project.namespace}.yml"
90
+ Dir.mkdir path.dirname unless path.dirname.exist?
91
+ pyf = ProjectYAMLFile.new path
92
+ pyf.append_project project
93
+ end
94
+
95
+ # Now that the projects have been archived, remove them from the pool
96
+ @projects.reject!(&:archivable?)
97
+ end
98
+
80
99
  def project_namespaces
81
100
  @projects.collect(&:namespace).uniq.sort
82
101
  end
@@ -86,12 +105,16 @@ module Plansheet
86
105
  end
87
106
 
88
107
  def write_projects
89
- # TODO: This leaves potential for duplicate projects where empty files
90
- # are involved once completed project directories are a thing - will need
91
- # to keep a list of project files to delete
92
- project_namespaces.each do |ns|
108
+ # Collect the namespaces *before* archiving is done, for the case where
109
+ # all active projects in a namespace have been completed and archived
110
+ namespaces_before_archiving = project_namespaces
111
+
112
+ archive_projects
113
+
114
+ namespaces_before_archiving.each do |ns|
115
+ projects = projects_in_namespace(ns)
93
116
  pyf = ProjectYAMLFile.new "#{@projects_dir}/#{ns}.yml"
94
- pyf.compare_and_write projects_in_namespace(ns)
117
+ pyf.compare_and_write projects
95
118
  end
96
119
  end
97
120
 
@@ -4,6 +4,7 @@ require "yaml"
4
4
  require "date"
5
5
  require "pathname"
6
6
 
7
+ require "diffy"
7
8
  require "kwalify"
8
9
 
9
10
  module Plansheet
@@ -41,6 +42,8 @@ module Plansheet
41
42
  - blocked # project is blocked by another project, but otherwise ready/wip
42
43
  - planning # project in planning phase (set manually)
43
44
  - idea # project is little more than an idea
45
+ - paused # project shouldn't be archived, but should be excluded from
46
+ # generated output unless paused projects are requested
44
47
  - dropped # project has been explicitly dropped, but
45
48
  # want to keep around for reference, etc
46
49
  - done # project is finished, but want to keep around
@@ -99,6 +102,12 @@ module Plansheet
99
102
  "completed_on":
100
103
  desc: When the (non-recurring) project was completed
101
104
  type: date
105
+ "dropped_on":
106
+ desc: When the project was dropped
107
+ type: date
108
+ "paused_on":
109
+ desc: When the project was paused
110
+ type: date
102
111
  "created_on":
103
112
  desc: When the project was created
104
113
  type: date
@@ -152,7 +161,13 @@ module Plansheet
152
161
  # TODO: this won't GC, inline validation instead?
153
162
  end
154
163
 
164
+ def stub_file
165
+ File.write @path, YAML.dump([])
166
+ end
167
+
155
168
  def load_file
169
+ stub_file unless File.exist? @path
170
+
156
171
  # Handle pre-Ruby 3.1 psych versions (this is brittle)
157
172
  @raw = if Psych::VERSION.split(".")[0].to_i >= 4
158
173
  YAML.load_file(@path, permitted_classes: [Date])
@@ -189,16 +204,22 @@ module Plansheet
189
204
  @projects.sort!
190
205
  end
191
206
 
207
+ def append_project(project)
208
+ load_file
209
+ compare_and_write(@projects.append(project))
210
+ end
211
+
192
212
  def compare_and_write(projects)
213
+ load_file
214
+ orig_string = yaml_dump(@projects)
193
215
  updated_projects_string = yaml_dump(projects)
194
216
 
195
217
  # Compare the existing file to the newly generated one - we only want a
196
218
  # write if something has changed
197
- return if updated_projects_string == yaml_dump(load_file)
219
+ return if updated_projects_string == orig_string
198
220
 
199
221
  puts "#{@path} has changed, writing"
200
- require "diffy"
201
- puts Diffy::Diff.new(yaml_dump(load_file), updated_projects_string).to_s(:color)
222
+ puts Diffy::Diff.new(orig_string, updated_projects_string).to_s(:color)
202
223
  File.write @path, updated_projects_string
203
224
  end
204
225
 
@@ -4,6 +4,7 @@ require "yaml"
4
4
  require "date"
5
5
  require_relative "project/yaml"
6
6
  require_relative "project/stringify"
7
+ require_relative "./time"
7
8
 
8
9
  # Needed for Project#time_estimate, would be much happier *not* patching Array
9
10
  class Array
@@ -20,8 +21,9 @@ module Plansheet
20
21
  "waiting" => 4,
21
22
  "planning" => 5,
22
23
  "idea" => 6,
23
- "dropped" => 7,
24
- "done" => 8
24
+ "paused" => 7,
25
+ "dropped" => 8,
26
+ "done" => 9
25
27
  }.freeze
26
28
 
27
29
  # Pre-compute the next days-of-week
@@ -30,36 +32,6 @@ module Plansheet
30
32
  [d.strftime("%A"), d]
31
33
  end.freeze
32
34
 
33
- def self.parse_date_duration(str)
34
- return Regexp.last_match(1).to_i if str.strip.match(/(\d+)[dD]/)
35
- return (Regexp.last_match(1).to_i * 7) if str.strip.match(/(\d+)[wW]/)
36
-
37
- raise "Can't parse time duration string #{str}"
38
- end
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
-
63
35
  # The use of instance_variable_set/get probably seems a bit weird, but the
64
36
  # intent is to avoid object allocation on non-existent project properties, as
65
37
  # well as avoiding a bunch of copy-paste boilerplate when adding a new
@@ -68,6 +40,7 @@ module Plansheet
68
40
  # unwrap the loops if it's not needed.
69
41
  class Project
70
42
  include Comparable
43
+ include Plansheet::TimeUtils
71
44
 
72
45
  TIME_EST_REGEX = /\((\d+\.?\d*[mMhH])\)$/.freeze
73
46
  TIME_EST_REGEX_NO_CAPTURE = /\(\d+\.?\d*[mMhH]\)$/.freeze
@@ -83,7 +56,8 @@ module Plansheet
83
56
  # namespace is derived from file name
84
57
  STRING_PROPERTIES = %w[priority status location notes time_estimate daily_time_roi weekly_time_roi yearly_time_roi
85
58
  day_of_week frequency last_for lead_time].freeze
86
- DATE_PROPERTIES = %w[due defer completed_on created_on starts_on last_done last_reviewed].freeze
59
+ DATE_PROPERTIES = %w[due defer paused_on dropped_on completed_on created_on starts_on last_done
60
+ last_reviewed].freeze
87
61
  ARRAY_PROPERTIES = %w[dependencies externals urls tasks done tags].freeze
88
62
 
89
63
  ALL_PROPERTIES = STRING_PROPERTIES + DATE_PROPERTIES + ARRAY_PROPERTIES
@@ -132,15 +106,15 @@ module Plansheet
132
106
  if @tasks
133
107
  @time_estimate_minutes = @tasks&.select do |t|
134
108
  t.match? TIME_EST_REGEX_NO_CAPTURE
135
- end&.nil_if_empty&.map { |t| Plansheet::Project.task_time_estimate(t) }&.sum
109
+ end&.nil_if_empty&.map { |t| task_time_estimate(t) }&.sum
136
110
  elsif @time_estimate
137
111
  # No tasks with estimates, but there's an explicit time_estimate
138
112
  # Convert the field to minutes
139
- @time_estimate_minutes = Plansheet.parse_time_duration(@time_estimate)
113
+ @time_estimate_minutes = parse_time_duration(@time_estimate)
140
114
  end
141
115
  if @time_estimate_minutes
142
116
  # Rewrite time_estimate field
143
- @time_estimate = Plansheet.build_time_duration(@time_estimate_minutes)
117
+ @time_estimate = build_time_duration(@time_estimate_minutes)
144
118
 
145
119
  yms = yearly_minutes_saved
146
120
  @time_roi_payoff = yms.to_f / @time_estimate_minutes if yms
@@ -152,16 +126,26 @@ module Plansheet
152
126
  remove_instance_variable("@time_estimate") if @time_estimate
153
127
  remove_instance_variable("@time_estimate_minutes") if @time_estimate
154
128
  remove_instance_variable("@time_roi_payoff") if @time_roi_payoff
129
+ elsif paused?
130
+ @paused_on ||= Date.today
131
+ remove_instance_variable("@status") if @status
132
+ elsif dropped?
133
+ @dropped_on ||= Date.today
134
+ remove_instance_variable("@status") if @status
155
135
  end
156
136
  end
157
137
 
138
+ def archive_month
139
+ @completed_on&.strftime("%Y-%m") || Date.today.strftime("%Y-%m")
140
+ end
141
+
158
142
  def yearly_minutes_saved
159
143
  if @daily_time_roi
160
- Plansheet.parse_time_duration(@daily_time_roi) * 365
144
+ parse_time_duration(@daily_time_roi) * 365
161
145
  elsif @weekly_time_roi
162
- Plansheet.parse_time_duration(@weekly_time_roi) * 52
146
+ parse_time_duration(@weekly_time_roi) * 52
163
147
  elsif @yearly_time_roi
164
- Plansheet.parse_time_duration(@yearly_time_roi)
148
+ parse_time_duration(@yearly_time_roi)
165
149
  end
166
150
  end
167
151
 
@@ -249,10 +233,10 @@ module Plansheet
249
233
  # Projects that are dropped or done are considered "complete", insofar as
250
234
  # they are only kept around for later reference.
251
235
  def compare_completeness(other)
252
- return 0 if dropped_or_done? && other.dropped_or_done?
253
- return 0 if !dropped_or_done? && !other.dropped_or_done?
254
-
255
- dropped_or_done? ? 1 : -1
236
+ retval = 0
237
+ retval += 1 if dropped_or_done?
238
+ retval -= 1 if other.dropped_or_done?
239
+ retval
256
240
  end
257
241
 
258
242
  def status
@@ -260,6 +244,8 @@ module Plansheet
260
244
  return recurring_status if recurring?
261
245
  return task_based_status if @tasks || @done
262
246
  return "done" if @completed_on && @tasks.nil?
247
+ return "dropped" if @dropped_on
248
+ return "paused" if @paused_on
263
249
 
264
250
  "idea"
265
251
  end
@@ -305,7 +291,7 @@ module Plansheet
305
291
 
306
292
  def recurring_due_date
307
293
  if @last_done
308
- return @last_done + Plansheet.parse_date_duration(@frequency) if @frequency
294
+ return @last_done + parse_date_duration(@frequency) if @frequency
309
295
 
310
296
  if @day_of_week
311
297
  return Date.today + 7 if @last_done == Date.today
@@ -328,11 +314,13 @@ module Plansheet
328
314
  end
329
315
 
330
316
  def last_for_deferral
331
- return @last_done + Plansheet.parse_date_duration(@last_for) if @last_done
317
+ return @last_done + parse_date_duration(@last_for) if @last_done
318
+
319
+ Date.today
332
320
  end
333
321
 
334
322
  def lead_time_deferral
335
- [(due - Plansheet.parse_date_duration(@lead_time)),
323
+ [(due - parse_date_duration(@lead_time)),
336
324
  Date.today].max
337
325
  end
338
326
 
@@ -341,19 +329,31 @@ module Plansheet
341
329
  end
342
330
 
343
331
  def recurring?
344
- !@frequency.nil? || !@day_of_week.nil? || !@last_done.nil?
332
+ !@frequency.nil? || !@day_of_week.nil? || !@last_done.nil? || !@last_for.nil?
345
333
  end
346
334
 
347
- def dropped_or_done?
348
- status == "dropped" || status == "done"
335
+ def paused?
336
+ status == "paused"
337
+ end
338
+
339
+ def dropped?
340
+ status == "dropped"
349
341
  end
350
342
 
351
343
  def done?
352
344
  status == "done"
353
345
  end
354
346
 
355
- def self.task_time_estimate(str)
356
- Plansheet.parse_time_duration(Regexp.last_match(1)) if str.match(TIME_EST_REGEX)
347
+ def dropped_or_done?
348
+ dropped? || done?
349
+ end
350
+
351
+ def archivable?
352
+ (!recurring? && @completed_on) || dropped?
353
+ end
354
+
355
+ def task_time_estimate(str)
356
+ parse_time_duration(Regexp.last_match(1)) if str.match(TIME_EST_REGEX)
357
357
  end
358
358
 
359
359
  def to_h
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ module Plansheet
5
+ # The Sheet class constructs a Markdown/LaTeX file for use with pandoc
6
+ class LaTeXSheet
7
+ def initialize(output_file, project_arr)
8
+ projects_str = String.new
9
+ projects_str << sheet_header
10
+
11
+ project_arr.each do |p|
12
+ projects_str << project_minipage(p)
13
+ end
14
+ puts "Writing to #{output_file}"
15
+ projects_str << sheet_footer
16
+ File.write(output_file, projects_str)
17
+ end
18
+
19
+ def sheet_header
20
+ # LaTeX used to be generated by pandoc
21
+ <<~FRONTMATTER
22
+ \\PassOptionsToPackage{unicode}{hyperref}
23
+ \\PassOptionsToPackage{hyphens}{url}
24
+ \\documentclass[]{article}
25
+ \\author{}
26
+ \\date{#{Date.today}}
27
+ \\usepackage{amsmath,amssymb}
28
+ \\usepackage{lmodern}
29
+ \\usepackage{iftex}
30
+ \\usepackage[T1]{fontenc}
31
+ \\usepackage[utf8]{inputenc}
32
+ \\usepackage{textcomp}
33
+ \\IfFileExists{upquote.sty}{\\usepackage{upquote}}{}
34
+ \\IfFileExists{microtype.sty}{
35
+ \\usepackage[]{microtype}
36
+ \\UseMicrotypeSet[protrusion]{basicmath}
37
+ }{}
38
+ \\makeatletter
39
+ \\@ifundefined{KOMAClassName}{
40
+ \\IfFileExists{parskip.sty}{
41
+ \\usepackage{parskip}
42
+ }{
43
+ \\setlength{\\parindent}{0pt}
44
+ \\setlength{\\parskip}{6pt plus 2pt minus 1pt}}
45
+ }{
46
+ \\KOMAoptions{parskip=half}}
47
+ \\makeatother
48
+ \\usepackage{xcolor}
49
+ \\IfFileExists{xurl.sty}{\\usepackage{xurl}}{}
50
+ \\IfFileExists{bookmark.sty}{\\usepackage{bookmark}}{\\usepackage{hyperref}}
51
+ \\hypersetup{
52
+ hidelinks,
53
+ pdfcreator={LaTeX via plansheet}}
54
+ \\urlstyle{same}
55
+ \\usepackage[margin=1.5cm]{geometry}
56
+ \\setlength{\\emergencystretch}{3em}
57
+ \\providecommand{\\tightlist}{
58
+ \\setlength{\\itemsep}{0pt}\\setlength{\\parskip}{0pt}}
59
+ \\setcounter{secnumdepth}{-\\maxdimen}
60
+
61
+ \\begin{document}
62
+
63
+ \\thispagestyle{empty}
64
+
65
+ \\section{Date: #{Date.today}}
66
+ FRONTMATTER
67
+ end
68
+
69
+ def sheet_footer
70
+ '\end{document}'
71
+ end
72
+
73
+ def project_minipage(proj)
74
+ str = String.new
75
+ str << "\\begin{minipage}{6cm}\n"
76
+ str << project_header(proj)
77
+ proj&.tasks&.each do |t|
78
+ str << "$\\square$ #{sanitize_string(t)} \\\\\n"
79
+ end
80
+ str << "\\end{minipage}\n"
81
+ str
82
+ end
83
+
84
+ def sanitize_string(str)
85
+ str.gsub("_", '\_')
86
+ end
87
+
88
+ def project_header(proj)
89
+ str = String.new
90
+ str << "#{sanitize_string(proj.namespace)}: #{sanitize_string(proj.name)}\\\\\n"
91
+ str << proj.status.to_s
92
+ str << " - #{sanitize_string(proj.location)}" if proj.location
93
+ str << " due: #{proj.due}" if proj.due
94
+ str << " time: #{proj.time_estimate}" if proj.time_estimate
95
+ str << " \\\\\n"
96
+ str
97
+ end
98
+ end
99
+ end
@@ -1,55 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "date"
4
- module Plansheet
5
- # The Sheet class constructs a Markdown/LaTeX file for use with pandoc
6
- class Sheet
7
- def initialize(output_file, project_arr)
8
- projects_str = String.new
9
- projects_str << sheet_header
10
-
11
- project_arr.each do |p|
12
- projects_str << project_minipage(p)
13
- end
14
- puts "Writing to #{output_file}"
15
- File.write(output_file, projects_str)
16
- end
17
-
18
- def sheet_header
19
- <<~FRONTMATTER
20
- ---
21
- geometry: margin=1.5cm
22
- ---
23
- \\thispagestyle{empty}
24
-
25
- # Date: #{Date.today}
26
- FRONTMATTER
27
- end
28
-
29
- def project_minipage(proj)
30
- str = String.new
31
- str << "\\begin{minipage}{6cm}\n"
32
- str << project_header(proj)
33
- proj&.tasks&.each do |t|
34
- str << "$\\square$ #{sanitize_string(t)} \\\\\n"
35
- end
36
- str << "\\end{minipage}\n"
37
- str
38
- end
39
-
40
- def sanitize_string(str)
41
- str.gsub("_", '\_')
42
- end
43
-
44
- def project_header(proj)
45
- str = String.new
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
51
- str << " \\\\\n"
52
- str
53
- end
54
- end
55
- end
3
+ require_relative "sheet/latex"
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plansheet
4
+ module TimeUtils
5
+ def parse_date_duration(str)
6
+ return Regexp.last_match(1).to_i if str.strip.match(/(\d+)[dD]/)
7
+ return (Regexp.last_match(1).to_i * 7) if str.strip.match(/(\d+)[wW]/)
8
+
9
+ raise "Can't parse time duration string #{str}"
10
+ end
11
+
12
+ def parse_time_duration(str)
13
+ if str.match(/(\d+h) (\d+m)/)
14
+ return (parse_time_duration(Regexp.last_match(1)) + parse_time_duration(Regexp.last_match(2)))
15
+ end
16
+
17
+ return Regexp.last_match(1).to_i if str.strip.match(/(\d+)m/)
18
+ return (Regexp.last_match(1).to_f * 60).to_i if str.strip.match(/(\d+\.?\d*)h/)
19
+
20
+ raise "Can't parse time duration string #{str}"
21
+ end
22
+
23
+ def build_time_duration(minutes)
24
+ if minutes > 59
25
+ if (minutes % 60).zero?
26
+ "#{minutes / 60}h"
27
+ else
28
+ "#{minutes / 60}h #{minutes % 60}m"
29
+ end
30
+ else
31
+ "#{minutes}m"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plansheet
4
- VERSION = "0.25.1"
4
+ VERSION = "0.30.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plansheet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.25.1
4
+ version: 0.30.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-07-12 00:00:00.000000000 Z
11
+ date: 2022-07-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dc-kwalify
@@ -76,6 +76,8 @@ files:
76
76
  - lib/plansheet/project/stringify.rb
77
77
  - lib/plansheet/project/yaml.rb
78
78
  - lib/plansheet/sheet.rb
79
+ - lib/plansheet/sheet/latex.rb
80
+ - lib/plansheet/time.rb
79
81
  - lib/plansheet/version.rb
80
82
  homepage: https://dafyddcrosby.com
81
83
  licenses: