class PostRunner::MonitoringStatistics

This class can be used to generate reports for sleep data. It uses the DailySleepAnalyzer class to compute the data and generates the report for a certain time period.

Public Class Methods

new(monitoring_files) click to toggle source

Create a new MonitoringStatistics object. @param monitoring_files [Array of Fit4Ruby::Monitoring_B] FIT files

# File lib/postrunner/MonitoringStatistics.rb, line 30
def initialize(monitoring_files)
  @monitoring_files = monitoring_files
  # Week starts on Monday
  @first_day_of_week = 1
end

Public Instance Methods

daily(day) click to toggle source

Generate a report for a certain day. @param day [String] Date of the day as YYYY-MM-DD string.

# File lib/postrunner/MonitoringStatistics.rb, line 38
def daily(day)
  sleep_analyzer = DailySleepAnalyzer.new(@monitoring_files, day,
                                          +12 * 60 * 60)
  monitoring_analyzer = DailyMonitoringAnalyzer.new(@monitoring_files, day)

  str = "Daily Monitoring Report for #{day}\n\n" +
        "#{daily_goals_table(monitoring_analyzer)}\n" +
        "#{daily_stats_table(monitoring_analyzer, sleep_analyzer)}\n"
  if sleep_analyzer.sleep_cycles.empty?
    str += 'No sleep data available for this day'
  else
    str += "Sleep Statistics for " +
           "#{sleep_analyzer.window_start_time.strftime('%Y-%m-%d')} - " +
           "#{sleep_analyzer.window_end_time.strftime('%Y-%m-%d')}\n\n" +
      daily_sleep_cycle_table(sleep_analyzer).to_s
  end

  str
end
daily_html(day, doc) click to toggle source

Generate a report for a certain day. @param day [String] Date of the day as YYYY-MM-DD string.

# File lib/postrunner/MonitoringStatistics.rb, line 60
def daily_html(day, doc)
  sleep_analyzer = DailySleepAnalyzer.new(@monitoring_files, day,
                                          +12 * 60 * 60)
  monitoring_analyzer = DailyMonitoringAnalyzer.new(@monitoring_files, day)

  doc.div {
    doc.h2("Daily Monitoring Report for #{day}")
    daily_goals_table(monitoring_analyzer).to_html(doc)
    doc.br
    daily_stats_table(monitoring_analyzer, sleep_analyzer).to_html(doc)

    if sleep_analyzer.sleep_cycles.empty?
      doc.h3('No sleep data available for this day')
    else
      doc.h2("Sleep Statistics for " +
             "#{sleep_analyzer.window_start_time.strftime('%Y-%m-%d')} - " +
             "#{sleep_analyzer.window_end_time.strftime('%Y-%m-%d')}")
      daily_sleep_cycle_table(sleep_analyzer).to_html(doc)
    end
  }
end
monthly(day) click to toggle source

Generate a report for a certain month. @param day [String] Date of a day in that months as YYYY-MM-DD string.

# File lib/postrunner/MonitoringStatistics.rb, line 93
def monthly(day)
  day_as_time = Time.parse(day)
  year = day_as_time.year
  month = day_as_time.month
  last_day_of_month = Date.new(year, month, -1).day

  "Monitoring Statistics for #{day_as_time.strftime('%B %Y')}\n\n" +
    monthly_goal_table(year, month, last_day_of_month).to_s + "\n" +
    monthly_sleep_table(year, month, last_day_of_month).to_s
end
weekly(start_day) click to toggle source

Generate a report for a certain week. @param start_day [String] Date of a day in that week as YYYY-MM-DD string.

# File lib/postrunner/MonitoringStatistics.rb, line 84
def weekly(start_day)
  "Monitoring Statistics for the week of " +
    "#{start_day.strftime('%Y-%m-%d')}\n\n" +
    weekly_goal_table(start_day).to_s + "\n" +
    weekly_sleep_table(start_day).to_s
