Class: Doing::Item

Inherits:
Object show all
Includes:
Color
Defined in:
lib/doing/item.rb

Overview

This class describes a single WWID item

Constant Summary

Constants included from Color

Color::ATTRIBUTES, Color::ATTRIBUTE_NAMES, Color::COLORED_REGEXP

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Color

attributes, coloring?, #support?, #uncolor

Constructor Details

#initialize(date, title, section, note = nil) ⇒ Item

Initialize an item with date, title, section, and optional note

Parameters:

  • date (Time)

    The item's start date

  • title (String)

    The title

  • section (String)

    The section to which the item belongs

  • note (Array or String) (defaults to: nil)

    The note (optional)



25
26
27
28
29
30
# File 'lib/doing/item.rb', line 25

def initialize(date, title, section, note = nil)
  @date = date.is_a?(Time) ? date : Time.parse(date)
  @title = title
  @section = section
  @note = Note.new(note)
end

Instance Attribute Details

#dateObject

Returns the value of attribute date.



8
9
10
# File 'lib/doing/item.rb', line 8

def date
  @date
end

#noteObject

Returns the value of attribute note.



8
9
10
# File 'lib/doing/item.rb', line 8

def note
  @note
end

#sectionObject

Returns the value of attribute section.



8
9
10
# File 'lib/doing/item.rb', line 8

def section
  @section
end

#titleObject

Returns the value of attribute title.



8
9
10
# File 'lib/doing/item.rb', line 8

def title
  @title
end

Instance Method Details

#calculate_end_date(opt) ⇒ Object



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/doing/item.rb', line 64

def calculate_end_date(opt)
  if opt[:took]
    if @date + opt[:took] > Time.now
      @date = Time.now - opt[:took]
      Time.now
    else
      @date + opt[:took]
    end
  elsif opt[:back]
    if opt[:back].is_a? Integer
      @date + opt[:back]
    else
      @date + (opt[:back] - @date)
    end
  else
    Time.now
  end
end

#cloneObject



455
456
457
# File 'lib/doing/item.rb', line 455

def clone
  Marshal.load(Marshal.dump(self))
end

#durationObject

If the entry doesn't have a @done date, return the elapsed time



37
38
39
40
41
42
43
# File 'lib/doing/item.rb', line 37

def duration
  return nil unless should_time? && should_finish?

  return nil if @title =~ /(?<=^| )@done\b/

  return Time.now - @date
end

#end_dateTime

Get the value of the item's @done tag

Returns:

  • (Time)

    @done value



60
61
62
# File 'lib/doing/item.rb', line 60

def end_date
  @end_date ||= Time.parse(Regexp.last_match(1)) if @title =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/
end

#equal?(other, match_section: false) ⇒ Boolean

Test for equality between items

Parameters:

  • other (Item)

    The other item

  • match_section (Boolean) (defaults to: false)

    If true, require item sections to match

Returns:

  • (Boolean)

    is equal?



98
99
100
101
102
103
104
105
106
107
108
# File 'lib/doing/item.rb', line 98

def equal?(other, match_section: false)
  return false if @title.strip != other.title.strip

  return false if @date != other.date

  return false unless @note.equal?(other.note)

  return false if match_section && @section != other.section

  true
end

#expand_date_tags(additional_tags = nil) ⇒ Object

Updates the title of the Item by expanding natural language dates within configured date tags (tags whose value is expected to be a date)

Parameters:

  • additional_tags (defaults to: nil)

    An array of additional tag names to consider dates



150
151
152
# File 'lib/doing/item.rb', line 150

def expand_date_tags(additional_tags = nil)
  @title.expand_date_tags(additional_tags)
end

#finished?Boolean

Test if item has a @done tag

Returns:

  • (Boolean)

    true item has @done tag



367
368
369
# File 'lib/doing/item.rb', line 367

def finished?
  tags?('done')
end

#highlight_search(search, distance: nil, negate: false, case_type: nil) ⇒ Object



279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/doing/item.rb', line 279

def highlight_search(search, distance: nil, negate: false, case_type: nil)
  prefs = Doing.setting('search', {})
  matching = prefs.fetch('matching', 'pattern').normalize_matching
  distance ||= prefs.fetch('distance', 3).to_i
  case_type ||= prefs.fetch('case', 'smart').normalize_case
  new_note = Note.new

  if search.rx? || matching == :fuzzy
    rx = search.to_rx(distance: distance, case_type: case_type)
    new_title = @title.gsub(rx) { |m| yellow(m) }
    new_note.add(@note.to_s.gsub(rx) { |m| yellow(m) })
  else
    query = search.strip.to_phrase_query

    if query[:must].nil? && query[:must_not].nil?
      query[:must] = query[:should]
      query[:should] = []
    end
    query[:must].concat(query[:should]).each do |s|
      rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
      new_title = @title.gsub(rx) { |m| yellow(m) }
      new_note.add(@note.to_s.gsub(rx) { |m| yellow(m) })
    end
  end

  Item.new(@date, new_title, @section, new_note)
