CycleHelper.swift 2.4 KB

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