DEV Community

Chris Myers
Chris Myers

Posted on

What I learned last week - 16 December 2019

Introduction

When I first started learning iOS development, I blogged weekly about what I was learning. Once I completed my bootcamp, I fell away from it as I was just trying to stay afloat at work.

However, blogging helped me retain a lot of information, and lately, I feel like I figure something out, feel happy, but then promptly forget it and a few weeks later, have to do the same thing again, and try and relearn. So this is my attempt to pick the writing habit back up. I'll just write about a challenge, or learning opportunity I faced and how I went about solving it, or in same instances, re-explain how it was explained to me--if for nothing else, but to reinforce the concept.

Challenge - Nested Data

A current feature I'm working on at work is to display a rate schedule for plumbers to view when installing a water heater. This schedule is a cost structure of the cheapest and most expensive times to heat water throughout the day, and their associated rate per kilowatt-hour (kWh).

Alt Text

The data we receive with the rates, times, seasons, etc... is...involved. There can be multiple schedules. Within each schedule, there can be multiple seasons. Each season has a two time periods. Within the time period, there can be anywhere from 1 - 5 rates and 1 - 5 times.

I've pared down the actual data into what I'm using just for this blog post, but below is a visual representation of how deeply nested the data coming back is.

struct UtilityScheduleResponse {
    let schedule: [UtilitySchedule]
}
struct UtilitySchedule {
    let scheduleID: String
    let scheduleName: String
    let seasons: [UtilitySeason]
}

struct UtilitySeason {
    let seasonID: String
    let touSeason: String
    let seasonName: String
    let seasonDescription: String
    let timePeriods: [TimePeriod]
}

struct TimePeriod {
    let periodID: String
    let periodName: String
    let periodDescription: String
    let tou: [TimeOfUse]
}

struct TimeOfUse {
    let touID: String
    let touName: String
    let touStartTimeFormatted: String
    let touEndTimeFormatted: String
    let touRate: String
}

However, for the purposes of displaying all rates and times, there is just a few items of data I need to throw into the RateScheduleCell.

So I started to dive into the data into the first way that popped into my head...Nested loops.

Attempt 1
func nestedScheduleDetailsExample(for schedule: UtilitySchedule) -> [RateDetail] {
    var rateDetailsArray: [RateDetail] = []
    for season in schedule.seasons {
        for timePeriod in season.timePeriods {
            for timeOfUse in timePeriod.tou {
                let rateDetail = RateDetail(rate: timeOfUse.touRate,
                                            startTime: timeOfUse.touStartTimeFormatted,
                                            endTime: timeOfUse.touEndTimeFormatted)
                rateDetailsArray.append(rateDetail)

            }
        }
    }
    return rateDetailsArray
}

Hmmm...okay, it works, but I know I need to work on functional programming and there has to be a way to do this functionally.

Functional Attempt 1
func functionalAttempt1(for schedule: UtilitySchedule) -> [RateDetail] {
    var rateDetailsArray: [RateDetail] = []
    for season in schedule.seasons {
        for timePeriod in season.timePeriods {
            rateDetailsArray = timePeriod.tou.map({ (tou) -> RateDetail in
                return RateDetail(rate: tou.touRate,
                                  startTime: tou.touStartTimeFormatted,
                                  endTime: tou.touEndTimeFormatted)
            })
        }
    }
    return rateDetailsArray
}

So this worked...sort of. All I ever got was the last rate detail in the json. This is because I failed to realized that after the first iteration of timePeriod in season.timePeriods, the rateDetailsArray would be overwritten with the next timePeriod. So while the map was working to turn the timePeriod.tou array into an array of RateDetail objects, it was removing the previously created ones on each iteration in the timePeriod for-in loop.

Okay, so now I need an array of timePeriods with its own array of RateDetail objects. So I rewrote the function.

