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

SOLVED: Function error: Conforming with initializer requirements... Creating a running total of income saved month after month, year after year

Forums > Swift

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"

2      

The problem you are running into is because Swift doesn't have a zip function that takes more than two parameters. If you want to do that, you will need to nest tuples.

Something like this:

func yearlyDepartmentsEarningsAndSavings() -> [(String, (Decimal, Decimal))] {
    let yearlyDepartments = yearlyDepartments()
    let yearlyEarnings = yearlyEarnings()
    let yearlySavings = yearlySavings()
    let yearlyDepartmentsEarningsAndSavings: [(departments: String, (earnings: Decimal, savings: Decimal))] =
        Array(zip(yearlyDepartments, zip(yearlyEarnings, yearlySavings)))
    return yearlyDepartmentsEarningsAndSavings
}

This will, of course, have cascading effects on the rest of your code as you will need to adjust for the extra nesting.

If you absolutely cannot have the nested tuples, you can do this to flatten them out:

func yearlyDepartmentsEarningsAndSavings() -> [(String, Decimal, Decimal)] {
    let yearlyDepartments = yearlyDepartments()
    let yearlyEarnings = yearlyEarnings()
    let yearlySavings = yearlySavings()
    let yearlyDepartmentsEarningsAndSavings: [(departments: String, earnings: Decimal, savings: Decimal)] =
        Array(zip(yearlyDepartments, zip(yearlyEarnings, yearlySavings))).map {
            (departments: $0.0, earnings: $0.1.0, savings: $0.1.1)
        }
    return yearlyDepartmentsEarningsAndSavings
}

3      

And a couple more comments, if you don't mind...

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
}

You don't need to pass in retireAge and birthday as parameters to this method on Employee, since the Employee struct already knows the retirementAge and birthdate of the employee it represents.

You should also be using Calendar instead of NSCalendar.

Finally, it's better to use nil coalescing to account for a nil result from the dateComponents(_:from:to:) call, replacing a failure with 0. And there's not need for a separate age variable.

So:

func calcYearsRemaining() -> Int {
    let calendar = Calendar(identifier: .gregorian)
    let calcAge = calendar.dateComponents([.year], from: birthdate, to: .now)
    let yearsRemaining = retirementAge - (calcAge.year ?? 0)
    return yearsRemaining
}

3      

@roosterboy Thank you! You hit the nail on the head: I didn't realize that the zip function can not take more than two parameters.

I think I would like to explore the first option you presented: nested tuples. After implementing your example function and then adjusting the printYearlyDepartmentsAndEarnings function as I think it should be, the print for departments still works but the print for earnings and savings each produces an error:

func printYearlyDepartmentsAndEarnings(_ yearlyDeptsAndEarnings: [(departments: String, (earnings: Decimal, savings: 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)") //<- first error is here
        print("Savings for Year \(year + yearGroup + 1): \(deptsAndEarnings.savings)") //<- second error is here
        print("Age: \(year + age)")
        print("")
    }
}

First error

Value of tuple type '(departments: String, (earnings: Decimal, savings: Decimal))' has no member 'earnings'

Second error

Value of tuple type '(departments: String, (earnings: Decimal, savings: Decimal))' has no member 'savings'

I realize that with the new parent function the earnings and savings are isolated by parentheses (earnings: Decimal, savings: Decimal). I don't know how to tap into them in order to print their results. Thank you so much for your help!!

2      

Since you're dealing with nested tuples and there is no label for the inner tuple, you would access earnings and savings positionally like this:

deptsAndEarnings.1.earnings
deptsAndEarnings.1.savings

Another option would be to give the inner tuple a label and then use that label when accessing earnings and savings.

2      

@roosterboy

Thank you!

If "percent" is 10, why is it that this function works if written this way....

static func computeMonthlySavings(hours hoursWorked: Int, atPayRate payRate: Int, percentSaved percent: Int) -> Decimal {
        let base = Decimal(hoursWorked * payRate) * Decimal(percent) as NSDecimalNumber
        return base.rounding(accordingToBehavior: pennyRoundingBehavior) as Decimal
    }

but not this way?...

static func computeMonthlySavings(hours hoursWorked: Int, atPayRate payRate: Int, percentSaved percent: Int) -> Decimal {
        let base = Decimal(hoursWorked * payRate) * Decimal(percent / 100) as NSDecimalNumber
        return base.rounding(accordingToBehavior: pennyRoundingBehavior) as Decimal
    }

Here is the result with percent / 100...

Department(s) for Year 3: Hospitality
Earnings for Year 3: 19200
Savings for Year 3: 0
Age: 57

I expect Savings for Year 3 to be 1920. What is it about the percent being divided by 100 that caused the result to be 0?

2      

You and I know that 10 / 100 is 0.1 but the Swift compiler is very strict and sees that you are doing division with Ints, which cannot have a fractional part, so the answer is truncated to 0.

3      

@roosterboy

Thank you so much!

2      

TAKE YOUR SKILLS TO THE NEXT LEVEL If you like Hacking with Swift, you'll love Hacking with Swift+ – it's my premium service where you can learn advanced Swift and SwiftUI, functional programming, algorithms, and more. Plus it comes with stacks of benefits, including monthly live streams, downloadable projects, a 20% discount on all books, and free gifts!

Find out more

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.