m26-js
v1.0.0
Published
A Node.js library for speed and pace calculations for sports like running and cycling. Age-graded times and heart-rate training-zones are also supported.
Downloads
56
Maintainers
Readme
m26-js
Purpose
A Node.js library for speed and pace calculations for sports like running, swimming, and cycling. Age-graded times and heart-rate training-zones are also supported.
Setup
Add m26-js to your project or package.json file:
npm install m26-js
Examples
This library is now implemented with TypeScript, and these examples are also in TypeScript.
Import the m26-js classes into your TypeScript code:
import {
Age,
AgeCalculator,
Constants,
Distance,
ElapsedTime,
RunWalkCalculation,
RunWalkCalculator,
Speed,
TrainingZone
} from "m26-js";
Class Constants
Class Constants defines the following:
public static LIB_NAME : string = 'm26-js';
public static LIB_VERSION : string = '1.0.0';
public static LIB_AUTHOR : string = 'Chris Joakim';
public static LIB_LICENSE : string = 'MIT';
public static UOM_MILES : string = 'm';
public static UOM_KILOMETERS : string = 'k';
public static UOM_YARDS : string = 'y';
public static UNITS_OF_MEASURE : string[] = [
Constants.UOM_MILES, Constants.UOM_KILOMETERS, Constants.UOM_YARDS];
public static KILOMETERS_PER_MILE : number = 1.609344;
public static MILES_PER_KILOMETER : number = 0.621371192237334;
public static YARDS_PER_KILOMETER : number = 1093.6132983377076;
public static FEET_PER_KILOMETER : number = 3280.839895013123;
public static FEET_PER_METER : number = 3.280839895013123;
public static YARDS_PER_MILE : number = 1760.0;
public static SECONDS_PER_MINUTE : number = 60.0;
public static SECONDS_PER_HOUR : number = 3600.0;
The remaining code examples are in the form of working unit tests.
Class Age
test("Age: maxPulse", () => {
let a19 = new Age(19.1);
let a66 = new Age(66);
expect(a19.value).toBe(19.1);
expect(a66.value).toBe(66);
expect(a19.maxPulse()).toBe(200);
expect(a66.maxPulse()).toBe(220 - 66);
});
test("Age: add", () => {
let a1 = new Age(19.1);
let a2 = new Age(66);
a1.add(a2);
expect(a1.value).toBe(85.1);
expect(a2.value).toBe(66);
});
test("Age: subtract", () => {
let a1 = new Age(19.1);
let a2 = new Age(66);
a2.subtract(a1);
expect(a1.value).toBe(19.1);
expect(a2.value).toBe(66 - 19.1);
});
test("Age: maxPulse and trainingZones", () => {
let a1 = new Age(19.1);
let a2 = new Age(66);
expect(a1.maxPulse()).toBe(200);
expect(a2.maxPulse()).toBe(154);
expect(a2.maxPulse()).toBe(220 - 66);
// first case; age 19.1
let zones = a1.trainingZones();
//console.log(zones);
expect(zones.length).toBe(5);
expect(zones[0].zone).toBe(1);
expect(zones[0].pct).toBe(0.75);
expect(zones[0].age).toBe(19.1);
expect(zones[0].maxHeartRate).toBe(a1.maxPulse());
expect(zones[0].zoneHeartRate).toBe(150);
expect(zones[4].zone).toBe(5);
expect(zones[4].pct).toBe(0.95);
expect(zones[4].age).toBe(19.1);
expect(zones[4].maxHeartRate).toBe(a1.maxPulse());
expect(zones[4].zoneHeartRate).toBe(190);
// second case; age 66
zones = a2.trainingZones();
//console.log(zones);
expect(zones.length).toBe(5);
expect(zones[0].zone).toBe(1);
expect(zones[0].pct).toBe(0.75);
expect(zones[0].age).toBe(66);
expect(zones[0].maxHeartRate).toBe(a2.maxPulse());
expect(zones[0].zoneHeartRate).toBe(116);
expect(zones[4].zone).toBe(5);
expect(zones[4].pct).toBe(0.95);
expect(zones[4].age).toBe(66);
expect(zones[4].maxHeartRate).toBe(a2.maxPulse());
expect(zones[4].zoneHeartRate).toBe(146);
});
Class AgeCalculator
test("AgeCalculator: calculate", () => {
let ac : AgeCalculator = new AgeCalculator();
let age1 : Age = ac.calculate('1960-10-01', '2014-10-01');
let age2 : Age = ac.calculate('2001-09-11', '2023-09-17');
expect(age1.value).toBeCloseTo(53.998631074606436, 0.00001);
expect(age2.value).toBeCloseTo(22.015058179329227, 0.00001);
});
Class Distance
test("Distance: constructor default uom", () => {
let d = new Distance(26.2);
expect(d.value).toBe(26.2);
expect(d.uom).toBe(Constants.UOM_MILES);
expect(d.asMiles()).toBe(26.2);
expect(d.asKilometers()).toBe(42.1648128);
expect(d.asYards()).toBe(46112.0);
});
test("Distance: constructor miles uom", () => {
let d = new Distance(26.2, Constants.UOM_MILES);
expect(d.value).toBe(26.2);
expect(d.uom).toBe(Constants.UOM_MILES);
expect(d.asMiles()).toBe(26.2);
expect(d.asKilometers()).toBe(42.1648128);
expect(d.asYards()).toBe(46112.0);
});
test("Distance: constructor kilometers uom", () => {
let d = new Distance(10.0, Constants.UOM_KILOMETERS);
expect(d.value).toBe(10.0);
expect(d.uom).toBe(Constants.UOM_KILOMETERS);
expect(d.asMiles()).toBe(6.2137119223733395);
expect(d.asKilometers()).toBe(10.0);
expect(d.asYards()).toBe(10936.132983377078);
});
test("Distance: constructor yards uom", () => {
let d = new Distance(1800, Constants.UOM_YARDS);
expect(d.value).toBe(1800);
expect(d.uom).toBe(Constants.UOM_YARDS);
expect(d.asMiles()).toBe(1.0227272727272727);
expect(d.asKilometers()).toBe(1.64592);
expect(d.asYards()).toBe(1800);
});
test("Distance: add", () => {
let d1 = new Distance(26.2);
let d2 = new Distance(4.8);
let d3 = d1.add(d2);
expect(d3.uom).toBe(Constants.UOM_MILES);
expect(d3.asMiles()).toBe(31.0);
});
test("Distance: subtract", () => {
let d1 = new Distance(26.2);
let d2 = new Distance(10.0, 'k');
let d3 = d1.subtract(d2);
expect(d3.uom).toBe(Constants.UOM_MILES);
expect(d3.asMiles()).toBe(19.98628807762666);
});
Class ElapsedTime
test("ElapsedTime: constructor with a number of seconds", () => {
let et = new ElapsedTime(' 3675 ');
expect(et.arg).toBe('3675');
expect(et.asHHMMSS()).toBe('01:01:15');
expect(et.hh).toBe(1);
expect(et.mm).toBe(1);
expect(et.ss).toBe(15);
expect(et.secs).toBe(3675);
});
test("ElapsedTime: constructor with a hh:mm:ss string", () => {
let et = new ElapsedTime('1:1:15');
expect(et.arg).toBe('1:1:15');
expect(et.asHHMMSS()).toBe('01:01:15');
expect(et.hh).toBe(1);
expect(et.mm).toBe(1);
expect(et.ss).toBe(15);
expect(et.secs).toBe(3675);
});
test("ElapsedTime: constructor with a mm:ss string", () => {
let et = new ElapsedTime('42:01');
expect(et.arg).toBe('42:01');
expect(et.asHHMMSS()).toBe('00:42:01');
expect(et.hh).toBe(0);
expect(et.mm).toBe(42);
expect(et.ss).toBe(1);
expect(et.secs).toBe((42 * 60) + 1);
});
test("ElapsedTime: constructor with a ss string", () => {
let et = new ElapsedTime('59');
expect(et.arg).toBe('59');
expect(et.asHHMMSS()).toBe('00:00:59');
expect(et.hh).toBe(0);
expect(et.mm).toBe(0);
expect(et.ss).toBe(59);
expect(et.secs).toBe(59);
});
test("ElapsedTime: constructor with an empty string", () => {
let et = new ElapsedTime('');
expect(et.arg).toBe('');
expect(et.asHHMMSS()).toBe('00:00:00');
expect(et.hh).toBe(0);
expect(et.mm).toBe(0);
expect(et.ss).toBe(0);
expect(et.secs).toBe(0);
});
test("ElapsedTime: constructor with a malformed string", () => {
// garbage-in, garbage-out
let et = new ElapsedTime('?what!');
expect(et.arg).toBe('?what!');
expect(et.asHHMMSS()).toBe('NaN:NaN:NaN');
});
Class Speed
test("Speed: property formats ss second values as nn.nn strings", () => {
let d1 = new Distance(1.0);
let et1 = new ElapsedTime('8:00');
let s1 = new Speed(d1, et1);
expect(s1.formattedSeconds(0)).toBe('00.00');
expect(s1.formattedSeconds(1)).toBe('01.00');
expect(s1.formattedSeconds(17.76)).toBe('17.76');
expect(s1.formattedSeconds(59.90)).toBe('59.90');
expect(s1.formattedSeconds(59.99)).toBe('59.99');
});
test("Speed: calculates a 2-mile walk, with round numbers", () => {
let d1 = new Distance(2.0);
let et1 = new ElapsedTime('30:00');
let s1 = new Speed(d1, et1);
expect(s1.secondsPerMile()).toBe(900);
expect(s1.pacePerMile()).toBe('15:00.00');
});
test("Speed: calculates a marathon with fractional seconds pace", () => {
let d1 = new Distance(26.2);
let et1 = new ElapsedTime('3:47:30');
let s1 = new Speed(d1, et1);
expect(s1.secondsPerMile()).toBe(520.9923664122138);
expect(s1.pacePerMile()).toBe('8:40.99');
});
test("Speed: project a time using simple linear formula", () => {
let d1 = new Distance(10.0);
let d2 = new Distance(20.0);
let et1 = new ElapsedTime('1:30:00');
let s1 = new Speed(d1, et1);
expect(s1.secondsPerMile()).toBe(540.0);
expect(s1.pacePerMile()).toBe('9:00.00');
let hhmmss = s1.projectedTime(d2);
expect(hhmmss).toBe('03:00:00');
});
test("Speed: project a time using the exponential riegel formula", () => {
let d1 = new Distance(10.0);
let d2 = new Distance(20.0);
let et1 = new ElapsedTime('1:30:00');
let s1 = new Speed(d1, et1);
expect(s1.secondsPerMile()).toBe(540.0);
expect(s1.pacePerMile()).toBe('9:00.00');
let hhmmss = s1.projectedTime(d2, 'riegel');
expect(hhmmss).toBe('03:07:38');
});
test("Speed: calculated age-graded speeds", () => {
let d = new Distance(26.2);
let et = new ElapsedTime('3:47:30');
let s1 = new Speed(d, et);
let a1 = new Age(42.5);
let a2 = new Age(43.5);
let a3 = new Age(57.1);
expect(s1.mph()).toBe(6.90989010989011);
let s2 = s1.ageGraded(a1, a2);
let s3 = s1.ageGraded(a1, a3);
expect(s2.mph()).toBe(6.871129889997814);
expect(s3.mph()).toBe(6.341693000739595);
});
Class RunWalkCalculator
test("RunWalkCalculator: calculate with all walking, no running", () => {
let calculator = new RunWalkCalculator()
let run_hhmmss = '00:00';
let run_ppm = '9:00';
let walk_hhmmss = '10:00';
let walk_ppm = '18:00';
let miles = 3.333;
let result : RunWalkCalculation =
calculator.calculate(run_hhmmss, run_ppm, walk_hhmmss, walk_ppm, miles);
// inputs
expect(result.runHhmmss).toBe(run_hhmmss);
expect(result.runPpm).toBe(run_ppm);
expect(result.walkHhmmss).toBe(walk_hhmmss);
expect(result.walkPpm).toBe(walk_ppm);
expect(result.miles).toBe(miles);
// calculated
expect(result.avgMph).toBeCloseTo(3.333333, 0.0001);
expect(result.avgPpm).toBe('18:00.00');
expect(result.projectedTime).toBe('00:59:59');
expect(result.projectedMiles).toBeCloseTo(3.333, 0.001);
});
test("RunWalkCalculator: calculate with all running, no walking", () => {
let calculator = new RunWalkCalculator()
let run_hhmmss = '10:00';
let run_ppm = '9:00';
let walk_hhmmss = '0:00';
let walk_ppm = '18:00';
let miles = 3.333;
let result : RunWalkCalculation =
calculator.calculate(run_hhmmss, run_ppm, walk_hhmmss, walk_ppm, miles);
// inputs
expect(result.runHhmmss).toBe(run_hhmmss);
expect(result.runPpm).toBe(run_ppm);
expect(result.walkHhmmss).toBe(walk_hhmmss);
expect(result.walkPpm).toBe(walk_ppm);
expect(result.miles).toBe(miles);
// calculated
expect(result.avgMph).toBeCloseTo(6.666666666666667, 0.00001);
expect(result.avgPpm).toBe('9:00.00');
expect(result.projectedTime).toBe('00:29:59');
expect(result.projectedMiles).toBeCloseTo(3.333, 0.001);
});
test("RunWalkCalculator: calculate a marathon with a 9:1 ratio of running to walking", () => {
let calculator = new RunWalkCalculator()
let run_hhmmss = '9:00';
let run_ppm = '9:00';
let walk_hhmmss = '1:00';
let walk_ppm = '18:00';
let miles = 26.2;
let result : RunWalkCalculation =
calculator.calculate(run_hhmmss, run_ppm, walk_hhmmss, walk_ppm, miles);
// inputs
expect(result.runHhmmss).toBe(run_hhmmss);
expect(result.runPpm).toBe(run_ppm);
expect(result.walkHhmmss).toBe(walk_hhmmss);
expect(result.walkPpm).toBe(walk_ppm);
expect(result.miles).toBe(miles);
// calculated
expect(result.avgMph).toBeCloseTo(6.060606, 0.000001);
expect(result.avgPpm).toBe('9:54.00');
expect(result.projectedTime).toBe('04:19:22');
expect(result.projectedMiles).toBeCloseTo(26.2, 0.001);
});
Release History
- 2023-09-19 - 1.0.0 - Re-implemented with TypeScript.
- 2015-08-04 - 0.4.0 - Added class RunWalkCalculator, and ported the project back to CoffeeScript.
- 2015-05-13 - 0.3.2 - Repackaged again, from a single merged m26-js.ts file. Tests enhanced.
- 2015-05-12 - 0.3.1 - Renamed lib from m26.js to m26-js.js, etc, and repackaged.
- 2015-05-12 - 0.3.0 - Project ported from CoffeeScript to Typescript, with m26.d.ts typings file.
- 2014-11-09 - 0.2.0 - Corrected the English-to-Metric and Metric-to-English conversions.
- 2014-11-06 - 0.1.5 - Fix require statement in examples documentation.
- 2014-11-05 - 0.1.4 - Removed a spec_helper function.
- 2014-11-02 - 0.1.3 - Added Age.training_zones()
- 2014-11-01 - 0.1.2 - Added Speed.age_graded()
- 2014-11-01 - 0.1.1 - Added Speed.projected_time(), Age, AgeCalculator.
- 2014-11-01 - 0.1.0 - Initial working version.
- 2014-11-01 - 0.0.3 - alpha 3
- 2014-11-01 - 0.0.2 - alpha 2
- 2014-11-01 - 0.0.1 - alpha 1