ingreedy 0.0.4 → 0.0.5

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 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: