chronic 0.9.0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -4
  3. data/.travis.yml +8 -0
  4. data/HISTORY.md +27 -0
  5. data/README.md +8 -0
  6. data/Rakefile +28 -8
  7. data/chronic.gemspec +8 -5
  8. data/lib/chronic/date.rb +82 -0
  9. data/lib/chronic/handler.rb +34 -25
  10. data/lib/chronic/handlers.rb +37 -9
  11. data/lib/chronic/numerizer.rb +11 -2
  12. data/lib/chronic/ordinal.rb +22 -20
  13. data/lib/chronic/parser.rb +33 -25
  14. data/lib/chronic/repeater.rb +18 -18
  15. data/lib/chronic/repeaters/repeater_day.rb +4 -3
  16. data/lib/chronic/repeaters/repeater_day_name.rb +5 -4
  17. data/lib/chronic/repeaters/repeater_day_portion.rb +4 -3
  18. data/lib/chronic/repeaters/repeater_fortnight.rb +4 -3
  19. data/lib/chronic/repeaters/repeater_hour.rb +4 -3
  20. data/lib/chronic/repeaters/repeater_minute.rb +4 -3
  21. data/lib/chronic/repeaters/repeater_month.rb +5 -4
  22. data/lib/chronic/repeaters/repeater_month_name.rb +4 -3
  23. data/lib/chronic/repeaters/repeater_season.rb +5 -3
  24. data/lib/chronic/repeaters/repeater_second.rb +4 -3
  25. data/lib/chronic/repeaters/repeater_time.rb +35 -25
  26. data/lib/chronic/repeaters/repeater_week.rb +4 -3
  27. data/lib/chronic/repeaters/repeater_weekday.rb +4 -3
  28. data/lib/chronic/repeaters/repeater_weekend.rb +4 -3
  29. data/lib/chronic/repeaters/repeater_year.rb +5 -4
  30. data/lib/chronic/scalar.rb +34 -69
  31. data/lib/chronic/separator.rb +107 -8
  32. data/lib/chronic/sign.rb +49 -0
  33. data/lib/chronic/span.rb +2 -2
  34. data/lib/chronic/tag.rb +5 -4
  35. data/lib/chronic/time.rb +40 -0
  36. data/lib/chronic/token.rb +4 -0
  37. data/lib/chronic.rb +16 -11
  38. data/test/helper.rb +2 -2
  39. data/test/test_chronic.rb +55 -4
  40. data/test/test_handler.rb +20 -0
  41. data/test/test_numerizer.rb +14 -0
  42. data/test/test_parsing.rb +91 -1
  43. data/test/test_repeater_time.rb +18 -0
  44. data/test/test_span.rb +1 -1
  45. metadata +46 -20
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d0bcdd2460eb86f8898212a9eca5e325c9aa5e61
4
+ data.tar.gz: d8abd19da58e28473139b8f137991cb0ee150c49
5
+ SHA512:
6
+ metadata.gz: 04bfd4f32ce8f8ef6585042425ea64cc95d07998287fb16fa45b8d0a06d5dd48f8b85e6fecbf00e69101e560f21527ef6539a8994a9457357d25c2c832c7b87f
7
+ data.tar.gz: 87db3c22292ed9d5a22ecc84e6b14dafb2ed1c7a4c2f829873593b86b37707999adfaf43a5e0d801daffaaec9c7b599beade2d366d108c60bf2e8989998ed4b4
data/.gitignore CHANGED
@@ -1,6 +1,6 @@
1
- pkg
2
1
  *.rbc
3
- rdoc
4
2
  .yardoc
