Skip to content

Commit 90da85b

Browse files
authored
Merge pull request #34 from mazerom/power
Support for power targets
2 parents 423a433 + 14d0e81 commit 90da85b

File tree

11 files changed

+140
-42
lines changed

11 files changed

+140
-42
lines changed

src/Command/WorkoutCommand.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ protected function configure(): void
4444
->addOption('prefix', 'r', InputOption::VALUE_OPTIONAL, 'A prefix to put before every workout name/title', null)
4545
->addOption('pool-size', null, InputOption::VALUE_OPTIONAL, 'The pool size specified for all workouts in the plan Ex.: 25yds OR 100m', null)
4646
->addOption('start', 's', InputOption::VALUE_REQUIRED, 'Date of the FIRST day of the first week of the plan Ex.: 2021-01-01 YYYY-MM-DD')
47-
->addOption('end', 'd', InputOption::VALUE_REQUIRED, 'Date of the LAST day of the last week of the plan Ex.: 2021-01-31 YYYY-MM-DD');
48-
47+
->addOption('end', 'd', InputOption::VALUE_REQUIRED, 'Date of the LAST day of the last week of the plan Ex.: 2021-01-31 YYYY-MM-DD')
48+
->addOption('critical-power', 'P', InputOption::VALUE_REQUIRED, 'Critical power for power-based targets');
4949
}
5050

5151
protected function execute(InputInterface $input, OutputInterface $output): int
@@ -64,6 +64,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
6464
$path = $input->getArgument('csv');
6565
$start = $input->getOption('start');
6666
$end = $input->getOption('end');
67+
$criticalPower = $input->getOption('critical-power');
6768

6869
if ($command === 'import' && (! empty($start) || ! empty($end))) {
6970
$io->error('You supplied the START and/or END date for scheduling workouts on a calendar, but IMPORT was specified for the command type. Please specify the schedule argument when running the command. For example - ./bin/console garmin:workout <file>.csv schedule -s <date>');
@@ -82,6 +83,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
8283
$handlerOptions->setStartDate($start);
8384
$handlerOptions->setEndDate($end);
8485
$handlerOptions->setCommand($command);
86+
$handlerOptions->setCriticalPower($criticalPower);
8587

8688
$this->registerSubscriber($io, $this->dispatcher);
8789

src/Library/Handler/AbstractHandler.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ public function parseWorkouts(HandlerOptions $handlerOptions)
9292

9393
$prefix = $handlerOptions->getPrefix();
9494
$poolSize = $handlerOptions->getPoolSize();
95-
$workouts = $this->parser->findAllWorkouts($prefix, $poolSize);
95+
$criticalPower = $handlerOptions->getCriticalPower();
96+
$workouts = $this->parser->findAllWorkouts($prefix, $poolSize, $criticalPower);
9697

9798
$debugMessages = $this->parser->getDebugMessages();
9899
$event->setDebugMessages($debugMessages);

src/Library/Handler/HandlerOptions.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ class HandlerOptions
4646
*/
4747
protected $poolSize;
4848

49+
private ?int $criticalPower = null;
50+
4951
public function getPath(): string
5052
{
5153
return $this->path;
@@ -196,4 +198,15 @@ public function setPoolSize(?string $poolSize): HandlerOptions
196198
$this->poolSize = $poolSize;
197199
return $this;
198200
}
201+
202+
public function getCriticalPower(): ?int
203+
{
204+
return $this->criticalPower;
205+
}
206+
207+
public function setCriticalPower(?string $criticalPower): self
208+
{
209+
$this->criticalPower = $criticalPower ? (int) $criticalPower : null;
210+
return $this;
211+
}
199212
}

