Search code examples
iosswiftclassdatamodel

Constructing a data model for Time Periods


I'm building an app that offers a service with something similar to dog walking. The people who will walk the dogs can upload the days and times they are available. Instead of them picking an actual date like Mon Jan 1st I let them just pick whatever days of the week and whatever times they are avail.

The problem I'm having is I can't figure out how to construct a data model for it.

What's in the photo is a collectionView with a cell and in each cell I show the available day and times slots that they can pick. Each day of the week has the same 7 time slots that a user who wants to be the dog walker can pick from.

The thing is if someone picks Sun 6am-9am, 12pm-3pm, and 6pm-9pm but they also pick Mon 6am-9m, how can I construct a data model that can differentiate between the days and times. For eg Sunday at 6am - 9am and Mon 6am-9am, how to tell the difference? Should those time slots be Doubles or Strings?

This is what I'm currently using for the collectionView data source and the cell:

// the collectionView's data source
var tableData = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]

//cellForItem
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: availabilityCell, for: indexPath) as! AvailabilityCell

cell.clearCellForReuse()
cell.dayOfWeek = tableData[indexPath.item]

// inside the AvailabilityCell itself
var dayOfWeek: String? {
    didSet {

        dayOfWeekLabel.text = dayOfWeek
    }
}

func clearCellForReuse() {

    dayOfWeekLabel.text = nil
    // deselect whatever radio buttons were selected to prevent scrolling issues
}

For a little further explanation what will eventually happen is when the user who wants their dog walks scrolls through to see who is avail, if the day and time their scrolling isn't on any of the days and times the person who posted (Sun & Mon with the chosen hours) isn't available, then their post shouldn't appear in the feed but if it is one of those days and one of those hours then their post will appear in the feed (in the example if someone is scrolling on Sunday at 10pm this post shouldn't appear). Whatever is in the data model will get compared to whatever day and time the posts are currently getting scrolled . I'm using Firebase for the backend.

What I came up with is rather convoluted and that's why I need something more reasonable.

class Availability {

    var monday: String?
    var tuesday: String?
    var wednesday: String?
    var thursday: String?
    var friday: String?
    var saturday: String?
    var sunday: String?

    var slotOne: Double? // sunday 6am-9am I was thinking about putting military hours  here that's why I used a double
    var slotTwo: Double? // sunday 9am-12pm
    var slotTwo: Double? // sunday 12pm-3pm
    // these slots would continue all through saturday and this doesn't seem like the correct way to do this. There would be 49 slots in total (7 days of the week * 7 different  slots per day)
}

I also thought about maybe separating them into different data models like a Monday class, Tuesday class etc but that didn't seem to work either because they all have to be the same data type for the collectionView datasource.

enter image description here

UPDATE In @rob's answer he gave me some insight to make some changes to my code. I'm still digesting it but I still have a a couple of problems. He made a cool project that shows his idea.

1- Since I’m saving the data to Firebase database, how should the data get structured to get saved? There can be multiple days with similar times.

2- I'm still wrapping my head around rob's code because I've never dealt with time ranges before so this is foreign to me. I'm still lost with what to sort against especially the time ranges against inside the callback

// someone is looking for a dog walker on Sunday at 10pm so the initial user who posted their post shouldn't appear in the feed

let postsRef = Database().database.reference().child("posts")

postsRef.observe( .value, with: { (snapshot) in

    guard let availabilityDict = snapshot.value as? [String: Any] else { return }

    let availability = Availability(dictionary: availabilityDict)

    let currentDayOfWeek = dayOfTheWeek()

    // using rob;s code this compares the days and it 100% works
    if currentDayOfWeek != availability.dayOfWeek.text {

        // don't add this post to the array
        return
    }

    let currentTime = Calendar.current.dateComponents([.hour,.minute,.second], from: Date())

    // how to compare the time slots to the current time?
    if currentTime != availability.??? {
        // don't add this post to the array
        return
    }

    // if it makes this far then the day and the time slots match up to append it to the array to get scrolled
})

func dayOfTheWeek() -> String? {        
    let dateFormatter = NSDateFormatter()
    dateFormatter.dateFormat = "EEEE"
    return dateFormatter.stringFromDate(self)
}

