use std::collections::BTreeMap;
use std::ops::RangeInclusive;
use crate::params::{BenefitParams, Bracket, StateParams, TaxParams};
use crate::plan::{AccountKind, Dollars, FilingStatus, Person, PlanDate};
pub const MEDICARE_AGE: u8 = 65;
pub const EARLIEST_CLAIM_AGE: i16 = 62;
const INDEXING_AGE: i16 = 60;
pub const FIRST_WORKING_AGE: i16 = 22;
pub const LATEST_CREDIT_AGE: i16 = 70;
pub const MONTHS_PER_YEAR: i32 = 12;
const COMPUTATION_YEARS: usize = 35;
const COMPUTATION_MONTHS: f64 = (COMPUTATION_YEARS as i32 * MONTHS_PER_YEAR) as f64;
const PIA_RATES: [f64; 3] = [0.90, 0.32, 0.15];
const EARLY_FIRST_MONTHS: i32 = 36;
const EARLY_FIRST_RATE: f64 = 5.0 / 900.0;
const EARLY_BEYOND_RATE: f64 = 5.0 / 1200.0;
const DELAYED_RATE: f64 = 2.0 / 300.0;
const FRA_65_LAST_BIRTH_YEAR: i16 = 1937;
const FRA_66_FIRST_BIRTH_YEAR: i16 = 1943;
const FRA_66_LAST_BIRTH_YEAR: i16 = 1954;
const FRA_67_FIRST_BIRTH_YEAR: i16 = 1960;
const FRA_STEP_MONTHS: i32 = 2;
pub const IRMAA_LOOKBACK_YEARS: i16 = 2;
#[must_use]
pub fn is_medicare_covered(person: &Person, year: i16) -> bool {
person.age_in_year(year) >= i16::from(MEDICARE_AGE)
}
const RMD_AGE_75_BIRTH_YEAR: i16 = 1960;
const RMD_START_AGE_EARLY: u8 = 73;
const RMD_START_AGE_LATE: u8 = 75;
#[must_use]
pub fn ordinary_tax(params: &TaxParams, status: FilingStatus, taxable: Dollars) -> Dollars {
walk_brackets(params.brackets.for_status(status), taxable)
}
#[must_use]
pub fn state_tax(
state: &StateParams,
status: FilingStatus,
income: Dollars,
taxable_social_security: Dollars,
) -> Dollars {
let benefits = if state.taxes_social_security {
taxable_social_security
} else {
0
};
let taxable = income + benefits - state.deduction.get(status);
walk_brackets(state.brackets.for_status(status), taxable)
}
fn walk_brackets(brackets: &[Bracket], taxable: Dollars) -> Dollars {
let mut tax = 0.0;
for (i, bracket) in brackets.iter().enumerate() {
if taxable <= bracket.over {
break;
}
let top = brackets
.get(i + 1)
.map_or(taxable, |next| taxable.min(next.over));
tax += bracket.rate * (top - bracket.over) as f64;
}
tax.round() as Dollars
}
#[must_use]
pub fn ltcg_tax(
params: &TaxParams,
status: FilingStatus,
ordinary_taxable: Dollars,
gains: Dollars,
) -> Dollars {
if gains <= 0 {
return 0;
}
let stack_bottom = ordinary_taxable.max(0);
let zero_top = params.ltcg.zero_until.get(status);
let fifteen_top = params.ltcg.fifteen_until.get(status);
let in_zero = (zero_top - stack_bottom).clamp(0, gains);
let above_zero = gains - in_zero;
let in_middle = (fifteen_top - stack_bottom.max(zero_top)).clamp(0, above_zero);
let in_top = above_zero - in_middle;
let tax = params.ltcg.middle_rate * in_middle as f64 + params.ltcg.top_rate * in_top as f64;
tax.round() as Dollars
}
#[must_use]
pub fn taxable_social_security(
params: &TaxParams,
status: FilingStatus,
other_income: Dollars,
benefit: Dollars,
) -> Dollars {
if benefit <= 0 {
return 0;
}
let provisional = other_income + benefit / 2;
let base = params.social_security.provisional_base.get(status);
let upper = params.social_security.provisional_upper.get(status);
if provisional <= base {
return 0;
}
if provisional <= upper {
return ((provisional - base) / 2).min(benefit / 2);
}
let from_lower_band = ((upper - base) / 2).min(benefit / 2);
let above_upper = (0.85 * (provisional - upper) as f64).round() as Dollars;
(above_upper + from_lower_band).min((0.85 * benefit as f64).round() as Dollars)
}
#[must_use]
pub fn full_retirement_months(birth_year: i16) -> i32 {
let steps = |from: i16| FRA_STEP_MONTHS * i32::from(birth_year - from);
if birth_year <= FRA_65_LAST_BIRTH_YEAR {
65 * MONTHS_PER_YEAR
} else if birth_year < FRA_66_FIRST_BIRTH_YEAR {
65 * MONTHS_PER_YEAR + steps(FRA_65_LAST_BIRTH_YEAR)
} else if birth_year <= FRA_66_LAST_BIRTH_YEAR {
66 * MONTHS_PER_YEAR
} else if birth_year < FRA_67_FIRST_BIRTH_YEAR {
66 * MONTHS_PER_YEAR + steps(FRA_66_LAST_BIRTH_YEAR)
} else {
67 * MONTHS_PER_YEAR
}
}
#[must_use]
pub fn social_security_benefit(
params: &BenefitParams,
birth_year: i16,
claim_age: i16,
earnings: &BTreeMap<i16, Dollars>,
) -> Dollars {
let pia = primary_insurance_amount(params, birth_year, earnings);
let monthly = (pia * claim_factor(birth_year, claim_age) * 100.0).round() / 100.0;
monthly.floor() as Dollars * MONTHS_PER_YEAR as Dollars
}
#[must_use]
pub fn earnings_at_wage(
params: &BenefitParams,
salary: Dollars,
paid_in: i16,
years: RangeInclusive<i16>,
) -> BTreeMap<i16, Dollars> {
years
.filter_map(|year| {
let scaled = salary as f64 * params.wage_ratio(year, paid_in)?;
Some((year, scaled.round() as Dollars))
})
.collect()
}
#[must_use]
pub fn claim_year_share(birth: PlanDate, claim_age: i16) -> f64 {
let first_paid = i16::from(birth.0.month())
+ match (claim_age == EARLIEST_CLAIM_AGE, birth.0.day()) {
(true, day) if day > 2 => 1,
(false, 1) => -1,
_ => 0,
};
let months = (MONTHS_PER_YEAR as i16 + 1 - first_paid).clamp(0, MONTHS_PER_YEAR as i16);
f64::from(months) / f64::from(MONTHS_PER_YEAR)
}
fn primary_insurance_amount(
params: &BenefitParams,
birth_year: i16,
earnings: &BTreeMap<i16, Dollars>,
) -> f64 {
let index_year = birth_year + INDEXING_AGE;
let to_index_year = |year| {
if year >= index_year {
1.0
} else {
params.wage_ratio(index_year, year).unwrap_or(1.0)
}
};
let mut indexed: Vec<f64> = earnings
.iter()
.map(|(&year, &amount)| amount.min(params.wage_base(year)) as f64 * to_index_year(year))
.collect();
indexed.sort_by(|a, b| b.total_cmp(a));
let total: f64 = indexed.iter().take(COMPUTATION_YEARS).sum();
let aime = (total / COMPUTATION_MONTHS).floor();
let eligibility_year = birth_year + EARLIEST_CLAIM_AGE;
let [first, second] = params
.bend_points(eligibility_year)
.map(|point| point as f64);
let pia = PIA_RATES[0] * aime.min(first)
+ PIA_RATES[1] * (aime.min(second) - first).max(0.0)
+ PIA_RATES[2] * (aime - second).max(0.0);
((pia * 100.0).round() / 10.0).floor() / 10.0
}
fn claim_factor(birth_year: i16, claim_age: i16) -> f64 {
let full = full_retirement_months(birth_year);
let claim = i32::from(claim_age.min(LATEST_CREDIT_AGE)) * MONTHS_PER_YEAR;
if claim >= full {
return 1.0 + f64::from(claim - full) * DELAYED_RATE;
}
let early = full - claim;
let first = early.min(EARLY_FIRST_MONTHS);
1.0 - f64::from(first) * EARLY_FIRST_RATE - f64::from(early - first) * EARLY_BEYOND_RATE
}
#[must_use]
pub fn irmaa_surcharge(
params: &TaxParams,
status: FilingStatus,
magi: Dollars,
part_d: bool,
) -> Dollars {
params
.irmaa
.iter()
.rev()
.find(|tier| magi > tier.magi_over.get(status))
.map_or(0, |tier| tier.part_b + if part_d { tier.part_d } else { 0 })
}
#[must_use]
pub fn irmaa_threshold(params: &TaxParams, status: FilingStatus, tier: u8) -> Option<Dollars> {
params
.irmaa
.get(usize::from(tier))
.map(|entry| entry.magi_over.get(status))
}
#[must_use]
pub fn rmd(params: &TaxParams, age: u8, balance: Dollars) -> Dollars {
if balance <= 0 {
return 0;
}
let divisors = ¶ms.rmd.divisors;
let Some(first) = divisors.first() else {
return 0;
};
if age < first.age {
return 0;
}
let divisor = divisors
.iter()
.rev()
.find(|row| row.age <= age)
.map_or(first.divisor, |row| row.divisor);
(balance as f64 / divisor).round() as Dollars
}
#[must_use]
pub fn rmd_start_age(birth_year: i16) -> u8 {
if birth_year >= RMD_AGE_75_BIRTH_YEAR {
RMD_START_AGE_LATE
} else {
RMD_START_AGE_EARLY
}
}
#[must_use]
pub fn is_penalty_exempt(kind: AccountKind) -> bool {
matches!(kind, AccountKind::K457b)
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum LimitPool {
EmployerPlan,
Deferred457,
Simple,
Ira,
Hsa,
}
#[must_use]
pub fn limit_pool(kind: AccountKind) -> Option<LimitPool> {
match kind {
AccountKind::K401k | AccountKind::K403b => Some(LimitPool::EmployerPlan),
AccountKind::K457b => Some(LimitPool::Deferred457),
AccountKind::SimpleIra => Some(LimitPool::Simple),
AccountKind::Ira => Some(LimitPool::Ira),
AccountKind::Hsa => Some(LimitPool::Hsa),
AccountKind::K414k | AccountKind::SepIra | AccountKind::Brokerage | AccountKind::Cash => {
None
}
}
}
#[must_use]
pub fn employee_limit(
params: &TaxParams,
status: FilingStatus,
kind: AccountKind,
age: u8,
) -> Option<Dollars> {
let limits = ¶ms.limits;
let plan_catch_up = |base_50: Dollars, base_60: Dollars| match age {
60..=63 => base_60,
50.. => base_50,
_ => 0,
};
let limit = match limit_pool(kind)? {
LimitPool::EmployerPlan | LimitPool::Deferred457 => {
limits.employer_plan
+ plan_catch_up(
limits.employer_plan_catch_up_50,
limits.employer_plan_catch_up_60,
)
}
LimitPool::Simple => {
limits.simple + plan_catch_up(limits.simple_catch_up_50, limits.simple_catch_up_60)
}
LimitPool::Ira => limits.ira + if age >= 50 { limits.ira_catch_up_50 } else { 0 },
LimitPool::Hsa => {
let coverage = match status {
FilingStatus::Single => limits.hsa_self,
FilingStatus::MarriedJoint => limits.hsa_family,
};
coverage + if age >= 55 { limits.hsa_catch_up_55 } else { 0 }
}
};
Some(limit)
}