""" Payback period. Linear interpolation within the crossing year: returns the moment when the running net cashflow first turns non-negative. Inputs are *risk-adjusted, undiscounted* cashflows by convention (TEI shows "<6 months" payback for the Amazon Connect composite using the risk-adjusted nominal cashflows from the Cash Flow Analysis table). """ from __future__ import annotations from collections.abc import Iterable def payback_years( initial_cost: float, yearly_net_benefits: Iterable[float], ) -> float | None: """ Years until cumulative net benefits cover the initial cost. Args: initial_cost: positive number — undiscounted year-0 outlay. yearly_net_benefits: sequence of (benefits − costs) per year, Y1+. Returns: Float number of years, or ``None`` if payback is never reached within the supplied horizon. Example:: >>> # Amazon Connect: initial cost $1.196M, Y1 net $19.998M >>> # → ~0.06 years ≈ 0.7 months — well under 6 months. >>> round(payback_years(1_196_250, [19_997_953, 31_562_489, 47_443_905]), 3) 0.06 """ remaining = float(initial_cost) if remaining <= 0: return 0.0 cumulative_year = 0 for cf in yearly_net_benefits: cumulative_year += 1 cf = float(cf) if cf <= 0: remaining += -cf # net loss this year increases the gap continue if cf >= remaining: # Crossing happens partway through this year. fraction = remaining / cf return (cumulative_year - 1) + fraction remaining -= cf return None def payback_months( initial_cost: float, yearly_net_benefits: Iterable[float], ) -> float | None: """Same as :func:`payback_years`, expressed in months.""" yrs = payback_years(initial_cost, yearly_net_benefits) return yrs * 12.0 if yrs is not None else None