diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb
index aa05673683..440c49c174 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)
+ def validate_each(record, attr_name, value, precision: Float::DIG)
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)
+ unless is_number?(raw_value, precision)
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)
+ value = parse_as_number(raw_value, precision)
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)
+ option_value = parse_as_number(option_value, precision)
unless value.send(CHECKS[option], option_value)
record.errors.add(attr_name, option, **filtered_options(value).merge!(count: option_value))
@@ -79,24 +79,24 @@ module ActiveModel
end
private
- def is_number?(raw_value)
- !parse_as_number(raw_value).nil?
- rescue ArgumentError, TypeError
- false
- end
-
- def parse_as_number(raw_value)
+ def parse_as_number(raw_value, precision)
if raw_value.is_a?(Float)
- raw_value.to_d
+ raw_value.to_d(precision)
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
+ Kernel.Float(raw_value).to_d(precision)
end
end
+ def is_number?(raw_value, precision)
+ !parse_as_number(raw_value, precision).nil?
+ rescue ArgumentError, TypeError
+ false
+ end
+
def is_integer?(raw_value)
INTEGER_REGEX.match?(raw_value.to_s)
end
@@ -132,7 +132,8 @@ module ActiveModel
# Validates whether the value of the specified attribute is numeric by
# trying to convert it to a float with Kernel.Float (if only_integer
# is +false+) or applying it to the regular expression /\A[\+\-]?\d+\z/
- # (if only_integer is set to +true+).
+ # (if only_integer is set to +true+). Precision of Kernel.Float values
+ # are guaranteed up to 15 digits.
#
# class Person < ActiveRecord::Base
# validates_numericality_of :value, on: :create
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index aa953b0d64..5a7a3b6029 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,8 @@
+* Add `ActiveRecord::Validations::NumericalityValidator` with
+ support for casting floats using a database columns' precision value.
+
+ *Gannon McGibbon*
+
* Enforce fresh ETag header after a collection's contents change by adding
ActiveRecord::Relation#cache_key_with_version. This method will be used by
ActionController::ConditionalGet to ensure that when collection cache versioning
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 86bfb661dc..818515b4e0 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -91,3 +91,4 @@ require "active_record/validations/uniqueness"
require "active_record/validations/presence"
require "active_record/validations/absence"
require "active_record/validations/length"
+require "active_record/validations/numericality"
diff --git a/activerecord/lib/active_record/validations/numericality.rb b/activerecord/lib/active_record/validations/numericality.rb
new file mode 100644
index 0000000000..0df93fede1
--- /dev/null
+++ b/activerecord/lib/active_record/validations/numericality.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module Validations
+ class NumericalityValidator < ActiveModel::Validations::NumericalityValidator # :nodoc:
+ def initialize(options)
+ super
+ @klass = options[:class]
+ end
+
+ def validate_each(record, attribute, value, precision: nil)
+ precision = column_precision_for(attribute) || Float::DIG
+ super
+ end
+
+ private
+ def column_precision_for(attribute)
+ if @klass < ActiveRecord::Base
+ @klass.type_for_attribute(attribute.to_s)&.precision
+ end
+ end
+ end
+
+ module ClassMethods
+ # Validates whether the value of the specified attribute is numeric by
+ # trying to convert it to a float with Kernel.Float (if only_integer
+ # is +false+) or applying it to the regular expression /\A[\+\-]?\d+\z/
+ # (if only_integer is set to +true+). Kernel.Float precision
+ # defaults to the column's precision value or 15.
+ #
+ # See ActiveModel::Validations::HelperMethods.validates_numericality_of for more information.
+ def validates_numericality_of(*attr_names)
+ validates_with NumericalityValidator, _merge_attributes(attr_names)
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/validations/numericality_validation_test.rb b/activerecord/test/cases/validations/numericality_validation_test.rb
new file mode 100644
index 0000000000..8a9a314844
--- /dev/null
+++ b/activerecord/test/cases/validations/numericality_validation_test.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/numeric_data"
+
+class NumericalityValidationTest < ActiveRecord::TestCase
+ def setup
+ @model_class = NumericData.dup
+ end
+
+ attr_reader :model_class
+
+ def test_column_with_precision
+ model_class.validates_numericality_of(
+ :bank_balance, equal_to: 10_000_000.12
+ )
+
+ subject = model_class.new(bank_balance: 10_000_000.121)
+
+ assert_predicate subject, :valid?
+ end
+
+ def test_no_column_precision
+ model_class.validates_numericality_of(
+ :decimal_number, equal_to: 1_000_000_000.12345
+ )
+
+ subject = model_class.new(decimal_number: 1_000_000_000.123454)
+
+ assert_predicate subject, :valid?
+ end
+
+ def test_virtual_attribute
+ model_class.attribute(:virtual_decimal_number, :decimal)
+ model_class.validates_numericality_of(
+ :virtual_decimal_number, equal_to: 1_000_000_000.12345
+ )
+
+ subject = model_class.new(virtual_decimal_number: 1_000_000_000.123454)
+
+ assert_predicate subject, :valid?
+ end
+end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index f2f95f3b2c..8a69019b9a 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -582,6 +582,7 @@ ActiveRecord::Schema.define do
t.decimal :big_bank_balance, precision: 15, scale: 2
t.decimal :world_population, precision: 20, scale: 0
t.decimal :my_house_population, precision: 2, scale: 0
+ t.decimal :decimal_number
t.decimal :decimal_number_with_default, precision: 3, scale: 2, default: 2.78
t.float :temperature
# Oracle/SQLServer supports precision up to 38
diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md
index 5c86de964d..992d639e69 100644
--- a/guides/source/active_record_validations.md
+++ b/guides/source/active_record_validations.md
@@ -487,7 +487,7 @@ If you set `:only_integer` to `true`, then it will use the
```
regular expression to validate the attribute's value. Otherwise, it will try to
-convert the value to a number using `Float`.
+convert the value to a number using `Float`. `Float`s are casted to `BigDecimal` using the column's precision value or 15.
```ruby
class Player < ApplicationRecord