Sorting HealthKit data by date - swift

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

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

exclude Array of DateInterval from Array of DateInterval

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
]

How to compare 2 dates with each other. Dates are converted to String

I want to sort my tableview on date. I know I can do that by using:
array.sort {
$0.date! < $1.date!
}
Problem is that the date is converted to a String. The date is saved in core data and changing the attribute date to NSDate is causing problems. SO how can I compare the dates without changing my Core Data?
Update for swift 3 :
if you want to compare date objects, used below lines of code:
extension Date {
func isGreater(than date: Date) -> Bool {
return self >= date
}
func isSmaller(than date: Date) -> Bool {
return self < date
}
func isEqual(to date: Date) -> Bool {
return self == date
}
}
// how to call it?
let isGreater = end_date.isGreater(than: start_date)
// where end_date and start_date is your date objects.
You should be able to sort by strings, what is your string date format?
Here is an example of sorting dates with strings:
let date1 = Date()
let date2 = Date().addingTimeInterval(TimeInterval.abs(60.0))
let date3 = Date().addingTimeInterval(TimeInterval.abs(60 * 60 * 24))
var arrayOfDates = [date1.description, date2.description, date3.description]
let sorted = arrayOfDates.sorted { $0.0 > $0.1 }
print(sorted.first!)
print(sorted[1])
print(sorted[2])
output:
2016-10-31 18:33:32 +0000
2016-10-30 18:34:32 +0000
2016-10-30 18:33:32 +0000
switching sorted to:
let sorted = arrayOfDates.sorted { $0.0 < $0.1 }
gives the following outside:
2016-10-30 18:35:58 +0000
2016-10-30 18:36:58 +0000
2016-10-31 18:35:58 +0000

Generating the start and end of the month. Swift 2

I have the following code:
func rangeOfPeriod(period: NSCalendarUnit, date: NSDate) -> (NSDate, NSDate) {
let calendar = NSCalendar.currentCalendar()
var startDate: NSDate? = nil
var duration: NSTimeInterval = 0
calendar.rangeOfUnit(period, startDate: &startDate, interval: &duration, forDate: date)
let endDate = startDate!.dateByAddingTimeInterval(duration - 1)
return (startDate!, endDate)
}
When i print the following lines:
print("Day test = \(rangeOfPeriod(.Day, date: NSDate()))")
print("Week test = \(rangeOfPeriod(.WeekOfYear, date: NSDate()))")
print("month test = \(rangeOfPeriod(.Month, date: NSDate()))")
print("Year test = \(rangeOfPeriod(.Year, date: NSDate()))")
All of them work as expected apart from month.
I get 'month test = (2016-03-01 00:00:00 +0000, 2016-03-31 22:59:59 +0000)' as a result and it seems to be missing a hour. For the time for all the others i get '2016-03-26 23:59:59 +0000'.
Any help would be appreciated
Maybe there is a confusion about the interval parameter in the rangeOfUnit method.
From the documentation
func rangeOfUnit(_ unit: NSCalendarUnit,
startDate datep: AutoreleasingUnsafeMutablePointer<NSDate?>,
interval tip: UnsafeMutablePointer<NSTimeInterval>,
forDate date: NSDate) -> Bool
.....
tip : Upon return, contains the duration (as NSTimeInterval) of the calendar unit unit that
contains the date date
For this month (March 2016) due to Daylight Saving Time change the duration is
31 * 86400.0 - 3600.0 = 2674800.0
That means there is an hour missing, but it's not specified when.
To get the end of the month this is more accurate
func endOfThisMonth() -> NSDate
{
let calendar = NSCalendar.currentCalendar()
let components = NSDateComponents()
components.day = 1
let startOfNextMonth = calendar.nextDateAfterDate(NSDate(), matchingComponents: components, options: .MatchNextTime)!
return calendar.dateByAddingUnit(.Second, value: -1, toDate: startOfNextMonth, options: [])!
}