CycleHelper.swift 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
  1. // LoopFollow
  2. // CycleHelper.swift
  3. // Created by Jonas Björkert on 2025-03-01.
  4. import Foundation
  5. enum CycleHelper {
  6. /// Returns a positive modulus value (always between 0 and modulus).
  7. static func positiveModulo(_ value: TimeInterval, modulus: TimeInterval) -> TimeInterval {
  8. let remainder = value.truncatingRemainder(dividingBy: modulus)
  9. return remainder < 0 ? remainder + modulus : remainder
  10. }
  11. /// Calculates the cycle offset for a given date relative to midnight.
  12. /// The offset is the number of seconds into the cycle (i.e., date mod interval).
  13. static func cycleOffset(for date: Date, interval: TimeInterval) -> TimeInterval {
  14. let calendar = Calendar.current
  15. let startOfDay = calendar.startOfDay(for: date)
  16. let secondsSinceMidnight = date.timeIntervalSince(startOfDay)
  17. return secondsSinceMidnight.truncatingRemainder(dividingBy: interval)
  18. }
  19. /// Same as above, but takes a timestamp (seconds since 1970) instead of a Date.
  20. static func cycleOffset(for timestamp: TimeInterval, interval: TimeInterval) -> TimeInterval {
  21. let date = Date(timeIntervalSince1970: timestamp)
  22. return cycleOffset(for: date, interval: interval)
  23. }
  24. /// Computes the delay experienced when using a heartbeat device to read a sensor value.
  25. /// The calculation is based on a sensor reference (Date) and sensor interval.
  26. /// All calculations assume midnight as the base reference.
  27. static func computeDelay(sensorReference: Date,
  28. sensorInterval: TimeInterval,
  29. heartbeatLast: Date,
  30. heartbeatInterval: TimeInterval) -> TimeInterval
  31. {
  32. let sensorOffset = cycleOffset(for: sensorReference, interval: sensorInterval)
  33. let hbOffset = cycleOffset(for: heartbeatLast, interval: heartbeatInterval)
  34. return positiveModulo(hbOffset - sensorOffset, modulus: heartbeatInterval)
  35. }
  36. /// Overloaded version of computeDelay where the sensor cycle offset is already known.
  37. static func computeDelay(sensorOffset: TimeInterval,
  38. heartbeatLast: Date,
  39. heartbeatInterval: TimeInterval) -> TimeInterval
  40. {
  41. let hbOffset = cycleOffset(for: heartbeatLast, interval: heartbeatInterval)
  42. return positiveModulo(hbOffset - sensorOffset, modulus: heartbeatInterval)
  43. }
  44. }