Current section
Files
Jump to
Current section
Files
src/lap.gleam
import birl.{type Time}
import birl/duration.{type Unit}
import gleam/float
import gleam/int
import gleam/list
import tobble
pub opaque type LapData {
LapData(
first_marker: String,
first_time: Time,
last_time: Time,
duration_unit: Unit,
durations: List(DurationData),
)
}
pub opaque type DurationData {
DurationData(start_marker: String, end_marker: String, value: Int)
}
type DurationTuple =
#(String, String, Int)
/// Begins a new timing session with the given marker and duration unit in
/// microseconds.
pub fn start_in_microseconds(marker: String) -> LapData {
start(marker, duration.MicroSecond)
}
/// Begins a new timing session with the given marker and duration unit in
/// milliseconds.
pub fn start_in_milliseconds(marker: String) -> LapData {
start(marker, duration.MilliSecond)
}
/// Begins a new timing session with the given marker and duration unit in
/// seconds.
pub fn start_in_seconds(marker: String) -> LapData {
start(marker, duration.Second)
}
/// Begins a new timing session with the given marker and duration unit in
/// minutes.
pub fn start_in_minutes(marker: String) -> LapData {
start(marker, duration.Minute)
}
/// Begins a new timing session with the given marker and duration unit. The
/// duration_unit is a [birl](https://hexdocs.pm/birl/) `Unit`.
pub fn start(marker: String, duration_unit: Unit) -> LapData {
// Warmup - I noticed in my testing that the first duration was always
// slightly longer than the others and that by calling birl.now, the durations
// are more consistent.
list.repeat(birl.now(), 3)
start_with_time(marker, duration_unit, birl.now())
}
@internal
pub fn start_with_time(
marker: String,
duration_unit: Unit,
time: Time,
) -> LapData {
LapData(
first_marker: marker,
first_time: time,
last_time: time,
duration_unit:,
durations: [],
)
}
/// Marks the end of the current lap with the given marker.
pub fn time(data: LapData, marker: String) -> LapData {
time_with_time(data, marker, birl.now())
}
@internal
pub fn time_with_time(data: LapData, marker: String, time: Time) -> LapData {
let #(start_marker, end_marker) = case data.durations {
[] -> #(data.first_marker, marker)
[previous_duration, ..] -> #(previous_duration.end_marker, marker)
}
LapData(
..data,
last_time: time,
durations: [
DurationData(
start_marker,
end_marker,
birl.difference(time, data.last_time)
|> duration.blur_to(data.duration_unit),
),
..data.durations
],
)
}
/// Returns the total time elapsed from the first marker to the last marker, in
/// the duration unit specified via one of the start functions.
pub fn total_time(data: LapData) -> Int {
data.last_time
|> birl.difference(data.first_time)
|> duration.blur_to(data.duration_unit)
}
/// Sorts the durations in descending order. This is useful for identifying the
/// slowest parts of your timed code.
pub fn sort_max(data: LapData) -> LapData {
// We sort in ascending order because calling `to_list` reverses the order
// of the list
let durations =
data.durations
|> list.sort(fn(a, b) { int.compare(a.value, b.value) })
LapData(..data, durations:)
}
/// Return the durations as a list of tuples. Each tuple contains:
/// 1. The start marker (String)
/// 2. The end marker (String)
/// 3. The duration (Int) in the specified time unit
pub fn to_list(data: LapData) -> List(DurationTuple) {
data.durations
|> list.map(fn(duration) {
#(duration.start_marker, duration.end_marker, duration.value)
})
|> list.reverse
}
/// Returns a table of the durations as a string. The table includes the start
/// and end markers, duration, and percentage of the total time.
pub fn pretty_print(data: LapData) -> String {
let duration_unit_label = duration_unit_label(data.duration_unit)
let builder =
tobble.builder()
|> tobble.add_row(["Start", "End", "Duration", "%"])
let total_time = total_time(data)
let builder =
data
|> to_list
|> list.fold(builder, fn(builder, duration) {
builder
|> tobble.add_row([
duration.0,
duration.1,
{ duration.2 |> int.to_string } <> " " <> duration_unit_label,
to_rounded_percentage(duration.2, total_time) |> float.to_string,
])
})
case tobble.build(builder) {
Ok(table) -> table |> tobble.render
Error(_) -> ""
}
}
fn duration_unit_label(duration_unit: Unit) -> String {
case duration_unit {
duration.MicroSecond -> "μs"
duration.MilliSecond -> "ms"
duration.Second -> "s"
duration.Minute -> "min"
duration.Hour -> "h"
duration.Day -> "d"
duration.Week -> "w"
duration.Month -> "mo"
duration.Year -> "y"
}
}
fn to_percentage(a: Int, b: Int) -> Float {
let ratio = int.to_float(a) /. int.to_float(b)
ratio *. 100.0
}
fn to_rounded_percentage(a: Int, b: Int) -> Float {
let percentage = to_percentage(a, b)
{
{ { percentage *. 100.0 } |> float.truncate }
|> int.to_float
}
/. 100.0
}