FunctionalAttempt 2
func functionalAttempt2(for schedule: UtilitySchedule) -> [[RateDetail]] {
    var rateDetailsArray: [[RateDetail]] = [[]]
    for season in schedule.seasons {
        rateDetailsArray = season.timePeriods.map { period in
             period.tou.map { timeOfUse in
                RateDetail(rate: timeOfUse.touRate,
                           startTime: timeOfUse.touStartTimeFormatted,
                           endTime: timeOfUse.touEndTimeFormatted)

            }
        }
    }
    return rateDetailsArray
}

But I didn't really wanted a nested array of objects. I just wanted an array of all the RateDetails. I was stuck. So I turned to the Slack boards to ask for help in making my function more...well, functional...but also to have a single array.

Almost immediately, I got several responses. I guess I knew this at one point and forgot, but flatMap will flatten an array, meaning taking a nested array and concatenating the result into one array. Mostly, when I thought of flatMap I remember it used to be a way to return an array that excluded nil values.

When Swift 4 was released, compactMap took over that role and I guess I thought 'flatMap` had been deprecated.

compactMap

Returns an array containing the non-nil results of calling the given transformation with each element of this sequence. Use this method to receive an array of non-optional values when your transformation produces an optional value.

flatMap

Returns an array containing the concatenated results of calling the given transformation with each element of this sequence. Use this method to receive a single-level collection when your transformation produces a sequence or collection for each element.

Turns out, flatMap was deprecated for the non-nil role, but is still alive and kicking doing what it's name implies...flattening an array.

For me, just being told to use something often leads to paralysis. I have an answer, but I'm not sure how to use it. Thankfully, everyone I talked to on Slack gave some examples of the use of flatMap.

Functional Attempt 3
func functionalAttempt3(for schedule: UtilitySchedule) -> [RateDetail] {
    let rateDetails: [RateDetail] = schedule.seasons
        .flatMap { (utilitySeason) -> [TimePeriod] in utilitySeason.timePeriods }
        .flatMap { (timePeriod) -> [TimeOfUse] in timePeriod.tou }
        .map { (tou) -> RateDetail in RateDetail(rate: tou.touRate,
                                                 startTime: tou.touStartTimeFormatted,
                                                 endTime: tou.touEndTimeFormatted)
    }

    return rateDetails
}

Okay, so there is a lot to unpack here. rateDetails has a type of Array<RateDetail>, so the final part of that code better be creating instances of RateDetail and appending it to the array. Everything leading up to it is diving through the UtilitySchedule to find the TimeOfUse array which contains instance of TimeOfUse. Each instance is then used to create a RateDetail.

I've purposely left the longer notation for map and flatMap because, for me, I prefer the readability. I know what kind of object each closure is taking in (i.e UtilitySeason, TimePeriod, TimeOfUse), and what the expected output at the end of each flatMap or map call should create (i.e. Array<TimePeriod>, Array<TimeOfUse>, Array<RateDetail>. If I had not used flatMap, I'd have something like:

Array<UtilitySeason<Array<TimePeriod<Array<TimeOfUseArray<Array<RateDetail>>>>

or to see it visually another way,

[[[[RateDetail]]]]]

I think we can agree a completely mapped version is pretty ugly and unwieldy to navigate through.

By using flatMap on the first three arrays (UtilitySeason, TimePeriod, and TimeOfUse), I'm able to flatten out the entire thing and just map the part I want to transform into one single array of RateDetail. This makes setting my tableView cell much easier and less error prone because lord knows trying to set the prices with something like

cell.weekdayPrice.text = utilitySchedule[0][0][3][1].rate

would have been just asking for disaster.

I am so grateful for the help on Slack. Every time I've ever asked a question, it's always been met with genuine interest in helping me. I've never been belittled for not knowing something, or ridiculed for asking what may seem like a "simple" problem. Everyone gets stuck on things sometimes, or has a hard time seeing through an answer. In the end, I refactored my models some so I would not have to write this function in the ViewController, but I think I need to continue to remind myself to START SOMEWHERE! I can always make it prettier later...but at the very least, make an attempt to solve it rather than trying to do it in my head and hoping to write the perfect algorithm on the first try.

Update: I also want to thank Donny Wals @donnywals for editing this for me. He gave me some helpful insight and was a wonderful sounding board.

Discussion (0)