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

Calculate Annual Earnings continued: How to account for employee's seniority with company when accessing pay rate array? And how to extend annual earning results to retirement age?

Forums > Swift

A huge thank you to @roosterboy and others for their help thus far in this project!

The latest wall I've been unable to get over is accounting for employees that are not brand new to the company. If an employee has been with the company for one year, the first print result should by Year 2, not Year 1. Ideally, I would like the future app user to use a Date Picker to define their New Hire Date and the code would calculate their "yearGroup" based on the current month and year. I also need to extend the length of prints to encompass all years of employment up to retirement age.

To that end I have updated the code. The Employee struct now contains variables for birthdate, hiredate and retirementAge. Within that struct I have also included a function for calculating yearGroup and a function for calculating years remaining to retirement age:

/*
 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

 */

import Foundation

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

    //birthdate
    let birthdate: Date
    //date of hire with company
    let hiredate: Date
    //the age the employee plans to retire
    let 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

    //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) {
        self.name = name
        self.birthdate = birthdate
        self.hiredate = hiredate
        self.retirementAge = retirementAge
        self.department = department
        self.monthlyHoursWorked = monthlyHoursWorked
        self.employmentHistory = []
    }
    //number of years employee has worked for the company
    func yearGroupCalc() -> Int {
        let calendar: NSCalendar! = NSCalendar(calendarIdentifier: .gregorian)
        let now = Date()
        let calcYearGroup = calendar.components(.year, from: hiredate, to: now, options: [])
        let yearGroup = calcYearGroup.year! + 1
        return yearGroup
    }
    //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
    }
}

//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 depertment they are in and
                //  how many years they have with the company
                let payRate = dept.payRate(forYearsWorked: index + yearGroupCalc() + 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
    }
}

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 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 + 1): \(deptsAndEarnings.departments)")
        print("Earnings for Year \(year + 1): \(deptsAndEarnings.earnings)")
    }
}

//create a new employee
var charlotte = Employee("Charlotte Grote", birthdate: Date.init(timeIntervalSince1970: 0), hiredate: Date.init(timeIntervalSinceNow: 32000000), retirementAge: 65, department: .other("Hospitality"))
//give our employee some work history
//2 years in Hospitality
charlotte.work(years: 2)
//1 month in Hospitality
charlotte.work(months: 1)
//ohh, a promotion!
charlotte.promote(to: .sales)
//finish out the year in Sales
charlotte.work(months: 11)
//6 more years in Sales
charlotte.work(years: 6)
//another promotion. way to go, Charlotte!
charlotte.promote(to: .management)
//3 years in Management
charlotte.work(years: 3)
//how much money has Charlotte made after all this time?
printYearlyDepartmentsAndEarnings(charlotte.yearlyDepartmentsAndEarnings())

You'll see that I added the yearGroupCalc() to the payRate constant but that didn't seem to make a difference in the print:

  let payRate = dept.payRate(forYearsWorked: index + yearGroupCalc() + 1)

My gut tells me that the index portion of this for-in loop is what needs to include a yearGroup variable, but I don't really understand what "index" is doing for us here.

I also believe that in order to extend the print to include all the employment years up to retirement age, the portion of the print function that states

//3 years in Management
charlotte.work(years: 3)

... needs instead of "3" to house a function that takes the yearsRemaining minus the years and months in previous departments. But that seem like something that should exist above the print code, not within it.

I hope that all makes sense.

//UPDATE//

I've converted the yearGroupCalc function into a computed property, then moved it down just above the print func. The yearGroupCalc requires a charlotte.hiredate variable input which is fine since I only plan on one app user interfacing with the app.

//number of years employee has worked for the company
var yearGroupCalc: Int {
    let calendar: NSCalendar! = NSCalendar(calendarIdentifier: .gregorian)
    let now = Date()
    let calcYearGroup = calendar.components(.year, from: charlotte.hiredate, to: now, options: [])
    let yearGroup = calcYearGroup.year! + 1
    return yearGroup
}

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)")
    }
}

//create a new employee
var charlotte = Employee("Charlotte Grote", birthdate: Date.init(timeIntervalSince1970: 0), hiredate: Date.init(timeIntervalSinceNow: -32000000), retirementAge: 65, department: .other("Hospitality"))

I'm still perplexed in knowing how to extend the print to retirement age. I'm thinking the mutating func work will need to be conditional: if there are no future promtions then the length will extend to retirement age.

I tried replacing mutating func work (months: Int) with this:

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))))
    }

I will need to improve the above by having birthday and retireAge reference the "create a new employee" variable "charlotte"... that can wait until later....

Although Charlotte is 52 years old with 13 years until retirement, the above modified code gives only 4 total years in result:

Department(s) for Year 3: Hospitality
Earnings for Year 3: 19200
Department(s) for Year 4: Hospitality,Sales
Earnings for Year 4: 40640
Department(s) for Year 5: Sales
Earnings for Year 5: 46080
Department(s) for Year 6: Management
Earnings for Year 6: 69120

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.