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.



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
137
138
139
140
141
# File 'lib/plansheet/project.rb', line 71

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?
    remove_instance_variable("@status") if @status
    unless recurring?
      @completed_on ||= Date.today
      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
    end
  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.



69
70
71
# File 'lib/plansheet/project.rb', line 69

def namespace
  @namespace
end

Instance Method Details

#<=>(other) ⇒ Object



157
158
159
160
161
162
163
164
# File 'lib/plansheet/project.rb', line 157

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)


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

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

#archive_monthObject



143
144
145
# File 'lib/plansheet/project.rb', line 143

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

#compare_completed_on(other) ⇒ Object



188
189
190
191
192
193
194
# File 'lib/plansheet/project.rb', line 188

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.



240
241
242
243
244
245
# File 'lib/plansheet/project.rb', line 240

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



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

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



229
230
231
232
233
234
235
236
# File 'lib/plansheet/project.rb', line 229

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



196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/plansheet/project.rb', line 196

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



184
185
186
# File 'lib/plansheet/project.rb', line 184

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

#compare_priority(other) ⇒ Object



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

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

#compare_status(other) ⇒ Object



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

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

#compare_time_roi(other) ⇒ Object



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

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

#deferObject



313
314
315
316
317
318
319
# File 'lib/plansheet/project.rb', line 313

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)


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

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

#dependent_on?(other) ⇒ Boolean

Returns:

  • (Boolean)


223
224
225
226
227
# File 'lib/plansheet/project.rb', line 223

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

#dropped_or_done?Boolean

Returns:

  • (Boolean)


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

def dropped_or_done?
  dropped? || done?
end

#dueObject

Due date either explicit or recurring



290
291
292
293
294
295
# File 'lib/plansheet/project.rb', line 290

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

  nil
end

#last_for_deferralObject



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

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

  Date.today
end

#lead_time_deferralObject



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

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

#recurring?Boolean

Returns:

  • (Boolean)


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

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

#recurring_due?Boolean

Returns:

  • (Boolean)


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

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

#recurring_due_dateObject



297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/plansheet/project.rb', line 297

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



270
271
272
273
274
275
276
277
278
279
# File 'lib/plansheet/project.rb', line 270

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



247
248
249
250
251
252
253
254
255
256
# File 'lib/plansheet/project.rb', line 247

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



281
282
283
284
285
286
287
# File 'lib/plansheet/project.rb', line 281

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



258
259
260
261
262
263
264
265
266
267
268
# File 'lib/plansheet/project.rb', line 258

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



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

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

#time_roi_payoffObject



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

def time_roi_payoff
  @time_roi_payoff || 0
end

#to_hObject



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

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



147
148
149
150
151
152
153
154
155
# File 'lib/plansheet/project.rb', line 147

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