use std::collections::HashSet;
#[derive(Debug, Clone, Default)]
pub struct SliceSpec {
pub cik_list: Option<HashSet<u64>>,
pub form_types: Option<HashSet<String>>,
pub year_range: Option<(u16, u16)>,
}
impl SliceSpec {
pub fn new() -> Self {
Self::default()
}
pub fn with_cik_list(mut self, ciks: impl IntoIterator<Item = u64>) -> Self {
self.cik_list = Some(ciks.into_iter().collect());
self
}
pub fn with_form_types(mut self, forms: impl IntoIterator<Item = String>) -> Self {
self.form_types = Some(forms.into_iter().collect());
self
}
pub fn with_year_range(mut self, start: u16, end: u16) -> Self {
self.year_range = Some((start, end));
self
}
pub fn from_optional_filters(
cik_list: Option<Vec<u64>>,
form_types: Option<Vec<String>>,
year_range: Option<(u16, u16)>,
) -> Self {
let mut s = Self::default();
if let Some(ciks) = cik_list {
if !ciks.is_empty() {
s = s.with_cik_list(ciks);
}
}
if let Some(forms) = form_types {
if !forms.is_empty() {
s = s.with_form_types(forms);
}
}
if let Some((lo, hi)) = year_range {
s = s.with_year_range(lo, hi);
}
s
}
pub fn cik_matches(&self, cik: u64) -> bool {
match &self.cik_list {
Some(set) => set.contains(&cik),
None => true,
}
}
pub fn form_matches(&self, form_type: &str) -> bool {
match &self.form_types {
Some(set) => set.contains(form_type),
None => true,
}
}
pub fn date_matches(&self, filed_date: &str) -> bool {
let Some((lo, hi)) = self.year_range else {
return true;
};
let year_str: String = filed_date.chars().take(4).collect();
match year_str.parse::<u16>() {
Ok(y) => y >= lo && y <= hi,
Err(_) => false,
}
}
pub fn matches(&self, cik: u64, form_type: &str, filed_date: &str) -> bool {
self.cik_matches(cik) && self.form_matches(form_type) && self.date_matches(filed_date)
}
pub fn is_unrestricted(&self) -> bool {
self.cik_list.is_none() && self.form_types.is_none() && self.year_range.is_none()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_admits_everything() {
let s = SliceSpec::default();
assert!(s.is_unrestricted());
assert!(s.matches(320193, "10-K", "2024-11-01"));
assert!(s.matches(0, "", ""));
}
#[test]
fn cik_list_filters() {
let s = SliceSpec::new().with_cik_list([320193u64, 789019u64]);
assert!(s.cik_matches(320193));
assert!(s.cik_matches(789019));
assert!(!s.cik_matches(123));
assert!(!s.is_unrestricted());
}
#[test]
fn form_types_filters_case_sensitive() {
let s = SliceSpec::new().with_form_types(["10-K".to_string(), "10-Q".to_string()]);
assert!(s.form_matches("10-K"));
assert!(s.form_matches("10-Q"));
assert!(!s.form_matches("10-K/A")); assert!(!s.form_matches("8-K"));
}
#[test]
fn year_range_filters_inclusively() {
let s = SliceSpec::new().with_year_range(2020, 2024);
assert!(s.date_matches("2020-01-15"));
assert!(s.date_matches("2024-12-31"));
assert!(s.date_matches("2022-06-30"));
assert!(!s.date_matches("2019-12-31"));
assert!(!s.date_matches("2025-01-01"));
}
#[test]
fn date_matches_handles_dense_format() {
let s = SliceSpec::new().with_year_range(2024, 2024);
assert!(s.date_matches("20240928"));
assert!(!s.date_matches("20230928"));
}
#[test]
fn date_matches_handles_malformed_input() {
let s = SliceSpec::new().with_year_range(2024, 2024);
assert!(!s.date_matches("not-a-date"));
assert!(!s.date_matches(""));
}
#[test]
fn combined_matches_requires_all_filters_to_pass() {
let s = SliceSpec::new()
.with_cik_list([320193u64])
.with_form_types(["10-K".to_string()])
.with_year_range(2024, 2024);
assert!(s.matches(320193, "10-K", "2024-11-01"));
assert!(!s.matches(320193, "8-K", "2024-11-01"));
assert!(!s.matches(320193, "10-K", "2023-11-01"));
assert!(!s.matches(789019, "10-K", "2024-11-01"));
}
}