1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
//! IUPAC naming with CIP stereo prefix.
//!
//! Wraps [`chematic_iupac::name`] and prepends `(R)-`, `(S)-`, or multi-centre
//! descriptors like `(1R,2S)-` when the molecule contains CIP-assigned
//! stereocentres.
use chematic_core::{CipCode, Molecule};
use chematic_iupac::IupacError;
/// Generate a local IUPAC name with CIP stereochemistry prefix.
///
/// Calls [`chematic_iupac::name`] for the base name, then
/// [`crate::assign_cip`] to collect R/S assignments. If any
/// stereocentres are found, their descriptors are prepended in atom-index
/// order: `(R)-butane-2-ol`, `(1R,2S)-cyclohexane-1,2-diol`, etc.
///
/// Returns `Err` for structures outside the current IUPAC naming scope
/// (same conditions as [`chematic_iupac::name`]).
pub fn iupac_name_stereo(mol: &Molecule) -> Result<String, IupacError> {
let base = chematic_iupac::name(mol)?;
let cip = crate::assign_cip(mol);
let mut rs_pairs: Vec<(u32, &'static str)> = cip
.assignments
.iter()
.filter_map(|(idx, code)| match code {
CipCode::R => Some((idx.0, "R")),
CipCode::S => Some((idx.0, "S")),
_ => None,
})
.collect();
if rs_pairs.is_empty() {
return Ok(base);
}
rs_pairs.sort_by_key(|(i, _)| *i);
let prefix = if rs_pairs.len() == 1 {
format!("({})", rs_pairs[0].1)
} else {
let parts: Vec<String> = rs_pairs
.iter()
.map(|(atom_idx, code)| format!("{}{}", atom_idx + 1, code))
.collect();
format!("({})", parts.join(","))
};
Ok(format!("{prefix}-{base}"))
}
#[cfg(test)]
mod tests {
use super::*;
use chematic_smiles::parse;
#[test]
fn achiral_butane_no_prefix() {
let mol = parse("CCCC").unwrap();
let name = iupac_name_stereo(&mol).unwrap();
assert!(
!name.starts_with('('),
"achiral molecule: no stereo prefix, got {name}"
);
}
#[test]
fn r_2_butanol_has_r_prefix() {
// (R)-butan-2-ol: SMILES with defined chirality
let mol = parse("[C@@H](O)(CC)C").unwrap();
let name = iupac_name_stereo(&mol);
// Name may not be supported if IUPAC naming scope is limited;
// if it succeeds it should contain R or S prefix.
if let Ok(n) = name {
assert!(
n.starts_with("(R)") || n.starts_with("(S)"),
"chiral 2-butanol should carry stereo prefix, got {n}"
);
}
}
#[test]
fn achiral_cyclohexane_no_prefix() {
let mol = parse("C1CCCCC1").unwrap();
let name = iupac_name_stereo(&mol).unwrap();
assert!(
!name.starts_with('('),
"achiral cyclohexane: no prefix, got {name}"
);
}
}