Constructing a data model for Time Periods - ios

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.
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)
}

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)
}
}

Related

Observe if my value reached zero in Swift? [duplicate]

It is possible to pass a date to Text() in SwiftUI, then format it as a timer using the style argument. However, a countdown like this never stops, it just keeps incrementing after zero. How to make it stop at 0?
func nextRollTime(in seconds: Int) -> Date {
let date = Calendar.current.date(byAdding: .second, value: seconds, to: Date())
return date ?? Date()
}
Above is the function I use to start a countdown, then I pass it as follows:
Text(nextRollTime(in: 20), style: .timer)
Here is a demo of possible approach - as .timer run from now for ever (by design), the idea is to replace it with regular text once specified period is over.
Tested with Xcode 12b3 / iOS 14.
struct DemoView: View {
#State private var run = false
var body: some View {
VStack {
if run {
Text(nextRollTime(in: 10), style: .timer)
} else {
Text("0:00")
}
}
.font(Font.system(.title, design: .monospaced))
.onAppear {
self.run = true
}
}
func nextRollTime(in seconds: Int) -> Date {
let date = Calendar.current.date(byAdding: .second, value: seconds, to: Date())
DispatchQueue.main.asyncAfter(deadline: .now() + Double(seconds)) {
self.run = false
}
return date ?? Date()
}
}

Swift 2d array empty after HealthKit function call - thread issues

I'm populating a twodimensional array with HealthKit data. It's working fine as long as the function loops through the 30 days. outside of that function the array is empty because HealthKit data is loaded in background and everything else executes before.
How can I wait until the data is loaded so I can access the data inside the arrays?
Here's what's happening inside my ViewController
print("function call starting") // 1
// Populate 2DArrays
self.hkManager.getTotalCalories(forDay: 30) {caloriesValue, date in
//check if caloriesValue is nil, only do something if it's not
guard let caloriesValue = caloriesValue else { return }
let dateValue = DateFormatter.localizedString(from: date!, dateStyle: .short, timeStyle: .none)
print("will append") // 7 (31x)
self.caloriesArray.append([(dateValue,caloriesValue)])
}
print("function call done") // 2
print(self.caloriesArray.count) // 3
hkManager.getWeightData(forDay: 30) {bodyMassValue, date in
// Check if bodyMassValue is nil, only do something, if it's not
guard let bodyMassValue = bodyMassValue else { return }
let dateValue = DateFormatter.localizedString(from: date!, dateStyle: .short, timeStyle: .none)
self.bodyMassArray.append([(dateValue,bodyMassValue)])
}
do {
self.age = try hkManager.getAge()
} catch let error {
print("Error calculating age: \(error)")
}
print(bodyMassArray) // 4
print(caloriesArray) // 5
print(age!) // 6
}
I've added numbers behind the print statements to make clear what get's executed when.
The functions I'm calling look like this:
func getTotalCalories(forDay days: Int, completion: #escaping ((_ calories: Int?, _ date: Date?) -> Void)) {
// Getting quantityType as .dietaryCaloriesConsumed
guard let calories = HKObjectType.quantityType(forIdentifier: .dietaryEnergyConsumed) else {
print("*** Unable to create a dietaryEnergyConsumed type ***")
return
}
let now = Date()
let startDate = Calendar.current.date(byAdding: DateComponents(day: -days), to: now)!
var interval = DateComponents()
interval.day = 1
var anchorComponents = Calendar.current.dateComponents([.day, .month, .year], from: now)
anchorComponents.hour = 0
let anchorDate = Calendar.current.date(from: anchorComponents)!
let query = HKStatisticsCollectionQuery(quantityType: calories,
quantitySamplePredicate: nil,
options: [.cumulativeSum],
anchorDate: anchorDate,
intervalComponents: interval)
query.initialResultsHandler = { _, results, error in
guard let results = results else {
print("ERROR")
return
}
results.enumerateStatistics(from: startDate, to: now) { statistics, _ in
DispatchQueue.main.async {
if let sum = statistics.sumQuantity() {
let calories = Int(sum.doubleValue(for: HKUnit.kilocalorie()).rounded())
completion(calories, statistics.startDate)
return
}
}
}
}
healthStore.execute(query)
}
Anyone any ideas what I need to do?
Thanks! :)