end

#idString

Generate a hash that represents the entry

Returns:



86
87
88
# File 'lib/doing/item.rb', line 86

def id
  @id ||= (@date.to_s + @title + @section).hash
end

#ignore_case(search, case_type) ⇒ Boolean

Determine if case should be ignored for searches

Parameters:

  • search (String)

    The search string

  • case_type (Symbol)

    The case type

Returns:

  • (Boolean)

    case should be ignored



275
276
277
# File 'lib/doing/item.rb', line 275

def ignore_case(search, case_type)
  (case_type == :smart && search !~ /[A-Z]/) || case_type == :ignore
end

#intervalObject

Get the difference between the item's start date and the value of its @done tag (if present)

Returns:

  • Interval in seconds



51
52
53
# File 'lib/doing/item.rb', line 51

def interval
  @interval ||= calc_interval
end

#move_to(new_section, label: true, log: true) ⇒ Object

Move item from current section to destination section

Parameters:

  • new_section (String)

    The destination section

  • label (Boolean) (defaults to: true)

    add @from(original section) tag

  • log (Boolean) (defaults to: true)

    log this action

Returns:

  • nothing



411
412
413
414
415
416
417
418
419
420
421
# File 'lib/doing/item.rb', line 411

def move_to(new_section, label: true, log: true)
  from = @section

  tag('from', rename_to: 'from', value: from, force: true) if label
  @section = new_section

  Doing.logger.count(@section == 'Archive' ? :archived : :moved) if log
  Doing.logger.debug("#{@section == 'Archive' ? 'Archived' : 'Moved'}:",
                     "#{@title.trunc(60)} from #{from} to #{@section}")
  self
end

#overlapping_time?(item_b) ⇒ Boolean

Test if the interval between start date and @done value overlaps with another item's

Parameters:

  • item_b (Item)

    The item to compare

Returns:

  • (Boolean)

    overlaps?



129
130
131
132
133
134
135
136
137
138
139
# File 'lib/doing/item.rb', line 129

def overlapping_time?(item_b)
  return true if same_time?(item_b)

  start_a = date
  a_interval = interval
  end_a = a_interval ? start_a + a_interval.to_i : start_a
  start_b = item_b.date
  b_interval = item_b.interval
  end_b = b_interval ? start_b + b_interval.to_i : start_b
  (start_a >= start_b && start_a <= end_b) || (end_a >= start_b && end_a <= end_b) || (start_a < start_b && end_a > end_b)
end

#same_time?(item_b) ⇒ Boolean

Test if two items occur at the same time (same start date and equal duration)

Parameters:

  • item_b (Item)

    The item to compare

Returns:

  • (Boolean)

    is equal?



117
118
119
# File 'lib/doing/item.rb', line 117

def same_time?(item_b)
  date == item_b.date ? interval == item_b.interval : false
end

#search(search, distance: nil, negate: false, case_type: nil) ⇒ Boolean

Test if item matches search string

Parameters:

  • search (String)

    The search string

  • negate (Boolean) (defaults to: false)

    negate results

  • case_type (Symbol) (defaults to: nil)

    The case-sensitivity type (:sensitive, :ignore, :smart)

Returns:

  • (Boolean)

    matches search criteria



318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/doing/item.rb', line 318

def search(search, distance: nil, negate: false, case_type: nil)
  prefs = Doing.setting('search', {})
  matching = prefs.fetch('matching', 'pattern').normalize_matching
  distance ||= prefs.fetch('distance', 3).to_i
  case_type ||= prefs.fetch('case', 'smart').normalize_case

  if search.rx? || matching == :fuzzy
    matches = @title + @note.to_s =~ search.to_rx(distance: distance, case_type: case_type)
  else
    query = search.strip.to_phrase_query

    if query[:must].nil? && query[:must_not].nil?
      query[:must] = query[:should]
      query[:should] = []
    end
    matches = no_searches?(query[:must_not], case_type: case_type)
    matches &&= all_searches?(query[:must], case_type: case_type)
    matches &&= any_searches?(query[:should], case_type: case_type)
  end
  # if search =~ /(?<=\A| )[+-]\S/
  # else
  #   text = @title + @note.to_s
  #   matches = text =~ search.to_rx(distance: distance, case_type: case_type)
  # end

  # if search.rx? || !fuzzy
  #   matches = text =~ search.to_rx(distance: distance, case_type: case_type)
  # else
  #   distance = 0.25 if distance > 1
  #   score = if (case_type == :smart && search !~ /[A-Z]/) || case_type == :ignore
  #             text.downcase.pair_distance_similar(search.downcase)
  #           else
  #             score = text.pair_distance_similar(search)
  #           end

  #   if score >= distance
  #     matches = true
  #     Doing.logger.debug('Fuzzy Match:', %(#{@title}, "#{search}" #{score}))
  #   end
  # end

  negate ? !matches : matches
end

#should_finish?Boolean

Test if item is included in never_finish config and thus should not receive a @done tag