end

Private Instance Methods

cell_right_aligned(table, text) click to toggle source
# File lib/postrunner/MonitoringStatistics.rb, line 110
def cell_right_aligned(table, text)
  table.cell(text, { :halign => :right })
end
daily_goals_table(monitoring_analyzer) click to toggle source
# File lib/postrunner/MonitoringStatistics.rb, line 181
def daily_goals_table(monitoring_analyzer)
  t = FlexiTable.new

  t.head
  t.row([ 'Steps', 'Intensity Minutes', 'Floors Climbed' ])

  t.body
  t.set_column_attributes(Array.new(3, { :halign => :center}))

  steps_distance_calories = monitoring_analyzer.steps_distance_calories
  steps = steps_distance_calories[:steps]
  steps_goal = monitoring_analyzer.steps_goal
  t.cell(steps)

  intensity_minutes = weekly_intensity_minutes(monitoring_analyzer)
  t.cell(intensity_minutes)

  floors = monitoring_analyzer.total_floors
  floors_climbed = floors[:floors_climbed]
  t.cell(floors_climbed)
  t.new_row

  t.cell("#{percent(steps, steps_goal)} of daily goal #{steps_goal}")
  t.cell("#{percent(intensity_minutes, 150)} of weekly goal 150")
  t.cell("#{percent(floors_climbed, 10)} of daily goal 10")
  t.new_row

  t
end
daily_sleep_cycle_table(analyzer) click to toggle source
# File lib/postrunner/MonitoringStatistics.rb, line 122
def daily_sleep_cycle_table(analyzer)
  ti = FlexiTable.new
  ti.head
  ti.row([ 'Cycle', 'From', 'To', 'Duration', 'REM Sleep',
           'Light Sleep', 'Deep Sleep'])
  ti.body
  utc_offset = analyzer.utc_offset
  format = { :halign => :right }
  totals = Hash.new(0)
  last_to_time = nil
  analyzer.sleep_cycles.each_with_index do |c, idx|
    if last_to_time && c.from_time > last_to_time
      # We have a gap in the sleep cycles.
      ti.cell('Wake')
      cell_right_aligned(ti, prefix_for_time(last_to_time, analyzer) + time_as_hm(last_to_time, utc_offset))
      cell_right_aligned(ti, prefix_for_time(c.from_time, analyzer) + time_as_hm(c.from_time, utc_offset))
      cell_right_aligned(ti, "(#{secsToHM(c.from_time - last_to_time)})")
      ti.cell('')
      ti.cell('')
      ti.cell('')
      ti.new_row
    end

    ti.cell((idx + 1).to_s, format)
    ti.cell(prefix_for_time(c.from_time.localtime(utc_offset), analyzer) + c.from_time.localtime(utc_offset).strftime('%H:%M'), format)
    ti.cell(prefix_for_time(c.to_time.localtime(utc_offset), analyzer) + c.to_time.localtime(utc_offset).strftime('%H:%M'), format)

    duration = c.to_time - c.from_time
    totals[:duration] += duration
    ti.cell(secsToHM(duration), format)

    totals[:rem] += c.total_seconds[:rem]
    ti.cell(secsToHM(c.total_seconds[:rem]), format)

    light_sleep = c.total_seconds[:nrem1] + c.total_seconds[:nrem2]
    totals[:light_sleep] += light_sleep
    ti.cell(secsToHM(light_sleep), format)

    totals[:deep_sleep] += c.total_seconds[:nrem3]
    ti.cell(secsToHM(c.total_seconds[:nrem3]), format)

    ti.new_row
    last_to_time = c.to_time
  end
  ti.foot
  ti.cell('Totals')
  ti.cell(analyzer.sleep_cycles[0].from_time.localtime(utc_offset).
          strftime('%H:%M'), format)
  ti.cell(analyzer.sleep_cycles[-1].to_time.localtime(utc_offset).
          strftime('%H:%M'), format)
  ti.cell(secsToHM(totals[:duration]), format)
  ti.cell(secsToHM(totals[:rem]), format)
  ti.cell(secsToHM(totals[:light_sleep]), format)
  ti.cell(secsToHM(totals[:deep_sleep]), format)
  ti.new_row

  ti
end
daily_stats_table(monitoring_analyzer, sleep_analyzer) click to toggle source
# File lib/postrunner/MonitoringStatistics.rb, line 211
def daily_stats_table(monitoring_analyzer, sleep_analyzer)
  t = FlexiTable.new
  t.set_column_attributes(Array.new(4, { :halign => :center}))

  t.head
  t.row([ 'Distance', 'Calories', 'Floors descended',
          'Resting Heart Rate' ])

  t.body
  steps_distance_calories = monitoring_analyzer.steps_distance_calories
  t.cell("#{'%.1f' % (steps_distance_calories[:distance] / 1000.0)} km")

  t.cell("#{steps_distance_calories[:calories].to_i}")

  floors = monitoring_analyzer.total_floors
  t.cell("#{floors[:floors_descended]}")

  t.cell("#{sleep_analyzer.resting_heart_rate} BPM")

  t
end
monthly_goal_table(year, month, last_day_of_month) click to toggle source
# File lib/postrunner/MonitoringStatistics.rb, line 327
def monthly_goal_table(year, month, last_day_of_month)
  t = FlexiTable.new
  left = { :halign => :left }
  right = { :halign => :right }
  t.set_column_attributes([ left ] + [ right ] * 10)
  t.head
  t.row([ 'Day', 'Steps', '% of', 'Goal', 'Wk. Int.', '% of',
          'Floors', '% of', 'Floors', 'Dist.', 'Cals.' ])
  t.row([ '', '', 'Goal', 'Steps', 'Minutes', '150', 'clmbd.', '10',
          'descd.', 'm', 'kCal' ])
  t.body
  totals = Hash.new(0)
  counted_days = 0
  intensity_minutes_sum = 0
  1.upto(last_day_of_month).each do |dom|
    break if (time = Time.new(year, month, dom)) > Time.now

    day_str = time.strftime('%d %a')
    t.cell(day_str)

    analyzer = DailyMonitoringAnalyzer.new(@monitoring_files,
                                           "#{year}-#{month}-#{dom}")

    steps_distance_calories = analyzer.steps_distance_calories
    steps = steps_distance_calories[:steps]
    totals[:steps] += steps
    steps_goal = analyzer.steps_goal
    totals[:steps_goal] += steps_goal
    t.cell(steps)
    t.cell(percent(steps, steps_goal))
    t.cell(steps_goal)

    if dom == 1
      intensity_minutes = weekly_intensity_minutes(analyzer)
    else
      intensity_minutes_sum = 0 if time.wday == @first_day_of_week
      intensity_minutes =
        analyzer.intensity_minutes[:moderate_minutes] +
        2 * analyzer.intensity_minutes[:vigorous_minutes]
    end
    intensity_minutes_sum += intensity_minutes
    totals[:intensity_minutes] += intensity_minutes
    t.cell(intensity_minutes_sum.to_i)
    t.cell(percent(intensity_minutes_sum, 150))

    floors = analyzer.total_floors
    floors_climbed = floors[:floors_climbed]
    totals[:floors_climbed] += floors_climbed
    t.cell(floors_climbed)
    t.cell(percent(floors_climbed, 10))

    floors_descended = floors[:floors_descended]
    totals[:floors_descended] += floors_descended
    t.cell(floors_descended)


    distance = steps_distance_calories[:distance]
    totals[:distance] += distance
    t.cell(distance.to_i)

    calories = steps_distance_calories[:calories]
    totals[:calories] += calories
    t.cell(calories.to_i)

    t.new_row
    counted_days += 1
  end

  t.foot
  t.cell('Totals')
  t.cell(totals[:steps])
  t.cell('')
  t.cell(totals[:steps_goal])
  t.cell(totals[:intensity_minutes].to_i)
  t.cell('')
  t.cell(totals[:floors_climbed])
  t.cell('')
  t.cell(totals[:floors_descended])
  t.cell(totals[:distance].to_i)
  t.cell(totals[:calories].to_i)
  t.new_row

  if counted_days > 0
    t.cell('Averages')
    t.cell((totals[:steps] / counted_days).to_i)
    t.cell(percent(totals[:steps], totals[:steps_goal]))
    t.cell((totals[:steps_goal] / counted_days).to_i)
    t.cell((totals[:intensity_minutes] / counted_days).to_i)
    t.cell(percent(totals[:intensity_minutes], (counted_days / 7.0) * 150))
    t.cell((totals[:floors_climbed] / counted_days).to_i)
    t.cell(percent(totals[:floors_climbed] / counted_days, 10))
    t.cell((totals[:floors_descended] / counted_days).to_i)
    t.cell('%.0f' % (totals[:distance] / counted_days))
    t.cell((totals[:calories] / counted_days).to_i)
  end

  t
