All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.io.github.lyxnx.util.time.Stopwatch.kt Maven / Gradle / Ivy

There is a newer version: 1.6.1
Show newest version
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
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy