chronic 0.1.5 → 0.6.5
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.
- data/.gemtest +0 -0
- data/.gitignore +5 -0
- data/.yardopts +3 -0
- data/HISTORY.md +180 -0
- data/LICENSE +21 -0
- data/README.md +176 -0
- data/Rakefile +39 -32
- data/chronic.gemspec +17 -0
- data/lib/chronic/chronic.rb +294 -208
- data/lib/chronic/grabber.rb +22 -17
- data/lib/chronic/handler.rb +90 -0
- data/lib/chronic/handlers.rb +365 -278
- data/lib/chronic/mini_date.rb +38 -0
- data/lib/chronic/numerizer.rb +121 -0
- data/lib/chronic/ordinal.rb +23 -19
- data/lib/chronic/pointer.rb +20 -17
- data/lib/chronic/repeater.rb +126 -105
- data/lib/chronic/repeaters/repeater_day.rb +49 -43
- data/lib/chronic/repeaters/repeater_day_name.rb +48 -42
- data/lib/chronic/repeaters/repeater_day_portion.rb +81 -80
- data/lib/chronic/repeaters/repeater_fortnight.rb +59 -53
- data/lib/chronic/repeaters/repeater_hour.rb +49 -43
- data/lib/chronic/repeaters/repeater_minute.rb +55 -39
- data/lib/chronic/repeaters/repeater_month.rb +77 -55
- data/lib/chronic/repeaters/repeater_month_name.rb +83 -82
- data/lib/chronic/repeaters/repeater_season.rb +107 -21
- data/lib/chronic/repeaters/repeater_season_name.rb +41 -22
- data/lib/chronic/repeaters/repeater_second.rb +39 -33
- data/lib/chronic/repeaters/repeater_time.rb +117 -103
- data/lib/chronic/repeaters/repeater_week.rb +63 -57
- data/lib/chronic/repeaters/repeater_weekday.rb +85 -0
- data/lib/chronic/repeaters/repeater_weekend.rb +64 -9
- data/lib/chronic/repeaters/repeater_year.rb +70 -51
- data/lib/chronic/scalar.rb +64 -29
- data/lib/chronic/season.rb +37 -0
- data/lib/chronic/separator.rb +50 -38
- data/lib/chronic/span.rb +31 -0
- data/lib/chronic/tag.rb +42 -0
- data/lib/chronic/time_zone.rb +30 -0
- data/lib/chronic/token.rb +45 -0
- data/lib/chronic.rb +86 -22
- data/test/helper.rb +6 -0
- data/test/test_Chronic.rb +118 -20
- data/test/test_DaylightSavings.rb +118 -0
- data/test/test_Handler.rb +36 -42
- data/test/test_MiniDate.rb +32 -0
- data/test/test_Numerizer.rb +72 -0
- data/test/test_RepeaterDayName.rb +15 -16
- data/test/test_RepeaterFortnight.rb +16 -17
- data/test/test_RepeaterHour.rb +22 -19
- data/test/test_RepeaterMinute.rb +34 -0
- data/test/test_RepeaterMonth.rb +20 -17
- data/test/test_RepeaterMonthName.rb +17 -18
- data/test/test_RepeaterSeason.rb +40 -0
- data/test/test_RepeaterTime.rb +20 -22
- data/test/test_RepeaterWeek.rb +16 -17
- data/test/test_RepeaterWeekday.rb +55 -0
- data/test/test_RepeaterWeekend.rb +74 -0
- data/test/test_RepeaterYear.rb +24 -18
- data/test/test_Span.rb +5 -6
- data/test/test_Token.rb +5 -6
- data/test/test_parsing.rb +743 -338
- metadata +81 -54
- data/History.txt +0 -29
- data/Manifest.txt +0 -43
- data/README.txt +0 -149
- data/test/parse_numbers.rb +0 -50
- data/test/suite.rb +0 -9
data/lib/chronic/chronic.rb
CHANGED
|
@@ -1,241 +1,327 @@
|
|
|
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
|
-
|
|
4
|
-
# Parses a string containing a natural language date or time
|
|
5
|
-
# can find a date or time, either a Time or Chronic::Span will be returned
|
|
6
|
-
# (depending on the value of <tt>:guess</tt>). If no date or time can be found,
|
|
7
|
-
# +nil+ will be returned.
|
|
13
|
+
|
|
14
|
+
# Parses a string containing a natural language date or time
|
|
8
15
|
#
|
|
9
|
-
#
|
|
16
|
+
# If the parser can find a date or time, either a Time or Chronic::Span
|
|
17
|
+
# will be returned (depending on the value of `:guess`). If no
|
|
18
|
+
# date or time can be found, `nil` will be returned
|
|
10
19
|
#
|
|
11
|
-
# [
|
|
12
|
-
# <tt>:past</tt> or <tt>:future</tt> (defaults to <tt>:future</tt>)
|
|
20
|
+
# @param [String] text The text to parse
|
|
13
21
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# past
|
|
22
|
+
# @option opts [Symbol] :context (:future)
|
|
23
|
+
# * If your string represents a birthday, you can set `:context` to
|
|
24
|
+
# `:past` and if an ambiguous string is given, it will assume it is
|
|
25
|
+
# in the past. Specify `:future` or omit to set a future context.
|
|
17
26
|
#
|
|
18
|
-
# [
|
|
19
|
-
#
|
|
27
|
+
# @option opts [Object] :now (Time.now)
|
|
28
|
+
# * By setting `:now` to a Time, all computations will be based off of
|
|
29
|
+
# that time instead of `Time.now`. If set to nil, Chronic will use
|
|
30
|
+
# `Time.now`
|
|
20
31
|
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
32
|
+
# @option opts [Boolean] :guess (true)
|
|
33
|
+
# * By default, the parser will guess a single point in time for the
|
|
34
|
+
# given date or time. If you'd rather have the entire time span
|
|
35
|
+
# returned, set `:guess` to `false` and a {Chronic::Span} will
|
|
36
|
+
# be returned
|
|
23
37
|
#
|
|
24
|
-
# [
|
|
25
|
-
#
|
|
38
|
+
# @option opts [Integer] :ambiguous_time_range (6)
|
|
39
|
+
# * If an Integer is given, ambiguous times (like 5:00) will be
|
|
40
|
+
# assumed to be within the range of that time in the AM to that time
|
|
41
|
+
# in the PM. For example, if you set it to `7`, then the parser
|
|
42
|
+
# will look for the time between 7am and 7pm. In the case of 5:00, it
|
|
43
|
+
# would assume that means 5:00pm. If `:none` is given, no
|
|
44
|
+
# assumption will be made, and the first matching instance of that
|
|
45
|
+
# time will be used
|
|
26
46
|
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
# [
|
|
32
|
-
# Integer or <tt>:none</tt> (defaults to <tt>6</tt> (6am-6pm))
|
|
47
|
+
# @option opts [Array] :endian_precedence ([:middle, :little])
|
|
48
|
+
# * By default, Chronic will parse "03/04/2011" as the fourth day
|
|
49
|
+
# of the third month. Alternatively you can tell Chronic to parse
|
|
50
|
+
# this as the third day of the fourth month by altering the
|
|
51
|
+
# `:endian_precedence` to `[:little, :middle]`
|
|
33
52
|
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
:guess => true,
|
|
46
|
-
:ambiguous_time_range => 6}
|
|
47
|
-
options = default_options.merge specified_options
|
|
48
|
-
|
|
53
|
+
# @option opts [Integer] :ambiguous_year_future_bias (50)
|
|
54
|
+
# * When parsing two digit years (ie 79) unlike Rubys Time class,
|
|
55
|
+
# Chronic will attempt to assume the full year using this figure.
|
|
56
|
+
# Chronic will look x amount of years into the future and past. If
|
|
57
|
+
# the two digit year is `now + x years` it's assumed to be the
|
|
58
|
+
# future, `now - x years` is assumed to be the past
|
|
59
|
+
#
|
|
60
|
+
# @return [Time, Chronic::Span, nil]
|
|
61
|
+
def parse(text, opts={})
|
|
62
|
+
options = DEFAULT_OPTIONS.merge opts
|
|
63
|
+
|
|
49
64
|
# ensure the specified options are valid
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
#
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
# scan the tokens with each token scanner
|
|
65
|
-
[Repeater].each do |tokenizer|
|
|
66
|
-
@tokens = tokenizer.scan(@tokens, options)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
[Grabber, Pointer, Scalar, Ordinal, Separator].each do |tokenizer|
|
|
70
|
-
@tokens = tokenizer.scan(@tokens)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# strip any non-tagged tokens
|
|
74
|
-
@tokens = @tokens.select { |token| token.tagged? }
|
|
75
|
-
|
|
65
|
+
(opts.keys - DEFAULT_OPTIONS.keys).each do |key|
|
|
66
|
+
raise ArgumentError, "#{key} is not a valid option key."
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
unless [:past, :future, :none].include?(options[:context])
|
|
70
|
+
raise ArgumentError, "Invalid context, :past/:future only"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
options[:text] = text
|
|
74
|
+
Chronic.now = options[:now] || Chronic.time_class.now
|
|
75
|
+
|
|
76
|
+
# tokenize words
|
|
77
|
+
tokens = tokenize(text, options)
|
|
78
|
+
|
|
76
79
|
if Chronic.debug
|
|
77
|
-
puts "
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
span = self.tokens_to_span(@tokens, options)
|
|
85
|
-
rescue
|
|
86
|
-
raise
|
|
87
|
-
return nil
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# guess a time within a span if required
|
|
91
|
-
if options[:guess]
|
|
92
|
-
return self.guess(span)
|
|
93
|
-
else
|
|
94
|
-
return span
|
|
80
|
+
puts "+#{'-' * 51}\n| #{tokens}\n+#{'-' * 51}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
span = tokens_to_span(tokens, options)
|
|
84
|
+
|
|
85
|
+
if span
|
|
86
|
+
options[:guess] ? guess(span) : span
|
|
95
87
|
end
|
|
96
88
|
end
|
|
97
|
-
|
|
98
|
-
# Clean up the specified
|
|
99
|
-
#
|
|
100
|
-
#
|
|
89
|
+
|
|
90
|
+
# Clean up the specified text ready for parsing
|
|
91
|
+
#
|
|
92
|
+
# Clean up the string by stripping unwanted characters, converting
|
|
93
|
+
# idioms to their canonical form, converting number words to numbers
|
|
94
|
+
# (three => 3), and converting ordinal words to numeric
|
|
101
95
|
# ordinals (third => 3rd)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
text
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
96
|
+
#
|
|
97
|
+
# @example
|
|
98
|
+
# Chronic.pre_normalize('first day in May')
|
|
99
|
+
# #=> "1st day in may"
|
|
100
|
+
#
|
|
101
|
+
# Chronic.pre_normalize('tomorrow after noon')
|
|
102
|
+
# #=> "next day future 12:00"
|
|
103
|
+
#
|
|
104
|
+
# Chronic.pre_normalize('one hundred and thirty six days from now')
|
|
105
|
+
# #=> "136 days future this second"
|
|
106
|
+
#
|
|
107
|
+
# @param [String] text The string to normalize
|
|
108
|
+
# @return [String] A new string ready for Chronic to parse
|
|
109
|
+
def pre_normalize(text)
|
|
110
|
+
text = text.to_s.downcase
|
|
111
|
+
text.gsub!(/['"\.]/, '')
|
|
112
|
+
text.gsub!(/,/, ' ')
|
|
113
|
+
text.gsub!(/\bsecond (of|day|month|hour|minute|second)\b/, '2nd \1')
|
|
114
|
+
text = Numerizer.numerize(text)
|
|
115
|
+
text.gsub!(/ \-(\d{4})\b/, ' tzminus\1')
|
|
116
|
+
text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
|
|
117
|
+
text.gsub!(/(?:^|\s)0(\d+:\d+\s*pm?\b)/, '\1')
|
|
118
|
+
text.gsub!(/\btoday\b/, 'this day')
|
|
119
|
+
text.gsub!(/\btomm?orr?ow\b/, 'next day')
|
|
120
|
+
text.gsub!(/\byesterday\b/, 'last day')
|
|
121
|
+
text.gsub!(/\bnoon\b/, '12:00pm')
|
|
122
|
+
text.gsub!(/\bmidnight\b/, '24:00')
|
|
123
|
+
text.gsub!(/\bnow\b/, 'this second')
|
|
124
|
+
text.gsub!(/\b(?:ago|before(?: now)?)\b/, 'past')
|
|
125
|
+
text.gsub!(/\bthis (?:last|past)\b/, 'last')
|
|
126
|
+
text.gsub!(/\b(?:in|during) the (morning)\b/, '\1')
|
|
127
|
+
text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1')
|
|
128
|
+
text.gsub!(/\btonight\b/, 'this night')
|
|
129
|
+
text.gsub!(/\b\d+:?\d*[ap]\b/,'\0m')
|
|
130
|
+
text.gsub!(/(\d)([ap]m|oclock)\b/, '\1 \2')
|
|
131
|
+
text.gsub!(/\b(hence|after|from)\b/, 'future')
|
|
134
132
|
text
|
|
135
133
|
end
|
|
136
|
-
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
|
|
140
|
-
|
|
134
|
+
|
|
135
|
+
# Convert number words to numbers (three => 3, fourth => 4th)
|
|
136
|
+
#
|
|
137
|
+
# @see Numerizer.numerize
|
|
138
|
+
# @param [String] text The string to convert
|
|
139
|
+
# @return [String] A new string with words converted to numbers
|
|
140
|
+
def numericize_numbers(text)
|
|
141
|
+
warn "Chronic.numericize_numbers will be deprecated in version 0.7.0. Please use Chronic::Numerizer.numerize instead"
|
|
142
|
+
Numerizer.numerize(text)
|
|
141
143
|
end
|
|
142
|
-
|
|
144
|
+
|
|
143
145
|
# Guess a specific time within the given span
|
|
144
|
-
|
|
145
|
-
|
|
146
|
+
#
|
|
147
|
+
# @param [Span] span
|
|
148
|
+
# @return [Time]
|
|
149
|
+
def guess(span)
|
|
146
150
|
if span.width > 1
|
|
147
151
|
span.begin + (span.width / 2)
|
|
148
152
|
else
|
|
149
153
|
span.begin
|
|
150
154
|
end
|
|
151
155
|
end
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
156
|
+
|
|
157
|
+
# List of {Handler} definitions. See {parse} for a list of options this
|
|
158
|
+
# method accepts
|
|
159
|
+
#
|
|
160
|
+
# @see parse
|
|
161
|
+
# @return [Hash] A Hash of Handler definitions
|
|
162
|
+
def definitions(options={})
|
|
163
|
+
options[:endian_precedence] ||= [:middle, :little]
|
|
164
|
+
|
|
165
|
+
@definitions ||= {
|
|
166
|
+
:time => [
|
|
167
|
+
Handler.new([:repeater_time, :repeater_day_portion?], nil)
|
|
168
|
+
],
|
|
169
|
+
|
|
170
|
+
:date => [
|
|
171
|
+
Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :repeater_time, :separator_slash_or_dash?, :time_zone, :scalar_year], :handle_rdn_rmn_sd_t_tz_sy),
|
|
172
|
+
Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day], :handle_rdn_rmn_sd),
|
|
173
|
+
Handler.new([:repeater_day_name, :repeater_month_name, :ordinal_day], :handle_rdn_rmn_od),
|
|
174
|
+
Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :repeater_time, :time_zone], :handle_sy_sm_sd_t_tz),
|
|
175
|
+
Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
|
|
176
|
+
Handler.new([:repeater_month_name, :ordinal_day, :scalar_year], :handle_rmn_od_sy),
|
|
177
|
+
Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
|
|
178
|
+
Handler.new([:repeater_month_name, :ordinal_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_od_sy),
|
|
179
|
+
Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
|
|
180
|
+
Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :scalar_day], :handle_rmn_sd_on),
|
|
181
|
+
Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
|
|
182
|
+
Handler.new([:ordinal_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_od_rmn_sy),
|
|
183
|
+
Handler.new([:ordinal_day, :repeater_month_name, :separator_at?, 'time?'], :handle_od_rmn),
|
|
184
|
+
Handler.new([:scalar_year, :repeater_month_name, :ordinal_day], :handle_sy_rmn_od),
|
|
185
|
+
Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :ordinal_day], :handle_rmn_od_on),
|
|
186
|
+
Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
|
|
187
|
+
Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
|
|
188
|
+
Handler.new([:scalar_day, :repeater_month_name, :separator_at?, 'time?'], :handle_sd_rmn),
|
|
189
|
+
Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
|
|
190
|
+
Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)
|
|
191
|
+
],
|
|
192
|
+
|
|
193
|
+
# tonight at 7pm
|
|
194
|
+
:anchor => [
|
|
195
|
+
Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
|
|
196
|
+
Handler.new([:grabber?, :repeater, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
|
|
197
|
+
Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)
|
|
198
|
+
],
|
|
199
|
+
|
|
200
|
+
# 3 weeks from now, in 2 months
|
|
201
|
+
:arrow => [
|
|
202
|
+
Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p),
|
|
203
|
+
Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r),
|
|
204
|
+
Handler.new([:scalar, :repeater, :pointer, 'anchor'], :handle_s_r_p_a)
|
|
205
|
+
],
|
|
206
|
+
|
|
207
|
+
# 3rd week in march
|
|
208
|
+
:narrow => [
|
|
209
|
+
Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r),
|
|
210
|
+
Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)
|
|
211
|
+
]
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
endians = [
|
|
215
|
+
Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
|
|
216
|
+
Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy)
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
case endian = Array(options[:endian_precedence]).first
|
|
220
|
+
when :little
|
|
221
|
+
@definitions[:endian] = endians.reverse
|
|
222
|
+
when :middle
|
|
223
|
+
@definitions[:endian] = endians
|
|
224
|
+
else
|
|
225
|
+
raise ArgumentError, "Unknown endian option '#{endian}'"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
@definitions
|
|
209
229
|
end
|
|
210
|
-
|
|
211
|
-
#
|
|
212
|
-
|
|
213
|
-
|
|
230
|
+
|
|
231
|
+
# Construct a time Object
|
|
232
|
+
#
|
|
233
|
+
# @return [Time]
|
|
234
|
+
def construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0)
|
|
235
|
+
if second >= 60
|
|
236
|
+
minute += second / 60
|
|
237
|
+
second = second % 60
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
if minute >= 60
|
|
241
|
+
hour += minute / 60
|
|
242
|
+
minute = minute % 60
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
if hour >= 24
|
|
246
|
+
day += hour / 24
|
|
247
|
+
hour = hour % 24
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# determine if there is a day overflow. this is complicated by our crappy calendar
|
|
251
|
+
# system (non-constant number of days per month)
|
|
252
|
+
day <= 56 || raise("day must be no more than 56 (makes month resolution easier)")
|
|
253
|
+
if day > 28
|
|
254
|
+
# no month ever has fewer than 28 days, so only do this if necessary
|
|
255
|
+
leap_year_month_days = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
256
|
+
common_year_month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
257
|
+
days_this_month = Date.leap?(year) ? leap_year_month_days[month - 1] : common_year_month_days[month - 1]
|
|
258
|
+
if day > days_this_month
|
|
259
|
+
month += day / days_this_month
|
|
260
|
+
day = day % days_this_month
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
if month > 12
|
|
265
|
+
if month % 12 == 0
|
|
266
|
+
year += (month - 12) / 12
|
|
267
|
+
month = 12
|
|
268
|
+
else
|
|
269
|
+
year += month / 12
|
|
270
|
+
month = month % 12
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
Chronic.time_class.local(year, month, day, hour, minute, second)
|
|
214
275
|
end
|
|
215
|
-
end
|
|
216
276
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
277
|
+
private
|
|
278
|
+
|
|
279
|
+
def tokenize(text, options)
|
|
280
|
+
text = pre_normalize(text)
|
|
281
|
+
tokens = text.split(' ').map { |word| Token.new(word) }
|
|
282
|
+
[Repeater, Grabber, Pointer, Scalar, Ordinal, Separator, TimeZone].each do |tok|
|
|
283
|
+
tok.scan(tokens, options)
|
|
284
|
+
end
|
|
285
|
+
tokens.select { |token| token.tagged? }
|
|
224
286
|
end
|
|
225
|
-
|
|
226
|
-
def
|
|
227
|
-
|
|
287
|
+
|
|
288
|
+
def tokens_to_span(tokens, options)
|
|
289
|
+
definitions = definitions(options)
|
|
290
|
+
|
|
291
|
+
(definitions[:endian] + definitions[:date]).each do |handler|
|
|
292
|
+
if handler.match(tokens, definitions)
|
|
293
|
+
good_tokens = tokens.select { |o| !o.get_tag Separator }
|
|
294
|
+
return handler.invoke(:date, good_tokens, options)
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
definitions[:anchor].each do |handler|
|
|
299
|
+
if handler.match(tokens, definitions)
|
|
300
|
+
good_tokens = tokens.select { |o| !o.get_tag Separator }
|
|
301
|
+
return handler.invoke(:anchor, good_tokens, options)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
definitions[:arrow].each do |handler|
|
|
306
|
+
if handler.match(tokens, definitions)
|
|
307
|
+
good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlashOrDash) || o.get_tag(SeparatorComma) }
|
|
308
|
+
return handler.invoke(:arrow, good_tokens, options)
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
definitions[:narrow].each do |handler|
|
|
313
|
+
if handler.match(tokens, definitions)
|
|
314
|
+
return handler.invoke(:narrow, tokens, options)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
puts "-none" if Chronic.debug
|
|
319
|
+
return nil
|
|
228
320
|
end
|
|
321
|
+
|
|
229
322
|
end
|
|
230
|
-
|
|
323
|
+
|
|
231
324
|
# Internal exception
|
|
232
|
-
class ChronicPain < Exception
|
|
233
|
-
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
# This exception is raised if an invalid argument is provided to
|
|
237
|
-
# any of Chronic's methods
|
|
238
|
-
class InvalidArgumentException < Exception
|
|
239
|
-
|
|
325
|
+
class ChronicPain < Exception
|
|
240
326
|
end
|
|
241
|
-
end
|
|
327
|
+
end
|
data/lib/chronic/grabber.rb
CHANGED
|
@@ -1,26 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
module Chronic
|
|
2
|
+
class Grabber < Tag
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
# Scan an Array of {Token}s and apply any necessary Grabber tags to
|
|
5
|
+
# each token
|
|
6
|
+
#
|
|
7
|
+
# @param [Array<Token>] tokens Array of tokens to scan
|
|
8
|
+
# @param [Hash] options Options specified in {Chronic.parse}
|
|
9
|
+
# @return [Array] list of tokens
|
|
10
|
+
def self.scan(tokens, options)
|
|
11
|
+
tokens.each do |token|
|
|
12
|
+
if t = scan_for_all(token) then token.tag(t); next end
|
|
7
13
|
end
|
|
8
|
-
tokens
|
|
9
14
|
end
|
|
10
|
-
|
|
15
|
+
|
|
16
|
+
# @param [Token] token
|
|
17
|
+
# @return [Grabber, nil]
|
|
11
18
|
def self.scan_for_all(token)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return nil
|
|
19
|
+
scan_for token, self,
|
|
20
|
+
{
|
|
21
|
+
/last/ => :last,
|
|
22
|
+
/this/ => :this,
|
|
23
|
+
/next/ => :next
|
|
24
|
+
}
|
|
19
25
|
end
|
|
20
|
-
|
|
26
|
+
|
|
21
27
|
def to_s
|
|
22
28
|
'grabber-' << @type.to_s
|
|
23
29
|
end
|
|
24
30
|
end
|
|
25
|
-
|
|
26
|
-
#end
|
|
31
|
+
end
|