///|
/// Keep non-overlapping matches using leftmost-longest, then highest weight.
pub fn select_leftmost_longest(
matches : ArrayView[LexiconMatch],
) -> Array[LexiconMatch] {
let sorted = matches.to_owned()
sorted.sort_by(fn(a, b) {
if a.start == b.start {
let len_a = a.end - a.start
let len_b = b.end - b.start
if len_a == len_b {
b.weight - a.weight
} else {
len_b - len_a
}
} else {
a.start - b.start
}
})
let out : Array[LexiconMatch] = []
let mut cursor = 0
for hit in sorted {
if hit.start >= cursor {
out.push(hit)
cursor = hit.end
}
}
out
}
///|
/// Keep at most `limit` highest-scoring candidates.
pub fn top_candidates(
candidates : ArrayView[Candidate],
limit? : Int = 10,
) -> Array[Candidate] {
let out = candidates.to_owned()
out.sort_by(fn(a, b) {
if a.score == b.score {
b.span_count - a.span_count
} else {
b.score - a.score
}
})
let bounded : Array[Candidate] = []
let mut count = 0
for candidate in out {
if count < limit {
bounded.push(candidate)
count = count + 1
}
}
bounded
}
///|
/// Count matches by label.
pub fn count_by_label(matches : ArrayView[LexiconMatch]) -> Map[String, Int] {
let counts : Map[String, Int] = Map([])
for hit in matches {
counts[hit.label] = counts.get_or_default(hit.label, 0) + 1
}
counts
}