chronic 0.9.1 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -4
  3. data/HISTORY.md +12 -0
  4. data/README.md +8 -0
  5. data/Rakefile +28 -8
  6. data/chronic.gemspec +7 -5
  7. data/lib/chronic.rb +10 -10
  8. data/lib/chronic/date.rb +82 -0
  9. data/lib/chronic/handler.rb +34 -25
  10. data/lib/chronic/handlers.rb +22 -3
  11. data/lib/chronic/ordinal.rb +22 -20
  12. data/lib/chronic/parser.rb +31 -26
  13. data/lib/chronic/repeater.rb +18 -18
  14. data/lib/chronic/repeaters/repeater_day.rb +4 -3
  15. data/lib/chronic/repeaters/repeater_day_name.rb +5 -4
  16. data/lib/chronic/repeaters/repeater_day_portion.rb +4 -3
  17. data/lib/chronic/repeaters/repeater_fortnight.rb +4 -3
  18. data/lib/chronic/repeaters/repeater_hour.rb +4 -3
  19. data/lib/chronic/repeaters/repeater_minute.rb +4 -3
  20. data/lib/chronic/repeaters/repeater_month.rb +5 -4
  21. data/lib/chronic/repeaters/repeater_month_name.rb +4 -3
  22. data/lib/chronic/repeaters/repeater_season.rb +5 -3
  23. data/lib/chronic/repeaters/repeater_second.rb +4 -3
  24. data/lib/chronic/repeaters/repeater_time.rb +35 -25
  25. data/lib/chronic/repeaters/repeater_week.rb +4 -3
  26. data/lib/chronic/repeaters/repeater_weekday.rb +4 -3
  27. data/lib/chronic/repeaters/repeater_weekend.rb +4 -3
  28. data/lib/chronic/repeaters/repeater_year.rb +5 -4
  29. data/lib/chronic/scalar.rb +34 -69
  30. data/lib/chronic/separator.rb +107 -8
  31. data/lib/chronic/sign.rb +49 -0
  32. data/lib/chronic/tag.rb +5 -4
  33. data/lib/chronic/time.rb +40 -0
  34. data/test/helper.rb +2 -2
  35. data/test/test_chronic.rb +4 -4
  36. data/test/test_handler.rb +20 -0
  37. data/test/test_parsing.rb +72 -3
  38. data/test/test_repeater_time.rb +18 -0
  39. metadata +24 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7033fa51e89c8552e935aab7ee0bb5673338d063
4
- data.tar.gz: 21a09efad9e8da2dcd3ed32a5c8605707f36abc7
3
+ metadata.gz: ce57c7b6e3bd50e021abd796f6cdbb4cad313dbc
4
+ data.tar.gz: 2ea7a98ce486caf4bbcf394903ee150a920ad888
5
5
  SHA512:
