module PostRunner::HRZoneDetector

Constants

GARMIN_ZONES

Number of heart rate zones supported by Garmin devices.

MAX_HR

Maximum heart rate that can be stored in FIT files.

Public Class Methods

detect_zones(fit_records, secs_in_zones) click to toggle source
# File lib/postrunner/HRZoneDetector.rb, line 22
def HRZoneDetector::detect_zones(fit_records, secs_in_zones)
  if fit_records.empty?
    raise RuntimeError, "records must not be empty"
  end
  if secs_in_zones.size != GARMIN_ZONES + 1
    raise RuntimeError, "secs_in_zones must have #{GARMIN_ZONES + 1} " +
      "elements"
  end

  # We generate a histogram of the time spent at each integer heart rate.
  histogram = Array.new(MAX_HR + 1, 0)

  last_timestamp = nil
  fit_records.each do |record|
    next unless record.heart_rate

    if last_timestamp
      # We ignore all intervals that are larger than 10 seconds. This
      # potentially conflicts with smart recording, but I can't see how a
      # larger sampling interval can yield usable results.
      if (delta_t = record.timestamp - last_timestamp) <= 10
        histogram[record.heart_rate] += delta_t
      end
    end
    last_timestamp = record.timestamp
  end

  # We'll process zones 5 downto 1.
  zone = GARMIN_ZONES
  hr_mins = Array.new(GARMIN_ZONES)
  # Sum of time spent in current zone.
  secs_in_current_zone = 0
  # We process the histogramm from highest to smallest HR value. Whenever
  # we have accumulated the provided amount of time we have found a HR
  # zone boundary. We complete the current zone and continue with the next
  # one.
  MAX_HR.downto(0) do |i|
    secs_in_current_zone += histogram[i]

    if secs_in_current_zone > secs_in_zones[zone]
      # In case we have collected more time than was specified for the
      # zone we carry the delta over to the next zone.
      secs_in_current_zone -= secs_in_zones[zone]
      # puts "Zone #{zone}: #{secs_in_current_zone}  #{secs_in_zones[zone]}"
      break if (zone -= 1) < 0
    end
    hr_mins[zone] = i
  end

  hr_mins
end