Class: Plansheet::Project

Inherits:
Object
  • Object
show all
Includes:
Comparable, TimeUtils
Defined in:
lib/plansheet/project.rb,
lib/plansheet/project/stringify.rb

Overview

The use of instance_variable_set/get probably seems a bit weird, but the intent is to avoid object allocation on non-existent project properties, as well as avoiding a bunch of copy-paste boilerplate when adding a new property. I suspect I’m guilty of premature optimization here, but it’s easier to do this at the start than untangle that later (ie easier to unwrap the loops if it’s not needed.

Constant Summary collapse

TIME_EST_REGEX =
/\((\d+\.?\d*[mMhH])\)$/.freeze
TIME_EST_REGEX_NO_CAPTURE =
/\(\d+\.?\d*[mMhH]\)$/.freeze
PROJECT_PRIORITY =
{
  "high" => 1,
  "medium" => 2,
  "low" => 3
}.freeze
COMPARISON_ORDER_SYMS =
Plansheet::Pool::POOL_COMPARISON_ORDER.map { |x| "compare_#{x}".to_sym }.freeze
STRING_PROPERTIES =

NOTE: The order of these affects presentation! namespace is derived from file name

%w[priority status location notes time_estimate daily_time_roi weekly_time_roi yearly_time_roi
day_of_week frequency last_for lead_time].freeze
DATE_PROPERTIES =
%w[due defer paused_on dropped_on completed_on created_on starts_on last_done
last_reviewed].freeze
ARRAY_PROPERTIES =
%w[dependencies externals urls tasks setup_tasks cleanup_tasks done tags].freeze
ALL_PROPERTIES =
STRING_PROPERTIES + DATE_PROPERTIES + ARRAY_PROPERTIES

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from TimeUtils

#build_time_duration, #parse_date_duration, #parse_time_duration

Constructor Details

#initialize(options) ⇒ Project

Returns a new instance of Project.



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/plansheet/project.rb', line 68

def initialize(options)
  @name = options["project"]
  @namespace = options["namespace"]

  ALL_PROPERTIES.each do |o|
    instance_variable_set("@#{o}", options[o]) if options[o]
  end

  # The "priority" concept feels flawed - it requires *me* to figure out
  # the priority, as opposed to the program understanding the project in
  # relation to other tasks. If I truly understood the priority of all the
  # projects, I wouldn't need a todo list program. The point is to remove
  # the need for willpower/executive function/coffee. The long-term value
  # of this field will diminish as I add more project properties that can
  # automatically hone in on the most important items based on due
  # date/external commits/penalties for project failure, etc
  #
  # Assume all projects are low priority unless stated otherwise.
  @priority_val = if @priority
                    PROJECT_PRIORITY[@priority]
                  else
                    PROJECT_PRIORITY["low"]
                  end

  # Remove stale defer dates
  remove_instance_variable("@defer") if @defer && (@defer < Date.today)

  # Add a created_on field if it doesn't exist
  instance_variable_set("@created_on", Date.today) unless @created_on

  # Handle nil-value tasks
  if @tasks
    @tasks.compact!
    remove_instance_variable("@tasks") if @tasks.empty?
  end

  # Generate time estimate from tasks if specified
  # Stomps time_estimate field
  if @tasks
    @time_estimate_minutes = @tasks&.select do |t|
                               t.match? TIME_EST_REGEX_NO_CAPTURE
                             end&.nil_if_empty&.map { |t| task_time_estimate(t) }&.sum
  elsif @time_estimate
    # No tasks with estimates, but there's an explicit time_estimate
    # Convert the field to minutes
    @time_estimate_minutes = parse_time_duration(@time_estimate)
  end
  if @time_estimate_minutes
    # Rewrite time_estimate field
    @time_estimate = build_time_duration(@time_estimate_minutes)

    yms = yearly_minutes_saved
    @time_roi_payoff = yms.to_f / @time_estimate_minutes if yms
  end

  if done?
    @completed_on ||= Date.today unless recurring?
    remove_instance_variable("@status") if @status
    remove_instance_variable("@time_estimate") if @time_estimate
    remove_instance_variable("@time_estimate_minutes") if @time_estimate
    remove_instance_variable("@time_roi_payoff") if @time_roi_payoff
  elsif paused?
    @paused_on ||= Date.today
    remove_instance_variable("@status") if @status
  elsif dropped?
    @dropped_on ||= Date.today
    remove_instance_variable("@status") if @status
  end
end

Instance Attribute Details

#namespaceObject

Returns the value of attribute namespace.



66
67
68
# File 'lib/plansheet/project.rb', line 66

def namespace
  @namespace
end

Instance Method Details

#<=>(other) ⇒ Object



152
153
154
155
156
157
158
159
# File 'lib/plansheet/project.rb', line 152

def <=>(other)
  ret_val = 0
  COMPARISON_ORDER_SYMS.each do |method|
    ret_val = send(method, other)
    break if ret_val != 0
  end
  ret_val
end

#archivable?Boolean

Returns:

  • (Boolean)


351
352
353
# File 'lib/plansheet/project.rb', line 351

def archivable?
  (!recurring? && @completed_on) || dropped?
end

#archive_monthObject



138
139
140
# File 'lib/plansheet/project.rb', line 138

def archive_month
  @completed_on&.strftime("%Y-%m") || Date.today.strftime("%Y-%m")
end

#compare_completed_on(other) ⇒ Object



183
184
185
186
187
188
189
# File 'lib/plansheet/project.rb', line 183

def compare_completed_on(other)
  retval = 0
  retval += 1 if @completed_on
  retval -= 1 if other.completed_on
  retval = (other.completed_on <=> @completed_on) if retval.zero?
  retval
end

#compare_completeness(other) ⇒ Object

Projects that are dropped or done are considered “complete”, insofar as they are only kept around for later reference.



235
236
237
238
239
240
# File 'lib/plansheet/project.rb', line 235

def compare_completeness(other)
  retval = 0
  retval += 1 if dropped_or_done?
  retval -= 1 if other.dropped_or_done?
  retval
end

#compare_defer(other) ⇒ Object



206
207
208
209
210
# File 'lib/plansheet/project.rb', line 206

def compare_defer(other)
  receiver = @defer.nil? || @defer < Date.today ? Date.today : @defer
  comparison = other.defer.nil? || other.defer < Date.today ? Date.today : other.defer
  receiver <=> comparison
end

#compare_dependency(other) ⇒ Object



224
225
226
227
228
229
230
231
# File 'lib/plansheet/project.rb', line 224

def compare_dependency(other)
  # This approach might seem odd,
  # but it's to handle circular dependencies
  retval = 0
  retval -= 1 if dependency_of?(other)
  retval += 1 if dependent_on?(other)
  retval
end

#compare_due(other) ⇒ Object



191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/plansheet/project.rb', line 191

def compare_due(other)
  # -1 is receiving object being older

  # Handle nil
  if @due.nil?
    return 0 if other.due.nil?

    return 1
  elsif other.due.nil?
    return -1
  end

  @due <=> other.due
end

#compare_name(other) ⇒ Object

This seems silly at first glance, but it’s to keep projects from flipping around on each sort when they are equal in all other respects



179
180
181
# File 'lib/plansheet/project.rb', line 179

def compare_name(other)
  @name <=> other.name
end

#compare_priority(other) ⇒ Object



161
162
163
# File 'lib/plansheet/project.rb', line 161

def compare_priority(other)
  priority_val <=> other.priority_val
end

#compare_status(other) ⇒ Object



173
174
175
# File 'lib/plansheet/project.rb', line 173

def compare_status(other)
  PROJECT_STATUS_PRIORITY[status] <=> PROJECT_STATUS_PRIORITY[other.status]
end

#compare_time_roi(other) ⇒ Object



169
170
171
# File 'lib/plansheet/project.rb', line 169

def compare_time_roi(other)
  other.time_roi_payoff <=> time_roi_payoff
end

#deferObject



308
309
310
311
312
313
314
# File 'lib/plansheet/project.rb', line 308

def defer
  return @defer if @defer
  return lead_time_deferral if @lead_time && due
  return last_for_deferral if @last_for

  nil
end

#dependency_of?(other) ⇒ Boolean

Returns:

  • (Boolean)


212
213
214
215
216
# File 'lib/plansheet/project.rb', line 212

def dependency_of?(other)
  other&.dependencies&.any? do |dep|
    @name&.downcase == dep.downcase
  end
end

#dependent_on?(other) ⇒ Boolean

Returns:

  • (Boolean)


218
219
220
221
222
# File 'lib/plansheet/project.rb', line 218

def dependent_on?(other)
  @dependencies&.any? do |dep|
    other&.name&.downcase == dep.downcase
  end
end

#done?Boolean

Returns:

  • (Boolean)


343
344
345
# File 'lib/plansheet/project.rb', line 343

def done?
  status == "done"
end

#dropped?Boolean

Returns:

  • (Boolean)


339
340
341
# File 'lib/plansheet/project.rb', line 339

def dropped?
  status == "dropped"
end

#dropped_or_done?Boolean

Returns:

  • (Boolean)


347
348
349
# File 'lib/plansheet/project.rb', line 347

def dropped_or_done?
  dropped? || done?
end

#dueObject

Due date either explicit or recurring



285
286
287
288
289
290
# File 'lib/plansheet/project.rb', line 285

def due
  return @due if @due
  return recurring_due_date if recurring_due?

  nil
end

#last_for_deferralObject



316
317
318
319
320
# File 'lib/plansheet/project.rb', line 316

def last_for_deferral
  return @last_done + parse_date_duration(@last_for) if @last_done

  Date.today
end

#lead_time_deferralObject



322
323
324
325
# File 'lib/plansheet/project.rb', line 322

def lead_time_deferral
  [(due - parse_date_duration(@lead_time)),
   Date.today].max
end

#paused?Boolean

Returns:

  • (Boolean)


335
336
337
# File 'lib/plansheet/project.rb', line 335

def paused?
  status == "paused"
end

#recurring?Boolean

Returns:

  • (Boolean)


331
332
333
# File 'lib/plansheet/project.rb', line 331

def recurring?
  !@frequency.nil? || !@day_of_week.nil? || !@last_done.nil? || !@last_for.nil?
end

#recurring_due?Boolean

Returns:

  • (Boolean)


327
328
329
# File 'lib/plansheet/project.rb', line 327

def recurring_due?
  !@frequency.nil? || !@day_of_week.nil?
end

#recurring_due_dateObject



292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/plansheet/project.rb', line 292

def recurring_due_date
  if @last_done
    return @last_done + parse_date_duration(@frequency) if @frequency

    if @day_of_week
      return Date.today + 7 if @last_done == Date.today
      return @last_done + 7 if @last_done < Date.today - 7

      return NEXT_DOW[@day_of_week]
    end
  end

  # Going to assume this is the first time, so due today
  Date.today
end

#recurring_statusObject



265
266
267
268
269
270
271
272
273
274
# File 'lib/plansheet/project.rb', line 265

def recurring_status
  # add frequency to last_done
  if @last_done
    # This project has been done once before
    subsequent_recurring_status
  else
    # This recurring project is being done for the first time
    task_based_status
  end
end

#statusObject



242
243
244
245
246
247
248
249
250
251
# File 'lib/plansheet/project.rb', line 242

def status
  return @status if @status
  return "dropped" if @dropped_on
  return "paused" if @paused_on
  return recurring_status if recurring?
  return task_based_status if @tasks || @done
  return "done" if @completed_on && @tasks.nil?

  "idea"
end

#stringify_array_property(prop) ⇒ Object



38
39
40
41
42
43
44
45
46
47
# File 'lib/plansheet/project/stringify.rb', line 38

def stringify_array_property(prop)
  str = String.new
  if instance_variable_defined? "@#{prop}"
    str << "#{prop}:\n"
    instance_variable_get("@#{prop}").each do |t|
      str << "- #{t}\n"
    end
  end
  str
end

#stringify_date_property(prop) ⇒ Object



30
31
32
33
34
35
36
# File 'lib/plansheet/project/stringify.rb', line 30

def stringify_date_property(prop)
  if instance_variable_defined? "@#{prop}"
    "#{prop}: #{instance_variable_get("@#{prop}")}\n"
  else
    ""
  end
end

#stringify_string_property(prop) ⇒ Object



22
23
24
25
26
27
28
# File 'lib/plansheet/project/stringify.rb', line 22

def stringify_string_property(prop)
  if instance_variable_defined? "@#{prop}"
    "#{prop}: #{instance_variable_get("@#{prop}")}\n"
  else
    ""
  end
end

#subsequent_recurring_statusObject



276
277
278
279
280
281
282
# File 'lib/plansheet/project.rb', line 276

def subsequent_recurring_status
  return "done" if @lead_time && defer > Date.today
  return "done" if @last_for && defer > Date.today
  return "done" if due && due > Date.today

  task_based_status
end

#task_based_statusObject



253
254
255
256
257
258
259
260
261
262
263
# File 'lib/plansheet/project.rb', line 253

def task_based_status
  if @tasks&.count&.positive? && @done&.count&.positive?
    "wip"
  elsif @tasks&.count&.positive?
    "ready"
  elsif @done&.count&.positive?
    "done"
  else
    "idea"
  end
end

#task_time_estimate(str) ⇒ Object



355
356
357
# File 'lib/plansheet/project.rb', line 355

def task_time_estimate(str)
  parse_time_duration(Regexp.last_match(1)) if str.match(TIME_EST_REGEX)
end

#time_roi_payoffObject



165
166
167
# File 'lib/plansheet/project.rb', line 165

def time_roi_payoff
  @time_roi_payoff || 0
end

#to_hObject



359
360
361
362
363
364
365
# File 'lib/plansheet/project.rb', line 359

def to_h
  h = { "project" => @name, "namespace" => @namespace }
  ALL_PROPERTIES.each do |prop|
    h[prop] = instance_variable_get("@#{prop}") if instance_variable_defined?("@#{prop}")
  end
  h
end

#to_sObject



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# File 'lib/plansheet/project/stringify.rb', line 5

def to_s
  str = String.new
  str << "# "
  str << "#{@namespace} - " if @namespace
  str << "#{@name}\n"
  STRING_PROPERTIES.each do |o|
    str << stringify_string_property(o)
  end
  DATE_PROPERTIES.each do |o|
    str << stringify_string_property(o)
  end
  ARRAY_PROPERTIES.each do |o|
    str << stringify_array_property(o)
  end
  str
end

#yearly_minutes_savedObject



142
143
144
145
146
147
148
149
150
# File 'lib/plansheet/project.rb', line 142

def yearly_minutes_saved
  if @daily_time_roi
    parse_time_duration(@daily_time_roi) * 365
  elsif @weekly_time_roi
    parse_time_duration(@weekly_time_roi) * 52
  elsif @yearly_time_roi
    parse_time_duration(@yearly_time_roi)
  end
end