ingreedy 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b9b4b6105807082714db4a684b0667635a6c0a82
4
+ data.tar.gz: 85c9ab384b6c419520723d9719b9f82d6ebca495
5
+ SHA512:
6
+ metadata.gz: 3321ead5195bc89f49a38e9b9fd762f624e9dfed41486a703d05d4ffade3a5f4f1ea3898b16b99d598a0790ea3f108940851e3b6ab831abc388d1160cf095771
7
+ data.tar.gz: 545113b12845639fb146fe26d9e263044452bcab8be636f5d83937752b0e74aaa07dbf6050550684e6ce444c5acdb6f3ff74ace23068c3cb7754af46e4ef6053
data/lib/ingreedy.rb CHANGED
@@ -1,13 +1,20 @@
1
1
  path = File.expand_path(File.join(File.dirname(__FILE__), 'ingreedy'))
2
2
 
3
+ require File.join(path, 'case_insensitive_parser')
3
4
  require File.join(path, 'ingreedy_parser')
5
+ require File.join(path, 'dictionary_collection')
4
6
 
5
7
  module Ingreedy
6
8
  class << self
9
+ attr_accessor :locale
10
+
7
11
  def parse(query)
8
- parser = IngreedyParser.new(query)
12
+ parser = Parser.new(query)
9
13
  parser.parse
10
- parser
14
+ end
15
+
16
+ def dictionaries
17
+ @dictionaries ||= DictionaryCollection.new
11
18
  end
12
19
  end
13
20
  end
