Merge pull request #38212 from gmcgibbon/numericality_validation_with_scale

Add scale support to ActiveRecord::Validations::NumericalityValidator
This commit is contained in:
Gannon McGibbon 2020-01-13 12:32:32 -05:00 committed by GitHub
commit 8c71793b3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 61 additions and 15 deletions

View File

@ -22,7 +22,7 @@ module ActiveModel
end end
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?" came_from_user = :"#{attr_name}_came_from_user?"
if record.respond_to?(came_from_user) if record.respond_to?(came_from_user)
@ -43,7 +43,7 @@ module ActiveModel
raw_value = value raw_value = value
end 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)) record.errors.add(attr_name, :not_a_number, **filtered_options(raw_value))
return return
end end
@ -53,7 +53,7 @@ module ActiveModel
return return
end 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| options.slice(*CHECKS.keys).each do |option, option_value|
case option case option
@ -69,7 +69,7 @@ module ActiveModel
option_value = record.send(option_value) option_value = record.send(option_value)
end 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) unless value.send(CHECKS[option], option_value)
record.errors.add(attr_name, option, **filtered_options(value).merge!(count: option_value)) record.errors.add(attr_name, option, **filtered_options(value).merge!(count: option_value))
@ -79,20 +79,24 @@ module ActiveModel
end end
private private
def parse_as_number(raw_value, precision) def parse_as_number(raw_value, precision, scale)
if raw_value.is_a?(Float) if raw_value.is_a?(Float)
raw_value.to_d(precision) parse_float(raw_value, precision, scale)
elsif raw_value.is_a?(Numeric) elsif raw_value.is_a?(Numeric)
raw_value raw_value
elsif is_integer?(raw_value) elsif is_integer?(raw_value)
raw_value.to_i raw_value.to_i
elsif !is_hexadecimal_literal?(raw_value) elsif !is_hexadecimal_literal?(raw_value)
Kernel.Float(raw_value).to_d(precision) parse_float(Kernel.Float(raw_value), precision, scale)
end end
end end
def is_number?(raw_value, precision) def parse_float(raw_value, precision, scale)
!parse_as_number(raw_value, precision).nil? (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 rescue ArgumentError, TypeError
false false
end end

View File

@ -1,3 +1,7 @@
* Add scale support to `ActiveRecord::Validations::NumericalityValidator`.
*Gannon McGibbon*
* Find orphans by looking for missing relations through chaining `where.missing`: * Find orphans by looking for missing relations through chaining `where.missing`:
Before: Before:

View File

@ -3,15 +3,20 @@
module ActiveRecord module ActiveRecord
module Validations module Validations
class NumericalityValidator < ActiveModel::Validations::NumericalityValidator # :nodoc: class NumericalityValidator < ActiveModel::Validations::NumericalityValidator # :nodoc:
def validate_each(record, attribute, value, precision: nil) def validate_each(record, attribute, value, precision: nil, scale: nil)
precision = [column_precision_for(attribute, record) || BigDecimal.double_fig, BigDecimal.double_fig].min precision = [column_precision_for(record, attribute) || BigDecimal.double_fig, BigDecimal.double_fig].min
super scale = column_scale_for(record, attribute)
super(record, attribute, value, precision: precision, scale: scale)
end end
private private
def column_precision_for(attribute, record) def column_precision_for(record, attribute)
record.class.type_for_attribute(attribute.to_s)&.precision record.class.type_for_attribute(attribute.to_s)&.precision
end end
def column_scale_for(record, attribute)
record.class.type_for_attribute(attribute.to_s)&.scale
end
end end
module ClassMethods module ClassMethods

View File

@ -12,10 +12,10 @@ class NumericalityValidationTest < ActiveRecord::TestCase
def test_column_with_precision def test_column_with_precision
model_class.validates_numericality_of( 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? assert_predicate subject, :valid?
end end
@ -30,6 +30,16 @@ class NumericalityValidationTest < ActiveRecord::TestCase
assert_predicate subject, :valid? assert_predicate subject, :valid?
end 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 def test_no_column_precision
model_class.validates_numericality_of( model_class.validates_numericality_of(
:decimal_number, equal_to: 1_000_000_000.123454 :decimal_number, equal_to: 1_000_000_000.123454
@ -70,4 +80,26 @@ class NumericalityValidationTest < ActiveRecord::TestCase
assert_predicate(subject, :valid?) assert_predicate(subject, :valid?)
end 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 end

View File

@ -580,6 +580,7 @@ ActiveRecord::Schema.define do
create_table :numeric_data, force: true do |t| create_table :numeric_data, force: true do |t|
t.decimal :bank_balance, precision: 10, scale: 2 t.decimal :bank_balance, precision: 10, scale: 2
t.decimal :big_bank_balance, precision: 15, 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 :world_population, precision: 20, scale: 0
t.decimal :my_house_population, precision: 2, scale: 0 t.decimal :my_house_population, precision: 2, scale: 0
t.decimal :decimal_number t.decimal :decimal_number