chronic 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/HISTORY.md +27 -0
  2. data/Manifest.txt +16 -5
  3. data/README.md +14 -8
  4. data/Rakefile +2 -8
  5. data/chronic.gemspec +8 -11
  6. data/lib/chronic.rb +21 -14
  7. data/lib/chronic/chronic.rb +38 -130
  8. data/lib/chronic/grabber.rb +11 -15
  9. data/lib/chronic/handlers.rb +63 -40
  10. data/lib/chronic/mini_date.rb +27 -0
  11. data/lib/chronic/numerizer.rb +120 -0
  12. data/lib/chronic/ordinal.rb +5 -10
  13. data/lib/chronic/pointer.rb +8 -10
  14. data/lib/chronic/repeater.rb +106 -109
  15. data/lib/chronic/repeaters/repeater_day.rb +43 -41
  16. data/lib/chronic/repeaters/repeater_day_name.rb +38 -36
  17. data/lib/chronic/repeaters/repeater_day_portion.rb +74 -73
  18. data/lib/chronic/repeaters/repeater_fortnight.rb +57 -55
  19. data/lib/chronic/repeaters/repeater_hour.rb +46 -44
  20. data/lib/chronic/repeaters/repeater_minute.rb +46 -44
  21. data/lib/chronic/repeaters/repeater_month.rb +52 -50
  22. data/lib/chronic/repeaters/repeater_month_name.rb +84 -80
  23. data/lib/chronic/repeaters/repeater_season.rb +97 -119
  24. data/lib/chronic/repeaters/repeater_season_name.rb +39 -39
  25. data/lib/chronic/repeaters/repeater_second.rb +32 -30
  26. data/lib/chronic/repeaters/repeater_time.rb +106 -101
  27. data/lib/chronic/repeaters/repeater_week.rb +60 -58
  28. data/lib/chronic/repeaters/repeater_weekday.rb +67 -58
  29. data/lib/chronic/repeaters/repeater_weekend.rb +54 -52
  30. data/lib/chronic/repeaters/repeater_year.rb +50 -48
  31. data/lib/chronic/scalar.rb +24 -16
  32. data/lib/chronic/separator.rb +15 -33
  33. data/lib/chronic/span.rb +31 -0
  34. data/lib/chronic/tag.rb +26 -0
  35. data/lib/chronic/time_zone.rb +7 -9
  36. data/lib/chronic/token.rb +35 -0
  37. data/test/helper.rb +5 -6
  38. data/test/test_Chronic.rb +5 -0
  39. data/test/test_Numerizer.rb +60 -39
  40. data/test/test_RepeaterHour.rb +4 -0
  41. data/test/test_parsing.rb +104 -13
  42. metadata +14 -20
  43. data/lib/chronic/numerizer/numerizer.rb +0 -97
data/HISTORY.md CHANGED
@@ -1,3 +1,30 @@
1
+ # 0.4.0 / 2011-06-04
2
+
3
+ * Ensure context is being passed through grabbers. Now "Sunday at 2:18pm"
4
+ with `:context => :past` will return the correct date
5
+ * Support parsing ordinal strings (eg first, twenty third => 1st, 23rd)
6
+ * Seasons now ignore DST and return 00 as an hour
7
+ * Support parsing 2 digit years and added `ambiguous_year_future_bias` option
8
+ * Support parsing 'thurs' for Thursday
9
+ * Fix pre_normalize() to remove periods before numerizing
10
+ * Fix RepeaterDays to not add an extra hour in future tense. This meant
11
+ when parsing 'yesterday' after 11PM, Chronic would return today
12
+ * Discard any prefixed 0 for time strings when using post noon portion
13
+ * Gemspec updates for RubyGems deprecations
14
+ * Ensure 0:10 is treated like 00:10
15
+ * Ensure we load classes after setting Chronic class instance variables
16
+ so we can debug initialization and do assignments at compile time
17
+ * Added a Tag.scan_for method for DRYing up some scanning code
18
+ * Move some classes into their own files for maintainability
19
+ * Numerizer.andition should be a private class method, make it so
20
+ * Namespaced Numerizer, Season and MiniDate (Sascha Teske)
21
+ * Support for Ruby 1.9 (Dave Lee, Aaron Hurley)
22
+ * Fix `:context => :past` where parsed date is in current month (Marc Hedlund)
23
+ * Fix undefined variable in RepeaterHour (Ryan Garver)
24
+ * Added support for parsing 'Fourty' as another mis-spelling (Lee Reilly)
25
+ * Added ordinal format support: ie 'February 14th, 2004' (Jeff Felchner)
26
+ * Fix dates when working with daylight saving times Mike Mangino
27
+
1
28
  # 0.3.0 / 2010-10-22
