UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: How to compute end of year totals for 401k monthly contributions?

Forums > Swift

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.

2      

In the supplied code you never actually assign a value to it (sumOfPersRoth).

You have the variable declaration, and the print statement, but that is it.

Do you set sumOfPersRoth to a value elsewhere in your code?

2      

I adjusted the following

 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
        )

2      

This solved the issue:


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
    var sumOfPersTrad       : Decimal = 0
    var sumOfCompRoth       : Decimal = 0
    var sumOfCompTrad       : Decimal = 0
    var sumOf401a           : Decimal = 0
    var sumOf401kExcess     : Decimal = 0
    var sumOfTakeHome       : Decimal = 0
}

....

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,
            sumOfPersTrad     : sumOfPersTrad + personalTraditional,
            sumOfCompRoth: sumOfCompRoth + companyRoth,
            sumOfCompTrad: sumOfCompTrad + companyTraditional,
            sumOf401a: sumOf401a + personal401aContribution,
            sumOf401kExcess: sumOf401kExcess + excessCompany401k,
            sumOfTakeHome: sumOfTakeHome + takeHome
        )

2      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.