emeals 0.0.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c883efc225e72b90fe4943bcce321f9933664452
4
+ data.tar.gz: f7b246806d4fc330a5bf42235871ec8ae4615e48
5
+ SHA512:
6
+ metadata.gz: 4f3596e65d12aeb0d1a58362eb83b408aa83fa05210b4b53b81fd63ecf921eebf1f39818edf17481bbb3f28cc0c1e39995888ad82cc68c2cbafc88e70586033f
7
+ data.tar.gz: 27e5874c713d6de1662e74a9d56cb4d54385eb794ee5369925db7f81e6e447fc9d5986c02153f8fb26c8ee8ba509a03a2b604608b69eb4ac5e2634e8aba1ed5f
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .idea
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
@@ -0,0 +1 @@
1
+ 2.0.0
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - rbx-19mode
6
+ script:
7
+ - sudo apt-get install xpdf
8
+ - bundle exec rake
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in emeals.gemspec
4
+ gemspec
5
+
6
+ gem 'rake'
7
+ gem 'rspec'
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Matt Moriarity
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,33 @@
1
+ [![Build Status](https://travis-ci.org/mjm/emeals.png?branch=master)](https://travis-ci.org/mjm/emeals)
2
+
3
+ # Emeals
4
+
5
+ A library for reading menus from [eMeals][].
6
+
7
+ [emeals]: http://emeals.com
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'emeals'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install emeals
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Contributing
28
+
29
+ 1. Fork it
30
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
31
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
32
+ 4. Push to the branch (`git push origin my-new-feature`)
33
+ 5. Create new Pull Request
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'emeals/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "emeals"
8
+ gem.version = Emeals::VERSION
9
+ gem.authors = ["Matt Moriarity"]
10
+ gem.email = ["matt@mattmoriarity.com"]
11
+ gem.description = %q{A client for interpreting eMeals menus.}
12
+ gem.summary = %q{eMeals Client}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ end
@@ -0,0 +1,6 @@
1
+ require "emeals/version"
2
+
3
+ module Emeals
4
+ end
5
+
6
+ require "emeals/client"
@@ -0,0 +1,34 @@
1
+ require 'emeals/menu'
2
+
3
+ require 'fileutils'
4
+ require 'tempfile'
5
+
6
+ class Emeals::Client
7
+ def parse(filename_or_file)
8
+ temp_file = copy_to_temp_file(filename_or_file)
9
+ text = pdf_to_text(temp_file.path)
10
+
11
+ Emeals::Menu.parse(text)
12
+ end
13
+
14
+ private
15
+
16
+ def copy_to_temp_file(filename_or_file)
17
+ Tempfile.open(['menu', '.pdf']) do |temp|
18
+ temp.binmode
19
+ temp.write(filename_or_file.is_a?(String) ? File.read(filename_or_file) : filename_or_file.read)
20
+ temp
21
+ end
22
+ end
23
+
24
+ def pdf_to_text(filename)
25
+ system(pdf_to_text_path, '-raw', '-enc', 'UTF-8', filename)
26
+ File.read(filename.sub(/\.pdf$/, '.txt'))
27
+ end
28
+
29
+ def pdf_to_text_path
30
+ path = `which pdftotext`.chop
31
+ return '/opt/boxen/homebrew/bin/pdftotext' if path.empty?
32
+ path
33
+ end
34
+ end
@@ -0,0 +1,61 @@
1
+ # encoding: utf-8
2
+
3
+ class Emeals::Dish
4
+ attr_accessor :name, :ingredients, :instructions
5
+
6
+ def initialize(name)
7
+ @name = name
8
+ @ingredients = []
9
+ @instructions = []
10
+ end
11
+ end
12
+
13
+ class Emeals::Quantity
14
+ UNITS = %w(teaspoon tablespoon cup oz lb bag clove).map(&:to_sym)
15
+ UNITS_WITH_PLURALS = UNITS + UNITS.map {|u| "#{u}s".to_sym }
16
+
17
+ attr_reader :amount, :unit
18
+
19
+ def initialize(amount, unit = nil)
20
+ @amount = to_amount(amount)
21
+ @unit = unit && unit.to_sym
22
+ end
23
+
24
+ def ==(other)
25
+ other.amount == @amount && other.unit == @unit
26
+ end
27
+
28
+ private
29
+
30
+ def to_amount(str)
31
+ case str
32
+ when "\xC2\xBC"; '1/4'
33
+ when "\xC2\xBD"; '1/2'
34
+ else str
35
+ end.to_r
36
+ end
37
+ end
38
+
39
+ class Emeals::Ingredient
40
+ attr_accessor :quantity, :description
41
+
42
+ def initialize(amount, unit, description)
43
+ @quantity = Emeals::Quantity.new(amount, unit)
44
+ @description = description
45
+ end
46
+
47
+ PARSE_REGEX = /((?:\d|\xC2\xBC|\xC2\xBD)+) (?:(#{Emeals::Quantity::UNITS_WITH_PLURALS.join("|")}) )?(.+)/
48
+
49
+ def self.parse(line)
50
+ if line =~ PARSE_REGEX
51
+ amount, unit, description = $1, $2, $3
52
+ new(amount, unit && unit.sub(/s$/, ''), description)
53
+ else
54
+ raise "tried to parse improperly formatted ingredient: #{line.inspect}"
55
+ end
56
+ end
57
+
58
+ def ==(other)
59
+ other.quantity == @quantity && other.description == @description
60
+ end
61
+ end
@@ -0,0 +1,135 @@
1
+ # encoding:utf-8
2
+
3
+ require 'emeals/dish'
4
+
5
+ class Emeals::Meal
6
+ attr_reader :entree, :side, :flags, :times
7
+
8
+ def initialize(entree = nil, side = nil)
9
+ @entree = entree
10
+ @side = side
11
+ @flags = []
12
+ @times = {}
13
+ end
14
+
15
+ def parse!(meal_text)
16
+ parse_state = :header
17
+ names = []
18
+ entree_instructions = []
19
+ side_instructions = []
20
+ meal_text.split("\n").each do |line|
21
+ case parse_state
22
+ when :header
23
+ parse_flags(line)
24
+ parse_state = :names
25
+ when :names
26
+ if line =~ /Prep Cook Total/
27
+ entree_name, side_name = separate_entree_and_side_names(names)
28
+ @entree = Emeals::Dish.new(entree_name)
29
+ @side = Emeals::Dish.new(side_name)
30
+ parse_state = :times
31
+ else
32
+ names << line
33
+ end
34
+ when :times
35
+ parse_times(line)
36
+ parse_state = :entree_ingredients
37
+ when :entree_ingredients
38
+ if line.include? "-------"
39
+ parse_state = :side_ingredients
40
+ else
41
+ add_ingredients_to_dish(line, @entree)
42
+ end
43
+ when :side_ingredients
44
+ if line =~ /^[A-Z]/
45
+ entree_instructions << line
46
+ parse_state = :entree_instructions
47
+ else
48
+ add_ingredients_to_dish(line, @side)
49
+ end
50
+ when :entree_instructions
51
+ if line.include? "-------"
52
+ add_instructions_to_dish(entree_instructions, @entree)
53
+ parse_state = :side_instructions
54
+ else
55
+ entree_instructions << line
56
+ end
57
+ when :side_instructions
58
+ if line =~ /^Copyright/
59
+ side_instructions = side_instructions[0..-3]
60
+ break
61
+ else
62
+ side_instructions << line
63
+ end
64
+ else
65
+
66
+ end
67
+ end
68
+
69
+ add_instructions_to_dish(side_instructions, @side)
70
+ self
71
+ end
72
+
73
+ FLAGS = {
74
+ "Slow Cooker" => :slow_cooker,
75
+ "On the Grill" => :on_the_grill,
76
+ "Super Fast" => :super_fast,
77
+ "Marinate Ahead" => :marinate_ahead
78
+ }
79
+
80
+ %w(slow_cooker on_the_grill super_fast marinate_ahead).each do |flag|
81
+ define_method "#{flag}?" do
82
+ @flags.include? flag.to_sym
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def separate_entree_and_side_names(names)
89
+ case names.size
90
+ when 2
91
+ names
92
+ when 3
93
+ if names[1].length < names[0].length
94
+ [join_names(names[0..1]), names[2]]
95
+ else
96
+ [names[0], join_names(names[1..2])]
97
+ end
98
+ else
99
+ [join_names(names[0..1]), join_names(names[2..-1])]
100
+ end
101
+ end
102
+
103
+ def join_names(names)
104
+ names.join(" ").gsub('- ', '-')
105
+ end
106
+
107
+ def parse_flags(line)
108
+ FLAGS.each do |flag, sym|
109
+ @flags << sym if line.include? flag
110
+ end
111
+ end
112
+
113
+ def parse_times(line)
114
+ times = line.split(" ")
115
+ @times[:prep] = times.first
116
+ @times[:cook] = times[1]
117
+ @times[:total] = times[2..-1].join(" ")
118
+ end
119
+
120
+ INGREDIENT_REGEX = /(?:\d|\xC2\xBC|\xC2\xBD)+ .+?(?=, (?:\d|\xC2\xBC|\xC2\xBD)+|$)/
121
+
122
+ def add_ingredients_to_dish(line, dish)
123
+ if line =~ /^\d|\xC2\xBC|\xC2\xBD/
124
+ line.scan(INGREDIENT_REGEX).each do |match|
125
+ dish.ingredients << Emeals::Ingredient.parse(match)
126
+ end
127
+ else
128
+ dish.ingredients.last.description << " #{line}"
129
+ end
130
+ end
131
+
132
+ def add_instructions_to_dish(lines, dish)
133
+ dish.instructions = lines.join(" ").split(/\. ?/)
134
+ end
135
+ end
@@ -0,0 +1,34 @@
1
+ require 'emeals/meal'
2
+
3
+ class Emeals::Menu
4
+ attr_reader :count, :meals
5
+
6
+ def initialize
7
+ @count = 0
8
+ @meals = []
9
+ end
10
+
11
+ def parse!(menu_text)
12
+ buffer = []
13
+ add_to_buffer = false
14
+ menu_text.split("\n").each do |line|
15
+ if line =~ /Meal (\d+)/
16
+ unless buffer.empty? and !add_to_buffer
17
+ @count = @count + 1
18
+ @meals << Emeals::Meal.new.parse!(buffer.join("\n"))
19
+ end
20
+
21
+ add_to_buffer = @count < $1.to_i
22
+ buffer = add_to_buffer ? [line] : []
23
+ next unless add_to_buffer
24
+ else
25
+ buffer << line if add_to_buffer
26
+ end
27
+ end
28
+ self
29
+ end
30
+
31
+ def self.parse(menu_text)
32
+ new.parse!(menu_text)
33
+ end
34
+ end
@@ -0,0 +1,3 @@
1
+ module Emeals
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,179 @@
1
+ require 'spec_helper'
2
+ require 'emeals'
3
+
4
+ describe Emeals::Client do
5
+ subject(:client) { Emeals::Client.new }
6
+ let(:menu_path) { File.join(File.dirname(__FILE__), 'fixtures', 'menu.pdf') }
7
+
8
+ before(:each) do
9
+ @menu = client.parse(menu_path)
10
+ @meals = @menu.meals
11
+ end
12
+
13
+ context "when given a File handle instead of a filename" do
14
+ it "completes successfully" do
15
+ File.open(menu_path) do |file|
16
+ expect(client.parse(file).count).to eq 7
17
+ end
18
+ end
19
+ end
20
+
21
+ it "counts the number of meals in the menu" do
22
+ expect(@menu.count).to eq 7
23
+ end
24
+
25
+ describe "entree names" do
26
+ it "reads the names of entrees when the side wraps to two lines" do
27
+ expect(@meals[5].entree.name).to eq "Baked Cod Provencal"
28
+ end
29
+
30
+ it "reads the names of entrees which wrap to two lines" do
31
+ expect(@meals.first.entree.name).to eq "Spicy Sausage and Egg Scramble"
32
+ end
33
+
34
+ it "reads the names of entrees when both the side and entree wrap to two lines" do
35
+ expect(@meals.last.entree.name).to eq "Peppery Grilled Ribeye Steaks"
36
+ end
37
+ end
38
+
39
+ describe "side names" do
40
+ it "reads the names of sides that are a single line" do
41
+ expect(@meals.first.side.name).to eq "Oregano Roasted Zucchini"
42
+ end
43
+
44
+ it "reads the names of sides that wrap to two lines" do
45
+ expect(@meals[5].side.name).to eq "Roasted Asparagus with Sun-Dried Tomatoes"
46
+ end
47
+
48
+ it "reads the names of sides when both the side and entree wrap to two lines" do
49
+ expect(@meals.last.side.name).to eq "Heirloom Tomato and Spinach Salad"
50
+ end
51
+ end
52
+
53
+ describe "flags" do
54
+ it "reads a meal with no flags correctly" do
55
+ expect(@meals.first.flags).to be_empty
56
+ end
57
+
58
+ it "reads the slow cooker flag correctly" do
59
+ expect(@meals[1].flags).to eq [:slow_cooker]
60
+ expect(@meals[1]).to be_slow_cooker
61
+ end
62
+
63
+ it "reads the on the grill flag correctly" do
64
+ expect(@meals[3].flags).to eq [:on_the_grill]
65
+ expect(@meals[3]).to be_on_the_grill
66
+ end
67
+
68
+ it "reads the super fast flag correctly" do
69
+ expect(@meals[5].flags).to eq [:super_fast]
70
+ expect(@meals[5]).to be_super_fast
71
+ end
72
+
73
+ it "reads the marinate ahead flag correctly" do
74
+ expect(@meals[6].flags).to eq [:marinate_ahead]
75
+ expect(@meals[6]).to be_marinate_ahead
76
+ end
77
+ end
78
+
79
+ describe "times" do
80
+ it "reads prep times correctly" do
81
+ expect(@meals.first.times[:prep]).to eq "10m"
82
+ expect(@meals[5].times[:prep]).to eq "15m"
83
+ end
84
+
85
+ it "reads cook times correctly" do
86
+ expect(@meals.first.times[:cook]).to eq "20m"
87
+ expect(@meals[1].times[:cook]).to eq "4h"
88
+ end
89
+
90
+ it "reads total times correctly" do
91
+ expect(@meals.first.times[:total]).to eq "30m"
92
+ expect(@meals[1].times[:total]).to eq "4h 10m"
93
+ end
94
+ end
95
+
96
+ describe "entree ingredients" do
97
+ include Emeals
98
+
99
+ let(:dish) { @meals.first.entree }
100
+
101
+ it "reads the correct number of ingredients" do
102
+ expect(dish.ingredients.size).to be 5
103
+ end
104
+
105
+ it "reads the correct descriptions of ingredients" do
106
+ expect(dish.ingredients[1].description).to eq "small onion, minced"
107
+ expect(dish.ingredients[3].description).to eq "kosher salt"
108
+ expect(dish.ingredients[4].description).to eq "pepper"
109
+ end
110
+
111
+ it "reads the correct quantities of ingredients" do
112
+ expect(dish.ingredients[0].quantity).to eq Emeals::Quantity.new('1/4', 'lb')
113
+ expect(dish.ingredients[1].quantity).to eq Emeals::Quantity.new('1/2')
114
+ expect(dish.ingredients[2].quantity).to eq Emeals::Quantity.new('4')
115
+ end
116
+ end
117
+
118
+ describe "side ingredients" do
119
+ include Emeals
120
+
121
+ let(:dish) { @meals[3].side }
122
+
123
+ it "reads the correct number of ingredients" do
124
+ expect(dish.ingredients.size).to be 5
125
+ end
126
+
127
+ it "reads the correct descriptions of ingredients" do
128
+ expect(dish.ingredients[0].description).to eq "(5-oz) bag baby spinach"
129
+ expect(dish.ingredients[1].description).to eq "drained and chopped roasted red bell peppers"
130
+ expect(dish.ingredients[2].description).to eq "pitted kalamata olives, cut in half"
131
+ end
132
+
133
+ it "reads the correct quantities of ingredients" do
134
+ expect(dish.ingredients[0].quantity).to eq Emeals::Quantity.new('1/2')
135
+ expect(dish.ingredients[3].quantity).to eq Emeals::Quantity.new('2')
136
+ expect(dish.ingredients[4].quantity).to eq Emeals::Quantity.new('1/4', 'cup')
137
+ end
138
+ end
139
+
140
+ describe "entree instructions" do
141
+ let(:dish) { @meals[2].entree }
142
+
143
+ it "reads the correct number of instructions" do
144
+ expect(dish.instructions.size).to be 7
145
+ end
146
+
147
+ it "reads the correct text of the instructions" do
148
+ expect(dish.instructions[0]).to eq "Preheat oven to 425 degrees"
149
+ expect(dish.instructions[4]).to eq "Dip fish in egg mixture; dredge in almond flour"
150
+ expect(dish.instructions[6]).to eq "Bake 8 minutes or until fish flakes with a fork"
151
+ end
152
+ end
153
+
154
+ describe "side instructions" do
155
+ let(:dish) { @meals[2].side }
156
+
157
+ it "reads the correct number of instructions" do
158
+ expect(dish.instructions.size).to be 7
159
+ end
160
+
161
+ it "reads the correct text of the instructions" do
162
+ expect(dish.instructions[0]).to eq "Preheat oven to 425 degrees"
163
+ expect(dish.instructions[1]).to eq "Toss zucchini, 1 tablespoon oil, salt and pepper on a large baking sheet; spread into a single layer"
164
+ expect(dish.instructions[6]).to eq "Bake 20 minutes or until center is set"
165
+ end
166
+
167
+ context "when the meal is the last on the page" do
168
+ let(:dish) { @meals[3].side }
169
+
170
+ it "reads the correct number of instructions" do
171
+ expect(dish.instructions.size).to be 1
172
+ end
173
+
174
+ it "reads the correct instruction text" do
175
+ expect(dish.instructions.first).to eq "Combine spinach, roasted bell peppers, olives, pepperoncini peppers and vinaigrette in a large bowl; toss well to coat"
176
+ end
177
+ end
178
+ end
179
+ end