2
29
 
3
30
  * Fix numerizer number combination bug (27 Oct 2006 7:30pm works now)
@@ -1,11 +1,16 @@
1
- History.txt
1
+ HISTORY.md
2
+ LICENSE
2
3
  Manifest.txt
3
- README.txt
4
+ README.md
4
5
  Rakefile
6
+ benchmark/benchmark.rb
7
+ chronic.gemspec
5
8
  lib/chronic.rb
6
9
  lib/chronic/chronic.rb
7
10
  lib/chronic/grabber.rb
8
11
  lib/chronic/handlers.rb
12
+ lib/chronic/mini_date.rb
13
+ lib/chronic/numerizer.rb
9
14
  lib/chronic/ordinal.rb
10
15
  lib/chronic/pointer.rb
11
16
  lib/chronic/repeater.rb
@@ -22,26 +27,32 @@ lib/chronic/repeaters/repeater_season_name.rb
22
27
  lib/chronic/repeaters/repeater_second.rb
23
28
  lib/chronic/repeaters/repeater_time.rb
24
29
  lib/chronic/repeaters/repeater_week.rb
30
+ lib/chronic/repeaters/repeater_weekday.rb
25
31
  lib/chronic/repeaters/repeater_weekend.rb
26
32
  lib/chronic/repeaters/repeater_year.rb
27
33
  lib/chronic/scalar.rb
28
34
  lib/chronic/separator.rb
35
+ lib/chronic/span.rb
36
+ lib/chronic/tag.rb
29
37
  lib/chronic/time_zone.rb
30
- lib/numerizer/numerizer.rb
31
- test/suite.rb
38
+ lib/chronic/token.rb
39
+ test/helper.rb
32
40
  test/test_Chronic.rb
41
+ test/test_DaylightSavings.rb
33
42
  test/test_Handler.rb
34
43
  test/test_Numerizer.rb
35
44
  test/test_RepeaterDayName.rb
36
45
  test/test_RepeaterFortnight.rb
37
46
  test/test_RepeaterHour.rb
47
+ test/test_RepeaterMinute.rb
38
48
  test/test_RepeaterMonth.rb
39
49
  test/test_RepeaterMonthName.rb
40
50
  test/test_RepeaterTime.rb
41
51
  test/test_RepeaterWeek.rb
52
+ test/test_RepeaterWeekday.rb
42
53
  test/test_RepeaterWeekend.rb
43
54
  test/test_RepeaterYear.rb
44
55
  test/test_Span.rb
45
56
  test/test_Time.rb
46
57
  test/test_Token.rb
47
- test/test_parsing.rb
58
+ test/test_parsing.rb
data/README.md CHANGED
@@ -9,23 +9,26 @@ for the wide variety of formats Chronic will parse.
9
9
 
10
10
  ## INSTALLATION
11
11
 
12
- The best way to install Gollum is with RubyGems:
12
+ ### RubyGems
13
13
 
14
14
  $ [sudo] gem install chronic
15
15
 
16
+ ### GitHub
17
+
18
+ $ git clone git://github.com/mojombo/chronic.git
19
+ $ cd chronic && gem build chronic.gemspec
20
+ $ gem install chronic-<version>.gem
21
+
16
22
 
17
23
  ## USAGE
18
24
 
19
25
  You can parse strings containing a natural language date using the
20
- Chronic.parse method.
26
+ `Chronic.parse` method.
21
27
 
22
- require 'rubygems'
23
28
  require 'chronic'
