use crate::document::DataBlock;
pub(super) struct OperatorTable {
operations: Vec<Box<str>>,
}
impl OperatorTable {
pub(super) fn of(block: &DataBlock) -> Self {
let category = block
.category("space_group_symop")
.map(|category| (category, "operation_xyz"))
.or_else(|| {
block
.category("symmetry_equiv")
.map(|category| (category, "pos_as_xyz"))
});
let mut operations = Vec::new();
if let Some((category, item)) = category {
for row in 0..category.row_count() {
if let Some(text) = category.text(item, row) {
operations.push(Box::from(text));
}
}
}
Self { operations }
}
fn operation(&self, number: u32) -> Option<&str> {
let position = usize::try_from(number).ok()?.checked_sub(1)?;
self.operations.get(position).map(Box::as_ref)
}
fn is_empty(&self) -> bool {
self.operations.is_empty()
}
}
pub(super) struct Unrepresentable<'a> {
pub(super) partner: &'static str,
pub(super) value: &'a str,
}
pub(super) fn unrepresentable<'a>(
first: Option<&'a str>,
second: Option<&'a str>,
operators: &OperatorTable,
) -> Option<Unrepresentable<'a>> {
for (partner, value) in [("ptnr1_symmetry", first), ("ptnr2_symmetry", second)] {
let Some(value) = value.filter(|text| is_stated(text)) else {
continue;
};
if !is_local(value, operators) {
return Some(Unrepresentable { partner, value });
}
}
None
}
fn is_stated(value: &str) -> bool {
let trimmed = value.trim();
!trimmed.is_empty() && trimmed != "?" && trimmed != "."
}
fn is_local(value: &str, operators: &OperatorTable) -> bool {
let value = value.trim();
let Some((operator, translation)) = parse(value) else {
return false;
};
if translation != [0, 0, 0] {
return false;
}
if operators.is_empty() {
return operator == 1;
}
operators
.operation(operator)
.is_some_and(is_identity_operation)
}
fn parse(value: &str) -> Option<(u32, [i32; 3])> {
match value.split_once('_') {
Some((operator, digits)) => {
if digits.len() != 3 {
return None;
}
let mut translation = [0i32; 3];
for (slot, character) in translation.iter_mut().zip(digits.bytes()) {
if !character.is_ascii_digit() {
return None;
}
let digit = u32::from(character) - u32::from(b'0');
*slot = i32::try_from(digit).ok()?.checked_sub(5)?;
}
Some((operator.parse().ok()?, translation))
}
None => Some((value.parse().ok()?, [0, 0, 0])),
}
}
fn is_identity_operation(operation: &str) -> bool {
let mut buffer = [0u8; 10];
let mut length = 0usize;
for byte in operation.bytes() {
if byte.is_ascii_whitespace() {
continue;
}
let Some(slot) = buffer.get_mut(length) else {
return false;
};
*slot = byte.to_ascii_lowercase();
length += 1;
}
let Some(text) = buffer.get(..length) else {
return false;
};
let text = match text.strip_prefix(b"+".as_slice()) {
Some(rest) => rest,
None => text,
};
matches!(text, b"x,y,z" | b"x,y,z,1" | b"x,y,z,1.0")
}
#[cfg(test)]
#[path = "bonds_symmetry_tests.rs"]
mod tests;