Module: Doing::ChronifyString

Included in:
String
Defined in:
lib/doing/chronify/string.rb

Overview

Chronify methods for strings

Instance Method Summary collapse

Instance Method Details

#chronify(**options) ⇒ DateTime

Converts input string into a Time object when input takes on the following formats: - interval format e.g. '1d2h30m', '45m' etc. - a semantic phrase e.g. 'yesterday 5:30pm' - a strftime e.g. '2016-03-15 15:32:04 PDT'

Parameters:

  • options

    Additional options

Options Hash (**options):

  • :future (Boolean)

    assume future date (default: false)

  • :guess (Symbol)

    :begin or :end to assume beginning or end of arbitrary time range

Returns:

  • (DateTime)

    result

Raises:



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/doing/chronify/string.rb', line 27

def chronify(**options)
  now = Time.now
  raise Errors::InvalidTimeExpression, "Invalid time expression #{inspect}" if to_s.strip == ''

  secs_ago = if match(/^(\d+)$/)
               # plain number, assume minutes
               Regexp.last_match(1).to_i * 60
             elsif (m = match(/^(?:(?<day>\d+)d)? *(?:(?<hour>\d+)h)? *(?:(?<min>\d+)m)?$/i))
               # day/hour/minute format e.g. 1d2h30m
               [[m['day'], 24 * 3600],
                [m['hour'], 3600],
                [m['min'], 60]].map { |qty, secs| qty ? (qty.to_i * secs) : 0 }.reduce(0, :+)
             end

  if secs_ago
    res = now - secs_ago
    Doing.logger.debug('Parser:', %(date/time string "#{self}" interpreted as #{res} (#{secs_ago} seconds ago)))
  else
    date_string = dup
    date_string = 'today' if date_string.match(Types::REGEX_DAY) && now.strftime('%a') =~ /^#{Regexp.last_match(1)}/i
    date_string = "#{options[:context].to_s} #{date_string}" if date_string =~ Types::REGEX_TIME && options[:context]

    res = Chronic.parse(date_string, {
                          guess: options.fetch(:guess, :begin),
                          context: options.fetch(:future, false) ? :future : :past,
                          ambiguous_time_range: 8
                        })

    Doing.logger.debug('Parser:', %(date/time string "#{self}" interpreted as #{res}))
  end

  res
end

#chronify_qtyInteger

Converts simple strings into seconds that can be added to a Time object

Input string can be HH:MM or XX[dhm][XXhm][XXm]

Returns:

  • (Integer)

    seconds



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
# File 'lib/doing/chronify/string.rb', line 70

def chronify_qty
  minutes = 0
  case self.strip
  when /^(\d+):(\d\d)$/
    minutes += Regexp.last_match(1).to_i * 60
    minutes += Regexp.last_match(2).to_i
  when /^(\d+(?:\.\d+)?)([hmd])?/
    scan(/(\d+(?:\.\d+)?)([hmd])?/).each do |m|
      amt = m[0]
      type = m[1].nil? ? 'm' : m[1]

      minutes += case type.downcase
                 when 'm'
                   amt.to_i
                 when 'h'
                   (amt.to_f * 60).round
                 when 'd'
                   (amt.to_f * 60 * 24).round
                 else
                   0
                 end
    end
  end
  minutes * 60
end

#expand_date_tags(additional_tags = nil) ⇒ Object

Convert (chronify) natural language dates within configured date tags (tags whose value is expected to be a date). Modifies string in place.

Parameters:

  • additional_tags (Array) (defaults to: nil)

    An array of additional tags to consider date_tags



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/doing/chronify/string.rb', line 130

def expand_date_tags(additional_tags = nil)
  iso_rx = /\d{4}-\d\d-\d\d \d\d:\d\d/

  watch_tags = [
    'start(?:ed)?',
    'beg[ia]n',
    'done',
    'finished',
    'completed?',
    'waiting',
    'defer(?:red)?'
  ]

  if additional_tags
    date_tags = additional_tags
    date_tags = date_tags.split(/ *, */) if date_tags.is_a?(String)
    date_tags.map! do |tag|
      tag.sub(/^@/, '').gsub(/\((?!\?:)(.*?)\)/, '(?:\1)').strip
    end
    watch_tags.concat(date_tags).uniq!
  end

  done_rx = /(?<=^| )@(?<tag>#{watch_tags.join('|')})\((?<date>.*?)\)/i

  gsub!(done_rx) do
    m = Regexp.last_match
    t = m['tag']
    d = m['date']
    future = t =~ /^(done|complete)/ ? false : true
    parsed_date = d =~ iso_rx ? Time.parse(d) : d.chronify(guess: :begin, future: future)
    parsed_date.nil? ? m[0] : "@#{t}(#{parsed_date.strftime('%F %R')})"
  end
end

#is_range?Boolean

Returns:

  • (Boolean)


164
165
166
# File 'lib/doing/chronify/string.rb', line 164

def is_range?
  self =~ / (to|through|thru|(un)?til|-+) /
end

#split_date_rangeArray<DateTime>

Splits a range string and returns an array of DateTime objects as [start, end]. If only one date is given, end time is nil.

Examples:

Process a natural language date range

"mon 3pm to mon 5pm".split_date_range

Returns:

  • (Array<DateTime>)

    Start and end dates as array



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/doing/chronify/string.rb', line 178

def split_date_range
  time_rx = /^(\d{1,2}(:\d{1,2})?( *(am|pm))?|midnight|noon)$/
  range_rx = / (to|through|thru|(?:un)?til|-+) /

  date_string = dup

  if date_string.is_range?
    # Do we want to differentiate between "to" and "through"?
    # inclusive = date_string =~ / (through|thru|-+) / ? true : false
    inclusive = true

    dates = date_string.split(range_rx)

    if dates[0].strip =~ time_rx && dates[-1].strip =~ time_rx
      start = dates[0].strip
      finish = dates[-1].strip
    else
      start = dates[0].chronify(guess: :begin, future: false)
      finish = dates[-1].chronify(guess: inclusive ? :end : :begin, future: true)
    end

    raise Errors::InvalidTimeExpression, "Unrecognized date string (#{dates[0]})" if start.nil?

    raise Errors::InvalidTimeExpression, "Unrecognized date string (#{dates[-1]})" if finish.nil?

  else
    if date_string.strip =~ time_rx
      start = date_string.strip
      finish = '11:59pm'
    else
      start = date_string.strip.chronify(guess: :begin, future: false)
      finish = date_string.strip.chronify(guess: :end)
    end
    raise Errors::InvalidTimeExpression, 'Unrecognized date string' unless start

  end


  if start.is_a? String
    Doing.logger.debug('Parser:', "--from string interpreted as time span, from #{start || '12am'} to #{finish || '11:59pm'}")
  else
    Doing.logger.debug('Parser:', "date range interpreted as #{start.strftime('%F %R')} -- #{finish ? finish.strftime('%F %R') : 'now'}")
  end
  [start, finish]
end

#time_string(format: :dhm) ⇒ Object

Convert DD:HH:MM to a natural language string

Parameters:

  • format (Symbol) (defaults to: :dhm)

    The format to output (:dhm, :hm, :m, :clock, :natural)



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

def time_string(format: :dhm)
  to_seconds.time_string(format: format)
end

#to_secondsInteger

Convert DD:HH:MM to seconds

Returns:

  • (Integer)

    rounded number of seconds

Raises:



101
102
103
104
105
106
107
108
109
110
# File 'lib/doing/chronify/string.rb', line 101

def to_seconds
  mtch = match(/(\d+):(\d+):(\d+)/)

  raise Errors::DoingRuntimeError, "Invalid time string: #{self}" unless mtch

  h = mtch[1]
  m = mtch[2]
  s = mtch[3]
  (h.to_i * 60 * 60) + (m.to_i * 60) + s.to_i
end