Hello my friends! I've made some significant strides in learning how to calculate an employee's yearly earnings while accounting for promotions. I am now trying to calculate savings this employee makes each year (a percentage of yearly earnings).
First, here is the code that works in the macOS Command Line Tool environment which calculates earnings only (not savings):
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: Int) -> Decimal {
let base = Decimal(hoursWorked * payRate * (100 - percent)) 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
case seniorManagement
//... 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 "Senior Management": self = .seniorManagement
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 .seniorManagement: return "Senior 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]
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]
}
//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
//birthdate
var birthdate: Date
//date of hire with company
var hiredate: Date
//the age the employee plans to retire
var retirementAge: Int
//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
var percentToSave: 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,
birthdate: Date,
hiredate: Date,
retirementAge: Int,
department: Department = .other("Employee"),
monthlyHoursWorked: Int = 160,
percentToSave: Int)
{
self.name = name
self.birthdate = birthdate
self.hiredate = hiredate
self.retirementAge = retirementAge
self.department = department
self.monthlyHoursWorked = monthlyHoursWorked
self.percentToSave = percentToSave
self.employmentHistory = []
}
}
//and the guts of working with an Employee instance
extension Employee {
//number of years between now and retirement age
func calcYearsRemaining(retireAge: Int, birthday: Date) -> Int {
let calendar: NSCalendar! = NSCalendar(calendarIdentifier: .gregorian)
let now = Date()
let calcAge = calendar.components(.year, from: birthday, to: now, options: [])
let age = calcAge.year
let yearsRemaining = retireAge - age!
return yearsRemaining
}
//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))
}
// mutating func work(months: Int) {
// employmentHistory
// .append(contentsOf: Array.init(repeating: department,
// count: calcYearsRemaining(retireAge: 65, birthday: Date.init(timeIntervalSince1970: 0)) > months ? months : calcYearsRemaining(retireAge: 65, birthday: Date.init(timeIntervalSince1970: 0))))
// }
//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 depertment 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
}
//calculate how much the employee has saved year-to-year
func yearlySavings() -> [Decimal] {
let years = employmentHistory.chunked(into: 12)
//initialize our result array
var savings: [Decimal] = []
//loop through our array of years
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
}
}
extension Employee {
//what departments was the employee in each year?
func yearlyDepartments() -> [String] {
//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)
//transform our array of years into an array of Department names
// for each year using map(_:)
let departments: [String] = years.map { months in
//create a Set so we can keep only unique departments
var seenDepts: Set<String> = []
//now we take our array of months for this year and use reduce(into:_:)
// to accumulate the departments for each month into a single String
//loop through the months and record the rawValue for the Department
//we pull them into an Array<String> and then concat them all
// together at the end
//we use a Set to make sure we don't end up with something like
// Sales,Sales,Sales,Sales,Sales,Sales,Sales,Sales,Sales,Management,Management,Management
// instead we'll just get Sales,Management
return months.reduce(into: [String]()) { acc, dept in
//the .inserted element of the tuple returned from .insert(_:)
// will be false if the department is already in the Set
// and therefore we don't need to add it
if seenDepts.insert(dept.rawValue).inserted {
acc.append(dept.rawValue)
}
}.joined(separator: ",") //concat the final result with , separator
}
return departments
}
//what departments was the employee part of and how much did they
// make year-to-year?
func yearlyDepartmentsAndEarnings() -> [(String, Decimal)] {
//first we get the departments as a comma-delimited String
// for each year
let yearlyDepartments = yearlyDepartments()
//then we get the accumulated earnings for each year
let yearlyEarnings = yearlyEarnings()
//then we get the accumulated savings for each year
// let yearlySavings = yearlySavings() // <- for future use
//then we merge them together into an array of tuples
// with the tuple elements labeled as departments and earnings
// to make using them later a little easier
let yearlyDepartmentsAndEarnings: [(departments: String, earnings: Decimal)] =
Array(zip(yearlyDepartments, yearlyEarnings))
//and send it back
return yearlyDepartmentsAndEarnings
}
}
//func printYearlyDepartmentsAndEarnings(_ yearlyDeptsAndEarnings: [(departments: String, earnings: Decimal)]) {
// for (year, deptsAndEarnings) in yearlyDeptsAndEarnings.enumerated() {
// print("Department(s) for Year \(year + yearGroupCalc + 1): \(deptsAndEarnings.departments)")
// print("Earnings for Year \(year + yearGroupCalc + 1): \(deptsAndEarnings.earnings)")
// }
//}
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: 2024, month: 12, day: 28).date!
var secondDeptMonths: Int { Calendar.current.dateComponents([.month], from: promotion1date, to: promotion2date).month! }
var promotion3date = DateComponents(calendar: .current, year: 2025, 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)]) {
for (year, deptsAndEarnings) in yearlyDeptsAndEarnings.enumerated() {
print("Department(s) for Year \(year + yearGroup + 1): \(deptsAndEarnings.departments)")
print("Earnings for Year \(year + yearGroup + 1): \(deptsAndEarnings.earnings)")
// print("Savings for Year \(year + yearGroup + 1): \(deptsAndEarnings.savings)") //<- for future use
print("Age: \(year + age)")
print("")
}
}
//create a new employee
var charlotte = Employee("Charlotte Grote", birthdate: birthdate, hiredate: hiredate, retirementAge: 65, department: .other("Hospitality"), percentToSave: 10)
//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.yearlyDepartmentsAndEarnings())
In an attempt to update the yearlyDepartmentsAndEarnings function, I have run into two errors. Here is how I have altered the function:
func yearlyDepartmentsAndEarnings() -> [(String, Decimal, Decimal)] {
let yearlyDepartments = yearlyDepartments()
let yearlyEarnings = yearlyEarnings()
let yearlySavings = yearlySavings()
let yearlyDepartmentsAndEarnings: [(departments: String, earnings: Decimal, savings: Decimal)] =
Array(zip(yearlyDepartments, yearlyEarnings, yearlySavings)) //<- errors are here
return yearlyDepartmentsAndEarnings
}
Two error messages occur. The first is associated with the Array(zip...
"Initializer 'init(_:)' requires the types '(departments: String, earnings: Decimal, savings: Decimal)' and '(Array<String>.Element, Array<Decimal>.Element)' be equivalent"
The second is associated with the yearlySavings in the same line...
"Extra argument in call"