pub const REVIT_VERSIONS: &[u16] = &[
2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026,
];
const TAG_DRIFT_CSV: &str = include_str!("../docs/data/tag-drift-2016-2026.csv");
pub fn tag_for_class(class_name: &str, version: u16) -> Option<u16> {
let col = version_column(version)?;
for line in TAG_DRIFT_CSV.lines().skip(1) {
let (name, rest) = line.split_once(',')?;
if name != class_name {
continue;
}
let cell = rest.split(',').nth(col - 1)?;
return parse_tag(cell);
}
None
}
pub fn class_for_tag(tag: u16, version: u16) -> Option<&'static str> {
let col = version_column(version)?;
for line in TAG_DRIFT_CSV.lines().skip(1) {
let Some((name, rest)) = line.split_once(',') else {
continue;
};
if let Some(cell) = rest.split(',').nth(col - 1) {
if let Some(t) = parse_tag(cell) {
if t == tag {
return Some(name);
}
}
}
}
None
}
pub fn dataset_size() -> usize {
TAG_DRIFT_CSV.lines().skip(1).count()
}
fn version_column(version: u16) -> Option<usize> {
REVIT_VERSIONS
.iter()
.position(|&v| v == version)
.map(|i| i + 1)
}
fn parse_tag(cell: &str) -> Option<u16> {
let c = cell.trim();
if c.is_empty() {
return None;
}
let hex = c.strip_prefix("0x").unwrap_or(c);
u16::from_str_radix(hex, 16).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn revit_versions_covers_2016_to_2026() {
assert_eq!(REVIT_VERSIONS.len(), 11);
assert_eq!(REVIT_VERSIONS[0], 2016);
assert_eq!(REVIT_VERSIONS[10], 2026);
}
#[test]
fn dataset_is_non_trivial() {
assert!(
dataset_size() >= 100,
"tag-drift dataset dropped below 100 classes: {}",
dataset_size()
);
}
#[test]
fn tag_for_known_class_matches_csv() {
assert_eq!(tag_for_class("ADocWarnings", 2016), Some(0x001b));
assert_eq!(tag_for_class("ADocWarnings", 2026), Some(0x001b));
}
#[test]
fn tag_for_unknown_class_is_none() {
assert_eq!(tag_for_class("DefinitelyNotAClass", 2024), None);
}
#[test]
fn tag_for_unknown_version_is_none() {
assert_eq!(tag_for_class("ADocWarnings", 2015), None);
assert_eq!(tag_for_class("ADocWarnings", 2027), None);
}
#[test]
fn reverse_lookup_roundtrips() {
let tag = tag_for_class("AProperties", 2024).expect("class present");
assert_eq!(class_for_tag(tag, 2024), Some("AProperties"));
}
#[test]
fn class_introduced_later_has_empty_early_cells() {
assert_eq!(tag_for_class("ATFProvenanceBaseCell", 2016), None);
assert_eq!(tag_for_class("ATFProvenanceBaseCell", 2021), None);
assert!(tag_for_class("ATFProvenanceBaseCell", 2022).is_some());
}
#[test]
fn parse_tag_handles_empty_and_malformed() {
assert_eq!(parse_tag(""), None);
assert_eq!(parse_tag(" "), None);
assert_eq!(parse_tag("not-hex"), None);
assert_eq!(parse_tag("0x001b"), Some(0x001b));
assert_eq!(parse_tag("001b"), Some(0x001b));
}
}