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

SOLVED: How to calculate annual earnings with a planned future promotion

Forums > Swift

Below I've enabled the annual earnings calculation to respond to the user's work department (i.e. "Sales", "Management" or other) and thus referencing the applicable array of pay rates (regular annual hourly pay increases). The problem I'm having trouble even wrapping my head around is how to factor a planned promotion, i.e. during year 5 of employment Joe plans to promote from Sales department to Management. I think I would need to interrupt

for yr in currentYearGroup...yearsWorked

but I have no idea how that could be done in code. I wish I could provide an example of code that I tried with the associated errors, but I don't know where to start with this task.

Here is the code that compiles without error but does not have any function to account for a future promotion:

import Foundation

let currentYearGroup = 1
let currentDept = "Sales"
let futureDept = "Management"
var yearOfPromotion = 5

enum Calculations {
    static func payRate(forYearsWorked numberOfYears: Int, department: String) -> Int {
        if department == "Sales" {
            let rates = [20,22,24,26,28,30,32,34,36,38,40]
            if numberOfYears > rates.count {
                return rates.last!
            } else {
                return rates[numberOfYears - 1]
            }
        } else if department == "Management" {
            let rates = [30,32,34,36,38,40,42,44,46,48,50]
            if numberOfYears > rates.count {
                return rates.last!
            } else {
                return rates[numberOfYears - 1]
            }
        } else {
            let rates = [10,12,14,16,18,20,22,24,26,28,30]
            if numberOfYears > rates.count {
                return rates.last!
            } else {
                return rates[numberOfYears - 1]
            }
        }
    }

    private static let pennyRoundingBehavior = NSDecimalNumberHandler(
        roundingMode: .bankers,
        scale: 2,
        raiseOnExactness: false,
        raiseOnOverflow: true,
        raiseOnUnderflow: true,
        raiseOnDivideByZero: true
    )

    static func computeMonthlyEarnings(
        hours hoursWorked   : Decimal,
        atRate payRate      : Decimal) -> Decimal
    {
        let base = (hoursWorked * payRate) as NSDecimalNumber
            return base.rounding(accordingToBehavior: pennyRoundingBehavior) as Decimal
        }

    static func computeYearlyEarnings(
        monthlyHours    : Int,
        yearsWorked     : Int) -> [YearlyEarnings] {
            var yearlyEarnings: [YearlyEarnings] = []

            for yr in currentYearGroup...yearsWorked {  
                yearlyEarnings.append(
                    YearlyEarnings(
                        monthlyHoursWorked: Decimal(monthlyHours),
                        atRate: payRate(forYearsWorked: yr, department: currentDept))
                )
            }
            return yearlyEarnings
        }
    }

    struct YearlyEarnings
    {
        var amount: Decimal

        init(monthlyHoursWorked: Decimal, atRate payRate: Int)
        {
            let earnings = Calculations.computeMonthlyEarnings(
                hours: monthlyHoursWorked,
                atRate: Decimal(payRate)
            )
            self.amount = earnings * 12
        }
    }

    var monthlyHoursWorked: Int = 160

    let yearlyEarnings = Calculations.computeYearlyEarnings(
        monthlyHours: monthlyHoursWorked,
        yearsWorked: 13)

    for idx in 0..<yearlyEarnings.count {
        print("Earnings for Year \(idx + 1)")
        print("    $\(yearlyEarnings[idx].amount)")
        print()
    }

2      

Presumably, a promotion only affects the pay rate from that point forward in a year, yes? So if, say, Joe gets a promotion from Sales to Management in May, then the first four months will be at one pay rate and the remaining eight will be at the other pay rate.

So there needs to be some way to track the pay rate on a month-by-month basis. Or, at the very least, something to indicate when in a year a promotion is granted.

Let me think on this a bit...

2      

@roosterboy you are absolutely right: the remaining eight months at a higher pay would need to be accounted for. Thank you for taking the time!

2      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

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

4      

And here are some usage examples:

