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'

Options Hash (**options):

  • :future (Boolean)

    assume future date (default: false)

  • :guess (Symbol)

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

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
60
61
# 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
    if date_string.match(Types::REGEX_DAY) && now.strftime('%a') =~ /^#{Regexp.last_match(1)}/i
      date_string = 'today'
    end
    date_string = "#{options[:context]} #{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]



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

def chronify_qty
  minutes = 0
  case 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.



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

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



166
167
168
# File 'lib/doing/chronify/string.rb', line 166

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


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

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 = start + (24 * 60 * 60)
    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



119
120
121
# File 'lib/doing/chronify/string.rb', line 119

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

#to_secondsInteger

Convert DD:HH:MM to seconds



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

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