timing 0.0.2 → 0.0.3
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.
- checksums.yaml +4 -4
- data/lib/timing/helpers.rb +58 -4
- data/lib/timing/natural_time_language.rb +2848 -0
- data/lib/timing/natural_time_language.treetop +227 -0
- data/lib/timing/natural_time_language_interpreters.rb +326 -0
- data/lib/timing/time_in_zone.rb +30 -2
- data/lib/timing/version.rb +1 -1
- data/lib/timing.rb +4 -0
- data/timing.gemspec +2 -0
- metadata +19 -2
@@ -0,0 +1,227 @@
|
|
1
|
+
module Timing
|
2
|
+
grammar NaturalTimeLanguage
|
3
|
+
|
4
|
+
rule expression
|
5
|
+
space* moment:moment space* zone_offset:zone_offset? space* <Expression>
|
6
|
+
end
|
7
|
+
|
8
|
+
rule moment
|
9
|
+
timestamp /
|
10
|
+
time_ago /
|
11
|
+
moment_at_time /
|
12
|
+
before_from_moment /
|
13
|
+
date_moment
|
14
|
+
end
|
15
|
+
|
16
|
+
rule date_moment
|
17
|
+
named_moment /
|
18
|
+
last_next_day_name /
|
19
|
+
day_month_name_year /
|
20
|
+
year_month_day /
|
21
|
+
beginning_end_interval
|
22
|
+
end
|
23
|
+
|
24
|
+
rule named_moment
|
25
|
+
now / today / tomorrow / yesterday
|
26
|
+
end
|
27
|
+
|
28
|
+
rule last_next_day_name
|
29
|
+
direction:last_next space* day_name:day_name <LastNextDayName>
|
30
|
+
end
|
31
|
+
|
32
|
+
rule day_month_name_year
|
33
|
+
day:integer space* month:month_name space* year:integer? space* <DayMonthNameYear>
|
34
|
+
end
|
35
|
+
|
36
|
+
rule year_month_day
|
37
|
+
year:integer '-' month:integer '-' day:integer <YearMonthDay>
|
38
|
+
end
|
39
|
+
|
40
|
+
rule beginning_end_interval
|
41
|
+
direction:beginning_end space* interval_type:interval <BeginningEndInterval>
|
42
|
+
end
|
43
|
+
|
44
|
+
rule time_ago
|
45
|
+
number:integer space* interval_type:interval space* 'ago'i <TimeAgo>
|
46
|
+
end
|
47
|
+
|
48
|
+
rule moment_at_time
|
49
|
+
moment:date_moment space* 'at'i space* time:hour_minute_second <MomentAtTime>
|
50
|
+
end
|
51
|
+
|
52
|
+
rule before_from_moment
|
53
|
+
number:integer space* interval_type:interval space* direction:before_from space* moment:moment <BeforeFromMoment>
|
54
|
+
end
|
55
|
+
|
56
|
+
rule timestamp
|
57
|
+
[\d]4..4 '-' [\d]2..2 '-' [\d]2..2 space* 'T'? space* [\d]2..2 ':' [\d]2..2 ':' [\d]2..2 <Timestamp>
|
58
|
+
end
|
59
|
+
|
60
|
+
rule now
|
61
|
+
'now'i <Now>
|
62
|
+
end
|
63
|
+
|
64
|
+
rule today
|
65
|
+
'today'i <Today>
|
66
|
+
end
|
67
|
+
|
68
|
+
rule tomorrow
|
69
|
+
'tomorrow'i <Tomorrow>
|
70
|
+
end
|
71
|
+
|
72
|
+
rule yesterday
|
73
|
+
'yesterday'i <Yesterday>
|
74
|
+
end
|
75
|
+
|
76
|
+
rule last_next
|
77
|
+
last / next
|
78
|
+
end
|
79
|
+
|
80
|
+
rule last
|
81
|
+
'last'i <LastNext>
|
82
|
+
end
|
83
|
+
|
84
|
+
rule next
|
85
|
+
'next'i <LastNext>
|
86
|
+
end
|
87
|
+
|
88
|
+
rule beginning_end
|
89
|
+
beginning_of / end_of
|
90
|
+
end
|
91
|
+
|
92
|
+
rule beginning_of
|
93
|
+
direction:'beginning'i space* 'of'i <BeginningEnd>
|
94
|
+
end
|
95
|
+
|
96
|
+
rule end_of
|
97
|
+
direction:'end'i space* 'of'i <BeginningEnd>
|
98
|
+
end
|
99
|
+
|
100
|
+
rule before_from
|
101
|
+
before / from
|
102
|
+
end
|
103
|
+
|
104
|
+
rule before
|
105
|
+
'before'i <BeforeFrom>
|
106
|
+
end
|
107
|
+
|
108
|
+
rule from
|
109
|
+
'from'i <BeforeFrom>
|
110
|
+
end
|
111
|
+
|
112
|
+
rule interval
|
113
|
+
second_interval /
|
114
|
+
minute_interval /
|
115
|
+
hour_interval /
|
116
|
+
day_interval /
|
117
|
+
week_interval /
|
118
|
+
month_interval /
|
119
|
+
year_interval
|
120
|
+
end
|
121
|
+
|
122
|
+
rule second_interval
|
123
|
+
'second'i 's'i? <SecondInterval>
|
124
|
+
end
|
125
|
+
|
126
|
+
rule minute_interval
|
127
|
+
'minute'i 's'i? <MinuteInterval>
|
128
|
+
end
|
129
|
+
|
130
|
+
rule hour_interval
|
131
|
+
'hour'i 's'i? <HourInterval>
|
132
|
+
end
|
133
|
+
|
134
|
+
rule day_interval
|
135
|
+
'day'i 's'i? <DayInterval>
|
136
|
+
end
|
137
|
+
|
138
|
+
rule week_interval
|
139
|
+
'week'i 's'i? <WeekInterval>
|
140
|
+
end
|
141
|
+
|
142
|
+
rule month_interval
|
143
|
+
'month'i 's'i? <MonthInterval>
|
144
|
+
end
|
145
|
+
|
146
|
+
rule year_interval
|
147
|
+
'year'i 's'i? <YearInterval>
|
148
|
+
end
|
149
|
+
|
150
|
+
rule day_name
|
151
|
+
long_day_name / short_day_name
|
152
|
+
end
|
153
|
+
|
154
|
+
rule long_day_name
|
155
|
+
'sunday'i <DayName> /
|
156
|
+
'monday'i <DayName> /
|
157
|
+
'tuesday'i <DayName> /
|
158
|
+
'wednesday'i <DayName> /
|
159
|
+
'thursday'i <DayName> /
|
160
|
+
'friday'i <DayName> /
|
161
|
+
'saturday'i <DayName>
|
162
|
+
end
|
163
|
+
|
164
|
+
rule short_day_name
|
165
|
+
'sun'i <DayName> /
|
166
|
+
'mon'i <DayName> /
|
167
|
+
'tue'i <DayName> /
|
168
|
+
'wed'i <DayName> /
|
169
|
+
'thu'i <DayName> /
|
170
|
+
'fri'i <DayName> /
|
171
|
+
'sat'i <DayName>
|
172
|
+
end
|
173
|
+
|
174
|
+
rule month_name
|
175
|
+
long_month_name / short_month_name
|
176
|
+
end
|
177
|
+
|
178
|
+
rule long_month_name
|
179
|
+
'january'i <MonthName> /
|
180
|
+
'february'i <MonthName> /
|
181
|
+
'march'i <MonthName> /
|
182
|
+
'april'i <MonthName> /
|
183
|
+
'may'i <MonthName> /
|
184
|
+
'june'i <MonthName> /
|
185
|
+
'july'i <MonthName> /
|
186
|
+
'august'i <MonthName> /
|
187
|
+
'september'i <MonthName> /
|
188
|
+
'october'i <MonthName> /
|
189
|
+
'november'i <MonthName> /
|
190
|
+
'december'i <MonthName>
|
191
|
+
end
|
192
|
+
|
193
|
+
rule short_month_name
|
194
|
+
'jan'i <MonthName> /
|
195
|
+
'feb'i <MonthName> /
|
196
|
+
'mar'i <MonthName> /
|
197
|
+
'apr'i <MonthName> /
|
198
|
+
'may'i <MonthName> /
|
199
|
+
'jun'i <MonthName> /
|
200
|
+
'jul'i <MonthName> /
|
201
|
+
'aug'i <MonthName> /
|
202
|
+
'sep'i <MonthName> /
|
203
|
+
'oct'i <MonthName> /
|
204
|
+
'nov'i <MonthName> /
|
205
|
+
'dec'i <MonthName>
|
206
|
+
end
|
207
|
+
|
208
|
+
rule zone_offset
|
209
|
+
[+-] [\d]2..2 ':'? [\d]2..2 <ZoneOffset>
|
210
|
+
end
|
211
|
+
|
212
|
+
rule hour_minute_second
|
213
|
+
[\d]2..2 ':' [\d]2..2 (':' [\d]2..2)? <HourMinuteSecond> /
|
214
|
+
'beginning' <HourMinuteSecond> /
|
215
|
+
'end' <HourMinuteSecond>
|
216
|
+
end
|
217
|
+
|
218
|
+
rule integer
|
219
|
+
[\d]+ <Int>
|
220
|
+
end
|
221
|
+
|
222
|
+
rule space
|
223
|
+
[\s\t\n]
|
224
|
+
end
|
225
|
+
|
226
|
+
end
|
227
|
+
end
|
@@ -0,0 +1,326 @@
|
|
1
|
+
module Timing
|
2
|
+
module NaturalTimeLanguage
|
3
|
+
|
4
|
+
class << self
|
5
|
+
|
6
|
+
def parse(expression)
|
7
|
+
parsed_expression = parser.parse expression
|
8
|
+
raise parser.failure_reason unless parsed_expression
|
9
|
+
parsed_expression.evaluate
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def parser
|
15
|
+
@parser ||= NaturalTimeLanguageParser.new
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
class Expression < Treetop::Runtime::SyntaxNode
|
21
|
+
def evaluate
|
22
|
+
moment.evaluate zone_offset.empty? ? nil : zone_offset.value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Now < Treetop::Runtime::SyntaxNode
|
27
|
+
def evaluate(zone_offset)
|
28
|
+
TimeInZone.now zone_offset
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class Today < Treetop::Runtime::SyntaxNode
|
33
|
+
def evaluate(zone_offset)
|
34
|
+
TimeInZone.now(zone_offset).beginning_of_day
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class Yesterday < Treetop::Runtime::SyntaxNode
|
39
|
+
def evaluate(zone_offset)
|
40
|
+
TimeInZone.now(zone_offset).beginning_of_day - Interval.days(1)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class Tomorrow < Treetop::Runtime::SyntaxNode
|
45
|
+
def evaluate(zone_offset)
|
46
|
+
TimeInZone.now(zone_offset).beginning_of_day + Interval.days(1)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class LastNextDayName < Treetop::Runtime::SyntaxNode
|
51
|
+
def evaluate(zone_offset)
|
52
|
+
today = TimeInZone.now(zone_offset).beginning_of_day
|
53
|
+
|
54
|
+
if direction.last?
|
55
|
+
if today.wday > day_name.value
|
56
|
+
today - Interval.days(today.wday - day_name.value)
|
57
|
+
else
|
58
|
+
today - Interval.weeks(1) + Interval.days(day_name.value - today.wday)
|
59
|
+
end
|
60
|
+
else
|
61
|
+
if today.wday < day_name.value
|
62
|
+
today + Interval.days(day_name.value - today.wday)
|
63
|
+
else
|
64
|
+
today + Interval.weeks(1) - Interval.days(today.wday - day_name.value)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class BeginningEndInterval < Treetop::Runtime::SyntaxNode
|
71
|
+
def evaluate(zone_offset)
|
72
|
+
now = TimeInZone.now zone_offset
|
73
|
+
if direction.beginning?
|
74
|
+
interval_type.beginning_of now
|
75
|
+
else
|
76
|
+
interval_type.end_of now
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class TimeAgo < Treetop::Runtime::SyntaxNode
|
82
|
+
def evaluate(zone_offset)
|
83
|
+
now = TimeInZone.now zone_offset
|
84
|
+
interval_type.time_ago now, number.value
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class DayMonthNameYear < Treetop::Runtime::SyntaxNode
|
89
|
+
def evaluate(zone_offset)
|
90
|
+
now = TimeInZone.now zone_offset
|
91
|
+
yyyy = year.empty? ? now.year : year.value
|
92
|
+
mm = month.value.to_s.rjust(2, '0')
|
93
|
+
dd = day.value.to_s.rjust(2, '0')
|
94
|
+
TimeInZone.parse "#{yyyy}-#{mm}-#{dd} 00:00:00 #{now.zone_offset}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
class YearMonthDay < Treetop::Runtime::SyntaxNode
|
99
|
+
def evaluate(zone_offset)
|
100
|
+
yyyy = year.value
|
101
|
+
mm = month.value.to_s.rjust(2, '0')
|
102
|
+
dd = day.value.to_s.rjust(2, '0')
|
103
|
+
TimeInZone.parse "#{yyyy}-#{mm}-#{dd} 00:00:00 #{zone_offset}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class MomentAtTime < Treetop::Runtime::SyntaxNode
|
108
|
+
def evaluate(zone_offset)
|
109
|
+
date = moment.evaluate zone_offset
|
110
|
+
TimeInZone.parse "#{date.strftime('%F')} #{time.value} #{date.zone_offset}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
class BeforeFromMoment < Treetop::Runtime::SyntaxNode
|
115
|
+
def evaluate(zone_offset)
|
116
|
+
time = moment.evaluate zone_offset
|
117
|
+
if direction.before?
|
118
|
+
interval_type.time_ago time, number.value
|
119
|
+
else
|
120
|
+
interval_type.time_after time, number.value
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class Timestamp < Treetop::Runtime::SyntaxNode
|
126
|
+
def evaluate(zone_offset)
|
127
|
+
TimeInZone.parse "#{text_value}#{zone_offset}"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
class BeginningEnd < Treetop::Runtime::SyntaxNode
|
132
|
+
def beginning?
|
133
|
+
direction.text_value.downcase == 'beginning'
|
134
|
+
end
|
135
|
+
|
136
|
+
def end?
|
137
|
+
direction.text_value.downcase == 'end'
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
class LastNext < Treetop::Runtime::SyntaxNode
|
142
|
+
def last?
|
143
|
+
text_value.downcase == 'last'
|
144
|
+
end
|
145
|
+
|
146
|
+
def next?
|
147
|
+
text_value.downcase == 'next'
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
class BeforeFrom < Treetop::Runtime::SyntaxNode
|
152
|
+
def before?
|
153
|
+
text_value.downcase == 'before'
|
154
|
+
end
|
155
|
+
|
156
|
+
def from?
|
157
|
+
text_value.downcase == 'from'
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
class SecondInterval < Treetop::Runtime::SyntaxNode
|
162
|
+
def time_ago(time, number)
|
163
|
+
time - Interval.seconds(number)
|
164
|
+
end
|
165
|
+
|
166
|
+
def time_after(time, number)
|
167
|
+
time + Interval.seconds(number)
|
168
|
+
end
|
169
|
+
|
170
|
+
def beginning_of(time)
|
171
|
+
raise 'Not supported'
|
172
|
+
end
|
173
|
+
|
174
|
+
def end_of(time)
|
175
|
+
raise 'Not supported'
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
class MinuteInterval < Treetop::Runtime::SyntaxNode
|
180
|
+
def time_ago(time, number)
|
181
|
+
time - Interval.minutes(number)
|
182
|
+
end
|
183
|
+
|
184
|
+
def time_after(time, number)
|
185
|
+
time + Interval.minutes(number)
|
186
|
+
end
|
187
|
+
|
188
|
+
def beginning_of(time)
|
189
|
+
raise 'Not supported'
|
190
|
+
end
|
191
|
+
|
192
|
+
def end_of(time)
|
193
|
+
raise 'Not supported'
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
class HourInterval < Treetop::Runtime::SyntaxNode
|
198
|
+
def time_ago(time, number)
|
199
|
+
time - Interval.hours(number)
|
200
|
+
end
|
201
|
+
|
202
|
+
def time_after(time, number)
|
203
|
+
time + Interval.hours(number)
|
204
|
+
end
|
205
|
+
|
206
|
+
def beginning_of(time)
|
207
|
+
time.beginning_of_hour
|
208
|
+
end
|
209
|
+
|
210
|
+
def end_of(time)
|
211
|
+
time.end_of_hour
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
class DayInterval < Treetop::Runtime::SyntaxNode
|
216
|
+
def time_ago(time, number)
|
217
|
+
time - Interval.days(number)
|
218
|
+
end
|
219
|
+
|
220
|
+
def time_after(time, number)
|
221
|
+
time + Interval.days(number)
|
222
|
+
end
|
223
|
+
|
224
|
+
def beginning_of(time)
|
225
|
+
time.beginning_of_day
|
226
|
+
end
|
227
|
+
|
228
|
+
def end_of(time)
|
229
|
+
time.end_of_day
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
class WeekInterval < Treetop::Runtime::SyntaxNode
|
234
|
+
def time_ago(time, number)
|
235
|
+
time - Interval.weeks(number)
|
236
|
+
end
|
237
|
+
|
238
|
+
def time_after(time, number)
|
239
|
+
time + Interval.weeks(number)
|
240
|
+
end
|
241
|
+
|
242
|
+
def beginning_of(time)
|
243
|
+
time.beginning_of_week
|
244
|
+
end
|
245
|
+
|
246
|
+
def end_of(time)
|
247
|
+
time.end_of_week
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
class MonthInterval < Treetop::Runtime::SyntaxNode
|
252
|
+
def time_ago(time, number)
|
253
|
+
time.months_ago(number)
|
254
|
+
end
|
255
|
+
|
256
|
+
def time_after(time, number)
|
257
|
+
time.months_after(number)
|
258
|
+
end
|
259
|
+
|
260
|
+
def beginning_of(time)
|
261
|
+
time.beginning_of_month
|
262
|
+
end
|
263
|
+
|
264
|
+
def end_of(time)
|
265
|
+
time.end_of_month
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
class YearInterval < Treetop::Runtime::SyntaxNode
|
270
|
+
def time_ago(time, number)
|
271
|
+
time.years_ago(number)
|
272
|
+
end
|
273
|
+
|
274
|
+
def time_after(time, number)
|
275
|
+
time.years_after(number)
|
276
|
+
end
|
277
|
+
|
278
|
+
def beginning_of(time)
|
279
|
+
time.beginning_of_year
|
280
|
+
end
|
281
|
+
|
282
|
+
def end_of(time)
|
283
|
+
time.end_of_year
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
class DayName < Treetop::Runtime::SyntaxNode
|
288
|
+
SHORT_NAMES = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
|
289
|
+
def value
|
290
|
+
SHORT_NAMES.index text_value[0..2].downcase
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
class MonthName < Treetop::Runtime::SyntaxNode
|
295
|
+
SHORT_NAMES = ['', 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']
|
296
|
+
def value
|
297
|
+
SHORT_NAMES.index text_value[0..2].downcase
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
class HourMinuteSecond < Treetop::Runtime::SyntaxNode
|
302
|
+
def value
|
303
|
+
if text_value.downcase == 'beginning'
|
304
|
+
'00:00:00'
|
305
|
+
elsif text_value.downcase == 'end'
|
306
|
+
'23:59:59'
|
307
|
+
else
|
308
|
+
"#{text_value}:00"[0..7]
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
class ZoneOffset < Treetop::Runtime::SyntaxNode
|
314
|
+
def value
|
315
|
+
::ZoneOffset.parse text_value
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
class Int < Treetop::Runtime::SyntaxNode
|
320
|
+
def value
|
321
|
+
text_value.to_i
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
end
|
326
|
+
end
|