Class: Qreport::TimeParser

Inherits:
Object
  • Object
show all
Defined in:
lib/qreport/time_parser.rb,
lib/qreport/time_parser/examples.rb,
lib/qreport/time_parser/time_unit.rb,
lib/qreport/time_parser/time_range.rb,
lib/qreport/time_parser/time_interval.rb,
lib/qreport/time_parser/time_relative.rb,
lib/qreport/time_parser/time_with_unit.rb

Defined Under Namespace

Modules: TimeUnit, Token Classes: Error, TimeInterval, TimeRange, TimeRelative, TimeWithUnit

Constant Summary collapse

EOS =
Object.new
@@operation_alias =
{
  :plus => :+,
  :minus => :-,
  :in => :+,
}
@@direction_alias =
{
  :ago => -1,
  :before => -1,
  :after => 1,
  :from => 1,
  :since => 1,
  :later => 1,
}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(start = nil) ⇒ TimeParser

Returns a new instance of TimeParser.



14
15
16
17
18
19
20
# File 'lib/qreport/time_parser.rb', line 14

def initialize start = nil
  @start = start
  @unit_for_now = { :today => :day, :t => :now }
  @debug = false
  # @debug = true
  initialize_copy nil
end

Instance Attribute Details

#debugObject

Returns the value of attribute debug.



12
13
14
# File 'lib/qreport/time_parser.rb', line 12

def debug
  @debug
end

#inputObject

Returns the value of attribute input.



12
13
14
# File 'lib/qreport/time_parser.rb', line 12

def input
  @input
end

#nowObject

Returns the value of attribute now.



12
13
14
# File 'lib/qreport/time_parser.rb', line 12

def now
  @now
end

#resultObject

Returns the value of attribute result.



12
13
14
# File 'lib/qreport/time_parser.rb', line 12

def result
  @result
end

#startObject

Returns the value of attribute start.



12
13
14
# File 'lib/qreport/time_parser.rb', line 12

def start
  @start
end

#unit_for_nowObject

Returns the value of attribute unit_for_now.



12
13
14
# File 'lib/qreport/time_parser.rb', line 12

def unit_for_now
  @unit_for_now
end

Class Method Details

.def_p(name, &blk) ⇒ Object



42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/qreport/time_parser.rb', line 42

def self.def_p name, &blk
  sel = :"p_#{name}"
  _sel = :"_#{sel}"
  define_method _sel, &blk
  define_method sel do
    _wrap_p! sel do
      restore_tokens_on_failure!(sel) do
        send(_sel)
      end
    end
  end
  sel
end

.examplesObject



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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/qreport/time_parser/examples.rb', line 5

