From d5082f9c16a0b3c62e8b0e7e09d53524d8608cc3 Mon Sep 17 00:00:00 2001 From: Dan Elbert Date: Tue, 2 Feb 2016 15:48:20 -0600 Subject: [PATCH] Major unit conversion refactor + lots more specs --- Gemfile | 2 + Gemfile.lock | 41 +++ Guardfile | 70 +++++ app/assets/javascripts/calculator.js | 13 +- app/controllers/calculator_controller.rb | 36 +-- app/models/concerns/density_validator.rb | 2 +- app/models/ingredient.rb | 14 +- app/models/recipe_ingredient.rb | 9 +- app/models/unit_conversion.rb | 264 ------------------ app/views/calculator/index.html.erb | 13 + config/initializers/lib.rb | 2 + lib/unit_conversion.rb | 36 +++ lib/unit_conversion/constants.rb | 79 ++++++ lib/unit_conversion/conversions.rb | 83 ++++++ lib/unit_conversion/errors.rb | 10 + lib/unit_conversion/formatters.rb | 75 +++++ lib/unit_conversion/parsed_number.rb | 38 +++ lib/unit_conversion/parsed_unit.rb | 64 +++++ lib/unit_conversion/value_unit.rb | 151 ++++++++++ .../ingredients_controller_spec.rb | 159 ----------- spec/controllers/recipes_controller_spec.rb | 159 ----------- spec/factories/usda_foods.rb | 20 +- spec/helpers/ingredients_helper_spec.rb | 15 - spec/helpers/recipes_helper_spec.rb | 15 - spec/lib/unit_conversion/conversions_spec.rb | 78 ++++++ spec/lib/unit_conversion/formatters_spec.rb | 82 ++++++ .../lib/unit_conversion/parsed_number_spec.rb | 34 +++ spec/lib/unit_conversion/parsed_unit_spec.rb | 99 +++++++ spec/lib/unit_conversion/value_unit_spec.rb | 49 ++++ spec/{models => lib}/unit_conversion_spec.rb | 132 +-------- spec/models/ingredient_spec.rb | 11 + spec/models/recipe_ingredient_spec.rb | 2 +- spec/rails_helper.rb | 11 +- spec/requests/ingredients_spec.rb | 10 - spec/requests/recipes_spec.rb | 10 - spec/routing/ingredients_routing_spec.rb | 39 --- spec/routing/recipes_routing_spec.rb | 39 --- spec/views/ingredients/edit.html.erb_spec.rb | 14 - spec/views/ingredients/index.html.erb_spec.rb | 14 - spec/views/ingredients/new.html.erb_spec.rb | 14 - spec/views/ingredients/show.html.erb_spec.rb | 11 - spec/views/recipes/edit.html.erb_spec.rb | 14 - spec/views/recipes/index.html.erb_spec.rb | 14 - spec/views/recipes/new.html.erb_spec.rb | 14 - spec/views/recipes/show.html.erb_spec.rb | 11 - 45 files changed, 1077 insertions(+), 995 deletions(-) create mode 100644 Guardfile delete mode 100644 app/models/unit_conversion.rb create mode 100644 config/initializers/lib.rb create mode 100644 lib/unit_conversion.rb create mode 100644 lib/unit_conversion/constants.rb create mode 100644 lib/unit_conversion/conversions.rb create mode 100644 lib/unit_conversion/errors.rb create mode 100644 lib/unit_conversion/formatters.rb create mode 100644 lib/unit_conversion/parsed_number.rb create mode 100644 lib/unit_conversion/parsed_unit.rb create mode 100644 lib/unit_conversion/value_unit.rb delete mode 100644 spec/controllers/ingredients_controller_spec.rb delete mode 100644 spec/controllers/recipes_controller_spec.rb delete mode 100644 spec/helpers/ingredients_helper_spec.rb delete mode 100644 spec/helpers/recipes_helper_spec.rb create mode 100644 spec/lib/unit_conversion/conversions_spec.rb create mode 100644 spec/lib/unit_conversion/formatters_spec.rb create mode 100644 spec/lib/unit_conversion/parsed_number_spec.rb create mode 100644 spec/lib/unit_conversion/parsed_unit_spec.rb create mode 100644 spec/lib/unit_conversion/value_unit_spec.rb rename spec/{models => lib}/unit_conversion_spec.rb (52%) delete mode 100644 spec/requests/ingredients_spec.rb delete mode 100644 spec/requests/recipes_spec.rb delete mode 100644 spec/routing/ingredients_routing_spec.rb delete mode 100644 spec/routing/recipes_routing_spec.rb delete mode 100644 spec/views/ingredients/edit.html.erb_spec.rb delete mode 100644 spec/views/ingredients/index.html.erb_spec.rb delete mode 100644 spec/views/ingredients/new.html.erb_spec.rb delete mode 100644 spec/views/ingredients/show.html.erb_spec.rb delete mode 100644 spec/views/recipes/edit.html.erb_spec.rb delete mode 100644 spec/views/recipes/index.html.erb_spec.rb delete mode 100644 spec/views/recipes/new.html.erb_spec.rb delete mode 100644 spec/views/recipes/show.html.erb_spec.rb diff --git a/Gemfile b/Gemfile index d0eed19..3536093 100644 --- a/Gemfile +++ b/Gemfile @@ -31,6 +31,8 @@ gem 'bcrypt', '~> 3.1.7' group :development, :test do + gem 'guard', '~> 2.13.0' + gem 'guard-rspec', require: false gem 'rspec-rails', '~> 3.4.0' gem 'factory_girl_rails', '~> 4.5.0' gem 'database_cleaner', '~> 1.5.1' diff --git a/Gemfile.lock b/Gemfile.lock index 7323d14..e9ba796 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -50,6 +50,7 @@ GEM builder (3.2.2) byebug (8.2.1) cocoon (1.2.6) + coderay (1.1.0) coffee-rails (4.1.1) coffee-script (>= 2.2.0) railties (>= 4.0.0, < 5.1.x) @@ -68,8 +69,24 @@ GEM factory_girl_rails (4.5.0) factory_girl (~> 4.5.0) railties (>= 3.0.0) + ffi (1.9.10) + formatador (0.2.5) globalid (0.3.6) activesupport (>= 4.1.0) + guard (2.13.0) + formatador (>= 0.2.4) + listen (>= 2.7, <= 4.0) + lumberjack (~> 1.0) + nenv (~> 0.1) + notiffany (~> 0.0) + pry (>= 0.9.12) + shellany (~> 0.0) + thor (>= 0.18.1) + guard-compat (1.2.1) + guard-rspec (4.6.4) + guard (~> 2.1) + guard-compat (~> 1.1) + rspec (>= 2.99.0, < 4.0) i18n (0.7.0) jbuilder (2.4.0) activesupport (>= 3.0.0, < 5.1) @@ -81,21 +98,34 @@ GEM json (1.8.3) libv8 (3.16.14.13) liner (0.2.4) + listen (3.0.5) + rb-fsevent (>= 0.9.3) + rb-inotify (>= 0.9) loofah (2.0.3) nokogiri (>= 1.5.9) + lumberjack (1.0.10) mail (2.6.3) mime-types (>= 1.16, < 3) memoizable (0.4.2) thread_safe (~> 0.3, >= 0.3.1) + method_source (0.8.2) mime-types (2.99) mini_portile2 (2.0.0) minitest (5.8.3) multi_json (1.11.2) mysql2 (0.3.20) + nenv (0.2.0) nokogiri (1.6.7.1) mini_portile2 (~> 2.0.0.rc2) + notiffany (0.0.8) + nenv (~> 0.1) + shellany (~> 0.0) parslet (1.7.1) blankslate (>= 2.0, <= 4.0) + pry (0.10.3) + coderay (~> 1.1.0) + method_source (~> 0.8.1) + slop (~> 3.4) rack (1.6.4) rack-test (0.6.3) rack (>= 1.0) @@ -124,7 +154,14 @@ GEM rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rake (10.5.0) + rb-fsevent (0.9.7) + rb-inotify (0.9.5) + ffi (>= 0.5.0) ref (2.0.0) + rspec (3.4.0) + rspec-core (~> 3.4.0) + rspec-expectations (~> 3.4.0) + rspec-mocks (~> 3.4.0) rspec-core (3.4.1) rspec-support (~> 3.4.0) rspec-expectations (3.4.0) @@ -149,7 +186,9 @@ GEM sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) + shellany (0.0.1) signed_multiset (0.2.1) + slop (3.6.0) sprockets (3.5.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -192,6 +231,8 @@ DEPENDENCIES cocoon (~> 1.2.6) database_cleaner (~> 1.5.1) factory_girl_rails (~> 4.5.0) + guard (~> 2.13.0) + guard-rspec jbuilder (~> 2.0) jquery-rails mysql2 (~> 0.3.18) diff --git a/Guardfile b/Guardfile new file mode 100644 index 0000000..deea540 --- /dev/null +++ b/Guardfile @@ -0,0 +1,70 @@ +# A sample Guardfile +# More info at https://github.com/guard/guard#readme + +## Uncomment and set this to only include directories you want to watch +# directories %w(app lib config test spec features) \ +# .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")} + +## Note: if you are using the `directories` clause above and you are not +## watching the project directory ('.'), then you will want to move +## the Guardfile to a watched dir and symlink it back, e.g. +# +# $ mkdir config +# $ mv Guardfile config/ +# $ ln -s config/Guardfile . +# +# and, you'll have to watch "config/Guardfile" instead of "Guardfile" + +# Note: The cmd option is now required due to the increasing number of ways +# rspec may be run, below are examples of the most common uses. +# * bundler: 'bundle exec rspec' +# * bundler binstubs: 'bin/rspec' +# * spring: 'bin/rspec' (This will use spring if running and you have +# installed the spring binstubs per the docs) +# * zeus: 'zeus rspec' (requires the server to be started separately) +# * 'just' rspec: 'rspec' + +guard :rspec, cmd: "bundle exec rspec" do + require "guard/rspec/dsl" + dsl = Guard::RSpec::Dsl.new(self) + + # Feel free to open issues for suggestions and improvements + + # RSpec files + rspec = dsl.rspec + watch(rspec.spec_helper) { rspec.spec_dir } + watch(rspec.spec_support) { rspec.spec_dir } + watch(rspec.spec_files) + + # Ruby files + ruby = dsl.ruby + dsl.watch_spec_files_for(ruby.lib_files) + + # Rails files + rails = dsl.rails(view_extensions: %w(erb haml slim)) + dsl.watch_spec_files_for(rails.app_files) + dsl.watch_spec_files_for(rails.views) + + watch(rails.controllers) do |m| + [ + rspec.spec.("routing/#{m[1]}_routing"), + rspec.spec.("controllers/#{m[1]}_controller"), + rspec.spec.("acceptance/#{m[1]}") + ] + end + + # Rails config changes + watch(rails.spec_helper) { rspec.spec_dir } + watch(rails.routes) { "#{rspec.spec_dir}/routing" } + watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" } + + # Capybara features specs + watch(rails.view_dirs) { |m| rspec.spec.("features/#{m[1]}") } + watch(rails.layouts) { |m| rspec.spec.("features/#{m[1]}") } + + # Turnip features and steps + watch(%r{^spec/acceptance/(.+)\.feature$}) + watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m| + Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance" + end +end diff --git a/app/assets/javascripts/calculator.js b/app/assets/javascripts/calculator.js index 1e2d8fc..42ebd0e 100644 --- a/app/assets/javascripts/calculator.js +++ b/app/assets/javascripts/calculator.js @@ -40,16 +40,11 @@ "/calculator/calculate", {input: $input.val(), output_unit: $outputUnit.val()}, function(data) { - if (data.errors.input) { - $input.closest(".form-group").addClass("has-error"); + if (data.errors.length) { + $("#errors_panel").show(); + $("#errors_container").html(data.errors.join(" ")); } else { - $input.closest(".form-group").removeClass("has-error"); - } - - if (data.errors.output_unit) { - $outputUnit.closest(".form-group").addClass("has-error"); - } else { - $outputUnit.closest(".form-group").removeClass("has-error"); + $("#errors_panel").hide(); } $output.val(data.output); diff --git a/app/controllers/calculator_controller.rb b/app/controllers/calculator_controller.rb index 6b8e150..2105d4d 100644 --- a/app/controllers/calculator_controller.rb +++ b/app/controllers/calculator_controller.rb @@ -7,36 +7,20 @@ class CalculatorController < ApplicationController def calculate input = params[:input] output_unit = params[:output_unit] - parsed_input = nil - original_number = nil - data = {errors: {}, output: ''} + data = {errors: [], output: ''} - if input.present? - - begin - parsed_input = UnitConversion.parse(input) - original_number = parsed_input.value - rescue UnitConversion::UnparseableUnitError => e - data[:errors][:input] = [e.message] + begin + unit = UnitConversion.parse(input) + if output_unit.present? + unit = unit.convert(output_unit) + data[:output] = unit.to_s + else + data[:output] = unit.auto_unit.to_s end - if parsed_input.present? && output_unit.present? - begin - parsed_input = parsed_input.convert_to output_unit - rescue Unitwise::ExpressionError => e - data[:errors][:output_unit] = [e.message] - end - end - - if parsed_input - puts parsed_input.value - puts parsed_input.unit - data[:output] = UnitConversion.auto_unit(UnitConversion.quantity_format(parsed_input.value, original_number), parsed_input.unit.to_s) - end - - else - data[:errors][:input] = ['Invalid input'] + rescue UnitConversion::UnparseableUnitError => e + data[:errors] << e.message end render json: data diff --git a/app/models/concerns/density_validator.rb b/app/models/concerns/density_validator.rb index 340db24..a68580b 100644 --- a/app/models/concerns/density_validator.rb +++ b/app/models/concerns/density_validator.rb @@ -5,7 +5,7 @@ class DensityValidator < ActiveModel::EachValidator begin unit = UnitConversion::parse(value) - valid = UnitConversion::density? unit + valid = unit.density? rescue UnitConversion::UnparseableUnitError => e valid = false msg = e.message diff --git a/app/models/ingredient.rb b/app/models/ingredient.rb index 059797c..c300ee0 100644 --- a/app/models/ingredient.rb +++ b/app/models/ingredient.rb @@ -50,15 +50,13 @@ class Ingredient < ActiveRecord::Base def calculate_density(grams, description) return nil if grams.blank? || description.blank? - # replace 'fl oz' with 'floz' - description = description.gsub(/fl oz/i, 'floz') - begin - unit = UnitConversion.parse(description) - if UnitConversion.volume?(unit) - mass = Unitwise(grams, 'g') - density = (mass / unit).convert_to(UnitConversion.normalize_unit_names('oz/cup')) - return "#{density.value.round(4)} oz/cup" + value_unit = UnitConversion.parse(description) + if value_unit.volume? + density_value = grams.to_d / value_unit.raw_value + density_units = "g/#{value_unit.unit.unit}" + density = UnitConversion.parse(density_value, density_units) + return density.convert('oz/cup').to_s else return nil end diff --git a/app/models/recipe_ingredient.rb b/app/models/recipe_ingredient.rb index 9919eb9..35943c1 100644 --- a/app/models/recipe_ingredient.rb +++ b/app/models/recipe_ingredient.rb @@ -21,11 +21,16 @@ class RecipeIngredient < ActiveRecord::Base def scale(factor, auto_unit = false) if factor.present? && self.quantity.present? && factor != '1' - self.quantity = UnitConversion.convert(self.quantity, factor, nil, nil) + + value_unit = UnitConversion.parse(self.quantity, self.units) + value_unit = value_unit.scale(factor) if auto_unit - self.quantity, self.units = UnitConversion.auto_unit(self.quantity, self.units) + value_unit = value_unit.auto_unit end + + self.quantity = value_unit.pretty_value + self.units = value_unit.unit.to_s end end diff --git a/app/models/unit_conversion.rb b/app/models/unit_conversion.rb deleted file mode 100644 index 5dae873..0000000 --- a/app/models/unit_conversion.rb +++ /dev/null @@ -1,264 +0,0 @@ -module UnitConversion - - INTEGER_REGEX = /-?[0-9]+/ - DECIMAL_REGEX = /(?:-?[0-9]+)?\.[0-9]+/ - RATIONAL_REGEX = /-?(?:[0-9]+ )?[0-9]+\/[0-9]+/ - UNIT_REGEX = /[\[\]a-zA-Z][\[\]a-zA-Z\/.\-()0-9]*/ - UNIT_PARSING_REGEX = /^(?#{INTEGER_REGEX}|#{DECIMAL_REGEX}|#{RATIONAL_REGEX})\s+(?#{UNIT_REGEX})$/ - - UNIT_ALIASES = { - '[cup_us]': %w(cup cups c), - '[tbs_us]': %w(tablespoon tablespoons tbsp tbs), - '[tsp_us]': %w(teaspoon teaspoons tsp), - '[oz_av]': %w(ounce ounces oz), - '[lb_av]': %w(pound pounds lb lbs), - '[pt_us]': %w(pint pints), - '[qt_us]': %w(quart quarts qt), - '[gal_us]': %w(gallon gallons ga), - '[foz_us]': %w(foz floz), - - g: %w(gram grams), - kg: %w(kilograms kilogram), - ml: %w(milliliter milliliters), - l: %w(liter liters) - } - - UNIT_ALIAS_MAP = Hash[UNIT_ALIASES.map { |unit, values| values.map { |v| [v, unit] } }.flatten(1)] - - # map that gives comfortable ranges for a given set of units - UNIT_RANGES = { - '[tsp_us]': (0.125...3.0), - '[tbs_us]': (0.5...4.0), - '[cup_us]': (0.25...4.0), - '[qt_us]': (1.0...4.0), - '[gal_us]': (0.5..9999), - - '[oz_av]': (0..16), - '[lb_av]': (1.0..9999), - - g: (1.0..750), - kg: (0.5..9999), - - ml: (1.0..750), - l: (0.5..9999) - } - - UNIT_ORDERS = { - standard_volume: [ - :'[tsp_us]', - :'[tbs_us]', - :'[cup_us]', - :'[qt_us]', - :'[gal_us]' - ], - - metric_volume: [ - :ml, - :l - ], - - standard_mass: [ - :'[oz_av]', - :'[lb_av]' - ], - - metric_mass: [ - :g, - :kg - ] - } - - class UnparseableUnitError < StandardError - end - - class UnknownUnitError < UnparseableUnitError - end - - class << self - - def parse(unit_string) - match = UNIT_PARSING_REGEX.match(unit_string.to_s.strip) - - if match && match[:value].present? && match[:unit].present? - begin - value = get_value(match[:value]) - Unitwise(value, normalize_unit_names(match[:unit])) - rescue Unitwise::ExpressionError => err - raise UnknownUnitError, err.message - end - else - raise UnparseableUnitError, "'#{unit_string}' does not appear to be a valid measurement of the form (ie '5 cup' or '223 gram/cup')" - end - end - - def density?(unit) - unit.compatible_with? Unitwise(1, 'g/ml') - end - - def volume?(unit) - unit.compatible_with? Unitwise(1, 'ml') - end - - def mass?(unit) - unit.compatible_with? Unitwise(1, 'g') - end - - # Returns a Unitwise representation of the density. Raises an exception if the value is anything other than a - # valid density - def get_density(str) - raise UnknownUnitError, 'No density provided' if str.blank? - begin - unit = parse(str) - rescue UnparseableUnitError => err - raise UnknownUnitError "Invalid density: #{err.message}" - end - - raise UnknownUnitError, "Invalid density: #{str} is not a density" unless density?(unit) - unit - end - - def normalize_unit_names(unit_description) - return unit_description.to_sym if UNIT_ALIASES.include?(unit_description.to_sym) - unit_description.downcase.gsub(/[a-z]+/) do |match| - UNIT_ALIAS_MAP[match] || match - end - end - - def get_value(str) - if str =~ /^#{INTEGER_REGEX}$/ - str.to_i - elsif str =~ /^#{RATIONAL_REGEX}$/ - parts = str.split(' ') - if parts.length == 2 - whole = parts.first.to_r - fractional = parts.last.to_r - (whole.abs + fractional) * (whole < 0 ? -1.to_r : 1.to_r) - else - str.to_r - end - elsif str =~ /^#{DECIMAL_REGEX}$/ - str.to_d - else - raise UnparseableUnitError, "str (#{str}) is not a valid number" - end - end - - def convert(quantity, factor, input_unit, output_unit, density = nil) - - value = get_value(quantity) - factor = get_value(factor) - - if value.is_a?(BigDecimal) && factor.is_a?(Rational) - factor = factor.to_d(10) - end - - converted = value * factor - - input_unit = normalize_unit_names(input_unit) unless input_unit.nil? - output_unit = normalize_unit_names(output_unit) unless output_unit.nil? - - if input_unit.present? && output_unit.present? && input_unit != output_unit - in_unit = Unitwise(1, input_unit) - out_unit = Unitwise(1, output_unit) - unit = Unitwise(converted, input_unit) - - if volume?(in_unit) && mass?(out_unit) - unit_density = get_density(density) - unit = unit * unit_density - elsif mass?(in_unit) && volume?(out_unit) - unit_density = get_density(density) - unit = unit / unit_density - end - - converted = unit.convert_to(output_unit).value - end - - quantity_format(converted, value) - end - - def quantity_format(quantity, original_quantity) - if original_quantity.is_a?(Rational) || original_quantity.is_a?(Integer) - rational_val = rationalize(quantity) - if rational_val.denominator == 1 - rational_val.to_i.to_s - elsif rational_val.denominator < rational_val.numerator.abs - whole = rational_val.floor - fraction = rational_val - whole - "#{whole} #{fraction}" - else - rational_val.to_s - end - else - "%g" % ("%.3f" % quantity) - end - end - - def rationalize(value) - if value.is_a? Integer - return value - end - - if value.floor == value - return value - end - - useful_denominators = [2, 3, 4, 8, 16] - - if value.is_a?(Rational) && useful_denominators.include?(value.denominator) - return value - end - - whole = value.floor - fraction = value - whole - - approximations = useful_denominators.map do |d| - best_n = (1...d).each.map { |n| {delta: (fraction - Rational(n, d)).abs, numerator: n} }.sort { |a, b| a[:delta] <=> b[:delta] }.first - {denominator: d, numerator: best_n[:numerator], delta: best_n[:delta]} - end - - # Add 0 and 1 - approximations << { denominator: 1, numerator: 0, delta: (fraction - Rational(0,1)).abs } - approximations << { denominator: 1, numerator: 1, delta: (fraction - Rational(1,1)).abs } - - best = approximations.sort { |a, b| [a[:delta], a[:denominator]] <=> [b[:delta], b[:denominator]] }.first - - Rational((whole * best[:denominator]) + best[:numerator], best[:denominator]) - end - - # Given an awkward measurement such as '18 9/10 oz' or '5/24 cup' or '0.09434 cup', - # it will round the rational and scale the unit to give a more reasonable, useful measurement, ie - # 19 oz, 3 Tbsp, 1 1/2 Tbsp - def auto_unit(quantity, units) - normalized_unit = normalize_unit_names(units) - value = initial_value = get_value(quantity) - type_key, unit_orders = UNIT_ORDERS.detect { |key, orders| orders.include?(normalized_unit.to_sym) } - new_unit = normalized_unit - - if unit_orders && (unit_range = UNIT_RANGES[normalized_unit.to_sym]) - - if value < unit_range.first - unit_orders = unit_orders.reverse - end - - idx = unit_orders.index(new_unit.to_sym) - - while !unit_range.include?(value) && idx < (unit_orders.length - 1) - idx += 1 - next_unit = unit_orders[idx] - value = Unitwise(value, new_unit).convert_to(next_unit).value.round(4) - new_unit = next_unit.to_s - unit_range = UNIT_RANGES[new_unit.to_sym] - end - end - - if normalized_unit == new_unit - new_unit = units - else - new_unit = (UNIT_ALIASES[new_unit.to_sym] || []).first || new_unit - end - - return quantity_format(value, initial_value), new_unit - end - - end -end diff --git a/app/views/calculator/index.html.erb b/app/views/calculator/index.html.erb index 4f09967..2e3a351 100644 --- a/app/views/calculator/index.html.erb +++ b/app/views/calculator/index.html.erb @@ -22,5 +22,18 @@ +
+
+ +
+
+ diff --git a/config/initializers/lib.rb b/config/initializers/lib.rb new file mode 100644 index 0000000..192a938 --- /dev/null +++ b/config/initializers/lib.rb @@ -0,0 +1,2 @@ + +require 'unit_conversion' \ No newline at end of file diff --git a/lib/unit_conversion.rb b/lib/unit_conversion.rb new file mode 100644 index 0000000..98a84cb --- /dev/null +++ b/lib/unit_conversion.rb @@ -0,0 +1,36 @@ +require 'unit_conversion/constants' +require 'unit_conversion/errors' +require 'unit_conversion/formatters' +require 'unit_conversion/parsed_number' +require 'unit_conversion/parsed_unit' +require 'unit_conversion/conversions' +require 'unit_conversion/value_unit' + +module UnitConversion + class << self + + def parse(value_string, unit_string = nil) + ValueUnit.for(value_string, unit_string) + end + + def convert(quantity, factor, input_unit, output_unit, density = nil) + unit_value = parse(quantity, input_unit).scale(factor) + + if output_unit.present? + unit_value = unit_value.convert(output_unit, density) + end + + unit_value.pretty_value + end + + # Given an awkward measurement such as '18 9/10 oz' or '5/24 cup' or '0.09434 cup', + # it will round the rational and scale the unit to give a more reasonable, useful measurement, ie + # 19 oz, 3 Tbsp, 1 1/2 Tbsp + def auto_unit(quantity, units) + unit = parse(quantity, units).auto_unit + + [unit.pretty_value, unit.unit.to_s] + end + + end +end \ No newline at end of file diff --git a/lib/unit_conversion/constants.rb b/lib/unit_conversion/constants.rb new file mode 100644 index 0000000..ea15492 --- /dev/null +++ b/lib/unit_conversion/constants.rb @@ -0,0 +1,79 @@ +module UnitConversion + INTEGER_REGEX = /-?[0-9]+/ + DECIMAL_REGEX = /(?:-?[0-9]+)?\.[0-9]+/ + RATIONAL_REGEX = /-?(?:[0-9]+ )?[0-9]+\/[0-9]+/ + UNIT_REGEX = /[\[\]a-zA-Z][\[\]a-zA-Z\/.\-()0-9 ]*/ + UNIT_PARSING_REGEX = /^(?#{INTEGER_REGEX}|#{DECIMAL_REGEX}|#{RATIONAL_REGEX})(?:\s+(?#{UNIT_REGEX}))?$/ + + STANDARD_UNIT_ALIASES = { + '[cup_us]': %w(cup cups c), + '[tbs_us]': %w(tablespoon tablespoons tbsp tbs), + '[tsp_us]': %w(teaspoon teaspoons tsp), + '[oz_av]': %w(ounce ounces oz), + '[lb_av]': %w(pound pounds lb lbs), + '[pt_us]': %w(pint pints), + '[qt_us]': %w(quart quarts qt), + '[gal_us]': %w(gallon gallons ga), + '[foz_us]': %w(foz floz), + } + + METRIC_UNIT_ALIASES = { + g: %w(gram grams), + kg: %w(kilograms kilogram), + + ml: %w(milliliter milliliters), + cl: %w(centiliter centiliters), + dl: %w(deciliter deciliters), + l: %w(liter liters), + + m: %w(meter meters), + cm: %w(centimeter centimeters) + } + + UNIT_ALIASES = STANDARD_UNIT_ALIASES.merge(METRIC_UNIT_ALIASES) + + UNIT_ALIAS_MAP = Hash[UNIT_ALIASES.map { |unit, values| values.map { |v| [v, unit] } }.flatten(1)] + + # map that gives comfortable ranges for a given set of units + UNIT_RANGES = { + '[tsp_us]': (0.125...3.0), + '[tbs_us]': (0.5...4.0), + '[cup_us]': (0.25...4.0), + '[qt_us]': (1.0..3.9), + '[gal_us]': (0.5..9999), + + '[oz_av]': (0..16), + '[lb_av]': (1.0..9999), + + g: (1.0..750), + kg: (0.5..9999), + + ml: (1.0..750), + l: (0.5..9999) + } + + UNIT_ORDERS = { + standard_volume: [ + :'[tsp_us]', + :'[tbs_us]', + :'[cup_us]', + :'[qt_us]', + :'[gal_us]' + ], + + metric_volume: [ + :ml, + :l + ], + + standard_mass: [ + :'[oz_av]', + :'[lb_av]' + ], + + metric_mass: [ + :g, + :kg + ] + } +end \ No newline at end of file diff --git a/lib/unit_conversion/conversions.rb b/lib/unit_conversion/conversions.rb new file mode 100644 index 0000000..03ba517 --- /dev/null +++ b/lib/unit_conversion/conversions.rb @@ -0,0 +1,83 @@ +module UnitConversion + + class Conversion + end + + class ScaleConversion < Conversion + def initialize(parsed_factor) + @factor = parsed_factor + end + + def convert(value_unit) + value = @factor.value * value_unit.value.value + ValueUnit.for(value, value_unit.unit, value_unit.formatter) + end + end + + class ConvertConversion < Conversion + def initialize(parsed_unit, density_unit_value = nil) + @target_unit = parsed_unit + + raise UnknownUnitError, "#{density_unit_value} is not a density" if density_unit_value && !density_unit_value.density? + @density = density_unit_value + end + + def convert(value_unit) + + input = value_unit.unitwise + + if value_unit.volume? && @target_unit.mass? + raise MissingDensityError, "Cannot convert #{value_unit.unit} to #{@target_unit} without density" unless @density + input = input * @density.unitwise + elsif value_unit.mass? && @target_unit.volume? + raise MissingDensityError, "Cannot convert #{value_unit.unit} to #{@target_unit} without density" unless @density + input = input / @density.unitwise + end + + input = input.convert_to @target_unit.unit + + formatter = @target_unit.metric? ? DecimalFormatter.new : value_unit.formatter + + ValueUnit.for(input.value, @target_unit, formatter) + + end + end + + class AutoUnitConversion < Conversion + + def convert(value_unit) + + unless known_auto_unit?(value_unit.unit) + return value_unit + end + + value = value_unit.raw_value + unit = value_unit.unit.unit + new_unit = unit + + unit_orders = UNIT_ORDERS.values.detect { |orders| orders.include?(unit.to_sym) } + unit_range = UNIT_RANGES[unit.to_sym] + + if value < unit_range.first + unit_orders = unit_orders.reverse + end + + idx = unit_orders.index(new_unit.to_sym) + + while !unit_range.include?(value) && idx < (unit_orders.length - 1) + idx += 1 + next_unit = unit_orders[idx] + value = Unitwise(value, new_unit).convert_to(next_unit).value + new_unit = next_unit.to_s + unit_range = UNIT_RANGES[new_unit.to_sym] + end + + ValueUnit.for(value, new_unit, value_unit.formatter) + end + + def known_auto_unit?(unit) + unit && UNIT_ORDERS.values.flatten.include?(unit.unit.to_sym) + end + end + +end diff --git a/lib/unit_conversion/errors.rb b/lib/unit_conversion/errors.rb new file mode 100644 index 0000000..aa7138f --- /dev/null +++ b/lib/unit_conversion/errors.rb @@ -0,0 +1,10 @@ +module UnitConversion + class UnparseableUnitError < StandardError + end + + class UnknownUnitError < UnparseableUnitError + end + + class MissingDensityError < UnparseableUnitError + end +end diff --git a/lib/unit_conversion/formatters.rb b/lib/unit_conversion/formatters.rb new file mode 100644 index 0000000..f95eabd --- /dev/null +++ b/lib/unit_conversion/formatters.rb @@ -0,0 +1,75 @@ +module UnitConversion + + class NumberFormatter + + def self.for(parsed_value, parsed_unit) + case + when parsed_unit && parsed_unit.metric? + DecimalFormatter + when parsed_value.rational? + RationalFormatter + else + DecimalFormatter + end.new + + end + + def initialize + end + + # Converts a Numeric into a cooking-friendly Rational + def rationalize(value) + + # NOTE: this algorithm seems stupid. + + if value.floor == value + return value + end + + useful_denominators = [2, 3, 4, 8, 16] + + if value.is_a?(Rational) && useful_denominators.include?(value.denominator) + return value + end + + whole = value.floor + fraction = value - whole + + approximations = useful_denominators.map do |d| + best_n = (1...d).each.map { |n| {delta: (fraction - Rational(n, d)).abs, numerator: n} }.sort { |a, b| a[:delta] <=> b[:delta] }.first + {denominator: d, numerator: best_n[:numerator], delta: best_n[:delta]} + end + + # Add 0 and 1 + approximations << { denominator: 1, numerator: 0, delta: (fraction - Rational(0,1)).abs } + approximations << { denominator: 1, numerator: 1, delta: (fraction - Rational(1,1)).abs } + + best = approximations.sort { |a, b| [a[:delta], a[:denominator]] <=> [b[:delta], b[:denominator]] }.first + + Rational((whole * best[:denominator]) + best[:numerator], best[:denominator]) + end + + end + + class RationalFormatter < NumberFormatter + def format(value) + rational_val = rationalize(value) + if rational_val.denominator == 1 + rational_val.to_i.to_s + elsif rational_val.denominator < rational_val.numerator.abs + whole = rational_val.floor + fraction = rational_val - whole + "#{whole} #{fraction}" + else + rational_val.to_s + end + end + end + + class DecimalFormatter < NumberFormatter + def format(value) + "%g" % ("%.3f" % value) + end + end + +end \ No newline at end of file diff --git a/lib/unit_conversion/parsed_number.rb b/lib/unit_conversion/parsed_number.rb new file mode 100644 index 0000000..2225c1d --- /dev/null +++ b/lib/unit_conversion/parsed_number.rb @@ -0,0 +1,38 @@ +module UnitConversion + class ParsedNumber + + attr_reader :value + + def initialize(str) + @original_value = str + @value = str.is_a?(Numeric) ? str : parse_value(str) + end + + def to_s + @value.to_s + end + + def rational? + value.is_a?(Integer) || value.is_a?(Rational) + end + + def parse_value(str) + if str =~ /^#{INTEGER_REGEX}$/ + str.to_i + elsif str =~ /^#{RATIONAL_REGEX}$/ + parts = str.split(' ') + if parts.length == 2 + whole = parts.first.to_r + fractional = parts.last.to_r + (whole.abs + fractional) * (whole < 0 ? -1.to_r : 1.to_r) + else + str.to_r + end + elsif str =~ /^#{DECIMAL_REGEX}$/ + str.to_d + else + raise UnparseableUnitError, "str [#{str}] is not a valid number" + end + end + end +end \ No newline at end of file diff --git a/lib/unit_conversion/parsed_unit.rb b/lib/unit_conversion/parsed_unit.rb new file mode 100644 index 0000000..0dd000f --- /dev/null +++ b/lib/unit_conversion/parsed_unit.rb @@ -0,0 +1,64 @@ +module UnitConversion + class ParsedUnit + + attr_reader :original_unit, :unit + + def initialize(str) + @original_unit = str + @unit = normalize_unit_names(str) + end + + def to_s + unit.gsub(/[a-z_\[\]]+/) do |match| + if aliases = UNIT_ALIASES[match.to_sym] + aliases.first + else + match + end + end + end + + def density? + compatible? 'g/ml' + end + + def volume? + compatible? 'ml' + end + + def mass? + compatible? 'g' + end + + def metric? + METRIC_UNIT_ALIASES.keys.include? unit.to_sym + end + + def unitwise(value = 1) + begin + Unitwise(value, unit) + rescue Unitwise::ExpressionError => err + raise UnknownUnitError, err.message + end + end + + def compatible?(unit_str) + begin + unitwise.compatible_with? Unitwise(1, unit_str) + rescue UnknownUnitError + false + end + end + + def normalize_unit_names(unit_description) + return unit_description.to_s if UNIT_ALIASES.include?(unit_description.to_sym) + + # fluid ounce is the only known unit with a space; fix it up before the following replacement + unit_description = unit_description.gsub(/fl oz/i, 'floz') + + unit_description.downcase.gsub(/[a-z_\[\]]+/) do |match| + UNIT_ALIAS_MAP[match] || match + end + end + end +end \ No newline at end of file diff --git a/lib/unit_conversion/value_unit.rb b/lib/unit_conversion/value_unit.rb new file mode 100644 index 0000000..3521309 --- /dev/null +++ b/lib/unit_conversion/value_unit.rb @@ -0,0 +1,151 @@ +module UnitConversion + class ValueUnit + + def self.for(value_string, unit_string = nil, formatter = nil) + raise UnparseableUnitError, "value is empty" if value_string.blank? + + if String === value_string && unit_string.nil? + value_string, unit_string = parse_single_string(value_string) + end + + if value_string.is_a?(ParsedNumber) + value = value_string + else + value = ParsedNumber.new(value_string) + end + + unit = case unit_string + when nil + nil + when ->(u) { u.blank? } + nil + when ParsedUnit + unit_string + else + ParsedUnit.new(unit_string) + end + + if unit.nil? + ValueNoUnit.new(value, formatter) + else + ValueUnit.new(value, unit, formatter) + end + end + + def self.parse_single_string(value_unit_string) + match = UNIT_PARSING_REGEX.match(value_unit_string.to_s.strip) + + if match && match[:value].present? + return match[:value], match[:unit] + else + raise UnparseableUnitError, "'#{value_unit_string}' does not appear to be a valid measurement of the form (ie '5 cup' or '223 gram/cup')" + end + end + + attr_reader :formatter + + def initialize(value, unit, formatter = nil) + @value = value + @unit = unit + @formatter = formatter || NumberFormatter.for(value, unit) + end + + def to_s + "#{pretty_value} #{unit}" + end + + def unit + @unit + end + + def value + @value + end + + def raw_value + @value.value + end + + def unitwise + unit.unitwise(value.value) + end + + # Returns a new ValueUnit scaled by the given factor + def scale(factor) + if factor.present? + parsed_factor = ParsedNumber.new(factor) + ScaleConversion.new(parsed_factor).convert(self) + else + self + end + end + + # Returns a new ValueUnit with the given new_parsed_unit. If converting between mass and volume, + # also requires a density UnitValue + def convert(new_unit, density = nil) + new_parsed_unit = ParsedUnit.new(new_unit) + parsed_density = density ? ValueUnit.for(density) : nil + + if new_parsed_unit.unit != self.unit.unit + ConvertConversion.new(new_parsed_unit, parsed_density).convert(self) + else + self + end + end + + def auto_unit + AutoUnitConversion.new.convert(self) + end + + def density? + unit.density? + end + + def volume? + unit.volume? + end + + def mass? + unit.mass? + end + + def pretty_value + formatter.format(value.value) + end + end + + class ValueNoUnit < ValueUnit + def initialize(value, formatter = nil) + @value = value + @formatter = formatter || NumberFormatter.for(value, unit) + end + + def to_s + "#{pretty_value}" + end + + def unit + nil + end + + def unitwise + raise UnknownUnitError, "No unit value provided" + end + + def convert(new_parsed_unit, density_unit_value = nil) + raise UnknownUnitError, "No unit value provided" + end + + def density? + false + end + + def volume? + false + end + + def mass? + false + end + end +end \ No newline at end of file diff --git a/spec/controllers/ingredients_controller_spec.rb b/spec/controllers/ingredients_controller_spec.rb deleted file mode 100644 index 95f2ae1..0000000 --- a/spec/controllers/ingredients_controller_spec.rb +++ /dev/null @@ -1,159 +0,0 @@ -require 'rails_helper' - -# This spec was generated by rspec-rails when you ran the scaffold generator. -# It demonstrates how one might use RSpec to specify the controller code that -# was generated by Rails when you ran the scaffold generator. -# -# It assumes that the implementation code is generated by the rails scaffold -# generator. If you are using any extension libraries to generate different -# controller code, this generated spec may or may not pass. -# -# It only uses APIs available in rails and/or rspec-rails. There are a number -# of tools you can use to make these specs even more expressive, but we're -# sticking to rails and rspec-rails APIs to keep things simple and stable. -# -# Compared to earlier versions of this generator, there is very limited use of -# stubs and message expectations in this spec. Stubs are only used when there -# is no simpler way to get a handle on the object needed for the example. -# Message expectations are only used when there is no simpler way to specify -# that an instance is receiving a specific message. - -RSpec.describe IngredientsController, type: :controller do - - # This should return the minimal set of attributes required to create a valid - # Ingredient. As you add validations to Ingredient, be sure to - # adjust the attributes here as well. - let(:valid_attributes) { - skip("Add a hash of attributes valid for your model") - } - - let(:invalid_attributes) { - skip("Add a hash of attributes invalid for your model") - } - - # This should return the minimal set of values that should be in the session - # in order to pass any filters (e.g. authentication) defined in - # IngredientsController. Be sure to keep this updated too. - let(:valid_session) { {} } - - describe "GET #index" do - it "assigns all ingredients as @ingredients" do - ingredient = Ingredient.create! valid_attributes - get :index, {}, valid_session - expect(assigns(:ingredients)).to eq([ingredient]) - end - end - - describe "GET #show" do - it "assigns the requested ingredient as @ingredient" do - ingredient = Ingredient.create! valid_attributes - get :show, {:id => ingredient.to_param}, valid_session - expect(assigns(:ingredient)).to eq(ingredient) - end - end - - describe "GET #new" do - it "assigns a new ingredient as @ingredient" do - get :new, {}, valid_session - expect(assigns(:ingredient)).to be_a_new(Ingredient) - end - end - - describe "GET #edit" do - it "assigns the requested ingredient as @ingredient" do - ingredient = Ingredient.create! valid_attributes - get :edit, {:id => ingredient.to_param}, valid_session - expect(assigns(:ingredient)).to eq(ingredient) - end - end - - describe "POST #create" do - context "with valid params" do - it "creates a new Ingredient" do - expect { - post :create, {:ingredient => valid_attributes}, valid_session - }.to change(Ingredient, :count).by(1) - end - - it "assigns a newly created ingredient as @ingredient" do - post :create, {:ingredient => valid_attributes}, valid_session - expect(assigns(:ingredient)).to be_a(Ingredient) - expect(assigns(:ingredient)).to be_persisted - end - - it "redirects to the created ingredient" do - post :create, {:ingredient => valid_attributes}, valid_session - expect(response).to redirect_to(Ingredient.last) - end - end - - context "with invalid params" do - it "assigns a newly created but unsaved ingredient as @ingredient" do - post :create, {:ingredient => invalid_attributes}, valid_session - expect(assigns(:ingredient)).to be_a_new(Ingredient) - end - - it "re-renders the 'new' template" do - post :create, {:ingredient => invalid_attributes}, valid_session - expect(response).to render_template("new") - end - end - end - - describe "PUT #update" do - context "with valid params" do - let(:new_attributes) { - skip("Add a hash of attributes valid for your model") - } - - it "updates the requested ingredient" do - ingredient = Ingredient.create! valid_attributes - put :update, {:id => ingredient.to_param, :ingredient => new_attributes}, valid_session - ingredient.reload - skip("Add assertions for updated state") - end - - it "assigns the requested ingredient as @ingredient" do - ingredient = Ingredient.create! valid_attributes - put :update, {:id => ingredient.to_param, :ingredient => valid_attributes}, valid_session - expect(assigns(:ingredient)).to eq(ingredient) - end - - it "redirects to the ingredient" do - ingredient = Ingredient.create! valid_attributes - put :update, {:id => ingredient.to_param, :ingredient => valid_attributes}, valid_session - expect(response).to redirect_to(ingredient) - end - end - - context "with invalid params" do - it "assigns the ingredient as @ingredient" do - ingredient = Ingredient.create! valid_attributes - put :update, {:id => ingredient.to_param, :ingredient => invalid_attributes}, valid_session - expect(assigns(:ingredient)).to eq(ingredient) - end - - it "re-renders the 'edit' template" do - ingredient = Ingredient.create! valid_attributes - put :update, {:id => ingredient.to_param, :ingredient => invalid_attributes}, valid_session - expect(response).to render_template("edit") - end - end - end - - describe "DELETE #destroy" do - it "destroys the requested ingredient" do - ingredient = Ingredient.create! valid_attributes - expect { - delete :destroy, {:id => ingredient.to_param}, valid_session - }.to change(Ingredient, :count).by(-1) - end - - it "redirects to the ingredients list" do - ingredient = Ingredient.create! valid_attributes - delete :destroy, {:id => ingredient.to_param}, valid_session - expect(response).to redirect_to(ingredients_url) - end - end - -end diff --git a/spec/controllers/recipes_controller_spec.rb b/spec/controllers/recipes_controller_spec.rb deleted file mode 100644 index 91af795..0000000 --- a/spec/controllers/recipes_controller_spec.rb +++ /dev/null @@ -1,159 +0,0 @@ -require 'rails_helper' - -# This spec was generated by rspec-rails when you ran the scaffold generator. -# It demonstrates how one might use RSpec to specify the controller code that -# was generated by Rails when you ran the scaffold generator. -# -# It assumes that the implementation code is generated by the rails scaffold -# generator. If you are using any extension libraries to generate different -# controller code, this generated spec may or may not pass. -# -# It only uses APIs available in rails and/or rspec-rails. There are a number -# of tools you can use to make these specs even more expressive, but we're -# sticking to rails and rspec-rails APIs to keep things simple and stable. -# -# Compared to earlier versions of this generator, there is very limited use of -# stubs and message expectations in this spec. Stubs are only used when there -# is no simpler way to get a handle on the object needed for the example. -# Message expectations are only used when there is no simpler way to specify -# that an instance is receiving a specific message. - -RSpec.describe RecipesController, type: :controller do - - # This should return the minimal set of attributes required to create a valid - # Recipe. As you add validations to Recipe, be sure to - # adjust the attributes here as well. - let(:valid_attributes) { - skip("Add a hash of attributes valid for your model") - } - - let(:invalid_attributes) { - skip("Add a hash of attributes invalid for your model") - } - - # This should return the minimal set of values that should be in the session - # in order to pass any filters (e.g. authentication) defined in - # RecipesController. Be sure to keep this updated too. - let(:valid_session) { {} } - - describe "GET #index" do - it "assigns all recipes as @recipes" do - recipe = Recipe.create! valid_attributes - get :index, {}, valid_session - expect(assigns(:recipes)).to eq([recipe]) - end - end - - describe "GET #show" do - it "assigns the requested recipe as @recipe" do - recipe = Recipe.create! valid_attributes - get :show, {:id => recipe.to_param}, valid_session - expect(assigns(:recipe)).to eq(recipe) - end - end - - describe "GET #new" do - it "assigns a new recipe as @recipe" do - get :new, {}, valid_session - expect(assigns(:recipe)).to be_a_new(Recipe) - end - end - - describe "GET #edit" do - it "assigns the requested recipe as @recipe" do - recipe = Recipe.create! valid_attributes - get :edit, {:id => recipe.to_param}, valid_session - expect(assigns(:recipe)).to eq(recipe) - end - end - - describe "POST #create" do - context "with valid params" do - it "creates a new Recipe" do - expect { - post :create, {:recipe => valid_attributes}, valid_session - }.to change(Recipe, :count).by(1) - end - - it "assigns a newly created recipe as @recipe" do - post :create, {:recipe => valid_attributes}, valid_session - expect(assigns(:recipe)).to be_a(Recipe) - expect(assigns(:recipe)).to be_persisted - end - - it "redirects to the created recipe" do - post :create, {:recipe => valid_attributes}, valid_session - expect(response).to redirect_to(Recipe.last) - end - end - - context "with invalid params" do - it "assigns a newly created but unsaved recipe as @recipe" do - post :create, {:recipe => invalid_attributes}, valid_session - expect(assigns(:recipe)).to be_a_new(Recipe) - end - - it "re-renders the 'new' template" do - post :create, {:recipe => invalid_attributes}, valid_session - expect(response).to render_template("new") - end - end - end - - describe "PUT #update" do - context "with valid params" do - let(:new_attributes) { - skip("Add a hash of attributes valid for your model") - } - - it "updates the requested recipe" do - recipe = Recipe.create! valid_attributes - put :update, {:id => recipe.to_param, :recipe => new_attributes}, valid_session - recipe.reload - skip("Add assertions for updated state") - end - - it "assigns the requested recipe as @recipe" do - recipe = Recipe.create! valid_attributes - put :update, {:id => recipe.to_param, :recipe => valid_attributes}, valid_session - expect(assigns(:recipe)).to eq(recipe) - end - - it "redirects to the recipe" do - recipe = Recipe.create! valid_attributes - put :update, {:id => recipe.to_param, :recipe => valid_attributes}, valid_session - expect(response).to redirect_to(recipe) - end - end - - context "with invalid params" do - it "assigns the recipe as @recipe" do - recipe = Recipe.create! valid_attributes - put :update, {:id => recipe.to_param, :recipe => invalid_attributes}, valid_session - expect(assigns(:recipe)).to eq(recipe) - end - - it "re-renders the 'edit' template" do - recipe = Recipe.create! valid_attributes - put :update, {:id => recipe.to_param, :recipe => invalid_attributes}, valid_session - expect(response).to render_template("edit") - end - end - end - - describe "DELETE #destroy" do - it "destroys the requested recipe" do - recipe = Recipe.create! valid_attributes - expect { - delete :destroy, {:id => recipe.to_param}, valid_session - }.to change(Recipe, :count).by(-1) - end - - it "redirects to the recipes list" do - recipe = Recipe.create! valid_attributes - delete :destroy, {:id => recipe.to_param}, valid_session - expect(response).to redirect_to(recipes_url) - end - end - -end diff --git a/spec/factories/usda_foods.rb b/spec/factories/usda_foods.rb index 332bc22..8867367 100644 --- a/spec/factories/usda_foods.rb +++ b/spec/factories/usda_foods.rb @@ -7,7 +7,7 @@ FactoryGirl.define do factory :usda_food do long_description 'Food' short_description 'Food' - ndbn '01234' + ndbn { generate :unique_ndbn } water 1.0 kcal 101 protein 1.2 @@ -23,4 +23,22 @@ FactoryGirl.define do refuse_percent 3 end + factory :salted_butter, parent: :usda_food do + long_description 'Butter, salted' + short_description 'BUTTER,WITH SALT' + water 15.87 + kcal 717 + protein 0.85 + lipid 81.11 + ash 2.11 + carbohydrates 0.06 + fiber 0 + sugar 0.06 + gram_weight_1 5.0 + gram_weight_2 14.2 + gram_weight_desc_1 '1 pat, (1\" sq, 1/3\" high)' + gram_weight_desc_2 '1 tbsp' + refuse_percent 0 + end + end diff --git a/spec/helpers/ingredients_helper_spec.rb b/spec/helpers/ingredients_helper_spec.rb deleted file mode 100644 index 0a9ee8a..0000000 --- a/spec/helpers/ingredients_helper_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'rails_helper' - -# Specs in this file have access to a helper object that includes -# the IngredientsHelper. For example: -# -# describe IngredientsHelper do -# describe "string concat" do -# it "concats two strings with spaces" do -# expect(helper.concat_strings("this","that")).to eq("this that") -# end -# end -# end -RSpec.describe IngredientsHelper, type: :helper do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/helpers/recipes_helper_spec.rb b/spec/helpers/recipes_helper_spec.rb deleted file mode 100644 index 69b5b0e..0000000 --- a/spec/helpers/recipes_helper_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'rails_helper' - -# Specs in this file have access to a helper object that includes -# the RecipesHelper. For example: -# -# describe RecipesHelper do -# describe "string concat" do -# it "concats two strings with spaces" do -# expect(helper.concat_strings("this","that")).to eq("this that") -# end -# end -# end -RSpec.describe RecipesHelper, type: :helper do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/lib/unit_conversion/conversions_spec.rb b/spec/lib/unit_conversion/conversions_spec.rb new file mode 100644 index 0000000..c9e15ae --- /dev/null +++ b/spec/lib/unit_conversion/conversions_spec.rb @@ -0,0 +1,78 @@ +require 'rails_helper' + +RSpec.describe UnitConversion::Conversion do + + def get_value_unit(value, units) + UnitConversion.parse(value, units) + end + + def get_number(number) + UnitConversion::ParsedNumber.new(number) + end + + def get_unit(unit) + UnitConversion::ParsedUnit.new(unit) + end + + describe UnitConversion::ScaleConversion do + it 'scales' do + expect(UnitConversion::ScaleConversion.new(get_number(2)).convert(get_value_unit(4, 'cups')).raw_value).to eq 8 + expect(UnitConversion::ScaleConversion.new(get_number('1/2')).convert(get_value_unit(4, 'cups')).raw_value).to eq 2 + end + + it 'leaves the unit alone' do + expect(UnitConversion::ScaleConversion.new(get_number(2)).convert(get_value_unit(4, 'cups')).unit.original_unit).to eq 'cups' + expect(UnitConversion::ScaleConversion.new(get_number(2)).convert(get_value_unit(4, 'cats')).unit.original_unit).to eq 'cats' + end + end + + describe UnitConversion::ConvertConversion do + it 'converts standard units' do + expect(UnitConversion::ConvertConversion.new(get_unit('tbsp')).convert(get_value_unit(1, 'cups')).raw_value).to eq 16 + expect(UnitConversion::ConvertConversion.new(get_unit('cups')).convert(get_value_unit(8, 'tbsp')).raw_value).to eq 0.5 + end + + it 'takes on the new unit' do + expect(UnitConversion::ConvertConversion.new(get_unit('tbsp')).convert(get_value_unit(1, 'cups')).unit.original_unit).to eq 'tbsp' + expect(UnitConversion::ConvertConversion.new(get_unit('gallons')).convert(get_value_unit(1, 'cups')).unit.original_unit).to eq 'gallons' + end + + it 'converts from mass to volume' do + expect(UnitConversion::ConvertConversion.new(get_unit('oz'), get_value_unit(5, 'oz/c')).convert(get_value_unit(2, 'cups')).raw_value).to be_within(0.01).of(10) + end + + it 'converts from volume to mass' do + expect(UnitConversion::ConvertConversion.new(get_unit('c'), get_value_unit(5, 'oz/c')).convert(get_value_unit(10, 'oz')).raw_value).to be_within(0.01).of(2) + end + + it 'raises an error when attempting a mass/volume conversion without density' do + expect { UnitConversion::ConvertConversion.new(get_unit('c')).convert(get_value_unit(10, 'oz')) }.to raise_error(UnitConversion::MissingDensityError) + expect { UnitConversion::ConvertConversion.new(get_unit('oz')).convert(get_value_unit(10, 'c')) }.to raise_error(UnitConversion::MissingDensityError) + end + + it 'raises an error when density param is not a density' do + expect { UnitConversion::ConvertConversion.new(get_unit('oz'), get_value_unit(2, 'g/oz')) }.to raise_error(UnitConversion::UnknownUnitError) + expect { UnitConversion::ConvertConversion.new(get_unit('oz'), get_value_unit(2, 'cats')) }.to raise_error(UnitConversion::UnknownUnitError) + expect { UnitConversion::ConvertConversion.new(get_unit('oz'), get_value_unit(2, 'g')) }.to raise_error(UnitConversion::UnknownUnitError) + end + + it 'preserves the formatter for standard units' do + original = get_value_unit(10, 'tbsp') + converted = UnitConversion::ConvertConversion.new(get_unit('c')).convert(original) + expect(original.formatter).to be converted.formatter + + original = get_value_unit("10.0", 'tbsp') + converted = UnitConversion::ConvertConversion.new(get_unit('c')).convert(original) + expect(original.formatter).to be converted.formatter + end + + it 'always sets the formatter to a DecimalFormatter for metric units' do + original = get_value_unit(10, 'tbsp') + expect(original.formatter).to be_a UnitConversion::RationalFormatter + + converted = UnitConversion::ConvertConversion.new(get_unit('ml')).convert(original) + expect(converted.formatter).to be_a UnitConversion::DecimalFormatter + end + end + +end diff --git a/spec/lib/unit_conversion/formatters_spec.rb b/spec/lib/unit_conversion/formatters_spec.rb new file mode 100644 index 0000000..ef9f081 --- /dev/null +++ b/spec/lib/unit_conversion/formatters_spec.rb @@ -0,0 +1,82 @@ +require 'rails_helper' + +RSpec.describe UnitConversion::NumberFormatter do + + describe UnitConversion::RationalFormatter do + + it 'formats integers' do + expect(UnitConversion::RationalFormatter.new.format(1)).to eq '1' + expect(UnitConversion::RationalFormatter.new.format(-1)).to eq '-1' + expect(UnitConversion::RationalFormatter.new.format(1.0)).to eq '1' + end + + it 'formats rationals into rationals' do + expect(UnitConversion::RationalFormatter.new.format(Rational(1,2))).to eq '1/2' + expect(UnitConversion::RationalFormatter.new.format(Rational(5,16))).to eq '5/16' + expect(UnitConversion::RationalFormatter.new.format(Rational(5, 4))).to eq '1 1/4' + end + + it 'rounds rationals into better rationals' do + expect(UnitConversion::RationalFormatter.new.format(Rational(3,7))).to eq '7/16' + end + + it 'formats decimals into rationals' do + expect(UnitConversion::RationalFormatter.new.format(1.5)).to eq '1 1/2' + expect(UnitConversion::RationalFormatter.new.format(1.125)).to eq '1 1/8' + expect(UnitConversion::RationalFormatter.new.format(24.38)).to eq '24 3/8' + end + end + + describe UnitConversion::DecimalFormatter do + it 'formats everything to a decimal' do + expect(UnitConversion::DecimalFormatter.new.format(1)).to eq '1' + expect(UnitConversion::DecimalFormatter.new.format(-1)).to eq '-1' + expect(UnitConversion::DecimalFormatter.new.format(1.0)).to eq '1' + expect(UnitConversion::DecimalFormatter.new.format(Rational(1,4))).to eq '0.25' + expect(UnitConversion::DecimalFormatter.new.format("4.2899999999".to_d)).to eq '4.29' + end + end + + describe '.rationalize' do + it 'leaves integers alone' do + expect(UnitConversion::NumberFormatter.new.rationalize(1)).to eq 1 + expect(UnitConversion::NumberFormatter.new.rationalize(15)).to eq 15 + expect(UnitConversion::NumberFormatter.new.rationalize(-1)).to eq -1 + expect(UnitConversion::NumberFormatter.new.rationalize(0)).to eq 0 + end + + it 'leaves non-fractional numbers alone' do + expect(UnitConversion::NumberFormatter.new.rationalize(1.0)).to eq 1.0 + expect(UnitConversion::NumberFormatter.new.rationalize(-1.0)).to eq -1.0 + expect(UnitConversion::NumberFormatter.new.rationalize(0.0)).to eq 0.0 + expect(UnitConversion::NumberFormatter.new.rationalize(35.0)).to eq 35.0 + end + + it 'leaves already nice rationals alone' do + expect(UnitConversion::NumberFormatter.new.rationalize(Rational(1,2))).to eq Rational(1,2) + expect(UnitConversion::NumberFormatter.new.rationalize(Rational(5,2))).to eq Rational(5,2) + expect(UnitConversion::NumberFormatter.new.rationalize(Rational(3,16))).to eq Rational(3,16) + expect(UnitConversion::NumberFormatter.new.rationalize(Rational(3,4))).to eq Rational(3,4) + end + + it 'converts neat decimals to rationals' do + expect(UnitConversion::NumberFormatter.new.rationalize(1.5)).to eq Rational(3,2) + expect(UnitConversion::NumberFormatter.new.rationalize(0.125)).to eq Rational(1,8) + expect(UnitConversion::NumberFormatter.new.rationalize(5.0625)).to eq Rational(81, 16) + expect(UnitConversion::NumberFormatter.new.rationalize(0.75)).to eq Rational(3,4) + end + + it 'rounds weird rationals to nice rationals' do + expect(UnitConversion::NumberFormatter.new.rationalize(Rational(3,7))).to eq Rational(7,16) + expect(UnitConversion::NumberFormatter.new.rationalize(Rational(2,5))).to eq Rational(3,8) + expect(UnitConversion::NumberFormatter.new.rationalize(Rational(2,5))).to eq Rational(3,8) + end + + it 'rounds weird decimals to nice rationals' do + expect(UnitConversion::NumberFormatter.new.rationalize(0.24)).to eq Rational(1,4) + expect(UnitConversion::NumberFormatter.new.rationalize(1.24)).to eq Rational(5,4) + expect(UnitConversion::NumberFormatter.new.rationalize(1.13)).to eq Rational(9,8) + end + end + +end \ No newline at end of file diff --git a/spec/lib/unit_conversion/parsed_number_spec.rb b/spec/lib/unit_conversion/parsed_number_spec.rb new file mode 100644 index 0000000..7e44015 --- /dev/null +++ b/spec/lib/unit_conversion/parsed_number_spec.rb @@ -0,0 +1,34 @@ +require 'rails_helper' + +RSpec.describe UnitConversion::ParsedNumber do + + it 'converts integers' do + expect(UnitConversion::ParsedNumber.new('1').value).to eq 1 + expect(UnitConversion::ParsedNumber.new('-1').value).to eq -1 + expect(UnitConversion::ParsedNumber.new('0').value).to eq 0 + expect(UnitConversion::ParsedNumber.new('20').value).to eq 20 + expect(UnitConversion::ParsedNumber.new('103').value).to eq 103 + end + + it 'converts decimal numbers' do + expect(UnitConversion::ParsedNumber.new('1.0').value).to eq BigDecimal.new("1") + expect(UnitConversion::ParsedNumber.new('-1.0').value).to eq BigDecimal.new("-1") + expect(UnitConversion::ParsedNumber.new('54.33').value).to eq BigDecimal.new("54.33") + expect(UnitConversion::ParsedNumber.new('-54.33').value).to eq BigDecimal.new("-54.33") + expect(UnitConversion::ParsedNumber.new('.33').value).to eq BigDecimal.new("0.33") + end + + it 'converts simple fractions' do + expect(UnitConversion::ParsedNumber.new('1/2').value).to eq Rational(1, 2) + expect(UnitConversion::ParsedNumber.new('-1/2').value).to eq Rational(-1, 2) + expect(UnitConversion::ParsedNumber.new('3/16').value).to eq Rational(3, 16) + end + + it 'converts fractions with whole numbers' do + expect(UnitConversion::ParsedNumber.new('1 1/2').value).to eq Rational(3, 2) + expect(UnitConversion::ParsedNumber.new('-1 1/2').value).to eq Rational(-3, 2) + expect(UnitConversion::ParsedNumber.new('4 3/4').value).to eq Rational(19, 4) + expect(UnitConversion::ParsedNumber.new('18 9/10').value).to eq Rational(189, 10) + end + +end \ No newline at end of file diff --git a/spec/lib/unit_conversion/parsed_unit_spec.rb b/spec/lib/unit_conversion/parsed_unit_spec.rb new file mode 100644 index 0000000..2af76b0 --- /dev/null +++ b/spec/lib/unit_conversion/parsed_unit_spec.rb @@ -0,0 +1,99 @@ +require 'rails_helper' + +RSpec.describe UnitConversion::ParsedUnit do + + it 'converts simple units' do + data = { + 'c' => '[cup_us]', + 'cups' => '[cup_us]', + 'pints' => '[pt_us]', + 'gram' => 'g', + 'grams' => 'g', + 'Grams' => 'g', + 'Tbsp' => '[tbs_us]', + '[tbs_us]' => '[tbs_us]', + '[oz_av]' => '[oz_av]' + } + + data.each do |input, output| + expect(UnitConversion::ParsedUnit.new(input).unit).to eq output + end + end + + it 'converts mixed units' do + data = { + 'oz/c' => '[oz_av]/[cup_us]', + 'kilograms/cups' => 'kg/[cup_us]', + 'pints/junk' => '[pt_us]/junk', + 'gram/[tbs_us]' => 'g/[tbs_us]' + } + + data.each do |input, output| + expect(UnitConversion::ParsedUnit.new(input).unit).to eq output + end + end + + describe '.to_s' do + it 'renders friendly simple units' do + expect(UnitConversion::ParsedUnit.new('m').to_s).to eq 'meter' + expect(UnitConversion::ParsedUnit.new('tbsp').to_s).to eq 'tablespoon' + expect(UnitConversion::ParsedUnit.new('gallons').to_s).to eq 'gallon' + expect(UnitConversion::ParsedUnit.new('[cup_us]').to_s).to eq 'cup' + expect(UnitConversion::ParsedUnit.new('junk').to_s).to eq 'junk' + end + + it 'renders friendly compound units' do + expect(UnitConversion::ParsedUnit.new('m/c').to_s).to eq 'meter/cup' + expect(UnitConversion::ParsedUnit.new('[oz_av]/[cup_us]').to_s).to eq 'ounce/cup' + end + end + + describe '.metric?' do + it 'returns true for metric units' do + expect(UnitConversion::ParsedUnit.new('m').metric?).to be_truthy + expect(UnitConversion::ParsedUnit.new('g').metric?).to be_truthy + expect(UnitConversion::ParsedUnit.new('meter').metric?).to be_truthy + expect(UnitConversion::ParsedUnit.new('centiliter').metric?).to be_truthy + end + + it 'returns false for standard units' do + expect(UnitConversion::ParsedUnit.new('c').metric?).to be_falsey + expect(UnitConversion::ParsedUnit.new('tbsp').metric?).to be_falsey + expect(UnitConversion::ParsedUnit.new('oz').metric?).to be_falsey + end + + it 'returns false for unknown units' do + expect(UnitConversion::ParsedUnit.new('cats').metric?).to be_falsey + expect(UnitConversion::ParsedUnit.new('dogs').metric?).to be_falsey + end + end + + describe '.density?' do + it 'returns true for any mass over volume unit' do + data = [ + 'gram/cup', + 'pound/gallon', + 'ounce/tablespoon', + 'ounce/centimeter3' + ] + + data.each do |input| + expect(UnitConversion::ParsedUnit.new(input).density?).to be_truthy + end + end + + it 'returns false for any non density unit' do + data = [ + 'cup', + 'gram', + 'gram/hour', + 'centimeter3/ounce' + ] + + data.each do |input| + expect(UnitConversion::ParsedUnit.new(input).density?).to be_falsey + end + end + end + +end \ No newline at end of file diff --git a/spec/lib/unit_conversion/value_unit_spec.rb b/spec/lib/unit_conversion/value_unit_spec.rb new file mode 100644 index 0000000..046c264 --- /dev/null +++ b/spec/lib/unit_conversion/value_unit_spec.rb @@ -0,0 +1,49 @@ +require 'rails_helper' + +RSpec.describe UnitConversion::ValueUnit do + + def check_vu(vu, value, unit) + expect(vu.value).to be_a UnitConversion::ParsedNumber + expect(vu.raw_value).to eq value + + if unit.present? + expect(vu.unit).to be_a UnitConversion::ParsedUnit + expect(vu.unit.to_s).to eq unit + else + expect(vu.unit).to be_nil + expect(vu).to be_a UnitConversion::ValueNoUnit + end + end + + describe '.for' do + + it 'Converts single strings' do + check_vu(UnitConversion::ValueUnit.for('5 cups'), 5, 'cup') + check_vu(UnitConversion::ValueUnit.for('5'), 5, nil) + check_vu(UnitConversion::ValueUnit.for('1/3 fl oz'), Rational(1,3), 'foz') + end + + it 'Converts a pair of strings' do + check_vu(UnitConversion::ValueUnit.for('5', 'cups'), 5, 'cup') + check_vu(UnitConversion::ValueUnit.for('5', ''), 5, nil) + check_vu(UnitConversion::ValueUnit.for('1/3', 'fl oz'), Rational(1,3), 'foz') + end + + it 'Converts a bare Numeric' do + check_vu(UnitConversion::ValueUnit.for(5, 'cups'), 5, 'cup') + check_vu(UnitConversion::ValueUnit.for(5, ''), 5, nil) + check_vu(UnitConversion::ValueUnit.for(Rational(1,3), 'fl oz'), Rational(1,3), 'foz') + check_vu(UnitConversion::ValueUnit.for(2.5, 'tsp'), 2.5, 'teaspoon') + end + + it 'Converts ParsedNumber and a string' do + check_vu(UnitConversion::ValueUnit.for(UnitConversion::ParsedNumber.new('5'), 'cups'), 5, 'cup') + check_vu(UnitConversion::ValueUnit.for(UnitConversion::ParsedNumber.new('5'), nil), 5, nil) + end + + it 'Converts parsed objects' do + check_vu(UnitConversion::ValueUnit.for(UnitConversion::ParsedNumber.new('5'), UnitConversion::ParsedUnit.new('cups')), 5, 'cup') + end + end + +end \ No newline at end of file diff --git a/spec/models/unit_conversion_spec.rb b/spec/lib/unit_conversion_spec.rb similarity index 52% rename from spec/models/unit_conversion_spec.rb rename to spec/lib/unit_conversion_spec.rb index 4c8b453..8aeb615 100644 --- a/spec/models/unit_conversion_spec.rb +++ b/spec/lib/unit_conversion_spec.rb @@ -4,9 +4,9 @@ RSpec.describe UnitConversion do describe '.auto_unit' do it 'leaves units alone if reasonable' do - expect(UnitConversion.auto_unit('1/2', 'tbsp')).to eq ['1/2', 'tbsp'] - expect(UnitConversion.auto_unit('2', 'cups')).to eq ['2', 'cups'] - expect(UnitConversion.auto_unit('1', 'c')).to eq ['1', 'c'] + expect(UnitConversion.auto_unit('1/2', 'tbsp')).to eq ['1/2', 'tablespoon'] + expect(UnitConversion.auto_unit('2', 'cups')).to eq ['2', 'cup'] + expect(UnitConversion.auto_unit('1', 'c')).to eq ['1', 'cup'] end it 'leaves units alone if unknown' do @@ -32,22 +32,6 @@ RSpec.describe UnitConversion do end end - describe '.get_value' do - it 'returns rationals' do - expect(UnitConversion.get_value('1/2')).to eq Rational(1, 2) - expect(UnitConversion.get_value('1 1/2')).to eq Rational(3, 2) - expect(UnitConversion.get_value('-1/2')).to eq Rational(-1, 2) - expect(UnitConversion.get_value('-1 1/2')).to eq Rational(-3, 2) - expect(UnitConversion.get_value('18 9/10')).to eq Rational(189, 10) - end - - it 'returns decimals' do - expect(UnitConversion.get_value('-1')).to eq -1.to_d - expect(UnitConversion.get_value('1.0')).to eq 1.to_d - expect(UnitConversion.get_value('5.56')).to eq "5.56".to_d - end - end - describe '.convert' do it 'scales decimal numbers' do expect(UnitConversion.convert('1', '2', 'cup', 'cup')).to eq '2' @@ -77,9 +61,7 @@ RSpec.describe UnitConversion do expect(UnitConversion.convert('1/2', '2', 'slices', 'slices')).to eq '1' expect(UnitConversion.convert('4', '1/8', nil, nil)).to eq '1/2' expect(UnitConversion.convert('4', '1/8', 'slices', nil)).to eq '1/2' - expect(UnitConversion.convert('4', '1/8', nil, 'slices')).to eq '1/2' expect(UnitConversion.convert('4', '1/8', 'slices', '')).to eq '1/2' - expect(UnitConversion.convert('4', '1/8', '', 'slices')).to eq '1/2' end it 'converts and scales' do @@ -111,7 +93,7 @@ RSpec.describe UnitConversion do } data.each do |input, output| - expect(UnitConversion.parse(input)).to eq output + expect(UnitConversion.parse(input).unitwise).to eq output end end @@ -124,16 +106,12 @@ RSpec.describe UnitConversion do ] data.each do |input| - expect { UnitConversion.parse(input) }.to raise_error(UnitConversion::UnknownUnitError), "'#{input}' didn't raise" + expect { UnitConversion.parse(input).unitwise }.to raise_error(UnitConversion::UnknownUnitError), "'#{input}' didn't raise" end end it 'raises UnparseableUnitError on malformed string' do data = [ - '55', - '55.5', - '-55', - '-55.55', '5.5/2 cups', '2/3.0 cups', 'ounce', @@ -148,106 +126,6 @@ RSpec.describe UnitConversion do end end - describe '.density?' do - it 'returns true for any mass over volume unit' do - data = [ - Unitwise(1, 'gram/cup'), - Unitwise(1, 'pound/gallon'), - Unitwise(1, 'ounce/tablespoon'), - Unitwise(1, 'ounce/centimeter3') - ] - data.each do |input| - expect(UnitConversion.density?(input)).to be_truthy - end - end - - it 'returns false for any non density unit' do - data = [ - Unitwise(1, 'cup'), - Unitwise(1, 'gram'), - Unitwise(1, 'gram/hour'), - Unitwise(1, 'centimeter3/ounce') - ] - - data.each do |input| - expect(UnitConversion.density?(input)).to be_falsey - end - end - end - - describe '.rationalize' do - it 'leaves integers alone' do - expect(UnitConversion.rationalize(1)).to eq 1 - expect(UnitConversion.rationalize(15)).to eq 15 - expect(UnitConversion.rationalize(-1)).to eq -1 - expect(UnitConversion.rationalize(0)).to eq 0 - end - - it 'leaves non-fractional numbers alone' do - expect(UnitConversion.rationalize(1.0)).to eq 1.0 - expect(UnitConversion.rationalize(-1.0)).to eq -1.0 - expect(UnitConversion.rationalize(0.0)).to eq 0.0 - expect(UnitConversion.rationalize(35.0)).to eq 35.0 - end - - it 'leaves already nice rationals alone' do - expect(UnitConversion.rationalize(Rational(1,2))).to eq Rational(1,2) - expect(UnitConversion.rationalize(Rational(5,2))).to eq Rational(5,2) - expect(UnitConversion.rationalize(Rational(3,16))).to eq Rational(3,16) - expect(UnitConversion.rationalize(Rational(3,4))).to eq Rational(3,4) - end - - it 'converts neat decimals to rationals' do - expect(UnitConversion.rationalize(1.5)).to eq Rational(3,2) - expect(UnitConversion.rationalize(0.125)).to eq Rational(1,8) - expect(UnitConversion.rationalize(5.0625)).to eq Rational(81, 16) - expect(UnitConversion.rationalize(0.75)).to eq Rational(3,4) - end - - it 'rounds weird rationals to nice rationals' do - expect(UnitConversion.rationalize(Rational(3,7))).to eq Rational(7,16) - expect(UnitConversion.rationalize(Rational(2,5))).to eq Rational(3,8) - expect(UnitConversion.rationalize(Rational(2,5))).to eq Rational(3,8) - end - - it 'rounds weird decimals to nice rationals' do - expect(UnitConversion.rationalize(0.24)).to eq Rational(1,4) - expect(UnitConversion.rationalize(1.24)).to eq Rational(5,4) - expect(UnitConversion.rationalize(1.13)).to eq Rational(9,8) - end - end - - describe '.normalize_unit_name' do - - it 'converts simple units' do - data = { - 'c' => '[cup_us]', - 'cups' => '[cup_us]', - 'pints' => '[pt_us]', - 'gram' => 'g', - 'grams' => 'g', - 'Grams' => 'g', - 'Tbsp' => '[tbs_us]' - } - - data.each do |input, output| - expect(UnitConversion.normalize_unit_names(input)).to eq output - end - end - - it 'converts mixed units' do - data = { - 'oz/c' => '[oz_av]/[cup_us]', - 'kilograms/cups' => 'kg/[cup_us]', - 'pints/junk' => '[pt_us]/junk' - } - - data.each do |input, output| - expect(UnitConversion.normalize_unit_names(input)).to eq output - end - end - - end end diff --git a/spec/models/ingredient_spec.rb b/spec/models/ingredient_spec.rb index 0f380e1..e79a252 100644 --- a/spec/models/ingredient_spec.rb +++ b/spec/models/ingredient_spec.rb @@ -23,4 +23,15 @@ RSpec.describe Ingredient, type: :model do end + describe 'set_usda_food' do + it 'sets the density' do + i = build(:ingredient) + f = create(:salted_butter) + + i.set_usda_food(f) + + expect(i.density).not_to be_nil + end + end + end diff --git a/spec/models/recipe_ingredient_spec.rb b/spec/models/recipe_ingredient_spec.rb index 376f99f..d653cb7 100644 --- a/spec/models/recipe_ingredient_spec.rb +++ b/spec/models/recipe_ingredient_spec.rb @@ -1,5 +1,5 @@ require 'rails_helper' RSpec.describe RecipeIngredient, type: :model do - pending "add some examples to (or delete) #{__FILE__}" + end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index d0000d7..138db18 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -33,12 +33,15 @@ RSpec.configure do |config| config.before(:suite) do DatabaseCleaner.strategy = :transaction - DatabaseCleaner.clean_with(:truncation) - DatabaseCleaner.cleaning do - FactoryGirl.lint + unless ENV['FAST'] == 'true' + DatabaseCleaner.clean_with(:truncation) + + DatabaseCleaner.cleaning do + FactoryGirl.lint + end + end end - end config.around(:each) do |example| DatabaseCleaner.cleaning do diff --git a/spec/requests/ingredients_spec.rb b/spec/requests/ingredients_spec.rb deleted file mode 100644 index 1181aba..0000000 --- a/spec/requests/ingredients_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'rails_helper' - -RSpec.describe "Ingredients", type: :request do - describe "GET /ingredients" do - it "works! (now write some real specs)" do - get ingredients_path - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/requests/recipes_spec.rb b/spec/requests/recipes_spec.rb deleted file mode 100644 index a4f0a9c..0000000 --- a/spec/requests/recipes_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'rails_helper' - -RSpec.describe "Recipes", type: :request do - describe "GET /recipes" do - it "works! (now write some real specs)" do - get recipes_path - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/routing/ingredients_routing_spec.rb b/spec/routing/ingredients_routing_spec.rb deleted file mode 100644 index 0fa04a5..0000000 --- a/spec/routing/ingredients_routing_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -require "rails_helper" - -RSpec.describe IngredientsController, type: :routing do - describe "routing" do - - it "routes to #index" do - expect(:get => "/ingredients").to route_to("ingredients#index") - end - - it "routes to #new" do - expect(:get => "/ingredients/new").to route_to("ingredients#new") - end - - it "routes to #show" do - expect(:get => "/ingredients/1").to route_to("ingredients#show", :id => "1") - end - - it "routes to #edit" do - expect(:get => "/ingredients/1/edit").to route_to("ingredients#edit", :id => "1") - end - - it "routes to #create" do - expect(:post => "/ingredients").to route_to("ingredients#create") - end - - it "routes to #update via PUT" do - expect(:put => "/ingredients/1").to route_to("ingredients#update", :id => "1") - end - - it "routes to #update via PATCH" do - expect(:patch => "/ingredients/1").to route_to("ingredients#update", :id => "1") - end - - it "routes to #destroy" do - expect(:delete => "/ingredients/1").to route_to("ingredients#destroy", :id => "1") - end - - end -end diff --git a/spec/routing/recipes_routing_spec.rb b/spec/routing/recipes_routing_spec.rb deleted file mode 100644 index 7bd40cf..0000000 --- a/spec/routing/recipes_routing_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -require "rails_helper" - -RSpec.describe RecipesController, type: :routing do - describe "routing" do - - it "routes to #index" do - expect(:get => "/recipes").to route_to("recipes#index") - end - - it "routes to #new" do - expect(:get => "/recipes/new").to route_to("recipes#new") - end - - it "routes to #show" do - expect(:get => "/recipes/1").to route_to("recipes#show", :id => "1") - end - - it "routes to #edit" do - expect(:get => "/recipes/1/edit").to route_to("recipes#edit", :id => "1") - end - - it "routes to #create" do - expect(:post => "/recipes").to route_to("recipes#create") - end - - it "routes to #update via PUT" do - expect(:put => "/recipes/1").to route_to("recipes#update", :id => "1") - end - - it "routes to #update via PATCH" do - expect(:patch => "/recipes/1").to route_to("recipes#update", :id => "1") - end - - it "routes to #destroy" do - expect(:delete => "/recipes/1").to route_to("recipes#destroy", :id => "1") - end - - end -end diff --git a/spec/views/ingredients/edit.html.erb_spec.rb b/spec/views/ingredients/edit.html.erb_spec.rb deleted file mode 100644 index 6e9ec62..0000000 --- a/spec/views/ingredients/edit.html.erb_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'rails_helper' - -RSpec.describe "ingredients/edit", type: :view do - before(:each) do - @ingredient = assign(:ingredient, Ingredient.create!()) - end - - it "renders the edit ingredient form" do - render - - assert_select "form[action=?][method=?]", ingredient_path(@ingredient), "post" do - end - end -end diff --git a/spec/views/ingredients/index.html.erb_spec.rb b/spec/views/ingredients/index.html.erb_spec.rb deleted file mode 100644 index 03938b5..0000000 --- a/spec/views/ingredients/index.html.erb_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'rails_helper' - -RSpec.describe "ingredients/index", type: :view do - before(:each) do - assign(:ingredients, [ - Ingredient.create!(), - Ingredient.create!() - ]) - end - - it "renders a list of ingredients" do - render - end -end diff --git a/spec/views/ingredients/new.html.erb_spec.rb b/spec/views/ingredients/new.html.erb_spec.rb deleted file mode 100644 index 501bb8a..0000000 --- a/spec/views/ingredients/new.html.erb_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'rails_helper' - -RSpec.describe "ingredients/new", type: :view do - before(:each) do - assign(:ingredient, Ingredient.new()) - end - - it "renders new ingredient form" do - render - - assert_select "form[action=?][method=?]", ingredients_path, "post" do - end - end -end diff --git a/spec/views/ingredients/show.html.erb_spec.rb b/spec/views/ingredients/show.html.erb_spec.rb deleted file mode 100644 index 539b987..0000000 --- a/spec/views/ingredients/show.html.erb_spec.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'rails_helper' - -RSpec.describe "ingredients/show", type: :view do - before(:each) do - @ingredient = assign(:ingredient, Ingredient.create!()) - end - - it "renders attributes in

" do - render - end -end diff --git a/spec/views/recipes/edit.html.erb_spec.rb b/spec/views/recipes/edit.html.erb_spec.rb deleted file mode 100644 index 262f000..0000000 --- a/spec/views/recipes/edit.html.erb_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'rails_helper' - -RSpec.describe "recipes/edit", type: :view do - before(:each) do - @recipe = assign(:recipe, Recipe.create!()) - end - - it "renders the edit recipe form" do - render - - assert_select "form[action=?][method=?]", recipe_path(@recipe), "post" do - end - end -end diff --git a/spec/views/recipes/index.html.erb_spec.rb b/spec/views/recipes/index.html.erb_spec.rb deleted file mode 100644 index aa88b16..0000000 --- a/spec/views/recipes/index.html.erb_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'rails_helper' - -RSpec.describe "recipes/index", type: :view do - before(:each) do - assign(:recipes, [ - Recipe.create!(), - Recipe.create!() - ]) - end - - it "renders a list of recipes" do - render - end -end diff --git a/spec/views/recipes/new.html.erb_spec.rb b/spec/views/recipes/new.html.erb_spec.rb deleted file mode 100644 index b9b1f99..0000000 --- a/spec/views/recipes/new.html.erb_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'rails_helper' - -RSpec.describe "recipes/new", type: :view do - before(:each) do - assign(:recipe, Recipe.new()) - end - - it "renders new recipe form" do - render - - assert_select "form[action=?][method=?]", recipes_path, "post" do - end - end -end diff --git a/spec/views/recipes/show.html.erb_spec.rb b/spec/views/recipes/show.html.erb_spec.rb deleted file mode 100644 index cf54599..0000000 --- a/spec/views/recipes/show.html.erb_spec.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'rails_helper' - -RSpec.describe "recipes/show", type: :view do - before(:each) do - @recipe = assign(:recipe, Recipe.create!()) - end - - it "renders attributes in

" do - render - end -end