Skip to content

Instantly share code, notes, and snippets.

@rubiii
Last active April 11, 2018 17:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rubiii/37d52acec8dd5d8edafac1bfb74c65c3 to your computer and use it in GitHub Desktop.
Save rubiii/37d52acec8dd5d8edafac1bfb74c65c3 to your computer and use it in GitHub Desktop.
Rails validator for Postgres decimal type with custom precision and scale
class DecimalValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
precision = options[:precision]
scale = options[:scale]
if !precision || !scale
raise ArgumentError, "#{self.class.name} expects :precision and :scale option"
end
original_value = record.public_send("#{attribute}_before_type_cast").to_s
real_wholes, real_decimals = original_value.split(".")
wholes = (precision - scale) + (scale - (real_decimals.try(:length) || 0))
regex = /\A\d{1,#{wholes}}(\.\d{0,#{scale}})?\z/
if regex !~ original_value
record.errors.add(attribute, :decimal_out_of_specs, precision: precision, scale: scale)
return
end
# Prevent PG::NumericValueOutOfRange: ERROR: numeric field overflow
# DETAIL: A field with precision 10, scale 4 must round to an absolute value less than 10^6.
if value.round >= 10 ** (precision - scale)
record.errors.add(attribute, :decimal_out_of_range, max: "10^#{precision - scale}")
end
end
end
require "test_helper"
class DecimalValidatorTest < ActiveSupport::TestCase
# unfortunately needs a real model to test Postgres errors
def valid_quantity?(quantity)
record = build(:test_model, quantity: quantity)
record.validate
errors = record.errors[:quantity]
errors.empty? && record.save
end
test "configuration matches testcases" do
validator = TestModel.validators_on(:quantity).find { |v| v.kind_of?(DecimalValidator) }
fail "Missing DecimalValidator" unless validator
configuration = validator.options
assert_equal 10, configuration[:precision]
assert_equal 4, configuration[:scale]
end
test "accepts valid values" do
assert valid_quantity?(0)
assert valid_quantity?("0.0001")
assert valid_quantity?("999999")
assert valid_quantity?("123456.1234")
assert valid_quantity?("999998.9999")
end
test "rejects invalid values" do
refute valid_quantity?(nil)
refute valid_quantity?("")
refute valid_quantity?(".")
refute valid_quantity?(".1")
refute valid_quantity?("string")
refute valid_quantity?(1_000_000)
refute valid_quantity?("0.12345")
refute valid_quantity?("1234567.1234")
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment