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

SOLVED: 401k Calculator: Unable to get "back-door-Roth" function to work properly. Seeking techniques to improve formulae

Forums > Swift

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

2      

Found a solution on StackOverflow. See the full thread here: link

2      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

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.