From 99e9764578ff7cda3cb5381b798d394d84b3e074 Mon Sep 17 00:00:00 2001 From: glaxxie <86179463+glaxxie@users.noreply.github.com> Date: Sun, 19 Oct 2025 15:46:01 -0500 Subject: [PATCH] [New Exercise]: Split Second Stopwatch Add exercise `split second stopwatch` --- config.json | 8 + .../.docs/instructions.md | 22 ++ .../.docs/introduction.md | 6 + .../.meta/SplitSecondStopwatch.example.ps1 | 97 +++++++ .../split-second-stopwatch/.meta/config.json | 19 ++ .../split-second-stopwatch/.meta/tests.toml | 97 +++++++ .../SplitSecondStopwatch.ps1 | 64 +++++ .../SplitSecondStopwatch.tests.ps1 | 248 ++++++++++++++++++ 8 files changed, 561 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/SplitSecondStopwatch.example.ps1 create mode 100644 exercises/practice/split-second-stopwatch/.meta/config.json create mode 100644 exercises/practice/split-second-stopwatch/.meta/tests.toml create mode 100644 exercises/practice/split-second-stopwatch/SplitSecondStopwatch.ps1 create mode 100644 exercises/practice/split-second-stopwatch/SplitSecondStopwatch.tests.ps1 diff --git a/config.json b/config.json index cf155c6..6d5d6ab 100644 --- a/config.json +++ b/config.json @@ -971,6 +971,14 @@ "prerequisites": [], "difficulty": 4 }, + { + "slug": "split-second-stopwatch", + "name": "Split Second Stopwatch", + "uuid": "09ae15d4-b79e-4666-ad37-58a6545bb4e9", + "practices": [], + "prerequisites": [], + "difficulty": 5 + }, { "slug": "affine-cipher", "name": "Affine Cipher", 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 0000000..30bdc98 --- /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 0000000..a843224 --- /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/SplitSecondStopwatch.example.ps1 b/exercises/practice/split-second-stopwatch/.meta/SplitSecondStopwatch.example.ps1 new file mode 100644 index 0000000..0741c07 --- /dev/null +++ b/exercises/practice/split-second-stopwatch/.meta/SplitSecondStopwatch.example.ps1 @@ -0,0 +1,97 @@ +<# +.SYNOPSIS +Build a stopwatch to keep precise track of lap times +.DESCRIPTION +Implement a stopwatch with these four commands (start, stop, lap, and reset) to keep track of: + 1. The current lap's tracked time + 2. Previously recorded lap times +The stopwatch also should able to report infomation about : current lap, total time and time of previous laps. + +.NOTES +Input and comparison in test suite using string in the format of "HH:MM:SS" for ease of reading. +However implementation should use Timespan as suggested. +#> + +Enum States { + Ready + Running + Stopped +} + +class ClockWrapper { + <# + This class act as a simple timeprovider for the stopwatch + DO NOT DELETE THIS CLASS + #> + [datetime]$Now + ClockWrapper() { + $this.Now = [datetime]::new(0) + } + + [void] Advance([string]$span) { + $this.Now += [TimeSpan]::Parse($span) + } +} + +class SplitSecondStopwatch { + [States]$State = [States]::Ready + [Timespan]$Total = [TimeSpan]::Zero + [Timespan[]]$PreviousLaps = @() + [ClockWrapper] hidden $_internalClock + [datetime] hidden $_startTime + [Timespan] hidden $_tracking + + SplitSecondStopwatch([object]$clock) { + $this._internalClock = $clock + } + + [TimeSpan] GetCurrentLap() { + if ($this.State -eq [States]::Ready) { + return [TimeSpan]::Zero + } elseif ($this.State -eq [States]::Running) { + return $this._tracking + ($this._internalClock.Now - $this._startTime) + } else { + return $this._tracking + } + } + + [TimeSpan] GetTotal() { + if ($this.State -eq [States]::Ready) { + return [TimeSpan]::Zero + } + $totalMs = ($this.PreviousLaps | ForEach-Object { $_.TotalMilliseconds } | Measure-Object -Sum).Sum + return [TimeSpan]::FromMilliseconds($totalMs) + $this.GetCurrentLap() + } + + [void] Start() { + if ($this.State -eq [States]::Running) { + throw "Invalid Operation" + } + $this.State = [States]::Running + $this._startTime = $this._internalClock.Now + } + + [void] Stop() { + if ($this.State -ne [States]::Running) { + throw "Invalid Operation" + } + $this._tracking = $this._internalClock.Now - $this._startTime + $this.State = [States]::Stopped + } + + [void] Lap() { + if ($this.State -ne [States]::Running) { + throw "Invalid Operation" + } + $this.PreviousLaps += $this.GetCurrentLap() + $this._startTime = $this._internalClock.Now + } + + [void] Reset() { + if ($this.State -ne [States]::Stopped) { + throw "Invalid Operation" + } + $this.State = [States]::Ready + $this.PreviousLaps = @() + } +} 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 0000000..00db03e --- /dev/null +++ b/exercises/practice/split-second-stopwatch/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "glaxxie" + ], + "files": { + "solution": [ + "SplitSecondStopwatch.ps1" + ], + "test": [ + "SplitSecondStopwatch.tests.ps1" + ], + "example": [ + ".meta/SplitSecondStopwatch.example.ps1" + ] + }, + "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/tests.toml b/exercises/practice/split-second-stopwatch/.meta/tests.toml new file mode 100644 index 0000000..323cb7a --- /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.ps1 b/exercises/practice/split-second-stopwatch/SplitSecondStopwatch.ps1 new file mode 100644 index 0000000..f31c417 --- /dev/null +++ b/exercises/practice/split-second-stopwatch/SplitSecondStopwatch.ps1 @@ -0,0 +1,64 @@ +<# +.SYNOPSIS +Build a stopwatch to keep precise track of lap times +.DESCRIPTION +Implement a stopwatch with these four commands (start, stop, lap, and reset) to keep track of: + 1. The current lap's tracked time + 2. Previously recorded lap times +The stopwatch also should able to report infomation about : current lap, total time and time of previous laps. + +.NOTES +Input and comparison in test suite using string in the format of "HH:MM:SS" for ease of reading. +However implementation should use Timespan as suggested. +#> + +Enum States { + Ready + Running + Stopped +} + +class ClockWrapper { + <# + This class act as a simple timeprovider for the stopwatch + DO NOT DELETE THIS CLASS + #> + [datetime]$Now + ClockWrapper() { + $this.Now = [datetime]::new(0) + } + + [void] Advance([string]$span) { + $this.Now += [TimeSpan]::Parse($span) + } +} + +class SplitSecondStopwatch { + [States]$State = [States]::Ready + [Timespan]$Total = [TimeSpan]::Zero + [Timespan[]]$PreviousLaps = @() + + [TimeSpan] GetCurrentLap() { + Throw "Please implement this function" + } + + [TimeSpan] GetTotal() { + Throw "Please implement this function" + } + + [void] Start() { + Throw "Please implement this function" + } + + [void] Stop() { + Throw "Please implement this function" + } + + [void] Lap() { + Throw "Please implement this function" + } + + [void] Reset() { + Throw "Please implement this function" + } +} diff --git a/exercises/practice/split-second-stopwatch/SplitSecondStopwatch.tests.ps1 b/exercises/practice/split-second-stopwatch/SplitSecondStopwatch.tests.ps1 new file mode 100644 index 0000000..28c0d1f --- /dev/null +++ b/exercises/practice/split-second-stopwatch/SplitSecondStopwatch.tests.ps1 @@ -0,0 +1,248 @@ +BeforeAll { + . "./SplitSecondStopwatch.ps1" +} + +Describe "SplitSecondStopwatch test cases" { + BeforeEach { + $clock = [ClockWrapper]::new() + $stopWatch = [SplitSecondStopwatch]::new($clock) + } + + It "new stopwatch starts in ready state" { + $stopWatch.State | Should -Be ([States]::Ready) + } + + It "new stopwatch's current lap has no elapsed time" { + $stopWatch.GetCurrentLap() | Should -BeExactly "00:00:00" + } + + It "new stopwatch's total has no elapsed time" { + $stopWatch.GetTotal() | Should -BeExactly "00:00:00" + } + + It "new stopwatch does not have previous laps" { + $stopWatch.PreviousLaps | Should -BeExactly @() + } + + It "start from ready state changes state to running" { + $stopWatch.Start() + $stopWatch.State | Should -Be ([States]::Running) + } + + It "start does not change previous laps" { + $stopWatch.Start() + $stopWatch.PreviousLaps | Should -BeExactly @() + } + + It "start initiates time tracking for current lap" { + $stopWatch.Start() + $clock.Advance("00:00:05") + $stopWatch.GetCurrentLap() | Should -BeExactly "00:00:05" + } + + It "start initiates time tracking for total" { + $stopWatch.Start() + $clock.Advance("00:00:23") + $stopWatch.GetTotal() | Should -BeExactly "00:00:23" + } + + It "start cannot be called from running state" { + $stopWatch.Start() + + { $stopWatch.Start() } | Should -Throw "*Invalid Operation*" + } + + It "stop from running state changes state to stopped" { + $stopWatch.Start() + $stopWatch.Stop() + + $stopWatch.State | Should -Be ([States]::Stopped) + } + + It "stop pauses time tracking for current lap" { + $stopWatch.Start() + $clock.Advance("00:00:05") + $stopWatch.Stop() + $clock.Advance("00:00:08") + + $stopWatch.GetCurrentLap() | Should -BeExactly "00:00:05" + } + + It "stop pauses time tracking for total" { + $stopWatch.Start() + $clock.Advance("00:00:13") + $stopWatch.Stop() + $clock.Advance("00:00:33") + + $stopWatch.GetTotal() | Should -BeExactly "00:00:13" + } + + It "stop cannot be called from ready state" { + { $stopWatch.Stop() } | Should -Throw "*Invalid Operation*" + } + + It "stop cannot be called from stopped state" { + $stopWatch.Start() + $stopWatch.Stop() + + { $stopWatch.Stop() } | Should -Throw "*Invalid Operation*" + } + + It "start from stopped state changes state to running" { + $stopWatch.Start() + $stopWatch.Stop() + $stopWatch.Start() + + $stopWatch.State | Should -Be ([States]::Running) + } + + It "start from stopped state resumes time tracking for current lap" { + $stopWatch.Start() + $clock.Advance("00:01:20") + $stopWatch.Stop() + $clock.Advance("00:00:20") + $stopWatch.Start() + $clock.Advance("00:00:08") + + $stopWatch.GetCurrentLap() | Should -BeExactly "00:01:28" + } + + It "start from stopped state resumes time tracking for total" { + $stopWatch.Start() + $clock.Advance("00:00:23") + $stopWatch.Stop() + $clock.Advance("00:00:44") + $stopWatch.Start() + $clock.Advance("00:00:09") + + $stopWatch.GetTotal() | Should -BeExactly "00:00:32" + } + + It "lap adds current lap to previous laps" { + $stopWatch.Start() + $clock.Advance("00:01:38") + $stopWatch.Lap() + + ($stopWatch.PreviousLaps | ForEach-Object ToString) | Should -Be @("00:01:38") + + $clock.Advance("00:00:44") + $stopWatch.Lap() + + ($stopWatch.PreviousLaps | ForEach-Object ToString) | Should -Be @("00:01:38", "00:00:44") + } + + It "lap resets current lap and resumes time tracking" { + $stopWatch.Start() + $clock.Advance("00:08:22") + $stopWatch.Lap() + + $stopWatch.GetCurrentLap() | Should -Be @("00:00:00") + + $clock.Advance("00:00:15") + + $stopWatch.GetCurrentLap() | Should -Be @("00:00:15") + } + + It "lap continues time tracking for total" { + $stopWatch.Start() + $clock.Advance("00:00:22") + $stopWatch.Lap() + $clock.Advance("00:00:33") + + $stopWatch.GetTotal() | Should -BeExactly "00:00:55" + } + + It "lap cannot be called from ready state" { + { $stopWatch.Lap() } | Should -Throw "*Invalid Operation*" + } + + It "lap cannot be called from stopped state" { + $stopWatch.Start() + $stopWatch.Stop() + + { $stopWatch.Lap() } | Should -Throw "*Invalid Operation*" + } + + It "stop does not change previous laps" { + $stopWatch.Start() + $clock.Advance("00:11:22") + $stopWatch.Lap() + + $stopWatch.PreviousLaps | Should -BeExactly @("00:11:22") + + $stopWatch.Stop() + + $stopWatch.PreviousLaps | Should -BeExactly @("00:11:22") + } + + It "reset from stopped state changes state to ready" { + $stopWatch.Start() + $stopWatch.Stop() + $stopWatch.Reset() + + $stopWatch.State | Should -Be ([States]::Ready) + } + + It "reset resets current lap" { + $stopWatch.Start() + $clock.Advance("00:00:10") + $stopWatch.Stop() + $stopWatch.Reset() + + $stopWatch.GetCurrentLap() | Should -Be "00:00:00" + } + + It "reset clears previous laps" { + $stopWatch.Start() + $clock.Advance("00:00:10") + $stopWatch.Lap() + $clock.Advance("00:00:20") + $stopWatch.Lap() + + $stopWatch.PreviousLaps | Should -Be @("00:00:10", "00:00:20") + + $stopWatch.Stop() + $stopWatch.Reset() + + $stopWatch.PreviousLaps | Should -Be @() + } + + It "reset cannot be called from ready state" { + { $stopWatch.Reset() } | Should -Throw "*Invalid Operation*" + } + + It "reset cannot be called from running state" { + $stopWatch.Start() + + { $stopWatch.Reset() } | Should -Throw "*Invalid Operation*" + } + + It "supports very long laps" { + $stopWatch.Start() + $clock.Advance("01:23:45") + + $stopWatch.GetCurrentLap() | Should -Be "01:23:45" + + $stopWatch.Lap() + + $stopWatch.PreviousLaps | Should -Be @("01:23:45") + + $clock.Advance("04:01:40") + + $stopWatch.GetCurrentLap() | Should -Be "04:01:40" + $stopWatch.GetTotal() | Should -Be "05:25:25" + + $stopWatch.Lap() + + $stopWatch.PreviousLaps | Should -Be @("01:23:45", "04:01:40") + + $clock.Advance("08:43:05") + + $stopWatch.GetCurrentLap() | Should -Be "08:43:05" + $stopWatch.GetTotal() | Should -Be "14:08:30" + + $stopWatch.Lap() + + $stopWatch.PreviousLaps | Should -Be @("01:23:45", "04:01:40", "08:43:05") + } +}