Purpose of project: allow an employee to calculate/project future 401k yearly balances based on several future promotions. With promotions come pay raises.
One assumption is that 401k monthly deposits are an unchanging percentage of income.
I'm currenty working within a macOS Command Line Tool for debugging purposes. Eventually I will move this into an iOS app project.
The entire code is found on GitHub here.
However I was able to fit it all here below. Here is the first section:
enum Calculations {
private static let pennyRoundingBehavior = NSDecimalNumberHandler(
roundingMode: .bankers,
scale: 2,
raiseOnExactness: false,
raiseOnOverflow: true,
raiseOnUnderflow: true,
raiseOnDivideByZero: true
)
static func computeMonthlyEarnings(hours hoursWorked: Int, atPayRate payRate: Int) -> Decimal {
let base = Decimal(hoursWorked * payRate) as NSDecimalNumber
return base.rounding(accordingToBehavior: pennyRoundingBehavior) as Decimal
}
static func computeMonthlySavings(hours hoursWorked: Int, atPayRate payRate: Int, percentSaved percent: Double) -> Decimal {
let base = Decimal(hoursWorked * payRate) * Decimal(percent / 100.0) as NSDecimalNumber
return base.rounding(accordingToBehavior: pennyRoundingBehavior) as Decimal
}
static func computeMonthlyRoth(hours hoursWorked: Int, atPayRate payRate: Int, percentToRoth percent: Double) -> Decimal {
let base = Decimal(hoursWorked * payRate) * Decimal(percent / 100.0) as NSDecimalNumber
return base.rounding(accordingToBehavior: pennyRoundingBehavior) as Decimal
}
}
extension Array {
func chunked(into size: Int) -> [[Element]] {
return stride(from: 0, to: count, by: size).map {
Array(self[$0 ..< Swift.min($0 + size, count)])
}
}
}
enum Department: RawRepresentable {
case sales
case management
case seniorManagement
case other(String)
init?(rawValue: String) {
switch rawValue {
case "Sales": self = .sales
case "Management": self = .management
case "Senior Management": self = .seniorManagement
case let dept: self = .other(dept)
}
}
var rawValue: String {
switch self {
case .sales: return "Sales"
case .management: return "Management"
case .seniorManagement: return "Senior Management"
case .other(let dept): return dept
}
}
func payRate(forYearsWorked numberOfYears: Int) -> Int {
let rates: [Int]
switch self {
case .sales: rates = [20,22,24,26,28,30,32,34,36,38,40]
case .management: rates = [30,32,34,36,38,40,42,44,46,48,50]
case .seniorManagement: rates = [40,42,44,46,48,50,52,54,56,58,60]
default: rates = [10,12,14,16,18,20,22,24,26,28,30]
}
return rates[min(numberOfYears, rates.count) - 1]
}
}
struct Employee {
let name: String
var birthdate: Date
var hiredate: Date
var retirementAge: Int
var department: Department
var monthlyHoursWorked: Int
var percentToSave: Double
var existing401kbalance: Decimal
var percentToRoth: Double
var percentToTrad: Double
private(set) var employmentHistory: [Department]
init(_ name: String,
birthdate: Date,
hiredate: Date,
retirementAge: Int,
department: Department = .other("Employee"),
monthlyHoursWorked: Int,
percentToSave: Double,
existing401kbalance: Decimal,
percentToRoth: Double,
percentToTrad: Double)
{
self.name = name
self.birthdate = birthdate
self.hiredate = hiredate
self.retirementAge = retirementAge
self.department = department
self.monthlyHoursWorked = monthlyHoursWorked
self.percentToSave = percentToSave
self.existing401kbalance = existing401kbalance
self.percentToRoth = percentToRoth
self.percentToTrad = percentToTrad
self.employmentHistory = []
}
}
extension Employee {
mutating func promote(to newDepartment: Department) {
department = newDepartment
}
mutating func work(months: Int) {
employmentHistory
.append(contentsOf: Array.init(repeating: department,
count: months))
}
mutating func work(years: Int) {
work(months: years * 12)
}
mutating func work(years: Int, months: Int) {
work(months: years * 12 + months)
}
func yearlyEarnings() -> [Decimal] {
let years = employmentHistory.chunked(into: 12)
var earnings: [Decimal] = []
for (index, months) in years.enumerated() {
let yearEarnings: Decimal = months.reduce(0) { runningTotal, dept in
let payRate = dept.payRate(forYearsWorked: index + 1)
let monthlyEarnings = Calculations.computeMonthlyEarnings(
hours: monthlyHoursWorked,
atPayRate: payRate
)
return runningTotal + monthlyEarnings
}
earnings.append(yearEarnings)
}
return earnings
}
func yearlySavings() -> [Decimal] {
let years = employmentHistory.chunked(into: 12)
var savings: [Decimal] = []
for (index, months) in years.enumerated() {
let yearSavings: Decimal = months.reduce(0) { runningTotal, dept in
let payRate = dept.payRate(forYearsWorked: index + 1)
let monthlySavings = Calculations.computeMonthlySavings(
hours: monthlyHoursWorked,
atPayRate: payRate,
percentSaved: percentToSave
)
return runningTotal + monthlySavings
}
savings.append(yearSavings)
}
return savings
}
func calc401k(principal: Decimal, deposits: Decimal, rateOfReturn: Decimal) -> Decimal {
//Compound interest for principal
// P(1 + r/n)^(nt)
//Future value of a series
// PMT x {[(1 + r/n)^(nt) - 1] / (r/n)}
//Key:
// PMT = monthly payment amount
// r = annual interest rate
// n = numebr of times interest is compounded per year
// t = time in years
// ^ = ... to the power of...
//Start with Conpound interest for principal
// first segment: (1 + r/n)
// n = compounded 12 times a year
let principalBase = 1 + ((rateOfReturn/100)/12)
// second segment: ^(nt)
// n = compounded 12 times a year
// t = time is 1 year
let principalPower = pow(principalBase,12)
// third segment: times P
let principalInterest = principal * principalPower
//Now Future value of the series
//first segment: (1 + r/n)
let rateFrequencyCompounded = 1+((rateOfReturn/100)/12)
//second segment:
// ^(nt) - 1
// n = compounded 12 times a year
// t = time is 1 year
let toThePower = pow(rateFrequencyCompounded,12) - 1
//third segment:
// divided: / (r/n)
// n = compounded 12 times a year
let divided = toThePower / ((rateOfReturn/100)/12)
//fourth segment:
// mlutiplied by monthly deposit PMT
let futureInterest = divided * deposits
return futureInterest + principalInterest
}
Here is the section of code that I need to perfect. I currently have three errors:
func depositsToRoth() -> [Decimal] {
let years = employmentHistory.chunked(into: 12)
var toRoth: [Decimal] = []
var rothBalances: [Decimal] = []
for (index, months) in years.enumerated() {
let yearToRoth: Decimal = months.reduce(0) { runningTotal, dept in
let payRate = dept.payRate(forYearsWorked: index + 1)
let monthlyToRoth = Calculations.computeMonthlyRoth(
hours: monthlyHoursWorked,
atPayRate: payRate,
percentToRoth: percentToRoth
)
return runningTotal + monthlyToRoth
}
toRoth.append(yearToRoth)
//chunk the toRoth array into 12 month groups and then reduce each to an average
let yearsAverageOfMonthlyRothDepositsRaw = toRoth.chunked(into: 12)
let yearsAverageOfMonthlyRothDeposits = yearsAverageOfMonthlyRothDepositsRaw.reduce(0, +) / yearsAverageOfMonthlyRothDepositsRaw.count //<- Error #1
for idx in yearsAverageOfMonthlyRothDeposits.indices {
let principal = idx == 0
? existing401kbalance
: yearsAverageOfMonthlyRothDeposits[idx - 1]
let balance401k = calc401k(principal: principal,
deposits: yearsAverageOfMonthlyRothDeposits[idx],
rateOfReturn: 10
)
return balance401k //<- Error #2
}
rothBalances.append(balance401k)
}
return toRoth; balance401k //<- Error #3
}
The rest of the code...
extension Employee {
func cumulativeSavingsSinceFirstHired() -> [Decimal] {
//get our savings amount for each year of employment
let savingsByYear = yearlySavings()
//create an array to hold the cumulative savings for each year
//we initialize each slot as 0
//this saves us having to do a bunch of appends
var cumulativeSavings: [Decimal] = .init(repeating: .zero, count: savingsByYear.count)
//now loop through the yearly savings
for idx in savingsByYear.indices {
//and use reduce on a slice of the array from the startIndex (i.e., 0)
// to the current idx
//so 0...0, then 0...1, then 0...2, etc
cumulativeSavings[idx] = savingsByYear[0...idx].reduce(0) { accum, yrAmount in
//simply add the cumulative total and the amount for the year
accum + yrAmount
}
}
//send it back out
return cumulativeSavings
}
}
extension Employee {
func yearlyDepartments() -> [String] {
let years = employmentHistory.chunked(into: 12)
let departments: [String] = years.map { months in
var seenDepts: Set<String> = []
return months.reduce(into: [String]()) { acc, dept in
if seenDepts.insert(dept.rawValue).inserted {
acc.append(dept.rawValue)
}
}.joined(separator: ",") //concat the final result with , separator
}
return departments
}
func yearlyDepartmentsEarningsAndSavings() -> [(String, (Decimal, Decimal))] {
let yearlyDepartments = yearlyDepartments()
let yearlyEarnings = yearlyEarnings()
let yearlySavings = yearlySavings()
let yearlyDepartmentsEarningsAndSavings: [(departments: String, (earnings: Decimal, savings: Decimal))] =
Array(zip(yearlyDepartments, zip(yearlyEarnings, yearlySavings)))
return yearlyDepartmentsEarningsAndSavings
}
}
var birthdate = DateComponents(calendar: .current, year: 1965, month: 7, day: 20).date!
var hiredate = DateComponents(calendar: .current, year: 2021, month: 12, day: 28).date!
var promotion1date = DateComponents(calendar: .current, year: 2023, month: 12, day: 28).date!
var firstDeptMonths: Int { Calendar.current.dateComponents([.month], from: hiredate, to: promotion1date).month! }
var promotion2date = DateComponents(calendar: .current, year: 2025, month: 6, day: 28).date!
var secondDeptMonths: Int { Calendar.current.dateComponents([.month], from: promotion1date, to: promotion2date).month! }
var promotion3date = DateComponents(calendar: .current, year: 2026, month: 12, day: 28).date!
var thirdDeptMonths: Int { Calendar.current.dateComponents([.month], from: promotion2date, to: promotion3date).month! }
var retireAge = 65
let retireDate = Calendar.current.date(byAdding: .year, value: retireAge, to: birthdate)!
var fourthDeptMonths: Int { Calendar.current.dateComponents([.month], from: promotion3date, to: retireDate).month! }
extension Date {
var age: Int { Calendar.current.dateComponents([.year], from: self, to: Date()).year! }
var yearGroup: Int { Calendar.current.dateComponents([.year], from: self, to: Date()).year! + 1}
var totalYearsRemaining: Int { Calendar.current.dateComponents([.year], from: self, to: retireDate).year! }
var totalMonthsRemaining: Int { Calendar.current.dateComponents([.month], from: self, to: retireDate).month! }
}
//age
let age = birthdate.age
//number of years employee has worked for the company
let yearGroup = hiredate.yearGroup
func printYearlyDepartmentsAndEarnings(_ yearlyDeptsAndEarnings: [(departments: String, (earnings: Decimal, savings: Decimal))]) {
for (year, deptsAndEarnings) in yearlyDeptsAndEarnings.enumerated() {
print("Department(s) for Year \(year + yearGroup + 1): \(deptsAndEarnings.departments)")
print("Earnings for Year \(year + yearGroup + 1): \(deptsAndEarnings.1.earnings)")
print("Savings for Year \(year + yearGroup + 1): \(deptsAndEarnings.1.savings)")
print("Age: \(year + age)")
print("")
}
}
func printSavingsBalance(_ savingsBalance: [(Decimal)]) {
for (year, save) in savingsBalance.enumerated() {
print("Savings Balance Year \(year + yearGroup + 1): \(save)")
}
}
//create a new employee
var charlotte = Employee("Charlotte Grote", birthdate: birthdate, hiredate: hiredate, retirementAge: 65, department: .other("Hospitality"), monthlyHoursWorked: 160, percentToSave: 10.0, existing401kbalance: 1000, percentToRoth: 10.0, percentToTrad: 16.0)
//give our employee some work history
//months in Hospitality
charlotte.work(months: firstDeptMonths)
//ohh, a promotion!
charlotte.promote(to: .sales)
//months in Sales
charlotte.work(months: secondDeptMonths)
//another promotion. way to go, Charlotte!
charlotte.promote(to: .management)
//months in Management
charlotte.work(months: thirdDeptMonths)
//another promotion. way to go, Charlotte!
charlotte.promote(to: .seniorManagement)
//months in Senior Management
charlotte.work(months: fourthDeptMonths)
//how much money has Charlotte made after all this time?
printYearlyDepartmentsAndEarnings(charlotte.yearlyDepartmentsEarningsAndSavings())
printSavingsBalance(charlotte.cumulativeSavingsSinceFirstHired())
Error 1: Cannot convert value of type '(Int) -> Int' to expected argument type '(Int, [Decimal]) throws -> Int'
Error 2 & 3: Cannot find 'balance401k' in scope
Error 1 leads me to believe that using .reduce
in this way is limited to Int parameters. Is that correct?
I want to be able to later print the yearly sum totals of each year's roth deposits. I think that will be possible by referencing the yearsAverageOfMonthlyRothDeposits
array, but I need to clear all these errors first to find out.
I also want to be able to later print each year's 401k balances.