exclude Array of DateInterval from Array of DateInterval - swift

I want to exclude Array of DateInterval from Array of DateInterval. This's my code, but I don't think it will not be helpful.. it goes in infinite loop sometimes and I couldn't solve it.
extension DateInterval {
static func exclude(_ excludedIntervals: [DateInterval], from periods: [DateInterval]) -> [DateInterval] {
if excludedIntervals.isEmpty { return periods }
var resultSlots: [DateInterval] = []
for period in periods {
let results = period.exclude(excludedIntervals)
resultSlots.append(contentsOf: results)
}
return resultSlots
}
func exclude(_ execludedIntervals: [DateInterval]) -> [DateInterval] {
if execludedIntervals.isEmpty { return [self] }
var sortedExecludedIntervals = execludedIntervals.sorted()
var resultSlots: [DateInterval] = []
var execludedInterval = sortedExecludedIntervals.removeFirst()
// remove execludedIntervals from self
if let intersection = self.intersection(with: execludedInterval) {
if self.start == intersection.start && self.end > intersection.end {
let newSlot = DateInterval(start: intersection.end, end: self.end)
resultSlots.append(contentsOf: newSlot.exclude(sortedExecludedIntervals))
} else if self.start < intersection.start && self.end == intersection.end {
let newSlot = DateInterval(start: self.start, end: intersection.start)
resultSlots.append(contentsOf: newSlot.exclude(sortedExecludedIntervals))
} else if self.start < intersection.start && self.end > intersection.end {
let preSlot = DateInterval(start: self.start, end: intersection.start)
resultSlots.append(contentsOf: preSlot.exclude(sortedExecludedIntervals))
let postSlot = DateInterval(start: intersection.end, end: self.end)
resultSlots.append(contentsOf: postSlot.exclude(sortedExecludedIntervals))
} else {
// start = start && end = end
resultSlots = []
return resultSlots
}
}
return resultSlots
}
}
For example, I want to exclude 1 pm- 3pm and 5 pm - 6 pm intervals from an interval 12 pm - 6pm. The function should return 12 pm - 1 pm and 3 pm - 5 pm.

A couple of thoughts:
If I want a method to operate on an array of DateInterval, I’d suggest putting it in an Array (or Sequence) extension, constrained to DateInterval types, rather than a static method on DateInterval:
extension Array where Element == DateInterval {
func exclude(_ excludedIntervals: [DateInterval]) -> [DateInterval] { ... }
}
When considering excluding a DateInterval from another, there are tons of different scenarios:
You could exclude some small window from the middle of the interval;
You could exclude a portion at the start of the interval;
You could exclude a portion at the end of the interval; and
You could exclude the whole interval.
In my mind, it gets too messy to think of all of those scenarios, so I decided to simplify this a bit and decide:
Where exactly does the excluded region intersect with the current interval (and DateInterval supplies a nice method to do that for us);
If I “cut out” that intersection from the date interval, I might end up with two intervals, a before interval and an after interval (e.g. if I cut 2pm-3pm out of noon-6pm, the before interval will be noon-2pm and the after interval will be 3pm-6pm);
The algorithm then distills down to “if the interval is intersected by the excluded region, replace the original interval with the two other intervals, the one before and the one after”; and
Given that I’m mutating the original array of resulting intervals, I’d suggest nested loops, with the outer loop being the intervals to be excluded and the inner loop being the resulting intervals which, because it’s mutating, I’ll iterate through using a while statement, manually checking and adjusting the current index.
That yields:
extension Array where Element == DateInterval {
func exclude(_ excludedIntervals: [DateInterval]) -> [DateInterval] {
var results: [DateInterval] = self
for excludedInterval in excludedIntervals {
var index = results.startIndex
while index < results.endIndex {
let interval = results[index]
if let intersection = interval.intersection(with: excludedInterval) {
var before: DateInterval?
var after: DateInterval?
if intersection.start > interval.start {
before = DateInterval(start: interval.start, end: intersection.start)
}
if intersection.end < interval.end {
after = DateInterval(start: intersection.end, end: interval.end)
}
let replacements = [before, after].compactMap { $0 }
results.replaceSubrange(index...index, with: replacements)
index += replacements.count
} else {
index += 1
}
}
}
return results
}
}
Then we can consider the exclusion applied to a single DateInterval as just a special case of an array with one item:
extension DateInterval {
func exclude(_ excludedIntervals: [DateInterval]) -> [DateInterval] {
return [self].exclude(excludedIntervals)
}
}
So:
let formatter = ISO8601DateFormatter()
let start = formatter.date(from: "2019-02-09T12:00:00Z")!
let end = formatter.date(from: "2019-02-09T18:00:00Z")!
let exclude1Start = formatter.date(from: "2019-02-09T13:00:00Z")!
let exclude1End = formatter.date(from: "2019-02-09T14:00:00Z")!
let exclude2Start = formatter.date(from: "2019-02-09T16:00:00Z")!
let exclude2End = formatter.date(from: "2019-02-09T17:00:00Z")!
let intervals = DateInterval(start: start, end: end)
.exclude([
DateInterval(start: exclude1Start, end: exclude1End),
DateInterval(start: exclude2Start, end: exclude2End)
])
print(intervals)
Will produce:
[
2019-02-09 12:00:00 +0000 to 2019-02-09 13:00:00 +0000,
2019-02-09 14:00:00 +0000 to 2019-02-09 16:00:00 +0000,
2019-02-09 17:00:00 +0000 to 2019-02-09 18:00:00 +0000
]

