Skip to content

Commit c5d8fe4

Browse files
Add split-second-stopwatch exercise #2968
Add split-second-stopwatch exercise Implement the split-second-stopwatch practice exercise with: - State management (ready/running/stopped) - Time tracking for current lap and total elapsed time - Lap functionality with previous laps history - Start, stop, reset operations with proper state validation - Error handling for invalid state transitions - Time formatting in HH:MM:SS format - Mock time advancement for testing Includes reference solution, starter implementation, and comprehensive test suite covering all canonical data test cases.
1 parent f02bae3 commit c5d8fe4

File tree

11 files changed

+921
-0
lines changed

11 files changed

+921
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Instructions
2+
3+
Your task is to build a stopwatch to keep precise track of lap times.
4+
5+
The stopwatch uses four commands (start, stop, lap, and reset) to keep track of:
6+
7+
1. The current lap's tracked time
8+
2. Previously recorded lap times
9+
10+
What commands can be used depends on which state the stopwatch is in:
11+
12+
1. Ready: initial state
13+
2. Running: tracking time
14+
3. Stopped: not tracking time
15+
16+
| Command | Begin state | End state | Effect |
17+
| ------- | ----------- | --------- | -------------------------------------------------------- |
18+
| Start | Ready | Running | Start tracking time |
19+
| Start | Stopped | Running | Resume tracking time |
20+
| Stop | Running | Stopped | Stop tracking time |
21+
| Lap | Running | Running | Add current lap to previous laps, then reset current lap |
22+
| Reset | Stopped | Ready | Reset current lap and clear previous laps |
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Introduction
2+
3+
You've always run for the thrill of it — no schedules, no timers, just the sound of your feet on the pavement.
4+
But now that you've joined a competitive running crew, things are getting serious.
5+
Training sessions are timed to the second, and every split second counts.
6+
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.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"authors": [
3+
"Jagdish Prajapati, Kah Goh & Emmanuel Berkowicz"
4+
],
5+
"files": {
6+
"solution": [
7+
"src/main/java/SplitSecondStopwatch.java"
8+
],
9+
"test": [
10+
"src/test/java/SplitSecondStopwatch.java"
11+
],
12+
"example": [
13+
".meta/src/reference/java/SplitSecondStopwatch.java"
14+
],
15+
"invalidator": [
16+
"build.gradle"
17+
]
18+
},
19+
"blurb": "Time to create a new and improved split second stopwatch using your friendly neighbourhood OOP language!.",
20+
"source": "Emmanuel Berkowicz",
21+
"source_url": "https://github.com/exercism/problem-specifications/tree/main/exercises/split-second-stopwatch"
22+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import java.util.ArrayList;
2+
import java.util.List;
3+
4+
public class SplitSecondStopwatch {
5+
6+
/**
7+
* A split-second stopwatch that tracks elapsed time with lap functionality.
8+
* Supports start, stop, reset, and lap operations with precise time tracking.
9+
* Times are formatted in HH:MM:SS format with two-digit precision.
10+
*
11+
* @see <a href="https://github.com/exercism/problem-specifications/tree/main/exercises/split-second-stopwatch">
12+
* Problem Specifications
13+
* </a>
14+
*/
15+
16+
private enum State { READY, RUNNING, STOPPED }
17+
18+
private State state;
19+
private long totalElapsed;
20+
private long currentLapStart;
21+
private long lastStopTime;
22+
private List<String> previousLaps;
23+
private long mockTime;
24+
25+
public SplitSecondStopwatch() {
26+
this.state = State.READY;
27+
this.totalElapsed = 0;
28+
this.currentLapStart = 0;
29+
this.lastStopTime = 0;
30+
this.previousLaps = new ArrayList<>();
31+
this.mockTime = 0;
32+
}
33+
34+
public void start() {
35+
if (state == State.RUNNING) {
36+
throw new IllegalStateException("cannot start an already running stopwatch");
37+
}
38+
39+
if (state == State.READY) {
40+
currentLapStart = mockTime;
41+
} else if (state == State.STOPPED) {
42+
long pausedDuration = mockTime - lastStopTime;
43+
currentLapStart += pausedDuration;
44+
}
45+
46+
state = State.RUNNING;
47+
}
48+
49+
public void stop() {
50+
if (state != State.RUNNING) {
51+
throw new IllegalStateException("cannot stop a stopwatch that is not running");
52+
}
53+
54+
lastStopTime = mockTime;
55+
totalElapsed += mockTime - currentLapStart;
56+
state = State.STOPPED;
57+
}
58+
59+
public void reset() {
60+
if (state != State.STOPPED) {
61+
throw new IllegalStateException("cannot reset a stopwatch that is not stopped");
62+
}
63+
64+
state = State.READY;
65+
totalElapsed = 0;
66+
currentLapStart = 0;
67+
lastStopTime = 0;
68+
previousLaps.clear();
69+
}
70+
71+
public void lap() {
72+
if (state != State.RUNNING) {
73+
throw new IllegalStateException("cannot lap a stopwatch that is not running");
74+
}
75+
76+
long currentLapTime = mockTime - currentLapStart;
77+
previousLaps.add(formatTime(currentLapTime));
78+
currentLapStart = mockTime;
79+
}
80+
81+
public String state() {
82+
return state.name().toLowerCase();
83+
}
84+
85+
public String currentLap() {
86+
if (state == State.RUNNING) {
87+
return formatTime(mockTime - currentLapStart);
88+
} else {
89+
return formatTime(lastStopTime - currentLapStart);
90+
}
91+
}
92+
93+
public String total() {
94+
if (state == State.RUNNING) {
95+
return formatTime(totalElapsed + (mockTime - currentLapStart));
96+
} else {
97+
return formatTime(totalElapsed);
98+
}
99+
}
100+
101+
public List<String> previousLaps() {
102+
return new ArrayList<>(previousLaps);
103+
}
104+
105+
public void advanceTime(String timeString) {
106+
String[] parts = timeString.split(":");
107+
long hours = Long.parseLong(parts[0]);
108+
long minutes = Long.parseLong(parts[1]);
109+
long seconds = Long.parseLong(parts[2]);
110+
111+
long milliseconds = (hours * 3600 + minutes * 60 + seconds) * 1000;
112+
mockTime += milliseconds;
113+
}
114+
115+
private String formatTime(long milliseconds) {
116+
long totalSeconds = milliseconds / 1000;
117+
long hours = totalSeconds / 3600;
118+
long minutes = (totalSeconds % 3600) / 60;
119+
long seconds = totalSeconds % 60;
120+
121+
return String.format("%02d:%02d:%02d", hours, minutes, seconds);
122+
}
123+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
plugins {
2+
id "java"
3+
}
4+
5+
repositories {
6+
mavenCentral()
7+
}
8+
9+
dependencies {
10+
testImplementation platform("org.junit:junit-bom:5.10.0")
11+
testImplementation "org.junit.jupiter:junit-jupiter"
12+
testImplementation "org.assertj:assertj-core:3.25.1"
13+
14+
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
15+
}
16+
17+
test {
18+
useJUnitPlatform()
19+
20+
testLogging {
21+
exceptionFormat = "full"
22+
showStandardStreams = true
23+
events = ["passed", "failed", "skipped"]
24+
}
25+
}
Binary file not shown.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
distributionBase=GRADLE_USER_HOME
2+
distributionPath=wrapper/dists
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
4+
validateDistributionUrl=true
5+
zipStoreBase=GRADLE_USER_HOME
6+
zipStorePath=wrapper/dists

0 commit comments

Comments
 (0)