attentive 0.1.1 → 0.2.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 +4 -4
- data/README.md +170 -15
- data/lib/attentive.rb +19 -0
- data/lib/attentive/abbreviations.rb +1 -1
- data/lib/attentive/composite_entity.rb +4 -5
- data/lib/attentive/config.rb +2 -0
- data/lib/attentive/cursor.rb +35 -3
- data/lib/attentive/entities/core.rb +3 -0
- data/lib/attentive/entities/core/date.rb +6 -0
- data/lib/attentive/entities/core/date/month.rb +7 -0
- data/lib/attentive/entities/core/date/relative.rb +6 -0
- data/lib/attentive/entities/core/date/relative/future.rb +27 -0
- data/lib/attentive/entities/core/date/relative/past.rb +24 -0
- data/lib/attentive/entities/core/date/wday.rb +7 -0
- data/lib/attentive/entities/core/email.rb +8 -0
- data/lib/attentive/entities/core/number.rb +8 -0
- data/lib/attentive/entities/core/number/float.rb +6 -0
- data/lib/attentive/entities/core/number/float/negative.rb +6 -0
- data/lib/attentive/entities/core/number/float/positive.rb +6 -0
- data/lib/attentive/entities/core/number/integer.rb +6 -0
- data/lib/attentive/entities/core/number/integer/negative.rb +5 -0
- data/lib/attentive/entities/core/number/integer/positive.rb +5 -0
- data/lib/attentive/entities/core/number/negative.rb +6 -0
- data/lib/attentive/entities/core/number/positive.rb +6 -0
- data/lib/attentive/entity.rb +37 -10
- data/lib/attentive/listener.rb +2 -2
- data/lib/attentive/matcher.rb +24 -35
- data/lib/attentive/token.rb +12 -0
- data/lib/attentive/tokenizer.rb +153 -108
- data/lib/attentive/tokens.rb +2 -2
- data/lib/attentive/tokens/any_of.rb +3 -3
- data/lib/attentive/tokens/regexp.rb +19 -2
- data/lib/attentive/version.rb +1 -1
- metadata +19 -4
- data/lib/attentive/entities/integer.rb +0 -5
- data/lib/attentive/entities/relative_date.rb +0 -44
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b5952398cd8d68e82b27a4e9be335b748b315845
|
4
|
+
data.tar.gz: 77f1e15ef3e6952861d110f3c5d7605664e2979f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 49de7e3eff7a8964082ffd35ac964497c5a7c706194f160fcb29bad5710c6977fc1d58ff86189d788cbe8290145575b07147cb35cafcbe16167b10607cd29063
|
7
|
+
data.tar.gz: b7692af23cbaa6b44467ab64c989883477ade8c1a9e26133c6c27e69168dbd132ba4e6dc7b4abbd8a8b5efeda09714a3138181e3e49290d16f449320f7e4d7d5
|
data/README.md
CHANGED
@@ -1,43 +1,196 @@
|
|
1
1
|
# Attentive
|
2
2
|
|
3
|
-
|
3
|
+
Attentive is a library for matching messages to natural-language listeners.
|
4
4
|
|
5
|
-
TODO: Delete this and the text above, and describe your gem
|
6
5
|
|
7
6
|
|
7
|
+
<br/>
|
8
|
+
## Usage
|
8
9
|
|
9
|
-
|
10
|
+
Its basic usage is like this:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
include Attentive
|
14
|
+
|
15
|
+
listen_for "hi", context: { in: :any } do
|
16
|
+
puts "nice to meet you!"
|
17
|
+
end
|
18
|
+
|
19
|
+
hear! "hi!" # => "nice to meet you!"
|
20
|
+
```
|
21
|
+
|
22
|
+
In the snippet above,
|
23
|
+
|
24
|
+
1. We defined a [listener](#listeners) that is active in any [context](#contexts).
|
25
|
+
2. We received a message.
|
26
|
+
3. Attentive matched the message to our listener and invoked the block.
|
10
27
|
|
11
|
-
|
28
|
+
|
29
|
+
<br/>
|
30
|
+
#### Optional Characters
|
31
|
+
|
32
|
+
You'll notice that we listened for `"hi"` but _heard_ `"hi!"`. Attentive treats punctuation and emojis as optional; but we can make them required by putting them in the listener:
|
12
33
|
|
13
34
|
```ruby
|
14
|
-
|
35
|
+
listen_for "hi!", context: { in: :any } do
|
36
|
+
puts "nice to meet you!"
|
37
|
+
end
|
38
|
+
|
39
|
+
hear! "hi" # => nothing happened, the listener is expecting the exclamation mark
|
15
40
|
```
|
16
41
|
|
17
|
-
|
42
|
+
> It's best to leave all but the most necessary punctuation out of listeners.
|
18
43
|
|
19
|
-
$ bundle
|
20
44
|
|
21
|
-
|
45
|
+
<br/>
|
46
|
+
#### Contractions and Abbreviations
|
22
47
|
|
23
|
-
|
48
|
+
Attentive understands contractions and abbreviations and can match those:
|
24
49
|
|
50
|
+
```ruby
|
51
|
+
listen_for "hi", context: { in: :any } do
|
52
|
+
puts "nice to meet you!"
|
53
|
+
end
|
25
54
|
|
55
|
+
hear! "hello!" # => "nice to meet you!"
|
56
|
+
|
57
|
+
listen_for "what is for lunch", context: { in: :any } do
|
58
|
+
puts "HAMBURGERS!"
|
59
|
+
end
|
60
|
+
|
61
|
+
hear! "what's for lunch?" # => "HAMBURGERS!"
|
62
|
+
```
|
63
|
+
|
64
|
+
> Although you _can_ use contractions and abbreviations in listeners, it's a good habit not to. Attentive will not let you define listeners that use ambiguous contractions like `"where's"` (`"where's"` might be a contraction for `"where is"`, `"where does"`, or `"where has"`, or `"where was"`).
|
26
65
|
|
27
|
-
|
66
|
+
|
67
|
+
<br/>
|
68
|
+
#### Listeners
|
69
|
+
|
70
|
+
Listeners are defined with three things:
|
71
|
+
|
72
|
+
1. One or more phrases
|
73
|
+
2. A set of [contexts](#contexts) where they're active
|
74
|
+
3. A block to be invoked when the listener is matched
|
75
|
+
|
76
|
+
Here's an example of a listener that matches more than one phrase:
|
28
77
|
|
29
78
|
```ruby
|
30
|
-
|
79
|
+
listen_for "what is for lunch",
|
80
|
+
"what is for lunch {{date:core.date.relative.future}}",
|
81
|
+
"what is for lunch on {{date:core.date}}",
|
82
|
+
"show me the menu for {{date:core.date.relative.future}}",
|
83
|
+
"show me the menu for {{date:core.date}}" do
|
84
|
+
# ...
|
85
|
+
end
|
86
|
+
```
|
87
|
+
|
88
|
+
(In the example above, the phrases `{{date:core.date.relative.future}}` and `{{date:core.date}}` are [entities](#entities): which we'll cover in a minute.)
|
89
|
+
|
90
|
+
|
91
|
+
<br/>
|
92
|
+
#### Contexts
|
93
|
+
|
94
|
+
A listener can require that messages be heard in a certain context in order to be matched or it can ignore messages if they are heard in certain contexts.
|
31
95
|
|
32
|
-
|
33
|
-
|
96
|
+
The following is a listener that will only match messages heard in the "#general" channel and only then if the conversation is not "serious".
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
listen_for "ouch", context: { in: %i{general}, not_in: %i{serious} } do
|
100
|
+
puts "On a scale of 1 to 10, how would you rate your pain?"
|
34
101
|
end
|
35
102
|
|
36
|
-
hear "
|
103
|
+
hear! "ouch" # => message has no context, listener isn't triggered
|
104
|
+
hear! "ouch", contexts: %i{general} # => "On a scale of 1 to 10..."
|
105
|
+
hear! "ouch", contexts: %i{general serious} # => listener ignores "serious" messages
|
37
106
|
```
|
38
107
|
|
108
|
+
If you don't specify context requirements for listeners, Attentive requires `conversation` and prohibits `quotation` by default:
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
# These two are the same:
|
112
|
+
listen_for "ouch"
|
113
|
+
listen_for "ouch", context: { in: %i{conversation}, not_in: %i{quotation} }
|
114
|
+
```
|
115
|
+
|
116
|
+
|
117
|
+
|
118
|
+
<br/>
|
119
|
+
#### Entities
|
120
|
+
|
121
|
+
Entities allow Attentive to match **_concepts_** rather than specific words.
|
122
|
+
|
123
|
+
There are built-in entities like `core.date`, `core.number`, and `core.email` for recognizing dates, numbers, and email addresses (see [Core Entities](https://github.com/houston/attentive/wiki/Core-Entities) for a complete list); but you can also define entities for domain-specific concepts. For example:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
Attentive::Entity.define "deweys.menu.beers",
|
127
|
+
"Bell's Oberon",
|
128
|
+
"Rogue Dead Guy Ale",
|
129
|
+
"Schalfly Dry Hopped IPA",
|
130
|
+
"4 Hands Contact High",
|
131
|
+
"Scrimshaw Pilsner"
|
132
|
+
```
|
133
|
+
|
134
|
+
Now we can take drink orders:
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
listen_for "I will have a pint of the {{deweys.menu.beers}}" do
|
138
|
+
puts "Good choice"
|
139
|
+
end
|
140
|
+
```
|
141
|
+
|
142
|
+
> It is a good idea to namespace entities (i.e. `deweys.menu.beers`). Attentive's convention is to treat namespaces as a taxonomy for concepts.
|
143
|
+
|
144
|
+
|
145
|
+
<br/>
|
146
|
+
#### Regular Expressions
|
147
|
+
|
148
|
+
As useful as enumerations are, entities can also be defined with regular expressions and with a block that converts the matched part of the message to a more useful value:
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
# Usernames can be up to 21 characters long.
|
152
|
+
# They can contain lowercase letters a to z
|
153
|
+
# (without accents), and numbers 0 to 9.
|
154
|
+
|
155
|
+
Attentive::Entity.define "slack.user", %q{(?<username>[a-z0-9]{1,21})} do |match|
|
156
|
+
Slack::User.find match["username"]
|
157
|
+
end
|
158
|
+
```
|
159
|
+
|
160
|
+
> Whenever possible, though, prefer composing entities to using regular expressions.
|
161
|
+
> For example:
|
162
|
+
> ```ruby
|
163
|
+
Attentive::Entity.define "core.date.relative.future",
|
164
|
+
"next {{core.date.wday}}"
|
165
|
+
```
|
166
|
+
> is better than:
|
167
|
+
> ```ruby
|
168
|
+
Attentive::Entity.define "core.date.relative.future",
|
169
|
+
"next (?<weekday>(:sun|mon|tues|wednes|thurs|fri|satur)day)"
|
170
|
+
```
|
171
|
+
|
172
|
+
|
173
|
+
|
174
|
+
<br/>
|
175
|
+
## Installation
|
176
|
+
|
177
|
+
Add this line to your application's Gemfile:
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
gem 'attentive'
|
181
|
+
```
|
182
|
+
|
183
|
+
And then execute:
|
184
|
+
|
185
|
+
$ bundle
|
186
|
+
|
187
|
+
Or install it yourself as:
|
188
|
+
|
189
|
+
$ gem install attentive
|
190
|
+
|
39
191
|
|
40
192
|
|
193
|
+
<br/>
|
41
194
|
## Development
|
42
195
|
|
43
196
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -46,12 +199,14 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
46
199
|
|
47
200
|
|
48
201
|
|
202
|
+
<br/>
|
49
203
|
## Contributing
|
50
204
|
|
51
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
205
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/houston/attentive.
|
52
206
|
|
53
207
|
|
54
208
|
|
209
|
+
<br/>
|
55
210
|
## License
|
56
211
|
|
57
212
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/lib/attentive.rb
CHANGED
@@ -7,6 +7,14 @@ module Attentive
|
|
7
7
|
# Default configuration
|
8
8
|
self.invocations = ["@me".freeze]
|
9
9
|
|
10
|
+
# Default contexts that listeners will require
|
11
|
+
# a message to be heard in.
|
12
|
+
self.default_required_contexts = %i{conversation}
|
13
|
+
|
14
|
+
# Default contexts in which listeners will ignore messages.
|
15
|
+
self.default_prohibited_contexts = %i{quotation}
|
16
|
+
|
17
|
+
|
10
18
|
|
11
19
|
# Attentive DSL
|
12
20
|
|
@@ -18,11 +26,22 @@ module Attentive
|
|
18
26
|
listeners.listen_for(*args, &block)
|
19
27
|
end
|
20
28
|
|
29
|
+
# Matches a message against all listeners
|
30
|
+
# and returns an array of matches
|
21
31
|
def hear(message, params={})
|
22
32
|
message = Attentive::Message.new(message, params) unless message.is_a?(Attentive::Message)
|
23
33
|
listeners.hear message
|
24
34
|
end
|
25
35
|
|
36
|
+
# Matches a message against all listeners
|
37
|
+
# and invokes the first listener that mathes
|
38
|
+
def hear!(message, params={})
|
39
|
+
hear(message, params).each do |match|
|
40
|
+
match.listener.call(match)
|
41
|
+
return
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
26
45
|
end
|
27
46
|
|
28
47
|
require "attentive/listener_collection"
|
@@ -1,3 +1,3 @@
|
|
1
1
|
module Attentive
|
2
|
-
ABBREVIATIONS = {"bye"=>"goodbye", "gonna"=>"going to", "hi"=>"hello", "ol'"=>"old", "'sup"=>"what is up", "thanks"=>"thank you", "wanna"=>"want to", "mon"=>"monday", "tue"=>"tuesday", "tues"=>"tuesday", "wed"=>"wednesday", "thu"=>"thursday", "thur"=>"thursday", "thurs"=>"thursday", "fri"=>"friday", "sat"=>"saturday", "sun"=>"sunday"}.freeze
|
2
|
+
ABBREVIATIONS = {"bye"=>"goodbye", "gonna"=>"going to", "hi"=>"hello", "ol'"=>"old", "'sup"=>"what is up", "thanks"=>"thank you", "wanna"=>"want to", "mon"=>"monday", "tue"=>"tuesday", "tues"=>"tuesday", "wed"=>"wednesday", "thu"=>"thursday", "thur"=>"thursday", "thurs"=>"thursday", "fri"=>"friday", "sat"=>"saturday", "sun"=>"sunday", "jan"=>"january", "feb"=>"february", "mar"=>"march", "apr"=>"april", "jun"=>"june", "jul"=>"july", "aug"=>"august", "sep"=>"september", "sept"=>"september", "oct"=>"october", "nov"=>"november", "dec"=>"december"}.freeze
|
3
3
|
end
|
@@ -9,10 +9,9 @@ module Attentive
|
|
9
9
|
attr_accessor :entities
|
10
10
|
|
11
11
|
def define(entity_name, *entities)
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
Entity.register! entity_name, entity_klass
|
12
|
+
create! entity_name do |entity_klass|
|
13
|
+
entity_klass.entities = entities.map { |entity| Entity[entity] }
|
14
|
+
end
|
16
15
|
end
|
17
16
|
end
|
18
17
|
|
@@ -23,7 +22,7 @@ module Attentive
|
|
23
22
|
|
24
23
|
def matches?(cursor)
|
25
24
|
entities.each do |entity|
|
26
|
-
match = entity.matches?(cursor
|
25
|
+
match = entity.matches?(cursor)
|
27
26
|
return match if match
|
28
27
|
end
|
29
28
|
false
|
data/lib/attentive/config.rb
CHANGED
data/lib/attentive/cursor.rb
CHANGED
@@ -8,21 +8,53 @@ module Attentive
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def peek
|
11
|
-
tokens[pos]
|
11
|
+
tokens[pos] || EOF
|
12
12
|
end
|
13
13
|
|
14
14
|
def pop
|
15
|
-
|
16
|
-
|
15
|
+
peek.tap do
|
16
|
+
advance
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def new_from_here
|
21
|
+
self.class.new(tokens[pos..-1])
|
17
22
|
end
|
18
23
|
|
19
24
|
def to_s
|
20
25
|
tokens[pos..-1].join
|
21
26
|
end
|
22
27
|
|
28
|
+
def inspect
|
29
|
+
"|#{(tokens[0...pos] || []).join}\e[7m#{tokens[pos]}\e[0m#{(tokens[(pos + 1)..-1] || []).join}|"
|
30
|
+
end
|
31
|
+
|
23
32
|
def offset
|
24
33
|
peek.pos
|
25
34
|
end
|
26
35
|
|
36
|
+
def advance(n=1)
|
37
|
+
@pos += n
|
38
|
+
end
|
39
|
+
|
40
|
+
def eof?
|
41
|
+
@pos == @tokens.length
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
class Eof
|
49
|
+
def whitespace?
|
50
|
+
false
|
51
|
+
end
|
52
|
+
|
53
|
+
def eof?
|
54
|
+
true
|
55
|
+
end
|
56
|
+
end
|
57
|
+
EOF = Eof.new.freeze
|
58
|
+
|
27
59
|
end
|
28
60
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "attentive/entity"
|
2
|
+
require "date"
|
3
|
+
|
4
|
+
Attentive::Entity.define "core.date.relative.future",
|
5
|
+
"today",
|
6
|
+
"tomorrow",
|
7
|
+
"{{core.date.wday}}",
|
8
|
+
"next {{core.date.wday}}" do |match|
|
9
|
+
|
10
|
+
today = Date.today
|
11
|
+
|
12
|
+
if match.matched?("core.date.wday")
|
13
|
+
wday = match["core.date.wday"]
|
14
|
+
days_until_wday = wday - today.wday
|
15
|
+
days_until_wday += 7 if days_until_wday < 0
|
16
|
+
date = today + days_until_wday
|
17
|
+
|
18
|
+
date += 7 if match.to_s.start_with?("next")
|
19
|
+
date
|
20
|
+
else
|
21
|
+
case match.to_s
|
22
|
+
when "today" then today
|
23
|
+
when "tomorrow" then today + 1
|
24
|
+
else raise NotImplementedError, "Unrecognized match: #{match.to_s}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require "attentive/entity"
|
2
|
+
require "date"
|
3
|
+
|
4
|
+
Attentive::Entity.define "core.date.relative.past",
|
5
|
+
"today",
|
6
|
+
"yesterday",
|
7
|
+
"{{core.date.wday}}",
|
8
|
+
"last {{core.date.wday}}" do |match|
|
9
|
+
|
10
|
+
today = Date.today
|
11
|
+
|
12
|
+
if match.matched?("core.date.wday")
|
13
|
+
wday = match["core.date.wday"]
|
14
|
+
days_since_wday = today.wday - wday
|
15
|
+
days_since_wday += 7 if days_since_wday < 0
|
16
|
+
today - days_since_wday
|
17
|
+
else
|
18
|
+
case match.to_s
|
19
|
+
when "today" then today
|
20
|
+
when "yesterday" then today - 1
|
21
|
+
else raise NotImplementedError, "Unrecognized match: #{match.to_s}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
require "attentive/entity"
|
2
|
+
|
3
|
+
# Email regex asserts that there are no @ symbols or whitespaces in either the
|
4
|
+
# localpart or the domain, and that there is a single @ symbol separating the
|
5
|
+
# localpart and the domain.
|
6
|
+
Attentive::Entity.define "core.email", %q{(?<email>[^@\s]+@[^@\s]+)} do |match|
|
7
|
+
match["email"]
|
8
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
require "attentive/entities/core/number/integer"
|
2
|
+
require "attentive/entities/core/number/float"
|
3
|
+
require "attentive/entities/core/number/positive"
|
4
|
+
require "attentive/entities/core/number/negative"
|
5
|
+
|
6
|
+
Attentive::CompositeEntity.define "core.number",
|
7
|
+
"core.number.float",
|
8
|
+
"core.number.integer"
|
data/lib/attentive/entity.rb
CHANGED
@@ -12,25 +12,44 @@ module Attentive
|
|
12
12
|
attr_accessor :token_name
|
13
13
|
|
14
14
|
def [](entity_name)
|
15
|
+
entity_name = entity_name.to_sym
|
15
16
|
@entities.fetch(entity_name)
|
16
17
|
rescue KeyError
|
17
18
|
raise Attentive::UndefinedEntityError.new("Undefined Entity #{entity_name.inspect}")
|
18
19
|
end
|
19
20
|
|
20
21
|
def define(entity_name, *phrases, &block)
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
create! entity_name do |entity_klass|
|
23
|
+
entity_klass.phrases = phrases.map do |phrase|
|
24
|
+
Attentive::Tokenizer.tokenize(phrase, entities: true, regexps: true, ambiguous: false)
|
25
|
+
end
|
26
|
+
entity_klass.send :define_method, :_value_from_match, &block if block_given?
|
25
27
|
end
|
26
|
-
|
27
|
-
|
28
|
+
end
|
29
|
+
|
30
|
+
def undefine(entity_name)
|
31
|
+
entity_symbol = entity_name.to_sym
|
32
|
+
unregister! entity_symbol
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
def create!(entity_name)
|
38
|
+
entity_symbol = entity_name.to_sym
|
39
|
+
entity_klass = Class.new(self)
|
40
|
+
entity_klass.token_name = entity_symbol
|
41
|
+
yield entity_klass
|
42
|
+
Entity.register! entity_symbol, entity_klass
|
28
43
|
end
|
29
44
|
|
30
45
|
def register!(entity_name, entity_klass)
|
31
|
-
#
|
46
|
+
raise ArgumentError, "Entity #{entity_name.inspect} has already been defined" if @entities.key?(entity_name)
|
32
47
|
@entities[entity_name] = entity_klass
|
33
48
|
end
|
49
|
+
|
50
|
+
def unregister!(entity_name)
|
51
|
+
@entities.delete entity_name
|
52
|
+
end
|
34
53
|
end
|
35
54
|
|
36
55
|
|
@@ -46,7 +65,11 @@ module Attentive
|
|
46
65
|
end
|
47
66
|
|
48
67
|
def to_s
|
49
|
-
|
68
|
+
if variable_name.to_s == self.class.token_name.to_s
|
69
|
+
"{{#{self.class.token_name}}}"
|
70
|
+
else
|
71
|
+
"{{#{variable_name}:#{self.class.token_name}}}"
|
72
|
+
end
|
50
73
|
end
|
51
74
|
|
52
75
|
def entity?
|
@@ -55,15 +78,19 @@ module Attentive
|
|
55
78
|
|
56
79
|
def matches?(cursor)
|
57
80
|
self.class.phrases.each do |phrase|
|
58
|
-
cursor_copy = cursor.
|
81
|
+
cursor_copy = cursor.new_from_here
|
59
82
|
match = Attentive::Matcher.new(phrase, cursor_copy).match!
|
60
83
|
if match
|
61
|
-
cursor.
|
84
|
+
cursor.advance cursor_copy.pos
|
62
85
|
return { variable_name => _value_from_match(match) }
|
63
86
|
end
|
64
87
|
end
|
65
88
|
false
|
66
89
|
end
|
67
90
|
|
91
|
+
def _value_from_match(match)
|
92
|
+
match.to_s
|
93
|
+
end
|
94
|
+
|
68
95
|
end
|
69
96
|
end
|
data/lib/attentive/listener.rb
CHANGED
@@ -8,10 +8,10 @@ module Attentive
|
|
8
8
|
|
9
9
|
def initialize(listeners, phrases, options, callback)
|
10
10
|
context_options = options.fetch(:context, {})
|
11
|
-
@required_contexts = context_options.fetch(:in,
|
11
|
+
@required_contexts = context_options.fetch(:in, Attentive.default_required_contexts)
|
12
12
|
@required_contexts = [] if @required_contexts == :any
|
13
13
|
@required_contexts = Set[*@required_contexts]
|
14
|
-
@prohibited_contexts = context_options.fetch(:not_in,
|
14
|
+
@prohibited_contexts = context_options.fetch(:not_in, Attentive.default_prohibited_contexts)
|
15
15
|
@prohibited_contexts = Set[*@prohibited_contexts]
|
16
16
|
|
17
17
|
@listeners = listeners
|
data/lib/attentive/matcher.rb
CHANGED
@@ -2,16 +2,21 @@ require "attentive/match"
|
|
2
2
|
|
3
3
|
module Attentive
|
4
4
|
class Matcher
|
5
|
-
attr_reader :phrase, :
|
5
|
+
attr_reader :phrase, :message, :cursor
|
6
6
|
|
7
|
-
def initialize(phrase,
|
7
|
+
def initialize(phrase, message, params={})
|
8
8
|
@phrase = phrase
|
9
|
-
@cursor =
|
10
|
-
@
|
9
|
+
@cursor = Cursor.new(phrase, params.fetch(:pos, 0))
|
10
|
+
@message = message
|
11
11
|
@match_params = params.each_with_object({}) { |(key, value), new_hash| new_hash[key] = value if %i{listener message}.member?(key) }
|
12
|
-
@pos += 1 while phrase[pos] && phrase[pos].whitespace?
|
13
12
|
@match_data = {}
|
14
13
|
@state = :matching
|
14
|
+
|
15
|
+
cursor.pop while cursor.peek.whitespace?
|
16
|
+
end
|
17
|
+
|
18
|
+
def pos
|
19
|
+
cursor.pos
|
15
20
|
end
|
16
21
|
|
17
22
|
def matching?
|
@@ -23,57 +28,41 @@ module Attentive
|
|
23
28
|
end
|
24
29
|
|
25
30
|
def match!
|
26
|
-
|
31
|
+
until (token = message.peek).eof?
|
27
32
|
if token.ambiguous?
|
28
|
-
unless
|
33
|
+
unless match_any!(token.possibilities)
|
29
34
|
@state = :mismatch
|
30
35
|
break
|
31
36
|
end
|
32
|
-
|
37
|
+
cursor.pop while cursor.peek.whitespace?
|
33
38
|
|
34
|
-
elsif match_data =
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
# Advance the cursor to the first token after the regexp match
|
40
|
-
cursor_pos = cursor.tokens.index { |token| token.pos >= new_character_index }
|
41
|
-
cursor_pos = cursor.tokens.length unless cursor_pos
|
42
|
-
cursor.instance_variable_set :@pos, cursor_pos
|
43
|
-
@pos += 1
|
44
|
-
else
|
45
|
-
@match_data.merge!(match_data) unless match_data == true
|
46
|
-
@pos += 1
|
47
|
-
end
|
48
|
-
@pos += 1 while phrase[pos] && phrase[pos].whitespace?
|
39
|
+
elsif match_data = cursor.peek.matches?(message)
|
40
|
+
@match_data.merge!(match_data) unless match_data == true
|
41
|
+
cursor.pop
|
42
|
+
cursor.pop while cursor.peek.whitespace?
|
49
43
|
@state = :found
|
50
44
|
|
51
|
-
|
52
45
|
# -> This is the one spot where we instantiate a Match
|
53
|
-
return Attentive::Match.new(phrase, @match_params.merge(match_data: @match_data)) if
|
46
|
+
return Attentive::Match.new(phrase, @match_params.merge(match_data: @match_data)) if cursor.eof?
|
54
47
|
|
55
48
|
elsif !token.skippable?
|
56
49
|
@state = :mismatch
|
57
50
|
break
|
58
51
|
end
|
59
52
|
|
60
|
-
|
61
|
-
|
62
|
-
while cursor.peek.whitespace?
|
63
|
-
cursor.pop
|
64
|
-
break unless cursor.peek
|
65
|
-
end
|
53
|
+
message.pop
|
54
|
+
message.pop while message.peek.whitespace?
|
66
55
|
end
|
67
56
|
|
68
57
|
nil
|
69
58
|
end
|
70
59
|
|
71
|
-
def
|
72
|
-
|
73
|
-
matcher = Matcher.new(phrase, Cursor.new(
|
60
|
+
def match_any!(messages)
|
61
|
+
messages.each do |message|
|
62
|
+
matcher = Matcher.new(phrase[pos..-1], Cursor.new(message))
|
74
63
|
matcher.match!
|
75
64
|
unless matcher.mismatch?
|
76
|
-
|
65
|
+
cursor.advance matcher.pos
|
77
66
|
return true
|
78
67
|
end
|
79
68
|
end
|
data/lib/attentive/token.rb
CHANGED
@@ -26,10 +26,18 @@ module Attentive
|
|
26
26
|
false
|
27
27
|
end
|
28
28
|
|
29
|
+
def eof?
|
30
|
+
false
|
31
|
+
end
|
32
|
+
|
29
33
|
def matches?(cursor)
|
30
34
|
self == cursor.peek
|
31
35
|
end
|
32
36
|
|
37
|
+
def inspect
|
38
|
+
"<#{self.class.name ? self.class.name.split("::").last : "Entity"} #{to_s.inspect}>"
|
39
|
+
end
|
40
|
+
|
33
41
|
end
|
34
42
|
|
35
43
|
|
@@ -50,6 +58,10 @@ module Attentive
|
|
50
58
|
string
|
51
59
|
end
|
52
60
|
|
61
|
+
def length
|
62
|
+
string.length
|
63
|
+
end
|
64
|
+
|
53
65
|
def ==(other)
|
54
66
|
self.class == other.class && self.string == other.string
|
55
67
|
end
|
data/lib/attentive/tokenizer.rb
CHANGED
@@ -8,141 +8,187 @@ require "attentive/errors"
|
|
8
8
|
|
9
9
|
module Attentive
|
10
10
|
class Tokenizer
|
11
|
-
|
12
|
-
|
13
|
-
# Splits apart words and punctuation,
|
14
|
-
# treats apostrophes and dashes as a word-characters,
|
15
|
-
# trims each fragment of whitepsace
|
16
|
-
# SPLITTER = /\s*([\w'-]+)\s*/.freeze
|
17
|
-
SPLITTER = /(\n|{{|}}|\s+|\.{2,}|[^\s\w'@-])/.freeze
|
18
|
-
PUNCTUATION = /^\W+$/.freeze
|
19
|
-
WHITESPACE = /^\s+$/.freeze
|
20
|
-
ENTITY_START = "{{".freeze
|
21
|
-
ENTITY_END = "}}".freeze
|
22
|
-
REGEXP_START = "(".freeze
|
23
|
-
REGEXP_END = ")".freeze
|
24
|
-
REGEXP_ESCAPE = "\\".freeze
|
11
|
+
include Attentive::Tokens
|
25
12
|
|
13
|
+
attr_reader :message, :chars, :options
|
26
14
|
|
27
|
-
def self.
|
28
|
-
|
15
|
+
def self.tokenize(message, options={})
|
16
|
+
self.new(message, options).tokenize
|
29
17
|
end
|
30
18
|
|
31
19
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
20
|
+
|
21
|
+
def initialize(message, options={})
|
22
|
+
@message = Attentive::Text.normalize(message)
|
23
|
+
@chars = self.message.each_char.to_a
|
24
|
+
@options = options
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
|
29
|
+
def tokenize
|
38
30
|
i = 0
|
39
|
-
|
40
|
-
while i <
|
41
|
-
|
42
|
-
case string
|
43
|
-
when ""
|
44
|
-
# do nothing
|
45
|
-
|
46
|
-
when WHITESPACE
|
47
|
-
tokens << whitespace(string, pos: pos)
|
48
|
-
|
49
|
-
when ":"
|
50
|
-
if strings[i + 2] == ":"
|
51
|
-
tokens << emoji(strings[i + 1], pos: pos)
|
52
|
-
pos += strings[i + 1].length + 1
|
53
|
-
i += 2
|
54
|
-
else
|
55
|
-
tokens << punctuation(":", pos: pos)
|
56
|
-
end
|
31
|
+
tokens = []
|
32
|
+
while i < chars.length
|
33
|
+
char = chars[i]
|
57
34
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
break
|
70
|
-
end
|
71
|
-
j += 1
|
72
|
-
end
|
73
|
-
next if found_entity
|
74
|
-
end
|
75
|
-
tokens << punctuation(ENTITY_START, pos: pos)
|
76
|
-
|
77
|
-
when REGEXP_START
|
78
|
-
if match_regexps && strings[i + 1] == "?"
|
79
|
-
j = i + 2
|
80
|
-
found_regexp = false
|
81
|
-
parens = 1
|
82
|
-
inside_square_bracket = false
|
83
|
-
while j < strings.length
|
84
|
-
if strings[j] == "[" && strings[j - 1] != REGEXP_ESCAPE
|
85
|
-
inside_square_bracket = true
|
86
|
-
elsif strings[j] == "]" && strings[j - 1] != REGEXP_ESCAPE
|
87
|
-
inside_square_bracket = false
|
88
|
-
end
|
89
|
-
|
90
|
-
unless inside_square_bracket
|
91
|
-
if strings[j] == REGEXP_START && strings[j - 1] != REGEXP_ESCAPE
|
92
|
-
parens += 1
|
93
|
-
elsif strings[j] == REGEXP_END && strings[j - 1] != REGEXP_ESCAPE
|
94
|
-
parens -= 1
|
95
|
-
end
|
96
|
-
|
97
|
-
if parens == 0
|
98
|
-
tokens << regexp(strings[i..j].join, pos: pos)
|
99
|
-
pos += strings[i..j].join.length + 2
|
100
|
-
i = j + 1
|
101
|
-
found_regexp = true
|
102
|
-
break
|
103
|
-
end
|
104
|
-
end
|
105
|
-
j += 1
|
106
|
-
end
|
107
|
-
next if found_regexp
|
108
|
-
end
|
109
|
-
tokens << punctuation(REGEXP_START, pos: pos)
|
35
|
+
if EMOJI_START === char && string = match_emoji_at(i)
|
36
|
+
tokens << emoji(string, pos: i)
|
37
|
+
i += string.length + 2
|
38
|
+
|
39
|
+
elsif ENTITY_START === char && string = match_entity_at(i)
|
40
|
+
tokens << entity(*string.split(":").reverse, pos: i)
|
41
|
+
i += string.length + 4
|
42
|
+
|
43
|
+
elsif REGEXP_START === char && string = match_regexp_at(i)
|
44
|
+
tokens << regexp(string, pos: i)
|
45
|
+
i += string.length
|
110
46
|
|
111
|
-
|
112
|
-
tokens <<
|
47
|
+
elsif WHITESPACE === char && string = match_whitespace_at(i)
|
48
|
+
tokens << whitespace(string, pos: i)
|
49
|
+
i += string.length
|
113
50
|
|
114
|
-
|
115
|
-
tokens <<
|
51
|
+
elsif NUMBER_START === char && string = match_number_at(i)
|
52
|
+
tokens << word(string, pos: i)
|
53
|
+
i += string.length
|
54
|
+
|
55
|
+
elsif PUNCTUATION === char # =~ /\W/
|
56
|
+
tokens << punctuation(char, pos: i)
|
57
|
+
i += 1
|
116
58
|
|
117
59
|
else
|
118
|
-
|
119
|
-
|
60
|
+
string = match_word_at(i)
|
61
|
+
if Attentive.invocations.member?(string)
|
62
|
+
tokens << invocation(string, pos: i)
|
63
|
+
|
64
|
+
elsif replace_with = Attentive::ABBREVIATIONS[string]
|
65
|
+
tokens.concat self.class.tokenize(replace_with, options)
|
120
66
|
|
121
67
|
elsif expands_to = Attentive::CONTRACTIONS[string]
|
122
68
|
possibilities = expands_to.map do |possibility|
|
123
|
-
tokenize(possibility, options)
|
69
|
+
self.class.tokenize(possibility, options)
|
124
70
|
end
|
125
71
|
|
126
72
|
if possibilities.length == 1
|
127
73
|
tokens.concat possibilities[0]
|
128
74
|
else
|
129
|
-
tokens << any_of(possibilities, pos:
|
75
|
+
tokens << any_of(string, possibilities, pos: i)
|
130
76
|
end
|
77
|
+
|
131
78
|
else
|
132
|
-
tokens << word(string, pos:
|
79
|
+
tokens << word(string, pos: i)
|
133
80
|
end
|
81
|
+
i += string.length
|
134
82
|
end
|
83
|
+
end
|
84
|
+
|
85
|
+
fail_if_ambiguous!(message, tokens) if fail_if_ambiguous?
|
86
|
+
Attentive::Phrase.new(tokens)
|
87
|
+
end
|
88
|
+
|
89
|
+
|
135
90
|
|
136
|
-
|
137
|
-
|
91
|
+
def match_emoji_at(i)
|
92
|
+
emoji = ""
|
93
|
+
while (i += 1) < chars.length
|
94
|
+
return if_present?(emoji) if EMOJI_END === chars[i]
|
95
|
+
return false if WHITESPACE === chars[i]
|
96
|
+
emoji << chars[i]
|
138
97
|
end
|
98
|
+
false
|
99
|
+
end
|
139
100
|
|
140
|
-
|
101
|
+
def match_entity_at(i)
|
102
|
+
return false unless match_entities?
|
103
|
+
return false unless chars[i += 1] == "{"
|
104
|
+
entity = ""
|
105
|
+
while (i += 1) < chars.length
|
106
|
+
return if_present?(entity) if ["}", "}"] == chars[i, 2]
|
107
|
+
return false unless ENTITY === chars[i]
|
108
|
+
entity << chars[i]
|
109
|
+
end
|
110
|
+
false
|
111
|
+
end
|
141
112
|
|
142
|
-
|
113
|
+
def match_regexp_at(i)
|
114
|
+
return false unless match_regexps?
|
115
|
+
return false unless chars[i += 1] == "?"
|
116
|
+
regexp = "(?"
|
117
|
+
parens = 1
|
118
|
+
inside_square_bracket = false
|
119
|
+
while (i += 1) < chars.length
|
120
|
+
regexp << chars[i]
|
121
|
+
next if chars[i - 1] == "\\"
|
122
|
+
inside_square_bracket = true if chars[i] == "["
|
123
|
+
inside_square_bracket = false if chars[i] == "]"
|
124
|
+
next if inside_square_bracket
|
125
|
+
parens += 1 if chars[i] == "("
|
126
|
+
parens -= 1 if chars[i] == ")"
|
127
|
+
return if_present?(regexp) if parens == 0
|
128
|
+
end
|
129
|
+
false
|
130
|
+
end
|
131
|
+
|
132
|
+
def match_whitespace_at(i)
|
133
|
+
whitespace = chars[i]
|
134
|
+
while (i += 1) < chars.length
|
135
|
+
break unless WHITESPACE === chars[i]
|
136
|
+
whitespace << chars[i]
|
137
|
+
end
|
138
|
+
whitespace
|
139
|
+
end
|
140
|
+
|
141
|
+
def match_number_at(i)
|
142
|
+
return false if CONDITIONAL_NUMBER_START === chars[i] && !(NUMBER === chars[i + 1])
|
143
|
+
number = chars[i]
|
144
|
+
while (i += 1) < chars.length
|
145
|
+
break unless NUMBER === chars[i] || (CONDITIONAL_NUMBER === chars[i] && NUMBER === chars[i + 1])
|
146
|
+
number << chars[i]
|
147
|
+
end
|
148
|
+
number
|
149
|
+
end
|
150
|
+
|
151
|
+
def match_word_at(i)
|
152
|
+
word = chars[i]
|
153
|
+
while (i += 1) < chars.length
|
154
|
+
break unless WORD === chars[i]
|
155
|
+
word << chars[i]
|
156
|
+
end
|
157
|
+
word
|
158
|
+
end
|
159
|
+
|
160
|
+
def if_present?(string)
|
161
|
+
string.empty? ? false : string
|
162
|
+
end
|
163
|
+
|
164
|
+
|
165
|
+
|
166
|
+
def match_entities?
|
167
|
+
options.fetch(:entities, false)
|
143
168
|
end
|
144
169
|
|
145
|
-
def
|
170
|
+
def match_regexps?
|
171
|
+
options.fetch(:regexps, false)
|
172
|
+
end
|
173
|
+
|
174
|
+
def fail_if_ambiguous?
|
175
|
+
!options.fetch(:ambiguous, true)
|
176
|
+
end
|
177
|
+
|
178
|
+
WHITESPACE = /\s/.freeze
|
179
|
+
PUNCTUATION = /[^\s\w'@-]/.freeze
|
180
|
+
EMOJI_START = ":".freeze
|
181
|
+
EMOJI_END = ":".freeze
|
182
|
+
ENTITY_START = "{".freeze
|
183
|
+
ENTITY = /[a-z0-9\.\-:]/.freeze
|
184
|
+
REGEXP_START = "(".freeze
|
185
|
+
NUMBER_START = /[\d\.\-]/.freeze
|
186
|
+
CONDITIONAL_NUMBER_START = /[\.\-]/.freeze
|
187
|
+
NUMBER = /\d/.freeze
|
188
|
+
CONDITIONAL_NUMBER = /[\.,]/.freeze
|
189
|
+
WORD = /[\w'\-@]/.freeze
|
190
|
+
|
191
|
+
def fail_if_ambiguous!(phrase, tokens)
|
146
192
|
ambiguous_token = tokens.find(&:ambiguous?)
|
147
193
|
return unless ambiguous_token
|
148
194
|
|
@@ -159,5 +205,4 @@ end
|
|
159
205
|
require "attentive/entity"
|
160
206
|
require "attentive/composite_entity"
|
161
207
|
|
162
|
-
require "attentive/entities/
|
163
|
-
require "attentive/entities/relative_date"
|
208
|
+
require "attentive/entities/core"
|
data/lib/attentive/tokens.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
module Attentive
|
2
2
|
module Tokens
|
3
3
|
|
4
|
-
def any_of(possibilities, pos: nil)
|
5
|
-
Attentive::Tokens::AnyOf.new possibilities, pos
|
4
|
+
def any_of(string, possibilities, pos: nil)
|
5
|
+
Attentive::Tokens::AnyOf.new string, possibilities, pos
|
6
6
|
end
|
7
7
|
|
8
8
|
def emoji(string, pos: nil)
|
@@ -2,12 +2,12 @@ require "attentive/token"
|
|
2
2
|
|
3
3
|
module Attentive
|
4
4
|
module Tokens
|
5
|
-
class AnyOf <
|
5
|
+
class AnyOf < StringToken
|
6
6
|
attr_reader :possibilities
|
7
7
|
|
8
|
-
def initialize(possibilities, pos)
|
8
|
+
def initialize(string, possibilities, pos)
|
9
|
+
super string, pos
|
9
10
|
@possibilities = possibilities
|
10
|
-
super pos
|
11
11
|
end
|
12
12
|
|
13
13
|
def ==(other)
|
@@ -15,11 +15,28 @@ module Attentive
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def matches?(cursor)
|
18
|
-
|
18
|
+
# Compare the original, untokenized, message to the regular expression
|
19
|
+
match_data = regexp.match(cursor.to_s)
|
20
|
+
return false unless match_data
|
21
|
+
|
22
|
+
# Find the first token following the match
|
23
|
+
new_character_index = cursor.offset + match_data.to_s.length
|
24
|
+
cursor_pos = cursor.tokens.index { |token| token.pos >= new_character_index }
|
25
|
+
cursor_pos = cursor.tokens.length unless cursor_pos
|
26
|
+
|
27
|
+
# If the match ends in the middle of a token, treat it as a mismatch
|
28
|
+
match_end_token = cursor.tokens[cursor_pos - 1]
|
29
|
+
return false if match_end_token.pos + match_end_token.length > new_character_index
|
30
|
+
|
31
|
+
# Advance the cursor to the first token after the regexp match
|
32
|
+
cursor.advance cursor_pos - cursor.pos
|
33
|
+
|
34
|
+
# Return the MatchData as a hash
|
35
|
+
Hash[match_data.names.zip(match_data.captures)]
|
19
36
|
end
|
20
37
|
|
21
38
|
def to_s
|
22
|
-
regexp.inspect
|
39
|
+
regexp.inspect
|
23
40
|
end
|
24
41
|
|
25
42
|
end
|
data/lib/attentive/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: attentive
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bob Lail
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-05-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thread_safe
|
@@ -158,8 +158,23 @@ files:
|
|
158
158
|
- lib/attentive/config.rb
|
159
159
|
- lib/attentive/contractions.rb
|
160
160
|
- lib/attentive/cursor.rb
|
161
|
-
- lib/attentive/entities/
|
162
|
-
- lib/attentive/entities/
|
161
|
+
- lib/attentive/entities/core.rb
|
162
|
+
- lib/attentive/entities/core/date.rb
|
163
|
+
- lib/attentive/entities/core/date/month.rb
|
164
|
+
- lib/attentive/entities/core/date/relative.rb
|
165
|
+
- lib/attentive/entities/core/date/relative/future.rb
|
166
|
+
- lib/attentive/entities/core/date/relative/past.rb
|
167
|
+
- lib/attentive/entities/core/date/wday.rb
|
168
|
+
- lib/attentive/entities/core/email.rb
|
169
|
+
- lib/attentive/entities/core/number.rb
|
170
|
+
- lib/attentive/entities/core/number/float.rb
|
171
|
+
- lib/attentive/entities/core/number/float/negative.rb
|
172
|
+
- lib/attentive/entities/core/number/float/positive.rb
|
173
|
+
- lib/attentive/entities/core/number/integer.rb
|
174
|
+
- lib/attentive/entities/core/number/integer/negative.rb
|
175
|
+
- lib/attentive/entities/core/number/integer/positive.rb
|
176
|
+
- lib/attentive/entities/core/number/negative.rb
|
177
|
+
- lib/attentive/entities/core/number/positive.rb
|
163
178
|
- lib/attentive/entity.rb
|
164
179
|
- lib/attentive/errors.rb
|
165
180
|
- lib/attentive/listener.rb
|
@@ -1,44 +0,0 @@
|
|
1
|
-
require "attentive/entity"
|
2
|
-
require "date"
|
3
|
-
|
4
|
-
weekday_regexp = "(?<weekday>sunday|monday|tuesday|wednesday|thursday|friday|saturday)"
|
5
|
-
Attentive::Entity.define :"relative-date",
|
6
|
-
"today",
|
7
|
-
"tomorrow",
|
8
|
-
"yesterday",
|
9
|
-
weekday_regexp,
|
10
|
-
"next #{weekday_regexp}",
|
11
|
-
"last #{weekday_regexp}" do |match|
|
12
|
-
|
13
|
-
today = Date.today
|
14
|
-
|
15
|
-
next_wday = lambda do |wday|
|
16
|
-
days_until_wday = wday - today.wday
|
17
|
-
days_until_wday += 7 if days_until_wday < 0
|
18
|
-
today + days_until_wday
|
19
|
-
end
|
20
|
-
|
21
|
-
if match.matched?("weekday")
|
22
|
-
date = case weekday = match["weekday"]
|
23
|
-
when /^sun/ then next_wday[0]
|
24
|
-
when /^mon/ then next_wday[1]
|
25
|
-
when /^tue/ then next_wday[2]
|
26
|
-
when /^wed/ then next_wday[3]
|
27
|
-
when /^thu/ then next_wday[4]
|
28
|
-
when /^fri/ then next_wday[5]
|
29
|
-
when /^sat/ then next_wday[6]
|
30
|
-
else raise NotImplementedError, "Unrecognized weekday: #{weekday.inspect}"
|
31
|
-
end
|
32
|
-
|
33
|
-
date += 7 if match.to_s.start_with?("next")
|
34
|
-
date -= 7 if match.to_s.start_with?("last")
|
35
|
-
date
|
36
|
-
else
|
37
|
-
case match.to_s
|
38
|
-
when "today" then today
|
39
|
-
when "tomorrow" then today + 1
|
40
|
-
when "yesterday" then today - 1
|
41
|
-
else raise NotImplementedError, "Unrecognized match: #{match.to_s}"
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|