///|
/// Event-aligned data, retaining the source annotation and absolute file times.
/// Complete means the requested interval is inside the recorded domain without
/// a known inter-record gap; it is not a claim about signal quality or diagnosis.
pub struct EventWindow {
annotation_index : Int
annotation : Annotation
start : Double
end : Double
complete : Bool
samples : Int
channels : Array[SignalWindow]
} derive(Debug, ToJson)
///|
pub extend EventWindow with @debug.Debug::{to_repr}
///|
pub extend EventWindow with ToJson::{to_json}
///|
// Reuse the writer's 100 ns representability/range check, then add integer
// ticks. Adding binary seconds first can include the excluded endpoint (e.g.
// 20.8 + 4.1 + .25 becomes 25.150000000000002 instead of 25.15).
fn event_ticks(value : Double) -> Int64 raise EdfError {
ignore(time_text(value, true))
(value * 10000000.0).round().to_int64()
}
///|
/// Select by annotation index, never ambiguous event text. Seconds remain
/// relative to the original file origin. All channels share a sample budget.
/// Missing recording time is rejected by default, never padded/interpolated.
pub fn Recording::event_window(
self : Recording,
annotation_index : Int,
signals : Array[Int],
before~ : Double,
after~ : Double,
include_duration? : Bool = true,
allow_partial? : Bool = false,
max_samples? : Int = 65536,
) -> EventWindow raise EdfError {
if annotation_index < 0 || annotation_index >= self.annotations.length() {
raise Invalid("event window annotation index")
}
if !finite(before) || !finite(after) || before < 0.0 || after < 0.0 {
raise Invalid("event padding requires finite nonnegative seconds")
}
if signals.is_empty() || self.records == 0 {
raise Invalid("event window requires signals and records")
}
let annotation = self.annotations[annotation_index]
let duration = if include_duration {
annotation.duration.unwrap_or(0.0)
} else {
0.0
}
let onset_ticks = event_ticks(annotation.onset)
let start = (onset_ticks - event_ticks(before)).to_double() / 10000000.0
let end = (onset_ticks + event_ticks(duration) + event_ticks(after)).to_double() /
10000000.0
ignore(time_text(start, true))
ignore(time_text(end, true))
check_window(start, end, max_samples)
if start >= end {
raise Invalid("event window must have positive duration")
}
let mut complete = start >= self.starts[0] &&
end <= self.starts[self.records - 1] + self.header.duration
for gap in self.gaps() {
if gap.start < end && gap.end > start {
complete = false
}
}
if !complete && !allow_partial {
raise Invalid(
"event window crosses missing recording time; explicitly allow_partial to retain it",
)
}
let seen : Map[Int, Bool] = Map([])
let mut samples = 0
// Preflight every channel before allocating sample objects.
for signal in signals {
if seen.contains(signal) {
raise Invalid("event window signals must be distinct")
}
seen[signal] = true
let (_, count) = self.window_spans(
signal,
start,
end,
max_samples - samples,
)
samples += count
}
let channels = signals.map(signal => {
self.time_window(signal, start, end, max_samples~)
})
{ annotation_index, annotation, start, end, complete, samples, channels, }
}