Swift Firebase Multithreading Issue

I'm trying to run a couple of for loops inside of a function that should return an array of strings.
Where I'm having trouble is with getting the correct results BEFORE the next for loop is run...and then again returning that results BEFORE I need to return the array of strings to complete the function.
In the first case, I have a for loop that's getting data from Firebase. I was able to use a dispatch group to get the value to print out - but then with the other loop after this - I was having issues from using the dispatch group in the prior task.
The code all works perfectly if executed with the correct values but I'm not sure how to go about this with regards to threading. Would really appreciate any help.
func findTopSpots() -> [String] {
var topFive = [String]()
var locationRatingDictionary = [String:Double]()
let myGroup = DispatchGroup()
let locationsArray = ["wyoming", "kansas", "arkansas", "florida", "california"]
// Use the days to find the most common month
let calendar = NSCalendar.current
var monthArray = [String]()
var date = self.departureDate!
let endDate = self.returnDate!
// Formatter for printing the month name
let fmt = DateFormatter()
fmt.dateFormat = "MMMM"
// Add each days month to an array
while date <= endDate {
date = calendar.date(byAdding: .day, value: 1, to: date)!
monthArray.append(fmt.string(from: date))
}
// Return the primary month from function
let primaryMonth = findMostCommonMonthInArray(array: monthArray).lowercased()
// Create a dictionary of location:rating for the primary month
for doc in locationsArray {
self.db.collection("locations").document(doc).collection("historic").document(primaryMonth).getDocument { (document, err) in
if let document = document, document.exists {
let rating = document["rating"] as? Double
locationRatingDictionary[doc] = rating
} else {
print("Document does not exist")
}
}
}
//---- THE CODE BELOW WILL NOT PRINT WITH ANY VALUES ----//
print(locationRatingDictionary)
// Sort the tuple array by rating
let locationRatingTupleArray = locationRatingDictionary.sorted{ $0.value > $1.value }
// Return 5 results
for (location,rating) in locationRatingTupleArray.prefix(5) {
print(location,rating)
topFive.append(location)
}
print("top five are \(topFive)")
return topFive
}
The issue here is that the firebase returns with query results asynchronously and you are not waiting for it to return.
I can see that you have instantiate DispatchGroup but have not used it. Lets try to use it to solve your issue. Also, you would need to change the method signature to take a closure. This avoids blocking thread to return function output.
func findTopSpots(completionHandler:([String])->Void) {
var topFive = [String]()
var locationRatingDictionary = [String:Double]()
let myGroup = DispatchGroup()
let locationsArray = ["wyoming", "kansas", "arkansas", "florida", "california"]
// Use the days to find the most common month
let calendar = NSCalendar.current
var monthArray = [String]()
var date = self.departureDate!
let endDate = self.returnDate!
// Formatter for printing the month name
let fmt = DateFormatter()
fmt.dateFormat = "MMMM"
// Add each days month to an array
while date <= endDate {
date = calendar.date(byAdding: .day, value: 1, to: date)!
monthArray.append(fmt.string(from: date))
}
// Return the primary month from function
let primaryMonth = findMostCommonMonthInArray(array: monthArray).lowercased()
// Create a dictionary of location:rating for the primary month
for doc in locationsArray {
myGroup.enter() self.db.collection("locations").document(doc).collection("historic").document(primaryMonth).getDocument { (document, err) in
if let document = document, document.exists {
let rating = document["rating"] as? Double
locationRatingDictionary[doc] = rating
} else {
print("Document does not exist")
}
myGroup.leave()
}
}
myGroup.notify(queue:.main) {
//---- THE CODE BELOW WILL NOT PRINT WITH ANY VALUES ----//
print(locationRatingDictionary)
// Sort the tuple array by rating
let locationRatingTupleArray = locationRatingDictionary.sorted{ $0.value > $1.value }
// Return 5 results
for (location,rating) in locationRatingTupleArray.prefix(5) {
print(location,rating)
topFive.append(location)
}
print("top five are \(topFive)")
completionHandler(topFive)
}
}
Your code is asynchronous fastest way is dispatchGroup with completion
//
func findTopSpots(completion:#escaping(_ arr:[string])->void){
let dispatchGroup = DispatchGroup()
var topFive = [String]()
var locationRatingDictionary = [String:Double]()
let locationsArray = ["wyoming", "kansas", "arkansas", "florida", "california"]
// Use the days to find the most common month
let calendar = NSCalendar.current
var monthArray = [String]()
var date = self.departureDate!
let endDate = self.returnDate!
// Formatter for printing the month name
let fmt = DateFormatter()
fmt.dateFormat = "MMMM"
// Add each days month to an array
while date <= endDate {
date = calendar.date(byAdding: .day, value: 1, to: date)!
monthArray.append(fmt.string(from: date))
}
// Return the primary month from function
let primaryMonth = findMostCommonMonthInArray(array: monthArray).lowercased()
// Create a dictionary of location:rating for the primary month
for doc in locationsArray {
dispatchGroup.enter()
self.db.collection("locations").document(doc).collection("historic").document(primaryMonth).getDocument { (document, err) in
if let document = document, document.exists {
let rating = document["rating"] as? Double
locationRatingDictionary[doc] = rating
} else {
print("Document does not exist")
}
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main) {
//---- THE CODE BELOW WILL NOT PRINT WITH ANY VALUES ----//
print(locationRatingDictionary)
// Sort the tuple array by rating
let locationRatingTupleArray = locationRatingDictionary.sorted{ $0.value > $1.value }
// Return 5 results
for (location,rating) in locationRatingTupleArray.prefix(5) {
print(location,rating)
topFive.append(location)
}
print("top five are \(topFive)")
completion(topFive)
}
}

Check if current time is between range of times considering after midnight - Swift

I have an opening and closing hours of restaurants from rest service:
[
{
"weekday":0,
"openingAt":"07:30:00",
"closingAt":"00:45:00"
},
{
"weekday":1,
"openingAt":"07:30:00",
"closingAt":"23:00:00"
},
{
"weekday":2,
"openingAt":"07:30:00",
"closingAt":"23:00:00"
},
{
"weekday":3,
"openingAt":"07:30:00",
"closingAt":"23:00:00"
},
{
"weekday":4,
"openingAt":"07:30:00",
"closingAt":"23:00:00"
},
{
"weekday":5,
"openingAt":"07:30:00",
"closingAt":"23:00:00"
},
{
"weekday":6,
"openingAt":"07:30:00",
"closingAt":"01:00:00"
}
]
I have created computed property for a check that:
var isClosed: Bool {
let todayIndex = (Calendar.current.component(.weekday, from: Date()) - 1) % 7
let yesterdayIndex = (Calendar.current.component(.weekday, from: Date()) - 2) % 7
let todayDate = Date()
if let wh = workingHour, wh.count > 7 {
let todayWh = wh[todayIndex]
if let openingStr = todayWh.openingAt, openingStr != "",
let openingDate = Date().setTimeHHmmss(formattedString: openingStr),
let closingStr = todayWh.closingAt, closingStr != "",
let closingDate = Date().setTimeHHmmss(formattedString: closingStr)
{
let yesterdayWh = wh[yesterdayIndex]
var fromYesterdayExtraHours = 0
if let yesterdayClosingStr = yesterdayWh.closingAt,
let yClosingDate = Date().setTimeHHmmss(formattedString: yesterdayClosingStr) {
if yClosingDate.hour > 0 {
fromYesterdayExtraHours = yClosingDate.hour
}
}
if closingDate < openingDate {
if todayDate.hour < fromYesterdayExtraHours {
return false // opened
} else {
return true // closed
}
} else if todayDate >= openingDate && todayDate <= closingDate {
return false // opened
} else {
return true // closed
}
}
}
return false // opened
}
What I do is:
Converting the strings to date object by setting the time on today object
Then checking if the closing time is on the next day (the worst part)
Then checking if the closing time is less than opening time, (like this: "openingAt":"07:30:00", "closingAt":"01:00:00"
Then checking if the current time is between opening and closing time
Any Swifty way suggestion to fix this mess?
Here is my solution that takes a little different approach by using a struct with minute and hour as Int's
struct Time: Comparable {
var hour = 0
var minute = 0
init(hour: Int, minute: Int) {
self.hour = hour
self.minute = minute
}
init(_ date: Date) {
let calendar = Calendar.current
hour = calendar.component(.hour, from: date)
minute = calendar.component(.minute, from: date)
}
static func == (lhs: Time, rhs: Time) -> Bool {
return lhs.hour == rhs.hour && lhs.minute == rhs.minute
}
static func < (lhs: Time, rhs: Time) -> Bool {
return (lhs.hour < rhs.hour) || (lhs.hour == rhs.hour && lhs.minute < rhs.minute)
}
static func create(time: String) -> Time? {
let parts = time.split(separator: ":")
if let hour = Int(parts[0]), let minute = Int(parts[1]) {
return Time(hour: hour, minute: minute)
}
return nil
}
static func isOpen(open: Time, close: Time) -> Bool {
let isClosingAfterMidnight = close.hour < open.hour ? true : false
let currentTime = Time(Date())
if isClosingAfterMidnight {
return currentTime > close && currentTime < open ? false : true
}
return currentTime >= open && currentTime < close
}
}
And it can be used like
if let open = Time.create(time: todayWh.openingAt), let close = Time.create(time: todayWh.closingAt) {
return Time.isOpen(open: open, close: close))
} else {
//error handling
}
It should work also after midnight :) Of course the Time struct could be used directly in the wh array.
My suggestion is to decode the JSON into a struct with Decodable and the opening and closing times for convenience reasons into DateComponents.
I assume that weekday == 0 is Sunday
let jsonString = """
[
{"weekday":0, "openingAt":"07:30:00", "closingAt":"00:45:00"},
{"weekday":1, "openingAt":"07:30:00", "closingAt":"23:00:00"},
{"weekday":2, "openingAt":"07:30:00", "closingAt":"23:00:00"},
{"weekday":3, "openingAt":"07:30:00", "closingAt":"23:00:00"},
{"weekday":4, "openingAt":"07:30:00", "closingAt":"23:00:00"},
{"weekday":5, "openingAt":"07:30:00", "closingAt":"23:00:00"},
{"weekday":6, "openingAt":"07:30:00", "closingAt":"01:00:00"}
]
"""
struct Schedule : Decodable {
let weekday : Int
let openingAt : DateComponents
let closingAt : DateComponents
private enum CodingKeys: String, CodingKey { case weekday, openingAt, closingAt }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
weekday = (try container.decode(Int.self, forKey: .weekday)) + 1
let open = try container.decode(String.self, forKey: .openingAt)
let openComponents = open.components(separatedBy:":")
openingAt = DateComponents(hour: Int(openComponents[0]), minute: Int(openComponents[1]), second: Int(openComponents[2]))
let close = try container.decode(String.self, forKey: .closingAt)
let closeComponents = close.components(separatedBy:":")
closingAt = DateComponents(hour: Int(closeComponents[0]), minute: Int(closeComponents[1]), second: Int(closeComponents[2]))
}
}
First declare now, start of today and the current calendar
let now = Date()
let calendar = Calendar.current
let midnight = calendar.startOfDay(for: now)
Then decode the JSON, filter the schedule for today by its weekday, create opening and closing time by adding the date components and compare the dates. endDate is calculated by finding the next occurrence of given date components after the start date. This solves the issue if the closing date is tomorrow.
do {
let data = Data(jsonString.utf8)
let schedule = try JSONDecoder().decode([Schedule].self, from: data)
let todaySchedule = schedule.first{ $0.weekday == calendar.component(.weekday, from: now) }
let startDate = calendar.date(byAdding: todaySchedule!.openingAt, to: midnight)!
let endDate = calendar.nextDate(after: startDate, matching: todaySchedule!.closingAt, matchingPolicy: .nextTime)!
let isOpen = now >= startDate && now < endDate
} catch { print(error) }
There is one limitation: The code does not work if the current time is in the range 0:00 to closing time (for example Sunday and Monday). That's a challenge for you 😉