6
- metadata.gz: f3d63e888e712dad2b0d4c70d11688607e56a3d1b4ac11632e14b3f23a57f21a2fd32403f50cf13002a9859607a89e6521b5c39f4ec003edd5631791cf7cf409
7
- data.tar.gz: 9e2d80dcf087643ce865ede613dbd2d006942b1986e46c02f75b012711ee79bb74f0b576ba812d8b54ab599411213548effc036cc888c97b82c70fdf21627365
6
+ metadata.gz: 0737803ebaad62093afbdb8cabece3ec65f0921c174e1a0dc4869fa7c48c539e82bd01834d8ecf3b763e7692016cfc6bbe7893d5650a2ca8cfcd9df98e554012
7
+ data.tar.gz: c508780a3ea66dbce11b4351ed546483e30adb6cfa27c5f86dc466132f07447d0e5f13165990d7053d6015277f05464dfc5018f365b40e82e775f3bc7e5e6ba0
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/HISTORY.md CHANGED
@@ -1,3 +1,15 @@
1
+ # 0.10.0 / 2013-08-25
2
+
3
+ * Chronic will parse subseconds correctly
4
+ for all supported date/time formats (#195, #198 and #200)
5
+ * Support for date format: dd.mm.yyyy (#197)
6
+ * Option `:hours24` to parse as 24 hour clock (#201 and #202)
7
+ * `:guess` option allows to specify which part of Span to return.
8
+ (accepted values `false`,`true`,`:begin`, `:middle`, `:end`)
9
+ * Replace `rcov` with `SimpleCov` for coverage generation
10
+ * Add more tests
11
+ * Various changes in codebase (#202 and #206)
12
+
1
13
  # 0.9.1 / 2013-02-25
2
14
 
3
15
  * Ensure Chronic strips periods from day portions (#173)
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
@@ -8,13 +8,15 @@ 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
+ end
@@ -2,6 +2,8 @@ require 'time'
2
2
  require 'date'
3
3
 
4
4
  require 'chronic/parser'
5
+ require 'chronic/date'
6
+ require 'chronic/time'
5
7
 
6
8
  require 'chronic/handler'
7
9
  require 'chronic/handlers'
@@ -14,6 +16,7 @@ require 'chronic/pointer'
14
16
  require 'chronic/scalar'
15
17
  require 'chronic/ordinal'
16
18
  require 'chronic/separator'
19
+ require 'chronic/sign'
17
20
  require 'chronic/time_zone'
18
21
  require 'chronic/numerizer'
19
22
  require 'chronic/season'
@@ -50,7 +53,7 @@ require 'chronic/repeaters/repeater_time'
50
53
  # Chronic.parse('monday', :context => :past)
51
54
  # #=> Mon Aug 21 12:00:00 PDT 2006
52
55
  module Chronic
53
- VERSION = "0.9.1"
56
+ VERSION = "0.10.0"
54
57
 
55
58
  class << self
56
59
 
@@ -72,7 +75,7 @@ module Chronic
72
75
  end
73
76
 
74
77
  self.debug = false
75
- self.time_class = Time
78
+ self.time_class = ::Time
76
79
 
77
80
 
78
81
  # Parses a string containing a natural language date or time.
@@ -98,7 +101,7 @@ module Chronic
98
101
  # second - Integer second.
99
102
  #
100
103
  # Returns a new Time object constructed from these params.
101
- def self.construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0)
104
+ def self.construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0, offset = nil)
102
105
  if second >= 60
103
106
  minute += second / 60
104
107
  second = second % 60
@@ -117,11 +120,8 @@ module Chronic
117
120
  # determine if there is a day overflow. this is complicated by our crappy calendar
118
121
  # system (non-constant number of days per month)
119
122
  day <= 56 || raise("day must be no more than 56 (makes month resolution easier)")
120
- if day > 28
121
- # no month ever has fewer than 28 days, so only do this if necessary
122
- leap_year_month_days = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
123
- common_year_month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
124
- days_this_month = Date.leap?(year) ? leap_year_month_days[month - 1] : common_year_month_days[month - 1]
123
+ if day > 28 # no month ever has fewer than 28 days, so only do this if necessary
124
+ days_this_month = ::Date.leap?(year) ? Date::MONTH_DAYS_LEAP[month] : Date::MONTH_DAYS[month]
125
125
  if day > days_this_month
126
126
  month += day / days_this_month
127
127
  day = day % days_this_month
@@ -137,8 +137,8 @@ module Chronic
137
137
  month = month % 12
138
138
  end
139
139
  end
140
-
141
- Chronic.time_class.local(year, month, day, hour, minute, second)
140
+ offset = Time::normalize_offset(offset) if Chronic.time_class.is_a?(DateTime)
141
+ Chronic.time_class.new(year, month, day, hour, minute, second, offset)
142
142
  end
143
143
 
144
144
  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
@@ -304,6 +306,23 @@ module Chronic
304
306
  end
305
307
  end
306
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
+
307
326
  # Handle RepeaterDayName OrdinalDay
308
327
  def handle_rdn_od(tokens, options)
309
328
  day = tokens[1].get_tag(OrdinalDay).type
@@ -476,9 +495,9 @@ module Chronic
476
495
  def day_or_time(day_start, time_tokens, options)
477
496
  outer_span = Span.new(day_start, day_start + (24 * 60 * 60))
478
497
 
479
- if !time_tokens.empty?
498
+ unless time_tokens.empty?
480
499
  self.now = outer_span.begin
481
- get_anchor(dealias_and_disambiguate_times(time_tokens, options), options)
500
+ get_anchor(dealias_and_disambiguate_times(time_tokens, options), options.merge(:context => :future))
482
501
  else
483
502
  outer_span
484
503
  end
@@ -525,7 +544,7 @@ module Chronic
525
544
  end
526
545
 
527
546
  def month_overflow?(year, month, day)
528
- if Date.leap?(year)
547
+ if ::Date.leap?(year)
529
548
  day > RepeaterMonth::MONTH_DAYS_LEAP[month - 1]
530
549
  else
531
550
  day > RepeaterMonth::MONTH_DAYS[month - 1]
@@ -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