end
monthly_sleep_table(year, month, last_day_of_month) click to toggle source
# File lib/postrunner/MonitoringStatistics.rb, line 496
def monthly_sleep_table(year, month, last_day_of_month)
  t = FlexiTable.new
  left = { :halign => :left }
  right = { :halign => :right }
  t.set_column_attributes([ left ] + [ right ] * 6)
  t.head
  t.row([ 'Date', 'Total Sleep', 'Cycles', 'REM Sleep', 'Light Sleep',
          'Deep Sleep', 'RHR' ])
  t.body
  totals = Hash.new(0)
  counted_days = 0
  rhr_days = 0

  1.upto(last_day_of_month).each do |dom|
    break if (time = Time.new(year, month, dom)) > Time.now

    day_str = time.strftime('%d %a')
    t.cell(day_str)

    analyzer = DailySleepAnalyzer.new(@monitoring_files,
                                      "#{year}-#{month}-#{dom}",
                                      -12 * 60 * 60)

    if (analyzer.sleep_cycles.empty?)
      5.times { t.cell('-') }
    else
      totals[:total_sleep] += analyzer.total_sleep
      totals[:cycles] += analyzer.sleep_cycles.length
      totals[:rem_sleep] += analyzer.rem_sleep
      totals[:light_sleep] += analyzer.light_sleep
      totals[:deep_sleep] += analyzer.deep_sleep
      counted_days += 1

      t.cell(secsToHM(analyzer.total_sleep))
      t.cell(analyzer.sleep_cycles.length)
      t.cell(secsToHM(analyzer.rem_sleep))
      t.cell(secsToHM(analyzer.light_sleep))
      t.cell(secsToHM(analyzer.deep_sleep))
    end

    if (rhr = analyzer.resting_heart_rate) && rhr > 0
      t.cell(rhr)
      totals[:rhr] += rhr
      rhr_days += 1
    else
      t.cell('-')
    end
    t.new_row
  end
  t.foot
  t.cell('Averages')
  if counted_days > 0
    t.cell(secsToHM(totals[:total_sleep] / counted_days))
    t.cell('%.1f' % (totals[:cycles].to_f / counted_days))
    t.cell(secsToHM(totals[:rem_sleep] / counted_days))
    t.cell(secsToHM(totals[:light_sleep] / counted_days))
    t.cell(secsToHM(totals[:deep_sleep] / counted_days))
  else
    5.times { t.cell('-') }
  end
  if rhr_days > 0
    t.cell('%.0f' % (totals[:rhr] / rhr_days))
  else
    t.cell('-')
  end
  t.new_row

  t
end
percent(value, total) click to toggle source
# File lib/postrunner/MonitoringStatistics.rb, line 106
def percent(value, total)
  "#{'%.0f' % ((value * 100.0) / total)}%"