24
29
 
25
30
  Time.now #=> Sun Aug 27 23:18:25 PDT 2006
26
31
 
27
- #---
28
-
29
32
  Chronic.parse('tomorrow')
30
33
  #=> Mon Aug 28 12:00:00 PDT 2006
31
34
 
@@ -44,7 +47,7 @@ Chronic.parse method.
44
47
  Chronic.parse('may 27th', :guess => false)
45
48
  #=> Sun May 27 00:00:00 PDT 2007..Mon May 28 00:00:00 PDT 2007
46
49
 
47
- See Chronic.parse for detailed usage instructions.
50
+ See `Chronic.parse` for detailed usage instructions.
48
51
 
49
52
 
50
53
  ## EXAMPLES
@@ -99,12 +102,14 @@ Complex
99
102
  Specific Dates
100
103
 
101
104
  * January 5
105
+ * February twenty first
102
106
  * dec 25
103
107
  * may 27th
104
108
  * October 2006
105
109
  * oct 06
106
110
  * jan 3 2010
107
111
  * february 14, 2004
112
+ * february 14th, 2004
108
113
  * 3 jan 2000
109
114
  * 17 april 85
110
115
  * 5/27/1979
@@ -149,7 +154,7 @@ Support for a wider range of times is planned for a future release.
149
154
 
150
155
  If you'd like to hack on Chronic, start by forking the repo on GitHub:
151
156
 
152
- http://github.com/github/chronic
157
+ https://github.com/mojombo/chronic
153
158
 
154
159
  To get all of the dependencies, install the gem first. The best way to get
155
160
  your changes merged back into core is as follows:
@@ -158,8 +163,9 @@ your changes merged back into core is as follows:
158
163
  1. Create a thoughtfully named topic branch to contain your change
159
164
  1. Hack away
160
165
  1. Add tests and make sure everything still passes by running `rake`
166
+ 1. Ensure your tests pass in multiple timezones
161
167
  1. If you are adding new functionality, document it in the README
162
168
  1. Do not change the version number, we will do that on our end
163
169
  1. If necessary, rebase your commits into logical chunks, without errors
164
170
  1. Push the branch up to GitHub
165
- 1. Send a pull request for your branch
171
+ 1. Send a pull request for your branch
data/Rakefile CHANGED
@@ -1,5 +1,3 @@
1
- require 'rubygems'
2
- require 'rake'
3
1
  require 'date'
4
2
 
5
3
  #############################################################################
@@ -17,10 +15,6 @@ def version
17
15
  line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
18
16
  end
19
17
 
20
- def date
21
- Date.today.to_s
22
- end
23
-
24
18
  def rubyforge_project
25
19
  name
26
20
  end
@@ -70,7 +64,7 @@ end
70
64
 
71
65
  desc "Open an irb session preloaded with this library"
72
66
  task :console do
73
- sh "irb -rubygems -r ./lib/#{name}.rb"
67
+ sh "irb -Ilib -rchronic"
74
68
  end
75
69
 
76
70
  #############################################################################
@@ -113,7 +107,7 @@ task :gemspec => :validate do
113
107
  # replace name version and date
114
108
  replace_header(head, :name)
115
109
  replace_header(head, :version)
116
- replace_header(head, :date)
110
+
117
111
  #comment this out if your rubyforge_project has a different name
118
112
  replace_header(head, :rubyforge_project)
119
113
 
@@ -1,22 +1,15 @@
1
1
  Gem::Specification.new do |s|
2
- s.specification_version = 2 if s.respond_to? :specification_version=
3
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
4
- s.rubygems_version = '1.3.5'
5
-
6
2
  s.name = 'chronic'
7
- s.version = '0.3.0'
8
- s.date = '2010-10-22'
3
+ s.version = '0.4.0'
9
4
  s.rubyforge_project = 'chronic'
10
5
 
11
6
  s.summary = "Natural language date/time parsing."
12
7
  s.description = "Chronic is a natural language date/time parser written in pure Ruby."
13
8
 
