My project involves creating a table of data with a row for each of the 12 months of the year. The user of the app will define their monthly pay and percentage of pay they want to contribute to his/her 401k. The table shows in which month the user has reached the IRS contribution limit ($22,500) and consequently cannot contribute any more until next year.
I'm using Xcode playground to test my formulae. Since each month from February through December must look back at contributions and sums from the previous months, there is a considerable amount of looping which drives excessive execution time. I have a computed variable titled "personal401kContributionMonthly" which computes what the user has defined for their desired percent of monthly pay to contribute to their 401k. This computed property ran 4,940,531 times before the playground crashed with the following error: "error: Execution was interrupted, reason: signal SIGKILL. [12211:365921] Unable to quarantine process. (Error: -1.)"
I am seeking for advice/techniques to simplify the code to avoid the crash.
var monthlyPay = 15203.0
var personal401kLimit = 22500.0
var personal401kPercentage = 10.0
var roth401kPercentage = 10.0
var personal401kContributionMonthly: Double {
monthlyPay * (personal401kPercentage/100)
}
personal401kContributionMonthly
var persTradContJan: Double {
personal401kContributionMonthly <= personal401kLimit ? (personal401kContributionMonthly * (1 - (roth401kPercentage/100))) : personal401kLimit * (1 - (roth401kPercentage/100))
}
persTradContJan
var persRothContJan: Double {
roth401kPercentage == 0.0 ? 0.0 : personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (roth401kPercentage/100) : personal401kLimit * (roth401kPercentage/100)
}
persRothContJan
var pers401ksumFeb: Double {
persTradContJan persRothContJan
}
var persTradContFeb: Double {
pers401ksumFeb personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (1 - (roth401kPercentage/100)) : pers401ksumFeb < personal401kLimit ? (1 - (roth401kPercentage/100)) * (personal401kLimit - (pers401ksumFeb)) : 0.0
}
persTradContFeb
var persRothContFeb: Double {
roth401kPercentage == 0.0 ? 0.0 : pers401ksumFeb personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (roth401kPercentage/100) : pers401ksumFeb < personal401kLimit ? (roth401kPercentage/100) * (personal401kLimit - pers401ksumFeb) : 0.0
}
persRothContFeb
var pers401ksumMar: Double {
persTradContJan persRothContJan persTradContFeb persRothContFeb
}
var persTradContMar: Double {
pers401ksumMar personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (1 - (roth401kPercentage/100)) : pers401ksumMar < personal401kLimit ? (1 - (roth401kPercentage/100)) * (personal401kLimit - (pers401ksumMar)) : 0.0
}
persTradContMar
var persRothContMar: Double {
roth401kPercentage == 0.0 ? 0.0 : pers401ksumMar personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (roth401kPercentage/100) : pers401ksumMar < personal401kLimit ? (roth401kPercentage/100) * (personal401kLimit - pers401ksumMar) : 0.0
}
persRothContMar
var pers401ksumApr: Double {
persTradContJan persRothContJan persTradContFeb persRothContFeb persTradContMar persRothContMar
}
var persTradContApr: Double {
pers401ksumApr personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (1 - (roth401kPercentage/100)) : pers401ksumApr < personal401kLimit ? (1 - (roth401kPercentage/100)) * (personal401kLimit - (pers401ksumApr)) : 0.0
}
persTradContApr
var persRothContApr: Double {
roth401kPercentage == 0.0 ? 0.0 : pers401ksumApr personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (roth401kPercentage/100) : pers401ksumApr < personal401kLimit ? (roth401kPercentage/100) * (personal401kLimit - pers401ksumApr) : 0.0
}
persRothContApr
var pers401ksumMay: Double {
persTradContJan persRothContJan persTradContFeb persRothContFeb persTradContMar persRothContMar persTradContApr persRothContApr
}
var persTradContMay: Double {
pers401ksumMay personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (1 - (roth401kPercentage/100)) : pers401ksumMay < personal401kLimit ? (1 - (roth401kPercentage/100)) * (personal401kLimit - (pers401ksumMay)) : 0.0
}
persTradContMay
var persRothContMay: Double {
roth401kPercentage == 0.0 ? 0.0 : pers401ksumMay personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (roth401kPercentage/100) : pers401ksumMay < personal401kLimit ? (roth401kPercentage/100) * (personal401kLimit - pers401ksumMay) : 0.0
}
persRothContMay
var pers401ksumJun: Double {
persTradContJan persRothContJan persTradContFeb persRothContFeb persTradContMar persRothContMar persTradContApr persRothContApr persTradContMay persRothContMay
}
var persTradContJun: Double {
pers401ksumJun personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (1 - (roth401kPercentage/100)) : pers401ksumJun < personal401kLimit ? (1 - (roth401kPercentage/100)) * (personal401kLimit - (pers401ksumJun)) : 0.0
}
persTradContJun
var persRothContJun: Double {
roth401kPercentage == 0.0 ? 0.0 : pers401ksumJun personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (roth401kPercentage/100) : pers401ksumJun < personal401kLimit ? (roth401kPercentage/100) * (personal401kLimit - pers401ksumJun) : 0.0
}
persRothContJun
var pers401ksumJul: Double {
persTradContJan persRothContJan persTradContFeb persRothContFeb persTradContMar persRothContMar persTradContApr persRothContApr persTradContMay persRothContMay persTradContJun persRothContJun
}
var persTradContJul: Double {
pers401ksumJul personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (1 - (roth401kPercentage/100)) : pers401ksumJul < personal401kLimit ? (1 - (roth401kPercentage/100)) * (personal401kLimit - (pers401ksumJul)) : 0.0
}
persTradContJul
var persRothContJul: Double {
roth401kPercentage == 0.0 ? 0.0 : pers401ksumJul personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (roth401kPercentage/100) : pers401ksumJul < personal401kLimit ? (roth401kPercentage/100) * (personal401kLimit - pers401ksumJul) : 0.0
}
persRothContJul
var pers401ksumAug: Double {
persTradContJan persRothContJan persTradContFeb persRothContFeb persTradContMar persRothContMar persTradContApr persRothContApr persTradContMay persRothContMay persTradContJun persRothContJun persTradContJul persRothContJul
}
var persTradContAug: Double {
pers401ksumAug personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (1 - (roth401kPercentage/100)) : pers401ksumAug < personal401kLimit ? (1 - (roth401kPercentage/100)) * (personal401kLimit - (pers401ksumAug)) : 0.0
}
persTradContAug
var persRothContAug: Double {
roth401kPercentage == 0.0 ? 0.0 : pers401ksumAug personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (roth401kPercentage/100) : pers401ksumAug < personal401kLimit ? (roth401kPercentage/100) * (personal401kLimit - pers401ksumAug) : 0.0
}
persRothContAug
var pers401ksumSep: Double {
persTradContJan persRothContJan persTradContFeb persRothContFeb persTradContMar persRothContMar persTradContApr persRothContApr persTradContMay persRothContMay persTradContJun persRothContJun persTradContJul persRothContJul persTradContAug persRothContAug
}
var persTradContSep: Double {
pers401ksumSep personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (1 - (roth401kPercentage/100)) : pers401ksumSep < personal401kLimit ? (1 - (roth401kPercentage/100)) * (personal401kLimit - (pers401ksumSep)) : 0.0
}
persTradContSep
var persRothContSep: Double {
roth401kPercentage == 0.0 ? 0.0 : pers401ksumSep personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (roth401kPercentage/100) : pers401ksumSep < personal401kLimit ? (roth401kPercentage/100) * (personal401kLimit - pers401ksumSep) : 0.0
}
persRothContSep
var pers401ksumOct: Double {
persTradContJan persRothContJan persTradContFeb persRothContFeb persTradContMar persRothContMar persTradContApr persRothContApr persTradContMay persRothContMay persTradContJun persRothContJun persTradContJul persRothContJul persTradContAug persRothContAug persTradContSep persRothContSep
}
var persTradContOct: Double {
pers401ksumOct personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (1 - (roth401kPercentage/100)) : pers401ksumOct < personal401kLimit ? (1 - (roth401kPercentage/100)) * (personal401kLimit - (pers401ksumOct)) : 0.0
}
persTradContOct
var persRothContOct: Double {
roth401kPercentage == 0.0 ? 0.0 : pers401ksumOct personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (roth401kPercentage/100) : pers401ksumOct < personal401kLimit ? (roth401kPercentage/100) * (personal401kLimit - pers401ksumOct) : 0.0
}
persRothContOct
var pers401ksumNov: Double {
persTradContJan persRothContJan persTradContFeb persRothContFeb persTradContMar persRothContMar persTradContApr persRothContApr persTradContMay persRothContMay persTradContJun persRothContJun persTradContJul persRothContJul persTradContAug persRothContAug persTradContSep persRothContSep persTradContOct persRothContOct
}
var persTradContNov: Double {
pers401ksumNov personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (1 - (roth401kPercentage/100)) : pers401ksumNov < personal401kLimit ? (1 - (roth401kPercentage/100)) * (personal401kLimit - (pers401ksumNov)) : 0.0
}
persTradContNov
var persRothContNov: Double {
roth401kPercentage == 0.0 ? 0.0 : pers401ksumNov personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (roth401kPercentage/100) : pers401ksumNov < personal401kLimit ? (roth401kPercentage/100) * (personal401kLimit - pers401ksumNov) : 0.0
}
persRothContNov
var pers401ksumDec: Double {
persTradContJan persRothContJan persTradContFeb persRothContFeb persTradContMar persRothContMar persTradContApr persRothContApr persTradContMay persRothContMay persTradContJun persRothContJun persTradContJul persRothContJul persTradContAug persRothContAug persTradContSep persRothContSep persTradContOct persRothContOct persTradContNov persRothContNov
}
var persTradContDec: Double {
pers401ksumDec personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (1 - (roth401kPercentage/100)) : pers401ksumDec < personal401kLimit ? (1 - (roth401kPercentage/100)) * (personal401kLimit - (pers401ksumDec)) : 0.0
}
persTradContDec
var persRothContDec: Double {
roth401kPercentage == 0.0 ? 0.0 : pers401ksumDec personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (roth401kPercentage/100) : pers401ksumDec < personal401kLimit ? (roth401kPercentage/100) * (personal401kLimit - pers401ksumDec) : 0.0
}
persRothContDec
CodePudding user response:
Your playground crashes because all those computed variables reference other computed variables in ways that explode the call tree exponentially, so as @matt, with tongue in cheek, pointed out in comments, it's a literal stack overflow. Computed variables are just syntactic sugar for function calls. Were you intending lazy assignment instead?
In any case, you asked about refactoring the code.
I think your example is the kind of thing that wants to be in a data structure where you index by the month rather than having individually named computed variables. As is often the case, it can be accomplished multiple ways. What follows is one way. I'll break into part.
I'm going to assume that your playground is intended as a sort of prototyping environment for some code that you want to use in a real app. If it's just a learning exercise, using Double
to represent money is fine, but binary floating point types can't exactly represent all decimal numbers. See here for examples and why this is the case. For this reason it's better to use Decimal
, so that's what I'll do.
Unfortunately Decimal
is slow compared to Double
or Float
, and doesn't have the most fully supported API in Swift
. Actually its Objective-C counterpart, NSDecimalNumber
is pretty crufty in Objective-C too. Still, it is the best standardly available type to do financial computations.
Next I don't see any explicit rounding in your code example, and as far as I know fractional penny contributions to 401ks (or IRAs) are not a thing, so you need a way to round to whole pennies. It's probably the most awkward code I'm going to present, so let's get it out of the way first.
There are two ways to do rounding on Decimal
, and unfortunately neither of them are especially convenient. Unlike Double
, Decimal
does not provide .rounded()
method.
One way is to call the NSDecimalRound
free function, which requires getting a pointer to the value to round. I'd probably use that way in my own code, but let's avoid pointers.
The second way is to make use of the fact that Decimal
bridges to NSDecimalNumber
which provides a rounding(accordingToBehavior:)
method. That requires creating a NSDecimalNumberBehaviors
instance that configures how the rounding will be done... Did I mention the API is crufty? We have to cast to NSDecimalNumber
then call rounding(accordingToBehavior:)
and then cast back to Decimal
. It looks like this:
import Foundation
let pennyRoundingBehavior = NSDecimalNumberHandler(
roundingMode: .bankers,
scale: 2,
raiseOnExactness: false,
raiseOnOverflow: true,
raiseOnUnderflow: true,
raiseOnDivideByZero: true
)
/**
Computes `dollarAmount * percentage`, rounded to the nearest penny.
Uses "Banker's" rounding: If truncated digits represent a value greater than
half a penny, the result is rounded up. If it's less than half a penny, the
result is rounded down. If it's exactly half a penny, the value is rounded to
the nearest even penny. So `10.015 is rounded to `10.02`, while `10.025` is
also rounded to `10.02`.
- Parameters:
- percentage: `Decimal` value in the range 0...100
- amount: `Decimal` to which `percentage` is applied.
*/
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
}
Fortunately, all the other math we need to do with Decimal
can be done just like for Double
.
Since you want contributions per month, I define an enum
to represent months:
enum ContributionMonth: Int, CaseIterable
{
case January, February, March, April, May, June,
July, August, September, October, November, December
static var count:Int { allCases.count }
}
I represent the contributions for a given month as a struct
struct MonthlyContributions
{
var roth : Decimal = 0
var traditional : Decimal = 0
}
extension MonthlyContributions
{
var total: Decimal { roth traditional }
init(combinedAmount: Decimal, rothPercentage: Decimal)
{
let roth = roundToNearestPenny(
percentage: rothPercentage,
of: combinedAmount
)
self.init(roth: roth, traditional: combinedAmount - roth)
}
}
Note the init
in the extension computes the contribution distribution, which will simplify later code.
Since I'll be storing instances of MonthlyContributions
in an Array
with elements that correspond to the months starting with January
it would be nice to just index that array by the month. To do that I extend Array
when it holds MonthlyContributions
as elements:
extension Array where Element == MonthlyContributions {
subscript (index: ContributionMonth) -> Element { self[index.rawValue] }
}
An alternative would be to use a Dictionary
instead of an Array
. For this example, I think Array
is slightly simpler, but it doesn't really matter much. With Dictionary
you wouldn't need the extension, but adding a value requires using both a key and a value, whereas with Array
I can just append
. Dictionary
would be the way to go if you don't want to populate it with contributions for all months.
So next up, for a given month, we need to adjust combined contribution so that total annual contributions will be capped to the legal limit:
func calculateCombinedContribution(
monthlyContribution monthly : Decimal,
annualContributionLimit limit : Decimal,
contributionsSoFarThisYear cummulative: Decimal) -> Decimal
{
min(max(0, limit - cummulative), monthly)
}
Now all of that is used to compute a whole years worth of monthly contributions:
func computeMonthlyContributions(
monthlyPay : Decimal,
contributionPercentage: Decimal,
rothPercentage : Decimal,
contributionLimit : Decimal) -> [MonthlyContributions]
{
assert((0...100).contains(contributionPercentage))
assert((0...100).contains(rothPercentage))
assert(monthlyPay >= 0)
assert(contributionLimit >= 0)
var totalContributions : Decimal = 0
var monthlyContributions: [MonthlyContributions] = []
monthlyContributions.reserveCapacity(ContributionMonth.count)
let monthlyContribution = roundToNearestPenny(
percentage: contributionPercentage,
of: monthlyPay
)
for _ in ContributionMonth.allCases
{
let combinedAmount = calculateCombinedContribution(
monthlyContribution : monthlyContribution,
annualContributionLimit : contributionLimit,
contributionsSoFarThisYear: totalContributions
)
let contribution = MonthlyContributions(
combinedAmount: combinedAmount,
rothPercentage: rothPercentage
)
monthlyContributions.append(contribution)
totalContributions = contribution.total
}
return monthlyContributions
}
The assertions are just there to validate basic numeric assumptions, at least in DEBUG
builds. For each month we determine how much the combined contribution will be for that month, which takes into account whether the total contributions have reached the legal annual limit, then create a MonthlyContributions
instance, which distributes the amount internally between roth
and traditional
, append it to the array, and update the total contributions.
Now we can call it with some values, and print out the calculated monthly contributions:
var monthlyPay : Decimal = 15203.0
var personal401kLimit : Decimal = 22500.0
var personal401kPercentage: Decimal = 10.0
var roth401kPercentage : Decimal = 10.0
let monthlyContributions = computeMonthlyContributions(
monthlyPay : monthlyPay,
contributionPercentage: personal401kPercentage,
rothPercentage : roth401kPercentage,
contributionLimit : personal401kLimit
)
for month in ContributionMonth.allCases
{
let curContribution = monthlyContributions[month]
print("Contribution for \(month)")
print(" Roth : $\(curContribution.roth)")
print(" Traditional: $\(curContribution.traditional)")
print()
}
This just uses the values you provide. The output is:
Contribution for January Roth : $152.03 Traditional: $1368.27 Contribution for February Roth : $152.03 Traditional: $1368.27 Contribution for March Roth : $152.03 Traditional: $1368.27 Contribution for April Roth : $152.03 Traditional: $1368.27 Contribution for May Roth : $152.03 Traditional: $1368.27 Contribution for June Roth : $152.03 Traditional: $1368.27 Contribution for July Roth : $152.03 Traditional: $1368.27 Contribution for August Roth : $152.03 Traditional: $1368.27 Contribution for September Roth : $152.03 Traditional: $1368.27 Contribution for October Roth : $152.03 Traditional: $1368.27 Contribution for November Roth : $152.03 Traditional: $1368.27 Contribution for December Roth : $152.03 Traditional: $1368.27
As you can see the annual contribution limit isn't triggered by that monthlyPay
value, so increasing it to 30000.0
does trigger it:
Contribution for January Roth : $300 Traditional: $2700 Contribution for February Roth : $300 Traditional: $2700 Contribution for March Roth : $300 Traditional: $2700 Contribution for April Roth : $300 Traditional: $2700 Contribution for May Roth : $300 Traditional: $2700 Contribution for June Roth : $300 Traditional: $2700 Contribution for July Roth : $300 Traditional: $2700 Contribution for August Roth : $150 Traditional: $1350 Contribution for September Roth : $0 Traditional: $0 Contribution for October Roth : $0 Traditional: $0 Contribution for November Roth : $0 Traditional: $0 Contribution for December Roth : $0 Traditional: $0