end
prefix_for_time(t, a) click to toggle source
# File lib/postrunner/MonitoringStatistics.rb, line 118
def prefix_for_time(t, a)
  a.window_start_time.strftime('%Y-%m-%d') == t.strftime('%Y-%m-%d') ? ' ' : '+'
end
time_as_hm(t, utc_offset) click to toggle source
# File lib/postrunner/MonitoringStatistics.rb, line 114
def time_as_hm(t, utc_offset)
  t.localtime(utc_offset).strftime('%H:%M')
end
weekly_goal_table(start_day) click to toggle source
# File lib/postrunner/MonitoringStatistics.rb, line 233
def weekly_goal_table(start_day)
  t = FlexiTable.new
  left = { :halign => :left }
  right = { :halign => :right }
  t.set_column_attributes([ left ] + [ right ] * 10)
  t.head
  t.row([ 'Day', 'Steps', '% of', 'Goal', 'Intens.', '% of',
          'Floors', '% of', 'Floors', 'Dist.', 'Cals.' ])
  t.row([ '', '', 'Goal', 'Steps', 'Minutes', '150', 'clmbd.', '10',
          'descd.', 'm', 'kCal' ])
  t.body
  totals = Hash.new(0)
  counted_days = 0
  intensity_minutes_sum = 0
  0.upto(7) do |dow|
    break if (time = start_day + 24 * 60 * 60 * dow) > Time.now

    day_str = time.strftime('%a')
    t.cell(day_str)

    analyzer = DailyMonitoringAnalyzer.new(@monitoring_files,
                                           time.strftime('%Y-%m-%d'))

    steps_distance_calories = analyzer.steps_distance_calories
    steps = steps_distance_calories[:steps]
    totals[:steps] += steps
    steps_goal = analyzer.steps_goal
    totals[:steps_goal] += steps_goal
    t.cell(steps)
    t.cell(percent(steps, steps_goal))
    t.cell(steps_goal)

    intensity_minutes =
      analyzer.intensity_minutes[:moderate_minutes] +
      2 * analyzer.intensity_minutes[:vigorous_minutes]
    intensity_minutes_sum += intensity_minutes
    totals[:intensity_minutes] += intensity_minutes
    t.cell(intensity_minutes.to_i)
    t.cell(percent(intensity_minutes_sum, 150))

    floors = analyzer.total_floors
    floors_climbed = floors[:floors_climbed]
    totals[:floors_climbed] += floors_climbed
    t.cell(floors_climbed)
    t.cell(percent(floors_climbed, 10))

    floors_descended = floors[:floors_descended]
    totals[:floors_descended] += floors_descended
    t.cell(floors_descended)


    distance = steps_distance_calories[:distance]
    totals[:distance] += distance
    t.cell(distance.to_i)

    calories = steps_distance_calories[:calories]
    totals[:calories] += calories
    t.cell(calories.to_i)

    t.new_row
    counted_days += 1
  end

  t.foot
  t.cell('Totals')
  t.cell(totals[:steps])
  t.cell('')
  t.cell(totals[:steps_goal])
  t.cell(totals[:intensity_minutes].to_i)
  t.cell('')
  t.cell(totals[:floors_climbed])
  t.cell('')
  t.cell(totals[:floors_descended])
  t.cell(totals[:distance].to_i)
  t.cell(totals[:calories].to_i)
  t.new_row

  if counted_days > 0
    t.cell('Averages')
    t.cell((totals[:steps] / counted_days).to_i)
    t.cell(percent(totals[:steps], totals[:steps_goal]))
    t.cell((totals[:steps_goal] / counted_days).to_i)
    t.cell((totals[:intensity_minutes] / counted_days).to_i)
    t.cell(percent(totals[:intensity_minutes], (counted_days / 7.0) * 150))
    t.cell((totals[:floors_climbed] / counted_days).to_i)
    t.cell(percent(totals[:floors_climbed] / counted_days, 10))
    t.cell((totals[:floors_descended] / counted_days).to_i)
    t.cell('%.0f' % (totals[:distance] / counted_days))
    t.cell((totals[:calories] / counted_days).to_i)
  end

  t
