natural 0.1.0 → 0.1.1
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.
- data/Gemfile +3 -2
- data/Gemfile.lock +8 -0
- data/README.markdown +10 -8
- data/Rakefile +5 -0
- data/VERSION +1 -1
- data/lib/natural.rb +41 -21
- data/lib/natural/fragment.rb +42 -37
- data/natural.gemspec +6 -3
- data/spec/natural_spec.rb +17 -5
- data/spec/spec_helper.rb +1 -0
- metadata +90 -24
data/Gemfile
CHANGED
@@ -10,8 +10,9 @@ gem 'activesupport'
|
|
10
10
|
group :development do
|
11
11
|
gem "rspec", "~> 2.8.0"
|
12
12
|
gem "rdoc", "~> 3.12"
|
13
|
-
gem "cucumber"
|
13
|
+
gem "cucumber"
|
14
14
|
gem "bundler", "~> 1.0.0"
|
15
15
|
gem "jeweler", "~> 1.8.3"
|
16
|
-
gem "simplecov"
|
16
|
+
gem "simplecov"
|
17
|
+
gem "pry"
|
17
18
|
end
|
data/Gemfile.lock
CHANGED
@@ -5,6 +5,7 @@ GEM
|
|
5
5
|
i18n (~> 0.6)
|
6
6
|
multi_json (~> 1.0)
|
7
7
|
builder (3.0.0)
|
8
|
+
coderay (1.0.5)
|
8
9
|
cucumber (1.1.9)
|
9
10
|
builder (>= 2.1.2)
|
10
11
|
diff-lcs (>= 1.1.2)
|
@@ -24,7 +25,12 @@ GEM
|
|
24
25
|
json (1.6.5)
|
25
26
|
logger (1.2.8)
|
26
27
|
map_by_method (0.8.3)
|
28
|
+
method_source (0.7.1)
|
27
29
|
multi_json (1.1.0)
|
30
|
+
pry (0.9.8.3)
|
31
|
+
coderay (~> 1.0.5)
|
32
|
+
method_source (~> 0.7.1)
|
33
|
+
slop (>= 2.4.4, < 3)
|
28
34
|
rake (0.9.2.2)
|
29
35
|
rdoc (3.12)
|
30
36
|
json (~> 1.4)
|
@@ -41,6 +47,7 @@ GEM
|
|
41
47
|
multi_json (~> 1.0)
|
42
48
|
simplecov-html (~> 0.5.3)
|
43
49
|
simplecov-html (0.5.3)
|
50
|
+
slop (2.4.4)
|
44
51
|
term-ansicolor (1.0.7)
|
45
52
|
|
46
53
|
PLATFORMS
|
@@ -53,6 +60,7 @@ DEPENDENCIES
|
|
53
60
|
jeweler (~> 1.8.3)
|
54
61
|
logger
|
55
62
|
map_by_method
|
63
|
+
pry
|
56
64
|
rdoc (~> 3.12)
|
57
65
|
rspec (~> 2.8.0)
|
58
66
|
rubytree
|
data/README.markdown
CHANGED
@@ -87,10 +87,6 @@ This is a bit easier to understand by looking at an example, take a gander at: l
|
|
87
87
|
end
|
88
88
|
end
|
89
89
|
|
90
|
-
#### Plurals and Inflectors
|
91
|
-
|
92
|
-
Everything is singularized and downcased before being matched. lib/infections.rb can be used to customize the singularization behavior.
|
93
|
-
|
94
90
|
### Compound Fragments
|
95
91
|
|
96
92
|
class StartsWithLetter < Natural::Fragment
|
@@ -127,11 +123,15 @@ Everything is singularized and downcased before being matched. lib/infections.rb
|
|
127
123
|
[n][tree] +---+ blu-rays expansion (0)
|
128
124
|
[n][tree] +---> movies fragment (0)
|
129
125
|
|
126
|
+
### Plurals and Inflectors
|
127
|
+
|
128
|
+
Everything is singularized and downcased before being matched. `lib/natural/inflections.rb` can be used to customize the singularization behavior.
|
129
|
+
|
130
130
|
### Scoring
|
131
131
|
|
132
132
|
## Generating the Answer
|
133
133
|
|
134
|
-
###
|
134
|
+
### Data
|
135
135
|
|
136
136
|
### Filters
|
137
137
|
|
@@ -139,15 +139,17 @@ Everything is singularized and downcased before being matched. lib/infections.rb
|
|
139
139
|
|
140
140
|
### Putting Them Together
|
141
141
|
|
142
|
-
|
142
|
+
use the built in answer method or DIY: navigate the tree and assemble data, filters, and aggregators any way you want
|
143
143
|
|
144
144
|
## Performance
|
145
145
|
|
146
146
|
Natural has not (yet) been optimized for cpu or memory usage. Natural works best with short questions and a small vocabulary.
|
147
147
|
|
148
|
-
|
148
|
+
### Matching
|
149
|
+
|
150
|
+
You can match according to the highest scoring combination of words (slower, defalt), or you can match in the order of the passed in fragment_classes, ignoring any words that are already matched (faster).
|
149
151
|
|
150
|
-
|
152
|
+
Natural.new('how many days of the week start with the letter T', :matching => :first_match, :fragment_classes => [StartsWithLetter, DayNames, Count]).answer
|
151
153
|
|
152
154
|
## Contributing to Natural
|
153
155
|
|
data/Rakefile
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.1
|
data/lib/natural.rb
CHANGED
@@ -5,30 +5,38 @@ require 'natural/fragment'
|
|
5
5
|
require 'natural/fragments/timeframes.rb'
|
6
6
|
require 'natural/fragments/misc.rb'
|
7
7
|
require 'natural/fragments/example.rb'
|
8
|
+
require 'pry'
|
8
9
|
|
9
10
|
class Natural
|
10
11
|
require 'map_by_method'
|
11
12
|
require 'active_support/inflector'
|
12
13
|
require 'active_support/core_ext'
|
13
|
-
|
14
14
|
require 'logger'
|
15
|
-
@@logger = Logger.new(STDOUT)
|
16
|
-
@@logger.level = Logger::DEBUG
|
17
15
|
|
18
16
|
GREEN = "\e[32m"
|
19
17
|
RED = "\e[31m"
|
20
18
|
YELLOW = "\e[33m"
|
21
19
|
CLEAR = "\e[0m"
|
22
20
|
|
21
|
+
MATCHING_OPTIONS = [:most_points, :first_match]
|
22
|
+
|
23
23
|
DEFAULT_SPELLINGS = {'week' => ['wek', 'weeek'], 'begin' => ['beginn', 'beegin']}
|
24
24
|
DEFAULT_SYNONYMS = {'1' => ['start', 'begin', 'commence'], '2' => ['stop', 'end', 'finish', 'conclude']}
|
25
25
|
DEFAULT_EXPANSIONS = {'food' => ['grocery', 'eat out', 'eating out', 'dining out', 'dine out', 'dine in'], 'music' => ['audio cd', 'audio tape'], 'movie' => ['blu-ray', 'dvd', 'video']}
|
26
|
+
DEFAULT_MATCHING = :most_points
|
26
27
|
|
27
28
|
def initialize(text, options={})
|
28
|
-
@text
|
29
|
-
@options
|
29
|
+
@text = text.squeeze(' ').strip
|
30
|
+
@options = options
|
31
|
+
|
32
|
+
if options[:logger]
|
33
|
+
@logger = options[:logger]
|
34
|
+
else
|
35
|
+
@logger = Logger.new(STDOUT)
|
36
|
+
@logger.level = Logger::DEBUG
|
37
|
+
end
|
30
38
|
|
31
|
-
@parse
|
39
|
+
@parse = parse
|
32
40
|
end
|
33
41
|
|
34
42
|
def text=(text)
|
@@ -45,6 +53,7 @@ class Natural
|
|
45
53
|
return @parse if @parse
|
46
54
|
|
47
55
|
start_at = Time.now
|
56
|
+
|
48
57
|
# search for all possible matches using all the different fragment classes
|
49
58
|
matches_by_class = {}
|
50
59
|
fragment_classes = @options[:fragment_classes] || ObjectSpace.each_object(Class)
|
@@ -52,20 +61,31 @@ class Natural
|
|
52
61
|
find_options = {
|
53
62
|
:text => @text,
|
54
63
|
:matches => matches_by_class,
|
64
|
+
:matching => @options[:matching] || DEFAULT_MATCHING,
|
55
65
|
:spellings => @options[:spellings] || DEFAULT_SPELLINGS,
|
56
66
|
:synonyms => @options[:synonyms] || DEFAULT_SYNONYMS,
|
57
67
|
:expansions => @options[:expansions] || DEFAULT_EXPANSIONS
|
58
68
|
}
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
#
|
63
|
-
|
64
|
-
|
69
|
+
|
70
|
+
if find_options[:matching] == :first_match
|
71
|
+
# once a match has been found, exclude those words from further consideration
|
72
|
+
# can help speed things up, but requires you order the candidate fragment_classes carefully
|
73
|
+
fragment_classes.each do |klass|
|
74
|
+
new_options = find_options.dup
|
75
|
+
new_options[:ignore] = matches_by_class.values.flatten.select{|a| a}.map_by_ids.flatten.uniq.sort
|
76
|
+
matches_by_class[klass] = klass.find(new_options)[klass] if klass.find(new_options)[klass]
|
77
|
+
end
|
78
|
+
else
|
79
|
+
ObjectSpace.each_object(Class).select {|a| a < Natural::Alternative}.each do |klass|
|
80
|
+
matches_by_class = klass.find(find_options)
|
81
|
+
end
|
82
|
+
fragment_classes.each do |klass|
|
83
|
+
matches_by_class = klass.find(find_options)
|
84
|
+
end
|
65
85
|
end
|
66
86
|
|
67
87
|
matching_at = Time.now
|
68
|
-
|
88
|
+
@logger.debug "[n][perf] matching took #{(matching_at - start_at).seconds.round(1)} seconds"
|
69
89
|
|
70
90
|
# find all valid combinations, choose the one with the highest score
|
71
91
|
sequences = []
|
@@ -74,8 +94,8 @@ class Natural
|
|
74
94
|
fragments = sequences.first || []
|
75
95
|
|
76
96
|
scoring_at = Time.now
|
77
|
-
|
78
|
-
|
97
|
+
@logger.debug "[n][perf] scoring took #{(scoring_at - matching_at).seconds.round(1)} seconds"
|
98
|
+
@logger.debug "[n]"
|
79
99
|
|
80
100
|
# tag the leftover words as unused
|
81
101
|
remaining_words = (0..@text.split(' ').size-1).to_a - (!fragments.blank? ? fragments.map_by_ids.flatten : [])
|
@@ -89,14 +109,14 @@ class Natural
|
|
89
109
|
@parse = Fragment.new(:ids => (0..@text.split(' ').size-1).to_a, :text => @text)
|
90
110
|
fragments.each {|a| @parse << a}
|
91
111
|
|
92
|
-
sequences.each {|a|
|
93
|
-
|
112
|
+
sequences.each {|a| @logger.debug "[n][scor] #{a.map_by_score.sum.to_s.rjust(2, '0')} #{a.sort{|b,c| b.ids.first <=> c.ids.first}.join(' | ')}"}
|
113
|
+
@logger.debug("[n]")
|
94
114
|
@parse.pretty_to_s.each_line do |line|
|
95
|
-
|
115
|
+
@logger.debug("[n][tree] #{line.gsub("\n", '')}")
|
96
116
|
end
|
97
|
-
|
98
|
-
|
99
|
-
|
117
|
+
@logger.debug("[n]")
|
118
|
+
@logger.info("[n][orig] #{@text}" + (@options[:context] ? " (#{@options[:context]})" : ""))
|
119
|
+
@logger.info("[n][used] #{interpretation}" + (@options[:context] ? " (#{@options[:context]})" : ""))
|
100
120
|
|
101
121
|
@parse
|
102
122
|
end
|
data/lib/natural/fragment.rb
CHANGED
@@ -87,6 +87,7 @@ class Natural
|
|
87
87
|
|
88
88
|
def self.find(options)
|
89
89
|
text_to_search = options[:text]
|
90
|
+
words_to_ignore = options[:ignore] || []
|
90
91
|
looking_for = options[:looking_for]
|
91
92
|
old_matches = options[:matches] || {}
|
92
93
|
if options[:matches] && (options[:merge_results].class == NilClass || options[:merge_results])
|
@@ -106,47 +107,49 @@ class Natural
|
|
106
107
|
# look for the longest possible matches and work our way down to the short ones
|
107
108
|
0.upto(words.size-1) do |first|
|
108
109
|
(words.size-1).downto(first) do |last|
|
109
|
-
|
110
|
-
|
110
|
+
if ((first..last).to_a & words_to_ignore).blank?
|
111
|
+
match = nil
|
112
|
+
selection = words[(first..last)].join(' ').strip.downcase
|
111
113
|
|
112
|
-
|
113
|
-
|
114
|
-
end
|
115
|
-
|
116
|
-
# didn't find a simple match, try swapping some or all words for alternatives and try again
|
117
|
-
if !match && !(match_class < Natural::Alternative)
|
118
|
-
fragments = old_matches.select {|k,v| k < Natural::Alternative && !v.blank?}.values.flatten.select {|a| a.ids.first >= first && a.ids.last <= last}
|
119
|
-
|
120
|
-
# assemble a list of all the possible, non-overlapping swaps
|
121
|
-
combinations = (1..fragments.size).inject([]) do |memo, i|
|
122
|
-
fragments.combination(i).each do |combo|
|
123
|
-
if !combo.combination(2).any? {|a| (a[0].ids.first..a[0].ids.last).overlaps?(a[1].ids.first..a[1].ids.last)}
|
124
|
-
memo << combo
|
125
|
-
end
|
126
|
-
end
|
127
|
-
memo
|
114
|
+
if looking_for.include?(selection.singularize.downcase)
|
115
|
+
match = match_class.new(:ids => (first..last).to_a, :text => selection)
|
128
116
|
end
|
129
117
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
118
|
+
# didn't find a simple match, try swapping some or all words for alternatives and try again
|
119
|
+
if !match && !(match_class < Natural::Alternative)
|
120
|
+
fragments = old_matches.select {|k,v| k < Natural::Alternative && !v.blank?}.values.flatten.select {|a| a.ids.first >= first && a.ids.last <= last}
|
121
|
+
|
122
|
+
# assemble a list of all the possible, non-overlapping swaps
|
123
|
+
combinations = (1..fragments.size).inject([]) do |memo, i|
|
124
|
+
fragments.combination(i).each do |combo|
|
125
|
+
if !combo.combination(2).any? {|a| (a[0].ids.first..a[0].ids.last).overlaps?(a[1].ids.first..a[1].ids.last)}
|
126
|
+
memo << combo
|
127
|
+
end
|
128
|
+
end
|
129
|
+
memo
|
138
130
|
end
|
139
|
-
alternative_selection = alternative_words[(first..last)].join(' ').strip.downcase
|
140
131
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
132
|
+
combinations.each do |combo|
|
133
|
+
alternative_words = words.clone
|
134
|
+
alternative_fragments = []
|
135
|
+
|
136
|
+
combo.each do |fragment|
|
137
|
+
alternative_words.slice!(fragment.ids.first..fragment.ids.last)
|
138
|
+
alternative_words.insert(fragment.ids.first, fragment.to_s)
|
139
|
+
alternative_fragments << fragment
|
140
|
+
end
|
141
|
+
alternative_selection = alternative_words[(first..last)].join(' ').strip.downcase
|
142
|
+
|
143
|
+
if looking_for.include?(alternative_selection.singularize.downcase)
|
144
|
+
match = match_class.new(:ids => (first..last).to_a, :text => alternative_selection)
|
145
|
+
leftovers = ((first..last).to_a - combo.map {|a| a.ids}.flatten).to_ranges
|
146
|
+
leftovers.each do |range|
|
147
|
+
alternative_fragments << Fragment.new(:ids => range.to_a, :text => words[range].join(' '))
|
148
|
+
end
|
149
|
+
alternative_fragments.sort_by {|a| a.ids.first}.each {|a| match << a}
|
146
150
|
end
|
147
|
-
alternative_fragments.sort_by {|a| a.ids.first}.each {|a| match << a}
|
148
|
-
end
|
149
151
|
|
152
|
+
end
|
150
153
|
end
|
151
154
|
end
|
152
155
|
|
@@ -188,9 +191,11 @@ class Natural
|
|
188
191
|
looking_for[1..-1].each do |term|
|
189
192
|
if term.class == Class && term <= Fragment
|
190
193
|
new_matches = term.find(:text => text_to_search, :matches => old_matches, :spellings => options[:spellings], :synonyms => options[:synonyms], :expansions => options[:expansions]) if !old_matches[term]
|
191
|
-
new_matches[term]
|
192
|
-
|
193
|
-
fragments
|
194
|
+
if new_matches[term]
|
195
|
+
new_matches[term].each do |match|
|
196
|
+
if match.ids.first == fragments.select {|a| a}.last.ids.last + 1
|
197
|
+
fragments << match
|
198
|
+
end
|
194
199
|
end
|
195
200
|
end
|
196
201
|
elsif [Array, Hash, String].include?(term.class)
|
data/natural.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "natural"
|
8
|
-
s.version = "0.1.
|
8
|
+
s.version = "0.1.1"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Scott Bonds"]
|
12
|
-
s.date = "2012-
|
12
|
+
s.date = "2012-08-24"
|
13
13
|
s.description = "Natural provides a framework for answering 'naturally' worded questions like 'how many books did I buy last month' or 'list my Facebook friends'."
|
14
14
|
s.email = "scott@ggr.com"
|
15
15
|
s.extra_rdoc_files = [
|
@@ -43,7 +43,7 @@ Gem::Specification.new do |s|
|
|
43
43
|
s.homepage = "http://github.com/bonds/natural"
|
44
44
|
s.licenses = ["MIT"]
|
45
45
|
s.require_paths = ["lib"]
|
46
|
-
s.rubygems_version = "1.8.
|
46
|
+
s.rubygems_version = "1.8.23"
|
47
47
|
s.summary = "natural language parser"
|
48
48
|
|
49
49
|
if s.respond_to? :specification_version then
|
@@ -60,6 +60,7 @@ Gem::Specification.new do |s|
|
|
60
60
|
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
61
61
|
s.add_development_dependency(%q<jeweler>, ["~> 1.8.3"])
|
62
62
|
s.add_development_dependency(%q<simplecov>, [">= 0"])
|
63
|
+
s.add_development_dependency(%q<pry>, [">= 0"])
|
63
64
|
else
|
64
65
|
s.add_dependency(%q<rubytree>, [">= 0"])
|
65
66
|
s.add_dependency(%q<logger>, [">= 0"])
|
@@ -71,6 +72,7 @@ Gem::Specification.new do |s|
|
|
71
72
|
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
72
73
|
s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
|
73
74
|
s.add_dependency(%q<simplecov>, [">= 0"])
|
75
|
+
s.add_dependency(%q<pry>, [">= 0"])
|
74
76
|
end
|
75
77
|
else
|
76
78
|
s.add_dependency(%q<rubytree>, [">= 0"])
|
@@ -83,6 +85,7 @@ Gem::Specification.new do |s|
|
|
83
85
|
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
84
86
|
s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
|
85
87
|
s.add_dependency(%q<simplecov>, [">= 0"])
|
88
|
+
s.add_dependency(%q<pry>, [">= 0"])
|
86
89
|
end
|
87
90
|
end
|
88
91
|
|
data/spec/natural_spec.rb
CHANGED
@@ -1,7 +1,19 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
describe "Natural" do
|
4
|
+
it "recognizes synonyms on top of misspellings" do
|
5
|
+
logger = double("logger")
|
6
|
+
logger.should_receive(:info).with("[n][orig] how many days of the wek beginn with the letter T")
|
7
|
+
logger.should_receive(:info).with("[n][used] how many days of the week start with the letter t")
|
8
|
+
logger.should_receive(:debug).any_number_of_times
|
9
|
+
Natural.new('how many days of the wek beginn with the letter T', :logger => logger).answer.should eq(2)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "expands movies into blu-rays" do
|
13
|
+
logger = double("logger")
|
14
|
+
logger.should_receive(:info).any_number_of_times
|
15
|
+
logger.should_receive(:debug).with("[n][scor] 01 blu-rays").at_least(1).times
|
16
|
+
logger.should_receive(:debug).any_number_of_times
|
17
|
+
Natural.new('movies', :logger => logger)
|
18
|
+
end
|
19
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -5,6 +5,7 @@ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
|
5
5
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
6
6
|
require 'rspec'
|
7
7
|
require 'natural'
|
8
|
+
require 'pry'
|
8
9
|
|
9
10
|
# Requires supporting files with custom matchers and macros, etc,
|
10
11
|
# in ./support/ and its subdirectories.
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: natural
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-08-24 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rubytree
|
16
|
-
requirement:
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,15 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements:
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
25
30
|
- !ruby/object:Gem::Dependency
|
26
31
|
name: logger
|
27
|
-
requirement:
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
28
33
|
none: false
|
29
34
|
requirements:
|
30
35
|
- - ! '>='
|
@@ -32,10 +37,15 @@ dependencies:
|
|
32
37
|
version: '0'
|
33
38
|
type: :runtime
|
34
39
|
prerelease: false
|
35
|
-
version_requirements:
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
36
46
|
- !ruby/object:Gem::Dependency
|
37
47
|
name: map_by_method
|
38
|
-
requirement:
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
39
49
|
none: false
|
40
50
|
requirements:
|
41
51
|
- - ! '>='
|
@@ -43,10 +53,15 @@ dependencies:
|
|
43
53
|
version: '0'
|
44
54
|
type: :runtime
|
45
55
|
prerelease: false
|
46
|
-
version_requirements:
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
47
62
|
- !ruby/object:Gem::Dependency
|
48
63
|
name: activesupport
|
49
|
-
requirement:
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
50
65
|
none: false
|
51
66
|
requirements:
|
52
67
|
- - ! '>='
|
@@ -54,10 +69,15 @@ dependencies:
|
|
54
69
|
version: '0'
|
55
70
|
type: :runtime
|
56
71
|
prerelease: false
|
57
|
-
version_requirements:
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
58
78
|
- !ruby/object:Gem::Dependency
|
59
79
|
name: rspec
|
60
|
-
requirement:
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
61
81
|
none: false
|
62
82
|
requirements:
|
63
83
|
- - ~>
|
@@ -65,10 +85,15 @@ dependencies:
|
|
65
85
|
version: 2.8.0
|
66
86
|
type: :development
|
67
87
|
prerelease: false
|
68
|
-
version_requirements:
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 2.8.0
|
69
94
|
- !ruby/object:Gem::Dependency
|
70
95
|
name: rdoc
|
71
|
-
requirement:
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
72
97
|
none: false
|
73
98
|
requirements:
|
74
99
|
- - ~>
|
@@ -76,10 +101,15 @@ dependencies:
|
|
76
101
|
version: '3.12'
|
77
102
|
type: :development
|
78
103
|
prerelease: false
|
79
|
-
version_requirements:
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ~>
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '3.12'
|
80
110
|
- !ruby/object:Gem::Dependency
|
81
111
|
name: cucumber
|
82
|
-
requirement:
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
83
113
|
none: false
|
84
114
|
requirements:
|
85
115
|
- - ! '>='
|
@@ -87,10 +117,15 @@ dependencies:
|
|
87
117
|
version: '0'
|
88
118
|
type: :development
|
89
119
|
prerelease: false
|
90
|
-
version_requirements:
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
91
126
|
- !ruby/object:Gem::Dependency
|
92
127
|
name: bundler
|
93
|
-
requirement:
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
94
129
|
none: false
|
95
130
|
requirements:
|
96
131
|
- - ~>
|
@@ -98,10 +133,15 @@ dependencies:
|
|
98
133
|
version: 1.0.0
|
99
134
|
type: :development
|
100
135
|
prerelease: false
|
101
|
-
version_requirements:
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ~>
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: 1.0.0
|
102
142
|
- !ruby/object:Gem::Dependency
|
103
143
|
name: jeweler
|
104
|
-
requirement:
|
144
|
+
requirement: !ruby/object:Gem::Requirement
|
105
145
|
none: false
|
106
146
|
requirements:
|
107
147
|
- - ~>
|
@@ -109,10 +149,15 @@ dependencies:
|
|
109
149
|
version: 1.8.3
|
110
150
|
type: :development
|
111
151
|
prerelease: false
|
112
|
-
version_requirements:
|
152
|
+
version_requirements: !ruby/object:Gem::Requirement
|
153
|
+
none: false
|
154
|
+
requirements:
|
155
|
+
- - ~>
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: 1.8.3
|
113
158
|
- !ruby/object:Gem::Dependency
|
114
159
|
name: simplecov
|
115
|
-
requirement:
|
160
|
+
requirement: !ruby/object:Gem::Requirement
|
116
161
|
none: false
|
117
162
|
requirements:
|
118
163
|
- - ! '>='
|
@@ -120,7 +165,28 @@ dependencies:
|
|
120
165
|
version: '0'
|
121
166
|
type: :development
|
122
167
|
prerelease: false
|
123
|
-
version_requirements:
|
168
|
+
version_requirements: !ruby/object:Gem::Requirement
|
169
|
+
none: false
|
170
|
+
requirements:
|
171
|
+
- - ! '>='
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
- !ruby/object:Gem::Dependency
|
175
|
+
name: pry
|
176
|
+
requirement: !ruby/object:Gem::Requirement
|
177
|
+
none: false
|
178
|
+
requirements:
|
179
|
+
- - ! '>='
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: '0'
|
182
|
+
type: :development
|
183
|
+
prerelease: false
|
184
|
+
version_requirements: !ruby/object:Gem::Requirement
|
185
|
+
none: false
|
186
|
+
requirements:
|
187
|
+
- - ! '>='
|
188
|
+
- !ruby/object:Gem::Version
|
189
|
+
version: '0'
|
124
190
|
description: Natural provides a framework for answering 'naturally' worded questions
|
125
191
|
like 'how many books did I buy last month' or 'list my Facebook friends'.
|
126
192
|
email: scott@ggr.com
|
@@ -167,7 +233,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
167
233
|
version: '0'
|
168
234
|
segments:
|
169
235
|
- 0
|
170
|
-
hash: -
|
236
|
+
hash: -2109308253021748277
|
171
237
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
172
238
|
none: false
|
173
239
|
requirements:
|
@@ -176,7 +242,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
176
242
|
version: '0'
|
177
243
|
requirements: []
|
178
244
|
rubyforge_project:
|
179
|
-
rubygems_version: 1.8.
|
245
|
+
rubygems_version: 1.8.23
|
180
246
|
signing_key:
|
181
247
|
specification_version: 3
|
182
248
|
summary: natural language parser
|