How to parse a ISO 8601 duration format in Swift?

I have a function below which I use to format a string. The string is something like this "PT1H3M20S" which means 1 hour 3 minutes and 20 seconds. In my function, I want to format the string to 1:03:20 and it works fine but sometimes, I get the string like this "PT1H20S" which means 1 hour and 20 seconds and my function format it like this 1:20 which makes people read it as 1 minute and 20 seconds. Any suggestions?
func formatDuration(videoDuration: String) -> String{
let formattedDuration = videoDuration.replacingOccurrences(of: "PT", with: "").replacingOccurrences(of: "H", with:":").replacingOccurrences(of: "M", with: ":").replacingOccurrences(of: "S", with: "")
let components = formattedDuration.components(separatedBy: ":")
var duration = ""
for component in components {
duration = duration.count > 0 ? duration + ":" : duration
if component.count < 2 {
duration += "0" + component
continue
}
duration += component
}
// instead of 01:10:10, display 1:10:10
if duration.first == "0"{
duration.remove(at: duration.startIndex)
}
return duration
}
Call it:
print(formatDuration(videoDuration: "PT1H15S")
You can also just search the indexes of your hours, minutes and seconds and use DateComponentsFormatter positional style to format your video duration:
Create a static positional date components formatter:
extension Formatter {
static let positional: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .positional
return formatter
}()
}
And your format duration method:
func formatVideo(duration: String) -> String {
var duration = duration
if duration.hasPrefix("PT") { duration.removeFirst(2) }
let hour, minute, second: Double
if let index = duration.firstIndex(of: "H") {
hour = Double(duration[..<index]) ?? 0
duration.removeSubrange(...index)
} else { hour = 0 }
if let index = duration.firstIndex(of: "M") {
minute = Double(duration[..<index]) ?? 0
duration.removeSubrange(...index)
} else { minute = 0 }
if let index = duration.firstIndex(of: "S") {
second = Double(duration[..<index]) ?? 0
} else { second = 0 }
return Formatter.positional.string(from: hour * 3600 + minute * 60 + second) ?? "0:00"
}
let duration = "PT1H3M20S"
formatVideo(duration: duration) // "1:03:20"
Since you need to see what unit is after each number, you can't start by removing the units from the string.
Here is a solution that uses Scanner to parse the original string and finds the number of hours, minutes, and seconds to build the final result.
This also changes the return value to be optional to indicate that the passed in string isn't valid.
func formatDuration(videoDuration: String) -> String? {
let scanner = Scanner(string: videoDuration)
if scanner.scanString("PT", into: nil) {
var hours = 0
var mins = 0
var secs = 0
let units = CharacterSet(charactersIn: "HMS")
while !scanner.isAtEnd {
var num = 0
if scanner.scanInt(&num) {
var unit: NSString?
if scanner.scanCharacters(from: units, into: &unit) {
switch unit! {
case "H":
hours = num
case "M":
mins = num
case "S":
secs = num
default:
return nil // Invalid unit
}
} else {
return nil // No unit after the number
}
} else {
return nil // No integer
}
}
if hours > 0 {
return String(format: "%d:%02d:%02d", hours, mins, secs)
} else {
return String(format: "%02d:%02d", mins, secs)
}
} else {
return nil // No leading PT
}
}
print(formatDuration(videoDuration: "PT1H3M20S") ?? "bad")
print(formatDuration(videoDuration: "PT1H15S") ?? "bad")
print(formatDuration(videoDuration: "PT4M6") ?? "bad")
Output:
1:03:20
1:00:15
bad
In your case, your string carries no character for minutes, so you can make a check if the string does not contain minutes, then add "00:" between 1:20 and format appropriately.

Resources