Related

Check if between two dates is no longer working under iOS16.3

Using iOS16.3, XCode14.2, Swift5.7.2,
Why is the following method no longer working ?
I call this method by setting date = Date() and maximumDate = Date() as well...
According to this solution, it should work - but it doesn't
public class THManager : ObservableObject {
#Published public var minimumDate: Date = Date()
#Published public var maximumDate: Date = Date()
public func isBetweenMinAndMaxDates(date: Date) -> Bool {
print(min(minimumDate, maximumDate))
print(max(minimumDate, maximumDate))
print(min(minimumDate, maximumDate)...max(minimumDate, maximumDate))
print(date)
print((min(minimumDate, maximumDate)...max(minimumDate, maximumDate)).contains(date))
return (min(minimumDate, maximumDate)...max(minimumDate, maximumDate)).contains(date)
}
}
2022-02-08 19:45:51 +0000
2023-02-03 19:45:51 +0000
2022-02-08 19:45:51 +0000...2023-02-03 19:45:51 +0000
2023-02-03 19:45:51 +0000
false
It supposed to return true ! Why does it return false ???
By the way it works if date = Date() and maximumDate = Date().addingTimeInterval(1)
Very strange, isn't it ?
There is no need for such complexity. Date objects conform to the Comparable and Equatable protocols, so testing for a date being between 2 other dates is one line:
extension Date {
func between(start: Date, end: Date) -> Bool {
return self > start && self < end
}
}
You'd use that like this:
let date = Date()
if date.betweeen(start: someDate, end: someOtherDate) {
// the date is between the start and the end
} else {
// The date is not between the start and end dates
}
The above will only return true if the date in question is not equal to start or end date. You could easily change it to match dates that match the beginning and end dates by using >= and <= instead of > and < in the comparisons.
And as discussed in the comments, Date objects have sub-millisecond precision. Two dates that appear identical may be different by a tiny fraction of a second, the best way to verify your comparisons is to convert your Dates to decimal seconds and log the seconds values. (See the Date property timeIntervalSinceReferenceDate.)
Edit:
Check out this sample code using the above exension:
extension Date {
func between(start: Date, end: Date) -> Bool {
return self > start && self < end
}
var asStringWithDecimal: String {
return DateFormatter.localizedString(from: self, dateStyle: .medium, timeStyle: .medium) + " ( \(self.timeIntervalSinceReferenceDate) seconds)"
}
}
let now = Date()
for _ in (1...5) {
let random = Double.random(in: -1.5...1.5)
let test = now.advanced(by: random)
let start = now.advanced(by: -1)
let end = now.advanced(by: 1)
let isBetween = test.between(start: start, end: end)
let isOrNot = isBetween ? "is" : "is not"
let output = "\(test.asStringWithDecimal) \n \(isOrNot) between \n \(start.asStringWithDecimal) and \n \(end.asStringWithDecimal)"
print(output)
}
That will generate 5 random dates ± 1.5 seconds from the current date, and then test each one to see if it is within 1 second of the current date. It logs the result as both Date strings and Doubles, so you can see what's happening when the seconds match (but the fractions of a second likely don't match.)

