From f72f743dc380d6b4f3518a8a1047414499b5a6c2 Mon Sep 17 00:00:00 2001 From: Gannon McGibbon Date: Fri, 10 Jan 2020 14:53:45 -0500 Subject: [PATCH] Add scale support to ActiveRecord::Validations::NumericalityValidator --- .../active_model/validations/numericality.rb | 22 +++++++----- activerecord/CHANGELOG.md | 4 +++ .../active_record/validations/numericality.rb | 13 ++++--- .../numericality_validation_test.rb | 36 +++++++++++++++++-- activerecord/test/schema/schema.rb | 1 + 5 files changed, 61 insertions(+), 15 deletions(-) diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index 440c49c1740..b50af1ea2d1 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -22,7 +22,7 @@ module ActiveModel end end - def validate_each(record, attr_name, value, precision: Float::DIG) + def validate_each(record, attr_name, value, precision: Float::DIG, scale: nil) came_from_user = :"#{attr_name}_came_from_user?" if record.respond_to?(came_from_user) @@ -43,7 +43,7 @@ module ActiveModel raw_value = value end - unless is_number?(raw_value, precision) + unless is_number?(raw_value, precision, scale) record.errors.add(attr_name, :not_a_number, **filtered_options(raw_value)) return end @@ -53,7 +53,7 @@ module ActiveModel return end - value = parse_as_number(raw_value, precision) + value = parse_as_number(raw_value, precision, scale) options.slice(*CHECKS.keys).each do |option, option_value| case option @@ -69,7 +69,7 @@ module ActiveModel option_value = record.send(option_value) end - option_value = parse_as_number(option_value, precision) + option_value = parse_as_number(option_value, precision, scale) unless value.send(CHECKS[option], option_value) record.errors.add(attr_name, option, **filtered_options(value).merge!(count: option_value)) @@ -79,20 +79,24 @@ module ActiveModel end private - def parse_as_number(raw_value, precision) + def parse_as_number(raw_value, precision, scale) if raw_value.is_a?(Float) - raw_value.to_d(precision) + parse_float(raw_value, precision, scale) elsif raw_value.is_a?(Numeric) raw_value elsif is_integer?(raw_value) raw_value.to_i elsif !is_hexadecimal_literal?(raw_value) - Kernel.Float(raw_value).to_d(precision) + parse_float(Kernel.Float(raw_value), precision, scale) end end - def is_number?(raw_value, precision) - !parse_as_number(raw_value, precision).nil? + def parse_float(raw_value, precision, scale) + (scale ? raw_value.truncate(scale) : raw_value).to_d(precision) + end + + def is_number?(raw_value, precision, scale) + !parse_as_number(raw_value, precision, scale).nil? rescue ArgumentError, TypeError false end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index cde5fda9a41..68cb6e5a8c2 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,7 @@ +* Add scale support to `ActiveRecord::Validations::NumericalityValidator`. + + *Gannon McGibbon* + * Find orphans by looking for missing relations through chaining `where.missing`: Before: diff --git a/activerecord/lib/active_record/validations/numericality.rb b/activerecord/lib/active_record/validations/numericality.rb index ab05cfeb56b..3b1da7f8ba9 100644 --- a/activerecord/lib/active_record/validations/numericality.rb +++ b/activerecord/lib/active_record/validations/numericality.rb @@ -3,15 +3,20 @@ module ActiveRecord module Validations class NumericalityValidator < ActiveModel::Validations::NumericalityValidator # :nodoc: - def validate_each(record, attribute, value, precision: nil) - precision = [column_precision_for(attribute, record) || BigDecimal.double_fig, BigDecimal.double_fig].min - super + def validate_each(record, attribute, value, precision: nil, scale: nil) + precision = [column_precision_for(record, attribute) || BigDecimal.double_fig, BigDecimal.double_fig].min + scale = column_scale_for(record, attribute) + super(record, attribute, value, precision: precision, scale: scale) end private - def column_precision_for(attribute, record) + def column_precision_for(record, attribute) record.class.type_for_attribute(attribute.to_s)&.precision end + + def column_scale_for(record, attribute) + record.class.type_for_attribute(attribute.to_s)&.scale + end end module ClassMethods diff --git a/activerecord/test/cases/validations/numericality_validation_test.rb b/activerecord/test/cases/validations/numericality_validation_test.rb index af29a9f44b9..5b96d8f7cab 100644 --- a/activerecord/test/cases/validations/numericality_validation_test.rb +++ b/activerecord/test/cases/validations/numericality_validation_test.rb @@ -12,10 +12,10 @@ class NumericalityValidationTest < ActiveRecord::TestCase def test_column_with_precision model_class.validates_numericality_of( - :bank_balance, equal_to: 10_000_000.12 + :unscaled_bank_balance, equal_to: 10_000_000.12 ) - subject = model_class.new(bank_balance: 10_000_000.121) + subject = model_class.new(unscaled_bank_balance: 10_000_000.121) assert_predicate subject, :valid? end @@ -30,6 +30,16 @@ class NumericalityValidationTest < ActiveRecord::TestCase assert_predicate subject, :valid? end + def test_column_with_scale + model_class.validates_numericality_of( + :bank_balance, greater_than: 10 + ) + + subject = model_class.new(bank_balance: 10.001) + + assert_not_predicate subject, :valid? + end + def test_no_column_precision model_class.validates_numericality_of( :decimal_number, equal_to: 1_000_000_000.123454 @@ -70,4 +80,26 @@ class NumericalityValidationTest < ActiveRecord::TestCase assert_predicate(subject, :valid?) end + + def test_virtual_attribute_with_precision + model_class.attribute(:virtual_decimal_number, :decimal, precision: 5) + model_class.validates_numericality_of( + :virtual_decimal_number, equal_to: 123.45 + ) + + subject = model_class.new(virtual_decimal_number: 123.455) + + assert_predicate subject, :valid? + end + + def test_virtual_attribute_with_scale + model_class.attribute(:virtual_decimal_number, :decimal, scale: 2) + model_class.validates_numericality_of( + :virtual_decimal_number, greater_than: 1 + ) + + subject = model_class.new(virtual_decimal_number: 1.001) + + assert_not_predicate subject, :valid? + end end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 2f22176717d..ef0bc0f0b6a 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -580,6 +580,7 @@ ActiveRecord::Schema.define do create_table :numeric_data, force: true do |t| t.decimal :bank_balance, precision: 10, scale: 2 t.decimal :big_bank_balance, precision: 15, scale: 2 + t.decimal :unscaled_bank_balance, precision: 10 t.decimal :world_population, precision: 20, scale: 0 t.decimal :my_house_population, precision: 2, scale: 0 t.decimal :decimal_number