From 295f8a635284eeaf91f313fb6d1e6d1b053948c0 Mon Sep 17 00:00:00 2001 From: Shane Jaroch Date: Mon, 8 Aug 2022 17:37:12 -0400 Subject: [PATCH] release-0.2.6 (#15) resolve duplicated code around colorama init add input validation. non-negative weight & reps add quadratic term to brzycki equation dos remedios full rep range; drop test & exc logic add .wls script for regression fits --- .gitattributes | 1 + CHANGELOG.rst | 25 +++++++-- MANIFEST.in | 1 + ntclient/__init__.py | 46 +++++----------- ntclient/argparser/__init__.py | 2 +- ntclient/argparser/funcs.py | 16 +++--- ntclient/resources/math/1rm-regressions.wls | 43 +++++++++++++++ ntclient/services/analyze.py | 10 ++-- ntclient/services/calculate.py | 37 ++++++++----- ntclient/utils/colors.py | 58 +++++++++++++++++++++ ntclient/utils/tree.py | 33 ++++-------- tests/services/test_calculate.py | 37 ++++--------- tests/test_cli.py | 13 +++-- 13 files changed, 202 insertions(+), 120 deletions(-) create mode 100644 .gitattributes create mode 100755 ntclient/resources/math/1rm-regressions.wls diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..28924fe --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.wls linguist-language=Mathematica diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bcebacb..41bc5c1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,19 +9,20 @@ and this project adheres to `Semantic Versioning None: """BMR, 1 rep-max, and other calculators""" calc_parser = subparsers.add_parser( - "calc", help="find you 1 rep max, body fat, BMR" + "calc", help="calculate 1-rep max, body fat, BMR, etc." ) calc_subparsers = calc_parser.add_subparsers(title="recipe subcommands") diff --git a/ntclient/argparser/funcs.py b/ntclient/argparser/funcs.py index 7890169..a9ac713 100644 --- a/ntclient/argparser/funcs.py +++ b/ntclient/argparser/funcs.py @@ -110,6 +110,13 @@ def calc_1rm(args: argparse.Namespace) -> tuple: reps = int(args.reps) print("Reps: %s" % reps) + if weight < 0: + print("ERROR: weight must be greater than zero") + return 1, None + if reps < 1 or reps > 20: + print("ERROR: reps must be between 1 and 20") + return 1, None + _epley = calc.orm_epley(weight, reps) _brzycki = calc.orm_brzycki(weight, reps) _dos_remedios = calc.orm_dos_remedios(weight, reps) @@ -122,18 +129,13 @@ def calc_1rm(args: argparse.Namespace) -> tuple: for _rep in _epley.keys(): row = [_rep] for _calc, _values in result.items(): - try: - # Round down for now - row.append(int(_values[_rep])) - except KeyError: - row.append(None) + # Round down for now + row.append(int(_values[_rep])) _all.append(row) # Print results print() print("Results for: epley, brzycki, and dos_remedios") - if "errMsg" in _dos_remedios: - print("WARN: Dos Remedios failed: %s" % _dos_remedios["errMsg"]) print() _table = tabulate(_all, headers=["n", "epl", "brz", "rmds"]) print(_table) diff --git a/ntclient/resources/math/1rm-regressions.wls b/ntclient/resources/math/1rm-regressions.wls new file mode 100755 index 0000000..962ea89 --- /dev/null +++ b/ntclient/resources/math/1rm-regressions.wls @@ -0,0 +1,43 @@ +#!/usr/bin/env wolframscript +(* ::Package:: *) + +(* ::Input:: *) +(*(* Epley *)*) +(*epley[n_]:=30/(29 +n)*) +(**) +(*(* Brzycki *)*) +(*(*brzLin[n_]:=(37-n)/36*)*) +(*brz[n_]:=(37-n+0.005n^2)/36*) +(**) +(*(* Dos Remedios *)*) +(*dosPts={{1,1},{2,0.92},{3,0.9},{5,0.87},{6,0.82},{8,0.75},{10,0.7},{12,0.65},{15,0.6},{20,0.55}};*) +(*(*ListLinePlot[{dosPts}, Filling->Axis]*)*) +(*dos:=Interpolation[dosPts]*) +(*(*dos[2]*)*) +(*(*epley[20.]*) +(*brz[20]*) +(*dos[20]*)*) +(*Plot[\!\(\**) +(*TagBox[*) +(*RowBox[{"{", *) +(*RowBox[{*) +(*RowBox[{"epley", "[", "n", "]"}], ",", *) +(*RowBox[{"brz", "[", "n", "]"}], ",", *) +(*RowBox[{"dos", "[", "n", "]"}]}], "}"}],*) +(*Short[#, 2]& ]\),{n,1,20}, AxesLabel->{"# of reps", "% of 1-rep max"},PlotLegends->"Expressions"]*) +(*dos[4]*) +(*dos[7]*) +(*dos[9]*) +(*dos[11]*) +(*dos[12] (* Known Value *)*) +(*dos[13]*) +(*dos[14]*) +(*dos[15] (* Known Value *)*) +(*dos[16]*) +(*dos[17]*) +(*dos[18]*) +(*dos[19]*) +(*dos[20]*) + + + diff --git a/ntclient/services/analyze.py b/ntclient/services/analyze.py index 738a3df..4b533db 100644 --- a/ntclient/services/analyze.py +++ b/ntclient/services/analyze.py @@ -213,7 +213,7 @@ def day_format(analysis: dict, nutrients: dict, buffer: int = 0) -> None: print("~~~~~~~~~~~~~~~~~~~~~~~~~~~") print("--> %s" % header) print("~~~~~~~~~~~~~~~~~~~~~~~~~~~") - print(CLI_CONFIG.color_reset_all) + print(CLI_CONFIG.style_reset_all) def print_macro_bar( _fat: float, _net_carb: float, _pro: float, _kcals_max: float, _buffer: int = 0 @@ -242,7 +242,7 @@ def day_format(analysis: dict, nutrients: dict, buffer: int = 0) -> None: + c_buf + CLI_CONFIG.color_red + p_buf - + CLI_CONFIG.color_reset_all + + CLI_CONFIG.style_reset_all ) # Bars @@ -250,7 +250,7 @@ def day_format(analysis: dict, nutrients: dict, buffer: int = 0) -> None: print(CLI_CONFIG.color_yellow + "=" * n_fat, end="") print(CLI_CONFIG.color_blue + "=" * n_car, end="") print(CLI_CONFIG.color_red + "=" * n_pro, end="") - print(CLI_CONFIG.color_reset_all + ">") + print(CLI_CONFIG.style_reset_all + ">") # Calorie footers k_fat = str(round(fat * 9)) @@ -267,7 +267,7 @@ def day_format(analysis: dict, nutrients: dict, buffer: int = 0) -> None: + c_buf + CLI_CONFIG.color_red + p_buf - + CLI_CONFIG.color_reset_all + + CLI_CONFIG.style_reset_all ) def print_nute_bar(_n_id: int, amount: float, _nutrients: dict) -> tuple: @@ -299,7 +299,7 @@ def day_format(analysis: dict, nutrients: dict, buffer: int = 0) -> None: print(" {0}<".format(color), end="") print("=" * left_pos + " " * (left_index - left_pos) + ">", end="") print(" {0}%\t[{1}]".format(perc, detail_amount), end="") - print(CLI_CONFIG.color_reset_all) + print(CLI_CONFIG.style_reset_all) return True, perc diff --git a/ntclient/services/calculate.py b/ntclient/services/calculate.py index 7bbda91..15a636b 100644 --- a/ntclient/services/calculate.py +++ b/ntclient/services/calculate.py @@ -33,7 +33,7 @@ def orm_epley(weight: float, reps: float) -> dict: return round(_un_rounded_result, 1) def weight_max_reps(target_reps: float) -> float: - _un_rounded_result = one_rm() / (1 + (target_reps - 1) / 30) + _un_rounded_result = one_rm() * 30 / (29 + target_reps) return round(_un_rounded_result, 1) maxes = {n_reps: weight_max_reps(n_reps) for n_reps in common_n_reps} @@ -43,21 +43,25 @@ def orm_epley(weight: float, reps: float) -> dict: def orm_brzycki(weight: float, reps: float) -> dict: """ Returns a dict {n_reps: max_weight, ...} - for n_reps: (1, 2, 3, 5, 6, 8, 10, 12, 15) + for n_reps: (1, 2, 3, 5, 6, 8, 10, 12, 15, 20) 1 RM = weight * 36 / (37 - reps) + NOTE: Adjusted formula is below, with quadratic term. + + 1 RM = weight * 36 / (37 - reps + 0.005 * reps^2) + Source: https://workoutable.com/one-rep-max-calculator/ """ def _one_rm() -> float: - _un_rounded_result = weight * 36 / (37 - reps) + _un_rounded_result = weight * 36 / (37 - reps + 0.005 * reps**2) return round(_un_rounded_result, 1) one_rm = _one_rm() def weight_max_reps(target_reps: float) -> float: - _un_rounded_result = one_rm * (37 - target_reps) / 36 + _un_rounded_result = one_rm * (37 - target_reps + 0.005 * target_reps**2) / 36 return round(_un_rounded_result, 1) maxes = {n_reps: weight_max_reps(n_reps) for n_reps in common_n_reps} @@ -67,7 +71,7 @@ def orm_brzycki(weight: float, reps: float) -> dict: def orm_dos_remedios(weight: float, reps: int) -> dict: """ Returns dict {n_reps: max_weight, ...} - for n_reps: (1, 2, 3, 5, 6, 8, 10, 12, 15) + for n_reps: (1, 2, 3, 5, 6, 8, 10, 12, 15, 20) Or an {"errMsg": "INVALID_RANGE", ...} @@ -79,12 +83,23 @@ def orm_dos_remedios(weight: float, reps: int) -> dict: 1: 1, 2: 0.92, 3: 0.9, + 4: 0.89, # NOTE: I added this 5: 0.87, 6: 0.82, + 7: 0.781, # NOTE: I added this 8: 0.75, + 9: 0.72375, # NOTE: I added this 10: 0.7, + 11: 0.674286, # NOTE: I added this 12: 0.65, + 13: 0.628571, # NOTE: I added this + 14: 0.611429, # NOTE: I added this 15: 0.6, + 16: 0.588, # NOTE: I added this + 17: 0.5775, # NOTE: I added this + 18: 0.568, # NOTE: I added this + 19: 0.559, # NOTE: I added this + 20: 0.55, # NOTE: I added this, 20 reps is NOT in the original equation. } def _one_rm() -> float: @@ -92,17 +107,11 @@ def orm_dos_remedios(weight: float, reps: int) -> dict: _un_rounded_result = weight / _multiplier return round(_un_rounded_result, 1) - try: - one_rm = _one_rm() - except KeyError: - # _logger.debug(traceback.format_exc()) - valid_reps = list(_common_n_reps.keys()) - return { - "errMsg": "INVALID_RANGE — " - + "requires: reps in %s, got %s" % (valid_reps, reps), - } + # Compute the 1-rep max + one_rm = _one_rm() def max_weight(target_reps: int) -> float: + """Used to calculate max weight based on actual reps, e.g. 5 or 12""" _multiplier = _common_n_reps[target_reps] _un_rounded_result = one_rm * _multiplier return round(_un_rounded_result, 1) diff --git a/ntclient/utils/colors.py b/ntclient/utils/colors.py index e69de29..ce9a81f 100644 --- a/ntclient/utils/colors.py +++ b/ntclient/utils/colors.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +""" +Created on Mon Aug 8 14:35:43 2022 + +@author: shane + +Allows the safe avoidance of ImportError on non-colorama capable systems. +""" + +try: + from colorama import Fore, Style + from colorama import init as colorama_init + + # Made it this far, so run the init function (which is needed on Windows) + colorama_init() + + # Styles + STYLE_BRIGHT = Style.BRIGHT + STYLE_DIM = Style.DIM + STYLE_RESET_ALL = Style.RESET_ALL + + # Colors + COLOR_WARN = Fore.YELLOW + COLOR_CRIT = Style.DIM + Fore.RED + COLOR_OVER = Style.DIM + Fore.MAGENTA + + COLOR_DEFAULT = Fore.CYAN + + # Used in macro bars + COLOR_YELLOW = Fore.YELLOW + COLOR_BLUE = Fore.BLUE + COLOR_RED = Fore.RED + + # Used by `tree.py` utility + COLOR_GREEN = Fore.GREEN + COLOR_CYAN = Fore.CYAN + +except ImportError: + # These will all just be empty strings if colorama isn't installed + + # Styles + STYLE_BRIGHT = str() + STYLE_DIM = str() + STYLE_RESET_ALL = str() + + # Colors + COLOR_WARN = str() + COLOR_CRIT = str() + COLOR_OVER = str() + + COLOR_DEFAULT = str() + + COLOR_YELLOW = str() + COLOR_BLUE = str() + COLOR_RED = str() + + COLOR_GREEN = str() + COLOR_CYAN = str() diff --git a/ntclient/utils/tree.py b/ntclient/utils/tree.py index 0bc8402..b994090 100644 --- a/ntclient/utils/tree.py +++ b/ntclient/utils/tree.py @@ -3,14 +3,7 @@ import os import sys -try: - from colorama import Fore, Style - from colorama import init as colorama_init - - COLORAMA_CAPABLE = True - colorama_init() -except ImportError: - COLORAMA_CAPABLE = False +from ntclient.utils import colors chars = {"nw": "\u2514", "nws": "\u251c", "ew": "\u2500", "ns": "\u2502"} @@ -21,17 +14,11 @@ strs = [ " ", ] -if COLORAMA_CAPABLE: - # Colors and termination strings - COLOR_DIR = Style.BRIGHT + Fore.BLUE - COLOR_EXEC = Style.BRIGHT + Fore.GREEN - COLOR_LINK = Style.BRIGHT + Fore.CYAN - COLOR_DEAD_LINK = Style.BRIGHT + Fore.RED -else: - COLOR_DIR = str() - COLOR_EXEC = str() - COLOR_LINK = str() - COLOR_DEAD_LINK = str() +# Colors and termination strings +COLOR_DIR = colors.STYLE_BRIGHT + colors.COLOR_BLUE +COLOR_EXEC = colors.STYLE_BRIGHT + colors.COLOR_GREEN +COLOR_LINK = colors.STYLE_BRIGHT + colors.COLOR_CYAN +COLOR_DEAD_LINK = colors.STYLE_BRIGHT + colors.COLOR_RED def colorize(path: str, full: bool = False) -> str: @@ -43,17 +30,17 @@ def colorize(path: str, full: bool = False) -> str: [ COLOR_LINK, file, - Style.RESET_ALL, + colors.STYLE_RESET_ALL, " -> ", colorize(os.readlink(path), full=True), ] ) if os.path.isdir(path): - return "".join([COLOR_DIR, file, Style.RESET_ALL]) + return "".join([COLOR_DIR, file, colors.STYLE_RESET_ALL]) if os.access(path, os.X_OK): - return "".join([COLOR_EXEC, file, Style.RESET_ALL]) + return "".join([COLOR_EXEC, file, colors.STYLE_RESET_ALL]) return file @@ -76,7 +63,7 @@ def print_dir(_dir: str, pre: str = str()) -> tuple: n_size = 0 if not pre: - print(COLOR_DIR + _dir + Style.RESET_ALL) + print(COLOR_DIR + _dir + colors.STYLE_RESET_ALL) dir_len = len(os.listdir(_dir)) - 1 for i, file in enumerate(sorted(os.listdir(_dir), key=str.lower)): diff --git a/tests/services/test_calculate.py b/tests/services/test_calculate.py index 299258e..ec3367b 100644 --- a/tests/services/test_calculate.py +++ b/tests/services/test_calculate.py @@ -14,34 +14,15 @@ import ntclient.services.calculate as calc @pytest.mark.parametrize("_eq", ["epley", "brzycki", "dos_remedios"]) @pytest.mark.parametrize( "weight,reps", - [ - (50.0, 1), - (50.0, 2), - (50.0, 3), - (50.0, 5), - (50.0, 6), - (50.0, 8), - (50.0, 10), - (50.0, 12), - (50.0, 15), - (50.0, 20), - ], + [(50.0, x) for x in (1, 2, 3, 5, 6, 8, 10, 12, 15, 20)], ) def test_000_orm_same_in_same_out(_eq: str, weight: float, reps: int) -> None: """Test one rep max: Epley""" - if _eq == "epley": - result = calc.orm_epley(weight, reps) - - elif _eq == "brzycki": - result = calc.orm_brzycki(weight, reps) - - else: # _eq == "dos_remedios" - result = calc.orm_dos_remedios(weight, reps) - - try: - # Check results - assert result[reps] == weight - except KeyError: - # dose Remedios does not work for 20 reps currently - assert _eq == "dos_remedios" - assert reps == 20 + result = { + "epley": calc.orm_epley(weight, reps), + "brzycki": calc.orm_brzycki(weight, reps), + "dos_remedios": calc.orm_dos_remedios(weight, reps), + } + + # Check results + assert result[_eq][reps] == weight diff --git a/tests/test_cli.py b/tests/test_cli.py index 887203f..325b2e8 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -209,12 +209,15 @@ class TestCli(unittest.TestCase): code, _ = args.func(args) assert code == 0 - # Invalid range for dos_remedios (11 instead of 12) - args = arg_parser.parse_args(args=["calc", "1rm", "225", "11"]) + # Reps > 20 (or reps < 1) + args = arg_parser.parse_args(args=["calc", "1rm", "225", "25"]) code, result = args.func(args) - assert code == 0 - assert set(result.keys()) == {"epley", "brzycki", "dos_remedios"} - assert "errMsg" in result["dos_remedios"] + assert code == 1 + + # Weight < 0 + args = arg_parser.parse_args(args=["calc", "1rm", "-10", "10"]) + code, result = args.func(args) + assert code == 1 # BMR # ----------------------------------- -- 2.52.0