ice_cube_english 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +51 -0
- data/lib/ice_cube/english.rb +2 -0
- data/lib/ice_cube/english/grammar_helper.rb +107 -0
- data/lib/ice_cube/english/rule_extension.rb +36 -0
- data/lib/ice_cube/english/rule_grammar.treetop +454 -0
- data/lib/ice_cube/english/schedule_extension.rb +44 -0
- data/lib/ice_cube/english/schedule_grammar.treetop +26 -0
- data/lib/ice_cube/english/version.rb +5 -0
- data/lib/ice_cube_english.rb +1 -0
- data/test/test_helper.rb +4 -0
- metadata +108 -0
data/README.rdoc
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
== ice_cube_english - easy date expansion in English
|
2
|
+
|
3
|
+
gem install ice_cube_english
|
4
|
+
|
5
|
+
ice_cube_english provides english-language date and time parsing for ice_cube.
|
6
|
+
|
7
|
+
Imagine you want:
|
8
|
+
|
9
|
+
Every friday the 13th that falls in October
|
10
|
+
|
11
|
+
You would write:
|
12
|
+
|
13
|
+
schedule.add_recurrence_rule "Every friday the 13th that falls in October"
|
14
|
+
|
15
|
+
---
|
16
|
+
|
17
|
+
===Contributors
|
18
|
+
|
19
|
+
* Dwayne Litzenberger - dlitz@patientway.com
|
20
|
+
|
21
|
+
---
|
22
|
+
|
23
|
+
===Issues?
|
24
|
+
|
25
|
+
Lighthouse: https://github.com/dlitz/ice_cube_english/issues
|
26
|
+
|
27
|
+
---
|
28
|
+
|
29
|
+
===License
|
30
|
+
|
31
|
+
(The MIT License)
|
32
|
+
|
33
|
+
Copyright © 2011 Infonium Inc.
|
34
|
+
|
35
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
36
|
+
this software and associated documentation files (the ‘Software’), to deal in
|
37
|
+
the Software without restriction, including without limitation the rights to
|
38
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
39
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
40
|
+
so, subject to the following conditions:
|
41
|
+
|
42
|
+
The above copyright notice and this permission notice shall be included in all
|
43
|
+
copies or substantial portions of the Software.
|
44
|
+
|
45
|
+
THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
46
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
47
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
48
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
49
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
50
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
51
|
+
SOFTWARE.
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module IceCube
|
2
|
+
module English
|
3
|
+
module GrammarHelper
|
4
|
+
class <<self
|
5
|
+
def deep_merge!(result, hash)
|
6
|
+
hash.each_pair do |k,v|
|
7
|
+
case v
|
8
|
+
when Hash
|
9
|
+
if result.include?(k)
|
10
|
+
raise ArgumentError("type mismatch: #{k.inspect}: #{v.class.name} -> #{result[k].class.name}") unless result[k].is_a?(Hash)
|
11
|
+
deep_merge!(result[k], v)
|
12
|
+
else
|
13
|
+
result[k] = {}
|
14
|
+
deep_merge!(result[k], v)
|
15
|
+
end
|
16
|
+
when Array
|
17
|
+
if result.include?(k)
|
18
|
+
raise ArgumentError("type mismatch: #{k.inspect}: #{v.class.name} -> #{result[k].class.name}") unless result[k].is_a?(Array)
|
19
|
+
result[k] += v
|
20
|
+
else
|
21
|
+
result[k] = []
|
22
|
+
result[k] += v
|
23
|
+
end
|
24
|
+
else
|
25
|
+
result[k] = v
|
26
|
+
end
|
27
|
+
end
|
28
|
+
result
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
RULE_TYPES = {
|
33
|
+
"secondly" => "IceCube::SecondlyRule",
|
34
|
+
"seconds" => "IceCube::SecondlyRule",
|
35
|
+
"second" => "IceCube::SecondlyRule",
|
36
|
+
"minutely" => "IceCube::MinutelyRule",
|
37
|
+
"minutes" => "IceCube::MinutelyRule",
|
38
|
+
"minute" => "IceCube::MinutelyRule",
|
39
|
+
"hourly" => "IceCube::HourlyRule",
|
40
|
+
"hours" => "IceCube::HourlyRule",
|
41
|
+
"hour" => "IceCube::HourlyRule",
|
42
|
+
"daily" => "IceCube::DailyRule",
|
43
|
+
"days" => "IceCube::DailyRule",
|
44
|
+
"day" => "IceCube::DailyRule",
|
45
|
+
"weekly" => "IceCube::WeeklyRule",
|
46
|
+
"weeks" => "IceCube::WeeklyRule",
|
47
|
+
"week" => "IceCube::WeeklyRule",
|
48
|
+
"monthly" => "IceCube::MonthlyRule",
|
49
|
+
"months" => "IceCube::MonthlyRule",
|
50
|
+
"month" => "IceCube::MonthlyRule",
|
51
|
+
"annually" => "IceCube::YearlyRule",
|
52
|
+
"yearly" => "IceCube::YearlyRule",
|
53
|
+
"years" => "IceCube::YearlyRule",
|
54
|
+
"year" => "IceCube::YearlyRule",
|
55
|
+
}
|
56
|
+
|
57
|
+
DAYS = {
|
58
|
+
"sundays" => 0,
|
59
|
+
"sunday" => 0,
|
60
|
+
"mondays" => 1,
|
61
|
+
"monday" => 1,
|
62
|
+
"tuesdays" => 2,
|
63
|
+
"tuesday" => 2,
|
64
|
+
"wednesdays" => 3,
|
65
|
+
"wednesday" => 3,
|
66
|
+
"thursdays" => 4,
|
67
|
+
"thursday" => 4,
|
68
|
+
"fridays" => 5,
|
69
|
+
"friday" => 5,
|
70
|
+
"saturdays" => 6,
|
71
|
+
"saturday" => 6,
|
72
|
+
}
|
73
|
+
|
74
|
+
DAYS_OF_WEEK = {
|
75
|
+
"sundays" => :sunday,
|
76
|
+
"sunday" => :sunday,
|
77
|
+
"mondays" => :monday,
|
78
|
+
"monday" => :monday,
|
79
|
+
"tuesdays" => :tuesday,
|
80
|
+
"tuesday" => :tuesday,
|
81
|
+
"wednesdays" => :wednesday,
|
82
|
+
"wednesday" => :wednesday,
|
83
|
+
"thursdays" => :thursday,
|
84
|
+
"thursday" => :thursday,
|
85
|
+
"fridays" => :friday,
|
86
|
+
"friday" => :friday,
|
87
|
+
"saturdays" => :saturday,
|
88
|
+
"saturday" => :saturday,
|
89
|
+
}
|
90
|
+
|
91
|
+
MONTHS_OF_YEAR = {
|
92
|
+
"january" => :january,
|
93
|
+
"february" => :february,
|
94
|
+
"march" => :march,
|
95
|
+
"april" => :april,
|
96
|
+
"may" => :may,
|
97
|
+
"june" => :june,
|
98
|
+
"july" => :july,
|
99
|
+
"august" => :august,
|
100
|
+
"september" => :september,
|
101
|
+
"october" => :october,
|
102
|
+
"november" => :november,
|
103
|
+
"december" => :december,
|
104
|
+
}
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'treetop'
|
2
|
+
require 'ice_cube'
|
3
|
+
require 'ice_cube/english/schedule_grammar'
|
4
|
+
require 'ice_cube/english/rule_grammar'
|
5
|
+
|
6
|
+
module IceCube
|
7
|
+
module English
|
8
|
+
module RuleExtension
|
9
|
+
def self.included(base)
|
10
|
+
base.extend(ClassMethods)
|
11
|
+
end
|
12
|
+
module ClassMethods
|
13
|
+
# Return a new IceCube rule corresponding to the specified english string.
|
14
|
+
#
|
15
|
+
# If the :multiple option is set, return an Array of rules.
|
16
|
+
def from_english(string, options={})
|
17
|
+
if options[:multiple]
|
18
|
+
grammar = ::IceCube::English::ScheduleGrammarParser.new
|
19
|
+
parse_tree = grammar.parse(string.downcase)
|
20
|
+
raise ArgumentError.new(grammar.failure_reason) unless parse_tree
|
21
|
+
parse_tree.attribute_hashes.map{ |h| from_hash(h) }
|
22
|
+
else
|
23
|
+
grammar = ::IceCube::English::RuleGrammarParser.new
|
24
|
+
parse_tree = grammar.parse(string.downcase)
|
25
|
+
raise ArgumentError.new(grammar.failure_reason) unless parse_tree
|
26
|
+
from_hash(parse_tree.attributes)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class ::IceCube::Rule # reopen
|
35
|
+
include ::IceCube::English::RuleExtension
|
36
|
+
end
|
@@ -0,0 +1,454 @@
|
|
1
|
+
require 'ice_cube/english/grammar_helper'
|
2
|
+
|
3
|
+
module IceCube
|
4
|
+
module English
|
5
|
+
grammar RuleGrammar
|
6
|
+
rule recurrence_rule
|
7
|
+
first_clause:recurrence_clause rest_clauses:( WS+ recurrence_clause )* {
|
8
|
+
def clauses
|
9
|
+
[first_clause] + rest_clauses
|
10
|
+
end
|
11
|
+
|
12
|
+
def rest_clauses
|
13
|
+
super.elements.map{|c| c.recurrence_clause}
|
14
|
+
end
|
15
|
+
|
16
|
+
def attributes
|
17
|
+
clause_attributes = []
|
18
|
+
clauses.each do |c|
|
19
|
+
clause_attributes << c.attributes
|
20
|
+
end
|
21
|
+
|
22
|
+
result = {:until => nil, :count => nil, :interval => 1, :validations => {}}
|
23
|
+
clause_attributes.each do |attributes|
|
24
|
+
GrammarHelper.deep_merge!(result, attributes)
|
25
|
+
end
|
26
|
+
|
27
|
+
# If :minute_of_hour is specified, make sure :second_of_minute is also specified.
|
28
|
+
if result[:validations][:minute_of_hour] and !result[:validations][:second_of_minute]
|
29
|
+
result[:validations][:second_of_minute] = [0]
|
30
|
+
end
|
31
|
+
|
32
|
+
# Make sure :rule_type is specified
|
33
|
+
unless result[:rule_type]
|
34
|
+
# XXX - Is this correct? Is it necessary to differentiate between these cases?
|
35
|
+
v = result[:validations]
|
36
|
+
result[:rule_type] = "IceCube::SecondlyRule"
|
37
|
+
result[:rule_type] = "IceCube::MinutelyRule" if (v[:second_of_minute] && v[:second_of_minute].length == 1) || v[:minute_of_hour]
|
38
|
+
result[:rule_type] = "IceCube::HourlyRule" if (v[:minute_of_hour] && v[:minute_of_hour].length == 1) || v[:hour_of_day]
|
39
|
+
result[:rule_type] = "IceCube::DailyRule" if v[:hour_of_day] && v[:hour_of_day].length == 1 || v[:day] || v[:day_of_week]
|
40
|
+
result[:rule_type] = "IceCube::WeeklyRule" if (v[:day] ? v[:day].length : 0) + (v[:day_of_week] ? v[:day_of_week].length : 0) == 1 || v[:day_of_month]
|
41
|
+
result[:rule_type] = "IceCube::MonthlyRule" if (v[:day_of_week] && v[:day_of_week].values.map{|vv| vv.length}.inject{|a,b| a+b} == 1) || (v[:day_of_month] && v[:day_of_month].length == 1)
|
42
|
+
end
|
43
|
+
result
|
44
|
+
end
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
rule recurrence_clause
|
49
|
+
frequency / for_n_occurrences / on_the_nth_x_of_the_y / the_nth / at_minutes_past_the_hour / at_time_of_day / of_month
|
50
|
+
end
|
51
|
+
|
52
|
+
# e.g. "daily", "every 2 days", "every 2nd day", "every monday", "every 2 mondays"
|
53
|
+
rule frequency
|
54
|
+
direct_frequency / every_frequency
|
55
|
+
end
|
56
|
+
|
57
|
+
rule direct_frequency
|
58
|
+
( "secondly" / "minutely" / "hourly" / "daily" / "weekly" / "monthly" / "yearly" / "annually" ) {
|
59
|
+
def attributes
|
60
|
+
{ :rule_type => GrammarHelper::RULE_TYPES[text_value] }
|
61
|
+
end
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
# e.g. "every day", "every 2nd day", "every 2nd monday", "every monday", "every 2 mondays"
|
66
|
+
rule every_frequency
|
67
|
+
"every" WS+
|
68
|
+
n:( m:positive_integer_or_ordinal WS+ )?
|
69
|
+
unit:( "seconds" / "minutes" / "hours" / "days" / "weeks" / "months" / "years" /
|
70
|
+
"second" / "minute" / "hour" / "day" / "week" / "month" / "year" /
|
71
|
+
days_of_week )
|
72
|
+
{
|
73
|
+
def attributes
|
74
|
+
result = {}
|
75
|
+
result[:interval] = n.m.number unless n.empty?
|
76
|
+
if unit.respond_to?(:day_validations) # days_of_week
|
77
|
+
result[:validations] = { :day => unit.day_validations }
|
78
|
+
else
|
79
|
+
result[:rule_type] = GrammarHelper::RULE_TYPES[unit.text_value]
|
80
|
+
end
|
81
|
+
result
|
82
|
+
end
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
rule days_of_week
|
87
|
+
first_day:day_of_week rest_days:( comma_joiner day_of_week )*
|
88
|
+
{
|
89
|
+
def days
|
90
|
+
[first_day] + rest_days
|
91
|
+
end
|
92
|
+
|
93
|
+
def rest_days
|
94
|
+
super.elements.map{|d| d.day_of_week}
|
95
|
+
end
|
96
|
+
|
97
|
+
def day_validations
|
98
|
+
days.map{|day_of_week| day_of_week.day_validation}
|
99
|
+
end
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
rule day_of_week
|
104
|
+
( "sundays" / "mondays" / "tuesdays" / "wednesdays" / "thursdays" / "fridays" / "saturdays" /
|
105
|
+
"sunday" / "monday" / "tuesday" / "wednesday" / "thursday" / "friday" / "saturday" )
|
106
|
+
{
|
107
|
+
def day_validation
|
108
|
+
GrammarHelper::DAYS[text_value]
|
109
|
+
end
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
# e.g. "every second", "every 1 second", "every 2 seconds"
|
114
|
+
rule number_frequency
|
115
|
+
n:( positive_integer WS+ )? time_unit {
|
116
|
+
def attributes
|
117
|
+
{ :rule_type => GrammarHelper::RULE_TYPES[time_unit.text_value], :interval => n.text_value.to_i }
|
118
|
+
end
|
119
|
+
}
|
120
|
+
end
|
121
|
+
|
122
|
+
rule for_n_occurrences
|
123
|
+
"for" WS+ positive_integer WS+ ( "occurrences" / "occurrence" ) {
|
124
|
+
def count
|
125
|
+
positive_integer.number
|
126
|
+
end
|
127
|
+
|
128
|
+
def attributes
|
129
|
+
{ :count => count }
|
130
|
+
end
|
131
|
+
}
|
132
|
+
end
|
133
|
+
|
134
|
+
rule on_the_nth_x_of_the_y
|
135
|
+
"on" WS+ "the" WS+ ordinals WS+ ("seconds" / "second") WS+ "of" WS+ "the" WS+ "minute" {
|
136
|
+
def attributes
|
137
|
+
{ :validations => {:second_of_minute => ordinals.numbers} }
|
138
|
+
end
|
139
|
+
}
|
140
|
+
/
|
141
|
+
"on" WS+ "the" WS+ ordinals WS+ ("minutes" / "minute") WS+ "of" WS+ "the" WS+ "hour" {
|
142
|
+
def attributes
|
143
|
+
{ :validations => {:minute_of_hour => ordinals.numbers} }
|
144
|
+
end
|
145
|
+
}
|
146
|
+
/
|
147
|
+
"on" WS+ "the" WS+ lordinals WS+ ("days" / "day") WS+ "of" WS+ "the" WS+ "month" {
|
148
|
+
def attributes
|
149
|
+
{ :validations => {:day_of_month => lordinals.numbers} }
|
150
|
+
end
|
151
|
+
}
|
152
|
+
/
|
153
|
+
"on" WS+ "the" WS+ lordinals WS+ days_of_week ( WS+ "of" WS+ "the" WS+ "month" )? {
|
154
|
+
def attributes
|
155
|
+
result = {}
|
156
|
+
days_of_week.days.each do |day_of_week|
|
157
|
+
GrammarHelper.deep_merge! result, { :validations => { :day_of_week => { GrammarHelper::DAYS_OF_WEEK[day_of_week.text_value] => lordinals.numbers } } }
|
158
|
+
end
|
159
|
+
result
|
160
|
+
end
|
161
|
+
}
|
162
|
+
end
|
163
|
+
|
164
|
+
# e.g. "the 13th"
|
165
|
+
rule the_nth
|
166
|
+
"the" WS+ ordinal {
|
167
|
+
def attributes
|
168
|
+
{ :validations => {:day_of_month => [ordinal.number]} }
|
169
|
+
end
|
170
|
+
}
|
171
|
+
end
|
172
|
+
|
173
|
+
# e.g. "that falls in october", "of october", "in October"
|
174
|
+
rule of_month
|
175
|
+
( "of" / "in" / "that" WS+ "falls" WS+ "in" ) WS+ month_names {
|
176
|
+
def attributes
|
177
|
+
{ :validations => {:month_of_year => month_names.months_of_year} }
|
178
|
+
end
|
179
|
+
}
|
180
|
+
end
|
181
|
+
|
182
|
+
rule month_names
|
183
|
+
first_month_name:month_name rest_month_names:(comma_joiner month_name)* {
|
184
|
+
def month_names
|
185
|
+
[first_month_name] + rest_month_names
|
186
|
+
end
|
187
|
+
|
188
|
+
def rest_month_names
|
189
|
+
super.elements.map{|e| e.month_name}
|
190
|
+
end
|
191
|
+
|
192
|
+
def months_of_year
|
193
|
+
month_names.map{|m| GrammarHelper::MONTHS_OF_YEAR[m.text_value]}
|
194
|
+
end
|
195
|
+
}
|
196
|
+
end
|
197
|
+
|
198
|
+
rule month_name
|
199
|
+
( "january" / "february" / "march" / "april" / "may" / "june" /
|
200
|
+
"july" / "august" / "september" / "october" / "november" / "december" )
|
201
|
+
end
|
202
|
+
|
203
|
+
# e.g. "at 15 minutes past the hour"
|
204
|
+
rule at_minutes_past_the_hour
|
205
|
+
"at" WS+ int60s WS+ "minutes" WS+ "past" WS+ "the" WS+ "hour" {
|
206
|
+
def attributes
|
207
|
+
{ :validations => {:minute_of_hour => int60s.numbers} }
|
208
|
+
end
|
209
|
+
}
|
210
|
+
end
|
211
|
+
|
212
|
+
rule time_unit
|
213
|
+
"seconds" / "minutes" / "hours" / "days" / "weeks" / "months" / "years" /
|
214
|
+
"second" / "minute" / "hour" / "day" / "week" / "month" / "year"
|
215
|
+
end
|
216
|
+
|
217
|
+
rule ordinal
|
218
|
+
( "first" / positive_integer ( "st" / "nd" / "rd" / "th" ) ) {
|
219
|
+
def number
|
220
|
+
if text_value == "first"
|
221
|
+
1
|
222
|
+
else
|
223
|
+
positive_integer.text_value.to_i
|
224
|
+
end
|
225
|
+
end
|
226
|
+
}
|
227
|
+
end
|
228
|
+
|
229
|
+
rule ordinals
|
230
|
+
first_ordinal:ordinal rest_ordinals:(comma_joiner ordinal)* {
|
231
|
+
def ordinals
|
232
|
+
[first_ordinal] + rest_ordinals
|
233
|
+
end
|
234
|
+
|
235
|
+
def rest_ordinals
|
236
|
+
super.elements.map{|e| e.ordinal}
|
237
|
+
end
|
238
|
+
|
239
|
+
def numbers
|
240
|
+
ordinals.map{|o| o.number}
|
241
|
+
end
|
242
|
+
}
|
243
|
+
end
|
244
|
+
|
245
|
+
# ordinal, including "last", "2nd last", etc.
|
246
|
+
rule lordinal
|
247
|
+
( "first" / "last" / positive_integer ( "st" / "nd" / "rd" / "th" ) l:(WS+ "last")? ) {
|
248
|
+
def number
|
249
|
+
if text_value == "first"
|
250
|
+
1
|
251
|
+
elsif text_value == "last"
|
252
|
+
-1
|
253
|
+
elsif l.empty?
|
254
|
+
positive_integer.text_value.to_i
|
255
|
+
else
|
256
|
+
-(positive_integer.text_value.to_i)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
}
|
260
|
+
end
|
261
|
+
|
262
|
+
rule lordinals
|
263
|
+
first_lordinal:lordinal rest_lordinals:(comma_joiner lordinal)* {
|
264
|
+
def lordinals
|
265
|
+
[first_lordinal] + rest_lordinals
|
266
|
+
end
|
267
|
+
|
268
|
+
def rest_lordinals
|
269
|
+
super.elements.map{|e| e.lordinal}
|
270
|
+
end
|
271
|
+
|
272
|
+
def numbers
|
273
|
+
lordinals.map{|o| o.number}
|
274
|
+
end
|
275
|
+
}
|
276
|
+
end
|
277
|
+
|
278
|
+
rule positive_integer_or_ordinal
|
279
|
+
( "first" / positive_integer ( "st" / "nd" / "rd" / "th" )? ) {
|
280
|
+
def number
|
281
|
+
if text_value == "first"
|
282
|
+
1
|
283
|
+
else
|
284
|
+
positive_integer.text_value.to_i
|
285
|
+
end
|
286
|
+
end
|
287
|
+
}
|
288
|
+
end
|
289
|
+
|
290
|
+
rule nonnegative_integers
|
291
|
+
first_nonnegative_integer:nonnegative_integer rest_nonnegative_integers:(comma_joiner nonnegative_integer)* {
|
292
|
+
def nonnegative_integers
|
293
|
+
[first_nonnegative_integer] + rest_nonnegative_integers
|
294
|
+
end
|
295
|
+
|
296
|
+
def rest_nonnegative_integers
|
297
|
+
super.elements.map{|e| e.nonnegative_integer}
|
298
|
+
end
|
299
|
+
|
300
|
+
def numbers
|
301
|
+
nonnegative_integers.map{|n| n.number}
|
302
|
+
end
|
303
|
+
}
|
304
|
+
end
|
305
|
+
|
306
|
+
rule nonnegative_integer
|
307
|
+
"0" / positive_integer
|
308
|
+
end
|
309
|
+
|
310
|
+
rule positive_integer
|
311
|
+
[1-9] [0-9]* {
|
312
|
+
def number
|
313
|
+
text_value.to_i
|
314
|
+
end
|
315
|
+
}
|
316
|
+
end
|
317
|
+
|
318
|
+
rule at_time_of_day
|
319
|
+
"at" WS+ t:( twelve_noon_or_midnight / time12 / time24 ) {
|
320
|
+
def attributes
|
321
|
+
{ :validations => { :hour_of_day => [t.hour], :minute_of_hour => [t.minute], :second_of_minute => [t.second] } }
|
322
|
+
end
|
323
|
+
}
|
324
|
+
end
|
325
|
+
|
326
|
+
# Time of day (24-hour clock)
|
327
|
+
rule time24
|
328
|
+
h:hour24 a:( ":" m:int60 b:( ":" s:int60 )? )?
|
329
|
+
{
|
330
|
+
def hour
|
331
|
+
h.text_value.to_i
|
332
|
+
end
|
333
|
+
|
334
|
+
def minute
|
335
|
+
return 0 if a.empty?
|
336
|
+
a.m.number
|
337
|
+
end
|
338
|
+
|
339
|
+
def second
|
340
|
+
return 0 if a.empty? or a.b.empty?
|
341
|
+
a.b.s.number
|
342
|
+
end
|
343
|
+
}
|
344
|
+
end
|
345
|
+
|
346
|
+
# Time of day (12-hour clock)
|
347
|
+
rule time12
|
348
|
+
h:hour12 a:( ":" m:int60 b:( ":" s:int60 )? )? WS* c:( "am" / "a.m." / "pm" / "p.m." ) {
|
349
|
+
def hour
|
350
|
+
hr = h.text_value.to_i
|
351
|
+
if c.text_value =~ /\Ap/
|
352
|
+
# PM
|
353
|
+
if hr == 12
|
354
|
+
12
|
355
|
+
else
|
356
|
+
hr + 12
|
357
|
+
end
|
358
|
+
else
|
359
|
+
# AM
|
360
|
+
if hr == 12
|
361
|
+
0
|
362
|
+
else
|
363
|
+
hr
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
def minute
|
369
|
+
return 0 if a.empty?
|
370
|
+
a.m.text_value.to_i
|
371
|
+
end
|
372
|
+
|
373
|
+
def second
|
374
|
+
return 0 if a.empty? or a.b.empty?
|
375
|
+
a.b.s.text_value.to_i
|
376
|
+
end
|
377
|
+
}
|
378
|
+
end
|
379
|
+
|
380
|
+
# Noon or midnight
|
381
|
+
rule twelve_noon_or_midnight
|
382
|
+
( "12" ( ":" "00" ( ":" "00" )? )? WS+ )? a:( "noon" / "midnight") {
|
383
|
+
def hour
|
384
|
+
if a.text_value == "noon"
|
385
|
+
12
|
386
|
+
else
|
387
|
+
0
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
def minute
|
392
|
+
0
|
393
|
+
end
|
394
|
+
|
395
|
+
def second
|
396
|
+
0
|
397
|
+
end
|
398
|
+
}
|
399
|
+
end
|
400
|
+
|
401
|
+
# The numbers 1 though 12 (possibly padded to 2 digits)
|
402
|
+
rule hour12
|
403
|
+
"0" [1-9] / "10" / "11" / "12" / [1-9]
|
404
|
+
end
|
405
|
+
|
406
|
+
# The numbers 0 through 24 (possibly padded to 2 digits)
|
407
|
+
rule hour24
|
408
|
+
"0" [0-9] / "1" [0-9] / "2" [0-3] / [0-9]
|
409
|
+
end
|
410
|
+
|
411
|
+
# The numbers 0 through 59, always padded to 2 digits.
|
412
|
+
# Used for minutes or seconds.
|
413
|
+
rule int60s
|
414
|
+
first_int60:int60 rest_int60s:(comma_joiner int60)* {
|
415
|
+
def int60s
|
416
|
+
[first_int60] + rest_int60s
|
417
|
+
end
|
418
|
+
|
419
|
+
def rest_int60s
|
420
|
+
super.elements.map{|e| e.int60}
|
421
|
+
end
|
422
|
+
|
423
|
+
def numbers
|
424
|
+
int60s.map{ |n| n.number }
|
425
|
+
end
|
426
|
+
}
|
427
|
+
end
|
428
|
+
|
429
|
+
# The numbers 0 through 59, always padded to 2 digits.
|
430
|
+
# Used for minutes or seconds.
|
431
|
+
rule int60
|
432
|
+
[0-5] [0-9] {
|
433
|
+
def number
|
434
|
+
text_value.to_i
|
435
|
+
end
|
436
|
+
}
|
437
|
+
end
|
438
|
+
|
439
|
+
rule comma_joiner
|
440
|
+
(WS* "," WS* ("and" WS+)? / WS+ "and" WS+)
|
441
|
+
end
|
442
|
+
|
443
|
+
rule comma_or_semicolon_joiner
|
444
|
+
(WS* [,;] WS* ("and" WS+)? / WS+ "and" WS+)
|
445
|
+
end
|
446
|
+
|
447
|
+
# whitespace
|
448
|
+
rule WS
|
449
|
+
[ \t]
|
450
|
+
end
|
451
|
+
end
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'treetop'
|
2
|
+
require 'ice_cube'
|
3
|
+
require 'ice_cube/english/schedule_grammar'
|
4
|
+
|
5
|
+
module IceCube
|
6
|
+
module English
|
7
|
+
module ScheduleExtension
|
8
|
+
# Add the specified rule to the Schedule as a recurrence rule.
|
9
|
+
# The rule may be a Rule object, or it may be an english String.
|
10
|
+
def add_recurrence_rule_with_english(rule_or_english)
|
11
|
+
return add_recurrence_rule_without_english(rule_or_english) unless rule_or_english.is_a?(String)
|
12
|
+
rules = ::IceCube::Rule.from_english(rule_or_english, :multiple=>true)
|
13
|
+
rules.each do |rule|
|
14
|
+
add_recurrence_rule(rule)
|
15
|
+
end
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
# Add the specified rule to the Schedule as a exception rule.
|
20
|
+
# The rule may be a Rule object, or it may be an english String.
|
21
|
+
def add_exception_rule_with_english(rule_or_english)
|
22
|
+
return add_exception_rule_without_english(rule_or_english) unless rule_or_english.is_a?(String)
|
23
|
+
rules = ::IceCube::Rule.from_english(rule_or_english, :multiple=>true)
|
24
|
+
rules.each do |rule|
|
25
|
+
add_exception_rule(rule)
|
26
|
+
end
|
27
|
+
self
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class IceCube::Schedule # reopen
|
34
|
+
include ::IceCube::English::ScheduleExtension
|
35
|
+
|
36
|
+
alias add_recurrence_rule_without_english add_recurrence_rule
|
37
|
+
alias add_recurrence_rule add_recurrence_rule_with_english
|
38
|
+
|
39
|
+
alias add_exception_rule_without_english add_exception_rule
|
40
|
+
alias add_exception_rule add_exception_rule_with_english
|
41
|
+
|
42
|
+
alias rrule add_recurrence_rule
|
43
|
+
alias exrule add_exception_rule
|
44
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'ice_cube/english/rule_grammar'
|
2
|
+
|
3
|
+
module IceCube
|
4
|
+
module English
|
5
|
+
grammar ScheduleGrammar
|
6
|
+
include IceCube::English::RuleGrammar
|
7
|
+
|
8
|
+
# Multiple recurrence rules
|
9
|
+
rule recurrence_rules
|
10
|
+
first_rule:recurrence_rule rest_rules:( comma_or_semicolon_joiner recurrence_rule )* "."? {
|
11
|
+
def rules
|
12
|
+
[first_rule] + rest_rules
|
13
|
+
end
|
14
|
+
|
15
|
+
def rest_rules
|
16
|
+
super.elements.map{|r| r.recurrence_rule}
|
17
|
+
end
|
18
|
+
|
19
|
+
def attribute_hashes
|
20
|
+
rules.map{ |r| r.attributes }
|
21
|
+
end
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'ice_cube/english'
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ice_cube_english
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 9
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: "0.1"
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Dwayne Litzenberger
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2011-03-30 00:00:00 -04:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: treetop
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 21
|
29
|
+
segments:
|
30
|
+
- 1
|
31
|
+
- 4
|
32
|
+
- 9
|
33
|
+
version: 1.4.9
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: ice_cube
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ~>
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 11
|
45
|
+
segments:
|
46
|
+
- 0
|
47
|
+
- 6
|
48
|
+
- 6
|
49
|
+
version: 0.6.6
|
50
|
+
type: :runtime
|
51
|
+
version_requirements: *id002
|
52
|
+
description: ice_cube_english provides english time and date parsing for ice_cube.
|
53
|
+
email: dlitz@patientway.com
|
54
|
+
executables: []
|
55
|
+
|
56
|
+
extensions: []
|
57
|
+
|
58
|
+
extra_rdoc_files:
|
59
|
+
- README.rdoc
|
60
|
+
files:
|
61
|
+
- lib/ice_cube/english/grammar_helper.rb
|
62
|
+
- lib/ice_cube/english/rule_extension.rb
|
63
|
+
- lib/ice_cube/english/schedule_extension.rb
|
64
|
+
- lib/ice_cube/english/version.rb
|
65
|
+
- lib/ice_cube/english.rb
|
66
|
+
- lib/ice_cube_english.rb
|
67
|
+
- lib/ice_cube/english/rule_grammar.treetop
|
68
|
+
- lib/ice_cube/english/schedule_grammar.treetop
|
69
|
+
- README.rdoc
|
70
|
+
- test/test_helper.rb
|
71
|
+
has_rdoc: true
|
72
|
+
homepage: https://github.com/dlitz/ice_cube_english
|
73
|
+
licenses: []
|
74
|
+
|
75
|
+
post_install_message:
|
76
|
+
rdoc_options: []
|
77
|
+
|
78
|
+
require_paths:
|
79
|
+
- lib
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
hash: 3
|
86
|
+
segments:
|
87
|
+
- 0
|
88
|
+
version: "0"
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
hash: 23
|
95
|
+
segments:
|
96
|
+
- 1
|
97
|
+
- 3
|
98
|
+
- 6
|
99
|
+
version: 1.3.6
|
100
|
+
requirements: []
|
101
|
+
|
102
|
+
rubyforge_project: ice-cube-english
|
103
|
+
rubygems_version: 1.3.7
|
104
|
+
signing_key:
|
105
|
+
specification_version: 3
|
106
|
+
summary: English time and date parsing for ice_cube
|
107
|
+
test_files:
|
108
|
+
- test/test_helper.rb
|