Source code for shuup.utils.numbers

# -*- coding: utf-8 -*-
# This file is part of Shuup.
#
# Copyright (c) 2012-2021, Shuup Commerce Inc. All rights reserved.
#
# This source code is licensed under the OSL-3.0 license found in the
# LICENSE file in the root directory of this source tree.
from __future__ import unicode_literals

import re
import six
from decimal import ROUND_HALF_EVEN, ROUND_HALF_UP, Decimal

from shuup.utils import update_module_attributes

from ._unitted_decimal import UnitMixupError, UnittedDecimal

__all__ = [
    "UnittedDecimal",
    "UnitMixupError",
    "bankers_round",
    "strip_non_float_chars",
    "parse_decimal_string",
    "try_parse_decimal_string",
    "get_string_sort_order",
]

quant_cache = {}


[docs]def bankers_round(value, ndigits=0): if isinstance(value, float): value = Decimal(str(value)) elif not isinstance(value, Decimal): value = Decimal(value) # We can cache the quantizers... quantizer = quant_cache.get(ndigits) if quantizer is None: quantizer = quant_cache[ndigits] = Decimal(10) ** (-int(ndigits)) return value.quantize(quantizer, rounding=ROUND_HALF_EVEN)
def nickel_round(value, quant=Decimal("0.05"), rounding=ROUND_HALF_UP): """ Round decimal value to nearest quant. >>> nickel_round(Decimal('10.32')) Decimal('10.30') >>> nickel_round(Decimal('10.33')) Decimal('10.35') >>> nickel_round(Decimal('10.325')) Decimal('10.35') >>> nickel_round(Decimal('10.3249')) Decimal('10.30') >>> nickel_round(Decimal('10.31'), Decimal('0.125')) Decimal('10.250') >>> nickel_round(Decimal('10.32'), Decimal('0.125')) Decimal('10.375') :type value: decimal.Decimal :type quant: decimal.Decimal :type rounding: str :rtype: decimal.Decimal """ assert isinstance(value, Decimal) assert isinstance(quant, Decimal) return (value / quant).quantize(1, rounding=rounding) * quant
[docs]def strip_non_float_chars(s): """ Strips characters that aren't part of normal floats. """ return re.sub("[^-+0123456789.]+", "", six.text_type(s))
_simple_decimal_rx = re.compile(r"^[-+]?(\d{1,50}\.\d{0,50}|\.?\d{1,50})$") raise_exception = object() def parse_simple_decimal(value, error=raise_exception): """ Parse simple decimal value from string. Simple decimal is basically a string of digits with optional sign and decimal point. Anything fancy, such as exponent forms, NaN or Infinity is an error. So are other unallowed characters. There is also a length limit of 50 digits before and after the decimal point. >>> assert parse_simple_decimal('42') == Decimal(42) >>> assert parse_simple_decimal('0') == Decimal(0) >>> assert parse_simple_decimal('3.5') == Decimal('3.5') >>> assert parse_simple_decimal('', None) is None >>> assert parse_simple_decimal(3.5, None ) is None >>> assert parse_simple_decimal(-5, None) is None >>> assert parse_simple_decimal('1e12', None) is None >>> assert parse_simple_decimal(float('inf'), None) is None :type value: str :param value: The input value as string :type error: Any :param error: Value to return on error, or by default raise an exception :rtype: Decimal|type(error) :raises ValueError: on errors by default """ decoded_value = value.decode("ascii", errors="replace") if six.PY2 and isinstance(value, bytes) else value if not isinstance(decoded_value, six.text_type) or (not _simple_decimal_rx.match(decoded_value)): if error is raise_exception: raise ValueError("Error! Value `%r` can't be parsed as a simple decimal." % (value,)) return error return Decimal(value)
[docs]def parse_decimal_string(s): """ Parse decimals with "best effort". Parses a string (or unicode) that may be embellished with spaces and other weirdness into the most probable Decimal. >>> assert parse_decimal_string('42') == Decimal(42) >>> assert parse_decimal_string('0') == Decimal(0) >>> assert parse_decimal_string('3.5') == Decimal('3.5') >>> assert parse_decimal_string('') == Decimal(0) >>> assert parse_decimal_string(3.5) == Decimal('3.5') >>> assert parse_decimal_string(-5) == Decimal(-5) >>> assert parse_decimal_string('1e12') == Decimal(112) >>> assert parse_decimal_string(float('inf')) == Decimal('inf') :param s: Input value :type s: str :return: Decimal :rtype: Decimal """ if isinstance(s, six.integer_types) or isinstance(s, Decimal): return Decimal(s) if isinstance(s, float): return Decimal(str(s)) s = s.strip().replace(" ", "") # Also 500 000.0 would be.. well, 500 :D if not s: return Decimal(0) if "," in s: # 500000,0? D: if "." in s: # Taking care of cases like 500,000.00 s = s.replace(",", "") # There we go, we have 500000.0 already else: s = s.replace(",", ".") # And there, it's a 500000.0 # Then clean up the rest and pray for DEITY HERE return Decimal(strip_non_float_chars(s.strip()))
[docs]def try_parse_decimal_string(s): try: return parse_decimal_string(s) except Exception: return None
GARMENT_SIZES = ("XXXS", "XXS", "XS", "S", "M", "L", "XL", "XXL", "XXXL")
[docs]def get_string_sort_order(s): """ Return a sorting order value for a string that contains a garment size. :param s: Input value (string or number) :type s: str :return: Sorting tuple :rtype: tuple """ # See if it's one of the predefined sizes for i, size in enumerate(GARMENT_SIZES): if size in s: return (10 + i, s) try: # If not, see if it looks enough like a decimal return (5, parse_decimal_string(s)) except Exception: # Otherwise just sort as a string return (1, s)
update_module_attributes(__all__, __name__)