14
- s.authors = ["Tom Preston-Werner"]
15
- s.email = 'tom@mojombo.com'
9
+ s.authors = ["Tom Preston-Werner", "Lee Jarvis"]
10
+ s.email = ['tom@mojombo.com', 'lee@jarvis.co']
16
11
  s.homepage = 'http://github.com/mojombo/chronic'
17
12
 
18
- s.require_paths = %w[lib]
19
-
20
13
  s.rdoc_options = ["--charset=UTF-8"]
21
14
  s.extra_rdoc_files = %w[README.md HISTORY.md LICENSE]
22
15
 
@@ -33,7 +26,8 @@ Gem::Specification.new do |s|
33
26
  lib/chronic/chronic.rb
34
27
  lib/chronic/grabber.rb
35
28
  lib/chronic/handlers.rb
36
- lib/chronic/numerizer/numerizer.rb
29
+ lib/chronic/mini_date.rb
30
+ lib/chronic/numerizer.rb
37
31
  lib/chronic/ordinal.rb
38
32
  lib/chronic/pointer.rb
39
33
  lib/chronic/repeater.rb
@@ -55,7 +49,10 @@ Gem::Specification.new do |s|
55
49
  lib/chronic/repeaters/repeater_year.rb
56
50
  lib/chronic/scalar.rb
57
51
  lib/chronic/separator.rb
52
+ lib/chronic/span.rb
53
+ lib/chronic/tag.rb
58
54
  lib/chronic/time_zone.rb
55
+ lib/chronic/token.rb
59
56
  test/helper.rb
60
57
  test/test_Chronic.rb
61
58
  test/test_DaylightSavings.rb
@@ -7,12 +7,27 @@
7
7
  #
8
8
  #=============================================================================
9
9
 
10
- $:.unshift File.dirname(__FILE__) # For use/testing when no gem is installed
10
+ module Chronic
11
+ VERSION = "0.4.0"
12
+
13
+ class << self
14
+ attr_accessor :debug
15
+ attr_accessor :time_class
16
+ end
17
+
18
+ self.debug = false
19
+ self.time_class = Time
20
+ end
11
21
 
12
22
  require 'time'
23
+ require 'date'
13
24
 
14
25
  require 'chronic/chronic'
15
26
  require 'chronic/handlers'
27
+ require 'chronic/mini_date'
28
+ require 'chronic/tag'
29
+ require 'chronic/span'
30
+ require 'chronic/token'
16
31
 
17
32
  require 'chronic/repeater'
18
33
  require 'chronic/repeaters/repeater_year'
@@ -39,19 +54,7 @@ require 'chronic/ordinal'
39
54
  require 'chronic/separator'
40
55
  require 'chronic/time_zone'
41
56
 
42
- require 'chronic/numerizer/numerizer'
43
-
44
- module Chronic
45
- VERSION = "0.3.0"
46
-
47
- class << self
48
- attr_accessor :debug
49
- attr_accessor :time_class
50
- end
51
-
52
- self.debug = false
53
- self.time_class = Time
54
- end
57
+ require 'chronic/numerizer'
55
58
 
56
59
  # class Time
57
60
  # def self.construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0)
@@ -124,4 +127,8 @@ class Time
124
127
 
125
128
  Chronic.time_class.local(year, month, day, hour, minute, second)
126
129
  end
130
+
131
+ def to_minidate
132
+ Chronic::MiniDate.new(self.month, self.day)
133
+ end
127
134
  end
@@ -1,4 +1,14 @@
1
1
  module Chronic
2
+
3
+ DEFAULT_OPTIONS = {
4
+ :context => :future,
5
+ :now => nil,
6
+ :guess => true,
7
+ :ambiguous_time_range => 6,
8
+ :endian_precedence => [:middle, :little],
9
+ :ambiguous_year_future_bias => 50
10
+ }
11
+
2
12
  class << self
3
13
 
4
14
  # Parses a string containing a natural language date or time. If the parser
@@ -38,48 +48,31 @@ module Chronic
38
48
  # assume that means 5:00pm. If <tt>:none</tt> is given, no assumption
39
49
  # will be made, and the first matching instance of that time will
40
50
  # be used.