def self.examples
  now = ::Time.parse("2011-03-10T15:10:37.981304-06:00")
  examples = {
    "now" => "nil 2011-03-10T15:10:37.981304-06:00",
    now.to_s => "nil 2011-03-10T15:10:37.000000-06:00",
    "2011-03-10 15:10:37.981304 -0600" => "nil 2011-03-10T15:10:37.981304-06:00",
    "today" => ":day 2011-03-10T00:00:00.000000-06:00",
    "tomorrow" => ":day 2011-03-11T00:00:00.000000-06:00",
    "yesterday" => ":day 2011-03-09T00:00:00.000000-06:00",
    "9:15am yesterday" => ":min 2011-03-09T09:15:00.000000-06:00",
    "yesterday 9:15am" => ":day 2011-03-09T00:00:00.000000-06:00", # FIXME
    "10 days ago" => ":day 2011-02-28T00:00:00.000000-06:00",
    "10 s ago" => ":sec 2011-03-10T15:10:27.000000-06:00",
    "day before yesterday" => ":day 2011-03-08T00:00:00.000000-06:00",
    "hr before tomorrow" => ":hour 2011-03-10T23:00:00.000000-06:00",
    "3 days before today" => ":day 2011-03-07T00:00:00.000000-06:00",
    "5 days after today" => ":day 2011-03-15T00:00:00.000000-05:00",
    "5 days before now" => "nil 2011-03-05T15:10:37.981304-06:00",
    "3 days before this minute" => ":min 2011-03-07T15:10:00.000000-06:00",
    "5 days before yesterday" => ":day 2011-03-04T00:00:00.000000-06:00",
    "2 days before 50 hours after tomorrow" => ":hour 2011-03-11T02:00:00.000000-06:00",
    "2 centuries after today" => ":day 2211-01-21T00:00:00.000000-06:00",
    "1pm" => ":hour 2011-03-10T13:00:00.000000-06:00",
    "12:30pm" => ":min 2011-03-10T12:30:00.000000-06:00",
    "9:20am tomorrow" => ":min 2011-03-11T09:20:00.000000-06:00",
    "6am 3 days from yesterday" => ":hour 2011-03-12T06:00:00.000000-06:00",
    "2001/01" => ":mon 2001-01-01T00:00:00.000000-06:00",
    "2001-01" => ":mon 2001-01-01T00:00:00.000000-06:00",
    # "01/2001" => ":mon 2001-01-01T00:00:00.000000-06:00",
    "01/2001" => "#<Qreport::TimeParser::Error::Syntax: syntax error at position 2: \"01 |^| /2001\">",
    "2001/02/03 12:23pm" => ":min 2001-02-03T12:23:00.000000-06:00",
    "12/31 12:59pm" => ":min 2011-12-31T12:59:00.000000-06:00",
    "12/31 last year" => ":day 2010-12-31T00:00:00.000000-06:00",
    "12:59:59pm 12/31 next year" => ":sec 2012-12-31T12:59:59.000000-06:00",
    "1:23:45pm 1/2 in 2 years" => ":sec 2013-01-01T13:23:45.000000-06:00",
    "2011-03-10T15:10:37-06:00" => "nil 2011-03-10T15:10:37.000000-06:00",
    "2011-03-10T15:10:37.981304-06:00" => "nil 2011-03-10T15:10:37.981304-06:00",
    "2011-03-10T15:10:37-06:00 plus 10 sec" => ":sec 2011-03-10T15:10:47.000000-06:00",
    "2011-03-10T15:10:37.981304-06:00 - 2 weeks" => "nil 2011-02-24T15:10:37.981304-06:00",
    "now minus 2.5 weeks" => "nil 2011-03-10T15:10:35.481304-06:00",
    "t - 10 sec" => "nil 2011-03-10T15:10:27.981304-06:00",
    "123.45 sec ago" => "nil 2011-03-10T15:08:34.531303-06:00",
    "year 2010" => ":year 2010-01-01T00:00:00.000000-06:00",
    "between 12:45pm and 1:15pm" => ":min 2011-03-10T12:45:00.000000-06:00 ... :min 2011-03-10T13:15:00.000000-06:00",
    "before 1:23pm tomorrow" => ":min 2011-03-11T13:22:00.000000-06:00",
    "this minute" => ":min 2011-03-10T15:10:00.000000-06:00",
    "last hour"     => ":hour 2011-03-10T14:00:00.000000-06:00",
    "previous hour" => ":hour 2011-03-10T14:00:00.000000-06:00",
    "last day"   => [ ":day 2011-03-09T00:00:00.000000-06:00", ":day 2011-03-09T00:00:00.000000-06:00 ... :day 2011-03-10T00:00:00.000000-06:00" ],
    "previous day" => ":day 2011-03-09T00:00:00.000000-06:00",
    "  2001-01 + 1234 ajsdkfsd hours" => "#<Qreport::TimeParser::Error::Syntax: syntax error at position 17: \"  2001-01 + 1234  |^| ajsdkfsd hours\">",
    "15 sec" => "#<Qreport::TimeParser::Error: Qreport::TimeParser::Error>",
    "12 minutes" => "#<Qreport::TimeParser::Error: Qreport::TimeParser::Error>",
  }
  examples[:now] = now
  examples
end

Instance Method Details

#_wrap_p!(sel, &blk) ⇒ Object



195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/qreport/time_parser.rb', line 195