How to iterate through a function each time altering the value of the parameters provided

Hi so I have a class Calculations with a series of functions one of these is keplerianElementsToEcef. In my view controller I hard code the values for the parameters and then call the function. However later on in a seperate class I have a bool isInRange. If my spacecraft is out of cellular range, I return false and a string as well. I also want to then iterate through the keplerianElementsToEcef function, each time increasing the timeOfCalculation parameter by two minutes until at some point in time in the future the satellite is in range.
I've tried to simply call the function but increase the value used initially as the time, current time, by two minutes. The other variables rangeMeanMotion etc, are the same as those hardcoded in the view controller
var isInRange: Bool
var rangeString: String
if distance < range {
isInRange = true
rangeString = "In Range"
} else {
isInRange = false
rangeString = "Not In Range"
while isInRange == false {
var dateString = dateFormatter.date(from: calculationTime!)!
var updatedDate = dateString.addingTimeInterval(TimeInterval(5.0 * 60.0))
var updateDateAsString = dateFormatter.string(from: updatedDate)
Calculations.shared.keplerianElementsToECEF(meanMotion: rangeMeanMotion, eccentricity: rangeEccentricity, Inclination: rangeInclination, LongitudeAscendingNode: rangeLongitudeAscendingNode, argumentPerigee: rangeArgumentPerigee, M0: rangeM0, epoch: rangeEpoch, date: updateDateAsString) {
}
}
}
In the function parameters under date: updateDateAsString I get the following error: Extra argument 'date' in call
var timeOfCalculation : TimeInterval = 0
func doItUntilSpacecraftIsInRange(){
repeat {
timeOfCalculation += TimeInterval(2.0 * 60.0)
Calculations.shared.keplerianElementsToECEF(meanMotion: rangeMeanMotion, eccentricity: rangeEccentricity, Inclination: rangeInclination, LongitudeAscendingNode: rangeLongitudeAscendingNode, argumentPerigee: rangeArgumentPerigee, M0: rangeM0, epoch: rangeEpoch, date: updateDateAsString)
} while spacecraft.isInRange == false
}
doItUntilSpacecraftIsInRange()
I solved this issue. I made the statement iterate during a certain time period (1 day) and my code looks like this:
else {
isInRange = false
rangeString = "Not In Range"
print(calculationTime)
if let calcTime = calculationTime {
let parsedDate = dateFormatter.date(from: calcTime) ?? Date()
for interval in stride(from: 0, to: 1440, by: 2) {
var updatedDate = parsedDate.addingTimeInterval(TimeInterval(interval * 60))
var updateDateAsString = dateFormatter.string(from: updatedDate)
Calculations.shared.keplerianElementsToECEF(meanMotion: rangeMeanMotion, eccentricity: rangeEccentricity, Inclination: rangeInclination, LongitudeAscendingNode: rangeLongitudeAscendingNode, argumentPerigee: rangeArgumentPerigee, M0: rangeM0, epoch: rangeEpoch, date: updateDateAsString)
let xDistance = ecefX - wgs84X
let yDistance = ecefY - wgs84Y
let zDistance = ecefZ - wgs84Z
let iteratedDistance = sqrt(xDistance*xDistance + yDistance*yDistance + zDistance*zDistance)
if iteratedDistance < 7000 {
nextVisible = updateDateAsString
break
}
}
}
}

How to tell if a business is open after midnight

