ingreedy 0.0.7 → 0.0.8
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/.gitignore +5 -0
- data/.rspec +1 -0
- data/.rubocop.yml +601 -0
- data/.rvmrc +1 -0
- data/.travis.yml +14 -0
- data/Gemfile +3 -0
- data/README.md +46 -0
- data/Rakefile +5 -0
- data/ingreedy.gemspec +26 -0
- data/lib/ingreedy.rb +17 -13
- data/lib/ingreedy/amount_parser.rb +17 -18
- data/lib/ingreedy/case_insensitive_parser.rb +1 -3
- data/lib/ingreedy/dictionaries/en.yml +137 -0
- data/lib/ingreedy/dictionary.rb +19 -19
- data/lib/ingreedy/dictionary_collection.rb +5 -3
- data/lib/ingreedy/ingreedy_parser.rb +34 -116
- data/lib/ingreedy/rationalizer.rb +12 -10
- data/lib/ingreedy/root_parser.rb +118 -0
- data/lib/ingreedy/unit_variation_mapper.rb +12 -9
- data/lib/ingreedy/version.rb +1 -2
- data/spec/ingreedy/amount_parser_spec.rb +99 -0
- data/spec/ingreedy/rationalizer_spec.rb +56 -0
- data/spec/ingreedy/unit_variation_mapper_spec.rb +13 -0
- data/spec/ingreedy_spec.rb +378 -0
- data/spec/spec_helper.rb +34 -0
- metadata +23 -26
@@ -0,0 +1,56 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "spec_helper"
|
3
|
+
|
4
|
+
describe Ingreedy::Rationalizer do
|
5
|
+
subject { described_class }
|
6
|
+
|
7
|
+
context "with an integer and a fraction" do
|
8
|
+
it "gives back an improper rational" do
|
9
|
+
rational = subject.rationalize(integer: "1", fraction: "1/2")
|
10
|
+
expect(rational).to eq("3/2".to_r)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
context "with a fraction" do
|
15
|
+
it "gives back a rational" do
|
16
|
+
expect(subject.rationalize(fraction: "1/2")).to eq("1/2".to_r)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context "with a vulgar fraction" do
|
21
|
+
it "gives back a rational" do
|
22
|
+
expect(subject.rationalize(fraction: "¼")).to eq("1/4".to_r)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "with an integer" do
|
27
|
+
it "gives back a rational" do
|
28
|
+
expect(subject.rationalize(integer: "2")).to eq("2".to_r)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context "with a float" do
|
33
|
+
it "gives back a rational" do
|
34
|
+
expect(subject.rationalize(float: "0.3")).to eq("3/10".to_r)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context "with a european float" do
|
39
|
+
it "gives back a rational" do
|
40
|
+
expect(subject.rationalize(float: "0,4")).to eq("4/10".to_r)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context "with an english digit" do
|
45
|
+
it "gives back a rational" do
|
46
|
+
expect(subject.rationalize(word: "one")).to eq("1".to_r)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context "with the word a or an" do
|
51
|
+
it "gives back a rational" do
|
52
|
+
expect(subject.rationalize(word: "a")).to eq("1".to_r)
|
53
|
+
expect(subject.rationalize(word: "AN")).to eq("1".to_r)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Ingreedy::UnitVariationMapper do
|
4
|
+
subject { described_class }
|
5
|
+
|
6
|
+
describe ".unit_from_variation" do
|
7
|
+
context "uppercased variation" do
|
8
|
+
it "gives back the correct unit as a symbol" do
|
9
|
+
expect(subject.unit_from_variation("TSP")).to eq(:teaspoon)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,378 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "spec_helper"
|
3
|
+
|
4
|
+
describe Ingreedy do
|
5
|
+
context "amount parsing" do
|
6
|
+
{
|
7
|
+
"1 cup flour" => 1,
|
8
|
+
"one cup flour" => 1,
|
9
|
+
"1 1/2 cups flour" => "3/2",
|
10
|
+
"¼ cups flour" => "1/4",
|
11
|
+
"1 ½ cups flour" => "3/2",
|
12
|
+
"1½ cups flour" => "3/2",
|
13
|
+
"1.0 cup flour" => 1,
|
14
|
+
"1.5 cups flour" => "3/2",
|
15
|
+
"1,5 cups flour" => "3/2",
|
16
|
+
"1 2/3 cups flour" => "5/3",
|
17
|
+
"1 (28 ounce) can crushed tomatoes" => 1,
|
18
|
+
"2 (28 ounce) can crushed tomatoes" => 2,
|
19
|
+
"3 28 ounce can crushed tomatoes" => 3,
|
20
|
+
"one 28 ounce can crushed tomatoes" => 1,
|
21
|
+
"two five-ounce can crushed tomatoes" => 2,
|
22
|
+
"two 28 ounce cans crushed tomatoes" => 2,
|
23
|
+
"three 28 ounce cans crushed tomatoes" => 3,
|
24
|
+
"1/2 cups flour" => "1/2",
|
25
|
+
".25 cups flour" => "1/4",
|
26
|
+
"12oz tequila" => 12,
|
27
|
+
"1 banana" => 1,
|
28
|
+
}.each do |query, expected|
|
29
|
+
it "parses the correct amount as a rational" do
|
30
|
+
expect(Ingreedy.parse(query)).to parse_the_amount(expected.to_r)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "unit parsing" do
|
36
|
+
context "(english)" do
|
37
|
+
end
|
38
|
+
context "(metric)" do
|
39
|
+
end
|
40
|
+
context "(nonstandard)" do
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
it "parses a simple example correctly" do
|
45
|
+
result = Ingreedy.parse("1 lb potatoes")
|
46
|
+
|
47
|
+
expect(result.amount).to eq(1)
|
48
|
+
expect(result.unit).to eq(:pound)
|
49
|
+
expect(result.ingredient).to eq("potatoes")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "english units" do
|
54
|
+
context "abbreviated" do
|
55
|
+
{
|
56
|
+
"1 c flour" => :cup,
|
57
|
+
"1 c. flour" => :cup,
|
58
|
+
"1 fl oz flour" => :fluid_ounce,
|
59
|
+
"1 fl. oz. flour" => :fluid_ounce,
|
60
|
+
"2 gal flour" => :gallon,
|
61
|
+
"2 gal. flour" => :gallon,
|
62
|
+
"1 ounce flour" => :ounce,
|
63
|
+
"2 ounces flour" => :ounce,
|
64
|
+
"1 oz flour" => :ounce,
|
65
|
+
"1 oz. flour" => :ounce,
|
66
|
+
"2 pt flour" => :pint,
|
67
|
+
"2 pt. flour" => :pint,
|
68
|
+
"1 lb flour" => :pound,
|
69
|
+
"1 lb. flour" => :pound,
|
70
|
+
"1 pound flour" => :pound,
|
71
|
+
"2 pounds flour" => :pound,
|
72
|
+
"2 qt flour" => :quart,
|
73
|
+
"2 qt. flour" => :quart,
|
74
|
+
"2 qts flour" => :quart,
|
75
|
+
"2 qts. flour" => :quart,
|
76
|
+
"2 tbsp flour" => :tablespoon,
|
77
|
+
"2 tbsp. flour" => :tablespoon,
|
78
|
+
"2 Tbs flour" => :tablespoon,
|
79
|
+
"2 Tbs. flour" => :tablespoon,
|
80
|
+
"2 T flour" => :tablespoon,
|
81
|
+
"2 T. flour" => :tablespoon,
|
82
|
+
"2 tsp flour" => :teaspoon,
|
83
|
+
"2 tsp. flour" => :teaspoon,
|
84
|
+
"2 t flour" => :teaspoon,
|
85
|
+
"2 t. flour" => :teaspoon,
|
86
|
+
"12oz tequila" => :ounce,
|
87
|
+
"2 TSP flour" => :teaspoon,
|
88
|
+
"1 LB flour" => :pound,
|
89
|
+
"1 tSP sugar" => :teaspoon,
|
90
|
+
}.each do |query, expected|
|
91
|
+
it "parses the units correctly" do
|
92
|
+
expect(Ingreedy.parse(query)).to parse_the_unit(expected)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
context "long form" do
|
98
|
+
{
|
99
|
+
"1 cup flour" => :cup,
|
100
|
+
"2 cups flour" => :cup,
|
101
|
+
"1 fluid ounce flour" => :fluid_ounce,
|
102
|
+
"2 fluid ounces flour" => :fluid_ounce,
|
103
|
+
"2 gallon flour" => :gallon,
|
104
|
+
"2 gallons flour" => :gallon,
|
105
|
+
"2 pint flour" => :pint,
|
106
|
+
"2 pints flour" => :pint,
|
107
|
+
"1 quart flour" => :quart,
|
108
|
+
"2 quarts flour" => :quart,
|
109
|
+
"2 tablespoon flour" => :tablespoon,
|
110
|
+
"2 tablespoons flour" => :tablespoon,
|
111
|
+
"2 teaspoon flour" => :teaspoon,
|
112
|
+
"2 teaspoons flour" => :teaspoon,
|
113
|
+
}.each do |query, expected|
|
114
|
+
it "parses the units correctly" do
|
115
|
+
expect(Ingreedy.parse(query)).to parse_the_unit(expected)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe "metric units" do
|
122
|
+
context "abbreviated" do
|
123
|
+
{
|
124
|
+
"1 g flour" => :gram,
|
125
|
+
"1 g. flour" => :gram,
|
126
|
+
"1 gr flour" => :gram,
|
127
|
+
"1 gr. flour" => :gram,
|
128
|
+
"1 kg flour" => :kilogram,
|
129
|
+
"1 kg. flour" => :kilogram,
|
130
|
+
"1 l water" => :liter,
|
131
|
+
"1 l. water" => :liter,
|
132
|
+
"1 mg water" => :milligram,
|
133
|
+
"1 mg. water" => :milligram,
|
134
|
+
"1 ml water" => :milliliter,
|
135
|
+
"1 ml. water" => :milliliter,
|
136
|
+
}.each do |query, expected|
|
137
|
+
it "parses the units correctly" do
|
138
|
+
expect(Ingreedy.parse(query)).to parse_the_unit(expected)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
context "long form" do
|
144
|
+
{
|
145
|
+
"1 gram flour" => :gram,
|
146
|
+
"2 grams flour" => :gram,
|
147
|
+
"1 kilogram flour" => :kilogram,
|
148
|
+
"2 kilograms flour" => :kilogram,
|
149
|
+
"1 liter water" => :liter,
|
150
|
+
"2 liters water" => :liter,
|
151
|
+
"1 milligram water" => :milligram,
|
152
|
+
"2 milligrams water" => :milligram,
|
153
|
+
"1 milliliter water" => :milliliter,
|
154
|
+
"2 milliliters water" => :milliliter,
|
155
|
+
}.each do |query, expected|
|
156
|
+
it "parses the units correctly" do
|
157
|
+
expect(Ingreedy.parse(query)).to parse_the_unit(expected)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
describe "nonstandard units" do
|
164
|
+
{
|
165
|
+
"1 pinch pepper" => :pinch,
|
166
|
+
"2 pinches pepper" => :pinch,
|
167
|
+
"1 dash salt" => :dash,
|
168
|
+
"2 dashes salt" => :dash,
|
169
|
+
"1 touch hot sauce" => :touch,
|
170
|
+
"2 touches hot sauce" => :touch,
|
171
|
+
"1 handful rice" => :handful,
|
172
|
+
"2 handfuls rice" => :handful,
|
173
|
+
"1 stick of butter" => :stick,
|
174
|
+
"2 cloves of garlic" => :clove,
|
175
|
+
"1 can of tomatoes" => :can,
|
176
|
+
}.each do |query, expected|
|
177
|
+
it "parses the units correctly" do
|
178
|
+
expect(Ingreedy.parse(query)).to parse_the_unit(expected)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
describe "without units" do
|
184
|
+
it "parses correctly" do
|
185
|
+
result = Ingreedy.parse "3 eggs, lightly beaten"
|
186
|
+
|
187
|
+
expect(result.amount).to eq(3)
|
188
|
+
expect(result.unit).to be_nil
|
189
|
+
expect(result.ingredient).to eq("eggs, lightly beaten")
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
describe "container as part of quantity" do
|
194
|
+
it "parses correctly" do
|
195
|
+
result = Ingreedy.parse "160g (2 cans) of tomatoes"
|
196
|
+
|
197
|
+
expect(result.amount).to eq(160)
|
198
|
+
expect(result.unit).to eq(:gram)
|
199
|
+
expect(result.container_amount).to eq(2)
|
200
|
+
expect(result.container_unit).to eq(:can)
|
201
|
+
expect(result.ingredient).to eq("tomatoes")
|
202
|
+
end
|
203
|
+
|
204
|
+
context "on language without preposition" do
|
205
|
+
before(:all) do
|
206
|
+
Ingreedy.dictionaries[:id] = {
|
207
|
+
units: {
|
208
|
+
can: ["kaleng"],
|
209
|
+
gram: ["g"],
|
210
|
+
to_taste: ["secukupnya"],
|
211
|
+
},
|
212
|
+
}
|
213
|
+
Ingreedy.locale = :id
|
214
|
+
end
|
215
|
+
|
216
|
+
after(:all) do
|
217
|
+
Ingreedy.locale = nil
|
218
|
+
end
|
219
|
+
|
220
|
+
it "parses correctly" do
|
221
|
+
result = Ingreedy.parse "160g (2 kaleng) tomat"
|
222
|
+
|
223
|
+
expect(result.amount).to eq(160)
|
224
|
+
expect(result.unit).to eq(:gram)
|
225
|
+
expect(result.container_amount).to eq(2)
|
226
|
+
expect(result.container_unit).to eq(:can)
|
227
|
+
expect(result.ingredient).to eq("tomat")
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
describe "with 'a' as quantity and preposition 'of'" do
|
233
|
+
it "parses correctly" do
|
234
|
+
result = Ingreedy.parse "a dash of ginger"
|
235
|
+
|
236
|
+
expect(result.amount).to eq(1)
|
237
|
+
expect(result.unit).to eq(:dash)
|
238
|
+
expect(result.ingredient).to eq("ginger")
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
describe "with 'reverse format'" do
|
243
|
+
it "works with words containing a 'word digit'" do
|
244
|
+
result = Ingreedy.parse "salt 200g"
|
245
|
+
|
246
|
+
expect(result.amount).to eq(200)
|
247
|
+
expect(result.unit).to eq(:gram)
|
248
|
+
expect(result.ingredient).to eq("salt")
|
249
|
+
end
|
250
|
+
|
251
|
+
it "works with words ending on a 'word digit'" do
|
252
|
+
result = Ingreedy.parse "quinoa 200g"
|
253
|
+
|
254
|
+
expect(result.amount).to eq(200)
|
255
|
+
expect(result.unit).to eq(:gram)
|
256
|
+
expect(result.ingredient).to eq("quinoa")
|
257
|
+
end
|
258
|
+
|
259
|
+
it "works with approximate quantities" do
|
260
|
+
result = Ingreedy.parse "salt to taste"
|
261
|
+
|
262
|
+
expect(result.ingredient).to eq("salt")
|
263
|
+
expect(result.amount).to be_nil
|
264
|
+
expect(result.unit).to eq(:to_taste)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
describe "Given a range" do
|
269
|
+
it "works with simple ranges" do
|
270
|
+
result = Ingreedy.parse "1-2 tbsp salt"
|
271
|
+
|
272
|
+
expect(result.amount).to eq([1, 2])
|
273
|
+
expect(result.unit).to eq(:tablespoon)
|
274
|
+
expect(result.ingredient).to eq("salt")
|
275
|
+
end
|
276
|
+
|
277
|
+
it "works with spaces" do
|
278
|
+
result = Ingreedy.parse "1 - 2 tbsp salt"
|
279
|
+
|
280
|
+
expect(result.amount).to eq([1, 2])
|
281
|
+
expect(result.unit).to eq(:tablespoon)
|
282
|
+
expect(result.ingredient).to eq("salt")
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
describe "parsing in language with no prepositions" do
|
287
|
+
before(:all) do
|
288
|
+
Ingreedy.dictionaries[:id] = {
|
289
|
+
units: {
|
290
|
+
gram: ["g"],
|
291
|
+
to_taste: ["secukupnya"],
|
292
|
+
},
|
293
|
+
}
|
294
|
+
Ingreedy.locale = :id
|
295
|
+
end
|
296
|
+
|
297
|
+
after(:all) do
|
298
|
+
Ingreedy.locale = nil
|
299
|
+
end
|
300
|
+
|
301
|
+
it "parses correctly" do
|
302
|
+
result = Ingreedy.parse "garam secukupnya"
|
303
|
+
|
304
|
+
expect(result.amount).to be_nil
|
305
|
+
expect(result.unit).to eq(:to_taste)
|
306
|
+
expect(result.ingredient).to eq("garam")
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
describe "custom dictionaries" do
|
311
|
+
context "using Ingreedy.locale=" do
|
312
|
+
before(:all) do
|
313
|
+
Ingreedy.dictionaries[:fr] = {
|
314
|
+
numbers: { "une" => 1 },
|
315
|
+
prepositions: ["de"],
|
316
|
+
units: { dash: ["pincee"] },
|
317
|
+
}
|
318
|
+
Ingreedy.locale = :fr
|
319
|
+
end
|
320
|
+
|
321
|
+
after(:all) do
|
322
|
+
Ingreedy.locale = nil
|
323
|
+
end
|
324
|
+
|
325
|
+
it "parses correctly" do
|
326
|
+
result = Ingreedy.parse "une pincee de sucre"
|
327
|
+
|
328
|
+
expect(result.amount).to eq(1)
|
329
|
+
expect(result.unit).to eq(:dash)
|
330
|
+
expect(result.ingredient).to eq("sucre")
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
context "using I18n.locale" do
|
335
|
+
before(:each) do
|
336
|
+
Ingreedy.dictionaries[:de] = { units: { dash: ["prise"] } }
|
337
|
+
stub_const "I18n", double("I18n", locale: :de)
|
338
|
+
end
|
339
|
+
|
340
|
+
it "parses correctly" do
|
341
|
+
result = Ingreedy.parse "1 Prise Zucker"
|
342
|
+
|
343
|
+
expect(result.amount).to eq(1)
|
344
|
+
expect(result.unit).to eq(:dash)
|
345
|
+
expect(result.ingredient).to eq("Zucker")
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
context "unknown locale" do
|
350
|
+
before(:all) do
|
351
|
+
Ingreedy.locale = :da
|
352
|
+
end
|
353
|
+
|
354
|
+
after(:all) do
|
355
|
+
Ingreedy.locale = nil
|
356
|
+
end
|
357
|
+
|
358
|
+
it "raises an informative exception" do
|
359
|
+
expect do
|
360
|
+
Ingreedy.parse "1 tsk salt"
|
361
|
+
end.to raise_exception("No dictionary found for :da locale")
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
context "Dictionary with no units" do
|
366
|
+
it "raises an informative exception" do
|
367
|
+
expect do
|
368
|
+
Ingreedy.dictionaries[:da] = {}
|
369
|
+
end.to raise_exception("No units found in dictionary")
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
describe "ingredient formatting" do
|
375
|
+
it "strips preceding or trailing whitespace" do
|
376
|
+
expect(Ingreedy.parse("1 cup flour ").ingredient).to eq("flour")
|
377
|
+
end
|
378
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
if ENV["CI"]
|
2
|
+
require "coveralls"
|
3
|
+
Coveralls.wear!
|
4
|
+
end
|
5
|
+
|
6
|
+
require "ingreedy"
|
7
|
+
require "parslet/rig/rspec"
|
8
|
+
require "pry"
|
9
|
+
|
10
|
+
RSpec::Matchers.define :parse_the_unit do |unit|
|
11
|
+
match do |ingreedy_output|
|
12
|
+
ingreedy_output.unit == unit
|
13
|
+
end
|
14
|
+
failure_message do |ingreedy_output|
|
15
|
+
<<-MSG
|
16
|
+
expected to parse the unit #{unit} from the query
|
17
|
+
'#{ingreedy_output.original_query}.'
|
18
|
+
got '#{ingreedy_output.unit}' instead
|
19
|
+
MSG
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
RSpec::Matchers.define :parse_the_amount do |amount|
|
24
|
+
match do |ingreedy_output|
|
25
|
+
ingreedy_output.amount == amount
|
26
|
+
end
|
27
|
+
failure_message do |ingreedy_output|
|
28
|
+
<<-MSG
|
29
|
+
expected to parse the amount #{amount} from the query
|
30
|
+
'#{ingreedy_output.original_query}.'
|
31
|
+
got '#{ingreedy_output.amount}' instead
|
32
|
+
MSG
|
33
|
+
end
|
34
|
+
end
|