From 25fdd1403a92fa92c5bbfaebc3b8102d32419c3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20B=20Nagy?= <20251272+BNAndras@users.noreply.github.com> Date: Thu, 5 Mar 2026 22:00:25 -0800 Subject: [PATCH 1/4] add `split-second-stopwatch` --- config.json | 11 + .../.docs/instructions.md | 22 ++ .../.docs/introduction.md | 6 + .../split-second-stopwatch/.gitignore | 5 + .../split-second-stopwatch/.meta/config.json | 25 ++ .../split-second-stopwatch/.meta/proof.ci.js | 77 ++++++ .../split-second-stopwatch/.meta/tests.toml | 97 +++++++ .../practice/split-second-stopwatch/.npmrc | 1 + .../practice/split-second-stopwatch/LICENSE | 21 ++ .../split-second-stopwatch/babel.config.js | 4 + .../split-second-stopwatch/eslint.config.mjs | 45 ++++ .../split-second-stopwatch/jest.config.js | 22 ++ .../split-second-stopwatch/package.json | 34 +++ .../split-second-stopwatch.js | 77 ++++++ .../split-second-stopwatch.spec.js | 239 ++++++++++++++++++ 15 files changed, 686 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/.gitignore create mode 100644 exercises/practice/split-second-stopwatch/.meta/config.json create mode 100644 exercises/practice/split-second-stopwatch/.meta/proof.ci.js create mode 100644 exercises/practice/split-second-stopwatch/.meta/tests.toml create mode 100644 exercises/practice/split-second-stopwatch/.npmrc create mode 100644 exercises/practice/split-second-stopwatch/LICENSE create mode 100644 exercises/practice/split-second-stopwatch/babel.config.js create mode 100644 exercises/practice/split-second-stopwatch/eslint.config.mjs create mode 100644 exercises/practice/split-second-stopwatch/jest.config.js create mode 100644 exercises/practice/split-second-stopwatch/package.json create mode 100644 exercises/practice/split-second-stopwatch/split-second-stopwatch.js create mode 100644 exercises/practice/split-second-stopwatch/split-second-stopwatch.spec.js diff --git a/config.json b/config.json index 90c064066b..0ac62ffcd5 100644 --- a/config.json +++ b/config.json @@ -671,6 +671,17 @@ "text_formatting" ] }, + { + "slug": "split-second-stopwatch", + "name": "Split Second Stopwatch", + "uuid": "8ddc2921-c0f6-400e-bb74-c2bec51b9d63", + "practices": [], + "prerequisites": [ + "classes", + "numbers" + ], + "difficulty": 4 + }, { "slug": "linked-list", "name": "Linked List", 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 0000000000..30bdc988da --- /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 0000000000..a843224771 --- /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/.gitignore b/exercises/practice/split-second-stopwatch/.gitignore new file mode 100644 index 0000000000..0c88ff6ec3 --- /dev/null +++ b/exercises/practice/split-second-stopwatch/.gitignore @@ -0,0 +1,5 @@ +/node_modules +/bin/configlet +/bin/configlet.exe +/package-lock.json +/yarn.lock 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 0000000000..4503fabbdd --- /dev/null +++ b/exercises/practice/split-second-stopwatch/.meta/config.json @@ -0,0 +1,25 @@ +{ + "authors": [ + "BNAndras" + ], + "files": { + "solution": [ + "split-second-stopwatch.js" + ], + "test": [ + "split-second-stopwatch.spec.js" + ], + "example": [ + ".meta/proof.ci.js" + ] + }, + "blurb": "Keep track of time through a digital stopwatch.", + "source": "Erik Schierboom", + "source_url": "https://github.com/exercism/problem-specifications/pull/2547", + "custom": { + "version.tests.compatibility": "jest-27", + "flag.tests.task-per-describe": false, + "flag.tests.may-run-long": false, + "flag.tests.includes-optional": false + } +} diff --git a/exercises/practice/split-second-stopwatch/.meta/proof.ci.js b/exercises/practice/split-second-stopwatch/.meta/proof.ci.js new file mode 100644 index 0000000000..9d4e19e92d --- /dev/null +++ b/exercises/practice/split-second-stopwatch/.meta/proof.ci.js @@ -0,0 +1,77 @@ +export class SplitSecondStopwatch { + constructor() { + this._state = 'ready'; + this._totalSeconds = 0; + this._currentLap = 0; + this._previousLaps = []; + } + + get state() { + return this._state; + } + + get currentLap() { + return this._formatTime(this._currentLap); + } + + get total() { + return this._formatTime(this._totalSeconds); + } + + get previousLaps() { + return this._previousLaps.map(this._formatTime); + } + + start() { + if (this._state === 'running') { + throw new Error('cannot start an already running stopwatch'); + } + this._state = 'running'; + } + + stop() { + if (this._state !== 'running') { + throw new Error('cannot stop a stopwatch that is not running'); + } + this._state = 'stopped'; + } + + lap() { + if (this._state !== 'running') { + throw new Error('cannot lap a stopwatch that is not running'); + } + this._previousLaps.push(this._currentLap); + this._currentLap = 0; + } + + reset() { + if (this._state !== 'stopped') { + throw new Error('cannot reset a stopwatch that is not stopped'); + } + this._state = 'ready'; + this._totalSeconds = 0; + this._currentLap = 0; + this._previousLaps = []; + } + + advanceTime(duration) { + if (this._state === 'running') { + const seconds = this._toSeconds(duration); + this._currentLap += seconds; + this._totalSeconds += seconds; + } + } + + _toSeconds(duration) { + const [h, m, s] = duration.split(':').map(Number); + return h * 3600 + m * 60 + s; + } + + _formatTime(seconds) { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + + return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; + } +} 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 0000000000..323cb7ae8f --- /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/.npmrc b/exercises/practice/split-second-stopwatch/.npmrc new file mode 100644 index 0000000000..d26df800bb --- /dev/null +++ b/exercises/practice/split-second-stopwatch/.npmrc @@ -0,0 +1 @@ +audit=false diff --git a/exercises/practice/split-second-stopwatch/LICENSE b/exercises/practice/split-second-stopwatch/LICENSE new file mode 100644 index 0000000000..90e73be03b --- /dev/null +++ b/exercises/practice/split-second-stopwatch/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Exercism + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/exercises/practice/split-second-stopwatch/babel.config.js b/exercises/practice/split-second-stopwatch/babel.config.js new file mode 100644 index 0000000000..a638497df1 --- /dev/null +++ b/exercises/practice/split-second-stopwatch/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]], + plugins: [], +}; diff --git a/exercises/practice/split-second-stopwatch/eslint.config.mjs b/exercises/practice/split-second-stopwatch/eslint.config.mjs new file mode 100644 index 0000000000..ca517111ed --- /dev/null +++ b/exercises/practice/split-second-stopwatch/eslint.config.mjs @@ -0,0 +1,45 @@ +// @ts-check + +import config from '@exercism/eslint-config-javascript'; +import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs'; + +import globals from 'globals'; + +export default [ + ...config, + ...maintainersConfig, + { + files: maintainersConfig[1].files, + rules: { + 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }], + }, + }, + { + files: ['scripts/**/*.mjs'], + languageOptions: { + globals: { + ...globals.node, + }, + }, + }, + // <> + { + ignores: [ + // # Protected or generated + '/.appends/**/*', + '/.github/**/*', + '/.vscode/**/*', + + // # Binaries + '/bin/*', + + // # Configuration + '/config', + '/babel.config.js', + + // # Typings + '/exercises/**/global.d.ts', + '/exercises/**/env.d.ts', + ], + }, +]; diff --git a/exercises/practice/split-second-stopwatch/jest.config.js b/exercises/practice/split-second-stopwatch/jest.config.js new file mode 100644 index 0000000000..ec8e908127 --- /dev/null +++ b/exercises/practice/split-second-stopwatch/jest.config.js @@ -0,0 +1,22 @@ +module.exports = { + verbose: true, + projects: [''], + testMatch: [ + '**/__tests__/**/*.[jt]s?(x)', + '**/test/**/*.[jt]s?(x)', + '**/?(*.)+(spec|test).[jt]s?(x)', + ], + testPathIgnorePatterns: [ + '/(?:production_)?node_modules/', + '.d.ts$', + '/test/fixtures', + '/test/helpers', + '__mocks__', + ], + transform: { + '^.+\\.[jt]sx?$': 'babel-jest', + }, + moduleNameMapper: { + '^(\\.\\/.+)\\.js$': '$1', + }, +}; diff --git a/exercises/practice/split-second-stopwatch/package.json b/exercises/practice/split-second-stopwatch/package.json new file mode 100644 index 0000000000..93c216d206 --- /dev/null +++ b/exercises/practice/split-second-stopwatch/package.json @@ -0,0 +1,34 @@ +{ + "name": "@exercism/javascript-split-second-stopwatch", + "description": "Exercism exercises in Javascript.", + "author": "Katrina Owen", + "private": true, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/exercism/javascript", + "directory": "exercises/practice/split-second-stopwatch" + }, + "devDependencies": { + "@exercism/babel-preset-javascript": "^0.5.1", + "@exercism/eslint-config-javascript": "^0.8.1", + "@jest/globals": "^29.7.0", + "@types/node": "^24.3.0", + "@types/shelljs": "^0.8.17", + "babel-jest": "^29.7.0", + "core-js": "~3.42.0", + "diff": "^8.0.2", + "eslint": "^9.28.0", + "expect": "^29.7.0", + "globals": "^16.3.0", + "jest": "^29.7.0" + }, + "dependencies": {}, + "scripts": { + "lint": "corepack pnpm eslint .", + "test": "corepack pnpm jest", + "watch": "corepack pnpm jest --watch", + "format": "corepack pnpm prettier -w ." + }, + "packageManager": "pnpm@9.15.2" +} diff --git a/exercises/practice/split-second-stopwatch/split-second-stopwatch.js b/exercises/practice/split-second-stopwatch/split-second-stopwatch.js new file mode 100644 index 0000000000..a532e71485 --- /dev/null +++ b/exercises/practice/split-second-stopwatch/split-second-stopwatch.js @@ -0,0 +1,77 @@ +export class SplitSecondStopwatch { + constructor() { + this._state = 'ready'; + this._totalSeconds = 0; + this._currentLap = 0; + this._previousLaps = []; + } + + get state() { + return this._state; + } + + get currentLap() { + return this._formatTime(this._currentLap); + } + + get total() { + return this._formatTime(this._totalSeconds); + } + + get previousLaps() { + return this._previousLaps.map((s) => this._formatTime(s)); + } + + start() { + if (this._state === 'running') { + throw new Error('cannot start an already running stopwatch'); + } + this._state = 'running'; + } + + stop() { + if (this._state !== 'running') { + throw new Error('cannot stop a stopwatch that is not running'); + } + this._state = 'stopped'; + } + + lap() { + if (this._state !== 'running') { + throw new Error('cannot lap a stopwatch that is not running'); + } + this._previousLaps.push(this._currentLap); + this._currentLap = 0; + } + + reset() { + if (this._state !== 'stopped') { + throw new Error('cannot reset a stopwatch that is not stopped'); + } + this._state = 'ready'; + this._totalSeconds = 0; + this._currentLap = 0; + this._previousLaps = []; + } + + advanceTime(duration) { + if (this._state === 'running') { + const seconds = this._toSeconds(duration); + this._currentLap += seconds; + this._totalSeconds += seconds; + } + } + + _toSeconds(duration) { + const [h, m, s] = duration.split(':').map(Number); + return h * 3600 + m * 60 + s; + } + + _formatTime(seconds) { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + + return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; + } +} diff --git a/exercises/practice/split-second-stopwatch/split-second-stopwatch.spec.js b/exercises/practice/split-second-stopwatch/split-second-stopwatch.spec.js new file mode 100644 index 0000000000..c99344966d --- /dev/null +++ b/exercises/practice/split-second-stopwatch/split-second-stopwatch.spec.js @@ -0,0 +1,239 @@ +import { describe, expect, test, xtest } from "@jest/globals"; +import { SplitSecondStopwatch } from "./split-second-stopwatch"; + +describe("SplitSecondStopwatch", () => { + test("new stopwatch starts in ready state", () => { + const stopwatch = new SplitSecondStopwatch(); + expect(stopwatch.state).toBe("ready"); + }); + + xtest("new stopwatch's current lap has no elapsed time", () => { + const stopwatch = new SplitSecondStopwatch(); + expect(stopwatch.currentLap).toBe("00:00:00"); + }); + + xtest("new stopwatch's total has no elapsed time", () => { + const stopwatch = new SplitSecondStopwatch(); + expect(stopwatch.total).toBe("00:00:00"); + }); + + xtest("new stopwatch does not have previous laps", () => { + const stopwatch = new SplitSecondStopwatch(); + expect(stopwatch.previousLaps).toEqual([]); + }); + + xtest("start from ready state changes state to running", () => { + const stopwatch = new SplitSecondStopwatch(); + stopwatch.start(); + expect(stopwatch.state).toBe("running"); + }); + + xtest("start does not change previous laps", () => { + const stopwatch = new SplitSecondStopwatch(); + stopwatch.start(); + expect(stopwatch.previousLaps).toEqual([]); + }); + + xtest("start initiates time tracking for current lap", () => { + const stopwatch = new SplitSecondStopwatch(); + stopwatch.start(); + stopwatch.advanceTime("00:00:05"); + expect(stopwatch.currentLap).toBe("00:00:05"); + }); + + xtest("start initiates time tracking for total", () => { + const stopwatch = new SplitSecondStopwatch(); + stopwatch.start(); + stopwatch.advanceTime("00:00:23"); + expect(stopwatch.total).toBe("00:00:23"); + }); + + xtest("start cannot be called from running state", () => { + const stopwatch = new SplitSecondStopwatch(); + stopwatch.start(); + expect(() => stopwatch.start()).toThrow("cannot start an already running stopwatch"); + }); + + xtest("stop from running state changes state to stopped", () => { + const stopwatch = new SplitSecondStopwatch(); + stopwatch.start(); + stopwatch.stop(); + expect(stopwatch.state).toBe("stopped"); + }); + + xtest("stop pauses time tracking for current lap", () => { + const stopwatch = new SplitSecondStopwatch(); + stopwatch.start(); + stopwatch.advanceTime("00:00:05"); + stopwatch.stop(); + stopwatch.advanceTime("00:00:08"); + expect(stopwatch.currentLap).toBe("00:00:05"); + }); + + xtest("stop pauses time tracking for total", () => { + const stopwatch = new SplitSecondStopwatch(); + stopwatch.start(); + stopwatch.advanceTime("00:00:13"); + stopwatch.stop(); + stopwatch.advanceTime("00:00:44"); + expect(stopwatch.total).toBe("00:00:13"); + }); + + xtest("stop cannot be called from ready state", () => { + const stopwatch = new SplitSecondStopwatch(); + expect(() => stopwatch.stop()).toThrow("cannot stop a stopwatch that is not running"); + }); + + xtest("stop cannot be called from stopped state", () => { + const stopwatch = new SplitSecondStopwatch(); + stopwatch.start(); + stopwatch.stop(); + expect(() => stopwatch.stop()).toThrow("cannot stop a stopwatch that is not running"); + }); + + xtest("start from stopped state changes state to running", () => { + const stopwatch = new SplitSecondStopwatch(); + stopwatch.start(); + stopwatch.stop(); + stopwatch.start(); + expect(stopwatch.state).toBe("running"); + }); + + xtest("start from stopped state resumes time tracking for current lap", () => { + const stopwatch = new SplitSecondStopwatch(); + stopwatch.start(); + stopwatch.advanceTime("00:01:20"); + stopwatch.stop(); + stopwatch.advanceTime("00:00:20"); + stopwatch.start(); + stopwatch.advanceTime("00:00:08"); + expect(stopwatch.currentLap).toBe("00:01:28"); + }); + + xtest("start from stopped state resumes time tracking for total", () => { + const stopwatch = new SplitSecondStopwatch(); + stopwatch.start(); + stopwatch.advanceTime("00:00:23"); + stopwatch.stop(); + stopwatch.advanceTime("00:00:44"); + stopwatch.start(); + stopwatch.advanceTime("00:00:09"); + expect(stopwatch.total).toBe("00:00:32"); + }); + + xtest("lap adds current lap to previous laps", () => { + const stopwatch = new SplitSecondStopwatch(); + stopwatch.start(); + stopwatch.advanceTime("00:01:38"); + stopwatch.lap(); + expect(stopwatch.previousLaps).toEqual(["00:01:38"]); + stopwatch.advanceTime("00:00:44"); + stopwatch.lap(); + expect(stopwatch.previousLaps).toEqual(["00:01:38", "00:00:44"]); + }); + + xtest("lap resets current lap and resumes time tracking", () => { + const stopwatch = new SplitSecondStopwatch(); + stopwatch.start(); + stopwatch.advanceTime("00:08:22"); + stopwatch.lap(); + expect(stopwatch.currentLap).toBe("00:00:00"); + stopwatch.advanceTime("00:00:15"); + expect(stopwatch.currentLap).toBe("00:00:15"); + }); + + xtest("lap continues time tracking for total", () => { + const stopwatch = new SplitSecondStopwatch(); + stopwatch.start(); + stopwatch.advanceTime("00:00:22"); + stopwatch.lap(); + stopwatch.advanceTime("00:00:33"); + expect(stopwatch.total).toBe("00:00:55"); + }); + + xtest("lap cannot be called from ready state", () => { + const stopwatch = new SplitSecondStopwatch(); + expect(() => stopwatch.lap()).toThrow("cannot lap a stopwatch that is not running"); + }); + + xtest("lap cannot be called from stopped state", () => { + const stopwatch = new SplitSecondStopwatch(); + stopwatch.start(); + stopwatch.stop(); + expect(() => stopwatch.lap()).toThrow("cannot lap a stopwatch that is not running"); + }); + + xtest("stop does not change previous laps", () => { + const stopwatch = new SplitSecondStopwatch(); + stopwatch.start(); + stopwatch.advanceTime("00:11:22"); + stopwatch.lap(); + expect(stopwatch.previousLaps).toEqual(["00:11:22"]); + stopwatch.stop(); + expect(stopwatch.previousLaps).toEqual(["00:11:22"]); + }); + + xtest("reset from stopped state changes state to ready", () => { + const stopwatch = new SplitSecondStopwatch(); + stopwatch.start(); + stopwatch.stop(); + stopwatch.reset(); + expect(stopwatch.state).toBe("ready"); + }); + + xtest("reset resets current lap", () => { + const stopwatch = new SplitSecondStopwatch(); + stopwatch.start(); + stopwatch.advanceTime("00:00:10"); + stopwatch.stop(); + stopwatch.reset(); + expect(stopwatch.currentLap).toBe("00:00:00"); + }); + + xtest("reset clears previous laps", () => { + const stopwatch = new SplitSecondStopwatch(); + stopwatch.start(); + stopwatch.advanceTime("00:00:10"); + stopwatch.lap(); + stopwatch.advanceTime("00:00:20"); + stopwatch.lap(); + expect(stopwatch.previousLaps).toEqual(["00:00:10", "00:00:20"]); + stopwatch.stop(); + stopwatch.reset(); + expect(stopwatch.previousLaps).toEqual([]); + }); + + xtest("reset cannot be called from ready state", () => { + const stopwatch = new SplitSecondStopwatch(); + expect(() => stopwatch.reset()).toThrow("cannot reset a stopwatch that is not stopped"); + }); + + xtest("reset cannot be called from running state", () => { + const stopwatch = new SplitSecondStopwatch(); + stopwatch.start(); + expect(() => stopwatch.reset()).toThrow("cannot reset a stopwatch that is not stopped"); + }); + + xtest("supports very long laps", () => { + const stopwatch = new SplitSecondStopwatch(); + stopwatch.start(); + stopwatch.advanceTime("01:23:45"); + expect(stopwatch.currentLap).toBe("01:23:45"); + stopwatch.lap(); + expect(stopwatch.previousLaps).toEqual(["01:23:45"]); + stopwatch.advanceTime("04:01:40"); + expect(stopwatch.currentLap).toBe("04:01:40"); + expect(stopwatch.total).toBe("05:25:25"); + stopwatch.lap(); + expect(stopwatch.previousLaps).toEqual(["01:23:45", "04:01:40"]); + stopwatch.advanceTime("08:43:05"); + expect(stopwatch.currentLap).toBe("08:43:05"); + expect(stopwatch.total).toBe("14:08:30"); + stopwatch.lap(); + expect(stopwatch.previousLaps).toEqual([ + "01:23:45", + "04:01:40", + "08:43:05", + ]); + }); +}); From 491a0cbb9c4c5cb78d1bb697e9d971acdc547944 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 6 Mar 2026 06:22:15 +0000 Subject: [PATCH 2/4] [CI] Format code --- .../split-second-stopwatch.spec.js | 202 ++++++++++-------- 1 file changed, 108 insertions(+), 94 deletions(-) diff --git a/exercises/practice/split-second-stopwatch/split-second-stopwatch.spec.js b/exercises/practice/split-second-stopwatch/split-second-stopwatch.spec.js index c99344966d..a782550840 100644 --- a/exercises/practice/split-second-stopwatch/split-second-stopwatch.spec.js +++ b/exercises/practice/split-second-stopwatch/split-second-stopwatch.spec.js @@ -1,239 +1,253 @@ -import { describe, expect, test, xtest } from "@jest/globals"; -import { SplitSecondStopwatch } from "./split-second-stopwatch"; +import { describe, expect, test, xtest } from '@jest/globals'; +import { SplitSecondStopwatch } from './split-second-stopwatch'; -describe("SplitSecondStopwatch", () => { - test("new stopwatch starts in ready state", () => { +describe('SplitSecondStopwatch', () => { + test('new stopwatch starts in ready state', () => { const stopwatch = new SplitSecondStopwatch(); - expect(stopwatch.state).toBe("ready"); + expect(stopwatch.state).toBe('ready'); }); xtest("new stopwatch's current lap has no elapsed time", () => { const stopwatch = new SplitSecondStopwatch(); - expect(stopwatch.currentLap).toBe("00:00:00"); + expect(stopwatch.currentLap).toBe('00:00:00'); }); xtest("new stopwatch's total has no elapsed time", () => { const stopwatch = new SplitSecondStopwatch(); - expect(stopwatch.total).toBe("00:00:00"); + expect(stopwatch.total).toBe('00:00:00'); }); - xtest("new stopwatch does not have previous laps", () => { + xtest('new stopwatch does not have previous laps', () => { const stopwatch = new SplitSecondStopwatch(); expect(stopwatch.previousLaps).toEqual([]); }); - xtest("start from ready state changes state to running", () => { + xtest('start from ready state changes state to running', () => { const stopwatch = new SplitSecondStopwatch(); stopwatch.start(); - expect(stopwatch.state).toBe("running"); + expect(stopwatch.state).toBe('running'); }); - xtest("start does not change previous laps", () => { + xtest('start does not change previous laps', () => { const stopwatch = new SplitSecondStopwatch(); stopwatch.start(); expect(stopwatch.previousLaps).toEqual([]); }); - xtest("start initiates time tracking for current lap", () => { + xtest('start initiates time tracking for current lap', () => { const stopwatch = new SplitSecondStopwatch(); stopwatch.start(); - stopwatch.advanceTime("00:00:05"); - expect(stopwatch.currentLap).toBe("00:00:05"); + stopwatch.advanceTime('00:00:05'); + expect(stopwatch.currentLap).toBe('00:00:05'); }); - xtest("start initiates time tracking for total", () => { + xtest('start initiates time tracking for total', () => { const stopwatch = new SplitSecondStopwatch(); stopwatch.start(); - stopwatch.advanceTime("00:00:23"); - expect(stopwatch.total).toBe("00:00:23"); + stopwatch.advanceTime('00:00:23'); + expect(stopwatch.total).toBe('00:00:23'); }); - xtest("start cannot be called from running state", () => { + xtest('start cannot be called from running state', () => { const stopwatch = new SplitSecondStopwatch(); stopwatch.start(); - expect(() => stopwatch.start()).toThrow("cannot start an already running stopwatch"); + expect(() => stopwatch.start()).toThrow( + 'cannot start an already running stopwatch', + ); }); - xtest("stop from running state changes state to stopped", () => { + xtest('stop from running state changes state to stopped', () => { const stopwatch = new SplitSecondStopwatch(); stopwatch.start(); stopwatch.stop(); - expect(stopwatch.state).toBe("stopped"); + expect(stopwatch.state).toBe('stopped'); }); - xtest("stop pauses time tracking for current lap", () => { + xtest('stop pauses time tracking for current lap', () => { const stopwatch = new SplitSecondStopwatch(); stopwatch.start(); - stopwatch.advanceTime("00:00:05"); + stopwatch.advanceTime('00:00:05'); stopwatch.stop(); - stopwatch.advanceTime("00:00:08"); - expect(stopwatch.currentLap).toBe("00:00:05"); + stopwatch.advanceTime('00:00:08'); + expect(stopwatch.currentLap).toBe('00:00:05'); }); - xtest("stop pauses time tracking for total", () => { + xtest('stop pauses time tracking for total', () => { const stopwatch = new SplitSecondStopwatch(); stopwatch.start(); - stopwatch.advanceTime("00:00:13"); + stopwatch.advanceTime('00:00:13'); stopwatch.stop(); - stopwatch.advanceTime("00:00:44"); - expect(stopwatch.total).toBe("00:00:13"); + stopwatch.advanceTime('00:00:44'); + expect(stopwatch.total).toBe('00:00:13'); }); - xtest("stop cannot be called from ready state", () => { + xtest('stop cannot be called from ready state', () => { const stopwatch = new SplitSecondStopwatch(); - expect(() => stopwatch.stop()).toThrow("cannot stop a stopwatch that is not running"); + expect(() => stopwatch.stop()).toThrow( + 'cannot stop a stopwatch that is not running', + ); }); - xtest("stop cannot be called from stopped state", () => { + xtest('stop cannot be called from stopped state', () => { const stopwatch = new SplitSecondStopwatch(); stopwatch.start(); stopwatch.stop(); - expect(() => stopwatch.stop()).toThrow("cannot stop a stopwatch that is not running"); + expect(() => stopwatch.stop()).toThrow( + 'cannot stop a stopwatch that is not running', + ); }); - xtest("start from stopped state changes state to running", () => { + xtest('start from stopped state changes state to running', () => { const stopwatch = new SplitSecondStopwatch(); stopwatch.start(); stopwatch.stop(); stopwatch.start(); - expect(stopwatch.state).toBe("running"); + expect(stopwatch.state).toBe('running'); }); - xtest("start from stopped state resumes time tracking for current lap", () => { + xtest('start from stopped state resumes time tracking for current lap', () => { const stopwatch = new SplitSecondStopwatch(); stopwatch.start(); - stopwatch.advanceTime("00:01:20"); + stopwatch.advanceTime('00:01:20'); stopwatch.stop(); - stopwatch.advanceTime("00:00:20"); + stopwatch.advanceTime('00:00:20'); stopwatch.start(); - stopwatch.advanceTime("00:00:08"); - expect(stopwatch.currentLap).toBe("00:01:28"); + stopwatch.advanceTime('00:00:08'); + expect(stopwatch.currentLap).toBe('00:01:28'); }); - xtest("start from stopped state resumes time tracking for total", () => { + xtest('start from stopped state resumes time tracking for total', () => { const stopwatch = new SplitSecondStopwatch(); stopwatch.start(); - stopwatch.advanceTime("00:00:23"); + stopwatch.advanceTime('00:00:23'); stopwatch.stop(); - stopwatch.advanceTime("00:00:44"); + stopwatch.advanceTime('00:00:44'); stopwatch.start(); - stopwatch.advanceTime("00:00:09"); - expect(stopwatch.total).toBe("00:00:32"); + stopwatch.advanceTime('00:00:09'); + expect(stopwatch.total).toBe('00:00:32'); }); - xtest("lap adds current lap to previous laps", () => { + xtest('lap adds current lap to previous laps', () => { const stopwatch = new SplitSecondStopwatch(); stopwatch.start(); - stopwatch.advanceTime("00:01:38"); + stopwatch.advanceTime('00:01:38'); stopwatch.lap(); - expect(stopwatch.previousLaps).toEqual(["00:01:38"]); - stopwatch.advanceTime("00:00:44"); + expect(stopwatch.previousLaps).toEqual(['00:01:38']); + stopwatch.advanceTime('00:00:44'); stopwatch.lap(); - expect(stopwatch.previousLaps).toEqual(["00:01:38", "00:00:44"]); + expect(stopwatch.previousLaps).toEqual(['00:01:38', '00:00:44']); }); - xtest("lap resets current lap and resumes time tracking", () => { + xtest('lap resets current lap and resumes time tracking', () => { const stopwatch = new SplitSecondStopwatch(); stopwatch.start(); - stopwatch.advanceTime("00:08:22"); + stopwatch.advanceTime('00:08:22'); stopwatch.lap(); - expect(stopwatch.currentLap).toBe("00:00:00"); - stopwatch.advanceTime("00:00:15"); - expect(stopwatch.currentLap).toBe("00:00:15"); + expect(stopwatch.currentLap).toBe('00:00:00'); + stopwatch.advanceTime('00:00:15'); + expect(stopwatch.currentLap).toBe('00:00:15'); }); - xtest("lap continues time tracking for total", () => { + xtest('lap continues time tracking for total', () => { const stopwatch = new SplitSecondStopwatch(); stopwatch.start(); - stopwatch.advanceTime("00:00:22"); + stopwatch.advanceTime('00:00:22'); stopwatch.lap(); - stopwatch.advanceTime("00:00:33"); - expect(stopwatch.total).toBe("00:00:55"); + stopwatch.advanceTime('00:00:33'); + expect(stopwatch.total).toBe('00:00:55'); }); - xtest("lap cannot be called from ready state", () => { + xtest('lap cannot be called from ready state', () => { const stopwatch = new SplitSecondStopwatch(); - expect(() => stopwatch.lap()).toThrow("cannot lap a stopwatch that is not running"); + expect(() => stopwatch.lap()).toThrow( + 'cannot lap a stopwatch that is not running', + ); }); - xtest("lap cannot be called from stopped state", () => { + xtest('lap cannot be called from stopped state', () => { const stopwatch = new SplitSecondStopwatch(); stopwatch.start(); stopwatch.stop(); - expect(() => stopwatch.lap()).toThrow("cannot lap a stopwatch that is not running"); + expect(() => stopwatch.lap()).toThrow( + 'cannot lap a stopwatch that is not running', + ); }); - xtest("stop does not change previous laps", () => { + xtest('stop does not change previous laps', () => { const stopwatch = new SplitSecondStopwatch(); stopwatch.start(); - stopwatch.advanceTime("00:11:22"); + stopwatch.advanceTime('00:11:22'); stopwatch.lap(); - expect(stopwatch.previousLaps).toEqual(["00:11:22"]); + expect(stopwatch.previousLaps).toEqual(['00:11:22']); stopwatch.stop(); - expect(stopwatch.previousLaps).toEqual(["00:11:22"]); + expect(stopwatch.previousLaps).toEqual(['00:11:22']); }); - xtest("reset from stopped state changes state to ready", () => { + xtest('reset from stopped state changes state to ready', () => { const stopwatch = new SplitSecondStopwatch(); stopwatch.start(); stopwatch.stop(); stopwatch.reset(); - expect(stopwatch.state).toBe("ready"); + expect(stopwatch.state).toBe('ready'); }); - xtest("reset resets current lap", () => { + xtest('reset resets current lap', () => { const stopwatch = new SplitSecondStopwatch(); stopwatch.start(); - stopwatch.advanceTime("00:00:10"); + stopwatch.advanceTime('00:00:10'); stopwatch.stop(); stopwatch.reset(); - expect(stopwatch.currentLap).toBe("00:00:00"); + expect(stopwatch.currentLap).toBe('00:00:00'); }); - xtest("reset clears previous laps", () => { + xtest('reset clears previous laps', () => { const stopwatch = new SplitSecondStopwatch(); stopwatch.start(); - stopwatch.advanceTime("00:00:10"); + stopwatch.advanceTime('00:00:10'); stopwatch.lap(); - stopwatch.advanceTime("00:00:20"); + stopwatch.advanceTime('00:00:20'); stopwatch.lap(); - expect(stopwatch.previousLaps).toEqual(["00:00:10", "00:00:20"]); + expect(stopwatch.previousLaps).toEqual(['00:00:10', '00:00:20']); stopwatch.stop(); stopwatch.reset(); expect(stopwatch.previousLaps).toEqual([]); }); - xtest("reset cannot be called from ready state", () => { + xtest('reset cannot be called from ready state', () => { const stopwatch = new SplitSecondStopwatch(); - expect(() => stopwatch.reset()).toThrow("cannot reset a stopwatch that is not stopped"); + expect(() => stopwatch.reset()).toThrow( + 'cannot reset a stopwatch that is not stopped', + ); }); - xtest("reset cannot be called from running state", () => { + xtest('reset cannot be called from running state', () => { const stopwatch = new SplitSecondStopwatch(); stopwatch.start(); - expect(() => stopwatch.reset()).toThrow("cannot reset a stopwatch that is not stopped"); + expect(() => stopwatch.reset()).toThrow( + 'cannot reset a stopwatch that is not stopped', + ); }); - xtest("supports very long laps", () => { + xtest('supports very long laps', () => { const stopwatch = new SplitSecondStopwatch(); stopwatch.start(); - stopwatch.advanceTime("01:23:45"); - expect(stopwatch.currentLap).toBe("01:23:45"); + stopwatch.advanceTime('01:23:45'); + expect(stopwatch.currentLap).toBe('01:23:45'); stopwatch.lap(); - expect(stopwatch.previousLaps).toEqual(["01:23:45"]); - stopwatch.advanceTime("04:01:40"); - expect(stopwatch.currentLap).toBe("04:01:40"); - expect(stopwatch.total).toBe("05:25:25"); + expect(stopwatch.previousLaps).toEqual(['01:23:45']); + stopwatch.advanceTime('04:01:40'); + expect(stopwatch.currentLap).toBe('04:01:40'); + expect(stopwatch.total).toBe('05:25:25'); stopwatch.lap(); - expect(stopwatch.previousLaps).toEqual(["01:23:45", "04:01:40"]); - stopwatch.advanceTime("08:43:05"); - expect(stopwatch.currentLap).toBe("08:43:05"); - expect(stopwatch.total).toBe("14:08:30"); + expect(stopwatch.previousLaps).toEqual(['01:23:45', '04:01:40']); + stopwatch.advanceTime('08:43:05'); + expect(stopwatch.currentLap).toBe('08:43:05'); + expect(stopwatch.total).toBe('14:08:30'); stopwatch.lap(); expect(stopwatch.previousLaps).toEqual([ - "01:23:45", - "04:01:40", - "08:43:05", + '01:23:45', + '04:01:40', + '08:43:05', ]); }); }); From 45360a46abd6984d45aabbd2c6a229ad6c13e567 Mon Sep 17 00:00:00 2001 From: BNAndras <20251272+BNAndras@users.noreply.github.com> Date: Fri, 6 Mar 2026 09:37:14 -0800 Subject: [PATCH 3/4] Trigger builds From f26433f3482b051f13e3132e1338b6bda33d7de8 Mon Sep 17 00:00:00 2001 From: BNAndras <20251272+BNAndras@users.noreply.github.com> Date: Fri, 6 Mar 2026 09:46:31 -0800 Subject: [PATCH 4/4] Fix stub --- .../split-second-stopwatch.js | 56 ++++--------------- 1 file changed, 10 insertions(+), 46 deletions(-) diff --git a/exercises/practice/split-second-stopwatch/split-second-stopwatch.js b/exercises/practice/split-second-stopwatch/split-second-stopwatch.js index a532e71485..1e7db922b8 100644 --- a/exercises/practice/split-second-stopwatch/split-second-stopwatch.js +++ b/exercises/practice/split-second-stopwatch/split-second-stopwatch.js @@ -1,77 +1,41 @@ export class SplitSecondStopwatch { constructor() { - this._state = 'ready'; - this._totalSeconds = 0; - this._currentLap = 0; - this._previousLaps = []; + throw new Error('Remove this line and implement the function'); } get state() { - return this._state; + throw new Error('Remove this line and implement the function'); } get currentLap() { - return this._formatTime(this._currentLap); + throw new Error('Remove this line and implement the function'); } get total() { - return this._formatTime(this._totalSeconds); + throw new Error('Remove this line and implement the function'); } get previousLaps() { - return this._previousLaps.map((s) => this._formatTime(s)); + throw new Error('Remove this line and implement the function'); } start() { - if (this._state === 'running') { - throw new Error('cannot start an already running stopwatch'); - } - this._state = 'running'; + throw new Error('Remove this line and implement the function'); } stop() { - if (this._state !== 'running') { - throw new Error('cannot stop a stopwatch that is not running'); - } - this._state = 'stopped'; + throw new Error('Remove this line and implement the function'); } lap() { - if (this._state !== 'running') { - throw new Error('cannot lap a stopwatch that is not running'); - } - this._previousLaps.push(this._currentLap); - this._currentLap = 0; + throw new Error('Remove this line and implement the function'); } reset() { - if (this._state !== 'stopped') { - throw new Error('cannot reset a stopwatch that is not stopped'); - } - this._state = 'ready'; - this._totalSeconds = 0; - this._currentLap = 0; - this._previousLaps = []; + throw new Error('Remove this line and implement the function'); } advanceTime(duration) { - if (this._state === 'running') { - const seconds = this._toSeconds(duration); - this._currentLap += seconds; - this._totalSeconds += seconds; - } - } - - _toSeconds(duration) { - const [h, m, s] = duration.split(':').map(Number); - return h * 3600 + m * 60 + s; - } - - _formatTime(seconds) { - const h = Math.floor(seconds / 3600); - const m = Math.floor((seconds % 3600) / 60); - const s = seconds % 60; - - return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; + throw new Error('Remove this line and implement the function'); } }