acute 1.0.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.
- checksums.yaml +7 -0
- data/LICENSE +17 -0
- data/data/assumptions.yml +124 -0
- data/data/words.yml +699 -0
- data/exe/acute +4 -0
- data/lib/acute/assumption.rb +13 -0
- data/lib/acute/calendar.rb +91 -0
- data/lib/acute/chronic.rb +176 -0
- data/lib/acute/chronic_duration.rb +321 -0
- data/lib/acute/cli.rb +168 -0
- data/lib/acute/context.rb +205 -0
- data/lib/acute/describe.rb +316 -0
- data/lib/acute/duration.rb +120 -0
- data/lib/acute/errors.rb +47 -0
- data/lib/acute/explain.rb +165 -0
- data/lib/acute/extract.rb +493 -0
- data/lib/acute/grammar/clock_words.rb +109 -0
- data/lib/acute/grammar/compact.rb +222 -0
- data/lib/acute/grammar/duration.rb +93 -0
- data/lib/acute/grammar/nesting.rb +104 -0
- data/lib/acute/grammar/numbers.rb +378 -0
- data/lib/acute/grammar/ordinals.rb +101 -0
- data/lib/acute/grammar/ranges.rb +112 -0
- data/lib/acute/grammar/rule.rb +277 -0
- data/lib/acute/grammar/shifts.rb +226 -0
- data/lib/acute/grammar/words.rb +307 -0
- data/lib/acute/grammar.rb +378 -0
- data/lib/acute/holidays.rb +97 -0
- data/lib/acute/lexer.rb +487 -0
- data/lib/acute/locale.rb +53 -0
- data/lib/acute/mcp.rb +224 -0
- data/lib/acute/parser.rb +66 -0
- data/lib/acute/read.rb +21 -0
- data/lib/acute/resolve/clock.rb +75 -0
- data/lib/acute/resolve/dates.rb +75 -0
- data/lib/acute/resolve/day.rb +24 -0
- data/lib/acute/resolve/fuzzy.rb +91 -0
- data/lib/acute/resolve/named.rb +116 -0
- data/lib/acute/resolve/nth.rb +47 -0
- data/lib/acute/resolve/period.rb +160 -0
- data/lib/acute/resolve/periodic.rb +274 -0
- data/lib/acute/resolve/ranges.rb +165 -0
- data/lib/acute/resolve/recur.rb +187 -0
- data/lib/acute/resolve/rel.rb +146 -0
- data/lib/acute/resolve/shift.rb +81 -0
- data/lib/acute/resolve.rb +245 -0
- data/lib/acute/result.rb +128 -0
- data/lib/acute/rule/cron.rb +222 -0
- data/lib/acute/rule/expand.rb +401 -0
- data/lib/acute/rule.rb +232 -0
- data/lib/acute/tables/assumptions.rb +39 -0
- data/lib/acute/tables/words.rb +656 -0
- data/lib/acute/tables/zones.rb +930 -0
- data/lib/acute/tool/compact.rb +82 -0
- data/lib/acute/tool/ruby_llm.rb +29 -0
- data/lib/acute/tool.rb +192 -0
- data/lib/acute/verify.rb +216 -0
- data/lib/acute/version.rb +3 -0
- data/lib/acute/zone.rb +252 -0
- data/lib/acute.rb +160 -0
- data/schema/description.json +12 -0
- data/schema/duration.json +26 -0
- data/schema/extraction.json +30 -0
- data/schema/failure.json +29 -0
- data/schema/now.json +17 -0
- data/schema/result.json +45 -0
- data/schema/rule.json +19 -0
- data/schema/tools/describe.json +13 -0
- data/schema/tools/duration.json +12 -0
- data/schema/tools/extract.json +13 -0
- data/schema/tools/now.json +9 -0
- data/schema/tools/parse.json +13 -0
- data/schema/tools/recurrence.json +12 -0
- data/schema/tools/verify.json +14 -0
- data/schema/verdict.json +25 -0
- metadata +117 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
module Acute
|
|
2
|
+
# Rules, read only by Acute.recurrence. The words that make one ("every", "daily", a plural weekday) fail a
|
|
3
|
+
# one-off parse, so phrase never comes here.
|
|
4
|
+
class Grammar
|
|
5
|
+
# interval: units between occurrences; days: weekdays picked (0 = Sunday); nths: [n, wday] within the month
|
|
6
|
+
# (R8); monthdays: days of the month, -1 the last; setpos: BYSETPOS over Monday to Friday; month and date:
|
|
7
|
+
# what was said (R9); until, start, within, except and window hold one-off trees; length the Units of "for 6
|
|
8
|
+
# weeks"
|
|
9
|
+
Recur = Struct.new(:interval, :unit, :days, :nths, :monthdays, :setpos, :month, :date, :clocks, :part,
|
|
10
|
+
:until, :start, :within, :except_days, :except, :count, :length, :window)
|
|
11
|
+
|
|
12
|
+
# R5
|
|
13
|
+
FREQUENCY = { daily: [:day, 1], hourly: [:hour, 1], weekly: [:week, 1], monthly: [:month, 1], yearly: [:year, 1],
|
|
14
|
+
quarterly: [:month, 3], semiannual: [:month, 6], biweekly: [:week, 2], bimonthly: [:month, 2] }.freeze
|
|
15
|
+
TAIL = { until: true, through: true, except: true, starting: true, in: true, for: true }.freeze
|
|
16
|
+
MON_FRI = [1, 2, 3, 4, 5].freeze
|
|
17
|
+
SAT_SUN = [6, 0].freeze
|
|
18
|
+
|
|
19
|
+
MARKS = { every: true, once: true, **FREQUENCY.transform_values { true } }.freeze
|
|
20
|
+
|
|
21
|
+
def self.rule(tokens) = new(tokens).rule
|
|
22
|
+
|
|
23
|
+
# Whether the tokens hold a word that makes a rule: "every", a frequency, or a plural day or part of day
|
|
24
|
+
def self.rule?(t)
|
|
25
|
+
(0...t.size).step(S).any? do |i|
|
|
26
|
+
k = t[i]
|
|
27
|
+
(k == L::WORD && MARKS[t[i + 1]]) ||
|
|
28
|
+
((k == L::WDAY || k == L::PART || k == L::WEEKEND || (k == L::UNIT && t[i + 1] == :business_day)) && t[i + 2] & L::PLURAL != 0)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
COUNTS = { "twice" => "2 times", "thrice" => "3 times" }.freeze
|
|
33
|
+
|
|
34
|
+
# R15: whether text that failed as a rule says how often but not when: a count per period with no days or
|
|
35
|
+
# times ("twice a week", "3 times a day"), or a part of the day with no clock ("every morning"). Only
|
|
36
|
+
# recurrence! asks, once the phrase has failed, so "twice" is no word the lexer has to know.
|
|
37
|
+
def self.vague?(text)
|
|
38
|
+
t = Lexer.lex(text.gsub(/\b(twice|thrice)\b/i) { COUNTS[$1.downcase] })
|
|
39
|
+
return false unless Array === t
|
|
40
|
+
count = days = part = false
|
|
41
|
+
(0...t.size).step(S) do |i|
|
|
42
|
+
k = t[i]
|
|
43
|
+
return false if k == L::CLOCK || k == L::AT || k == L::MER
|
|
44
|
+
days ||= k == L::WDAY || k == L::ORD || k == L::MONTH
|
|
45
|
+
part ||= k == L::PART
|
|
46
|
+
next unless k == L::NUM && t[i + S] == L::WORD && t[i + S + 1] == :times
|
|
47
|
+
j = i + 2 * S
|
|
48
|
+
count ||= (t[j] == L::WORD && FREQUENCY[t[j + 1]]) || # "3 times daily"
|
|
49
|
+
((t[j] == L::ART || (t[j] == L::WORD && t[j + 1] == :every)) && t[j + S] == L::UNIT)
|
|
50
|
+
end
|
|
51
|
+
(count && !days) || (part && rule?(t))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def rule
|
|
55
|
+
r = Recur.new(nil, nil, [], [], [], nil, nil, nil, [], nil, nil, nil, nil, [], [], nil, nil, nil)
|
|
56
|
+
every = false
|
|
57
|
+
while @i < @n
|
|
58
|
+
k = @t[@i]
|
|
59
|
+
v = @t[@i + 1]
|
|
60
|
+
if k == L::WORD && v == :every
|
|
61
|
+
every = true
|
|
62
|
+
@i += S
|
|
63
|
+
k = @t[@i]
|
|
64
|
+
if k == L::WORD && @t[@i + 1] == :other # R7
|
|
65
|
+
r.interval = 2
|
|
66
|
+
@i += S
|
|
67
|
+
elsif k == L::ORD && @t[@i + 1] == 2 && !r.unit && every_second?(@i) # R5: "every second" is the unit, not the 2nd of the month
|
|
68
|
+
r.unit = :second
|
|
69
|
+
@i += @i + S < @n && @t[@i + S + 1] == :of ? 4 * S : S
|
|
70
|
+
elsif (k == L::NUM || (k == L::ORD && @t[@i + 2 * S + 1] != :of)) && @t[@i + S] == L::UNIT &&
|
|
71
|
+
@t[@i + S + 1] != :business_day # R5: "every 2 weeks", "every third day"
|
|
72
|
+
r.interval = @t[@i + 1]
|
|
73
|
+
@i += S
|
|
74
|
+
end
|
|
75
|
+
elsif k == L::WORD && (f = FREQUENCY[v])
|
|
76
|
+
return failure(:conflict) if r.unit
|
|
77
|
+
every = true
|
|
78
|
+
r.unit, r.interval = f
|
|
79
|
+
@i += S
|
|
80
|
+
elsif k == L::WORD && v == :once && @t[@i + S] == L::ART && @t[@i + 2 * S] == L::UNIT # R5: "once a week"
|
|
81
|
+
every = true
|
|
82
|
+
@i += 2 * S
|
|
83
|
+
elsif k == L::WORD && (v == :the || v == :on)
|
|
84
|
+
@i += S
|
|
85
|
+
elsif k == L::WORD && v == :of
|
|
86
|
+
@i += S
|
|
87
|
+
k = @t[@i]
|
|
88
|
+
if k == L::WORD && ((w = @t[@i + 1]) == :every || w == :the) && @t[@i + S] == L::UNIT && @t[@i + S + 1] == :month
|
|
89
|
+
every ||= w == :every
|
|
90
|
+
@i += 2 * S
|
|
91
|
+
elsif k != L::MONTH
|
|
92
|
+
return failure(:conflict)
|
|
93
|
+
end
|
|
94
|
+
elsif k == L::WORD && v == :except
|
|
95
|
+
@i += S
|
|
96
|
+
if (k = @t[@i]) == L::WDAY || k == L::WEEKEND || (k == L::UNIT && @t[@i + 1] == :business_day)
|
|
97
|
+
while (k = @t[@i]) == L::WDAY || k == L::WEEKEND || (k == L::UNIT && @t[@i + 1] == :business_day)
|
|
98
|
+
r.except_days.concat(k == L::WDAY ? [@t[@i + 1]] : k == L::WEEKEND ? SAT_SUN : MON_FRI)
|
|
99
|
+
@i += S
|
|
100
|
+
@i += S if @t[@i] == L::WORD && @t[@i + 1] == :and
|
|
101
|
+
end
|
|
102
|
+
else
|
|
103
|
+
loop do
|
|
104
|
+
x = tail_phrase(true)
|
|
105
|
+
return x if Failure === x
|
|
106
|
+
r.except << x
|
|
107
|
+
break unless @t[@i] == L::WORD && @t[@i + 1] == :and
|
|
108
|
+
@i += S
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
elsif k == L::WORD && (v == :until || v == :through)
|
|
112
|
+
return failure(:conflict) if r.until
|
|
113
|
+
@i += S
|
|
114
|
+
r.until = tail_phrase
|
|
115
|
+
return r.until if Failure === r.until
|
|
116
|
+
elsif starting?(@i)
|
|
117
|
+
@i += S
|
|
118
|
+
r.start = tail_phrase
|
|
119
|
+
return r.start if Failure === r.start
|
|
120
|
+
elsif k == L::WORD && (v == :from || v == :between) && r.unit && !r.window # R27: "every 15 minutes from 9 to 5"
|
|
121
|
+
stop = @i + S
|
|
122
|
+
stop += S while stop < @n && !window_end?(stop)
|
|
123
|
+
r.window = Grammar.new(@t[@i...stop]).phrase
|
|
124
|
+
return r.window if Failure === r.window
|
|
125
|
+
@i = stop
|
|
126
|
+
elsif k == L::WORD && v == :in
|
|
127
|
+
@i += S
|
|
128
|
+
r.within = tail_phrase
|
|
129
|
+
return r.within if Failure === r.within
|
|
130
|
+
elsif k == L::WORD && v == :for
|
|
131
|
+
@i += S
|
|
132
|
+
if @t[@i] == L::NUM && @t[@i + S + 1] == :times # R11: "for 10 occurrences"
|
|
133
|
+
r.count = @t[@i + 1]
|
|
134
|
+
@i += 2 * S
|
|
135
|
+
else
|
|
136
|
+
stop = tail_end(@i)
|
|
137
|
+
units = Grammar.new(@t[@i...stop]).duration
|
|
138
|
+
return units if Failure === units
|
|
139
|
+
r.length = units
|
|
140
|
+
@i = stop
|
|
141
|
+
end
|
|
142
|
+
elsif k == L::NUM && @t[@i + S + 1] == :times && @t[@i + S] == L::WORD
|
|
143
|
+
@i += 2 * S
|
|
144
|
+
# R15: "3 times a day" is a count per period
|
|
145
|
+
return failure(:conflict) if (k = @t[@i]) == L::ART || (k == L::WORD && @t[@i + 1] == :every)
|
|
146
|
+
r.count = @t[@i - 2 * S + 1]
|
|
147
|
+
elsif k == L::ORD || (k == L::REL && v == :last)
|
|
148
|
+
ords = rule_ords
|
|
149
|
+
k = @t[@i]
|
|
150
|
+
if k == L::WDAY # R8
|
|
151
|
+
days, plural = rule_days
|
|
152
|
+
every ||= plural
|
|
153
|
+
ords.each { |n| days.each { |w| r.nths << [n, w] } }
|
|
154
|
+
elsif k == L::UNIT && @t[@i + 1] == :business_day
|
|
155
|
+
r.setpos = ords
|
|
156
|
+
@i += S
|
|
157
|
+
else
|
|
158
|
+
@i += S if k == L::UNIT && @t[@i + 1] == :day
|
|
159
|
+
return failure(:conflict) if ords.any? { |n| n == 0 || n > 31 }
|
|
160
|
+
r.monthdays.concat(ords)
|
|
161
|
+
end
|
|
162
|
+
elsif k == L::WDAY
|
|
163
|
+
days, plural = rule_days
|
|
164
|
+
every ||= plural
|
|
165
|
+
r.days.concat(days)
|
|
166
|
+
elsif k == L::WEEKEND || (k == L::UNIT && v == :business_day) # R6
|
|
167
|
+
every ||= @t[@i + 2] & L::PLURAL != 0
|
|
168
|
+
r.days.concat(k == L::WEEKEND ? SAT_SUN : MON_FRI)
|
|
169
|
+
@i += S
|
|
170
|
+
elsif k == L::UNIT
|
|
171
|
+
return failure(:conflict) if r.unit
|
|
172
|
+
r.unit = v
|
|
173
|
+
@i += S
|
|
174
|
+
elsif k == L::PART
|
|
175
|
+
return failure(:conflict) if r.part
|
|
176
|
+
every ||= @t[@i + 2] & L::PLURAL != 0
|
|
177
|
+
r.part = v
|
|
178
|
+
@i += S
|
|
179
|
+
elsif k == L::MONTH
|
|
180
|
+
d = month_atom
|
|
181
|
+
return failure(:conflict) if d.year || r.month
|
|
182
|
+
r.month = d.month
|
|
183
|
+
r.date = d if d.day
|
|
184
|
+
elsif k == L::AT || clock_at?(@i) || k == L::NUM
|
|
185
|
+
@i += S if k == L::AT
|
|
186
|
+
loop do
|
|
187
|
+
at = @i
|
|
188
|
+
c = atom(true)
|
|
189
|
+
return c if Failure === c
|
|
190
|
+
if Date === c && !c.year && c.month && c.day && !r.month # "each 4/15"
|
|
191
|
+
r.date = c
|
|
192
|
+
break
|
|
193
|
+
end
|
|
194
|
+
return failure(:conflict, at) unless Clock === c
|
|
195
|
+
r.clocks << c
|
|
196
|
+
break unless @t[@i] == L::WORD && @t[@i + 1] == :and && (clock_at?(@i + S) || @t[@i + S] == L::AT)
|
|
197
|
+
@i += S
|
|
198
|
+
@i += S if @t[@i] == L::AT
|
|
199
|
+
end
|
|
200
|
+
else
|
|
201
|
+
return failure(:conflict)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
every ? r : failure(:incomplete)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
private
|
|
208
|
+
|
|
209
|
+
# R5: an ORD 2 after "every" that is "second" written as a word, and ends the phrase, goes on with a rule word
|
|
210
|
+
# or is "of the day": "every second", "every second until 5pm". Before a day, a unit or "of the month" it is
|
|
211
|
+
# an ordinal.
|
|
212
|
+
def every_second?(i)
|
|
213
|
+
return false unless @t[i + 2] & L::ENGLISH != 0
|
|
214
|
+
j = i + S
|
|
215
|
+
return true if j >= @n
|
|
216
|
+
return false unless @t[j] == L::WORD
|
|
217
|
+
w = @t[j + 1]
|
|
218
|
+
TAIL[w] || (w == :of && (@t[j + S + 1] == :the || @t[j + S + 1] == :every) && @t[j + 2 * S] == L::UNIT &&
|
|
219
|
+
@t[j + 2 * S + 1] == :day)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# "first and third", "last", "second to last" (R8)
|
|
223
|
+
def rule_ords
|
|
224
|
+
out = []
|
|
225
|
+
loop do
|
|
226
|
+
if @t[@i] == L::ORD && @t[@i + S + 1] == :to && @t[@i + 2 * S] == L::REL && @t[@i + 2 * S + 1] == :last
|
|
227
|
+
out << -@t[@i + 1]
|
|
228
|
+
@i += 3 * S
|
|
229
|
+
else
|
|
230
|
+
out << (@t[@i] == L::REL ? -1 : @t[@i + 1])
|
|
231
|
+
@i += S
|
|
232
|
+
end
|
|
233
|
+
break unless @t[@i] == L::WORD && @t[@i + 1] == :and && (@t[@i + S] == L::ORD || @t[@i + S + 1] == :last)
|
|
234
|
+
@i += S
|
|
235
|
+
end
|
|
236
|
+
out
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# "monday and wednesday", "mondays, wednesdays and fridays", and whether one was plural (R6)
|
|
240
|
+
def rule_days
|
|
241
|
+
out = []
|
|
242
|
+
plural = false
|
|
243
|
+
loop do
|
|
244
|
+
out << @t[@i + 1]
|
|
245
|
+
plural ||= @t[@i + 2] & L::PLURAL != 0
|
|
246
|
+
@i += S
|
|
247
|
+
break unless @t[@i] == L::WDAY || (@t[@i] == L::WORD && @t[@i + 1] == :and && @t[@i + S] == L::WDAY)
|
|
248
|
+
@i += S if @t[@i] == L::WORD
|
|
249
|
+
end
|
|
250
|
+
[out, plural]
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# The one-off phrase up to the next rule word, or with and_stop: true also up to "and"
|
|
254
|
+
def tail_phrase(and_stop = false)
|
|
255
|
+
stop = tail_end(@i, and_stop)
|
|
256
|
+
return failure(:incomplete) if stop == @i
|
|
257
|
+
node = Grammar.new(@t[@i...stop]).phrase
|
|
258
|
+
@i = stop
|
|
259
|
+
node
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# A window's clock range ends at the days it is on, or at the next rule word
|
|
263
|
+
def window_end?(j)
|
|
264
|
+
k = @t[j]
|
|
265
|
+
w = @t[j + 1]
|
|
266
|
+
(k == L::WORD && (TAIL[w] || w == :on || w == :every)) || k == L::WDAY || k == L::WEEKEND ||
|
|
267
|
+
(k == L::UNIT && w == :business_day) || (k == L::NUM && @t[j + S + 1] == :times)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def tail_end(j, and_stop = false)
|
|
271
|
+
j += S
|
|
272
|
+
j += S while j < @n && !(@t[j] == L::WORD && (TAIL[w = @t[j + 1]] || (and_stop && w == :and))) &&
|
|
273
|
+
!(@t[j] == L::NUM && @t[j + S + 1] == :times)
|
|
274
|
+
j > @n ? @n : j
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
module Acute
|
|
2
|
+
# Counts of units: "3 days", "a week"; moved from now or an anchor: "3 days ago", "2 years 4 months ago", "+2 days"
|
|
3
|
+
class Grammar
|
|
4
|
+
private
|
|
5
|
+
|
|
6
|
+
WEEKS = { week: true, fortnight: true }.freeze
|
|
7
|
+
|
|
8
|
+
# After a quantity: "ago", "from now", "from tomorrow", "after next tuesday", or nothing. D44: more
|
|
9
|
+
# quantities before the tail are each applied in the order said ("1 week and a day from now").
|
|
10
|
+
def shift_tail(q)
|
|
11
|
+
start = @i
|
|
12
|
+
more = more_units(q)
|
|
13
|
+
w = @t[@i + 1]
|
|
14
|
+
w = :after if w == :past && @t[@i] == L::REL # "three hours past noon"
|
|
15
|
+
if more && !SHIFT_TAIL[w]
|
|
16
|
+
@i = start
|
|
17
|
+
more = nil
|
|
18
|
+
end
|
|
19
|
+
case w
|
|
20
|
+
when :ago
|
|
21
|
+
@i += S
|
|
22
|
+
shifts(-1, q, more, nil)
|
|
23
|
+
when :hence, :later
|
|
24
|
+
@i += S
|
|
25
|
+
shifts(1, q, more, nil)
|
|
26
|
+
when :from, :after, :before
|
|
27
|
+
@i += S
|
|
28
|
+
anchor = item(true)
|
|
29
|
+
return anchor if Failure === anchor
|
|
30
|
+
shifts(w == :before ? -1 : 1, q, more, Now === anchor ? nil : anchor)
|
|
31
|
+
else
|
|
32
|
+
# "a week today", "a fortnight on tuesday": that long after the day
|
|
33
|
+
if WEEKS[q.unit] && !more && (@t[@i] == L::DAY || (@t[@i + 1] == :on && @t[@i + S] == L::WDAY))
|
|
34
|
+
@i += S if @t[@i] != L::DAY
|
|
35
|
+
anchor = atom(true)
|
|
36
|
+
return Failure === anchor ? anchor : shifts(1, q, nil, anchor)
|
|
37
|
+
end
|
|
38
|
+
return q unless !more && starting?(@i)
|
|
39
|
+
@i += S
|
|
40
|
+
@i += S if @t[@i + 1] == :from
|
|
41
|
+
start = item(true)
|
|
42
|
+
Failure === start ? start : Lasting.new(q, start)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# D44: "for 3 days from today", "for 1 week, from 20th dec": that long from the start of what follows;
|
|
47
|
+
# nil when no "from" or "starting" follows the length
|
|
48
|
+
def for_from
|
|
49
|
+
i = @i
|
|
50
|
+
@i += S
|
|
51
|
+
q = duration_quantity
|
|
52
|
+
if q && !(Failure === q) && (@t[@i + 1] == :from || starting?(@i))
|
|
53
|
+
@i += S
|
|
54
|
+
@i += S if @t[@i + 1] == :from
|
|
55
|
+
start = item(true)
|
|
56
|
+
return Failure === start ? start : Lasting.new(q, start)
|
|
57
|
+
end
|
|
58
|
+
@i = i
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# D48: "from 4pm for thirty minutes", "from 18th Dec for 10 days": that long from its start, as "for 10
|
|
63
|
+
# days from 18th Dec"
|
|
64
|
+
def lasting(start)
|
|
65
|
+
@i += S
|
|
66
|
+
q = duration_quantity
|
|
67
|
+
return q if Failure === q
|
|
68
|
+
q ? Lasting.new(q, start) : failure(@i < @n ? :conflict : :incomplete)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# D44: "starting", "commencing", or "beginning" with no "of" after it: from the start of what follows. Not
|
|
72
|
+
# "start from october", a verb before its own "from".
|
|
73
|
+
def starting?(i)
|
|
74
|
+
return true if @t[i] == L::WORD && @t[i + 1] == :starting
|
|
75
|
+
@t[i] == L::FUZZ && @t[i + 1] == :start && (w = @t[i + S + 1]) != :of && w != :from
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# D44: "3 fridays from now", "2nd monday before may 25", "two weekends ago", D46: "5 mornings ago": i is the day after the count
|
|
79
|
+
def counted?(i)
|
|
80
|
+
k = @t[i]
|
|
81
|
+
(k == L::WDAY || k == L::WEEKEND || k == L::HOLIDAY || k == L::PART || k == L::SEASON) && @t[i + S] == L::WORD &&
|
|
82
|
+
SHIFT_TAIL[@t[i + S + 1]]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# D44: "the sunday after next wed", "the monday before christmas" are the first one after or before the
|
|
86
|
+
# day named; "friday after next", "March after the next", "tuesday before last" the one after next friday
|
|
87
|
+
def around(node)
|
|
88
|
+
return node unless @t[@i] == L::WORD && ((w = @t[@i + 1]) == :after || w == :before)
|
|
89
|
+
j = @i + S
|
|
90
|
+
j += S if @t[j] == L::WORD && @t[j + 1] == :the
|
|
91
|
+
k = @t[j]
|
|
92
|
+
if k == L::REL && (r = @t[j + 1]) == (w == :after ? :next : :last) && nothing_own?(j + S)
|
|
93
|
+
@i = j + S
|
|
94
|
+
return Counted.new(w == :after ? 1 : -1, node, Rel.new(r, node))
|
|
95
|
+
end
|
|
96
|
+
return node unless k == L::REL || k == L::WDAY || k == L::DAY || k == L::HOLIDAY || k == L::MONTH
|
|
97
|
+
@i += S
|
|
98
|
+
anchor = item(true)
|
|
99
|
+
return anchor if Failure === anchor
|
|
100
|
+
Counted.new(w == :after ? 1 : -1, node, anchor)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# "next" in "the week after next" has nothing of its own when what follows is a time or nothing
|
|
104
|
+
def nothing_own?(j)
|
|
105
|
+
k = @t[j]
|
|
106
|
+
!k || k == L::CLOCK || k == L::NUM || k == L::AT || k == L::PART || (k == L::WORD && @t[j + 1] == :in)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def counted(n)
|
|
110
|
+
k = @t[@i]
|
|
111
|
+
v = @t[@i + 1]
|
|
112
|
+
node = k == L::WDAY ? Weekday.new(v) : k == L::HOLIDAY ? Holiday.new(v) : k == L::PART ? Part.new(v) :
|
|
113
|
+
k == L::SEASON ? Season.new(v) : WEEKEND
|
|
114
|
+
w = @t[@i + S + 1]
|
|
115
|
+
@i += 2 * S
|
|
116
|
+
return Counted.new(-n, node, nil) if w == :ago
|
|
117
|
+
return Counted.new(n, node, nil) if w == :hence || w == :later
|
|
118
|
+
anchor = item(true)
|
|
119
|
+
return anchor if Failure === anchor
|
|
120
|
+
Counted.new(w == :before ? -n : n, node, Now === anchor ? nil : anchor)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def shifts(sign, q, more, anchor)
|
|
124
|
+
node = Shift.new(sign * q.count, q.unit, anchor)
|
|
125
|
+
more&.each { |u| node = Shift.new(sign * u.count, u.unit, node) }
|
|
126
|
+
node
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# "4 months" in "2 years 4 months ago", "a day" in "1 week and a day", "and a half" in "an hour and a
|
|
130
|
+
# half ago"; nil when none follow
|
|
131
|
+
def more_units(last)
|
|
132
|
+
list = nil
|
|
133
|
+
while @i < @n
|
|
134
|
+
save = @i
|
|
135
|
+
@i += S if @t[@i + 1] == :and
|
|
136
|
+
unless (q = duration_tail(last.unit) || quantity)
|
|
137
|
+
@i = save
|
|
138
|
+
break
|
|
139
|
+
end
|
|
140
|
+
(list ||= []) << q
|
|
141
|
+
last = q
|
|
142
|
+
end
|
|
143
|
+
list
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# D44: "+2 days", "-3y", "- 3 seconds", "2023-12-29 -10days": a signed count of units shifts what comes
|
|
147
|
+
# before it, or now. After other atoms a minus needs a space before it and none after.
|
|
148
|
+
def signed?(last)
|
|
149
|
+
return false unless @t[@i + S] == L::NUM && unit_at(@i + 2 * S)
|
|
150
|
+
# "now - 2 hours" is two hours ago, never a range
|
|
151
|
+
@t[@i] == L::PLUS || last.nil? || last.equal?(NOW) || (@t[@i + 3] > @t[@i - 1] && @t[@i + 4] == @t[@i + S + 3])
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Each sign holds for the unsigned counts after it: "+1 day 2 hours", "+1 decade -2 years"
|
|
155
|
+
def signed_shifts(node)
|
|
156
|
+
sign = 1
|
|
157
|
+
while @i < @n
|
|
158
|
+
k = @t[@i]
|
|
159
|
+
if (signed = k == L::PLUS || k == L::DASH)
|
|
160
|
+
sign = k == L::PLUS ? 1 : -1
|
|
161
|
+
@i += S
|
|
162
|
+
end
|
|
163
|
+
unless (q = quantity)
|
|
164
|
+
return failure(:incomplete) if signed
|
|
165
|
+
break
|
|
166
|
+
end
|
|
167
|
+
node = Shift.new(sign * q.count, q.unit, node)
|
|
168
|
+
end
|
|
169
|
+
node
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# "a week", "3 days", "1/2 hour"; nil (nothing read) unless the tokens are a count and a unit
|
|
173
|
+
def quantity
|
|
174
|
+
i = @i
|
|
175
|
+
count = nil
|
|
176
|
+
if (said = spoken(i))
|
|
177
|
+
count, i = said
|
|
178
|
+
elsif @t[i] == L::ART then count = 1
|
|
179
|
+
elsif @t[i] == L::NUM
|
|
180
|
+
count = @t[i + 1]
|
|
181
|
+
if @t[i + S] == L::SLASH && @t[i + 2 * S] == L::NUM && @t[i + 2 * S + 1] > 0
|
|
182
|
+
count = Rational(count, @t[i + 2 * S + 1])
|
|
183
|
+
i += 2 * S
|
|
184
|
+
elsif @t[i + S] == L::DOT && @t[i + 2 * S] == L::NUM && unit_at(i + 3 * S) && touching?(i, i + 2 * S) # "2.5 hours", not "1. 5 hours" (D47)
|
|
185
|
+
count += Rational(@t[i + 2 * S + 1], 10**(@t[i + 2 * S + 2] & L::DIGITS))
|
|
186
|
+
i += 2 * S
|
|
187
|
+
end
|
|
188
|
+
elsif @t[i + 1] == :half && @t[i + S] == L::ART # "half an hour"
|
|
189
|
+
count = Rational(1, 2)
|
|
190
|
+
i += S
|
|
191
|
+
elsif @t[i + 1] == :couple # D44: "a couple of days" is two
|
|
192
|
+
count = 2
|
|
193
|
+
elsif @t[i] == L::DECADE && @t[i + 2] & (L::WORDY | L::QUOTED) == 0 # "30s", "5400s": seconds, not a decade (D47); "70's" and "nineties" are decades (D50)
|
|
194
|
+
@i = i + S
|
|
195
|
+
return Unit.new(:second, @t[i + 1])
|
|
196
|
+
else return
|
|
197
|
+
end
|
|
198
|
+
unit = unit_at(i + S)
|
|
199
|
+
return unless unit
|
|
200
|
+
@i = i + 2 * S
|
|
201
|
+
Unit.new(unit, count)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# "this time tomorrow", "same time next week": now moved by the day or unit
|
|
205
|
+
def same_time
|
|
206
|
+
i = @i
|
|
207
|
+
@i += S
|
|
208
|
+
return failure(:incomplete) if @i >= @n
|
|
209
|
+
x = atom(true)
|
|
210
|
+
if DayRef === x then Shift.new(x.offset, :day, nil)
|
|
211
|
+
elsif Rel === x && Unit === x.node && !x.node.count && (x.word == :next || x.word == :last)
|
|
212
|
+
Shift.new(x.word == :next ? 1 : -1, x.node.unit, nil)
|
|
213
|
+
else Failure === x ? x : failure(:conflict, i)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def touching?(from, to) = (from...to).step(S).all? { |j| @t[j + 4] == @t[j + S + 3] }
|
|
218
|
+
|
|
219
|
+
# "second" is the ordinal unless it follows a count
|
|
220
|
+
def unit_at(i)
|
|
221
|
+
k = @t[i]
|
|
222
|
+
return @t[i + 1] if k == L::UNIT
|
|
223
|
+
:second if k == L::ORD && @t[i + 1] == 2 && @t[i + 2] & L::WORDY != 0 && @t[i + S] != L::UNIT # not "2005 second quarter"
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|