diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 86a43ce..97b2ce3 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -5,7 +5,7 @@ Schema, Required, Extra, Invalid, In, Remove, Literal, Url, MultipleInvalid, LiteralInvalid, NotIn, Match, Email, Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA, - validate, ExactSequence, Equal, Unordered + validate, ExactSequence, Equal, Unordered, Number ) from voluptuous.humanize import humanize_error @@ -557,3 +557,93 @@ def fn(arg): return "hello" assert_raises(Invalid, fn, 1) + + +def test_number_validation_with_string(): + """ test with Number with string""" + schema = Schema({"number" : Number(precision=6, scale=2)}) + try: + schema({"number": 'teststr'}) + except MultipleInvalid as e: + assert_equal(str(e), + "Value must be a number enclosed with string for dictionary value @ data['number']") + else: + assert False, "Did not raise Invalid for String" + + +def test_number_validation_with_invalid_precision_invalid_scale(): + """ test with Number with invalid precision and scale""" + schema = Schema({"number" : Number(precision=6, scale=2)}) + try: + schema({"number": '123456.712'}) + except MultipleInvalid as e: + assert_equal(str(e), + "Precision must be equal to 6, and Scale must be equal to 2 for dictionary value @ data['number']") + else: + assert False, "Did not raise Invalid for String" + + +def test_number_validation_with_valid_precision_scale_yield_decimal_true(): + """ test with Number with valid precision and scale""" + schema = Schema({"number" : Number(precision=6, scale=2, yield_decimal=True)}) + out_ = schema({"number": '1234.00'}) + assert_equal(float(out_.get("number")), 1234.00) + + +def test_number_when_precision_scale_none_yield_decimal_true(): + """ test with Number with no precision and scale""" + schema = Schema({"number" : Number(yield_decimal=True)}) + out_ = schema({"number": '12345678901234'}) + assert_equal(out_.get("number"), 12345678901234) + + +def test_number_when_precision_none_n_valid_scale_case1_yield_decimal_true(): + """ test with Number with no precision and valid scale case 1""" + schema = Schema({"number" : Number(scale=2, yield_decimal=True)}) + out_ = schema({"number": '123456789.34'}) + assert_equal(float(out_.get("number")), 123456789.34) + + +def test_number_when_precision_none_n_valid_scale_case2_yield_decimal_true(): + """ test with Number with no precision and valid scale case 2 with zero in decimal part""" + schema = Schema({"number" : Number(scale=2, yield_decimal=True)}) + out_ = schema({"number": '123456789012.00'}) + assert_equal(float(out_.get("number")), 123456789012.00) + + +def test_number_when_precision_none_n_invalid_scale_yield_decimal_true(): + """ test with Number with no precision and invalid scale""" + schema = Schema({"number" : Number(scale=2, yield_decimal=True)}) + try: + schema({"number": '12345678901.234'}) + except MultipleInvalid as e: + assert_equal(str(e), + "Scale must be equal to 2 for dictionary value @ data['number']") + else: + assert False, "Did not raise Invalid for String" + + +def test_number_when_valid_precision_n_scale_none_yield_decimal_true(): + """ test with Number with no precision and valid scale""" + schema = Schema({"number" : Number(precision=14, yield_decimal=True)}) + out_ = schema({"number": '1234567.8901234'}) + assert_equal(float(out_.get("number")), 1234567.8901234) + + +def test_number_when_invalid_precision_n_scale_none_yield_decimal_true(): + """ test with Number with no precision and invalid scale""" + schema = Schema({"number" : Number(precision=14, yield_decimal=True)}) + try: + schema({"number": '12345674.8901234'}) + except MultipleInvalid as e: + assert_equal(str(e), + "Precision must be equal to 14 for dictionary value @ data['number']") + else: + assert False, "Did not raise Invalid for String" + + +def test_number_validation_with_valid_precision_scale_yield_decimal_false(): + """ test with Number with valid precision, scale and no yield_decimal""" + schema = Schema({"number" : Number(precision=6, scale=2, yield_decimal=False)}) + out_ = schema({"number": '1234.00'}) + assert_equal(out_.get("number"), '1234.00') diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 4e3a3ac..51e247f 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -3,7 +3,7 @@ import datetime import sys from functools import wraps - +from decimal import Decimal, InvalidOperation try: from schema_builder import Schema, raises, message @@ -794,3 +794,62 @@ def __call__(self, v): def __repr__(self): return 'Unordered([{}])'.format(", ".join(repr(v) for v in self.validators)) + + +class Number(object): + """ + Verify the number of digits that are present in the number(Precision), + and the decimal places(Scale) + + :raises Invalid: If the value does not match the provided Precision and Scale. + + >>> schema = Schema(Number(precision=6, scale=2)) + >>> schema('1234.01') + '1234.01' + >>> schema = Schema(Number(precision=6, scale=2, yield_decimal=True)) + >>> schema('1234.01') + Decimal('1234.01') + """ + + def __init__(self, precision=None, scale=None, msg=None, yield_decimal=False): + self.precision = precision + self.scale = scale + self.msg = msg + self.yield_decimal = yield_decimal + + def __call__(self, v): + """ + :param v: is a number enclosed with string + :return: Decimal number + """ + precision, scale, decimal_num = self._get_precision_scale(v) + + if self.precision is not None and self.scale is not None and\ + precision != self.precision and scale != self.scale: + raise Invalid(self.msg or "Precision must be equal to %s, and Scale must be equal to %s" %(self.precision, self.scale)) + else: + if self.precision is not None and precision != self.precision: + raise Invalid(self.msg or "Precision must be equal to %s"%self.precision) + + if self.scale is not None and scale != self.scale : + raise Invalid(self.msg or "Scale must be equal to %s"%self.scale) + + if self.yield_decimal: + return decimal_num + else: + return v + + def __repr__(self): + return ('Number(precision=%s, scale=%s, msg=%s)' % (self.precision, self.scale, self.msg)) + + def _get_precision_scale(self, number): + """ + :param number: + :return: tuple(precision, scale, decimal_number) + """ + try: + decimal_num = Decimal(number) + except InvalidOperation: + raise Invalid(self.msg or 'Value must be a number enclosed with string') + + return (len(decimal_num.as_tuple().digits), -(decimal_num.as_tuple().exponent), decimal_num)