// // FetchData.swift // RIT Dining // // Created by Campbell on 8/31/25. // import Foundation enum InvalidHTTPError: Error { case invalid } // This API requesting code came from another project of mine and was used to fetch the GitHub API for update checking. I just copied it // here, but it can probably be made simpler for this use case. // Get information for all dining locations. func getAllDiningInfo(date: String?, completionHandler: @escaping (Result) -> Void) { // The endpoint requires that you specify a date, so get today's. let date_string: String = date ?? getAPIFriendlyDateString(date: Date()) let url_string = "https://tigercenter.rit.edu/tigerCenterApi/tc/dining-all?date=\(date_string)" guard let url = URL(string: url_string) else { print("Invalid URL") return } let request = URLRequest(url: url) URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { completionHandler(.failure(error)) return } guard let data = data else { completionHandler(.failure(URLError(.badServerResponse))) return } guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { completionHandler(.failure(InvalidHTTPError.invalid)) return } let decoded: Result = Result(catching: { try JSONDecoder().decode(DiningLocationsParser.self, from: data) }) completionHandler(decoded) }.resume() } // Get information for just one dining location based on its location ID. func getSingleDiningInfo(date: String?, locationId: Int, completionHandler: @escaping (Result) -> Void) { // The current date and the location ID are required to get information for just one location. let date_string: String = date ?? getAPIFriendlyDateString(date: Date()) let url_string = "https://tigercenter.rit.edu/tigerCenterApi/tc/dining-single?date=\(date_string)&locId=\(locationId)" print("making request to \(url_string)") guard let url = URL(string: url_string) else { print("Invalid URL") return } let request = URLRequest(url: url) URLSession.shared.dataTask(with: request) { data, response, error in guard case .none = error else { return } guard let data = data else { print("Data error.") return } guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { completionHandler(.failure(InvalidHTTPError.invalid)) return } let decoded: Result = Result(catching: { try JSONDecoder().decode(DiningLocationParser.self, from: data) }) completionHandler(decoded) }.resume() } 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. 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. if exceptions[0].open { openStrings.append(exceptions[0].startTime) closeStrings.append(exceptions[0].endTime) } } else { 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) }