$life_months) return 0.0; $remaining = round(self::depreciableBase($cost, $salvage) - $accumulated, 2); if ($remaining <= 0) return 0.0; if ($period_index === $life_months) return $remaining; return min(self::monthlyAmount($cost, $salvage, $life_months), $remaining); } /** * Full projected schedule. * * @return array */ public static function schedule(float $cost, float $salvage, int $life_months, string $in_service_date): array { $rows = []; $accumulated = 0.0; $period = self::firstPeriod($in_service_date); for ($i = 1; $i <= $life_months; $i++) { $amount = self::amountFor($cost, $salvage, $life_months, $i, $accumulated); $accumulated = round($accumulated + $amount, 2); $rows[] = [ 'period' => $period, 'amount' => $amount, 'accumulated' => $accumulated, 'book_value' => round($cost - $accumulated, 2), ]; $period = self::addMonths($period, 1); } return $rows; } /** 1-based position of $period in the asset's life (0 or less = before in service). */ public static function periodIndex(string $in_service_date, string $period): int { return self::monthsBetween(self::firstPeriod($in_service_date), $period) + 1; } public static function assertInputs(float $cost, float $salvage, int $life_months): void { if ($cost <= 0) throw new Exception('Cost must be greater than zero.'); if ($salvage < 0) throw new Exception('Salvage value cannot be negative.'); if ($salvage >= $cost) throw new Exception('Salvage value must be less than cost.'); if ($life_months < 1) throw new Exception('Useful life must be at least 1 month.'); if ($life_months > 1200) throw new Exception('Useful life cannot exceed 1200 months.'); if (self::monthlyAmount($cost, $salvage, $life_months) <= 0) { throw new Exception('The amount to depreciate is too small for this useful life.'); } } private static function splitPeriod(string $period): array { if (!preg_match('/^(\d{4})-(\d{2})$/', $period, $m) || (int)$m[2] < 1 || (int)$m[2] > 12) { throw new Exception("Invalid period '{$period}'."); } return [(int)$m[1], (int)$m[2]]; } }