I've looked at this question, but a lot of it doesn't make sense nor work:
How to determine if a business is open given the hours of operation (Swift-iOS)
Some places on my list open at like 7:30 am and close the next day at 4 am. I have the times on my Parse-server listed as such:
openTime (Number): 7.5 (for 7:30 am)
closeTime (Number): and 4 for (4 am)
However, when I use the logic from the linked questions,
if now.hour! > Int(openTime) && now.hour! < Int(closeTime) {}
it keeps saying that business is closed. How could I adjust the numbers or the logic, in order to make it work for places that close late night / early morning the next day?
You can consider that closeTime must be superior to openTime, otherwise its the day after.
so it will become something like:
let realCloseTime = closeTime < openTime ? closeTime + 24 : closeTime
if now.hour! > Int(openTime) && now.hour! < Int(realCloseTime) {}
You will have a problem with minutes. Stores that open at 7:30 will report being open at 7:00. Stores that close at 11:30 will report being closed at 11:00.
Assuming that open and close are doubles.
var openTime: Double
var closeTime: Double
then
func isOpen(at date: Date = Date()) -> Bool {
guard let openDate = createDate(bySettingHours: openTime, of: date) else { return false }
guard let closeDate = createDate(bySettingHours: closeTime, of: date) else { return false }
guard let adjustedCloseDate = add24Hours(to: closeDate) else { return false }
let realCloseDate = openDate < closeDate ? closeDate : adjustedCloseDate
return openDate <= date && date <= realCloseDate
}
private func createDate(bySettingHours double: Double, of date: Date) -> Date? {
let hour = Int(floor(double)) % 24
let minute = Int(double * 30) % 30
return Calendar.current.date(bySettingHour: hour, minute: minute, second: 0, of: date)
}
private func add24Hours(to date: Date) -> Date? {
return Calendar.current.date(byAdding: .hour, value: 24, to: date)
}
I was assuming you have some kind of model like
class Business {
var openTime: Double
var closeTime: Double
}
My suggestion was to add the isOpen(at:) method here.
class Business {
var openTime: Double
var closeTime: Double
func isOpen(at date: Date = Date()) -> Bool {
// implmentation
}
}
It would be used something like this
var business = Business()
// Setup `business`
business.isOpen()
// or
let now = Date()
business.isOpen(at: now)

Sorting HealthKit data by date

I'm trying to get pulse data from HealthKit and sort them by date for use in a line chart. I'm running a 'for loop' to get the correct dates and put the results in an array before putting the results in the chart but it seems like they get put in a random order and I don't understand why.
class Pulse {
var pulse = 0.0
var startDate = Date()
}
var pulseData: [Pulse] = []
func getHeartBeatsForAWeek() {
for i in 1...7 {
getHeartBeats(startDate: date.getStartOfSpecificDateByAddingToToday(day: -i), endDate: date.getStartOfSpecificDateByAddingToToday(day: -i + 1))
}
}
func getHeartBeats(startDate: Date, endDate: Date) {
PulseHelper.shared.averageHearthRate(startDate: startDate, endDate: endDate) { (data) in
DispatchQueue.main.async {
self.pulseData.append(data)
self.updateGraph()
}
}
}
Here is my function for fetching the heart rate:
func averageHearthRate(startDate: Date, endDate: Date, completion: #escaping (Pulse) -> Void) {
let typeHeart = HKQuantityType.quantityType(forIdentifier: .heartRate)
let startDate = startDate
let endDate = endDate
let predicate: NSPredicate? = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: HKQueryOptions.strictEndDate)
let query = HKStatisticsQuery(quantityType: typeHeart!, quantitySamplePredicate: predicate, options: .discreteAverage, completionHandler: {(query: HKStatisticsQuery, result: HKStatistics?, error: Error?) -> Void in
DispatchQueue.main.async(execute: {() -> Void in
let quantity: HKQuantity? = result?.averageQuantity()
let beats: Double? = quantity?.doubleValue(for: HKUnit.count().unitDivided(by: HKUnit.minute()))
print("Got: \(String(format: "%.f", beats!)) from \(startDate)")
let pulse = Pulse.init()
pulse.pulse = beats!
pulse.startDate = startDate
completion(pulse)
})
})
PermissionsHelper.shared.store.execute(query)
}
This is what I get when I print the results:
Got: 82 from 2019-03-30 23:00:00 +0000
Got: 74 from 2019-03-31 22:00:00 +0000
Got: 73 from 2019-03-25 23:00:00 +0000
Got: 74 from 2019-03-27 23:00:00 +0000
Got: 70 from 2019-03-26 23:00:00 +0000
Got: 74 from 2019-03-29 23:00:00 +0000
Got: 108 from 2019-03-28 23:00:00 +0000
I'd like them to get in correct order.
I found a solution to my own question that works. I will leave this question open because I'm pretty new to Swift and I think there are probably better ways to do this than the way I did.
func getHeartBeats(startDate: Date, endDate: Date) {
PulseHelper.shared.averageHearthRate(startDate: startDate, endDate: endDate) { (data) in
DispatchQueue.main.async {
self.pulseData.append(data)
self.pulseData = self.pulseData.sorted(by: {$0.startDate < $1.startDate})
self.updateGraph()
}
}
}
So what I did was instead of having an array of Doubles with the heart rate I created a Pulse class with pulse and startDate and sorted them by date using
self.pulseData.sorted(by: {$0.startDate < $1.startDate})
Just after adding an element to your array, you need to use the sort() function.
Here you can find the documentation about all the ways you can implement it.
In this situation, if pulseData is an array of Date ( [Date] ), you just need to call:
self.pulseData.sort()
EDIT:
As you could see in the link to documentation I posted, there are several ways to use the sort() function in order to apply your sorting rule to a custom object.
In this situation, let's assume you have this class:
class Pulse {
var pulse: Double = 0.0
var startDate = Date()
init(p: Double,d: Date) {
self.pulse = p
self.startDate = d
}
}
Create an array of Pulse objects:
let pulse1 = Pulse(p: .., d: Date(...))
let pulse2 = Pulse(p: .., d: Date(...))
var pulseData: [Pulse] = [pulse1, pulse2]
You can sort the array in this way:
//Ascending order
pulseData.sort(by: {$0.startDate < $1.startDate})

