use std::collections::BTreeSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum NameBucket {
MaterialLike,
LevelLike,
SpaceLike,
}
pub fn is_display_name(s: &str) -> bool {
let t = s.trim();
if t.len() < 2 || t.len() > 80 {
return false;
}
if t.starts_with("autodesk.")
|| t.starts_with("http://")
|| t.starts_with("https://")
|| t.contains("://")
|| t.starts_with("Uniformat")
|| t.starts_with("OmniClass")
{
return false;
}
let hexish: String = t.chars().filter(|c| c.is_ascii_hexdigit()).collect();
if hexish.len() >= 32 {
return false;
}
if !t.chars().any(|c| c.is_alphabetic()) {
return false;
}
if t.chars()
.all(|c| c.is_ascii_digit() || " .-_/\\'\"".contains(c))
{
return false;
}
true
}
pub fn classify_name(s: &str) -> Option<NameBucket> {
if !is_display_name(s) {
return None;
}
let lower = s.to_ascii_lowercase();
if lower.contains("concrete")
|| lower.contains("gypsum")
|| lower.contains("glass")
|| lower.contains("steel")
|| lower.contains("aluminum")
|| lower.contains("aluminium")
|| lower.contains("brick")
|| lower.contains("insulation")
|| lower.contains("hardwood")
|| lower.contains("masonry")
|| (lower.contains("wood") && !lower.contains("hardwoodschema"))
{
if lower.contains("mats/") || lower.contains("mats\\") || lower.ends_with(".xml") {
return None;
}
return Some(NameBucket::MaterialLike);
}
if lower.starts_with("level ")
|| lower == "level 1"
|| lower == "ground floor"
|| lower == "roof"
|| lower.contains("first floor")
|| lower.contains("second floor")
{
return Some(NameBucket::LevelLike);
}
if (lower.contains("conference")
|| lower.contains("corridor")
|| lower.contains("lobby")
|| lower.contains("office")
|| lower.contains("classroom")
|| lower.starts_with("space "))
&& s.len() <= 48
{
return Some(NameBucket::SpaceLike);
}
None
}
pub fn collect_name_candidates<'a, I>(strings: I) -> BTreeSet<(NameBucket, String)>
where
I: IntoIterator<Item = &'a str>,
{
let mut out = BTreeSet::new();
for s in strings {
if let Some(bucket) = classify_name(s) {
out.insert((bucket, s.trim().to_string()));
}
}
out
}
pub fn is_building_storey_name(s: &str) -> bool {
let lower = s.trim().to_ascii_lowercase();
if lower.is_empty() {
return false;
}
if lower.contains("head")
|| lower.contains("layout")
|| lower.contains("upgrade")
|| lower.contains("view")
|| lower.contains("plan")
{
return false;
}
if matches!(
lower.as_str(),
"ground floor" | "groundfloor" | "roof" | "mezzanine" | "basement" | "podium"
) {
return true;
}
if lower == "first floor" || lower == "second floor" || lower == "third floor" {
return true;
}
if let Some(rest) = lower.strip_prefix("level ") {
return !rest.is_empty()
&& rest
.chars()
.all(|c| c.is_ascii_digit() || c.is_whitespace());
}
false
}
pub fn building_storey_name_candidates<'a, I>(strings: I) -> Vec<String>
where
I: IntoIterator<Item = &'a str>,
{
let mut names: Vec<String> = collect_name_candidates(strings)
.into_iter()
.filter(|(bucket, name)| *bucket == NameBucket::LevelLike && is_building_storey_name(name))
.map(|(_, name)| name)
.collect();
names.sort();
names.dedup();
names
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classifies_known_material_and_level_names() {
assert_eq!(classify_name("Concrete"), Some(NameBucket::MaterialLike));
assert_eq!(classify_name("Level 1"), Some(NameBucket::LevelLike));
assert_eq!(classify_name("Lobby"), Some(NameBucket::SpaceLike));
}
#[test]
fn rejects_forge_and_asset_paths() {
assert!(classify_name("autodesk.unit.unit:meters-1.0.0").is_none());
assert!(classify_name("Mats/Hardwood/Generic.xml").is_none());
}
#[test]
fn building_storey_filter_keeps_level_and_roof() {
assert!(is_building_storey_name("Level 1"));
assert!(is_building_storey_name("Roof"));
assert!(is_building_storey_name("Ground floor"));
assert!(!is_building_storey_name("Level Head - Upgrade"));
assert!(!is_building_storey_name("Level 3 - Wall Layouts 1"));
}
}