end
weekly_intensity_minutes(monitoring_analyzer) click to toggle source
# File lib/postrunner/MonitoringStatistics.rb, line 566
def weekly_intensity_minutes(monitoring_analyzer)
  current_date = monitoring_analyzer.window_start_time.localtime

  intensity_minutes = 0
  # Get intensity minutes for previous days of the current week.
  if current_date.wday != @first_day_of_week
    1.upto(5) do |i|
      date = current_date - 24 * 60 * 60 * i

      ma = DailyMonitoringAnalyzer.new(@monitoring_files,
                                       date.strftime('%Y-%m-%d'))
      intensity_minutes +=
        ma.intensity_minutes[:moderate_minutes] +
        2 * ma.intensity_minutes[:vigorous_minutes]

      break if date.wday == @first_day_of_week
    end
  end

  # Finally add the intensity minutes of the current day.
  intensity_minutes +=
    monitoring_analyzer.intensity_minutes[:moderate_minutes] +
    2 * monitoring_analyzer.intensity_minutes[:vigorous_minutes]

  intensity_minutes
end
weekly_sleep_table(start_day) click to toggle source
# File lib/postrunner/MonitoringStatistics.rb, line 426
def weekly_sleep_table(start_day)
  t = FlexiTable.new
  left = { :halign => :left }
  right = { :halign => :right }
  t.set_column_attributes([ left ] + [ right ] * 6)
  t.head
  t.row([ 'Date', 'Total Sleep', 'Cycles', 'REM Sleep', 'Light Sleep',
          'Deep Sleep', 'RHR' ])
  t.body
  totals = Hash.new(0)
  counted_days = 0
  rhr_days = 0

  0.upto(7) do |dow|
    break if (time = start_day + 24 * 60 * 60 * dow) > Time.now

    day_str = time.strftime('%a')
    t.cell(day_str)

    analyzer = DailySleepAnalyzer.new(@monitoring_files,
                                      time.strftime('%Y-%m-%d'),
                                      -12 * 60 * 60)

    if (analyzer.sleep_cycles.empty?)
      5.times { t.cell('-') }
    else
      totals[:total_sleep] += analyzer.total_sleep
      totals[:cycles] += analyzer.sleep_cycles.length
      totals[:rem_sleep] += analyzer.rem_sleep
      totals[:light_sleep] += analyzer.light_sleep
      totals[:deep_sleep] += analyzer.deep_sleep
      counted_days += 1

      t.cell(secsToHM(analyzer.total_sleep))
      t.cell(analyzer.sleep_cycles.length)
      t.cell(secsToHM(analyzer.rem_sleep))
      t.cell(secsToHM(analyzer.light_sleep))
      t.cell(secsToHM(analyzer.deep_sleep))
    end

    if (rhr = analyzer.resting_heart_rate) && rhr > 0
      t.cell(rhr)
      totals[:rhr] += rhr
      rhr_days += 1
    else
      t.cell('-')
    end
    t.new_row
  end
  t.foot
  t.cell('Averages')
  if counted_days > 0
    t.cell(secsToHM(totals[:total_sleep] / counted_days))
    t.cell('%.1f' % (totals[:cycles].to_f / counted_days))
    t.cell(secsToHM(totals[:rem_sleep] / counted_days))
    t.cell(secsToHM(totals[:light_sleep] / counted_days))
    t.cell(secsToHM(totals[:deep_sleep] / counted_days))
  else
    5.times { t.cell('-') }
  end
  if rhr_days > 0
    t.cell('%.0f' % (totals[:rhr] / rhr_days))
  else
    t.cell('-')
  end
  t.new_row

  t
end