src/Library/Parser/Model/Step/AbstractStep.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,13 @@ abstract class AbstractStep implements \JsonSerializable
4949
* @param int|null $stepOrder
5050
* @param string|null $notes
5151
*/
52-
public function __construct($stepText, protected $notes, protected $order, $swimming)
52+
public function __construct($stepText, protected $notes, protected $order, $swimming, protected ?int $criticalPower = null)
5353
{
5454
$duration = $this->parseTextDuration($stepText);
5555
$target = $this->parseTextTarget($stepText);
5656

5757
$this->duration = DurationFactory::build($duration);
58-
$this->target = TargetFactory::build($target);
58+
$this->target = TargetFactory::build($target, $criticalPower);
5959

6060
// Check for specific strokes and equipment
6161
if ($swimming) {

src/Library/Parser/Model/Step/StepFactory.php

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,26 @@
44

55
class StepFactory
66
{
7-
public static function build($header, $parameters, $notes, $order, $swimming = false): WarmupStep|CooldownStep|IntervalStep|RecoverStep|RestStep|RepeaterStep|null
7+
public static function build($header, $parameters, $notes, $order, $swimming = false, ?int $criticalPower = null): WarmupStep|CooldownStep|IntervalStep|RecoverStep|RestStep|RepeaterStep|null
88
{
99
switch ($header) {
1010
case 'warmup':
11-
return new WarmupStep($parameters, $notes, $order, $swimming);
11+
return new WarmupStep($parameters, $notes, $order, $swimming, $criticalPower);
1212
case 'cooldown':
13-
return new CooldownStep($parameters, $notes, $order, $swimming);
13+
return new CooldownStep($parameters, $notes, $order, $swimming, $criticalPower);
1414
case 'run':
1515
case 'bike':
16+
return new IntervalStep($parameters, $notes, $order, $swimming, $criticalPower);
1617
case 'go':
1718
case 'other':
1819
case 'swim':
19-
return new IntervalStep($parameters, $notes, $order, $swimming);
20+
return new IntervalStep($parameters, $notes, $order, $swimming, $criticalPower);
2021
case 'recover':
21-
return new RecoverStep($parameters, $notes, $order, $swimming);
22+
return new RecoverStep($parameters, $notes, $order, $swimming), $criticalPower;
2223
case 'rest':
23-
return new RestStep($parameters, $notes, $order, $swimming);
24+
return new RestStep($parameters, $notes, $order, $swimming, $criticalPower);
2425
case 'repeat':
25-
return new RepeaterStep($parameters, $order);
26+
return new RepeaterStep($parameters, $order, $swimming, $criticalPower;
2627
default:
2728
break;
2829
}

src/Library/Parser/Model/Target/PowerTarget.php

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,60 @@
44

55
class PowerTarget extends AbstractTarget
66
{
7-
public const REGEX = '/^(\d{1,2}:\d{2})\s*-\s*(\d{1,2}:\d{2})\s*(mpk|mpm)?$/';
7+
public const REGEX_WATTS = '/^(\d+)(?:\s*-\s*(\d+))?\s*(w|watts)$/i';
8+
public const REGEX_CP = '/^(\d+)(?:\s*-\s*(\d+))?\s*(%cp|pcp)$/i';
89

9-
public static function testPower($powerText): false|\App\Library\Parser\Model\Target\PowerTarget
10+
public static function testPower($powerText, ?int $criticalPower = null): false|\App\Library\Parser\Model\Target\PowerTarget
1011
{
11-
$result = $powerText && preg_match(self::REGEX, (string) $powerText, $power);
12-
if (!$result) {
13-
return false;
14-
}
15-
if (!isset($power[1])) {
16-
return false;
17-
}
18-
if (empty($power[1])) {
19-
return false;
20-
}
21-
if (!isset($power[2])) {
22-
return false;
12+
$powerText = (string) $powerText;
13+
14+
// First, try to match percentage-based power (%cp)
15+
if ($criticalPower > 0 && preg_match(self::REGEX_CP, $powerText, $matches)) {
16+
if (!isset($matches[1]) || empty($matches[1])) {
17+
return false;
18+
}
19+
20+
$fromPercent = (int) $matches[1];
21+
$from = (int) round($criticalPower * ($fromPercent / 100));
22+
23+
// If a range is provided (e.g., 90-95%cp), calculate the 'to' value.
24+
if (isset($matches[2]) && !empty($matches[2])) {
25+
$toPercent = (int) $matches[2];
26+
$to = (int) round($criticalPower * ($toPercent / 100));
27+
} else {
28+
$to = $from + 1;
29+
}
30+
31+
return new PowerTarget($from, $to);
2332
}
24-
if (empty($power[2])) {
25-
return false;
33+
34+
// If %cp doesn't match or isn't applicable, try to match absolute watts
35+
if (preg_match(self::REGEX_WATTS, $powerText, $matches)) {
36+
if (!isset($matches[1]) || empty($matches[1])) {
37+
return false;
38+
}
39+
40+
$from = (int) $matches[1];
41+
42+
if (isset($matches[2]) && !empty($matches[2])) {
43+
$to = (int) $matches[2];
44+
} else {
45+
$to = $from + 1;
46+
}
47+
48+
return new PowerTarget($from, $to);
2649
}
27-
return new PowerTarget($power[1], $power[2]);
50+
51+
return false;
2852
}
2953

30-
public function __construct(protected $from, protected $to)
54+
public function __construct(protected int $from, protected int $to)
3155
{
56+
// Ensure 'from' is always less than 'to'.
57+
if ($this->from > $this->to) {
58+
[$this->from, $this->to] = [$this->to, $this->from]; // Swap values
59+
}
60+
3261
}
3362

3463
protected function getTypeId(): int
@@ -38,7 +67,7 @@ protected function getTypeId(): int
3867

3968
protected function getTypeKey(): string
4069
{
41-
return 'power.zone';
70+
return 'power.range';
4271
}
4372

4473
protected function getTargetValueOne()

src/Library/Parser/Model/Target/TargetFactory.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
class TargetFactory
66
{
7-
public static function build($targetText)
7+
public static function build($targetText, ?int $criticalPower = null)
88
{
99
$paceTarget = PaceTarget::testPace($targetText);
1010

@@ -23,6 +23,13 @@ public static function build($targetText)
2323
if ($hrCustomTarget) {
2424
return $hrCustomTarget;
2525
}
26+
27+
$powerTarget = PowerTarget::testPower($targetText, $criticalPower);
28+
29+
if ($powerTarget) {
30+
return $powerTarget;
31+
}
32+
2633
return null;
2734
}
2835
}

src/Library/Parser/Model/Workout/AbstractWorkout.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ abstract class AbstractWorkout implements \JsonSerializable, \Stringable
2929
/**
3030
* @param string|null $name
3131
*/
32-
public function __construct(protected $name)
32+
public function __construct(protected $name, protected ?int $criticalPower = null)
3333
{
3434
$this->steps = new ArrayCollection();
3535
}
3636

37-
public function steps($steps, $swimming = false)
37+
public function steps(array $steps, bool $swimming = false): self
3838
{
3939
$repStep = [];
4040

@@ -44,7 +44,7 @@ public function steps($steps, $swimming = false)
4444
$parameters = $this->parseStepDetails($step);
4545
$notes = $this->parseStepNotes($step);
4646

47-
$stepFactory = StepFactory::build($header, $parameters, $notes, $index, $swimming);
47+
$stepFactory = StepFactory::build($header, $parameters, $notes, $index, $swimming, $this->criticalPower);
4848

4949
if ($stepFactory instanceof RepeaterStep) {
5050
//Store it into array with the index being whitespace to reference children steps later

src/Library/Parser/Model/Workout/WorkoutFactory.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44

55
class WorkoutFactory
66
{
7-
public static function build($type, $name, $steps, $poolSize = null)
7+
public static function build($type, $name, $steps, $poolSize = null, ?int $criticalPower = null)
88
{
99
switch ($type) {
1010
case 'running':
11-
$workout = new RunningWorkout($name);
11+
$workout = new RunningWorkout($name, $criticalPower);
1212
return $workout->steps($steps);
1313
case 'cycling':
1414
$workout = new CyclingWorkout($name);

src/Library/Parser/Parser.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public function getTotalWeeks(): int
5151
return count($this->records);
5252
}
5353

54-
public function findAllWorkouts($prefix = null, $poolSize = null): array
54+
public function findAllWorkouts($prefix = null, $poolSize = null, ?int $criticalPower = null): array
5555
{
5656
$workouts = [];
5757

@@ -66,7 +66,7 @@ public function findAllWorkouts($prefix = null, $poolSize = null): array
6666
if (! empty($workoutGroups)) {
6767
foreach ($workoutGroups as $workoutGroupText) {
6868
//Try to parse workout
69-
$workout = $this->parseWorkout($workoutGroupText, $poolSize);
69+
$workout = $this->parseWorkout($workoutGroupText, $poolSize, $criticalPower);
7070
if ($workout) {
7171
//Workout must have been made
7272
$name = $workout->getName();
@@ -269,9 +269,9 @@ public function parseSteps($stepsText)
269269
return $steps[0];
270270
}
271271

272-
public function parseWorkout($workoutText, $poolSize = null)
272+
public function parseWorkout($workoutText, $poolSize = null, ?int $criticalPower = null)
273273
{
274-
//Read first line
274+
//Read first line
275275
$workoutType = $this->parseWorkoutType($workoutText);
276276
$workoutName = $this->parseWorkoutName($workoutText);
277277

@@ -280,6 +280,6 @@ public function parseWorkout($workoutText, $poolSize = null)
280280
//Read steps into array
281281
$steps = $this->parseSteps($stepsText);
282282

283-
return WorkoutFactory::build($workoutType, $workoutName, $steps, $poolSize);
283+
return WorkoutFactory::build($workoutType, $workoutName, $steps, $poolSize, $criticalPower);
284284
}
285285
}

0 commit comments

Comments
 (0)