From f64aaed2bbc23c2dcd0ea7303f32ecce94230d39 Mon Sep 17 00:00:00 2001 From: resu-xuniL Date: Mon, 24 Aug 2026 22:26:45 +0200 Subject: [PATCH 1/6] configlet create --practice-exercise split-second-stopwatch --- config.json | 8 ++ .../.docs/instructions.md | 22 +++++ .../.docs/introduction.md | 6 ++ .../split-second-stopwatch/.meta/config.json | 17 ++++ .../split-second-stopwatch/.meta/example.php | 0 .../split-second-stopwatch/.meta/tests.toml | 97 +++++++++++++++++++ .../SplitSecondStopwatch.php | 0 .../SplitSecondStopwatchTest.php | 0 8 files changed, 150 insertions(+) create mode 100644 exercises/practice/split-second-stopwatch/.docs/instructions.md create mode 100644 exercises/practice/split-second-stopwatch/.docs/introduction.md create mode 100644 exercises/practice/split-second-stopwatch/.meta/config.json create mode 100644 exercises/practice/split-second-stopwatch/.meta/example.php create mode 100644 exercises/practice/split-second-stopwatch/.meta/tests.toml create mode 100644 exercises/practice/split-second-stopwatch/SplitSecondStopwatch.php create mode 100644 exercises/practice/split-second-stopwatch/SplitSecondStopwatchTest.php diff --git a/config.json b/config.json index f2a2de7be..8c4cfdf5c 100644 --- a/config.json +++ b/config.json @@ -1378,6 +1378,14 @@ "practices": [], "prerequisites": [], "difficulty": 7 + }, + { + "slug": "split-second-stopwatch", + "name": "Split-Second Stopwatch", + "uuid": "25befc0a-cd5b-44d6-9f5f-647e12208381", + "practices": [], + "prerequisites": [], + "difficulty": 1 } ], "foregone": [ diff --git a/exercises/practice/split-second-stopwatch/.docs/instructions.md b/exercises/practice/split-second-stopwatch/.docs/instructions.md new file mode 100644 index 000000000..30bdc988d --- /dev/null +++ b/exercises/practice/split-second-stopwatch/.docs/instructions.md @@ -0,0 +1,22 @@ +# Instructions + +Your task is to build a stopwatch to keep precise track of lap times. + +The stopwatch uses four commands (start, stop, lap, and reset) to keep track of: + +1. The current lap's tracked time +2. Previously recorded lap times + +What commands can be used depends on which state the stopwatch is in: + +1. Ready: initial state +2. Running: tracking time +3. Stopped: not tracking time + +| Command | Begin state | End state | Effect | +| ------- | ----------- | --------- | -------------------------------------------------------- | +| Start | Ready | Running | Start tracking time | +| Start | Stopped | Running | Resume tracking time | +| Stop | Running | Stopped | Stop tracking time | +| Lap | Running | Running | Add current lap to previous laps, then reset current lap | +| Reset | Stopped | Ready | Reset current lap and clear previous laps | diff --git a/exercises/practice/split-second-stopwatch/.docs/introduction.md b/exercises/practice/split-second-stopwatch/.docs/introduction.md new file mode 100644 index 000000000..a84322477 --- /dev/null +++ b/exercises/practice/split-second-stopwatch/.docs/introduction.md @@ -0,0 +1,6 @@ +# Introduction + +You've always run for the thrill of it — no schedules, no timers, just the sound of your feet on the pavement. +But now that you've joined a competitive running crew, things are getting serious. +Training sessions are timed to the second, and every split second counts. +To keep pace, you've picked up the _Split-Second Stopwatch_ — a sleek, high-tech gadget that's about to become your new best friend. diff --git a/exercises/practice/split-second-stopwatch/.meta/config.json b/exercises/practice/split-second-stopwatch/.meta/config.json new file mode 100644 index 000000000..b550f3c8b --- /dev/null +++ b/exercises/practice/split-second-stopwatch/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [], + "files": { + "solution": [ + "SplitSecondStopwatch.php" + ], + "test": [ + "SplitSecondStopwatchTest.php" + ], + "example": [ + ".meta/example.php" + ] + }, + "blurb": "Keep track of time through a digital stopwatch.", + "source": "Erik Schierboom", + "source_url": "https://github.com/exercism/problem-specifications/pull/2547" +} diff --git a/exercises/practice/split-second-stopwatch/.meta/example.php b/exercises/practice/split-second-stopwatch/.meta/example.php new file mode 100644 index 000000000..e69de29bb diff --git a/exercises/practice/split-second-stopwatch/.meta/tests.toml b/exercises/practice/split-second-stopwatch/.meta/tests.toml new file mode 100644 index 000000000..323cb7ae8 --- /dev/null +++ b/exercises/practice/split-second-stopwatch/.meta/tests.toml @@ -0,0 +1,97 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[ddb238ea-99d4-4eaa-a81d-3c917a525a23] +description = "new stopwatch starts in ready state" + +[b19635d4-08ad-4ac3-b87f-aca10e844071] +description = "new stopwatch's current lap has no elapsed time" + +[492eb532-268d-43ea-8a19-2a032067d335] +description = "new stopwatch's total has no elapsed time" + +[8a892c1e-9ef7-4690-894e-e155a1fe4484] +description = "new stopwatch does not have previous laps" + +[5b2705b6-a584-4042-ba3a-4ab8d0ab0281] +description = "start from ready state changes state to running" + +[748235ce-1109-440b-9898-0a431ea179b6] +description = "start does not change previous laps" + +[491487b1-593d-423e-a075-aa78d449ff1f] +description = "start initiates time tracking for current lap" + +[a0a7ba2c-8db6-412c-b1b6-cb890e9b72ed] +description = "start initiates time tracking for total" + +[7f558a17-ef6d-4a5b-803a-f313af7c41d3] +description = "start cannot be called from running state" + +[32466eef-b2be-4d60-a927-e24fce52dab9] +description = "stop from running state changes state to stopped" + +[621eac4c-8f43-4d99-919c-4cad776d93df] +description = "stop pauses time tracking for current lap" + +[465bcc82-7643-41f2-97ff-5e817cef8db4] +description = "stop pauses time tracking for total" + +[b1ba7454-d627-41ee-a078-891b2ed266fc] +description = "stop cannot be called from ready state" + +[5c041078-0898-44dc-9d5b-8ebb5352626c] +description = "stop cannot be called from stopped state" + +[3f32171d-8fbf-46b6-bc2b-0810e1ec53b7] +description = "start from stopped state changes state to running" + +[626997cb-78d5-4fe8-b501-29fdef804799] +description = "start from stopped state resumes time tracking for current lap" + +[58487c53-ab26-471c-a171-807ef6363319] +description = "start from stopped state resumes time tracking for total" + +[091966e3-ed25-4397-908b-8bb0330118f8] +description = "lap adds current lap to previous laps" + +[1aa4c5ee-a7d5-4d59-9679-419deef3c88f] +description = "lap resets current lap and resumes time tracking" + +[4b46b92e-1b3f-46f6-97d2-0082caf56e80] +description = "lap continues time tracking for total" + +[ea75d36e-63eb-4f34-97ce-8c70e620bdba] +description = "lap cannot be called from ready state" + +[63731154-a23a-412d-a13f-c562f208eb1e] +description = "lap cannot be called from stopped state" + +[e585ee15-3b3f-4785-976b-dd96e7cc978b] +description = "stop does not change previous laps" + +[fc3645e2-86cf-4d11-97c6-489f031103f6] +description = "reset from stopped state changes state to ready" + +[20fbfbf7-68ad-4310-975a-f5f132886c4e] +description = "reset resets current lap" + +[00a8f7bb-dd5c-43e5-8705-3ef124007662] +description = "reset clears previous laps" + +[76cea936-6214-4e95-b6d1-4d4edcf90499] +description = "reset cannot be called from ready state" + +[ba4d8e69-f200-4721-b59e-90d8cf615153] +description = "reset cannot be called from running state" + +[0b01751a-cb57-493f-bb86-409de6e84306] +description = "supports very long laps" diff --git a/exercises/practice/split-second-stopwatch/SplitSecondStopwatch.php b/exercises/practice/split-second-stopwatch/SplitSecondStopwatch.php new file mode 100644 index 000000000..e69de29bb diff --git a/exercises/practice/split-second-stopwatch/SplitSecondStopwatchTest.php b/exercises/practice/split-second-stopwatch/SplitSecondStopwatchTest.php new file mode 100644 index 000000000..e69de29bb From 839a9996d6def8d85092e33fa9d6defd08e65e79 Mon Sep 17 00:00:00 2001 From: resu-xuniL Date: Tue, 25 Aug 2026 13:45:46 +0200 Subject: [PATCH 2/6] Coded test and example --- .../split-second-stopwatch/.meta/example.php | 88 ++++ .../SplitSecondStopwatch.php | 68 +++ .../SplitSecondStopwatchTest.php | 451 ++++++++++++++++++ 3 files changed, 607 insertions(+) diff --git a/exercises/practice/split-second-stopwatch/.meta/example.php b/exercises/practice/split-second-stopwatch/.meta/example.php index e69de29bb..91ff4cccd 100644 --- a/exercises/practice/split-second-stopwatch/.meta/example.php +++ b/exercises/practice/split-second-stopwatch/.meta/example.php @@ -0,0 +1,88 @@ +state === 'running') { + $seconds = $this->formatSeconds($duration); + $this->currentLap += $seconds; + $this->total += $seconds; + } + } + + private function formatTime(int $seconds): string + { + $hours = $seconds / 3600; + $minutes = ($seconds % 3600) / 60; + $seconds = $seconds % 60; + + return sprintf("%02d:%02d:%02d", $hours, $minutes, $seconds); + } + + private function formatSeconds(string $duration): int + { + [$hours, $minutes, $seconds] = explode(':', $duration); + + return $hours * 3600 + $minutes * 60 + $seconds; + } + + public function getCurrentLap(): string + { + return $this->formatTime($this->currentLap); + } + + public function getTotal(): string + { + return $this->formatTime($this->total); + } + + public function start(): void + { + if ($this->state === "running") { + throw new Exception("Error: cannot start an already running stopwatch"); + } + + $this->state = "running"; + } + + public function stop(): void + { + if ($this->state !== "running") { + throw new Exception("Error: cannot stop a stopwatch that is not running"); + } + + $this->state = "stopped"; + } + + public function lap(): void + { + if ($this->state !== "running") { + throw new Exception("Error: cannot lap a stopwatch that is not running"); + } + + $this->previousLaps[] = $this->getCurrentLap(); + $this->currentLap = 0; + } + + public function reset(): void + { + if ($this->state !== "stopped") { + throw new Exception("Error: cannot reset a stopwatch that is not stopped"); + } + + $this->state = "ready"; + $this->currentLap = 0; + $this->previousLaps = []; + } +} diff --git a/exercises/practice/split-second-stopwatch/SplitSecondStopwatch.php b/exercises/practice/split-second-stopwatch/SplitSecondStopwatch.php index e69de29bb..8c5108207 100644 --- a/exercises/practice/split-second-stopwatch/SplitSecondStopwatch.php +++ b/exercises/practice/split-second-stopwatch/SplitSecondStopwatch.php @@ -0,0 +1,68 @@ +. + * + * To disable strict typing, comment out the directive below. + */ + +declare(strict_types=1); + +class SplitSecondStopwatch +{ + public function __construct() + { + throw new \BadMethodCallException(sprintf('Implement the SplitSecondStopwatch %s method', __FUNCTION__)); + } + + public function advanceTime(string $duration): void + { + throw new \BadMethodCallException(sprintf('Implement the %s method', __FUNCTION__)); + } + + public function getCurrentLap(): string + { + throw new \BadMethodCallException(sprintf('Implement the %s method', __FUNCTION__)); + } + + public function getTotal(): string + { + throw new \BadMethodCallException(sprintf('Implement the %s method', __FUNCTION__)); + } + + public function start(): void + { + throw new \BadMethodCallException(sprintf('Implement the %s method', __FUNCTION__)); + } + + public function stop(): void + { + throw new \BadMethodCallException(sprintf('Implement the %s method', __FUNCTION__)); + } + + public function lap(): void + { + throw new \BadMethodCallException(sprintf('Implement the %s method', __FUNCTION__)); + } + + public function reset(): void + { + throw new \BadMethodCallException(sprintf('Implement the %s method', __FUNCTION__)); + } +} diff --git a/exercises/practice/split-second-stopwatch/SplitSecondStopwatchTest.php b/exercises/practice/split-second-stopwatch/SplitSecondStopwatchTest.php index e69de29bb..bb4c16695 100644 --- a/exercises/practice/split-second-stopwatch/SplitSecondStopwatchTest.php +++ b/exercises/practice/split-second-stopwatch/SplitSecondStopwatchTest.php @@ -0,0 +1,451 @@ +assertEquals("ready", $stopwatch->state); + } + + /** + * uuid: b19635d4-08ad-4ac3-b87f-aca10e844071 + */ + #[TestDox("New stopwatch's current lap has no elapsed time")] + public function testNewStopwatchSCurrentLapHasNoElapsedTime(): void + { + $stopwatch = new SplitSecondStopwatch(); + + $this->assertEquals("00:00:00", $stopwatch->getCurrentLap()); + } + + /** + * uuid: 492eb532-268d-43ea-8a19-2a032067d335 + */ + #[TestDox("New stopwatch's total has no elapsed time")] + public function testNewStopwatchSTotalHasNoElapsedTime(): void + { + $stopwatch = new SplitSecondStopwatch(); + + $this->assertEquals("00:00:00", $stopwatch->getTotal()); + } + + /** + * uuid: 8a892c1e-9ef7-4690-894e-e155a1fe4484 + */ + #[TestDox("New stopwatch does not have previous laps")] + public function testNewStopwatchDoesNotHavePreviousLaps(): void + { + $stopwatch = new SplitSecondStopwatch(); + + $this->assertEquals([], $stopwatch->previousLaps); + } + + /** + * uuid: 5b2705b6-a584-4042-ba3a-4ab8d0ab0281 + */ + #[TestDox("Start from ready state changes state to running")] + public function testStartFromReadyStateChangesStateToRunning(): void + { + $stopwatch = new SplitSecondStopwatch(); + $stopwatch->start(); + + $this->assertEquals("running", $stopwatch->state); + } + + /** + * uuid: 748235ce-1109-440b-9898-0a431ea179b6 + */ + #[TestDox("Start does not change previous laps")] + public function testStartDoesNotChangePreviousLaps(): void + { + $stopwatch = new SplitSecondStopwatch(); + $stopwatch->start(); + + $this->assertEquals([], $stopwatch->previousLaps); + } + + /** + * uuid: 491487b1-593d-423e-a075-aa78d449ff1f + */ + #[TestDox("Start initiates time tracking for current lap")] + public function testStartInitiatesTimeTrackingForCurrentLap(): void + { + $stopwatch = new SplitSecondStopwatch(); + $stopwatch->start(); + $stopwatch->advanceTime("00:00:05"); + + $this->assertEquals("00:00:05", $stopwatch->getCurrentLap()); + } + + /** + * uuid: a0a7ba2c-8db6-412c-b1b6-cb890e9b72ed + */ + #[TestDox("Start initiates time tracking for total")] + public function testStartInitiatesTimeTrackingForTotal(): void + { + $stopwatch = new SplitSecondStopwatch(); + $stopwatch->start(); + $stopwatch->advanceTime("00:00:23"); + + $this->assertEquals("00:00:23", $stopwatch->getTotal()); + } + + /** + * uuid: 7f558a17-ef6d-4a5b-803a-f313af7c41d3 + */ + #[TestDox("Start cannot be called from running state")] + public function testStartCannotBeCalledFromRunningState(): void + { + $stopwatch = new SplitSecondStopwatch(); + $stopwatch->start(); + + $this->expectException(Exception::class); + $this->expectExceptionMessage("cannot start an already running stopwatch"); + $stopwatch->start(); + } + + /** + * uuid: 32466eef-b2be-4d60-a927-e24fce52dab9 + */ + #[TestDox("Stop from running state changes state to stopped")] + public function testStopFromRunningStateChangesStateToStopped(): void + { + $stopwatch = new SplitSecondStopwatch(); + $stopwatch->start(); + $stopwatch->stop(); + + $this->assertEquals("stopped", $stopwatch->state); + } + + /** + * uuid: 621eac4c-8f43-4d99-919c-4cad776d93df + */ + #[TestDox("Stop pauses time tracking for current lap")] + public function testStopPausesTimeTrackingForCurrentLap(): void + { + $stopwatch = new SplitSecondStopwatch(); + $stopwatch->start(); + $stopwatch->advanceTime("00:00:05"); + $stopwatch->stop(); + $stopwatch->advanceTime("00:00:08"); + + $this->assertEquals("00:00:05", $stopwatch->getCurrentLap()); + } + + /** + * uuid: 465bcc82-7643-41f2-97ff-5e817cef8db4 + */ + #[TestDox("Stop pauses time tracking for total")] + public function testStopPausesTimeTrackingForTotal(): void + { + $stopwatch = new SplitSecondStopwatch(); + $stopwatch->start(); + $stopwatch->advanceTime("00:00:13"); + $stopwatch->stop(); + $stopwatch->advanceTime("00:00:44"); + + $this->assertEquals("00:00:13", $stopwatch->getTotal()); + } + + /** + * uuid: b1ba7454-d627-41ee-a078-891b2ed266fc + */ + #[TestDox("Stop cannot be called from ready state")] + public function testStopCannotBeCalledFromReadyState(): void + { + $stopwatch = new SplitSecondStopwatch(); + + $this->expectException(Exception::class); + $this->expectExceptionMessage("cannot stop a stopwatch that is not running"); + $stopwatch->stop(); + } + + /** + * uuid: 5c041078-0898-44dc-9d5b-8ebb5352626c + */ + #[TestDox("Stop cannot be called from stopped state")] + public function testStopCannotBeCalledFromStoppedState(): void + { + $stopwatch = new SplitSecondStopwatch(); + $stopwatch->start(); + $stopwatch->stop(); + + $this->expectException(Exception::class); + $this->expectExceptionMessage("cannot stop a stopwatch that is not running"); + $stopwatch->stop(); + } + + /** + * uuid: 3f32171d-8fbf-46b6-bc2b-0810e1ec53b7 + */ + #[TestDox("Start from stopped state changes state to running")] + public function testStartFromStoppedStateChangesStateToRunning(): void + { + $stopwatch = new SplitSecondStopwatch(); + $stopwatch->start(); + $stopwatch->stop(); + $stopwatch->start(); + + $this->assertEquals("running", $stopwatch->state); + } + + /** + * uuid: 626997cb-78d5-4fe8-b501-29fdef804799 + */ + #[TestDox("Start from stopped state resumes time tracking for current lap")] + public function testStartFromStoppedStateResumesTimeTrackingForCurrentLap(): void + { + $stopwatch = new SplitSecondStopwatch(); + $stopwatch->start(); + $stopwatch->advanceTime("00:01:20"); + $stopwatch->stop(); + $stopwatch->advanceTime("00:00:20"); + $stopwatch->start(); + $stopwatch->advanceTime("00:00:08"); + + $this->assertEquals("00:01:28", $stopwatch->getCurrentLap()); + } + + /** + * uuid: 58487c53-ab26-471c-a171-807ef6363319 + */ + #[TestDox("Start from stopped state resumes time tracking for total")] + public function testStartFromStoppedStateResumesTimeTrackingForTotal(): void + { + $stopwatch = new SplitSecondStopwatch(); + $stopwatch->start(); + $stopwatch->advanceTime("00:00:23"); + $stopwatch->stop(); + $stopwatch->advanceTime("00:00:44"); + $stopwatch->start(); + $stopwatch->advanceTime("00:00:09"); + + $this->assertEquals("00:00:32", $stopwatch->getTotal()); + } + + /** + * uuid: 091966e3-ed25-4397-908b-8bb0330118f8 + */ + #[TestDox("Lap adds current lap to previous laps")] + public function testLapAddsCurrentLapToPreviousLaps(): void + { + $stopwatch = new SplitSecondStopwatch(); + $stopwatch->start(); + $stopwatch->advanceTime("00:01:38"); + $stopwatch->lap(); + + $this->assertEquals(["00:01:38"], $stopwatch->previousLaps); + + $stopwatch->advanceTime("00:00:44"); + $stopwatch->lap(); + + $this->assertEquals(["00:01:38", "00:00:44"], $stopwatch->previousLaps); + } + + /** + * uuid: 1aa4c5ee-a7d5-4d59-9679-419deef3c88f + */ + #[TestDox("Lap resets current lap and resumes time tracking")] + public function testLapResetsCurrentLapAndResumesTimeTracking(): void + { + $stopwatch = new SplitSecondStopwatch(); + $stopwatch->start(); + $stopwatch->advanceTime("00:08:22"); + $stopwatch->lap(); + + $this->assertEquals("00:00:00", $stopwatch->getCurrentLap()); + + $stopwatch->advanceTime("00:00:15"); + + $this->assertEquals("00:00:15", $stopwatch->getCurrentLap()); + } + + /** + * uuid: 4b46b92e-1b3f-46f6-97d2-0082caf56e80 + */ + #[TestDox("Lap continues time tracking for total")] + public function testLapContinuesTimeTrackingForTotal(): void + { + $stopwatch = new SplitSecondStopwatch(); + $stopwatch->start(); + $stopwatch->advanceTime("00:00:22"); + $stopwatch->lap(); + $stopwatch->advanceTime("00:00:33"); + + $this->assertEquals("00:00:55", $stopwatch->getTotal()); + } + + /** + * uuid: ea75d36e-63eb-4f34-97ce-8c70e620bdba + */ + #[TestDox("Lap cannot be called from ready state")] + public function testLapCannotBeCalledFromReadyState(): void + { + $stopwatch = new SplitSecondStopwatch(); + + $this->expectException(Exception::class); + $this->expectExceptionMessage("cannot lap a stopwatch that is not running"); + $stopwatch->lap(); + } + + /** + * uuid: 63731154-a23a-412d-a13f-c562f208eb1e + */ + #[TestDox("Lap cannot be called from stopped state")] + public function testLapCannotBeCalledFromStoppedState(): void + { + $stopwatch = new SplitSecondStopwatch(); + $stopwatch->start(); + $stopwatch->stop(); + + $this->expectException(Exception::class); + $this->expectExceptionMessage("cannot lap a stopwatch that is not running"); + $stopwatch->lap(); + } + + /** + * uuid: e585ee15-3b3f-4785-976b-dd96e7cc978b + */ + #[TestDox("Stop does not change previous laps")] + public function testStopDoesNotChangePreviousLaps(): void + { + $stopwatch = new SplitSecondStopwatch(); + $stopwatch->start(); + $stopwatch->advanceTime("00:11:22"); + $stopwatch->lap(); + + $this->assertEquals(["00:11:22"], $stopwatch->previousLaps); + + $stopwatch->stop(); + + $this->assertEquals(["00:11:22"], $stopwatch->previousLaps); + } + + /** + * uuid: fc3645e2-86cf-4d11-97c6-489f031103f6 + */ + #[TestDox("Reset from stopped state changes state")] + public function testResetFromStoppedStateChangesState(): void + { + $stopwatch = new SplitSecondStopwatch(); + $stopwatch->start(); + $stopwatch->stop(); + $stopwatch->reset(); + + $this->assertEquals("ready", $stopwatch->state); + } + + /** + * uuid: 20fbfbf7-68ad-4310-975a-f5f132886c4e + */ + #[TestDox("Reset resets current lap")] + public function testResetResetsCurrentLap(): void + { + $stopwatch = new SplitSecondStopwatch(); + $stopwatch->start(); + $stopwatch->advanceTime("00:00:10"); + $stopwatch->stop(); + $stopwatch->reset(); + + $this->assertEquals("00:00:00", $stopwatch->getCurrentLap()); + } + + /** + * uuid: 00a8f7bb-dd5c-43e5-8705-3ef124007662 + */ + #[TestDox("Reset clears previous laps")] + public function testResetClearsPreviousLaps(): void + { + $stopwatch = new SplitSecondStopwatch(); + $stopwatch->start(); + $stopwatch->advanceTime("00:00:10"); + $stopwatch->lap(); + $stopwatch->advanceTime("00:00:20"); + $stopwatch->lap(); + + $this->assertEquals(["00:00:10", "00:00:20"], $stopwatch->previousLaps); + + $stopwatch->stop(); + $stopwatch->reset(); + + $this->assertEquals([], $stopwatch->previousLaps); + } + + /** + * uuid: 76cea936-6214-4e95-b6d1-4d4edcf90499 + */ + #[TestDox("Reset cannot be called from ready state")] + public function testResetCannotBeCalledFromReadyState(): void + { + $stopwatch = new SplitSecondStopwatch(); + + $this->expectException(Exception::class); + $this->expectExceptionMessage("cannot reset a stopwatch that is not stopped"); + $stopwatch->reset(); + } + + /** + * uuid: ba4d8e69-f200-4721-b59e-90d8cf615153 + */ + #[TestDox("Reset cannot be called from running state")] + public function testResetCannotBeCalledFromRunningState(): void + { + $stopwatch = new SplitSecondStopwatch(); + $stopwatch->start(); + + $this->expectException(Exception::class); + $this->expectExceptionMessage("cannot reset a stopwatch that is not stopped"); + $stopwatch->reset(); + } + + /** + * uuid: 0b01751a-cb57-493f-bb86-409de6e84306 + */ + #[TestDox("Supports very long laps")] + public function testSupportsVeryLongLap(): void + { + $stopwatch = new SplitSecondStopwatch(); + $stopwatch->start(); + $stopwatch->advanceTime("01:23:45"); + + $this->assertEquals("01:23:45", $stopwatch->getCurrentLap()); + + $stopwatch->lap(); + + $this->assertEquals(["01:23:45"], $stopwatch->previousLaps); + + $stopwatch->advanceTime("04:01:40"); + + $this->assertEquals("04:01:40", $stopwatch->getCurrentLap()); + $this->assertEquals("05:25:25", $stopwatch->getTotal()); + + $stopwatch->lap(); + + $this->assertEquals(["01:23:45", "04:01:40"], $stopwatch->previousLaps); + + $stopwatch->advanceTime("08:43:05"); + + $this->assertEquals("08:43:05", $stopwatch->getCurrentLap()); + $this->assertEquals("14:08:30", $stopwatch->getTotal()); + + $stopwatch->lap(); + + $this->assertEquals(["01:23:45", "04:01:40", "08:43:05"], $stopwatch->previousLaps); + } +} From 3bb6cf1ee2b3f4197be70eb3e816f39f05e5f9a7 Mon Sep 17 00:00:00 2001 From: resu-xuniL Date: Tue, 25 Aug 2026 13:47:18 +0200 Subject: [PATCH 3/6] Set the difficulty rank to 3 --- config.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/config.json b/config.json index 8c4cfdf5c..79b018a77 100644 --- a/config.json +++ b/config.json @@ -977,6 +977,14 @@ "prerequisites": [], "difficulty": 3 }, + { + "slug": "split-second-stopwatch", + "name": "Split-Second Stopwatch", + "uuid": "25befc0a-cd5b-44d6-9f5f-647e12208381", + "practices": [], + "prerequisites": [], + "difficulty": 3 + }, { "slug": "bob", "name": "Bob", @@ -1378,14 +1386,6 @@ "practices": [], "prerequisites": [], "difficulty": 7 - }, - { - "slug": "split-second-stopwatch", - "name": "Split-Second Stopwatch", - "uuid": "25befc0a-cd5b-44d6-9f5f-647e12208381", - "practices": [], - "prerequisites": [], - "difficulty": 1 } ], "foregone": [ From 411f81aa32a1bf26011d73f13f2839cc599029b2 Mon Sep 17 00:00:00 2001 From: resu-xuniL Date: Tue, 25 Aug 2026 13:48:41 +0200 Subject: [PATCH 4/6] Added GitHub handle to `config.json` --- exercises/practice/split-second-stopwatch/.meta/config.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/exercises/practice/split-second-stopwatch/.meta/config.json b/exercises/practice/split-second-stopwatch/.meta/config.json index b550f3c8b..6151395d9 100644 --- a/exercises/practice/split-second-stopwatch/.meta/config.json +++ b/exercises/practice/split-second-stopwatch/.meta/config.json @@ -1,5 +1,7 @@ { - "authors": [], + "authors": [ + "resu-xuniL" + ], "files": { "solution": [ "SplitSecondStopwatch.php" From 35fc4f7c6969d5618894845fd3d935ab51db15f8 Mon Sep 17 00:00:00 2001 From: resu-xuniL Date: Tue, 25 Aug 2026 14:01:00 +0200 Subject: [PATCH 5/6] remove `private(set)` (php >= 8.4) --- .../practice/split-second-stopwatch/.meta/example.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/exercises/practice/split-second-stopwatch/.meta/example.php b/exercises/practice/split-second-stopwatch/.meta/example.php index 91ff4cccd..fed11cafc 100644 --- a/exercises/practice/split-second-stopwatch/.meta/example.php +++ b/exercises/practice/split-second-stopwatch/.meta/example.php @@ -5,10 +5,10 @@ class SplitSecondStopwatch { public function __construct( - private(set) string $state = "ready", - private(set) int $total = 0, - private(set) int $currentLap = 0, - private(set) array $previousLaps = [] + public string $state = "ready", + public int $total = 0, + public int $currentLap = 0, + public array $previousLaps = [] ) { } From b70df5c793d633ff197e34122c0d73c5a2c50216 Mon Sep 17 00:00:00 2001 From: resu-xuniL Date: Fri, 28 Aug 2026 21:29:11 +0200 Subject: [PATCH 6/6] Apply `mk-mxp`'s suggestions --- config.json | 16 ++++---- .../SplitSecondStopwatch.php | 41 +++---------------- 2 files changed, 14 insertions(+), 43 deletions(-) diff --git a/config.json b/config.json index 79b018a77..78d58addd 100644 --- a/config.json +++ b/config.json @@ -977,14 +977,6 @@ "prerequisites": [], "difficulty": 3 }, - { - "slug": "split-second-stopwatch", - "name": "Split-Second Stopwatch", - "uuid": "25befc0a-cd5b-44d6-9f5f-647e12208381", - "practices": [], - "prerequisites": [], - "difficulty": 3 - }, { "slug": "bob", "name": "Bob", @@ -1265,6 +1257,14 @@ "text_formatting" ] }, + { + "slug": "split-second-stopwatch", + "name": "Split-Second Stopwatch", + "uuid": "25befc0a-cd5b-44d6-9f5f-647e12208381", + "practices": [], + "prerequisites": [], + "difficulty": 5 + }, { "slug": "state-of-tic-tac-toe", "name": "State of Tic-Tac-Toe", diff --git a/exercises/practice/split-second-stopwatch/SplitSecondStopwatch.php b/exercises/practice/split-second-stopwatch/SplitSecondStopwatch.php index 8c5108207..226b161c9 100644 --- a/exercises/practice/split-second-stopwatch/SplitSecondStopwatch.php +++ b/exercises/practice/split-second-stopwatch/SplitSecondStopwatch.php @@ -26,43 +26,14 @@ class SplitSecondStopwatch { + /** + * In PHP 8.4 and newer you can use Asymmetric Property Visibility to enhance data encapsulation + * @see https://www.php.net/manual/en/language.oop5.visibility.php#language.oop5.visibility-members-aviz + */ public function __construct() { - throw new \BadMethodCallException(sprintf('Implement the SplitSecondStopwatch %s method', __FUNCTION__)); + throw new \BadMethodCallException(sprintf("Please implement the SplitSecondStopwatch class!")); } - public function advanceTime(string $duration): void - { - throw new \BadMethodCallException(sprintf('Implement the %s method', __FUNCTION__)); - } - - public function getCurrentLap(): string - { - throw new \BadMethodCallException(sprintf('Implement the %s method', __FUNCTION__)); - } - - public function getTotal(): string - { - throw new \BadMethodCallException(sprintf('Implement the %s method', __FUNCTION__)); - } - - public function start(): void - { - throw new \BadMethodCallException(sprintf('Implement the %s method', __FUNCTION__)); - } - - public function stop(): void - { - throw new \BadMethodCallException(sprintf('Implement the %s method', __FUNCTION__)); - } - - public function lap(): void - { - throw new \BadMethodCallException(sprintf('Implement the %s method', __FUNCTION__)); - } - - public function reset(): void - { - throw new \BadMethodCallException(sprintf('Implement the %s method', __FUNCTION__)); - } + // Add methods as expected by the tests! }