So instead of storing how many years an employee has worked for the company and calculating their earnings from that, what I've done below is record on a monthly basis what department the employee belongs to. Then when it comes time to calculate their yearly earnings, we total up each year's months using whatever pay rate applies to the department they were in each month.
See if this makes sense...
/*
Assumptions and notes:
1. An employee will always work the same number of hours every month,
even after switching departments
2. When an employee switches departments, they maintain the same
position in the pay rate scale as they had in the old department
(i.e., if someone in Hospitality for 5 years gets promoted to Sales,
they don't start at Year 1 on the Sales pay scale)
3. Employees can't switch departments in mid-month
4. We keep everything as an Int until we have to convert to Decimal;
I didn't really see any need to be using Decimals all the time
5. I got rid of the YearlyEarnings struct because there was not really
any need for it any more, given how we are storing the months and
calculating the yearly earnings from them
6. Some optimizations/enhancements that could be made include:
1. Storing the number of hours worked on a monthly basis
2. Calculating and storing a year's earnings as each group of
12 months is completed, rather than calculating them all every time
*/
import Foundation
//nothing much to say about these...
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
}
}
//a convenience function for when it comes time to group an employee's
// work history into 12-month blocks
//this uses the function Paul gives at
// https://www.hackingwithswift.com/example-code/language/how-to-split-an-array-into-chunks
// but one could instead use the Swift Algorithms package, which offers
// a similar function, but which is probably optimized a great deal.
// so if performance is a serious concern, maybe that's a better choice
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)])
}
}
}
//all available departments an employee can belong to
enum Department: RawRepresentable {
case sales
case management
//... add anything else you want
//a catch-all for other departments
//supply a department name when creating an .other
// e.g. .other("Hospitality")
case other(String)
//custom init and rawValue property allow us to
// use RawRepresentable and associated values
// (as in our .other case) at the same time
//add a switch case for each specific department
//we should almost never have to use this; it's
// really just here because it has to be
init?(rawValue: String) {
switch rawValue {
case "Sales": self = .sales
case "Management": self = .management
case let dept: self = .other(dept)
}
}
//add a switch case for each specific department
var rawValue: String {
switch self {
case .sales: return "Sales"
case .management: return "Management"
case .other(let dept): return dept
}
}
//for each department, you can add specialized pay rates
// or just use the default rates
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]
default: rates = [10,12,14,16,18,20,22,24,26,28,30]
}
//we use min(numberOfYears, rates.count) to ensure
// we always use the last rate if numberOfYears
// exceeds the number of rates we have
//this also means we can have variable counts of rates
// for specific departments
//(e.g., if we have a maintenance department that has
// a pay rate scale like [10, 12, 16, 20])
return rates[min(numberOfYears, rates.count) - 1]
}
}
//a data structure for capturing information about an employee
// and their pay over the years
struct Employee {
//we are assuming an employee's name never changes
// this may not actually be the case, but for the sake of
// example, that's how we're doing it
let name: String
//what department does the employee currently belong to?
//we'll use .other("Employee") as a default in the init
// but that can be overriden by supplying something
// different
var department: Department
//this example assumes the employee will ALWAYS work the
// same number of hours in a month
//if we wanted to add some complexity, we could make it so
// that you have to enter the hours worked on a monthly basis
var monthlyHoursWorked: Int
//we store the employee's work history as a list of departments
// they belonged to on a monthly basis; this allows us to
// switch departments any time during the year
//we will chunk the months into batches of 12 when it comes
// time to calculate the yearly earnings
//we make this property private(set) so that it can be read
// from outside the struct but not altered; you MUST go through
// the monthWork(_:) and yearWork(_:) functions to change
// the history, and you can only add to it, not delete
private(set) var employmentHistory: [Department]
//initialize the employee
//some default values are provided for convenience
init(_ name: String,
department: Department = .other("Employee"),
monthlyHoursWorked: Int = 160) {
self.name = name
self.department = department
self.monthlyHoursWorked = monthlyHoursWorked
self.employmentHistory = []
}
}
//and the guts of working with an Employee instance
extension Employee {
//pretty self-explanatory
//this isn't strictly necessary, as the department
// property can be manipulated directly, but it reads
// nicer
mutating func promote(to newDepartment: Department) {
department = newDepartment
}
//this adds 1 or more months to the employee's work history
//we use their current department and just create as many entries
// in the employmentHistory array as indicated by the parameter
mutating func work(months: Int) {
employmentHistory
.append(contentsOf: Array.init(repeating: department,
count: months))
}
//convenience function for adding entire year(s) to the history at once
mutating func work(years: Int) {
work(months: years * 12)
}
//convenience function for adding full + partial years at once
mutating func work(years: Int, months: Int) {
work(months: years * 12 + months)
}
//calculate how much the employee has earned year-to-year
func yearlyEarnings() -> [Decimal] {
//first, chunk the monthly employment history into years
//if the number of months is not evenly divisible by 12,
// then the last chunk will indicate a partial year's earnings
//this gives us [[Department]] as the type, with each inner
// array representing 1 year (whole or partial)
let years = employmentHistory.chunked(into: 12)
//initialize our result array
var earnings: [Decimal] = []
//loop through our array of years
//using enumerated() lets us get the index into the array (useful
// when it comes to figuring out what pay rate to use) and the
// array of departments the employee belonged to in each month
for (index, months) in years.enumerated() {
//now we take our array of months for this year and use reduce(_:_:)
// to accumulate the earnings for each month into a yearly total
let yearEarnings: Decimal = months.reduce(0) { runningTotal, dept in
//first get the pay rate to use for this month
//this is dependent on what department they are in and
// how many years they have with the company
let payRate = dept.payRate(forYearsWorked: index + 1)
//pass these numbers to our calculation function
let monthlyEarnings = Calculations.computeMonthlyEarnings(
hours: monthlyHoursWorked,
atPayRate: payRate
)
//add what we have already calculated for previous months
// in this year to the current calculation and return the new total
//the first time through each year, runningTotal = 0
return runningTotal + monthlyEarnings
}
//append this year's grand total to the result array
earnings.append(yearEarnings)
}
//send it back
return earnings
}
}
//print out earnings nicely for display
func printYearlyEarnings(_ yearlyEarnings: [Decimal]) {
for (year, earnings) in yearlyEarnings.enumerated() {
print("Earnings for Year \(year + 1): $\(earnings)")
}
}