5
- doc
6
- tags
3
+ /pkg/
4
+ /coverage/
5
+ /doc/
6
+ /tags/
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 1.9.2
5
+ - 1.8.7
6
+ - jruby-19mode
7
+ - jruby-18mode
8
+ - rbx-19mode
data/HISTORY.md CHANGED
@@ -1,3 +1,30 @@
1
+ # 0.10.2 / 2013-09-09
2
+
3
+ * Fix 1.8.7 support (due to be dropped in 0.11.0)
4
+ * Bugfix for times with negative zones
5
+
6
+ # 0.10.1 / 2013-08-27
7
+
8
+ * Support `ActiveSupport::TimeZone` (#209, #208)
9
+
10
+ # 0.10.0 / 2013-08-25
11
+
12
+ * Chronic will parse subseconds correctly
13
+ for all supported date/time formats (#195, #198 and #200)
14
+ * Support for date format: dd.mm.yyyy (#197)
15
+ * Option `:hours24` to parse as 24 hour clock (#201 and #202)
16
+ * `:guess` option allows to specify which part of Span to return.
17
+ (accepted values `false`,`true`,`:begin`, `:middle`, `:end`)
18
+ * Replace `rcov` with `SimpleCov` for coverage generation
19
+ * Add more tests
20
+ * Various changes in codebase (#202 and #206)
21
+
22
+ # 0.9.1 / 2013-02-25
23
+
24
+ * Ensure Chronic strips periods from day portions (#173)
25
+ * Properly numerize "twelfth", "twentieth" etc. (#172, James McKinney)
26
+ * Ensure Chronic is compatible with Ruby 2.0.0 (#165, Ravil Bayramgalin)
27
+
1
28
  # 0.9.0 / 2012-12-21
2
29
 
3
30
  * Implement Chronic::Parser class and create an instance of this class
data/README.md CHANGED
@@ -37,8 +37,15 @@ Chronic.parse('may 27th', :guess => false)
37
37
 
38
38
  Chronic.parse('6/4/2012', :endian_precedence => :little)
39
39
  #=> Fri Apr 06 00:00:00 PDT 2012
40
+
41
+ Chronic.parse('INVALID DATE')
42
+ #=> nil
40
43
  ```
41
44
 
45
+ If the parser can find a date or time, either a Time or Chronic::Span
46
+ will be returned (depending on the value of `:guess`). If no
47
+ date or time can be found, `nil` will be returned.
48
+
42
49
  See `Chronic.parse` for detailed usage instructions.
43
50
 
44
51
  ## Examples
@@ -128,6 +135,7 @@ Specific Times (many of the above with an added time)
128
135
  * January 5 at 7pm
129
136
  * 22nd of june at 8am
130
137
  * 1979-05-27 05:00:00
138
+ * 03/01/2012 07:25:09.234567
131
139
  * etc
132
140
 
133
141
 
data/Rakefile CHANGED
@@ -5,17 +5,37 @@ def version
5
5
  contents[/VERSION = "([^"]+)"/, 1]
6
6
  end
7
7
 
8
- task :test do
8
+ def do_test
9
9
  $:.unshift './test'
10
10
  Dir.glob('test/test_*.rb').each { |t| require File.basename(t) }
11
11
  end
12
12
 
13
- desc "Generate RCov test coverage and open in your browser"
13
+ def open_command
14
+ case RUBY_PLATFORM
15
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
16
+ 'start'
17
+ when /darwin|mac os/
18
+ 'open'
19
+ else
20
+ 'xdg-open'
21
+ end
22
+ end
23
+
24
+ task :test do
25
+ do_test
26
+ end
27
+
28
+ desc "Generate SimpleCov test coverage and open in your browser"
14
29
  task :coverage do
15
- require 'rcov'
16
- sh "rm -fr coverage"
17
- sh "rcov test/test_*.rb"
18
- sh "open coverage/index.html"
30
+ require 'simplecov'
31
+ FileUtils.rm_rf("./coverage")
32
+ SimpleCov.command_name 'Unit Tests'
33
+ SimpleCov.at_exit do
34
+ SimpleCov.result.format!
35
+ sh "#{open_command} #{SimpleCov.coverage_path}/index.html"
36
+ end
37
+ SimpleCov.start
38
+ do_test
19
39
  end
20
40
 
21
41
  desc "Open an irb session preloaded with this library"
@@ -38,9 +58,9 @@ end
38
58
 
39
59
  desc "Build a gem from the gemspec"
40
60
  task :build do
41
- sh "mkdir -p pkg"
61
+ FileUtils.mkdir_p "pkg"
42
62
  sh "gem build chronic.gemspec"
43
- sh "mv chronic-#{version}.gem pkg"
63
+ FileUtils.mv("./chronic-#{version}.gem", "pkg")
44
64
  end
45
65
 
46
66
  task :default => :test
data/chronic.gemspec CHANGED
@@ -8,13 +8,16 @@ Gem::Specification.new do |s|
8
8
  s.summary = 'Natural language date/time parsing.'
9
9
  s.description = 'Chronic is a natural language date/time parser written in pure Ruby.'
10
10
  s.authors = ['Tom Preston-Werner', 'Lee Jarvis']
11
- s.email = ['tom@mojombo.com', 'lee@jarvis.co']
11
+ s.email = ['tom@mojombo.com', 'ljjarvis@gmail.com']
12
12
  s.homepage = 'http://github.com/mojombo/chronic'
13
+ s.license = 'MIT'
13
14
  s.rdoc_options = ['--charset=UTF-8']
14
15
  s.extra_rdoc_files = %w[README.md HISTORY.md LICENSE]
15
- s.files = `git ls-files`.split("\n")
16
- s.test_files = `git ls-files -- test`.split("\n")
16
+ s.files = `git ls-files`.split($/)
17
+ s.test_files = `git ls-files -- test`.split($/)
17
18
 
18
19
  s.add_development_dependency 'rake'
19
- s.add_development_dependency 'minitest'
20
- end
20
+ s.add_development_dependency 'simplecov'
21
+ s.add_development_dependency 'minitest', '~> 5.0'
22
+ s.add_development_dependency 'activesupport'
23
+ end
@@ -0,0 +1,82 @@
1
+ module Chronic
2
+ class Date
3
+ YEAR_MONTHS = 12
4
+ MONTH_DAYS = [nil, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
5
+ MONTH_DAYS_LEAP = [nil, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
6
+ YEAR_SECONDS = 31_536_000 # 365 * 24 * 60 * 60
7
+ SEASON_SECONDS = 7_862_400 # 91 * 24 * 60 * 60
8
+ MONTH_SECONDS = 2_592_000 # 30 * 24 * 60 * 60
9
+ FORTNIGHT_SECONDS = 1_209_600 # 14 * 24 * 60 * 60
10
+ WEEK_SECONDS = 604_800 # 7 * 24 * 60 * 60
11
+ WEEKEND_SECONDS = 172_800 # 2 * 24 * 60 * 60
12
+ DAY_SECONDS = 86_400 # 24 * 60 * 60
13
+ MONTHS = {
14
+ :january => 1,
15
+ :february => 2,
16
+ :march => 3,
17
+ :april => 4,
18
+ :may => 5,
19
+ :june => 6,
20
+ :july => 7,
21
+ :august => 8,
22
+ :september => 9,
23
+ :october => 10,
24
+ :november => 11,
25
+ :december => 12
26
+ }
27
+ DAYS = {
28
+ :sunday => 0,
29
+ :monday => 1,
30
+ :tuesday => 2,
31
+ :wednesday => 3,
32
+ :thursday => 4,
33
+ :friday => 5,
34
+ :saturday => 6
35
+ }
36
+
37
+ # Checks if given number could be day
38
+ def self.could_be_day?(day)
39
+ day >= 1 && day <= 31
40
+ end
41
+
42
+ # Checks if given number could be month
43
+ def self.could_be_month?(month)
44
+ month >= 1 && month <= 12
45
+ end
46
+
47
+ # Checks if given number could be year
48
+ def self.could_be_year?(year)
49
+ year >= 1 && year <= 9999
50
+ end
51
+
52
+ # Build a year from a 2 digit suffix.
53
+ #
54
+ # year - The two digit Integer year to build from.
55
+ # bias - The Integer amount of future years to bias.
56
+ #
57
+ # Examples:
58
+ #
59
+ # make_year(96, 50) #=> 1996
60
+ # make_year(79, 20) #=> 2079
61
+ # make_year(00, 50) #=> 2000
62
+ #
63
+ # Returns The Integer 4 digit year.
64
+ def self.make_year(year, bias)
65
+ return year if year.to_s.size > 2
66
+ start_year = Chronic.time_class.now.year - bias
67
+ century = (start_year / 100) * 100
68
+ full_year = century + year
69
+ full_year += 100 if full_year < start_year
70
+ full_year
71
+ end
72
+
73
+ def self.month_overflow?(year, month, day)
74
+ if ::Date.leap?(year)
75
+ day > Date::MONTH_DAYS_LEAP[month]
76
+ else
77
+ day > Date::MONTH_DAYS[month]
78
+ end
79
+ end
80
+
81
+ end
82
+ end
@@ -19,39 +19,48 @@ module Chronic
19
19
  # Returns true if a match is found.
20
20
  def match(tokens, definitions)
21
21
  token_index = 0
22
+ @pattern.each do |elements|
23
+ was_optional = false
24
+ elements = [elements] unless elements.is_a?(Array)
25
+
26
+ elements.each_index do |i|
27
+ name = elements[i].to_s
28
+ optional = name[-1, 1] == '?'
29
+ name = name.chop if optional
30
+
31
+ case elements[i]
32
+ when Symbol
33
+ if tags_match?(name, tokens, token_index)
34
+ token_index += 1
35
+ break
36
+ else
37
+ if optional
38
+ was_optional = true
39
+ next
40
+ elsif i + 1 < elements.count
41
+ next
42
+ else
43
+ return false unless was_optional
44
+ end
45
+ end
22
46
 
23
- @pattern.each do |element|
24
- name = element.to_s
25
- optional = name[-1, 1] == '?'
26
- name = name.chop if optional
47
+ when String
48
+ return true if optional && token_index == tokens.size
27
49
 
28
- case element
29
- when Symbol
30
- if tags_match?(name, tokens, token_index)
31
- token_index += 1
32
- next
33
- else
34
- if optional
35
- next
50
+ if definitions.key?(name.to_sym)
51
+ sub_handlers = definitions[name.to_sym]
36
52
  else
37
- return false
53
+ raise "Invalid subset #{name} specified"
38
54
  end
39
- end
40
- when String
41
- return true if optional && token_index == tokens.size
42
55
 
43
- if definitions.key?(name.to_sym)
44
- sub_handlers = definitions[name.to_sym]
56
+ sub_handlers.each do |sub_handler|
57
+ return true if sub_handler.match(tokens[token_index..tokens.size], definitions)
58
+ end
45
59
  else
46
- raise "Invalid subset #{name} specified"
47
- end
48
-
49
- sub_handlers.each do |sub_handler|
50
- return true if sub_handler.match(tokens[token_index..tokens.size], definitions)
60
+ raise "Invalid match type: #{elements[i].class}"
51
61
  end
52
- else
53
- raise "Invalid match type: #{element.class}"
54
62
  end
63
+
55
64
  end
56
65
 
57
66
  return false if token_index != tokens.size
@@ -134,6 +134,8 @@ module Chronic
134
134
  def handle_generic(tokens, options)
135
135
  t = Chronic.time_class.parse(options[:text])
136
136
  Span.new(t, t + 1)
137
+ rescue ArgumentError => e
138
+ raise e unless e.message =~ /out of range/
137
139
  end
138
140
 
139
141
  # Handle repeater-month-name/scalar-day/scalar-year
@@ -250,11 +252,7 @@ module Chronic
250
252
  handle_sm_sd(new_tokens + time_tokens, options)
251
253
  end
252
254
 
253
- # Handle scalar-month/scalar-year
254
- def handle_sm_sy(tokens, options)
255
- month = tokens[0].get_tag(ScalarMonth).type
256
- year = tokens[1].get_tag(ScalarYear).type
257
-
255
+ def handle_year_and_month(year, month)
258
256
  if month == 12
259
257
  next_month_year = year + 1
260
258
  next_month_month = 1
@@ -271,6 +269,20 @@ module Chronic
271
269
  end
272
270
  end
273
271
 
272
+ # Handle scalar-month/scalar-year
273
+ def handle_sm_sy(tokens, options)
274
+ month = tokens[0].get_tag(ScalarMonth).type
275
+ year = tokens[1].get_tag(ScalarYear).type
276
+ handle_year_and_month(year, month)
277
+ end
278
+
279
+ # Handle scalar-year/scalar-month
280
+ def handle_sy_sm(tokens, options)
281
+ year = tokens[0].get_tag(ScalarYear).type
282
+ month = tokens[1].get_tag(ScalarMonth).type
283
+ handle_year_and_month(year, month)
284
+ end
285
+
274
286
  # Handle RepeaterDayName RepeaterMonthName OrdinalDay
275
287
  def handle_rdn_rmn_od(tokens, options)
276
288
  month = tokens[1].get_tag(RepeaterMonthName)
@@ -294,6 +306,23 @@ module Chronic
294
306
  end
295
307
  end
296
308
 
309
+ # Handle RepeaterDayName RepeaterMonthName OrdinalDay ScalarYear
310
+ def handle_rdn_rmn_od_sy(tokens, options)
311
+ month = tokens[1].get_tag(RepeaterMonthName)
312
+ day = tokens[2].get_tag(OrdinalDay).type
313
+ year = tokens[3].get_tag(ScalarYear).type
314
+
315
+ return if month_overflow?(year, month.index, day)
316
+
317
+ begin
318
+ start_time = Chronic.time_class.local(year, month.index, day)
319
+ end_time = time_with_rollover(year, month.index, day + 1)
320
+ Span.new(start_time, end_time)
321
+ rescue ArgumentError
322
+ nil
323
+ end
324
+ end
325
+
297
326
  # Handle RepeaterDayName OrdinalDay
298
327
  def handle_rdn_od(tokens, options)
299
328
  day = tokens[1].get_tag(OrdinalDay).type
@@ -404,7 +433,6 @@ module Chronic
404
433
 
405
434
  # Handle scalar/repeater/pointer
406
435
  def handle_s_r_p(tokens, options)
407
- repeater = tokens[1].get_tag(Repeater)
408
436
  span = Span.new(self.now, self.now + 1)
409
437
 
410
438
  handle_srp(tokens, span, options)
@@ -467,9 +495,9 @@ module Chronic
467
495
  def day_or_time(day_start, time_tokens, options)
468
496
  outer_span = Span.new(day_start, day_start + (24 * 60 * 60))
469
497
 
470
- if !time_tokens.empty?
498
+ unless time_tokens.empty?
471
499
  self.now = outer_span.begin
472
- get_anchor(dealias_and_disambiguate_times(time_tokens, options), options)
500
+ get_anchor(dealias_and_disambiguate_times(time_tokens, options), options.merge(:context => :future))
473
501
  else
474
502
  outer_span
475
503
  end
@@ -516,7 +544,7 @@ module Chronic
516
544
  end
517
545
 
518
546
  def month_overflow?(year, month, day)
519
- if Date.leap?(year)
547
+ if ::Date.leap?(year)
520
548
  day > RepeaterMonth::MONTH_DAYS_LEAP[month - 1]
521
549
  else
522
550
  day > RepeaterMonth::MONTH_DAYS[month - 1]
@@ -37,7 +37,16 @@ module Chronic
37
37
  ['seventh', '7'],
38
38
  ['eighth', '8'],
39
39
  ['ninth', '9'],
40
- ['tenth', '10']
40
+ ['tenth', '10'],
41
+ ['twelfth', '12'],
42
+ ['twentieth', '20'],
43
+ ['thirtieth', '30'],
44
+ ['fourtieth', '40'],
45
+ ['fiftieth', '50'],
46
+ ['sixtieth', '60'],
47
+ ['seventieth', '70'],
48
+ ['eightieth', '80'],
49
+ ['ninetieth', '90']
41
50
  ]
42
51
 
43
52
  TEN_PREFIXES = [
@@ -90,7 +99,7 @@ module Chronic
90
99
  # hundreds, thousands, millions, etc.
91
100
 
92
101
  BIG_PREFIXES.each do |bp|
93
- string.gsub!(/(?:<num>)?(\d*) *#{bp[0]}/i) { '<num>' + (bp[1] * $1.to_i).to_s}
102
+ string.gsub!(/(?:<num>)?(\d*) *#{bp[0]}/i) { $1.empty? ? bp[1] : '<num>' + (bp[1] * $1.to_i).to_s}
94
103
  andition(string)
95
104
  end
96
105
 
@@ -9,26 +9,16 @@ module Chronic
9
9
  #
10
10
  # Returns an Array of tokens.
11
11
  def self.scan(tokens, options)
12
- tokens.each do |token|
13
- if t = scan_for_ordinals(token) then token.tag(t) end
14
- if t = scan_for_days(token) then token.tag(t) end
15
- end
16
- end
17
-
18
- # token - The Token object we want to scan.
19
- #
20
- # Returns a new Ordinal object.
21
- def self.scan_for_ordinals(token)
22
- Ordinal.new($1.to_i) if token.word =~ /^(\d*)(st|nd|rd|th)$/
23
- end
24
-
25
- # token - The Token object we want to scan.
26
- #
27
- # Returns a new Ordinal object.
28
- def self.scan_for_days(token)
29
- if token.word =~ /^(\d*)(st|nd|rd|th)$/
30
- unless $1.to_i > 31 || $1.to_i < 1
31
- OrdinalDay.new(token.word.to_i)
12
+ tokens.each_index do |i|
13
+ if tokens[i].word =~ /^(\d+)(st|nd|rd|th|\.)$/
14
+ ordinal = $1.to_i
15
+ tokens[i].tag(Ordinal.new(ordinal))
16
+ tokens[i].tag(OrdinalDay.new(ordinal)) if Chronic::Date::could_be_day?(ordinal)
17
+ tokens[i].tag(OrdinalMonth.new(ordinal)) if Chronic::Date::could_be_month?(ordinal)
18
+ if Chronic::Date::could_be_year?(ordinal)
19
+ year = Chronic::Date::make_year(ordinal, options[:ambiguous_year_future_bias])
20
+ tokens[i].tag(OrdinalYear.new(year.to_i))
21
+ end
32
22
  end
33
23
  end
34
24
  end
@@ -44,4 +34,16 @@ module Chronic
44
34
  end
45
35
  end
46
36
 
37
+ class OrdinalMonth < Ordinal #:nodoc:
38
+ def to_s
39
+ super << '-month-' << @type.to_s
40
+ end
41
+ end
42
+
43
+ class OrdinalYear < Ordinal #:nodoc:
44
+ def to_s
45
+ super << '-year-' << @type.to_s
46
+ end
47
+ end
48
+
47
49
  end
@@ -8,6 +8,7 @@ module Chronic
8
8
  DEFAULT_OPTIONS = {
9
9
  :context => :future,
10
10
  :now => nil,
11
+ :hours24 => nil,
11
12
  :guess => true,
12
13
  :ambiguous_time_range => 6,
13
14
  :endian_precedence => [:middle, :little],
@@ -23,10 +24,13 @@ module Chronic
23
24
  # given, it will assume it is in the past.
24
25
  # :now - Time, all computations will be based off of time
25
26
  # instead of Time.now.
27
+ # :hours24 - Time will be parsed as it would be 24 hour clock.
26
28
  # :guess - By default the parser will guess a single point in time
27
29
  # for the given date or time. If you'd rather have the
28
30
  # entire time span returned, set this to false
29
- # and a Chronic::Span will be returned.
31
+ # and a Chronic::Span will be returned. Setting :guess to :end
32
+ # will return last time from Span, to :middle for middle (same as just true)
33
+ # and :begin for first time from span.
30
34
  # :ambiguous_time_range - If an Integer is given, ambiguous times
31
35
  # (like 5:00) will be assumed to be within the range of
32
36
  # that time in the AM to that time in the PM. For
@@ -58,9 +62,7 @@ module Chronic
58
62
 
59
63
  puts "+#{'-' * 51}\n| #{tokens}\n+#{'-' * 51}" if Chronic.debug
60
64
 
61
- if span
62
- options[:guess] ? guess(span) : span
63
- end
65
+ guess(span, options[:guess]) if span
64
66
  end
65
67
 
66
68
  # Clean up the specified text ready for parsing.
@@ -86,13 +88,16 @@ module Chronic
86
88
  # Returns a new String ready for Chronic to parse.
87
89
  def pre_normalize(text)
88
90
  text = text.to_s.downcase
91
+ text.gsub!(/\b(\d{2})\.(\d{2})\.(\d{4})\b/, '\3 / \2 / \1')
92
+ text.gsub!(/\b([ap])\.m\.?/, '\1m')
93
+ text.gsub!(/(\s+|:\d{2}|:\d{2}\.\d{3})\-(\d{2}:?\d{2})\b/, '\1tzminus\2')
89
94
  text.gsub!(/\./, ':')
95
+ text.gsub!(/([ap]):m:?/, '\1m')
90
96
  text.gsub!(/['"]/, '')
91
97
  text.gsub!(/,/, ' ')
92
98
  text.gsub!(/^second /, '2nd ')
93
99
  text.gsub!(/\bsecond (of|day|month|hour|minute|second)\b/, '2nd \1')
94
100
  text = Numerizer.numerize(text)
95
- text.gsub!(/\-(\d{2}:?\d{2})\b/, 'tzminus\1')
96
101
  text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
97
102
  text.gsub!(/(?:^|\s)0(\d+:\d+\s*pm?\b)/, ' \1')
98
103
  text.gsub!(/\btoday\b/, 'this day')
@@ -116,6 +121,7 @@ module Chronic
116
121
  text.gsub!(/\b(hence|after|from)\b/, 'future')
117
122
  text.gsub!(/^\s?an? /i, '1 ')
118
123
  text.gsub!(/\b(\d{4}):(\d{2}):(\d{2})\b/, '\1 / \2 / \3') # DTOriginal
124
+ text.gsub!(/\b0(\d+):(\d{2}):(\d{2}) ([ap]m)\b/, '\1:\2:\3 \4')
119
125
  text
120
126
  end
121
127
 
@@ -124,12 +130,11 @@ module Chronic
124
130
  # span - The Chronic::Span object to calcuate a guess from.
125
131
  #
126
132
  # Returns a new Time object.
127
- def guess(span)
128
- if span.width > 1
129
- span.begin + (span.width / 2)
130
- else
131
- span.begin
132
- end
133
+ def guess(span, mode = :middle)
134
+ return span if not mode
135
+ return span.begin + span.width / 2 if span.width > 1 and (mode == true or mode == :middle)
136
+ return span.end if mode == :end
137
+ span.begin
133
138
  end
134
139
 
135
140
  # List of Handler definitions. See Chronic.parse for a list of options this
@@ -147,19 +152,21 @@ module Chronic
147
152
  ],
148
153
 
149
154
  :date => [
150
- Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :repeater_time, :separator_slash_or_dash?, :time_zone, :scalar_year], :handle_generic),
155
+ Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :repeater_time, [:separator_slash?, :separator_dash?], :time_zone, :scalar_year], :handle_generic),
151
156
  Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day], :handle_rdn_rmn_sd),
152
157
  Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :scalar_year], :handle_rdn_rmn_sd_sy),
153
158
  Handler.new([:repeater_day_name, :repeater_month_name, :ordinal_day], :handle_rdn_rmn_od),
159
+ Handler.new([:repeater_day_name, :repeater_month_name, :ordinal_day, :scalar_year], :handle_rdn_rmn_od_sy),
154
160
  Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rdn_rmn_sd),
155
161
  Handler.new([:repeater_day_name, :repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rdn_rmn_od),
156
162
  Handler.new([:repeater_day_name, :ordinal_day, :separator_at?, 'time?'], :handle_rdn_od),
157
- Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :repeater_time, :time_zone], :handle_generic),
163
+ Handler.new([:scalar_year, [:separator_slash, :separator_dash], :scalar_month, [:separator_slash, :separator_dash], :scalar_day, :repeater_time, :time_zone], :handle_generic),
164
+ Handler.new([:ordinal_day], :handle_generic),
158
165
  Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
159
166
  Handler.new([:repeater_month_name, :ordinal_day, :scalar_year], :handle_rmn_od_sy),
160
167
  Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
161
168
  Handler.new([:repeater_month_name, :ordinal_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_od_sy),
162
- Handler.new([:repeater_month_name, :separator_slash_or_dash?, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
169
+ Handler.new([:repeater_month_name, [:separator_slash?, :separator_dash?], :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
163
170
  Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :scalar_day], :handle_rmn_sd_on),
164
171
  Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
165
172
  Handler.new([:ordinal_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_od_rmn_sy),
@@ -169,11 +176,12 @@ module Chronic
169
176
  Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :ordinal_day], :handle_rmn_od_on),
170
177
  Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
171
178
  Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
172
- Handler.new([:scalar_day, :separator_slash_or_dash?, :repeater_month_name, :separator_at?, 'time?'], :handle_sd_rmn),
173
- Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
174
- Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy),
175
- Handler.new([:scalar_day, :separator_slash_or_dash, :repeater_month_name, :separator_slash_or_dash, :scalar_year, :repeater_time?], :handle_sm_rmn_sy),
176
- Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar?, :time_zone], :handle_generic),
179
+ Handler.new([:scalar_day, [:separator_slash?, :separator_dash?], :repeater_month_name, :separator_at?, 'time?'], :handle_sd_rmn),
180
+ Handler.new([:scalar_year, [:separator_slash, :separator_dash], :scalar_month, [:separator_slash, :separator_dash], :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
181
+ Handler.new([:scalar_year, [:separator_slash, :separator_dash], :scalar_month], :handle_sy_sm),
182
+ Handler.new([:scalar_month, [:separator_slash, :separator_dash], :scalar_year], :handle_sm_sy),
183
+ Handler.new([:scalar_day, [:separator_slash, :separator_dash], :repeater_month_name, [:separator_slash, :separator_dash], :scalar_year, :repeater_time?], :handle_sm_rmn_sy),
184
+ Handler.new([:scalar_year, [:separator_slash, :separator_dash], :scalar_month, [:separator_slash, :separator_dash], :scalar?, :time_zone], :handle_generic),
177
185
  ],
178
186
 
179
187
  :anchor => [
@@ -196,10 +204,10 @@ module Chronic
196
204
  }
197
205
 
198
206
  endians = [
199
- Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
200
- Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sm_sd),
201
- Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_at?, 'time?'], :handle_sd_sm),
202
- Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy)
207
+ Handler.new([:scalar_month, [:separator_slash, :separator_dash], :scalar_day, [:separator_slash, :separator_dash], :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
208
+ Handler.new([:scalar_month, [:separator_slash, :separator_dash], :scalar_day, :separator_at?, 'time?'], :handle_sm_sd),
209
+ Handler.new([:scalar_day, [:separator_slash, :separator_dash], :scalar_month, :separator_at?, 'time?'], :handle_sd_sm),
210
+ Handler.new([:scalar_day, [:separator_slash, :separator_dash], :scalar_month, [:separator_slash, :separator_dash], :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy)
203
211
  ]
204
212
 
205
213
  case endian = Array(options[:endian_precedence]).first
@@ -217,7 +225,7 @@ module Chronic
217
225
  def tokenize(text, options)
218
226
  text = pre_normalize(text)
219
227
  tokens = text.split(' ').map { |word| Token.new(word) }
220
- [Repeater, Grabber, Pointer, Scalar, Ordinal, Separator, TimeZone].each do |tok|
228
+ [Repeater, Grabber, Pointer, Scalar, Ordinal, Separator, Sign, TimeZone].each do |tok|
221
229
  tok.scan(tokens, options)
222
230
  end
223
231
  tokens.select { |token| token.tagged? }
@@ -242,7 +250,7 @@ module Chronic
242
250
 
243
251
  definitions[:arrow].each do |handler|
244
252
  if handler.match(tokens, definitions)
245
- good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlashOrDash) || o.get_tag(SeparatorComma) || o.get_tag(SeparatorAnd) }
253
+ good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlash) || o.get_tag(SeparatorDash) || o.get_tag(SeparatorComma) || o.get_tag(SeparatorAnd) }
246
254
  return handler.invoke(:arrow, good_tokens, self, options)
247
255
  end
248
256
  end