Karvonen Heart Rate using C# and Spectre.Console

5 minute read article dotnet   microsoft   advent   spectre.console   csharp Comments

Note: This post is part of the .NET Advent 2022! Check out the rest of the entries here!

As we approach the new year, many people are probably starting to think about getting back into an exercise program. It doesn't matter if you're biking, running, walking, or lifting weights; you need to ensure you're doing it healthfully. I tend to rush into things thinking I'm still 20 when I'm not, which results in enough soreness to make me quit, OR even worse, an injury. As I get older, it's really important that I stay active, but I also want to make sure I'm not overdoing it.

For some background on heart rate, check out How’s your heart rate and why it matters?

So what does all that have to do with .NET? I'm a huge fan of Brian P. Hogan's Exercises for Programmers published back in 2015. I've done several exercises in the book over the years and even did one for the Second Annual C# Advent in 2018. This year one exercise in particular looked fun: #31 - Karvonen Heart Rate.

From the exercise

When getting into a fitness program, you may want to figure out your target heart rate so you don’t overexert yourself. The Karvonen heartrate formula is one method you can use to determine your rate. Create a program that prompts for your age and your resting heart rate. Use the Karvonen formula to determine the target heart rate based on a range of intensities from 55% to 95%. Generate a table with the results as shown in the example output.

I decided to tackle this one as a C# console application, but since I've heard so much about Spectre.Console, I also wanted to use it. I am using .NET 7, but there's nothing specific to .NET 7 or C# 11 in the solution I'm about to present.

The requirements:

  1. Create a program that prompts for your age and your resting heart rate.
  2. Use the Karvonen formula to determine the target heart rate based on a range of intensities from 55% to 95%.
  3. Generate a table with the results as shown in the example output.

You can start by creating a console application:

dotnet new console -o karvonen
To make sure everything was created correctly, change into the karvonen folder and run
dotnet run
You should see something like this

The console output from running dotnet new and dotnet run.

The next step is to add Spectre.Console to the project.

dotnet add package Spectre.Console

In order to prompt for the age and resting heart rate, you can use Spectre.Console instead of rolling your own input-handling code. In the simplest case, you can write something like this (after adding 'using Spectre.Console;'):

var age = AnsiConsole.Ask<int>("What's your [green]age[/]?");
Running the program now gives you a prompt that forces you to enter an integer value, but it doesn't do any validation - in other words, you can enter any valid integer. That doesn't really make sense for age, so to put validation on the input and ensure the user enters valid ages, the code would look like this:
var age = AnsiConsole.Prompt(
  new TextPrompt<int>("How old are you?")
    .PromptStyle("green")
    .ValidationErrorMessage("[red]That's not a valid age[/]")
    .Validate(age =>
        age switch
        {
            <= 16 => ValidationResult.Error("[red]You must at least be 16 years old.[/]"),
            >= 101 => ValidationResult.Error("[red]You must be younger than 100.[/]"),
            _ => ValidationResult.Success(),
        })
    );
In this case, the age must be between 16 and 100, anything else results in an error.

To request the resting heart rate, you can do something similar:

var restingHeartRate = AnsiConsole.Prompt(
  new TextPrompt<int>("What is your resting heart rate?")
      .PromptStyle("green")
      .ValidationErrorMessage("[red]That's not a valid heart rate[/]")
      .Validate(rhr =>
          rhr switch
          {
              <= 20 => ValidationResult.Error("[red]Your resting heart rate should be greater than 20.[/]"),
              >= 101 => ValidationResult.Error("[red]Your resting heart rate should NOT be greater than 100.[/]"),
              _ => ValidationResult.Success(),
          })
      );
Feel free to change the validation to allow a different range of numbers to see how it all works.

That hits the first requirement, so now on to using those values and calculating the data that will be presented in the final step. The formula for Karvonen is:

target = (((220 - age) - restingHeartRate) * intensity) + restingHeartRate
Because the requirement says to determine the target based on a range of intensities, a loop will work although I'm sure someone more clever can come up with a better way. Instead of showing every single intensity between 55 and 95, you'll only show every 5 starting at 55 and ending at 95.
decimal lowerBounds = 55m;
decimal upperBounds = 95m;
int step = 5;

for(decimal counter = lowerBounds; counter <= upperBounds; counter += step)
{
  var intensity = counter / 100;

  var target = (int)(((220 - age - restingHeartRate) * intensity) + restingHeartRate);
  Console.WriteLine($"Intensity: {intensity:P0} : {target} bpm");
}

Ok, so, requirements one and two are done, now to get the output in a tabular format. For this you can go back to Spectre.Console. It's such a great package that makes this kind of thing really easy!

var table = new Table();
table.AddColumn("Intensity");
table.AddColumn("Rate");
A grid with two columns, one for intensity and one for rate.

Going back to the loop where the calculation was performed, you can get rid of the Console.WriteLine and add a row to the table for each calculated target:

decimal lowerBounds = 55m;
decimal upperBounds = 95m;
int step = 5;

var table = new Table();
table.AddColumn("Intensity");
table.AddColumn("Rate");

for(decimal counter = lowerBounds; counter <= upperBounds; counter += step)
{
  var intensity = counter / 100;

  var target = (int)(((220 - age - restingHeartRate) * intensity) + restingHeartRate);
  table.AddRow($"{intensity:P0}", $"{target} bpm");
}
AnsiConsole.Write(table);
That produces this:

A grid with two columns, intensity and rate, with the heart rate for each intensity between 55% and 95%.

As a bonus, you can add the very simple calculation for max heart rate:

Console.WriteLine($"Your maximum heart rate: {220 - age}");
Now, from start to finish, here's the output:
The program running in the terminal, prompting for age and resting heart rate, and finally display the results.

This was a fun exercise, and I can certainly think of ways to refactor the code, but for now, I'm happy with it. I hope you enjoyed this one as much as I did.

Make sure you check out the code on GitHub!

Updated:

Comments