use crate::decoder::EntityIndex;
use crate::parser::EntityScanner;
use std::sync::Arc;
pub struct ColumnarEntityIndex {
ids: Vec<u32>,
starts: Vec<u32>,
lengths: Vec<u32>,
}
impl ColumnarEntityIndex {
pub fn from_columns(ids: &[u32], starts: &[u32], lengths: &[u32]) -> Self {
let n = ids.len();
if n == 0 || starts.len() != n || lengths.len() != n {
return Self {
ids: Vec::new(),
starts: Vec::new(),
lengths: Vec::new(),
};
}
if is_strictly_ascending(ids) {
return Self {
ids: ids.to_vec(),
starts: starts.to_vec(),
lengths: lengths.to_vec(),
};
}
Self::from_unsorted(ids.to_vec(), starts.to_vec(), lengths.to_vec())
}
pub fn from_hashmap_consuming(map: EntityIndex) -> Self {
let n = map.len();
let mut rows: Vec<(u32, u32, u32)> = Vec::with_capacity(n);
for (id, (start, end)) in map {
debug_assert!(end <= u32::MAX as usize, "entity offset exceeds the u32 column ceiling");
rows.push((id, start as u32, (end - start) as u32));
}
rows.sort_unstable_by_key(|r| r.0);
let mut ids = Vec::with_capacity(rows.len());
let mut starts = Vec::with_capacity(rows.len());
let mut lengths = Vec::with_capacity(rows.len());
for (id, start, len) in rows {
ids.push(id);
starts.push(start);
lengths.push(len);
}
Self { ids, starts, lengths }
}
pub fn from_hashmap(map: &EntityIndex) -> Self {
let n = map.len();
let mut ids = Vec::with_capacity(n);
let mut starts = Vec::with_capacity(n);
let mut lengths = Vec::with_capacity(n);
for (&id, &(start, end)) in map.iter() {
debug_assert!(end <= u32::MAX as usize, "entity offset exceeds the u32 column ceiling");
ids.push(id);
starts.push(start as u32);
lengths.push((end - start) as u32);
}
Self::from_unsorted(ids, starts, lengths)
}
pub fn from_scan<T>(content: &T) -> Self
where
T: AsRef<[u8]> + ?Sized,
{
let content = content.as_ref();
let estimated = content.len() / 50;
let mut ids = Vec::with_capacity(estimated);
let mut starts = Vec::with_capacity(estimated);
let mut lengths = Vec::with_capacity(estimated);
let mut scanner = EntityScanner::new(content);
while let Some((id, _type_name, start, end)) = scanner.next_entity() {
debug_assert!(end <= u32::MAX as usize, "entity offset exceeds the u32 column ceiling");
ids.push(id);
starts.push(start as u32);
lengths.push((end - start) as u32);
}
Self::from_unsorted(ids, starts, lengths)
}
fn from_unsorted(ids: Vec<u32>, starts: Vec<u32>, lengths: Vec<u32>) -> Self {
let n = ids.len();
let mut perm: Vec<u32> = (0..n as u32).collect();
perm.sort_unstable_by(|&a, &b| {
let ka = ids[a as usize];
let kb = ids[b as usize];
ka.cmp(&kb).then_with(|| a.cmp(&b))
});
let mut out_ids: Vec<u32> = Vec::with_capacity(n);
let mut out_starts: Vec<u32> = Vec::with_capacity(n);
let mut out_lengths: Vec<u32> = Vec::with_capacity(n);
for &p in &perm {
let p = p as usize;
let id = ids[p];
if out_ids.last() == Some(&id) {
let li = out_ids.len() - 1;
out_starts[li] = starts[p];
out_lengths[li] = lengths[p];
} else {
out_ids.push(id);
out_starts.push(starts[p]);
out_lengths.push(lengths[p]);
}
}
out_ids.shrink_to_fit();
out_starts.shrink_to_fit();
out_lengths.shrink_to_fit();
Self {
ids: out_ids,
starts: out_starts,
lengths: out_lengths,
}
}
#[inline]
pub fn lookup(&self, id: u32) -> Option<(usize, usize)> {
match self.ids.binary_search(&id) {
Ok(i) => {
let start = self.starts[i] as usize;
Some((start, start + self.lengths[i] as usize))
}
Err(_) => None,
}
}
#[inline]
pub fn ids(&self) -> &[u32] {
&self.ids
}
#[inline]
pub fn starts(&self) -> &[u32] {
&self.starts
}
#[inline]
pub fn lengths(&self) -> &[u32] {
&self.lengths
}
#[inline]
pub fn len(&self) -> usize {
self.ids.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.ids.is_empty()
}
}
#[inline]
fn is_strictly_ascending(ids: &[u32]) -> bool {
ids.windows(2).all(|w| w[0] < w[1])
}
pub(crate) enum EntityIndexStore {
Hash(Arc<EntityIndex>),
Columnar(Arc<ColumnarEntityIndex>),
}
impl EntityIndexStore {
#[inline]
pub(crate) fn lookup(&self, id: u32) -> Option<(usize, usize)> {
match self {
EntityIndexStore::Hash(m) => m.get(&id).copied(),
EntityIndexStore::Columnar(c) => c.lookup(id),
}
}
}
impl<'a> crate::EntityDecoder<'a> {
pub fn with_arc_columnar_index<T>(content: &'a T, index: Arc<ColumnarEntityIndex>) -> Self
where
T: AsRef<[u8]> + ?Sized,
{
let mut decoder = Self::new(content);
decoder.set_columnar_index(index);
decoder
}
pub fn set_columnar_index(&mut self, index: Arc<ColumnarEntityIndex>) {
self.entity_index = Some(EntityIndexStore::Columnar(index));
}
}
#[cfg(test)]
mod columnar_index_tests {
use super::*;
#[test]
fn sorted_unique_uses_fast_path_and_looks_up() {
let ids = [1u32, 5, 9, 100];
let starts = [10u32, 20, 30, 40];
let lengths = [3u32, 4, 5, 6];
let idx = ColumnarEntityIndex::from_columns(&ids, &starts, &lengths);
assert_eq!(idx.len(), 4);
assert_eq!(idx.lookup(1), Some((10, 13)));
assert_eq!(idx.lookup(5), Some((20, 24)));
assert_eq!(idx.lookup(100), Some((40, 46)));
assert_eq!(idx.lookup(2), None);
assert_eq!(idx.lookup(101), None);
}
#[test]
fn unsorted_input_is_sorted_then_searched() {
let ids = [100u32, 1, 9, 5];
let starts = [40u32, 10, 30, 20];
let lengths = [6u32, 3, 5, 4];
let idx = ColumnarEntityIndex::from_columns(&ids, &starts, &lengths);
assert_eq!(idx.ids(), &[1, 5, 9, 100]);
assert_eq!(idx.lookup(1), Some((10, 13)));
assert_eq!(idx.lookup(5), Some((20, 24)));
assert_eq!(idx.lookup(9), Some((30, 35)));
assert_eq!(idx.lookup(100), Some((40, 46)));
}
#[test]
fn duplicate_id_last_wins() {
let ids = [7u32, 3, 7];
let starts = [10u32, 20, 30];
let lengths = [1u32, 2, 3];
let idx = ColumnarEntityIndex::from_columns(&ids, &starts, &lengths);
assert_eq!(idx.len(), 2);
assert_eq!(idx.lookup(7), Some((30, 33)));
assert_eq!(idx.lookup(3), Some((20, 22)));
}
#[test]
fn empty_and_mismatched_columns_are_empty() {
assert!(ColumnarEntityIndex::from_columns(&[], &[], &[]).is_empty());
assert!(ColumnarEntityIndex::from_columns(&[1, 2], &[0], &[0, 0]).is_empty());
}
#[test]
fn from_hashmap_matches_lookup() {
let content = "ISO-10303-21;\nHEADER;\nENDSEC;\nDATA;\n\
#1=IFCCARTESIANPOINT((0.,0.,0.));\n\
#7=IFCCARTESIANPOINT((1.,2.,3.));\n\
ENDSEC;\nEND-ISO-10303-21;\n";
let map = crate::build_entity_index(content);
let col = ColumnarEntityIndex::from_hashmap(&map);
assert_eq!(col.len(), map.len());
for (&id, &(s, e)) in map.iter() {
assert_eq!(col.lookup(id), Some((s, e)));
}
let scanned = ColumnarEntityIndex::from_scan(content);
assert_eq!(scanned.ids(), col.ids());
for &id in col.ids() {
assert_eq!(scanned.lookup(id), col.lookup(id));
}
}
#[test]
fn consuming_and_borrowing_hashmap_builds_agree() {
let mut map: crate::EntityIndex = Default::default();
for (id, start, end) in [(7u32, 100usize, 150usize), (3, 0, 40), (9, 200, 260), (1, 41, 99)] {
map.insert(id, (start, end));
}
let borrowed = ColumnarEntityIndex::from_hashmap(&map);
let consumed = ColumnarEntityIndex::from_hashmap_consuming(map);
for id in [0u32, 1, 3, 7, 9, 10, u32::MAX] {
assert_eq!(borrowed.lookup(id), consumed.lookup(id), "id {id}");
}
assert_eq!(consumed.len(), 4);
}
}