use std::collections::BTreeMap;
#[derive(Clone, Debug)]
pub struct QueryResult {
pub(super) matches: Vec<(String, f64)>,
}
impl QueryResult {
#[must_use]
pub fn sum(&self) -> f64 {
self.matches.iter().map(|(_, v)| *v).sum()
}
#[must_use]
pub const fn len(&self) -> usize {
self.matches.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.matches.is_empty()
}
#[must_use]
pub fn by_subject(&self) -> BTreeMap<String, f64> {
let mut acc = BTreeMap::new();
for (path, value) in &self.matches {
let subject = path.split_once('.').map_or_else(|| path.clone(), |(s, _)| s.to_string());
*acc.entry(subject).or_insert(0.0) += *value;
}
acc
}
pub fn iter(&self) -> impl Iterator<Item = (&str, f64)> {
self.matches.iter().map(|(p, v)| (p.as_str(), *v))
}
}
pub(super) fn matches(pattern: &str, path: &str) -> bool {
let pat: Vec<&str> = pattern.split('.').collect();
let seg: Vec<&str> = path.split('.').collect();
match_slices(&pat, &seg)
}
fn match_slices(pat: &[&str], seg: &[&str]) -> bool {
match (pat.first(), seg.first()) {
(None, None) => true,
(Some(&"**"), _) => {
if match_slices(&pat[1..], seg) {
return true;
}
if seg.is_empty() {
return false;
}
match_slices(pat, &seg[1..])
}
(None, Some(_)) | (Some(_), None) => false,
(Some(p), Some(s)) => segment_matches(p, s) && match_slices(&pat[1..], &seg[1..]),
}
}
fn segment_matches(pat: &str, seg: &str) -> bool {
let bytes_pat = pat.as_bytes();
let bytes_seg = seg.as_bytes();
glob_match(bytes_pat, bytes_seg)
}
fn glob_match(pat: &[u8], seg: &[u8]) -> bool {
match (pat.first(), seg.first()) {
(None, None) => true,
(Some(&b'*'), _) => {
if glob_match(&pat[1..], seg) {
return true;
}
if seg.is_empty() {
return false;
}
glob_match(pat, &seg[1..])
}
(None, Some(_)) | (Some(_), None) => false,
(Some(p), Some(s)) => p == s && glob_match(&pat[1..], &seg[1..]),
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::*;
#[test]
fn literal_matches() {
assert!(matches("core0.commit.insts", "core0.commit.insts"));
assert!(!matches("core0.commit.insts", "core0.commit.other"));
assert!(!matches("core0.commit.insts", "core0.commit.insts.extra"));
}
#[test]
fn single_star_matches_one_segment() {
assert!(matches("core*.commit.insts", "core0.commit.insts"));
assert!(matches("core*.commit.insts", "core42.commit.insts"));
assert!(!matches("core*.commit.insts", "core0.commit.other"));
assert!(!matches("*.insts", "core0.commit.insts"));
}
#[test]
fn double_star_matches_any_depth() {
assert!(matches("**.misses", "core0.cache.l1d.misses"));
assert!(matches("**.misses", "core0.misses"));
assert!(matches("core0.**.hits", "core0.cache.l1d.hits"));
assert!(matches("core0.**.hits", "core0.hits"));
assert!(!matches("**.hits", "core0.cache.l1d.misses"));
}
#[test]
fn double_star_matches_zero_segments() {
assert!(matches("core0.**", "core0"));
assert!(matches("core0.**.hits", "core0.hits"));
}
#[test]
fn query_result_helpers() {
let q = QueryResult {
matches: vec![("core0.commit.insts".into(), 10.0), ("core1.commit.insts".into(), 20.0)],
};
assert_eq!(q.sum(), 30.0);
assert_eq!(q.len(), 2);
let by = q.by_subject();
assert_eq!(by["core0"], 10.0);
assert_eq!(by["core1"], 20.0);
}
}