Solution

  • There are lots of ways to skin the cat, but I might define the availability as day enumeration and time range:

    struct Availability {
        let dayOfWeek: DayOfWeek
        let timeRange: TimeRange
    }
    

    Your day of the week might be:

    enum DayOfWeek: String, CaseIterable {
        case sunday, monday, tuesday, wednesday, thursday, friday, saturday
    }
    

    Or you could also do:

    enum DayOfWeek: Int, CaseIterable {
        case sunday = 0, monday, tuesday, wednesday, thursday, friday, saturday
    }
    

    Their are pros and cons of both Int and String. The string representation is easier to read in the Firestore web-based UI. The integer representation offers easier sorting potential.

    Your time range:

    typealias Time = Double
    typealias TimeRange = Range<Time>
    
    extension TimeRange {
        static let allCases: [TimeRange] = [
            6 ..< 9,
            9 ..< 12,
            12 ..< 15,
            15 ..< 18,
            18 ..< 21,
            21 ..< 24,
            24 ..< 30
        ]
    }
    

    In terms of interacting with Firebase, it doesn’t understand enumerations and ranges, so I’d define an init method and dictionary property to map to and from [String: Any] dictionaries that you can exchange with Firebase:

    struct Availability {
        let dayOfWeek: DayOfWeek
        let timeRange: TimeRange
    
        init(dayOfWeek: DayOfWeek, timeRange: TimeRange) {
            self.dayOfWeek = dayOfWeek
            self.timeRange = timeRange
        }
    
        init?(dictionary: [String: Any]) {
            guard
                let dayOfWeekRaw = dictionary["dayOfWeek"] as? DayOfWeek.RawValue,
                let dayOfWeek = DayOfWeek(rawValue: dayOfWeekRaw),
                let startTime = dictionary["startTime"] as? Double,
                let endTime = dictionary["endTime"] as? Double
            else {
                return nil
            }
    
            self.dayOfWeek = dayOfWeek
            self.timeRange = startTime ..< endTime
        }
    
        var dictionary: [String: Any] {
            return [
                "dayOfWeek": dayOfWeek.rawValue,
                "startTime": timeRange.lowerBound,
                "endTime": timeRange.upperBound
            ]
        }
    }
    

    You could also define a few extensions to make this easier to work with, e.g.,

    extension Availability {
        func overlaps(_ availability: Availability) -> Bool {
            return dayOfWeek == availability.dayOfWeek && timeRange.overlaps(availability.timeRange)
        }
    }
    
    extension TimeRange {
        private func string(forHour hour: Int) -> String {
            switch hour % 24 {
            case 0:      return NSLocalizedString("Midnight", comment: "Hour text")
            case 1...11: return "\(hour % 12)" + NSLocalizedString("am", comment: "Hour text")
            case 12:     return NSLocalizedString("Noon", comment: "Hour text")
            default:     return "\(hour % 12)" + NSLocalizedString("pm", comment: "Hour text")
            }
        }
    
        var text: String {
            return string(forHour: Int(lowerBound)) + "-" + string(forHour: Int(upperBound))
        }
    }
    
    extension DayOfWeek {
        var text: String {
            switch self {
            case .sunday:    return NSLocalizedString("Sunday", comment: "DayOfWeek text")
            case .monday:    return NSLocalizedString("Monday", comment: "DayOfWeek text")
            case .tuesday:   return NSLocalizedString("Tuesday", comment: "DayOfWeek text")
            case .wednesday: return NSLocalizedString("Wednesday", comment: "DayOfWeek text")
            case .thursday:  return NSLocalizedString("Thursday", comment: "DayOfWeek text")
            case .friday:    return NSLocalizedString("Friday", comment: "DayOfWeek text")
            case .saturday:  return NSLocalizedString("Saturday", comment: "DayOfWeek text")
            }
        }
    }
    

    If you don’t want to use Range, you can just define TimeRange as a struct:

    enum DayOfWeek: String, CaseIterable {
        case sunday, monday, tuesday, wednesday, thursday, friday, saturday
    }
    
    extension DayOfWeek {
        var text: String {
            switch self {
            case .sunday:    return NSLocalizedString("Sunday", comment: "DayOfWeek text")
            case .monday:    return NSLocalizedString("Monday", comment: "DayOfWeek text")
            case .tuesday:   return NSLocalizedString("Tuesday", comment: "DayOfWeek text")
            case .wednesday: return NSLocalizedString("Wednesday", comment: "DayOfWeek text")
            case .thursday:  return NSLocalizedString("Thursday", comment: "DayOfWeek text")
            case .friday:    return NSLocalizedString("Friday", comment: "DayOfWeek text")
            case .saturday:  return NSLocalizedString("Saturday", comment: "DayOfWeek text")
            }
        }
    }
    
    struct TimeRange {
        typealias Time = Double
    
        let startTime: Time
        let endTime: Time
    }
    
    extension TimeRange {
        static let allCases: [TimeRange] = [
            TimeRange(startTime: 6, endTime: 9),
            TimeRange(startTime: 9, endTime: 12),
            TimeRange(startTime: 12, endTime: 15),
            TimeRange(startTime: 15, endTime: 18),
            TimeRange(startTime: 18, endTime: 21),
            TimeRange(startTime: 21, endTime: 24),
            TimeRange(startTime: 24, endTime: 30)
        ]
    
        func overlaps(_ availability: TimeRange) -> Bool {
            return (startTime ..< endTime).overlaps(availability.startTime ..< availability.endTime)
        }
    }
    
    extension TimeRange {
        private func string(forHour hour: Int) -> String {
            switch hour % 24 {
            case 0:      return NSLocalizedString("Midnight", comment: "Hour text")
            case 1...11: return "\(hour % 12)" + NSLocalizedString("am", comment: "Hour text")
            case 12:     return NSLocalizedString("Noon", comment: "Hour text")
            default:     return "\(hour % 12)" + NSLocalizedString("pm", comment: "Hour text")
            }
        }
    
        var text: String {
            return string(forHour: Int(startTime)) + "-" + string(forHour: Int(endTime))
        }
    }
    
    struct Availability {
        let dayOfWeek: DayOfWeek
        let timeRange: TimeRange
    
        init(dayOfWeek: DayOfWeek, timeRange: TimeRange) {
            self.dayOfWeek = dayOfWeek
            self.timeRange = timeRange
        }
    
        init?(dictionary: [String: Any]) {
            guard
                let dayOfWeekRaw = dictionary["dayOfWeek"] as? DayOfWeek.RawValue,
                let dayOfWeek = DayOfWeek(rawValue: dayOfWeekRaw),
                let startTime = dictionary["startTime"] as? Double,
                let endTime = dictionary["endTime"] as? Double
            else {
                return nil
            }
    
            self.dayOfWeek = dayOfWeek
            self.timeRange = TimeRange(startTime: startTime, endTime: endTime)
        }
    
        var dictionary: [String: Any] {
            return [
                "dayOfWeek": dayOfWeek.rawValue,
                "startTime": timeRange.startTime,
                "endTime": timeRange.endTime
            ]
        }
    }
    
    extension Availability {
        func overlaps(_ availability: Availability) -> Bool {
            return dayOfWeek == availability.dayOfWeek && timeRange.overlaps(availability.timeRange)
        }
    }