def _wrap_p! sel, &blk
  if @debug
    $stderr.puts "  #{' ' * @p_depth} #{sel} ... | #{token.inspect} #{token.value.inspect}"
    @p_depth += 1
    begin
      restore_tokens_on_failure!(sel, &blk)
    ensure
      @p_depth -= 1
      $stderr.puts "  #{' ' * @p_depth} #{sel} => #{result.inspect} | #{token.inspect}"
    end
  else
    restore_tokens_on_failure!(sel, &blk)
  end
end

#describe_current_parse_positionObject



381
382
383
384
385
# File 'lib/qreport/time_parser.rb', line 381

def describe_current_parse_position
  s = @input_orig.dup
  s[@pos, 0] = " |^| "
  s
end

#get_unit_for_now(name) ⇒ Object



410
411
412
413
# File 'lib/qreport/time_parser.rb', line 410

def get_unit_for_now name
  name = name.to_sym
  @unit_for_now[name] || @unit_for_now[nil]
end

#initialize_copy(x) ⇒ Object



22
23
24
25
26
27
# File 'lib/qreport/time_parser.rb', line 22

def initialize_copy x
  @input = ''
  @token = nil
  @token_stack = [ ]
  @taken_tokens = [ ]
end

#lexObject



254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
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
301
302
303
304
305
306
307
308
309
310
311
312
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
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
# File 'lib/qreport/time_parser.rb', line 254

