#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SecFormBucket {
Form3,
Form4,
Form5,
Form144,
Form13f,
Form8k,
Sc13d,
Sc13g,
Def14a,
Form10k,
}
impl SecFormBucket {
pub const fn matching_forms(self) -> &'static [&'static str] {
match self {
SecFormBucket::Form3 => &["3", "3/A"],
SecFormBucket::Form4 => &["4", "4/A"],
SecFormBucket::Form5 => &["5", "5/A"],
SecFormBucket::Form144 => &["144", "144/A"],
SecFormBucket::Form13f => &["13F-HR", "13F-HR/A"],
SecFormBucket::Form8k => &["8-K", "8-K/A"],
SecFormBucket::Sc13d => &["SC 13D", "SC 13D/A", "SCHEDULE 13D", "SCHEDULE 13D/A"],
SecFormBucket::Sc13g => &["SC 13G", "SC 13G/A", "SCHEDULE 13G", "SCHEDULE 13G/A"],
SecFormBucket::Def14a => &["DEF 14A", "DEFA14A", "PRE 14A"],
SecFormBucket::Form10k => &["10-K", "10-K/A"],
}
}
pub const fn as_str(self) -> &'static str {
match self {
SecFormBucket::Form3 => "form3",
SecFormBucket::Form4 => "form4",
SecFormBucket::Form5 => "form5",
SecFormBucket::Form144 => "form144",
SecFormBucket::Form13f => "form13f",
SecFormBucket::Form8k => "form8k",
SecFormBucket::Sc13d => "sc13d",
SecFormBucket::Sc13g => "sc13g",
SecFormBucket::Def14a => "def14a",
SecFormBucket::Form10k => "form10k",
}
}
pub fn from_form_string(form: &str) -> Option<Self> {
for bucket in ALL_BUCKETS {
if bucket.matching_forms().contains(&form) {
return Some(*bucket);
}
}
None
}
}
pub const ALL_BUCKETS: &[SecFormBucket] = &[
SecFormBucket::Form3,
SecFormBucket::Form4,
SecFormBucket::Form5,
SecFormBucket::Form144,
SecFormBucket::Form13f,
SecFormBucket::Form8k,
SecFormBucket::Sc13d,
SecFormBucket::Sc13g,
SecFormBucket::Def14a,
SecFormBucket::Form10k,
];
pub const LEAN_FETCH_BUCKETS: &[SecFormBucket] = &[
SecFormBucket::Form3,
SecFormBucket::Form4,
SecFormBucket::Form5,
SecFormBucket::Form8k,
];
pub fn all_buckets() -> Vec<(&'static str, Vec<&'static str>)> {
ALL_BUCKETS
.iter()
.map(|b| (b.as_str(), b.matching_forms().to_vec()))
.collect()
}
pub fn resolve_fetch_buckets(form_types: Option<&[&str]>) -> (Vec<SecFormBucket>, Vec<String>) {
let Some(form_types) = form_types else {
return (LEAN_FETCH_BUCKETS.to_vec(), Vec::new());
};
let mut active = Vec::new();
let mut unmatched = Vec::new();
for form in form_types {
match SecFormBucket::from_form_string(form) {
Some(bucket) if !active.contains(&bucket) => active.push(bucket),
Some(_) => {} None => unmatched.push((*form).to_string()),
}
}
let mut ordered = Vec::with_capacity(active.len());
for bucket in ALL_BUCKETS {
if active.contains(bucket) {
ordered.push(*bucket);
}
}
(ordered, unmatched)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lean_default_when_form_types_none() {
let (buckets, unmatched) = resolve_fetch_buckets(None);
assert_eq!(buckets, LEAN_FETCH_BUCKETS);
assert!(unmatched.is_empty());
}
#[test]
fn explicit_form_types_map_to_buckets() {
let (buckets, unmatched) = resolve_fetch_buckets(Some(&["4", "13F-HR"]));
assert_eq!(buckets, vec![SecFormBucket::Form4, SecFormBucket::Form13f]);
assert!(unmatched.is_empty());
}
#[test]
fn variant_spellings_accepted() {
let (buckets, _) = resolve_fetch_buckets(Some(&["SC 13D", "SCHEDULE 13D"]));
assert_eq!(buckets, vec![SecFormBucket::Sc13d]);
}
#[test]
fn unknown_forms_land_in_unmatched() {
let (buckets, unmatched) = resolve_fetch_buckets(Some(&["4", "XYZ-99"]));
assert_eq!(buckets, vec![SecFormBucket::Form4]);
assert_eq!(unmatched, vec!["XYZ-99".to_string()]);
}
#[test]
fn bucket_order_is_declaration_order_not_input_order() {
let (buckets, _) = resolve_fetch_buckets(Some(&["10-K", "4", "8-K"]));
assert_eq!(
buckets,
vec![
SecFormBucket::Form4,
SecFormBucket::Form8k,
SecFormBucket::Form10k,
]
);
}
}