Returns:

  • (Boolean)

    item should receive @done tag



386
387
388
# File 'lib/doing/item.rb', line 386

def should_finish?
  should?('never_finish')
end

#should_time?Boolean

Test if item is included in never_time config and thus should not receive a date on the @done tag

Returns:

  • (Boolean)

    item should receive @done date



396
397
398
# File 'lib/doing/item.rb', line 396

def should_time?
  should?('never_time')
end

#tag(tags, **options) ⇒ Object

Add (or remove) tags from the title of the item

Parameters:

  • tags (Array)

    The tags to apply

  • options

    Additional options

Options Hash (**options):

  • :date (Boolean)

    Include timestamp?

  • :single (Boolean)

    Log as a single change?

  • :value (String)

    A value to include as @tag(value)

  • :remove (Boolean)

    if true remove instead of adding

  • :rename_to (String)

    if not nil, rename target tag to this tag name

  • :regex (Boolean)

    treat target tag string as regex pattern

  • :force (Boolean)

    with rename_to, add tag if it doesn't exist



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/doing/item.rb', line 168

def tag(tags, **options)
  added = []
  removed = []

  date = options.fetch(:date, false)
  options[:value] ||= date ? Time.now.strftime('%F %R') : nil
  options.delete(:date)

  single = options.fetch(:single, false)
  options.delete(:single)

  tags = tags.to_tags if tags.is_a? ::String

  remove = options.fetch(:remove, false)
  tags.each do |tag|
    bool = remove ? :and : :not
    if tags?(tag, bool)
      @title = @title.tag(tag, **options).strip
      remove ? removed.push(tag) : added.push(tag)
    end
  end

  Doing.logger.log_change(tags_added: added, tags_removed: removed, count: 1, item: self, single: single)

  self
end

#tag_arrayArray

convert tags on item to an array with @ symbols removed

Returns:

  • (Array)

    array of tags



209
210
211
# File 'lib/doing/item.rb', line 209

def tag_array
  tags.tags_to_array
end

#tag_values?(queries, bool = :and, negate: false) ⇒ Boolean

Test if item matches tag values

Parameters:

  • queries (Array)

    The tag value queries to test

  • bool (Symbol) (defaults to: :and)

    The boolean to use for multiple tags (:and, :or, :not)

  • negate (Boolean) (defaults to: false)

    negate the result?

Returns:

  • (Boolean)

    true if tag/bool combination passes



253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/doing/item.rb', line 253

def tag_values?(queries, bool = :and, negate: false)
  bool = bool.normalize_bool

  matches = case bool
            when :and
              all_values?(queries)
            when :not
              no_values?(queries)
            else
              any_values?(queries)
            end
  negate ? !matches : matches
end

#tagsArray

Get a list of tags on the item

Returns:

  • (Array)

    array of tags (no values)



200
201
202
# File 'lib/doing/item.rb', line 200

def tags
  @title.scan(/(?<= |\A)@([^\s(]+)/).map { |tag| tag[0] }.sort.uniq
end

#tags?(tags, bool = :and, negate: false) ⇒ Boolean

Test if item contains tag(s)

Parameters:

  • tags (Array or String)

    The tags to test. Can be an array or a comma-separated string.

  • bool (Symbol) (defaults to: :and)

    The boolean to use for multiple tags (:and, :or, :not)

  • negate (Boolean) (defaults to: false)

    negate the result?

Returns:

  • (Boolean)

    true if tag/bool combination passes



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/doing/item.rb', line 222

def tags?(tags, bool = :and, negate: false)
  if bool == :pattern
    tags = tags.to_tags.tags_to_array.join(' ')
    matches = tag_pattern?(tags)

    return negate ? !matches : matches
  end

  tags = split_tags(tags)
  bool = bool.normalize_bool

  matches = case bool
            when :and
              all_tags?(tags)
            when :not
              no_tags?(tags)
            else
              any_tags?(tags)
            end
  negate ? !matches : matches
end

#to_pretty(elements: %i[date title section])) ⇒ Object

outputs a colored string with relative date and highlighted tags

Returns:

  • Pretty representation of the object.



433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
# File 'lib/doing/item.rb', line 433

def to_pretty(elements: i[date title section])
  output = []
  elements.each do |e|
    case e
    when :date
      output << format('%13s |', @date.relative_date).cyan
    when :section
      output << "#{magenta}(#{white(@section)}#{magenta})"
    when :title
      output << @title.white.highlight_tags('cyan')
    end
  end

  output.join(' ')
end

#to_sObject

outputs item in Doing file format, including leading tab



424
425
426
# File 'lib/doing/item.rb', line 424

def to_s
  "\t- #{@date.strftime('%Y-%m-%d %H:%M')} | #{@title}#{@note.good? ? "\n#{@note}" : ''}"
end

#unfinished?Boolean

Test if item does not contain @done tag

Returns:

  • (Boolean)

    true if item is missing @done tag



376
377
378
# File 'lib/doing/item.rb', line 376

def unfinished?
  tags?('done', negate: true)
end