commonMain.io.github.lyxnx.util.time.Stopwatch.kt Maven / Gradle / Ivy
package io.github.lyxnx.util.time
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlin.time.Duration
import kotlin.time.TimeMark
import kotlin.time.TimeSource
/**
* A stopwatch implementation that uses a [timeSource] for measuring time intervals
*
* @param timeSource The time source used for measuring time intervals. The default [TimeSource.Monotonic] is fine in
* most cases
*/
public class Stopwatch(private val timeSource: TimeSource = TimeSource.Monotonic) {
private val _state = MutableStateFlow(State.STOPPED)
/**
* The current state of the stopwatch that can be observed
*/
public val stateFlow: StateFlow = _state.asStateFlow()
/**
* The instantaneous state of the stopwatch
*/
public var state: State
get() = _state.value
private set(value) {
_state.update { value }
}
private val _laps = MutableStateFlow(emptyList())
/**
* The current laps that have been recorded that can be observed
*/
public val lapsFlow: StateFlow> = _laps.asStateFlow()
/**
* The current laps that have been recorded
*/
public val laps: List get() = _laps.value
/**
* The fastest lap recorded, or null if no laps have been recorded yet
*/
public val fastestLap: Lap? get() = laps.minByOrNull { it.lapTime }
/**
* The slowest lap recorded, or null if no laps have been recorded yet
*/
public val slowestLap: Lap? get() = laps.maxByOrNull { it.lapTime }
/**
* The average lap time of all recorded laps, or null if no laps have been recorded yet
*/
public val averageLap: Lap?
get() {
if (laps.isEmpty()) return null
val totalLapTimes = laps.map { it.lapTime }.sum()
return Lap(
number = laps.size + 1,
lapTime = totalLapTimes / laps.size,
elapsedTime = totalLapTimes / laps.size
)
}
private var startTime: TimeMark? = null
private var elapsedDurationOnPause = Duration.ZERO
/**
* The total elapsed time of the stopwatch
*/
public val elapsedTime: Duration
get() = when (state) {
State.RUNNING -> startTime!!.elapsedNow()
else -> elapsedDurationOnPause
}
/**
* Starts the stopwatch if stopped or resumes it if paused
*
* This does nothing if the stopwatch is already running
*/
public fun start() {
if (state == State.STOPPED) {
startTime = timeSource.markNow()
} else if (state == State.PAUSED) {
startTime = timeSource.markNow() - elapsedDurationOnPause
}
state = State.RUNNING
}
/**
* Stops the stopwatch, and resets the elapsed time and recorded laps
*/
public fun reset() {
state = State.STOPPED
startTime = null
elapsedDurationOnPause = Duration.ZERO
_laps.update { emptyList() }
}
/**
* Pauses the stopwatch to allow resuming later
*/
public fun pause() {
elapsedDurationOnPause = startTime!!.elapsedNow()
state = State.PAUSED
}
/**
* Toggles the stopwatch state
*
* If running, it will be paused; if paused, it will be resumed
*/
public fun toggle() {
if (state == State.RUNNING) {
pause()
} else {
start()
}
}
/**
* Records a lap
*
* It will include the time since last lap (or start if no previous laps) and the total elapsed time
*/
public fun lap() {
val currentLapTime = elapsedTime - (laps.firstOrNull()?.elapsedTime ?: Duration.ZERO)
if (currentLapTime == Duration.ZERO) return
_laps.update {
listOf(
Lap(
number = laps.size + 1,
lapTime = currentLapTime,
elapsedTime = elapsedTime
)
) + it
}
}
/**
* Represents a recorded lap
*
* @property number The lap number
* @property lapTime Duration of the lap
* @property elapsedTime Total elapsed time of the stopwatch when the lap was recorded
*/
public data class Lap(
val number: Int,
val lapTime: Duration,
val elapsedTime: Duration
)
/**
* Represents the current state of the stopwatch
*/
public enum class State {
/**
* The stopwatch is active and running
*/
RUNNING,
/**
* The stopwatch is active but paused
*/
PAUSED,
/**
* The stopwatch is stopped and has not been started yet
*/
STOPPED
}
}