def lex
  debug = @debug
  type = value = nil
  @input.sub!(/\A(\s+)/, '')
  pre_whitespace = $1
  @pos += pre_whitespace.size if pre_whitespace
  # $stderr.puts "  @input = #{@input.inspect[0, 20]}..."; debugger
  case @input
  when ''
    return EOS
  when /\A(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(-[\d:]+)?)\b/ # iso8601
    value = TimeWithUnit.new(Time.parse($1), nil)
  when /\A(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d+)?(\s+[-+]?[\d]+)?)\b/ # Time#to_s
    value = TimeWithUnit.new(Time.parse($1), nil)
  when /\A(year\s+(\d+))/i
    year = $2 && $2.to_i
    value = TimeRelative.new
    value.year = year
    type = :date_relative
  when /\A((\d{4})(?:([-\/])(0?[1-9]|1[0-2])(?:\3([0-2][0-9]|3[01]))?))\b/i
           year = $2 && $2.to_i
                     sep = $3
                           mon = $4 && $4.to_i
                                                 day = $5 && $5.to_i
    value = TimeRelative.new
    value.year = year
    value.mon = mon
    value.day = day
    type = :date_relative
  when /\A((0[1-9]|1[0-2]|[1-9])(?:[-\/](0[1-9]|[1-2][0-9]|3[01]|[1-9])))\b/i
           mon = $2 && $2.to_i
                                   day = $3 && $3.to_i
    value = TimeRelative.new
    value.mon = mon
    value.day = day
    type = :date_relative
    # debug = true
  when /\A((0?[1-9]|1[0-2])(?::([0-5][0-9])(?::([0-5][0-9]|60))?)\s*(am?|pm?)?)\b/i
    hour = $2.to_i
    min = $3 && $3.to_i
    sec = $4 && $4.to_i
    meridian = ($5 || '').downcase
    hour = 0 if hour == 12
    hour += 12 if meridian.index('p')
    value = TimeRelative.new
    value.hour = hour
    value.min = min
    value.sec = sec
    type = :time_relative
  when /\A(\d\d?)([\/-])(\d\d\d\d)\b/i
    mon = $1 && $1.to_i
    sep = $2
    year = $3 && $3.to_i
    value = TimeRelative.new
    value.year = year
    value.mon = mon
    type = :date_relative
  when /\A((0?[1-9]|1[0-2])\s*(am?|pm?))\b/i
    hour = $2.to_i
    meridian = ($3 || '').downcase
    hour = 0 if hour == 12
    hour += 12 if meridian.index('p')
    value = TimeRelative.new
    value.hour = hour
    type = :time_relative
  when /\A([-+]?\d+\.\d*|\.\d+)/
    value = $1.to_f
    type = :number
  when /\A([-+]?\d+)/
    value = $1.to_i
    type = :number
  when /\A(\+|\-|plus\b|minus\b|in\b)/i
    value = $1.downcase.to_sym
    value = @@operation_alias[value] || value
    type = :operation
  when /\A(today|now|t)\b/i
    case unit = get_unit_for_now($1)
    when :now
      value = TimeWithUnit.new(now, nil)
    else
      value = TimeWithUnit.new(now, unit)
    end
  when /\A(yesterday)\b/i
    value = TimeWithUnit.new(now, :day) - 1
  when /\A(tomorrow)\b/i
    value = TimeWithUnit.new(now, :day) + 1
  when /\A((this)\s+(#{TimeUnit::UNIT_REGEXP}))\b/io
    value = TimeWithUnit.new(now, $3)
  when /\A((previous|last|next)\s+(#{TimeUnit::UNIT_REGEXP}))\b/io
    value = TimeWithUnit.new(now, $3) + TimeInterval.new($2, $3)
  when /\A(#{TimeUnit::UNIT_REGEXP})\b/io
    value = TimeInterval.new(1, $1)
    type = :unit
  when /\A(ago)\b/i
    value = $1.downcase.to_sym
    value = @@direction_alias[value]
    type = :relative
  when /\A(before|after|from|since)\b/i
    value = $1.downcase.to_sym
    value = @@direction_alias[value]
    type = :relation
  when /\A(between)\b/i
    value = $1.downcase.to_sym
    type = :range
  when /\A(and|or)\b/i
    value = $1.downcase.to_sym
    type = :logical
  else
    desc = describe_current_parse_position
    err = Error::Syntax.new("syntax error at position #{@pos}: #{desc.inspect}")
    err.position = @pos
    err.description = desc
    raise err
  end
  token = $1
  pos = @pos
  @input[0, token.size] = ''
  @pos += token.size
  token.extend(Token)
  token.pos = pos
  token.pre = pre_whitespace
  token.value = value
  token.type = type
  $stderr.puts "  token => #{token.inspect}" if debug
  token
end

#parse(str, start = nil) ⇒ Object



29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/qreport/time_parser.rb', line 29

def parse str, start = nil
  start ||= @start
  $stderr.puts "\n  parse #{str.inspect} #{start.inspect}" if @debug
  @input_orig = str.dup
  @input = str.dup
  @pos = 0
  @p_depth = 1
  @result = send(start || :p_start)
  @result = @result.value if @result.respond_to?(:value)
  $stderr.puts "  parse #{str.inspect} #{start.inspect} => #{@result.inspect}\n\n" if @debug
  @result
end

#push_token!(token) ⇒ Object



244
245
246
247
248
249
250
251
252
# File 'lib/qreport/time_parser.rb', line 244

def push_token! token
  if @token
    @token_stack.unshift @token 
    $stderr.puts "push_token! #{@token.inspect}" if @debug
  end
  @token = token
  $stderr.puts "push_token! #{@token.inspect}" if @debug
  self
end

#restore_tokens_on_failure!(sel) ⇒ Object



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/qreport/time_parser.rb', line 210

def restore_tokens_on_failure!(sel)
  restore = true
  (@taken_tokens_stack ||= [ ]) << @taken_tokens
  @taken_tokens = [ ]
  begin
    result = yield
    # $stderr.puts "  #{sel.inspect} taken_tokens = #{@taken_tokens.inspect}"
    restore = false if result
    result
  ensure
    if restore && ! @taken_tokens.empty?
      $stderr.puts "  #{sel.inspect} restoring tokens #{@taken_tokens.inspect}" if @debug
      @taken_tokens.reverse.each do | t |
        push_token! t
      end
    end
    @taken_tokens = @taken_tokens_stack.pop
  end
end

#take_tokenObject



237
238
239
240
241
242
# File 'lib/qreport/time_parser.rb', line 237

def take_token
  t = token
  @taken_tokens << t
  @token = nil
  t
end

#tokenObject



232
233
234
235
# File 'lib/qreport/time_parser.rb', line 232

def token
  @token ||= 
    (@token_stack.first ? @token_stack.shift : lex)
end