From 31d8ed4b8b9894ab49bcf1b887fd67a92a91e6bf Mon Sep 17 00:00:00 2001 From: BNAndras <20251272+BNAndras@users.noreply.github.com> Date: Fri, 6 Mar 2026 13:39:50 -0800 Subject: [PATCH] add `split-second-stopwatch` --- config.json | 8 + .../.docs/instructions.md | 22 ++ .../.docs/introduction.md | 6 + .../split-second-stopwatch/.meta/config.json | 19 ++ .../split-second-stopwatch/.meta/proof.ci.ts | 77 ++++++ .../split-second-stopwatch/.meta/tests.toml | 97 +++++++ .../.vscode/extensions.json | 7 + .../.vscode/settings.json | 7 + .../split-second-stopwatch/.yarnrc.yml | 3 + .../split-second-stopwatch/babel.config.cjs | 5 + .../split-second-stopwatch/eslint.config.mjs | 26 ++ .../split-second-stopwatch/jest.config.cjs | 22 ++ .../split-second-stopwatch/package.json | 38 +++ .../split-second-stopwatch.test.ts | 249 ++++++++++++++++++ .../split-second-stopwatch.ts | 41 +++ .../split-second-stopwatch/test-runner.mjs | 111 ++++++++ .../split-second-stopwatch/tsconfig.json | 38 +++ .../practice/split-second-stopwatch/yarn.lock | 0 18 files changed, 776 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/proof.ci.ts create mode 100644 exercises/practice/split-second-stopwatch/.meta/tests.toml create mode 100644 exercises/practice/split-second-stopwatch/.vscode/extensions.json create mode 100644 exercises/practice/split-second-stopwatch/.vscode/settings.json create mode 100644 exercises/practice/split-second-stopwatch/.yarnrc.yml create mode 100644 exercises/practice/split-second-stopwatch/babel.config.cjs create mode 100644 exercises/practice/split-second-stopwatch/eslint.config.mjs create mode 100644 exercises/practice/split-second-stopwatch/jest.config.cjs create mode 100644 exercises/practice/split-second-stopwatch/package.json create mode 100644 exercises/practice/split-second-stopwatch/split-second-stopwatch.test.ts create mode 100644 exercises/practice/split-second-stopwatch/split-second-stopwatch.ts create mode 100644 exercises/practice/split-second-stopwatch/test-runner.mjs create mode 100644 exercises/practice/split-second-stopwatch/tsconfig.json create mode 100644 exercises/practice/split-second-stopwatch/yarn.lock diff --git a/config.json b/config.json index b9cc9b1a2..e2f55a8d8 100644 --- a/config.json +++ b/config.json @@ -266,6 +266,14 @@ "strings" ] }, + { + "slug": "split-second-stopwatch", + "name": "Split-Second Stopwatch", + "uuid": "bc1fc50f-36ed-4a16-92bd-3001c8cd53d3", + "practices": [], + "prerequisites": [], + "difficulty": 4 + }, { "slug": "secret-handshake", "name": "Secret Handshake", 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..3fd9a7add --- /dev/null +++ b/exercises/practice/split-second-stopwatch/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "BNAndras" + ], + "files": { + "solution": [ + "split-second-stopwatch.ts" + ], + "test": [ + "split-second-stopwatch.test.ts" + ], + "example": [ + ".meta/proof.ci.ts" + ] + }, + "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/proof.ci.ts b/exercises/practice/split-second-stopwatch/.meta/proof.ci.ts new file mode 100644 index 000000000..628dcc737 --- /dev/null +++ b/exercises/practice/split-second-stopwatch/.meta/proof.ci.ts @@ -0,0 +1,77 @@ +export type State = 'ready' | 'running' | 'stopped' + +export class SplitSecondStopwatch { + private _state: State = 'ready' + private totalSeconds = 0 + private currentLapSeconds = 0 + private previousLapSeconds: number[] = [] + + public get state(): State { + return this._state + } + + public get currentLap(): string { + return this.formatTime(this.currentLapSeconds) + } + + public get total(): string { + return this.formatTime(this.totalSeconds) + } + + public get previousLaps(): string[] { + return this.previousLapSeconds.map((s) => this.formatTime(s)) + } + + public start(): void { + if (this._state === 'running') { + throw new Error('cannot start an already running stopwatch') + } + this._state = 'running' + } + + public stop(): void { + if (this._state !== 'running') { + throw new Error('cannot stop a stopwatch that is not running') + } + this._state = 'stopped' + } + + public lap(): void { + if (this._state !== 'running') { + throw new Error('cannot lap a stopwatch that is not running') + } + this.previousLapSeconds.push(this.currentLapSeconds) + this.currentLapSeconds = 0 + } + + public reset(): void { + if (this._state !== 'stopped') { + throw new Error('cannot reset a stopwatch that is not stopped') + } + this._state = 'ready' + this.totalSeconds = 0 + this.currentLapSeconds = 0 + this.previousLapSeconds = [] + } + + public advanceTime(duration: string): void { + if (this._state === 'running') { + const seconds = this.toSeconds(duration) + this.currentLapSeconds += seconds + this.totalSeconds += seconds + } + } + + private toSeconds(duration: string): number { + const [h, m, s] = duration.split(':').map(Number) + return h * 3600 + m * 60 + s + } + + private formatTime(seconds: number): string { + 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 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/.vscode/extensions.json b/exercises/practice/split-second-stopwatch/.vscode/extensions.json new file mode 100644 index 000000000..daaa5ee2e --- /dev/null +++ b/exercises/practice/split-second-stopwatch/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "arcanis.vscode-zipfs", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" + ] +} diff --git a/exercises/practice/split-second-stopwatch/.vscode/settings.json b/exercises/practice/split-second-stopwatch/.vscode/settings.json new file mode 100644 index 000000000..761fb422a --- /dev/null +++ b/exercises/practice/split-second-stopwatch/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "cSpell.words": ["exercism"], + "search.exclude": { + "**/.yarn": true, + "**/.pnp.*": true + } +} diff --git a/exercises/practice/split-second-stopwatch/.yarnrc.yml b/exercises/practice/split-second-stopwatch/.yarnrc.yml new file mode 100644 index 000000000..23e4a6d3d --- /dev/null +++ b/exercises/practice/split-second-stopwatch/.yarnrc.yml @@ -0,0 +1,3 @@ +compressionLevel: mixed + +enableGlobalCache: true diff --git a/exercises/practice/split-second-stopwatch/babel.config.cjs b/exercises/practice/split-second-stopwatch/babel.config.cjs new file mode 100644 index 000000000..164552797 --- /dev/null +++ b/exercises/practice/split-second-stopwatch/babel.config.cjs @@ -0,0 +1,5 @@ +module.exports = { + // eslint-disable-next-line @typescript-eslint/no-require-imports + presets: [[require('@exercism/babel-preset-typescript'), { corejs: '3.38' }]], + 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 000000000..1be39c53f --- /dev/null +++ b/exercises/practice/split-second-stopwatch/eslint.config.mjs @@ -0,0 +1,26 @@ +// @ts-check + +import tsEslint from 'typescript-eslint' +import config from '@exercism/eslint-config-typescript' +import maintainersConfig from '@exercism/eslint-config-typescript/maintainers.mjs' + +export default [ + ...tsEslint.config(...config, { + files: ['.meta/proof.ci.ts', '.meta/exemplar.ts', '*.test.ts'], + extends: maintainersConfig, + }), + { + ignores: [ + // # Protected or generated + '.git/**/*', + '.vscode/**/*', + + //# When using npm + 'node_modules/**/*', + + // # Configuration files + 'babel.config.cjs', + 'jest.config.cjs', + ], + }, +] diff --git a/exercises/practice/split-second-stopwatch/jest.config.cjs b/exercises/practice/split-second-stopwatch/jest.config.cjs new file mode 100644 index 000000000..0aba1a59e --- /dev/null +++ b/exercises/practice/split-second-stopwatch/jest.config.cjs @@ -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 000000000..90193d59a --- /dev/null +++ b/exercises/practice/split-second-stopwatch/package.json @@ -0,0 +1,38 @@ +{ + "name": "@exercism/typescript-split-second-stopwatch", + "version": "1.0.0", + "description": "Exercism exercises in Typescript.", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/exercism/typescript" + }, + "type": "module", + "engines": { + "node": "^18.16.0 || >=20.0.0" + }, + "devDependencies": { + "@exercism/babel-preset-typescript": "^0.6.0", + "@exercism/eslint-config-typescript": "^0.8.0", + "@jest/globals": "^29.7.0", + "@types/node": "~22.7.6", + "babel-jest": "^29.7.0", + "core-js": "~3.38.1", + "eslint": "^9.12.0", + "expect": "^29.7.0", + "jest": "^29.7.0", + "prettier": "^3.3.3", + "tstyche": "^2.1.1", + "typescript": "~5.6.3", + "typescript-eslint": "^8.10.0" + }, + "scripts": { + "test": "corepack yarn node test-runner.mjs", + "test:types": "corepack yarn tstyche", + "test:implementation": "corepack yarn jest --no-cache --passWithNoTests", + "lint": "corepack yarn lint:types && corepack yarn lint:ci", + "lint:types": "corepack yarn tsc --noEmit -p .", + "lint:ci": "corepack yarn eslint . --ext .tsx,.ts" + }, + "packageManager": "yarn@4.5.1" +} diff --git a/exercises/practice/split-second-stopwatch/split-second-stopwatch.test.ts b/exercises/practice/split-second-stopwatch/split-second-stopwatch.test.ts new file mode 100644 index 000000000..a0276c2ea --- /dev/null +++ b/exercises/practice/split-second-stopwatch/split-second-stopwatch.test.ts @@ -0,0 +1,249 @@ +import { describe, expect, it, xit } from '@jest/globals' +import { SplitSecondStopwatch } from './split-second-stopwatch.ts' + +describe('SplitSecondStopwatch', () => { + it('new stopwatch starts in ready state', () => { + const stopwatch = new SplitSecondStopwatch() + expect(stopwatch.state).toBe('ready') + }) + + xit("new stopwatch's current lap has no elapsed time", () => { + const stopwatch = new SplitSecondStopwatch() + expect(stopwatch.currentLap).toBe('00:00:00') + }) + + xit("new stopwatch's total has no elapsed time", () => { + const stopwatch = new SplitSecondStopwatch() + expect(stopwatch.total).toBe('00:00:00') + }) + + xit('new stopwatch does not have previous laps', () => { + const stopwatch = new SplitSecondStopwatch() + expect(stopwatch.previousLaps).toEqual([]) + }) + + xit('start from ready state changes state to running', () => { + const stopwatch = new SplitSecondStopwatch() + stopwatch.start() + expect(stopwatch.state).toBe('running') + }) + + xit('start does not change previous laps', () => { + const stopwatch = new SplitSecondStopwatch() + stopwatch.start() + expect(stopwatch.previousLaps).toEqual([]) + }) + + xit('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') + }) + + xit('start initiates time tracking for total', () => { + const stopwatch = new SplitSecondStopwatch() + stopwatch.start() + stopwatch.advanceTime('00:00:23') + expect(stopwatch.total).toBe('00:00:23') + }) + + xit('start cannot be called from running state', () => { + const stopwatch = new SplitSecondStopwatch() + stopwatch.start() + expect(() => stopwatch.start()).toThrow( + 'cannot start an already running stopwatch' + ) + }) + + xit('stop from running state changes state to stopped', () => { + const stopwatch = new SplitSecondStopwatch() + stopwatch.start() + stopwatch.stop() + expect(stopwatch.state).toBe('stopped') + }) + + xit('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') + }) + + xit('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') + }) + + xit('stop cannot be called from ready state', () => { + const stopwatch = new SplitSecondStopwatch() + expect(() => stopwatch.stop()).toThrow( + 'cannot stop a stopwatch that is not running' + ) + }) + + xit('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' + ) + }) + + xit('start from stopped state changes state to running', () => { + const stopwatch = new SplitSecondStopwatch() + stopwatch.start() + stopwatch.stop() + stopwatch.start() + expect(stopwatch.state).toBe('running') + }) + + xit('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') + }) + + xit('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') + }) + + xit('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']) + }) + + xit('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') + }) + + xit('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') + }) + + xit('lap cannot be called from ready state', () => { + const stopwatch = new SplitSecondStopwatch() + expect(() => stopwatch.lap()).toThrow( + 'cannot lap a stopwatch that is not running' + ) + }) + + xit('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' + ) + }) + + xit('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']) + }) + + xit('reset from stopped state changes state to ready', () => { + const stopwatch = new SplitSecondStopwatch() + stopwatch.start() + stopwatch.stop() + stopwatch.reset() + expect(stopwatch.state).toBe('ready') + }) + + xit('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') + }) + + xit('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([]) + }) + + xit('reset cannot be called from ready state', () => { + const stopwatch = new SplitSecondStopwatch() + expect(() => stopwatch.reset()).toThrow( + 'cannot reset a stopwatch that is not stopped' + ) + }) + + xit('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' + ) + }) + + xit('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']) + }) +}) diff --git a/exercises/practice/split-second-stopwatch/split-second-stopwatch.ts b/exercises/practice/split-second-stopwatch/split-second-stopwatch.ts new file mode 100644 index 000000000..f7ae62c72 --- /dev/null +++ b/exercises/practice/split-second-stopwatch/split-second-stopwatch.ts @@ -0,0 +1,41 @@ +export class SplitSecondStopwatch { + constructor() { + throw new Error('Remove this line and implement the function') + } + + public get state(): unknown { + throw new Error('Remove this line and implement the function') + } + + public get currentLap(): unknown { + throw new Error('Remove this line and implement the function') + } + + public get total(): unknown { + throw new Error('Remove this line and implement the function') + } + + public get previousLaps(): unknown[] { + throw new Error('Remove this line and implement the function') + } + + public start(): unknown { + throw new Error('Remove this line and implement the function') + } + + public stop(): unknown { + throw new Error('Remove this line and implement the function') + } + + public lap(): unknown { + throw new Error('Remove this line and implement the function') + } + + public reset(): unknown { + throw new Error('Remove this line and implement the function') + } + + public advanceTime(duration: unknown): unknown { + throw new Error('Remove this line and implement the function') + } +} diff --git a/exercises/practice/split-second-stopwatch/test-runner.mjs b/exercises/practice/split-second-stopwatch/test-runner.mjs new file mode 100644 index 000000000..44b205fc2 --- /dev/null +++ b/exercises/practice/split-second-stopwatch/test-runner.mjs @@ -0,0 +1,111 @@ +#!/usr/bin/env node + +/** + * 👋🏽 Hello there reader, + * + * It looks like you are working on this solution using the Exercism CLI and + * not the online editor. That's great! The file you are looking at executes + * the various steps the online test-runner also takes. + * + * @see https://github.com/exercism/typescript-test-runner + * + * TypeScript track exercises generally consist of at least two out of three + * types of tests to run. + * + * 1. tsc, the TypeScript compiler. This tests if the TypeScript code is valid + * 2. tstyche, static analysis tests to see if the types used are expected + * 3. jest, runtime implementation tests to see if the solution is correct + * + * If one of these three fails, this script terminates with -1, -2, or -3 + * respectively. If it succeeds, it terminates with exit code 0. + * + * @note you need corepack (bundled with node LTS) enabled in order for this + * test runner to work as expected. Follow the installation and test + * instructions if you see errors about corepack or pnp. + */ + +import { execSync } from 'node:child_process' +import { existsSync, readFileSync } from 'node:fs' +import { exit } from 'node:process' +import { URL } from 'node:url' + +/** + * Before executing any tests, the test runner attempts to find the + * exercise config.json file which has metadata about which types of tests + * to run for this solution. + */ +const metaDirectory = new URL('./.meta/', import.meta.url) +const exercismDirectory = new URL('./.exercism/', import.meta.url) +const configDirectory = existsSync(metaDirectory) + ? metaDirectory + : existsSync(exercismDirectory) + ? exercismDirectory + : null + +if (configDirectory === null) { + throw new Error( + 'Expected .meta or .exercism directory to exist, but I cannot find it.' + ) +} + +const configFile = new URL('./config.json', configDirectory) +if (!existsSync(configFile)) { + throw new Error('Expected config.json to exist at ' + configFile.toString()) +} + +// Experimental: import config from './config.json' with { type: 'json' } +/** @type {import('./config.json') } */ +const config = JSON.parse(readFileSync(configFile)) + +const jest = !config.custom || config.custom['flag.tests.jest'] +const tstyche = config.custom?.['flag.tests.tstyche'] +console.log( + `[tests] tsc: ✅, tstyche: ${tstyche ? '✅' : '❌'}, jest: ${jest ? '✅' : '❌'}, ` +) + +/** + * 1. tsc: the typescript compiler + */ +try { + console.log('[tests] tsc (compile)') + execSync('corepack yarn lint:types', { + stdio: 'inherit', + cwd: process.cwd(), + }) +} catch { + exit(-1) +} + +/** + * 2. tstyche: type tests + */ +if (tstyche) { + try { + console.log('[tests] tstyche (type tests)') + execSync('corepack yarn test:types', { + stdio: 'inherit', + cwd: process.cwd(), + }) + } catch { + exit(-2) + } +} + +/** + * 3. jest: implementation tests + */ +if (jest) { + try { + console.log('[tests] tstyche (implementation tests)') + execSync('corepack yarn test:implementation', { + stdio: 'inherit', + cwd: process.cwd(), + }) + } catch { + exit(-3) + } +} + +/** + * Done! 🥳 + */ diff --git a/exercises/practice/split-second-stopwatch/tsconfig.json b/exercises/practice/split-second-stopwatch/tsconfig.json new file mode 100644 index 000000000..574616245 --- /dev/null +++ b/exercises/practice/split-second-stopwatch/tsconfig.json @@ -0,0 +1,38 @@ +{ + "display": "Configuration for Exercism TypeScript Exercises", + "compilerOptions": { + // Allows you to use the newest syntax, and have access to console.log + // https://www.typescriptlang.org/tsconfig#lib + "lib": ["ES2020", "dom"], + // Make sure typescript is configured to output ESM + // https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c#how-can-i-make-my-typescript-project-output-esm + "module": "Node16", + // Since this project is using babel, TypeScript may target something very + // high, and babel will make sure it runs on your local Node version. + // https://babeljs.io/docs/en/ + "target": "ES2020", // ESLint doesn't support this yet: "es2022", + + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + + // Because jest-resolve isn't like node resolve, the absolute path must be .ts + "allowImportingTsExtensions": true, + "noEmit": true, + + // Because we'll be using babel: ensure that Babel can safely transpile + // files in the TypeScript project. + // + // https://babeljs.io/docs/en/babel-plugin-transform-typescript/#caveats + "isolatedModules": true + }, + "include": [ + "*.ts", + "*.tsx", + ".meta/*.ts", + ".meta/*.tsx", + "__typetests__/*.tst.ts" + ], + "exclude": ["node_modules"] +} diff --git a/exercises/practice/split-second-stopwatch/yarn.lock b/exercises/practice/split-second-stopwatch/yarn.lock new file mode 100644 index 000000000..e69de29bb