I currently working within Playground and macOS Command Line Tool project and am able to successfully print month-by-month contributions to a 401k account, both personal contributions and company direct contributions (not matching) dividing each between roth and traditional in accordance with user preferences. I am also able to provide COMBINED running totals (i.e. March's personal combined roth and traditional contribution total is the sum of January, February and March contributions).
The problem I am currently struggling with is computing December totals for INDIVIDUAL groups. I need to compute the Jan-through-Dec totals for each of the following SEPARATELY: personal roth, personal traditional, personal 401a, company roth, company traditional, monthly take home, and 401k excess.
There is some extraneous content within the code below as a result of my unsuccessful attempts.
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 yearToDate401a : Decimal = 0
var yearToDateRoth : Decimal = 0
var yearToDateAll : Decimal = 0
var pers401a : Decimal = 0
var excessCompany401k : Decimal = 0
var sumOfPersRoth : Decimal = 0
}
struct ContributionSplitInfo
{
// These are static because they are shared among all employees.
static let personal401kLimit : Decimal = 22500.0
static let combined401kAnnualLimit : Decimal = 66000.0
static let companyDirContPercentage : Decimal = 16.0
/*
These are provided as convenience, and because if for some reason they
need to be individualized for each employee, that can be done without
affecting the code that uses it. For example, tax law could change limits,
but existing employees might be grandfathered with the old limits.
Similarly the company might change its matching rate for new hires, but
not for new hires, or might have increasing matching with years of service.
*/
var personal401kAnnualLimit : Decimal { Self.personal401kLimit }
var combined401kAnnualLimit : Decimal { Self.combined401kAnnualLimit }
var companyDirContPercentage : Decimal { Self.companyDirContPercentage }
// These are stored instance properties because each employee can set them
// differently
var personal401kPercentage : Decimal
var rothPercentage : Decimal
}
extension MonthlyContributions
{
var total: Decimal { roth + traditional }
var companyTotal: Decimal { rothCompany + traditionalCompany }
var takeHome: Decimal { monthlyPay - (roth + traditional + pers401a)}
var allPersContributions: Decimal { roth + traditional + pers401a}
func nextMonth(pay: Decimal, using splitInfo: ContributionSplitInfo) -> Self
{
assert((0...100).contains(splitInfo.personal401kPercentage))
assert((0...100).contains(splitInfo.companyDirContPercentage))
assert((0...100).contains(splitInfo.rothPercentage))
assert(splitInfo.personal401kAnnualLimit >= 0)
assert(splitInfo.combined401kAnnualLimit >= 0)
assert(pay >= 0)
let (personal401kContribution, prelimit401aContribution) =
self.personal401kContribution(fromPay: pay, using: splitInfo)
var combinedYearToDateContributions = yearToDateAll
let personal401aContribution = clampContribution(
prelimit401aContribution,
whenCumulativeValue: combinedYearToDateContributions,
exceeds: splitInfo.combined401kAnnualLimit
)
combinedYearToDateContributions += personal401aContribution
let company401kContribution = self.company401kContribution(
fromPersonalContribution: personal401kContribution,
andCombinedYearToDateContributions: &combinedYearToDateContributions,
using: splitInfo
)
let (personalTraditional, personalRoth) = split401kContribution(
contribution: personal401kContribution,
rothPercentage: splitInfo.rothPercentage
)
let (companyTraditional, companyRoth) = split401kContribution(
contribution: company401kContribution,
rothPercentage: splitInfo.rothPercentage
)
let excessCompany401k = excess401k(companyContribution: company401kContribution)
let contributions = Self(
roth : personalRoth,
traditional : personalTraditional,
rothCompany : companyRoth,
traditionalCompany: companyTraditional,
yearToDate : yearToDate + personal401kContribution,
yearToDate401a : yearToDate + personal401kContribution,
yearToDateRoth : yearToDate + personalRoth,
yearToDateAll : combinedYearToDateContributions,
pers401a : personal401aContribution,
excessCompany401k : excessCompany401k,
sumOfPersRoth : sumOfPersRoth + personalRoth //< added this
)
assert(contributions.yearToDate <= splitInfo.personal401kAnnualLimit)
assert(contributions.yearToDateAll <= splitInfo.combined401kAnnualLimit)
return contributions
}
private func sumOfContribution(
contribution1: Decimal, contributionType: Decimal) -> Decimal
{
let endOfYearTotal = contribution1 + yearToDate
return endOfYearTotal
}
private func split401kContribution(
contribution: Decimal,
rothPercentage: Decimal) -> (traditional: Decimal, roth: Decimal)
{
let roth = roundToNearestPenny(
percentage: rothPercentage,
of: contribution
)
return (contribution - roth, roth)
}
private func personal401kContribution(
fromPay pay: Decimal,
using splitInfo: ContributionSplitInfo)
-> (contribution: Decimal, unused: Decimal)
{
let prelimitContribution = roundToNearestPenny(
percentage: splitInfo.personal401kPercentage,
of: pay
)
let actualContribution = clampContribution(
prelimitContribution,
whenCumulativeValue: yearToDate,
exceeds: splitInfo.personal401kAnnualLimit
)
return (actualContribution, prelimitContribution - actualContribution)
}
private func company401kContribution(
fromPersonalContribution personalContribution: Decimal,
andCombinedYearToDateContributions yearToDate: inout Decimal,
using splitInfo: ContributionSplitInfo) -> Decimal
{
yearToDate += personalContribution
let companyCombinedContribution = roundToNearestPenny(
percentage: splitInfo.companyDirContPercentage,
of: monthlyPay
)
let companyContribution = clampContribution(
companyCombinedContribution,
whenCumulativeValue: yearToDate,
exceeds: splitInfo.combined401kAnnualLimit
)
yearToDate += companyContribution
return companyContribution
}
private func excess401k(
companyContribution: Decimal) -> Decimal
{
let excess401k = companyContribution == 0 ? roundToNearestPenny(percentage: splitInfo.companyDirContPercentage, of: monthlyPay) : 0
return excess401k
}
}
extension Array where Element == MonthlyContributions {
subscript (index: ContributionMonth) -> Element { self[index.rawValue] }
}
func clampContribution(
_ current : Decimal,
whenCumulativeValue cummulative: Decimal,
exceeds limit : Decimal) -> Decimal
{
min(max(0, limit - cummulative), current)
}
func computeMonthlyContributions(
monthlyPay : Decimal,
using splitInfo : ContributionSplitInfo) -> [MonthlyContributions]
{
var contribution = MonthlyContributions()
var monthlyContributions: [MonthlyContributions] = []
monthlyContributions.reserveCapacity(ContributionMonth.count)
for _ in ContributionMonth.allCases
{
contribution = contribution.nextMonth(pay: monthlyPay, using: splitInfo)
monthlyContributions.append(contribution)
}
return monthlyContributions
}
var monthlyPay: Decimal = 12758.0
let splitInfo =
ContributionSplitInfo(personal401kPercentage: 100.0, rothPercentage: 10.0)
let monthlyContributions =
computeMonthlyContributions(monthlyPay: monthlyPay, using: splitInfo)
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(" Company Trad : $\(curContribution.traditionalCompany)")
print(" Company Roth : $\(curContribution.rothCompany)")
print(" Pers Year-To-Date : $\(curContribution.yearToDate)")
print(" Combined Year-To-Date: $\(curContribution.yearToDateAll)")
print(" Company 401k Excess : $\(curContribution.excessCompany401k)")
print(" Take Home Pay : $\(curContribution.takeHome)")
print(" Total Pers Roth : $\(curContribution.sumOfPersRoth)")
print()
}
Here is what prints out:
Contribution for January
Traditional : $11482.2
Roth : $1275.8
Pers 401a : $0
Company Trad : $1837.15
Company Roth : $204.13
Pers Year-To-Date : $12758
Combined Year-To-Date: $14799.28
Company 401k Excess : $0
Take Home Pay : $0
Total Pers Roth : $0
Contribution for February
Traditional : $8767.8
Roth : $974.2
Pers 401a : $3016
Company Trad : $1837.15
Company Roth : $204.13
Pers Year-To-Date : $22500
Combined Year-To-Date: $29598.56
Company 401k Excess : $0
Take Home Pay : $0
Total Pers Roth : $0
Contribution for March
Traditional : $0
Roth : $0
Pers 401a : $12758
Company Trad : $1837.15
Company Roth : $204.13
Pers Year-To-Date : $22500
Combined Year-To-Date: $44397.84
Company 401k Excess : $0
Take Home Pay : $0
Total Pers Roth : $0
Contribution for April
Traditional : $0
Roth : $0
Pers 401a : $12758
Company Trad : $1837.15
Company Roth : $204.13
Pers Year-To-Date : $22500
Combined Year-To-Date: $59197.12
Company 401k Excess : $0
Take Home Pay : $0
Total Pers Roth : $0
Contribution for May
Traditional : $0
Roth : $0
Pers 401a : $6802.88
Company Trad : $0
Company Roth : $0
Pers Year-To-Date : $22500
Combined Year-To-Date: $66000
Company 401k Excess : $2041.28
Take Home Pay : $5955.12
Total Pers Roth : $0
Contribution for June
Traditional : $0
Roth : $0
Pers 401a : $0
Company Trad : $0
Company Roth : $0
Pers Year-To-Date : $22500
Combined Year-To-Date: $66000
Company 401k Excess : $2041.28
Take Home Pay : $12758
Total Pers Roth : $0
Contribution for July
Traditional : $0
Roth : $0
Pers 401a : $0
Company Trad : $0
Company Roth : $0
Pers Year-To-Date : $22500
Combined Year-To-Date: $66000
Company 401k Excess : $2041.28
Take Home Pay : $12758
Total Pers Roth : $0
Contribution for August
Traditional : $0
Roth : $0
Pers 401a : $0
Company Trad : $0
Company Roth : $0
Pers Year-To-Date : $22500
Combined Year-To-Date: $66000
Company 401k Excess : $2041.28
Take Home Pay : $12758
Total Pers Roth : $0
Contribution for September
Traditional : $0
Roth : $0
Pers 401a : $0
Company Trad : $0
Company Roth : $0
Pers Year-To-Date : $22500
Combined Year-To-Date: $66000
Company 401k Excess : $2041.28
Take Home Pay : $12758
Total Pers Roth : $0
Contribution for October
Traditional : $0
Roth : $0
Pers 401a : $0
Company Trad : $0
Company Roth : $0
Pers Year-To-Date : $22500
Combined Year-To-Date: $66000
Company 401k Excess : $2041.28
Take Home Pay : $12758
Total Pers Roth : $0
Contribution for November
Traditional : $0
Roth : $0
Pers 401a : $0
Company Trad : $0
Company Roth : $0
Pers Year-To-Date : $22500
Combined Year-To-Date: $66000
Company 401k Excess : $2041.28
Take Home Pay : $12758
Total Pers Roth : $0
Contribution for December
Traditional : $0
Roth : $0
Pers 401a : $0
Company Trad : $0
Company Roth : $0
Pers Year-To-Date : $22500
Combined Year-To-Date: $66000
Company 401k Excess : $2041.28
Take Home Pay : $12758
Total Pers Roth : $0
Program ended with exit code: 0
The print is accurate with exception of "Total Pers Roth."
Given the above monthly pay, personal401kPercentage and rothPercentage, the total sum of personal roth should be $2,250. The total sum of personal traditional contributions should be $20,250. The total for company roth: $816. Total for company traditional: $7348. Total for 401a: $35,335. Total for monthly take-home pay: $95,256. Total 401k excess: $16,330.