@@ -0,0 +1,60 @@
1
+ require 'parslet'
2
+
3
+ module Ingreedy
4
+
5
+ class AmountParser < Parslet::Parser
6
+ include CaseInsensitiveParser
7
+
8
+ def initialize(options = {})
9
+ @key_prefix = options[:key_prefix] ? "#{options[:key_prefix]}_" : ''
10
+ end
11
+
12
+ def capture_key(key)
13
+ (@key_prefix + key.to_s).to_sym
14
+ end
15
+
16
+ rule(:whitespace) do
17
+ match("\s")
18
+ end
19
+
20
+ rule(:integer) do
21
+ match('[0-9]').repeat(1)
22
+ end
23
+
24
+ rule(:float) do
25
+ integer.maybe >>
26
+ str('.') >> integer
27
+ end
28
+
29
+ rule(:fraction) do
30
+ (integer.as(capture_key(:integer_amount)) >> whitespace).maybe >>
31
+ (integer >> match('/') >> integer).as(capture_key(:fraction_amount))
32
+ end
33
+
34
+ rule(:word_digit) do
35
+ word_digits.map { |d| stri(d) }.inject(:|) || any
36
+ end
37
+
38
+ rule(:amount_unit_separator) do
39
+ whitespace | str('-')
40
+ end
41
+
42
+ rule(:amount) do
43
+ fraction |
44
+ float.as(capture_key(:float_amount)) |
45
+ integer.as(capture_key(:integer_amount)) |
46
+ word_digit.as(capture_key(:word_integer_amount)) >> amount_unit_separator
47
+ end
48
+
49
+ root(:amount)
50
+
51
+ private
52
+
53
+ def word_digits
54
+ Ingreedy.dictionaries.current.numbers.keys
55
+ end
56
+
57
+ end
58
+
59
+
60
+ end
@@ -0,0 +1,12 @@
1
+ module Ingreedy
2
+ module CaseInsensitiveParser
3
+
4
+ def stri(str)
5
+ key_chars = str.split(//)
6
+ key_chars.
7
+ collect! { |char| match["#{char.upcase}#{char.downcase}"] }.
8
+ reduce(:>>)
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ module Ingreedy
2
+ class Dictionary
3
+ attr_reader :units, :numbers, :prepositions
4
+
5
+ def initialize(entries = {})
6
+ @units = entries[:units] || raise('No units found in dictionary')
7
+ @numbers = entries[:numbers] || {}
8
+ @prepositions = entries[:prepositions] || []
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,35 @@
1
+ require 'yaml'
2
+ require_relative 'dictionary'
3
+
4
+ module Ingreedy
5
+ class DictionaryCollection
6
+ def initialize
7
+ @collection = {}
8
+ end
9
+
10
+ def []=(locale, attributes)
11
+ @collection[locale] = Dictionary.new(attributes)
12
+ end
13
+
14
+ def current
15
+ @collection[locale] ||= Dictionary.new load_yaml(locale)
16
+ end
17
+
18
+ private
19
+
20
+ def locale
21
+ Ingreedy.locale || i18n_gem_locale || :en
22
+ end
23
+
24
+ def i18n_gem_locale
25
+ I18n.locale if defined?(I18n)
26
+ end
27
+
28
+ def load_yaml(locale)
29
+ path = File.expand_path(File.join(File.dirname(__FILE__), 'dictionaries', "#{locale}.yml"))
30
+ YAML.load_file(path)
31
+ rescue Errno::ENOENT
32
+ raise "No dictionary found for :#{locale} locale"
33
+ end
34
+ end
35
+ end
@@ -1,103 +1,151 @@
1
- class IngreedyParser
1
+ require 'parslet'
2
2
 
3
- attr_reader :amount, :unit, :ingredient, :query
3
+ require_relative 'amount_parser'
4
+ require_relative 'rationalizer'
5
+ require_relative 'unit_parser'
6
+ require_relative 'unit_variation_mapper'
4
7
 
5
- def initialize(query)
6
- @query = query
7
- end
8
+ module Ingreedy
8
9
 
9
- def parse
10
- ingreedy_regex = %r{
11
- (?<amount> .?\d+(\.\d+)? ) {0}
12
- (?<fraction> \d\/\d ) {0}
10
+ class Parser < Parslet::Parser
13
11
 
14
- (?<container_amount> \d+(\.\d+)?) {0}
15
- (?<container_unit> .+) {0}
16
- (?<container_size> \(\g<container_amount>\s\g<container_unit>\)) {0}
17
- (?<unit_and_ingredient> .+ ) {0}
12
+ attr_reader :original_query
13
+ Result = Struct.new(:amount, :unit, :ingredient, :original_query)
18
14
 
19
- (\g<fraction>\s)?(\g<amount>\s?)?(\g<fraction>\s)?(\g<container_size>\s)?\g<unit_and_ingredient>
20
- }x
21
- results = ingreedy_regex.match(@query)
15
+ rule(:amount) do
16
+ AmountParser.new.as(:amount)
17
+ end
22
18
 
23
- @ingredient_string = results[:unit_and_ingredient]
24
- @container_amount = results[:container_amount]
25
- @container_unit = results[:container_unit]
19
+ rule(:whitespace) do
20
+ match("\s")
21
+ end
26
22
 
27
- parse_amount results[:amount], results[:fraction]
28
- parse_unit_and_ingredient
29
- end
23
+ rule(:container_amount) do
24
+ AmountParser.new(key_prefix: 'container')
25
+ end
30
26
 
31
- private
27
+ rule(:unit) do
28
+ UnitParser.new
29
+ end
32
30
 
33
- def parse_amount(amount_string, fraction_string)
34
- fraction = 0
35
- if fraction_string
36
- numbers = fraction_string.split("\/")
37
- numerator = numbers[0].to_f
38
- denominator = numbers[1].to_f
39
- fraction = numerator / denominator
31
+ rule(:unit_and_preposition) do
32
+ if prepositions.empty?
33
+ unit.as(:unit) >> (whitespace | any.absent?)
34
+ else
35
+ unit.as(:unit) >> (preposition | whitespace | any.absent?)
36
+ end
40
37
  end
41
- @amount = amount_string.to_f + fraction
42
- @amount *= @container_amount.to_f if @container_amount
43
- end
44
- def set_unit_variations(unit, variations)
45
- variations.each do |abbrev|
46
- @unit_map[abbrev] = unit
38
+
39
+ rule(:preposition) do
40
+ whitespace >>
41
+ prepositions.map { |con| str(con) }.inject(:|) >>
42
+ whitespace
47
43
  end
48
- end
49
- def create_unit_map
50
- @unit_map = {}
51
- # english units
52
- set_unit_variations :cup, ["c.", "c", "cup", "cups"]
53
- set_unit_variations :fluid_ounce, ["fl. oz.", "fl oz", "fluid ounce", "fluid ounces"]
54
- set_unit_variations :gallon, ["gal", "gal.", "gallon", "gallons"]
55
- set_unit_variations :ounce, ["oz", "oz.", "ounce", "ounces"]
56
- set_unit_variations :pint, ["pt", "pt.", "pint", "pints"]
57
- set_unit_variations :pound, ["lb", "lb.", "pound", "pounds"]
58
- set_unit_variations :quart, ["qt", "qt.", "qts", "qts.", "quart", "quarts"]
59
- set_unit_variations :tablespoon, ["tbsp.", "tbsp", "T", "T.", "tablespoon", "tablespoons"]
60
- set_unit_variations :teaspoon, ["tsp.", "tsp", "t", "t.", "teaspoon", "teaspoons"]
61
- # metric units
62
- set_unit_variations :gram, ["g", "g.", "gr", "gr.", "gram", "grams"]
63
- set_unit_variations :kilogram, ["kg", "kg.", "kilogram", "kilograms"]
64
- set_unit_variations :liter, ["l", "l.", "liter", "liters"]
65
- set_unit_variations :milligram, ["mg", "mg.", "milligram", "milligrams"]
66
- set_unit_variations :milliliter, ["ml", "ml.", "milliliter", "milliliters"]
67
- end
68
- def parse_unit
69
- create_unit_map if @unit_map.nil?
70
-
71
- @unit_map.each do |abbrev, unit|
72
- if @ingredient_string.start_with?(abbrev + " ")
73
- # if a unit is found, remove it from the ingredient string
74
- @ingredient_string.sub! abbrev, ""
75
- @unit = unit
76
- end
44
+
45
+ rule(:container_unit) do
46
+ UnitParser.new
47
+ end
48
+
49
+ rule(:amount_unit_separator) do
50
+ whitespace | str('-')
51
+ end
52
+
53
+ rule(:container_size) do
54
+ # e.g. (12 ounce) or 12 ounce
55
+ str('(').maybe >>
56
+ container_amount.as(:container_amount) >>
57
+ amount_unit_separator.maybe >>
58
+ container_unit.as(:unit) >>
59
+ str(')').maybe >> whitespace
60
+ end
61
+
62
+ rule(:amount_and_unit) do
63
+ amount >>
64
+ whitespace.maybe >>
65
+ unit_and_preposition.maybe >>
66
+ container_size.maybe
67
+ end
68
+
69
+ rule(:quantity) do
70
+ amount_and_unit | unit_and_preposition
71
+ end
72
+
73
+ rule(:standard_format) do
74
+ # e.g. 1/2 (12 oz) can black beans
75
+ quantity >> any.repeat.as(:ingredient)
76
+ end
77
+
78
+ rule(:reverse_format) do
79
+ # e.g. flour 200g
80
+ ((whitespace >> quantity).absent? >> any).repeat.as(:ingredient) >> whitespace >> quantity
81
+ end
82
+
83
+ rule(:ingredient_addition) do
84
+ standard_format | reverse_format
85
+ end
86
+
87
+ root :ingredient_addition
88
+
89
+ def initialize(original_query)
90
+ @original_query = original_query
77
91
  end
78
92
 
79
- # if no unit yet, try it again downcased
80
- if @unit.nil?
81
- @ingredient_string.downcase!
82
- @unit_map.each do |abbrev, unit|
83
- if @ingredient_string.start_with?(abbrev + " ")
84
- # if a unit is found, remove it from the ingredient string
85
- @ingredient_string.sub! abbrev, ""
86
- @unit = unit
87
- end
93
+ def parse
94
+ result = Result.new
95
+ result[:original_query] = original_query
96
+
97
+ parslet_output = super(original_query)
98
+
99
+ result[:amount] = rationalize_total_amount(parslet_output[:amount], parslet_output[:container_amount])
100
+
101
+ if parslet_output[:unit]
102
+ result[:unit] = convert_unit_variation_to_canonical(parslet_output[:unit].to_s)
88
103
  end
104
+
105
+ result[:ingredient] = parslet_output[:ingredient].to_s.lstrip.rstrip #TODO cheating
106
+
107
+ result
108
+ end
109
+
110
+ private
111
+
112
+ def prepositions
113
+ Ingreedy.dictionaries.current.prepositions
114
+ end
115
+
116
+ def convert_unit_variation_to_canonical(unit_variation)
117
+ UnitVariationMapper.unit_from_variation(unit_variation)
89
118
  end
90
119
 
91
- # if we still don't have a unit, check to see if we have a container unit
92
- if @unit.nil? and @container_unit
93
- @unit_map.each do |abbrev, unit|
94
- @unit = unit if abbrev == @container_unit
120
+ def rationalize_total_amount(amount, container_amount)
121
+ if container_amount
122
+ rationalize_amount(amount) * rationalize_amount(container_amount, 'container_')
123
+ else
124
+ rationalize_amount(amount)
95
125
  end
96
126
  end
97
- end
98
- def parse_unit_and_ingredient
99
- parse_unit
100
- # clean up ingredient string
101
- @ingredient = @ingredient_string.lstrip.rstrip
127
+
128
+ def rationalize_amount(amount, capture_key_prefix = '')
129
+ return unless amount
130
+ integer = amount["#{capture_key_prefix}integer_amount".to_sym]
131
+ integer &&= integer.to_s
132
+
133
+ float = amount["#{capture_key_prefix}float_amount".to_sym]
134
+ float &&= float.to_s
135
+
136
+ fraction = amount["#{capture_key_prefix}fraction_amount".to_sym]
137
+ fraction &&= fraction.to_s
138
+
139
+ word = amount["#{capture_key_prefix}word_integer_amount".to_sym]
140
+ word &&= word.to_s
141
+
142
+ Rationalizer.rationalize(
143
+ integer: integer,
144
+ float: float,
145
+ fraction: fraction,
146
+ word: word
147
+ )
148
+ end
149
+
102
150
  end
103
151
  end
@@ -0,0 +1,41 @@
1
+ module Ingreedy
2
+
3
+ class Rationalizer
4
+
5
+ def self.rationalize(options)
6
+ new(options).rationalize
7
+ end
8
+
9
+ def initialize(options)
10
+ @integer = options.fetch(:integer, nil)
11
+ @float = options.fetch(:float, nil)
12
+ @fraction = options.fetch(:fraction, nil)
13
+ @word = options.fetch(:word, nil)
14
+ end
15
+
16
+ def rationalize
17
+ if @word
18
+ result = rationalize_word
19
+ elsif @fraction
20
+ result = @fraction.to_r
21
+ if @integer
22
+ result += @integer.to_i
23
+ end
24
+ elsif @integer
25
+ result = @integer.to_r
26
+ elsif @float
27
+ result = @float.to_r
28
+ end
29
+
30
+ result
31
+ end
32
+
33
+ private
34
+
35
+ def rationalize_word
36
+ Ingreedy.dictionaries.current.numbers[@word.downcase]
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,20 @@
1
+ module Ingreedy
2
+
3
+ class UnitParser < Parslet::Parser
4
+ include CaseInsensitiveParser
5
+
6
+ rule(:unit) do
7
+ unit_variations.map { |var| str(var) | stri(var) }.reduce(:|)
8
+ end
9
+
10
+ root :unit
11
+
12
+ private
13
+
14
+ def unit_variations
15
+ UnitVariationMapper.all_variations
16
+ end
17
+
18
+ end
19
+
20
+ end
@@ -0,0 +1,31 @@
1
+ module Ingreedy
2
+ class UnitVariationMapper
3
+
4
+ def self.all_variations
5
+ # Return these in order of size, descending
6
+ # That way, the longer versions will try to be parsed first, then the shorter versions
7
+ # e.g. so '1 cup flour' will be parsed as 'cup' instead of 'c'
8
+ variations_map.values.flatten.sort { |a, b| b.length <=> a.length }
9
+ end
10
+
11
+ def self.unit_from_variation(variation)
12
+ return if variations_map.empty?
13
+ hash_entry_as_array = variations_map.detect { |unit, variations| variations.include?(variation) }
14
+
15
+ if hash_entry_as_array
16
+ hash_entry_as_array.first
17
+ else
18
+ # try again with the variation downcased
19
+ # this is a hacky way to deal with the abbreviations for teaspoon and tablespoon
20
+ hash_entry_as_array = variations_map.detect { |unit, variations| variations.include?(variation.downcase) }
21
+ hash_entry_as_array.first
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def self.variations_map
28
+ Ingreedy.dictionaries.current.units
29
+ end
30
+ end
31
+ end
@@ -1,4 +1,4 @@
1
1
  module Ingreedy
2
- VERSION = '0.0.4'
2
+ VERSION = '0.0.5'
3
3
  end
4
4
 
metadata CHANGED
@@ -1,59 +1,169 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: ingreedy
3
- version: !ruby/object:Gem::Version
4
- prerelease:
5
- version: 0.0.4
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.5
6
5
  platform: ruby
7
- authors:
6
+ authors:
8
7
  - Ian C. Anderson
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
-
13
- date: 2011-09-05 00:00:00 -04:00
14
- default_executable:
15
- dependencies: []
16
-
17
- description: Natural language recipe ingredient parser that supports numeric amount, units, and ingredient
18
- email:
11
+ date: 2015-10-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: parslet
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.7.0
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.7.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: 1.7.0
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.7.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: rake
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.9'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0.9'
43
+ type: :development
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '0.9'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0.9'
53
+ - !ruby/object:Gem::Dependency
54
+ name: rspec
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: 3.3.0
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 3.3.0
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: 3.3.0
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 3.3.0
73
+ - !ruby/object:Gem::Dependency
74
+ name: rspec-its
75
+ requirement: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - "~>"
78
+ - !ruby/object:Gem::Version
79
+ version: 1.2.0
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 1.2.0
83
+ type: :development
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 1.2.0
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: 1.2.0
93
+ - !ruby/object:Gem::Dependency
94
+ name: coveralls
95
+ requirement: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - "~>"
98
+ - !ruby/object:Gem::Version
99
+ version: 0.7.0
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: 0.7.0
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: 0.7.0
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: 0.7.0
113
+ - !ruby/object:Gem::Dependency
114
+ name: pry
115
+ requirement: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ type: :development
121
+ prerelease: false
122
+ version_requirements: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ description: Natural language recipe ingredient parser that supports numeric amount,
128
+ units, and ingredient
129
+ email:
19
130
  - anderson.ian.c@gmail.com
20
131
  executables: []
21
-
22
132
  extensions: []
23
-
24
133
  extra_rdoc_files: []
25
-
26
- files:
134
+ files:
135
+ - lib/ingreedy.rb
136
+ - lib/ingreedy/amount_parser.rb
137
+ - lib/ingreedy/case_insensitive_parser.rb
138
+ - lib/ingreedy/dictionary.rb
139
+ - lib/ingreedy/dictionary_collection.rb
27
140
  - lib/ingreedy/ingreedy_parser.rb
141
+ - lib/ingreedy/rationalizer.rb
142
+ - lib/ingreedy/unit_parser.rb
143
+ - lib/ingreedy/unit_variation_mapper.rb
28
144
  - lib/ingreedy/version.rb
29
- - lib/ingreedy.rb
30
- has_rdoc: true
31
145
  homepage: http://github.com/iancanderson/ingreedy
32
146
  licenses: []
33
-
147
+ metadata: {}
34
148
  post_install_message:
35
149
  rdoc_options: []
36
-
37
- require_paths:
150
+ require_paths:
38
151
  - lib
39
- required_ruby_version: !ruby/object:Gem::Requirement
40
- none: false
41
- requirements:
152
+ required_ruby_version: !ruby/object:Gem::Requirement
153
+ requirements:
42
154
  - - ">="
43
- - !ruby/object:Gem::Version
44
- version: "0"
45
- required_rubygems_version: !ruby/object:Gem::Requirement
46
- none: false
47
- requirements:
155
+ - !ruby/object:Gem::Version
156
+ version: '0'
157
+ required_rubygems_version: !ruby/object:Gem::Requirement
158
+ requirements:
48
159
  - - ">="
49
- - !ruby/object:Gem::Version
50
- version: "0"
160
+ - !ruby/object:Gem::Version
161
+ version: '0'
51
162
  requirements: []
52
-
53
163
  rubyforge_project:
54
- rubygems_version: 1.6.2
164
+ rubygems_version: 2.4.8
55
165
  signing_key:
56
- specification_version: 3
166
+ specification_version: 4
57
167
  summary: Recipe parser
58
168
  test_files: []
59
-
169
+ has_rdoc: