from .math_round_div import round_div from .money_amount import Money, money from .retail_price_rounding_types import PriceDirection def _whole(value: object) -> bool: return isinstance(value, int) and not isinstance(value, bool) def round_price(price: Money, step: int, ending: int, direction: PriceDirection) -> Money: """Snap a price to the points k x step - ending. Shifting by ``ending`` turns every point into a multiple of ``step``, so the whole policy is one integer division with the right rounding mode. """ if not _whole(step) or step < 1: raise ValueError("step must be 1 or more, received %r" % (step,)) if not _whole(ending) or ending < 0 or ending >= step: raise ValueError("ending must be from 0 to step - 1, received %r" % (ending,)) if price.minor < 0: raise ValueError("price must not be negative, received %d" % price.minor) shifted = price.minor + ending if direction == "up": point = round_div(shifted, step, "up") * step - ending elif direction == "down": point = round_div(shifted, step, "down") * step - ending if point < 0: raise ValueError("no price point at or below %d" % price.minor) elif direction == "nearest": point = round_div(shifted, step, "half-up") * step - ending # Below the first point the only candidate is above. if point < 0: point = step - ending else: raise ValueError('unknown direction "%s"' % (direction,)) return money(point, price.currency)