51
+ #
52
+ # [<tt>:endian_precedence</tt>]
53
+ # Array (defaults to <tt>[:middle, :little]</tt>)
54
+ #
55
+ # By default, Chronic will parse "03/04/2011" as the fourth day
56
+ # of the third month. Alternatively you can tell Chronic to parse
57
+ # this as the third day of the fourth month by altering the
58
+ # <tt>:endian_precedence</tt> to <tt>[:little, :middle]</tt>.
41
59
  def parse(text, specified_options = {})
42
60
  @text = text
43
-
44
- # get options and set defaults if necessary
45
- default_options = {:context => :future,
46
- :now => Chronic.time_class.now,
47
- :guess => true,
48
- :ambiguous_time_range => 6,
49
- :endian_precedence => nil}
50
- options = default_options.merge specified_options
51
-
52
- # handle options that were set to nil
53
- options[:context] = :future unless options[:context]
54
- options[:now] = Chronic.time_class.now unless options[:context]
55
- options[:ambiguous_time_range] = 6 unless options[:ambiguous_time_range]
61
+ options = DEFAULT_OPTIONS.merge specified_options
56
62
 
57
63
  # ensure the specified options are valid
58
- specified_options.keys.each do |key|
59
- default_options.keys.include?(key) || raise(InvalidArgumentException, "#{key} is not a valid option key.")
60
- end
64
+ (specified_options.keys-DEFAULT_OPTIONS.keys).each {|key| raise(InvalidArgumentException, "#{key} is not a valid option key.")}
65
+
61
66
  [:past, :future, :none].include?(options[:context]) || raise(InvalidArgumentException, "Invalid value ':#{options[:context]}' for :context specified. Valid values are :past and :future.")
62
67
 
63
- # store now for later =)
68
+ options[:now] ||= Chronic.time_class.now
64
69
  @now = options[:now]
65
70
 
66
71
  # put the text into a normal format to ease scanning
67
- text = self.pre_normalize(text)
68
-
69
- # get base tokens for each word
70
- @tokens = self.base_tokenize(text)
71
-
72
- # scan the tokens with each token scanner
73
- [Repeater].each do |tokenizer|
74
- @tokens = tokenizer.scan(@tokens, options)
75
- end
76
-
77
- [Grabber, Pointer, Scalar, Ordinal, Separator, TimeZone].each do |tokenizer|
78
- @tokens = tokenizer.scan(@tokens)
79
- end
72
+ text = pre_normalize(text)
80
73
 
81
- # strip any non-tagged tokens
82
- @tokens = @tokens.select { |token| token.tagged? }
74
+ # tokenize words
75
+ @tokens = tokenize(text, options)
83
76
 
84
77
  if Chronic.debug
85
78
  puts "+---------------------------------------------------"
@@ -87,19 +80,12 @@ module Chronic
87
80
  puts "+---------------------------------------------------"
88
81
  end
89
82
 
90
- # do the heavy lifting
91
- begin
92
- span = self.tokens_to_span(@tokens, options)
93
- rescue
94
- raise
95
- return nil
96
- end
83
+ span = tokens_to_span(@tokens, options)
97
84
 
98
- # guess a time within a span if required
99
85
  if options[:guess]
100
- return self.guess(span)
86
+ guess span
101
87
  else
102
- return span
88
+ span
103
89
  end
104
90
  end
105
91
 
@@ -109,10 +95,12 @@ module Chronic
109
95
  # ordinals (third => 3rd)
110
96
  def pre_normalize(text) #:nodoc:
111
97
  normalized_text = text.to_s.downcase
