emeals 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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