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,222 @@
|
|
|
1
|
+
module Acute
|
|
2
|
+
# Written forms with no words in them: ISO and compact ISO dates, ISO weeks, bare digits as clocks, dotted clocks, spaced dates
|
|
3
|
+
class Grammar
|
|
4
|
+
SEPARATOR = { L::DASH => true, L::SLASH => true, L::DOT => true }.freeze
|
|
5
|
+
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
# D44: "20261123", "20030925T104941", "20030925T10", "19980915T09:05:32": a compact ISO date, then the time after its T
|
|
9
|
+
def compact_date(v, at)
|
|
10
|
+
m = v / 100 % 100
|
|
11
|
+
d = v % 100
|
|
12
|
+
return failure(:invalid_date, at) unless m >= 1 && m <= 12 && d >= 1 && d <= 31
|
|
13
|
+
date = Date.new(v / 10_000, m, d, nil, false)
|
|
14
|
+
k = @t[@i]
|
|
15
|
+
return date unless (k == L::NUM || k == L::CLOCK) && @t[@i + 3] - @t[at + 4] <= 1
|
|
16
|
+
if k == L::CLOCK
|
|
17
|
+
c = written(@t[@i + 1], @t[@i + 2])
|
|
18
|
+
return c if Failure === c
|
|
19
|
+
else
|
|
20
|
+
n = @t[@i + 1]
|
|
21
|
+
digits = @t[@i + 2] & L::DIGITS
|
|
22
|
+
if digits == 2
|
|
23
|
+
return failure(:invalid_date, @i) if n > 23
|
|
24
|
+
@i += S
|
|
25
|
+
return Within.new(Clock.new(n, 0, 0, nil, nil, true, :hour), date)
|
|
26
|
+
end
|
|
27
|
+
return failure(:invalid_date, @i) unless digits == 6 || digits == 4
|
|
28
|
+
n *= 100 if digits == 4
|
|
29
|
+
j = @i + S
|
|
30
|
+
# D48: "20030925T104941.5": a fraction of the second, as after "10:49:41"
|
|
31
|
+
frac = digits == 6 && @t[j] == L::DOT && @t[j + S] == L::NUM && @t[j - 1] == @t[j + 3] && @t[j + 4] == @t[j + S + 3]
|
|
32
|
+
f = frac ? (@t[j + S + 1] << 5 | @t[j + S + 2] & L::DIGITS) << L::FRACTION : 0
|
|
33
|
+
c = written(n, digits == 6 ? L::SECONDS | f : 0)
|
|
34
|
+
return c if Failure === c
|
|
35
|
+
@i += 2 * S if frac
|
|
36
|
+
end
|
|
37
|
+
c.h24 = true unless c.meridiem
|
|
38
|
+
Within.new(c, date)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# D44: "330 pm", "at 730 in the evening", "tonight 815": three digits marked as a time, by am or pm after
|
|
42
|
+
# them or "at" or a part of day before, are its hour and minutes; so are four before am or pm, "1140 a.m." (D5)
|
|
43
|
+
def three_digits(v, at)
|
|
44
|
+
h = v / 100
|
|
45
|
+
m = v % 100
|
|
46
|
+
return failure(:invalid_date, at) if m > 59
|
|
47
|
+
if @t[@i] == L::MER
|
|
48
|
+
return failure(:invalid_date, at) if h > 12
|
|
49
|
+
@i += S
|
|
50
|
+
return Clock.new(h, m, 0, @t[@i - S + 1] == 0 ? :am : :pm, nil, false, false)
|
|
51
|
+
end
|
|
52
|
+
Clock.new(h, m, 0, nil, nil, false, false)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# D44: "15h30", "3h18": a joined "h" between hours and minutes is a 24-hour clock; "15h" alone after "at",
|
|
56
|
+
# another atom or before a zone ("at 16h", "Thursday 8h GMT"). Nil when it counts: "2h ago", "15h" alone.
|
|
57
|
+
def h_clock(v, i, loose)
|
|
58
|
+
j = i + 2 * S
|
|
59
|
+
if @t[j] == L::NUM && @t[j + 2] & L::DIGITS == 2 && @t[j + 3] == @t[i + S + 4]
|
|
60
|
+
return failure(:invalid_date, j) if @t[j + 1] > 59
|
|
61
|
+
@i = j + S
|
|
62
|
+
return Clock.new(v, @t[j + 1], 0, nil, nil, true, false)
|
|
63
|
+
end
|
|
64
|
+
k = @t[j]
|
|
65
|
+
return unless (loose && !(k == L::WORD && SHIFT_TAIL[@t[j + 1]])) || k == L::ZONE
|
|
66
|
+
@i = j
|
|
67
|
+
Clock.new(v, 0, 0, nil, nil, true, false)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# D5: a time with a leading zero, after "at", or with "h"/"hrs"; otherwise a year
|
|
71
|
+
def four_digits(v, flags, at)
|
|
72
|
+
if flags & L::ZERO != 0 || (at >= S && @t[at - S] == L::AT) || @t[at + S] == L::UNIT
|
|
73
|
+
return failure(:invalid_date, at) if v / 100 > 23 || v % 100 > 59
|
|
74
|
+
Clock.new(v / 100, v % 100, 0, nil, nil, flags & L::ZERO != 0, false)
|
|
75
|
+
else
|
|
76
|
+
Date.new(v, nil, nil, nil, false)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# D44: "9.30pm", "12.55 pm", "01.03.12 a.m.", "at 12.30": numbers joined by dots before am or pm, or two
|
|
81
|
+
# after "at" or beside a written date ("29.02.2020 13.12", "8.20 2017-06-30"), are a clock
|
|
82
|
+
def dotted_clock?(i)
|
|
83
|
+
j = i + 2 * S
|
|
84
|
+
return false unless @t[i + 2] & L::DIGITS <= 2 && @t[j] == L::NUM && @t[j + 2] & L::DIGITS == 2
|
|
85
|
+
# D48: "18.20" makes no date either way round; "last fri 9.30" has its day already
|
|
86
|
+
if (i >= S && (@t[i - S] == L::AT || @t[i - S] == L::WDAY)) || (i >= 2 * S && @t[i - S] == L::NUM && SEPARATOR[@t[i - 2 * S]]) ||
|
|
87
|
+
(@t[j + S] == L::NUM && SEPARATOR[@t[j + 2 * S]]) || (@t[i + 1] > 12 && @t[j + 1] > 12)
|
|
88
|
+
return @t[j + S] != L::DOT && @t[i + 1] <= 23 && @t[j + 1] <= 59
|
|
89
|
+
end
|
|
90
|
+
j += 2 * S if @t[j + S] == L::DOT && @t[j + 2 * S] == L::NUM && @t[j + 2 * S + 2] & L::DIGITS == 2
|
|
91
|
+
# D48: "9.30-4.30pm": the start of a range of dotted clocks with a meridiem
|
|
92
|
+
j += 4 * S if @t[j + S] == L::DASH && @t[j + 2 * S] == L::NUM && @t[j + 3 * S] == L::DOT && @t[j + 4 * S] == L::NUM
|
|
93
|
+
@t[j + S] == L::MER
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# D44: "2014 12 28", "10 31 1974": digits and spaces only, a year first or last, read as a slashed date (D37)
|
|
97
|
+
def spaced_date?(i, digits)
|
|
98
|
+
return false unless @t[i + 2 * S] == L::NUM && (digits == 4 || digits <= 2)
|
|
99
|
+
b = @t[i + S + 2]
|
|
100
|
+
c = @t[i + 2 * S + 2]
|
|
101
|
+
return false if (b | c | @t[i + 2]) & L::WORDY != 0 || b & L::DIGITS > 2
|
|
102
|
+
digits == 4 ? c & L::DIGITS <= 2 : c & L::DIGITS == 4
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def spaced_date(v, digits)
|
|
106
|
+
t = @t
|
|
107
|
+
i = @i
|
|
108
|
+
@i += 3 * S
|
|
109
|
+
return Date.new(v, t[i + S + 1], t[i + 2 * S + 1], nil, false) if digits == 4
|
|
110
|
+
Date.new(t[i + 2 * S + 1], v, t[i + S + 1], :slash, false)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def dotted_clock(i)
|
|
114
|
+
v = @t[i + 1] * 10_000 + @t[i + 2 * S + 1] * 100
|
|
115
|
+
@i = i + 2 * S
|
|
116
|
+
if @t[@i + S] == L::DOT
|
|
117
|
+
@i += 2 * S
|
|
118
|
+
return written(v + @t[@i + 1], L::SECONDS)
|
|
119
|
+
end
|
|
120
|
+
written(v, 0)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def iso_date
|
|
124
|
+
t = @t
|
|
125
|
+
i = @i
|
|
126
|
+
y = t[i + 1]
|
|
127
|
+
m = t[i + 2 * S + 1]
|
|
128
|
+
if t[i + 2 * S + 2] & L::DIGITS == 3
|
|
129
|
+
# "2026-001": the day of the year
|
|
130
|
+
days = Calendar.days_from_civil(y, 1, 1) + m - 1
|
|
131
|
+
return failure(:invalid_date, i + 2 * S) unless m >= 1 && Calendar.year_of(days) == y
|
|
132
|
+
ymd = Calendar.civil_from_days(days)
|
|
133
|
+
m = ymd / 100 % 100
|
|
134
|
+
d = ymd % 100
|
|
135
|
+
@i += 3 * S
|
|
136
|
+
elsif @t[i + 3 * S] == L::DASH && @t[i + 4 * S] == L::NUM
|
|
137
|
+
d = t[i + 4 * S + 1]
|
|
138
|
+
@i += 5 * S
|
|
139
|
+
else
|
|
140
|
+
@i += 3 * S
|
|
141
|
+
return Date.new(y, m, nil, nil, false)
|
|
142
|
+
end
|
|
143
|
+
date = Date.new(y, m, d, nil, false)
|
|
144
|
+
@i += S if @t[@i] == L::WORD && @t[@i + 1] == :t && @t[@i + S] == L::CLOCK
|
|
145
|
+
# D44: "2003-09-25T10", "1978-12-22T23Z": two digits alone after the date are its hour, at the hour's grain
|
|
146
|
+
if @t[@i] == L::NUM && @t[@i + 2] & L::DIGITS == 2 && ((k = @t[@i + S]).nil? || k == L::ZONE) &&
|
|
147
|
+
@t[@i + 3] - @t[@i - 1] <= 1
|
|
148
|
+
return failure(:invalid_date, @i) if @t[@i + 1] > 23
|
|
149
|
+
@i += S
|
|
150
|
+
return Within.new(Clock.new(@t[@i - S + 1], 0, 0, nil, nil, true, :hour), date)
|
|
151
|
+
end
|
|
152
|
+
dotted = @t[@i] == L::NUM && @t[@i + S] == L::DOT && dotted_clock?(@i)
|
|
153
|
+
return date unless dotted || @t[@i] == L::CLOCK
|
|
154
|
+
c = dotted ? dotted_clock(@i) : written(@t[@i + 1], @t[@i + 2])
|
|
155
|
+
return c if Failure === c
|
|
156
|
+
c.h24 = true unless c.meridiem || c.named # D4, D29: after an ISO date the time is 24-hour
|
|
157
|
+
Within.new(c, date)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# D13: "week 42", "W42", "W42-3", "W423", "week 53 of 2026"
|
|
161
|
+
def iso_week
|
|
162
|
+
w = @t[@i + S + 1]
|
|
163
|
+
day = nil
|
|
164
|
+
if @t[@i + S + 2] & L::DIGITS == 3
|
|
165
|
+
day = w % 10
|
|
166
|
+
w /= 10
|
|
167
|
+
end
|
|
168
|
+
@i += 2 * S
|
|
169
|
+
if !day && joined?(@i) && @t[@i + S] == L::NUM && @t[@i + S + 2] & L::DIGITS == 1 && @t[@i + S + 1].between?(1, 7)
|
|
170
|
+
day = @t[@i + S + 1]
|
|
171
|
+
@i += 2 * S
|
|
172
|
+
end
|
|
173
|
+
of(IsoWeek.new(w, day))
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def week_number?(i)
|
|
177
|
+
return false unless @t[i] == L::NUM
|
|
178
|
+
v = @t[i + 1]
|
|
179
|
+
# "W481": week 48, day 1, with no space after the W
|
|
180
|
+
return @t[i - 1] == @t[i + 3] && (v / 10).between?(1, 53) && (v % 10).between?(1, 7) if @t[i + 2] & L::DIGITS == 3
|
|
181
|
+
return false unless @t[i + 2] & L::DIGITS <= 2 && v.between?(1, 53)
|
|
182
|
+
k = @t[i + S]
|
|
183
|
+
k != L::UNIT && k != L::MER && k != L::SLASH && k != L::DOT && k != L::MONTH
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# D44: "3 in the AM", "4:30 in the pm": am or pm after "in the" is the clock's
|
|
187
|
+
def in_the_meridiem? = @t[@i + 1] == :in && @t[@i + S + 1] == :the && @t[@i + 2 * S] == L::MER && @t[@i] == L::WORD
|
|
188
|
+
|
|
189
|
+
# D44: "0am", "00:00 am": hour 0 with am is midnight, as said by those who write 24-hour clocks
|
|
190
|
+
def midnight_am?(kind) = kind == L::MER && @t[@i + 1] == 0
|
|
191
|
+
|
|
192
|
+
def written(v, flags)
|
|
193
|
+
hour = v / 10_000
|
|
194
|
+
min = v / 100 % 100
|
|
195
|
+
sec = v % 100
|
|
196
|
+
at = @i
|
|
197
|
+
@i += S
|
|
198
|
+
f = flags >> L::FRACTION
|
|
199
|
+
frac = Rational(f >> 5, 10**(f & 31)) if f > 0
|
|
200
|
+
return failure(:invalid_date, at) if (hour > 23 && (v != 240_000 || frac)) || min > 59 || sec > 59
|
|
201
|
+
@i += 2 * S if in_the_meridiem?
|
|
202
|
+
kind = @t[@i]
|
|
203
|
+
# D44: "24:00" is the end of the day, 24-hour only
|
|
204
|
+
return Clock.new(24, 0, 0, nil, nil, true, flags & L::SECONDS != 0) if hour == 24 && kind != L::MER && kind != L::ART
|
|
205
|
+
if kind == L::MER || (kind == L::ART && @t[at + 4] == @t[@i + 3]) # "9:00a"
|
|
206
|
+
return failure(:invalid_date, at) if (hour < 1 && !midnight_am?(kind)) || hour > 12
|
|
207
|
+
meridiem = kind == L::ART || @t[@i + 1] == 0 ? :am : :pm
|
|
208
|
+
@i += S
|
|
209
|
+
Clock.new(hour, min, sec, meridiem, nil, false, flags & L::SECONDS != 0, frac)
|
|
210
|
+
elsif kind == L::NAMED && hour == 12 && min == 0 && sec == 0
|
|
211
|
+
@i += S
|
|
212
|
+
named(@t[@i - S + 1])
|
|
213
|
+
# D44: "06:00h"; D48: "06:00 hours", "18:30 hours", written as a 24-hour clock
|
|
214
|
+
elsif kind == L::UNIT && @t[@i + 1] == :hour && (@t[@i + 4] - @t[at + 4] == 1 || flags & L::ZERO != 0 || hour > 12)
|
|
215
|
+
@i += S
|
|
216
|
+
Clock.new(hour, min, sec, nil, nil, true, flags & L::SECONDS != 0, frac)
|
|
217
|
+
else
|
|
218
|
+
Clock.new(hour, min, sec, nil, nil, flags & L::ZERO != 0, flags & L::SECONDS != 0, frac)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
module Acute
|
|
2
|
+
# Lengths of time: counted units only, so "next friday" is not one
|
|
3
|
+
class Grammar
|
|
4
|
+
def self.duration(tokens) = new(tokens).duration
|
|
5
|
+
|
|
6
|
+
# "3 days", "1h 30m", "2 weeks and 3 days", "an hour and a half", "half an hour", "for 5 days": the Units said
|
|
7
|
+
def duration
|
|
8
|
+
parts = []
|
|
9
|
+
@i += S if @t[0] == L::WORD && @t[1] == :for
|
|
10
|
+
while @i < @n
|
|
11
|
+
@i += S if parts.any? && (@t[@i + 1] == :and || @t[@i] == L::PLUS)
|
|
12
|
+
if parts.any? && (q = duration_tail(parts.last.unit))
|
|
13
|
+
parts << q
|
|
14
|
+
next
|
|
15
|
+
end
|
|
16
|
+
half = @t[@i + 1] == :half
|
|
17
|
+
@i += S if half
|
|
18
|
+
if half && @t[@i] == L::UNIT # "half hour"
|
|
19
|
+
q = Unit.new(@t[@i + 1], 1)
|
|
20
|
+
@i += S
|
|
21
|
+
elsif half && @t[@i] != L::ART # D47: "half an hour", not "half145h"
|
|
22
|
+
return failure(@i < @n ? :conflict : :incomplete)
|
|
23
|
+
else
|
|
24
|
+
q = duration_quantity
|
|
25
|
+
end
|
|
26
|
+
return failure(@i < @n ? :conflict : :incomplete) unless q&.count&.positive?
|
|
27
|
+
q.count = Rational(q.count, 2) if half
|
|
28
|
+
parts << q
|
|
29
|
+
end
|
|
30
|
+
parts.empty? ? failure(:incomplete) : parts
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# After a unit: "and a half", "and half", "and three quarters" are that much of it; a bare number last is
|
|
36
|
+
# the next smaller unit's ("1 hour thirty", "2 hours and 10")
|
|
37
|
+
def duration_tail(unit)
|
|
38
|
+
if (f = fraction(@i)) && @t[f[0]] != L::UNIT && @t[f[0]] != L::ART
|
|
39
|
+
@i = f[0]
|
|
40
|
+
return Unit.new(unit, f[1])
|
|
41
|
+
end
|
|
42
|
+
return unless @t[@i] == L::NUM && @i + S == @n && (unit == :hour || unit == :minute) && @t[@i + 1].between?(1, 59)
|
|
43
|
+
@i += S
|
|
44
|
+
Unit.new(unit == :hour ? :minute : :second, @t[@i - S + 1])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# "two and a half years", "one and three quarters of an hour", "a quarter of an hour", "3-min", "2 more
|
|
48
|
+
# minutes", or a quantity
|
|
49
|
+
def duration_quantity
|
|
50
|
+
i = @i
|
|
51
|
+
if (f = fraction(i)) && @t[f[0] + 1] == :of && @t[f[0]] == L::WORD
|
|
52
|
+
j = f[0] + S
|
|
53
|
+
j += S if @t[j] == L::ART
|
|
54
|
+
if (unit = unit_at(j))
|
|
55
|
+
@i = j + S
|
|
56
|
+
return Unit.new(unit, f[1])
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
if @t[i] == L::DECADE && @t[i + 2] & (L::WORDY | L::QUOTED) == 0 # "5400s": in a length, seconds, whatever the digits; not "70's" or "nineties" (D50)
|
|
60
|
+
@i = i + S
|
|
61
|
+
return Unit.new(:second, @t[i + 1])
|
|
62
|
+
end
|
|
63
|
+
return quantity unless @t[i] == L::NUM
|
|
64
|
+
if @t[i + S + 1] == :and && (f = fraction(i + 2 * S))
|
|
65
|
+
j = f[0]
|
|
66
|
+
j += S if @t[j] == L::WORD && @t[j + 1] == :of
|
|
67
|
+
j += S if @t[j] == L::ART
|
|
68
|
+
if (unit = unit_at(j))
|
|
69
|
+
@i = j + S
|
|
70
|
+
return Unit.new(unit, @t[i + 1] + f[1])
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
joined = @t[i + S] == L::DASH && @t[i + 4] == @t[i + S + 3] && @t[i + S + 4] == @t[i + 2 * S + 3]
|
|
74
|
+
if (joined || @t[i + S + 1] == :more) && (unit = unit_at(i + 2 * S))
|
|
75
|
+
@i = i + 3 * S
|
|
76
|
+
return Unit.new(unit, @t[i + 1])
|
|
77
|
+
end
|
|
78
|
+
quantity
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# A fraction from j, and where it ends: "a half", "half", "a quarter", "three quarters"
|
|
82
|
+
def fraction(j)
|
|
83
|
+
j += S if @t[j] == L::ART && @t[j + S + 1] == :half
|
|
84
|
+
return [j + S, Rational(1, 2)] if @t[j] == L::WORD && @t[j + 1] == :half
|
|
85
|
+
n = 1
|
|
86
|
+
if @t[j] == L::ART || @t[j] == L::NUM
|
|
87
|
+
n = @t[j + 1]
|
|
88
|
+
j += S
|
|
89
|
+
end
|
|
90
|
+
[j + S, Rational(n, 4)] if @t[j] == L::UNIT && @t[j + 1] == :quarter && n.between?(1, 3)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
module Acute
|
|
2
|
+
# An item's atoms nested by size, the coarsest outside
|
|
3
|
+
class Grammar
|
|
4
|
+
private
|
|
5
|
+
|
|
6
|
+
def pair(x, xs, y, ys)
|
|
7
|
+
if Clock === x && fuzzy_part?(y) then y = y.node
|
|
8
|
+
elsif Clock === y && fuzzy_part?(x) then x = x.node
|
|
9
|
+
end
|
|
10
|
+
a = size(x)
|
|
11
|
+
b = size(y)
|
|
12
|
+
a >= b ? nest(x, y, a == b, ys) : nest(y, x, false, xs)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def combine(atoms, starts)
|
|
16
|
+
return atoms[0] if atoms.size == 1
|
|
17
|
+
return pair(atoms[0], starts[0], atoms[1], starts[1]) if atoms.size == 2
|
|
18
|
+
atoms.map! { fuzzy_part?(_1) ? _1.node : _1 } if atoms.any? { Clock === _1 }
|
|
19
|
+
sizes = atoms.map { size(_1) }
|
|
20
|
+
# coarsest first, in the order said among equals; an insertion sort, as there are only a few
|
|
21
|
+
order = []
|
|
22
|
+
j = 0
|
|
23
|
+
while j < sizes.size
|
|
24
|
+
k = order.size
|
|
25
|
+
k -= 1 while k > 0 && sizes[order[k - 1]] < sizes[j]
|
|
26
|
+
order.insert(k, j)
|
|
27
|
+
j += 1
|
|
28
|
+
end
|
|
29
|
+
node = atoms[order[0]]
|
|
30
|
+
o = 1
|
|
31
|
+
while o < order.size
|
|
32
|
+
j = order[o]
|
|
33
|
+
node = nest(node, atoms[j], sizes[j] == sizes[order[o - 1]], starts[j])
|
|
34
|
+
if Failure === node
|
|
35
|
+
# The atoms nested so far conflict whatever follows the last said of them, so a shorter try (Grammar.longest)
|
|
36
|
+
# need not keep the atoms after it
|
|
37
|
+
last = order[0..o].max + 1
|
|
38
|
+
@stuck = starts[last] if last < starts.size
|
|
39
|
+
return node
|
|
40
|
+
end
|
|
41
|
+
o += 1
|
|
42
|
+
end
|
|
43
|
+
node
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# a inside node, the coarser; same: the two are the same size, which conflicts but for a weekday and a date
|
|
47
|
+
def nest(node, a, same, start)
|
|
48
|
+
if (Shift === node || Shift === a) && (r = shifted(node, a))
|
|
49
|
+
return r
|
|
50
|
+
end
|
|
51
|
+
if same && !(Weekday === a && Date === node && node.day) && !(Date === a && a.day && Weekday === node)
|
|
52
|
+
return failure(:conflict, start)
|
|
53
|
+
end
|
|
54
|
+
return failure(:conflict, start) if Unit === a || Unit === node
|
|
55
|
+
# D37: a weekday with a date only picks among the date's readings
|
|
56
|
+
Weekday === node && Date === a ? Within.new(node, a) : Within.new(a, node)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# D44: "early morning at 8:00": beside a clock, early or late adds nothing to what the part says, its half
|
|
60
|
+
# of the day
|
|
61
|
+
def fuzzy_part?(a) = Fuzzy === a && Part === a.node
|
|
62
|
+
|
|
63
|
+
# D44: a shift from now beside a day moves a day that is already fixed ("3 years ago tomorrow"); one that
|
|
64
|
+
# recurs is within it, which resolve reads as the one in the period around the shifted instant
|
|
65
|
+
def shifted(x, y)
|
|
66
|
+
s, d = Shift === x ? [x, y] : [y, x]
|
|
67
|
+
return if s.anchor || Shift === d || UNIT_SIZE[s.unit] < 2 || size(d) < 2
|
|
68
|
+
return Shift.new(s.count, s.unit, d) if DayRef === d || Rel === d || (Date === d && d.year) || UNIT_SIZE[s.unit] == 2
|
|
69
|
+
Within.new(d, s)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# "23 oct" + "2026", or "10/23" + "2026", is one date
|
|
73
|
+
def merge_year(atoms, starts)
|
|
74
|
+
y = atoms.index { |a| Date === a && a.year && a.month.nil? && a.sep.nil? }
|
|
75
|
+
return unless y
|
|
76
|
+
d = atoms.index { |a| Date === a && a.year.nil? && a.month }
|
|
77
|
+
return unless d
|
|
78
|
+
atoms[d] = Date.new(atoms[y].year, atoms[d].month, atoms[d].day, atoms[d].sep, atoms[y].short)
|
|
79
|
+
atoms.delete_at(y)
|
|
80
|
+
starts.delete_at(y)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# How coarse a constituent is, for nesting: clock 0, part 1, day 2, week 3, month 5, season 6, year 7
|
|
84
|
+
def size(a)
|
|
85
|
+
case KIND[a.class]
|
|
86
|
+
when :clock, :approx then 0
|
|
87
|
+
when :part then 1
|
|
88
|
+
when :day_ref, :weekday, :holiday then 2
|
|
89
|
+
when :weekend, :iso_week then 3
|
|
90
|
+
when :date then a.day ? 2 : a.month ? 5 : 7
|
|
91
|
+
when :season then 6
|
|
92
|
+
when :fiscal then 7
|
|
93
|
+
when :unit, :years then UNIT_SIZE[a.unit]
|
|
94
|
+
when :rel then size(a.node)
|
|
95
|
+
when :fuzzy then size(a.node) - 1
|
|
96
|
+
when :nth then Unit === a.node ? UNIT_SIZE[a.node.unit] : size(a.node)
|
|
97
|
+
when :shift then UNIT_SIZE[a.unit] >= 2 ? 2 : 0
|
|
98
|
+
when :within then size(a.inner)
|
|
99
|
+
when :range then size(a.from)
|
|
100
|
+
else 2
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|