112
- normalized_text = numericize_numbers(normalized_text)
113
98
  normalized_text.gsub!(/['"\.,]/, '')
99
+ normalized_text.gsub!(/\bsecond (of|day|month|hour|minute|second)\b/, '2nd \1')
100
+ normalized_text = numericize_numbers(normalized_text)
114
101
  normalized_text.gsub!(/ \-(\d{4})\b/, ' tzminus\1')
115
102
  normalized_text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
103
+ normalized_text.gsub!(/\b0(\d+:\d+\s*pm?\b)/, '\1')
116
104
  normalized_text.gsub!(/\btoday\b/, 'this day')
117
105
  normalized_text.gsub!(/\btomm?orr?ow\b/, 'next day')
118
106
  normalized_text.gsub!(/\byesterday\b/, 'last day')
@@ -129,7 +117,7 @@ module Chronic
129
117
  normalized_text.gsub!(/\b\d+:?\d*[ap]\b/,'\0m')
130
118
  normalized_text.gsub!(/(\d)([ap]m|oclock)\b/, '\1 \2')
131
119
  normalized_text.gsub!(/\b(hence|after|from)\b/, 'future')
132
- normalized_text = numericize_ordinals(normalized_text)
120
+ normalized_text
133
121
  end
134
122
 
135
123
  # Convert number words to numbers (three => 3)
@@ -137,15 +125,12 @@ module Chronic
137
125
  Numerizer.numerize(text)
138
126
  end
139
127
 
140
- # Convert ordinal words to numeric ordinals (third => 3rd)
141
- def numericize_ordinals(text) #:nodoc:
142
- text
143
- end
144
-
145
- # Split the text on spaces and convert each word into
146
- # a Token
147
- def base_tokenize(text) #:nodoc:
148
- text.split(' ').map { |word| Token.new(word) }
128
+ def tokenize(text, options) #:nodoc:
129
+ tokens = text.split(' ').map { |word| Token.new(word) }
130
+ [Repeater, Grabber, Pointer, Scalar, Ordinal, Separator, TimeZone].each do |tok|
131
+ tokens = tok.scan(tokens, options)
132
+ end
133
+ tokens.delete_if { |token| !token.tagged? }
149
134
  end
150
135
 
151
136
  # Guess a specific time within the given span
@@ -159,83 +144,6 @@ module Chronic
159
144
  end
160
145
  end
161
146
 
162
- class Token #:nodoc:
163
- attr_accessor :word, :tags
164
-
165
- def initialize(word)
166
- @word = word
167
- @tags = []
168
- end
169
-
170
- # Tag this token with the specified tag
171
- def tag(new_tag)
172
- @tags << new_tag
173
- end
174
-
175
- # Remove all tags of the given class
176
- def untag(tag_class)
177
- @tags = @tags.select { |m| !m.kind_of? tag_class }
178
- end
179
-
180
- # Return true if this token has any tags
181
- def tagged?
182
- @tags.size > 0
183
- end
184
-
185
- # Return the Tag that matches the given class
186
- def get_tag(tag_class)
187
- matches = @tags.select { |m| m.kind_of? tag_class }
188
- #matches.size < 2 || raise("Multiple identical tags found")
189
- return matches.first
190
- end
191
-
192
- # Print this Token in a pretty way
193
- def to_s
194
- @word << '(' << @tags.join(', ') << ') '
195
- end
196
- end
197
-
198
- # A Span represents a range of time. Since this class extends
199
- # Range, you can use #begin and #end to get the beginning and
200
- # ending times of the span (they will be of class Time)
201
- class Span < Range
202
- # Returns the width of this span in seconds
203
- def width
204
- (self.end - self.begin).to_i
205
- end
206
-
207
- # Add a number of seconds to this span, returning the
208
- # resulting Span
209
- def +(seconds)
210
- Span.new(self.begin + seconds, self.end + seconds)
211
- end
212
-
213
- # Subtract a number of seconds to this span, returning the
214
- # resulting Span
215
- def -(seconds)
216
- self + -seconds
217
- end
218
-
219
- # Prints this span in a nice fashion
220
- def to_s
221
- '(' << self.begin.to_s << '..' << self.end.to_s << ')'
222
- end
223
- end
224
-
225
- # Tokens are tagged with subclassed instances of this class when
226
- # they match specific criteria
227
- class Tag #:nodoc:
228
- attr_accessor :type
229
-
230
- def initialize(type)
231
- @type = type
232
- end
233
-
234
- def start=(s)
235
- @now = s
236
- end
237
- end
238
-
239
147
  # Internal exception
240
148
  class ChronicPain < Exception #:nodoc:
241
149