///|
/// A row in a signal timeline view.
pub struct TimelinePoint {
time : Int64
values : Array[(String, Value?)]
} derive(Debug, Eq)
///|
/// Build snapshots at a fixed cadence across a bounded range.
pub fn timeline(
trace : Trace,
start : Int64,
end : Int64,
step : Int64,
) -> Array[TimelinePoint] {
let safe_step = Int64::max(1L, step)
let left = Int64::min(start, end)
let right = Int64::max(start, end)
let result : Array[TimelinePoint] = []
let mut timestamp = left
while timestamp <= right {
result.push({ time: timestamp, values: trace.snapshot_at(timestamp) })
if timestamp > right - safe_step {
break
}
timestamp += safe_step
}
result
}
///|
/// Return only one signal's samples from a timeline.
pub fn timeline_signal(
points : Array[TimelinePoint],
signal_id : String,
) -> Array[(Int64, Value?)] {
points.map(fn(point) {
let value = point.values.fold(init=None, fn(found, item) {
if item.0 == signal_id {
Some(item.1)
} else {
found
}
})
(
point.time,
match value {
Some(item) => item
None => None
},
)
})
}
///|
/// Render a timeline as CSV with one row per signal value.
pub fn timeline_csv(points : Array[TimelinePoint]) -> String {
let out : Array[String] = ["time,id,value"]
for point in points {
for item in point.values {
let value = match item.1 {
Some(value) => value.display()
None => ""
}
out.push(
point.time.to_string() +
"," +
escape_text(item.0) +
"," +
escape_text(value),
)
}
}
out.join("\n")
}
///|
/// Return the first timestamp at which a signal differs from a baseline.
pub fn first_difference_from(
trace : Trace,
signal_id : String,
baseline : Value,
) -> Int64? {
for change in signal_changes(trace, signal_id) {
if !values_equal(change.value, baseline, true) {
return Some(change.time)
}
}
None
}
///|
/// Count signal changes in equally sized windows.
pub fn change_counts_by_window(
trace : Trace,
signal_id : String,
width : Int64,
) -> Array[(Int64, Int)] {
let safe = Int64::max(1L, width)
let result : Array[(Int64, Int)] = []
let mut start = 0L
while start <= trace.end_time {
let end = Int64::min(trace.end_time, start + safe)
let count = signal_changes(trace, signal_id).count_if(fn(change) {
change.time >= start && change.time <= end
})
result.push((start, count))
if end == trace.end_time {
break
}
start += safe
}
result
}
///|
/// Return the maximum number of changes in any window.
pub fn maximum_window_count(
trace : Trace,
signal_id : String,
width : Int64,
) -> Int {
change_counts_by_window(trace, signal_id, width).fold(init=0, fn(best, item) {
Int::max(best, item.1)
})
}
///|
/// Return all timestamps with a value matching a predicate-like value.
pub fn times_with_value(
trace : Trace,
signal_id : String,
expected : Value,
) -> Array[Int64] {
signal_changes(trace, signal_id)
.filter(fn(change) { values_equal(change.value, expected, true) })
.map(fn(change) { change.time })
}
///|
/// Return the duration between the first and last matching value.
pub fn matching_value_span(
trace : Trace,
signal_id : String,
expected : Value,
) -> Int64 {
let times = times_with_value(trace, signal_id, expected)
if times.length() < 2 {
0L
} else {
times[times.length() - 1] - times[0]
}
}
///|
/// Return a stable snapshot manifest at one timestamp.
pub fn snapshot_manifest(trace : Trace, timestamp : Int64) -> String {
let out : Array[String] = []
for item in trace.snapshot_at(timestamp) {
let value = match item.1 {
Some(value) => value.display()
None => ""
}
out.push(item.0 + "=" + value)
}
out.join(" ")
}
///|
/// Return the number of signals that have a known value at a timestamp.
pub fn known_snapshot_count(trace : Trace, timestamp : Int64) -> Int {
trace
.snapshot_at(timestamp)
.count_if(fn(item) {
match item.1 {
Some(value) => !value.is_unknown()
None => false
}
})
}
///|
/// Return whether all requested timeline timestamps are monotonic.
pub fn timeline_is_monotonic(points : Array[TimelinePoint]) -> Bool {
let mut previous : Int64? = None
for point in points {
match previous {
Some(value) if point.time < value => return false
_ => ()
}
previous = Some(point.time)
}
true
}