chronic 0.9.1 → 0.10.0

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 (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