const RAW: &str = include_str!("../data/excel-template-search-demand-us-2026.csv");
#[derive(Debug, Clone, PartialEq)]
pub struct Row {
pub keyword: &'static str,
pub monthly_search_volume_us: u32,
pub category: &'static str,
pub free_template_url: Option<&'static str>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CategoryStats {
pub category: &'static str,
pub keywords: usize,
pub combined_monthly_volume_us: u64,
}
pub fn all() -> Vec<Row> {
RAW.lines()
.skip(1)
.filter(|l| !l.trim().is_empty())
.map(|line| {
let mut it = line.splitn(5, ',');
let keyword = it.next().unwrap_or_default();
let volume = it.next().unwrap_or("0").parse().unwrap_or(0);
let category = it.next().unwrap_or_default();
let _has_free = it.next();
let url = it.next().unwrap_or_default().trim();
Row {
keyword,
monthly_search_volume_us: volume,
category,
free_template_url: if url.is_empty() { None } else { Some(url) },
}
})
.collect()
}
pub fn top(n: usize) -> Vec<Row> {
let mut rows = all();
rows.sort_by(|a, b| b.monthly_search_volume_us.cmp(&a.monthly_search_volume_us));
rows.truncate(n);
rows
}
pub fn search(needle: &str) -> Vec<Row> {
let q = needle.to_lowercase();
let mut rows: Vec<Row> = all()
.into_iter()
.filter(|r| r.keyword.to_lowercase().contains(&q))
.collect();
rows.sort_by(|a, b| b.monthly_search_volume_us.cmp(&a.monthly_search_volume_us));
rows
}
pub fn by_category(slug: &str) -> Vec<Row> {
let mut rows: Vec<Row> = all().into_iter().filter(|r| r.category == slug).collect();
rows.sort_by(|a, b| b.monthly_search_volume_us.cmp(&a.monthly_search_volume_us));
rows
}
pub fn categories() -> Vec<CategoryStats> {
let mut cats: Vec<CategoryStats> = Vec::new();
for row in all() {
match cats.iter_mut().find(|c| c.category == row.category) {
Some(c) => {
c.keywords += 1;
c.combined_monthly_volume_us += u64::from(row.monthly_search_volume_us);
}
None => cats.push(CategoryStats {
category: row.category,
keywords: 1,
combined_monthly_volume_us: u64::from(row.monthly_search_volume_us),
}),
}
}
cats.sort_by(|a, b| b.combined_monthly_volume_us.cmp(&a.combined_monthly_volume_us));
cats
}
pub fn with_free_template() -> Vec<Row> {
let mut rows: Vec<Row> = all().into_iter().filter(|r| r.free_template_url.is_some()).collect();
rows.sort_by(|a, b| b.monthly_search_volume_us.cmp(&a.monthly_search_volume_us));
rows
}