//create a new employee
var charlotte = Employee("Charlotte Grote", 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)
//ooh, 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)
//2 years in Management
charlotte.work(years: 2)
//how much money has Charlotte made after all this time?
printYearlyEarnings(charlotte.yearlyEarnings())
Earnings for Year 1: $19200
Earnings for Year 2: $23040
Earnings for Year 3: $44480
Earnings for Year 4: $49920
Earnings for Year 5: $53760
Earnings for Year 6: $57600
Earnings for Year 7: $61440
Earnings for Year 8: $65280
Earnings for Year 9: $69120
Earnings for Year 10: $92160
Earnings for Year 11: $96000
//create another new employee
var shauna = Employee("Shauna Wickle", department: .other("Hospitality"))
//Shauna works in Hospitality for 11 years with no promotions :(
shauna.work(years: 11)
//how much money has Shauna made after all this time?
printYearlyEarnings(shauna.yearlyEarnings())
Earnings for Year 1: $19200
Earnings for Year 2: $23040
Earnings for Year 3: $26880
Earnings for Year 4: $30720
Earnings for Year 5: $34560
Earnings for Year 6: $38400
Earnings for Year 7: $42240
Earnings for Year 8: $46080
Earnings for Year 9: $49920
Earnings for Year 10: $53760
Earnings for Year 11: $57600

You can see how changing the employee's department changes their earnings.

And one more:

//and another
var mildred = Employee("Mildred Haversham")
mildred.work(months: 8)
mildred.promote(to: .management)
mildred.work(years: 3, months: 4)
printYearlyEarnings(mildred.yearlyEarnings())

With the result:

Earnings for Year 1: $32000
Earnings for Year 2: $61440
Earnings for Year 3: $65280
Earnings for Year 4: $69120

3      

@roosterboy Wow! Tell me, how can I contribute to your coffee fund? This is amazing!

2      

@roosterboy I am having difficulty modifying your print function in order to display annual earnings AND associated department. I would like the print to read something like: "Department for Year 1: Hospitality... Earnings for Year 1: $ 19200"

Here is the code I developed:

func printYearlyDepartmentsAndEarnigns(_ employmentHistory: [String], _ yearlyEarnings: [Decimal]) {
    for (year, department) in employmentHistory.enumerated() {
        print("Department for Year \(year + 1): \(department)")
    }
    for (year, earnings) in yearlyEarnings.enumerated() {
        print("Earnings for Year \(year + 1): $\(earnings)")
    }
}
...
printYearlyDepartmentsAndEarnigns(charlotte.employmentHistory, charlotte.yearlyEarnings()) //<- error is here

The following is the error: "Cannot convert value of type '[Department]' to expected argument type '[String]'"

2      

You need to pass the employment history, which is an [Department], but also remember that the department durations are chunked into months

func printYearlyDepartmentsAndEarnings(_ employmentHistory: [Department], _ yearlyEarnings: [Decimal]) {
    for (month, department) in employmentHistory.enumerated() {
        print("Department for Month \(month + 1): \(department)")        // months
    }
    for (year, earnings) in yearlyEarnings.enumerated() {
        print("Earnings for Year \(year + 1): $\(earnings)")       // years
    }
}

2      

A couple of problems here:

  1. employmentHistory is an array of Departments and your function is looking for an array of Strings as its first parameter.
  2. employmentHistory is recorded on a monthly basis rather than yearly, since employees can switch departments mid-year.

Try this:

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

And using one of the data examples from a previous post:

printYearlyDepartmentsAndEarnings(charlotte.yearlyDepartmentsAndEarnings())

output:

Departments(s) for Year 1: Hospitality
Earnings for Year 1: 19200
Departments(s) for Year 2: Hospitality
Earnings for Year 2: 23040
Departments(s) for Year 3: Hospitality,Sales
Earnings for Year 3: 44480
Departments(s) for Year 4: Sales
Earnings for Year 4: 49920
Departments(s) for Year 5: Sales
Earnings for Year 5: 53760
Departments(s) for Year 6: Sales
Earnings for Year 6: 57600
Departments(s) for Year 7: Sales
Earnings for Year 7: 61440
Departments(s) for Year 8: Sales
Earnings for Year 8: 65280
Departments(s) for Year 9: Sales
Earnings for Year 9: 69120
Departments(s) for Year 10: Management
Earnings for Year 10: 92160
Departments(s) for Year 11: Management
Earnings for Year 11: 96000

3      

That is exactly what I was trying to achieve! Well done!

@roosterboy let me know if you have a Patreon account or if there is any other way I can show support. Thank you for all you've taught me thus far!

2      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

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.