from .math_integer_sqrt import integer_sqrt from .math_round_div import RoundingMode from .money_amount import Money, assert_same_currency MAX_SAFE = 2**53 - 1 MAX_I64 = 2**63 - 1 def economic_batch_quantity( annual_demand: int, annual_production_rate: int, setup_cost: Money, holding_cost: Money, mode: RoundingMode, ) -> int: """Economic batch quantity, sqrt(2DSP / (H(P - D))), in whole units. The root is never taken in floating point: the floor is an exact integer square root and the rounding decision compares whole numbers. """ if isinstance(annual_demand, bool) or not isinstance(annual_demand, int) or annual_demand < 0 or annual_demand > MAX_SAFE: raise ValueError("annualDemand must be a whole number of units, not negative, received %r" % (annual_demand,)) if ( isinstance(annual_production_rate, bool) or not isinstance(annual_production_rate, int) or annual_production_rate <= annual_demand or annual_production_rate > MAX_SAFE ): raise ValueError( "annualProductionRate must be a whole number greater than annualDemand, received %r" % (annual_production_rate,) ) assert_same_currency(setup_cost, holding_cost) if setup_cost.minor < 0: raise ValueError("setupCost must not be negative, received %d" % setup_cost.minor) if holding_cost.minor <= 0: raise ValueError("holdingCost must be greater than zero, received %d" % holding_cost.minor) d, p, s = annual_demand, annual_production_rate, setup_cost.minor eight = 8 * d * s * p if eight > MAX_I64: raise ValueError( "annualDemand, setupCost and annualProductionRate are too large: 8 x D x S x P must stay within 2^63 - 1" ) two = 2 * d * s * p m = holding_cost.minor * (p - d) n = integer_sqrt(two // m) odd = 2 * n + 1 if mode == "down": return n if mode == "up": return n if n * n * m == two else n + 1 if mode == "half-up": return n + 1 if odd * odd * m <= eight else n if mode == "half-even": half = odd * odd * m if half < eight: return n + 1 if half > eight: return n return n if n % 2 == 0 else n + 1 raise ValueError('unknown rounding mode "%s"' % (mode,))