How can I apply custom sort to this array of objects?

I have a collection of objects where I want to sort the object by SortDate where SortDate is sorted by date newest to current date then from future date to past date. E.g. If my Events array contains objects with SortDate equal to June 4, June 8 and June 20. I want to sort it so that June 8 is shown first then June 20 then June 4. Where June 8 is closest to today's date June 6. How can I do that?
Here's my attempt:
self.eventsArray = Array(self.realm.objects(Event.self).filter("EventType == \"Event\"").sorted(byKeyPath: "SortDate", ascending: false))
let dateObjectsFiltered = self.eventsArray.filter ({ ($0.SortDate?.toDate)! > Date() })
self.eventsArray = dateObjectsFiltered.sorted { return $0.SortDate! < $1.SortDate! }
you can use this, assuming that all your date optionals are not nil.
func days(fromDate: Date, toDate: Date) -> Int {
return Calendar.current.dateComponents(Set<Calendar.Component>([.day]), from: fromDate, to: toDate).day ?? 0
}
let today = Date()
self.eventsArray.sort {
let first = days(fromDate: today, toDate: $0.SortDate!.toDate!)
let second = days(fromDate: today, toDate: $1.SortDate!.toDate!)
return (first >= 0 && second < 0) ? true : ((first < 0 && second >= 0) ? false : (first < second))
}
You can custom sort the array using Array's sort(by:) function.
Here is a sample:
import Foundation
struct event {
var SortDate: Date
}
//Create the array
var unsortedArray = [event]()
unsortedArray.append(event(SortDate: Date(timeIntervalSince1970: 1528070400)))
unsortedArray.append(event(SortDate: Date(timeIntervalSince1970: 1528416000)))
unsortedArray.append(event(SortDate: Date(timeIntervalSince1970: 1529452800)))
//Determine the closest date to the current date
let currentDate = Date(timeIntervalSinceNow: 0)
var lowestDiff = -1
var closestDate: Date?
var component:Set<Calendar.Component> = Set<Calendar.Component>()
component.insert(.second)
//Loop through the dates, keep track of the current closest date
for element in unsortedArray {
let dateComponents = Calendar.current.dateComponents(component, from: currentDate, to: element.SortDate)
if (lowestDiff == -1 || (abs(dateComponents.second!) < lowestDiff)) {
lowestDiff = abs(dateComponents.second!)
closestDate = element.SortDate
}
}
//Sort the array
unsortedArray = unsortedArray.sorted(by:
{
//If the closest date is in the comparison, return the closest date as greater.
if (closestDate != nil) {
if ($0.SortDate == closestDate) {
print($0.SortDate)
return true
}
else if ($1.SortDate == closestDate){
print($1.SortDate)
return false
}
}
//Otherwise, compare the dates normally
return $0.SortDate > $1.SortDate
}
)