I'm using Playground as a testing ground for code with the intention to transition in the near future to creating an iOS app.
Background:
In case the term "back-door-Roth" is unfamiliar, let me attempt to level the bubble. If, in a given calendar year, an employee contributes to their 401k up to an amount totaling the IRS contribution limit (currently $22,500 in most cases), the employee typically is restricted by law to discontinue contributions. However, an employer may offer a "back-door" option which would allow the employee to continue contributing beyond the IRS contribution limit. However those "above-the-limit" contributions are placed into a separate Roth account, often labeled a "401a" account. This employee may continue to contribute to their 401a account throughout that calendar year so long as the sum total of his/her contributions and the company's contributions do not exceed the IRS combined contribution limit (currently $66,000 in most cases).
https://docs.google.com/spreadsheets/d/1Qvv5lNmbYbM-1WApM1LMZESy0vViDknf2_AED2FevmM/edit#gid=0
You'll see that 401a contributions in January and February look correct (matches the excel spreadsheet) but March and on are not correct (do not match the excel spreadsheet):
import Foundation
let pennyRoundingBehavior = NSDecimalNumberHandler(
roundingMode: .bankers,
scale: 2,
raiseOnExactness: false,
raiseOnOverflow: true,
raiseOnUnderflow: true,
raiseOnDivideByZero: true
)
func roundToNearestPenny(percentage: Decimal, of dollarAmount: Decimal) -> Decimal
{
assert((0...100).contains(percentage))
let x = ((dollarAmount * percentage / 100) as NSDecimalNumber)
return x.rounding(accordingToBehavior: pennyRoundingBehavior) as Decimal
}
enum ContributionMonth: Int, CaseIterable
{
case January, February, March, April, May, June,
July, August, September, October, November, December
static var count:Int { allCases.count }
}
struct MonthlyContributions
{
var roth : Decimal = 0
var traditional : Decimal = 0
var pers401a : Decimal = 0
var rothCompany : Decimal = 0
var traditionalCompany : Decimal = 0
var yearToDate : Decimal = 0
var yearToDateAll : Decimal = 0
}
extension MonthlyContributions
{
var total: Decimal { roth + traditional }
var companyTotal: Decimal { rothCompany + traditionalCompany }
func nextMonth(
combinedAmount: Decimal,
rothPercentage: Decimal,
rothPercentageCompany: Decimal,
combinedAmountCompany: Decimal
) -> Self
{
let roth = roundToNearestPenny(
percentage: rothPercentage,
of: combinedAmount
)
let rothCompany = roundToNearestPenny(
percentage: rothPercentageCompany,
of: combinedAmountCompany
)
return Self(
roth: roth,
traditional: combinedAmount - roth,
pers401a: combinedAmount < total ? total - combinedAmount : 0,
rothCompany: rothCompany,
traditionalCompany: combinedAmountCompany - rothCompany,
yearToDate: combinedAmount + pers401a + yearToDate,
yearToDateAll: combinedAmount + combinedAmountCompany + yearToDateAll
)
}
}
extension Array where Element == MonthlyContributions {
subscript (index: ContributionMonth) -> Element { self[index.rawValue] }
}
func calculatePersCombinedContribution(
monthlyContribution monthly : Decimal,
annualContributionLimit limit : Decimal,
contributionsSoFarThisYear cummulative : Decimal) -> Decimal
{
min(max(0, limit - cummulative), monthly)
}
func calculate401aContribution(
monthlyContribution monthly : Decimal,
combinedContributionLimit limit : Decimal,
contributionsSoFarThisYear cummulative : Decimal) -> Decimal
{
min(max(0, limit - cummulative), monthly)
}
func calculateAllCombinedContribution(
monthlyContribution monthly : Decimal,
annualCombinedContributionLimit limit : Decimal,
allContributionsSoFarThisYear cummulative: Decimal) -> Decimal
{
min(max(0, limit - cummulative), monthly)
}
func computeMonthlyContributions(
monthlyPay : Decimal,
contributionPercentage: Decimal,
companyContributionPercentage: Decimal,
rothPercentage : Decimal,
contributionLimit : Decimal,
combinedContributionLimit: Decimal) -> [MonthlyContributions]
{
assert((0...100).contains(contributionPercentage))
assert((0...100).contains(rothPercentage))
assert(monthlyPay >= 0)
assert(contributionLimit >= 0)
var contribution = MonthlyContributions()
var monthlyContributions: [MonthlyContributions] = []
monthlyContributions.reserveCapacity(ContributionMonth.count)
let monthlyContribution = roundToNearestPenny(
percentage: contributionPercentage,
of: monthlyPay
)
let monthlyCompanyContribution = roundToNearestPenny(
percentage: companyContributionPercentage,
of: monthlyPay
)
for _ in ContributionMonth.allCases
{
let combinedPersAmount = calculatePersCombinedContribution(
monthlyContribution : monthlyContribution,
annualContributionLimit : contributionLimit,
contributionsSoFarThisYear: contribution.yearToDate
)
let pers401Amount = calculate401aContribution(
monthlyContribution: monthlyContribution,
combinedContributionLimit: combinedContributionLimit,
contributionsSoFarThisYear: contribution.yearToDateAll
)
let combinedAllAmount = calculateAllCombinedContribution(
monthlyContribution : monthlyCompanyContribution,
annualCombinedContributionLimit : combinedContributionLimit,
allContributionsSoFarThisYear : contribution.yearToDateAll
)
contribution = contribution.nextMonth(
combinedAmount: combinedPersAmount,
rothPercentage: rothPercentage,
rothPercentageCompany: rothPercentage,
combinedAmountCompany: combinedAllAmount
)
monthlyContributions.append(contribution)
}
return monthlyContributions
}
var monthlyPay : Decimal = 12758.0
var personal401kLimit : Decimal = 22500.0
var personal401kPercentage: Decimal = 100.0
var companyContributionPercentage: Decimal = 16.0
var roth401kPercentage : Decimal = 10.0
var combinedContributionLimit : Decimal = 66000.0
let monthlyContributions = computeMonthlyContributions(
monthlyPay : monthlyPay,
contributionPercentage: personal401kPercentage,
companyContributionPercentage : companyContributionPercentage,
rothPercentage : roth401kPercentage,
contributionLimit : personal401kLimit,
combinedContributionLimit : combinedContributionLimit
)
for month in ContributionMonth.allCases
{
let curContribution = monthlyContributions[month]
print("Contribution for \(month)")
print(" Traditional : $\(curContribution.traditional)")
print(" Roth : $\(curContribution.roth)")
print(" Pers 401a : $\(curContribution.pers401a)")
// print(" Pers Year-To-Date: $\(curContribution.yearToDate)")
print(" Company Trad: $\(curContribution.traditionalCompany)")
print(" Company Roth: $\(curContribution.rothCompany)")
print(" Combined Year-To-Date: $\(curContribution.yearToDateAll)")
print()
}
The output:
Contribution for January
Traditional : $11482.2
Roth : $1275.8
Pers 401a : $0
Company Trad: $1837.15
Company Roth: $204.13
Combined Year-To-Date: $14799.28
Contribution for February
Traditional : $8767.8
Roth : $974.2
Pers 401a : $3016
Company Trad: $1837.15
Company Roth: $204.13
Combined Year-To-Date: $26582.56
Contribution for March
Traditional : $0
Roth : $0
Pers 401a : $9742
Company Trad: $1837.15
Company Roth: $204.13
Combined Year-To-Date: $28623.84
Contribution for April
Traditional : $0
Roth : $0
Pers 401a : $0
Company Trad: $1837.15
Company Roth: $204.13
Combined Year-To-Date: $30665.12
Contribution for May
Traditional : $0
Roth : $0
Pers 401a : $0
Company Trad: $1837.15
Company Roth: $204.13
Combined Year-To-Date: $32706.4
Contribution for June
Traditional : $0
Roth : $0
Pers 401a : $0
Company Trad: $1837.15
Company Roth: $204.13
Combined Year-To-Date: $34747.68
Contribution for July
Traditional : $0
Roth : $0
Pers 401a : $0
Company Trad: $1837.15
Company Roth: $204.13
Combined Year-To-Date: $36788.96
...
Contribution for December
Traditional : $0
Roth : $0
Pers 401a : $0
Company Trad: $1837.15
Company Roth: $204.13
Combined Year-To-Date: $46995.36
The goal is for the printouts for "Pers 401a" and "Combined Year-To-Date" monthly values to reflect the excel spreadsheet.
https://docs.google.com/spreadsheets/d/1Qvv5lNmbYbM-1WApM1LMZESy0vViDknf2_AED2FevmM/edit#gid=0
I have outlined the conditionals which define each month's 401a contributions in comments below and applied the comments into code, but the results are messy and inaccurate.
import Foundation
let pennyRoundingBehavior = NSDecimalNumberHandler(
roundingMode: .bankers,
scale: 2,
raiseOnExactness: false,
raiseOnOverflow: true,
raiseOnUnderflow: true,
raiseOnDivideByZero: true
)
func roundToNearestPenny(percentage: Decimal, of dollarAmount: Decimal) -> Decimal
{
assert((0...100).contains(percentage))
let x = ((dollarAmount * percentage / 100) as NSDecimalNumber)
return x.rounding(accordingToBehavior: pennyRoundingBehavior) as Decimal
}
enum ContributionMonth: Int, CaseIterable
{
case January, February, March, April, May, June,
July, August, September, October, November, December
static var count:Int { allCases.count }
}
struct MonthlyContributions
{
var roth : Decimal = 0
var traditional : Decimal = 0
var rothCompany : Decimal = 0
var traditionalCompany : Decimal = 0
var yearToDate : Decimal = 0
var yearToDateAll : Decimal = 0
var pers401a : Decimal = 0
}
extension MonthlyContributions
{
var total: Decimal { roth + traditional }
var companyTotal: Decimal { rothCompany + traditionalCompany }
func nextMonth(
combinedAmount: Decimal,
rothPercentage: Decimal,
rothPercentageCompany: Decimal,
combinedAmountCompany: Decimal
) -> Self
{
let roth = roundToNearestPenny(
percentage: rothPercentage,
of: combinedAmount
)
let rothCompany = roundToNearestPenny(
percentage: rothPercentageCompany,
of: combinedAmountCompany
)
return Self(
roth: roth,
traditional: combinedAmount - roth,
rothCompany: rothCompany,
traditionalCompany: combinedAmountCompany - rothCompany,
yearToDate: combinedAmount + yearToDate,
yearToDateAll: combinedAmount + pers401a + combinedAmountCompany + yearToDateAll,
/*
pers401a conditionals:
1. If the sum of the previous month's running total of personal and company contributions + this month's personal contributions is < personal401klimit ($22,500), then pers401a = 0, otherwise...
2. If the sum of the previous month's running total of personal and company contributions >= the combined401klimit ($66,500), then pers401a = 0, otherwise...
3. If the sum of the previous month's running total of personal and company contributions + this month's personal roth and traditional contributions < the combined401klimit ($66,500), then...
a. if ((this month's personal roth + traditional contributions) - (sum of all previous month's peresonal roth & traditional contributions)) = 0, then pers401a = 0, otherwise...
b. pers401a = (this month's personal roth + traditional contributions) - (sum of all previous month's peresonal roth & traditional contributions), otherwise...
4. pers401a = combined401klimit - the sum of the previous month's running total of personal and company contributions.
*/
pers401a:
yearToDateAll + total < personal401kLimit ? 0 :
yearToDateAll >= combinedContributionLimit ? 0 :
yearToDateAll + total < combinedContributionLimit ?
(total - yearToDate) == 0 ? 0 :
(total - yearToDate) :
combinedContributionLimit - yearToDateAll
)
}
}
extension Array where Element == MonthlyContributions {
subscript (index: ContributionMonth) -> Element { self[index.rawValue] }
}
func calculateAllCombinedContribution(
monthlyContribution monthly : Decimal,
annualContributionLimit limit : Decimal,
contributionsSoFarThisYear cummulative: Decimal) -> Decimal
{
min(max(0, limit - cummulative), monthly)
}
func computeMonthlyContributions(
monthlyPay : Decimal,
contributionPercentage : Decimal,
companyContributionPercentage : Decimal,
rothPercentage : Decimal,
contributionLimit : Decimal,
combinedContributionLimit : Decimal) -> [MonthlyContributions]
{
assert((0...100).contains(contributionPercentage))
assert((0...100).contains(rothPercentage))
assert(monthlyPay >= 0)
assert(contributionLimit >= 0)
var contribution = MonthlyContributions()
var monthlyContributions: [MonthlyContributions] = []
monthlyContributions.reserveCapacity(ContributionMonth.count)
let monthlyContribution = roundToNearestPenny(
percentage: contributionPercentage,
of: monthlyPay
)
let monthlyCompanyContribution = roundToNearestPenny(
percentage: companyContributionPercentage,
of: monthlyPay
)
for _ in ContributionMonth.allCases
{
let combinedPersAmount = calculateAllCombinedContribution(
monthlyContribution : monthlyContribution,
annualContributionLimit : contributionLimit,
contributionsSoFarThisYear: contribution.yearToDate
)
let pers401Amount = calculateAllCombinedContribution(
monthlyContribution : monthlyContribution,
annualContributionLimit : combinedContributionLimit,
//incorporate new yearToDate401a
contributionsSoFarThisYear : contribution.yearToDateAll
)
let combinedAllAmount = calculateAllCombinedContribution(
monthlyContribution : monthlyCompanyContribution,
annualContributionLimit : combinedContributionLimit,
contributionsSoFarThisYear : contribution.yearToDateAll
)
contribution = contribution.nextMonth(
combinedAmount: combinedPersAmount,
rothPercentage: rothPercentage,
rothPercentageCompany: rothPercentage,
combinedAmountCompany: combinedAllAmount
)
monthlyContributions.append(contribution)
}
return monthlyContributions
}
var monthlyPay : Decimal = 12758.0
var personal401kLimit : Decimal = 22500.0
var personal401kPercentage: Decimal = 100.0
var companyContributionPercentage: Decimal = 16.0
var roth401kPercentage : Decimal = 10.0
var combinedContributionLimit : Decimal = 66000.0
let monthlyContributions = computeMonthlyContributions(
monthlyPay : monthlyPay,
contributionPercentage: personal401kPercentage,
companyContributionPercentage : companyContributionPercentage,
rothPercentage : roth401kPercentage,
contributionLimit : personal401kLimit,
combinedContributionLimit : combinedContributionLimit
)
for month in ContributionMonth.allCases
{
let curContribution = monthlyContributions[month]
print("Contribution for \(month)")
print(" Traditional : $\(curContribution.traditional)")
print(" Roth : $\(curContribution.roth)")
print(" Pers 401a : $\(curContribution.pers401a)")
// print(" Pers Year-To-Date: $\(curContribution.yearToDate)")
print(" Company Trad: $\(curContribution.traditionalCompany)")
print(" Company Roth: $\(curContribution.rothCompany)")
print(" Combined Year-To-Date: $\(curContribution.yearToDateAll)")
print()
}
The output:
Contribution for January
Traditional : $11482.2
Roth : $1275.8
Pers 401a : $0
Company Trad: $1837.15
Company Roth: $204.13
Combined Year-To-Date: $14799.28
Contribution for February
Traditional : $8767.8
Roth : $974.2
Pers 401a : $0
Company Trad: $1837.15
Company Roth: $204.13
Combined Year-To-Date: $26582.56
Contribution for March
Traditional : $0
Roth : $0
Pers 401a : $-12758
Company Trad: $1837.15
Company Roth: $204.13
Combined Year-To-Date: $28623.84
Contribution for April
Traditional : $0
Roth : $0
Pers 401a : $-22500
Company Trad: $1837.15
Company Roth: $204.13
Combined Year-To-Date: $17907.12
Contribution for May
Traditional : $0
Roth : $0
Pers 401a : $0
Company Trad: $1837.15
Company Roth: $204.13
Combined Year-To-Date: $-2551.6