use super::{
MeetConstraint, MeetTierInput,
anchors::explicit_table_or_culet,
blocks::{Block, classify_blocks},
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TokenResolution {
Tiers(Vec<usize>),
Partial(Vec<usize>),
Ignorable,
Unresolved,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedNames {
pub refs: Vec<usize>,
pub fully: bool,
}
const IGNORABLE_MEET_WORDS: &[&str] = &[
"all",
"and",
"apex",
"at",
"between",
"center",
"centerpoint",
"corner",
"corners",
"cp",
"cut",
"edge",
"facet",
"facets",
"form",
"frosted",
"gmp",
"index",
"keel",
"leave",
"level",
"line",
"lines",
"meet",
"mp",
"note",
"of",
"on",
"or",
"pcp",
"point",
"points",
"preform",
"see",
"tcp",
"the",
"tip",
"to",
"top",
"with",
];
pub struct MeetNameResolver<'a> {
tiers: &'a [MeetTierInput],
blocks: Vec<Block>,
girdle_tier: Option<usize>,
culet_tier: Option<usize>,
table_tier: Option<usize>,
}
impl<'a> MeetNameResolver<'a> {
#[must_use]
pub fn new(tiers: &'a [MeetTierInput]) -> Self {
let blocks = classify_blocks(tiers);
let girdle_tier = tiers
.iter()
.position(|t| t.angle_deg.abs() >= 85.0)
.or_else(|| {
tiers
.iter()
.position(|t| matches!(t.constraint, MeetConstraint::ScaleReference(_)))
});
let culet_tier = explicit_table_or_culet(tiers, &blocks, Block::Pavilion);
let table_tier = explicit_table_or_culet(tiers, &blocks, Block::Crown);
Self {
tiers,
blocks,
girdle_tier,
culet_tier,
table_tier,
}
}
fn name_match(&self, token: &str) -> Option<usize> {
self.tiers
.iter()
.position(|t| t.names.iter().any(|n| n == token))
.or_else(|| {
self.tiers
.iter()
.position(|t| t.names.iter().any(|n| n.eq_ignore_ascii_case(token)))
})
}
fn resolve_simple(&self, token: &str) -> TokenResolution {
if let Some(i) = self.name_match(token) {
return TokenResolution::Tiers(vec![i]);
}
let lower = token.to_ascii_lowercase();
let girdleish = lower.contains("girdle")
|| (lower.as_bytes().first() == Some(&b'g')
&& lower.as_bytes()[1..].iter().all(u8::is_ascii_digit));
if girdleish && let Some(g) = self.girdle_tier {
return TokenResolution::Tiers(vec![g]);
}
if lower.contains("culet") {
return self.culet_tier.map_or(TokenResolution::Ignorable, |c| {
TokenResolution::Tiers(vec![c])
});
}
if lower == "table"
&& let Some(t) = self.table_tier
{
return TokenResolution::Tiers(vec![t]);
}
if IGNORABLE_MEET_WORDS.contains(&lower.as_str()) {
return TokenResolution::Ignorable;
}
let mut chars = token.chars();
if let Some(first) = chars.next() {
let want = match first {
'P' | 'p' => Some(Block::Pavilion),
'C' | 'c' => Some(Block::Crown),
_ => None,
};
let rest = chars.as_str();
if let Some(want) = want
&& !rest.is_empty()
&& let Some(i) = self.name_match(rest)
&& self.blocks[i] == want
{
return TokenResolution::Tiers(vec![i]);
}
}
if token.len() > 2
&& let Some(stripped) = token.strip_suffix(['s', 'S'])
&& let Some(i) = self.name_match(stripped)
{
return TokenResolution::Tiers(vec![i]);
}
if !token.is_empty() && token.chars().all(|c| c.is_ascii_digit()) {
let pavilion_name = format!("P{token}");
if let Some(i) = self.name_match(&pavilion_name)
&& self.blocks[i] == Block::Pavilion
{
return TokenResolution::Tiers(vec![i]);
}
let pavilion_f_name = format!("PF{token}");
if let Some(i) = self.name_match(&pavilion_f_name)
&& self.blocks[i] == Block::Pavilion
{
return TokenResolution::Tiers(vec![i]);
}
}
if token.len() == 1
&& let Some(c) = token.chars().next()
&& c.is_ascii_alphabetic()
{
let letter_idx = (c.to_ascii_uppercase() as u8).saturating_sub(b'A') + 1;
let c_cand = format!("C{letter_idx}");
if let Some(i) = self.name_match(&c_cand)
&& self.blocks[i] == Block::Crown
{
return TokenResolution::Tiers(vec![i]);
}
}
TokenResolution::Unresolved
}
#[must_use]
pub fn resolve_token(&self, token: &str) -> TokenResolution {
match self.resolve_simple(token) {
TokenResolution::Unresolved => {}
resolved => return resolved,
}
if token.contains('-') {
let parts: Vec<&str> = token.split('-').filter(|p| !p.is_empty()).collect();
if parts.len() >= 2 {
let mut refs = Vec::new();
let mut all_resolved = true;
for part in parts {
match self.resolve_simple(part) {
TokenResolution::Tiers(v) | TokenResolution::Partial(v) => refs.extend(v),
TokenResolution::Ignorable => {}
TokenResolution::Unresolved => all_resolved = false,
}
}
if !refs.is_empty() {
return if all_resolved {
TokenResolution::Tiers(refs)
} else {
TokenResolution::Partial(refs)
};
}
}
}
TokenResolution::Unresolved
}
#[must_use]
pub fn resolve_names(&self, names: &[String]) -> ResolvedNames {
let mut refs: Vec<usize> = Vec::new();
let mut fully = true;
let mut any = false;
for name in names {
match self.resolve_token(name) {
TokenResolution::Tiers(v) => {
refs.extend(v);
any = true;
}
TokenResolution::Partial(v) => {
refs.extend(v);
any = true;
fully = false;
}
TokenResolution::Ignorable => {}
TokenResolution::Unresolved => fully = false,
}
}
refs.sort_unstable();
refs.dedup();
ResolvedNames {
refs,
fully: fully && any,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tier(angle_deg: f64, indices: &[f64], names: &[&str]) -> MeetTierInput {
MeetTierInput {
angle_deg,
indices: indices.to_vec(),
constraint: MeetConstraint::MeetExisting,
names: names.iter().map(|s| (*s).to_string()).collect(),
}
}
fn resolver_fixture() -> Vec<MeetTierInput> {
vec![
tier(-42.0, &[8.0, 24.0], &["1"]),
tier(-90.0, &[8.0, 24.0], &[]),
tier(-50.0, &[16.0], &["2"]),
tier(41.0, &[8.0, 24.0], &["A"]),
tier(30.0, &[16.0], &["B"]),
tier(0.0, &[], &["T"]),
]
}
#[test]
fn resolver_exact_match_always_wins_over_fallbacks() {
let mut tiers = resolver_fixture();
tiers[4].names = vec!["g".to_string()]; let r = MeetNameResolver::new(&tiers);
assert_eq!(r.resolve_token("g"), TokenResolution::Tiers(vec![4]));
}
#[test]
fn resolver_case_insensitive_and_girdle_fallbacks() {
let tiers = resolver_fixture();
let r = MeetNameResolver::new(&tiers);
assert_eq!(r.resolve_token("a"), TokenResolution::Tiers(vec![3]));
assert_eq!(r.resolve_token("girdle"), TokenResolution::Tiers(vec![1]));
assert_eq!(r.resolve_token("g"), TokenResolution::Tiers(vec![1]));
assert_eq!(r.resolve_token("G1"), TokenResolution::Tiers(vec![1]));
}
#[test]
fn resolver_splits_compound_vertex_specs() {
let tiers = resolver_fixture();
let r = MeetNameResolver::new(&tiers);
assert_eq!(
r.resolve_token("1-2-G1"),
TokenResolution::Tiers(vec![0, 2, 1])
);
assert_eq!(r.resolve_token("1-Z9"), TokenResolution::Partial(vec![0]));
}
#[test]
fn resolver_ignores_point_references_and_prose() {
let tiers = resolver_fixture();
let r = MeetNameResolver::new(&tiers);
for word in ["PCP", "TCP", "at", "corner", "index", "Level"] {
assert_eq!(
r.resolve_token(word),
TokenResolution::Ignorable,
"{word:?} should be ignorable"
);
}
assert_eq!(r.resolve_token("culet"), TokenResolution::Ignorable);
let names: Vec<String> = ["at", "corner", "of", "1-2-G1"]
.iter()
.map(|s| (*s).to_string())
.collect();
let resolved = r.resolve_names(&names);
assert!(resolved.fully);
assert_eq!(resolved.refs, vec![0, 1, 2]);
let prose: Vec<String> = ["at", "corner"].iter().map(|s| (*s).to_string()).collect();
let resolved = r.resolve_names(&prose);
assert!(!resolved.fully);
assert_eq!(resolved.refs, Vec::<usize>::new());
}
#[test]
fn resolver_strips_side_prefix_and_plural() {
let mut tiers = resolver_fixture();
tiers[4].names = vec!["Star".to_string()];
let r = MeetNameResolver::new(&tiers);
assert_eq!(r.resolve_token("P1"), TokenResolution::Tiers(vec![0]));
assert_eq!(r.resolve_token("C1"), TokenResolution::Unresolved);
assert_eq!(r.resolve_token("Stars"), TokenResolution::Tiers(vec![4]));
}
#[test]
fn resolver_finds_flat_table_when_referenced_by_word() {
let tiers = resolver_fixture();
let r = MeetNameResolver::new(&tiers);
assert_eq!(r.resolve_token("table"), TokenResolution::Tiers(vec![5]));
}
#[test]
fn resolver_matches_legacy_123_abc_to_modern_labels() {
let mut tiers = resolver_fixture();
tiers[0].names = vec!["P1".to_string()];
tiers[2].names = vec!["P2".to_string()];
tiers[3].names = vec!["C1".to_string()];
tiers[4].names = vec!["C2".to_string()];
let r = MeetNameResolver::new(&tiers);
assert_eq!(r.resolve_token("1"), TokenResolution::Tiers(vec![0]));
assert_eq!(r.resolve_token("2"), TokenResolution::Tiers(vec![2]));
assert_eq!(r.resolve_token("A"), TokenResolution::Tiers(vec![3]));
assert_eq!(r.resolve_token("a"), TokenResolution::Tiers(vec![3]));
assert_eq!(r.resolve_token("B"), TokenResolution::Tiers(vec![4]));
assert_eq!(r.resolve_token("b"), TokenResolution::Tiers(vec![4]));
}
}