Class: Doing::Item

Inherits:
Object
  • 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



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

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

#durationObject

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



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

def duration
  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



58
59
60
# File 'lib/doing/item.rb', line 58

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) ⇒ Boolean

Test for equality between items

Parameters:

  • other (Item)

    The other item

Returns:

  • (Boolean)

    is equal?



95
96
97
98
99
100
101
102
103
# File 'lib/doing/item.rb', line 95

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

  return false if @date != other.date

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

  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



145
146
147
# File 'lib/doing/item.rb', line 145

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

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



274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/doing/item.rb', line 274

def highlight_search(search, distance: nil, negate: false, case_type: nil)
  prefs = Doing.config.settings['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.is_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:



84
85
86
# File 'lib/doing/item.rb', line 84

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



270
271
272
# File 'lib/doing/item.rb', line 270

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



49
50
51
# File 'lib/doing/item.rb', line 49

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



388
389
390
391
392
393
394
395
396
397
398
# File 'lib/doing/item.rb', line 388

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.truncate(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?



124
125
126
127
128
129
130
131
132
133
134
# File 'lib/doing/item.rb', line 124

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?



112
113
114
# File 'lib/doing/item.rb', line 112

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



313
314
315
316
317
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
# File 'lib/doing/item.rb', line 313

def search(search, distance: nil, negate: false, case_type: nil)
  prefs = Doing.config.settings['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.is_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.is_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



363
364
365
# File 'lib/doing/item.rb', line 363

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



373
374
375
# File 'lib/doing/item.rb', line 373

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



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/doing/item.rb', line 163

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.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



204
205
206
# File 'lib/doing/item.rb', line 204

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



248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/doing/item.rb', line 248

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)



195
196
197
# File 'lib/doing/item.rb', line 195

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



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/doing/item.rb', line 217

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.



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

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



401
402
403
# File 'lib/doing/item.rb', line 401

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