// // Parsers.swift // RIT Dining // // Created by Campbell on 9/19/25. // import Foundation func parseOpenStatus(openTime: Date, closeTime: Date) -> OpenStatus { // This can probably be done a little cleaner but it's okay for now. If the location is open but the close date is within the next // 30 minutes, label it as closing soon, and do the opposite if it's closed but the open date is within the next 30 minutes. let calendar = Calendar.current let now = Date() var openStatus: OpenStatus = .closed if now >= openTime && now <= closeTime { // This is basically just for Bytes, it checks the case where the open and close times are exactly 24 hours apart, which is // only true for 24-hour locations. if closeTime == calendar.date(byAdding: .day, value: 1, to: openTime)! { openStatus = .open } else if closeTime < calendar.date(byAdding: .minute, value: 30, to: now)! { openStatus = .closingSoon } else { openStatus = .open } } else if openTime <= calendar.date(byAdding: .minute, value: 30, to: now)! && closeTime > now { openStatus = .openingSoon } else { openStatus = .closed } return openStatus } func parseLocationInfo(location: DiningLocationParser) -> DiningLocation { print("beginning parse for \(location.name)") // The descriptions sometimes have HTML
tags despite also having \n. Those need to be removed. let desc = location.description.replacingOccurrences(of: "
", with: "") // Early return if there are no events, good for things like the food trucks which can very easily have no openings in a week. if location.events.isEmpty { return DiningLocation( id: location.id, name: location.name, summary: location.summary, desc: desc, mapsUrl: location.mapsUrl, diningTimes: nil, open: .closed, visitingChefs: nil, dailySpecials: nil) } var openStrings: [String] = [] var closeStrings: [String] = [] // Dining locations have a regular schedule, but then they also have exceptions listed for days like weekends or holidays. If there // are exceptions, use those times for the day, otherwise we can just use the default times. Also check for repeats! The response data // can include those somtimes, for reasons:tm: for event in location.events { if let exceptions = event.exceptions, !exceptions.isEmpty { // Only save the exception times if the location is actually open during those times, and if these times aren't a repeat. // I've seen repeats for Brick City Cafe specifically, where both the breakfast and lunch standard open periods had // exceptions listing the same singluar brunch period. That feels like a stupid choice but oh well. if exceptions[0].open, !openStrings.contains(exceptions[0].startTime), !closeStrings.contains(exceptions[0].endTime) { openStrings.append(exceptions[0].startTime) closeStrings.append(exceptions[0].endTime) } } else { if !openStrings.contains(event.startTime), !closeStrings.contains(event.endTime) { openStrings.append(event.startTime) closeStrings.append(event.endTime) } } } // Early return if there are no valid opening times, most likely because the day's exceptions dictate that the location is closed. // Mostly comes into play on holidays. if openStrings.isEmpty || closeStrings.isEmpty { return DiningLocation( id: location.id, name: location.name, summary: location.summary, desc: desc, mapsUrl: location.mapsUrl, diningTimes: nil, open: .closed, visitingChefs: nil, dailySpecials: nil) } // I hate all of this date component nonsense. var openDates: [Date] = [] var closeDates: [Date] = [] let calendar = Calendar.current let now = Date() for i in 0.. 1 ? String(splitString[1]) : "").replacingOccurrences(of: ")", with: ""))) } } visitingChefs = chefs dailySpecials = specials } else { visitingChefs = nil dailySpecials = nil } return DiningLocation( id: location.id, name: location.name, summary: location.summary, desc: desc, mapsUrl: location.mapsUrl, diningTimes: diningTimes, open: openStatus, visitingChefs: visitingChefs, dailySpecials: dailySpecials) } extension DiningLocation { mutating func updateOpenStatus() { var openStatus: OpenStatus = .closed if let diningTimes = diningTimes, !diningTimes.isEmpty { for i in diningTimes.indices { openStatus = parseOpenStatus(openTime: diningTimes[i].openTime, closeTime: diningTimes[i].closeTime) // If the first event pass came back closed, loop again in case a later event has a different status. This is mostly to // accurately catch Gracie's multiple open periods each day. if openStatus != .closed { break } } self.open = openStatus } else { self.open = .closed } } }