1use crate::scene::MoleculeIn;
6
7pub fn cx_atom_labels(smiles_or_cx: &str) -> Vec<Option<String>> {
12 if smiles_or_cx.is_empty() {
13 return Vec::new();
14 }
15 let Some(start) = smiles_or_cx.find("|$") else {
16 return Vec::new();
17 };
18 let rest = &smiles_or_cx[start + 2..];
19 let Some(end) = rest.find("$|") else {
20 return Vec::new();
21 };
22 let raw = &rest[..end];
23 let mut parts: Vec<&str> = raw.split(';').collect();
24 if parts.last().is_some_and(|p| p.is_empty()) {
25 parts.pop();
26 }
27 let mut out = Vec::with_capacity(parts.len());
28 for p in parts {
29 let p = p.trim();
30 if p.is_empty() {
31 out.push(None);
32 continue;
33 }
34 let label = if let Some(stripped) = p.strip_prefix('_') {
35 if stripped.is_empty() {
36 p.to_string()
37 } else {
38 stripped.to_string()
39 }
40 } else {
41 p.to_string()
42 };
43 out.push(Some(label));
44 }
45 out
46}
47
48pub fn smiles_base(smiles_or_cx: &str) -> String {
50 if smiles_or_cx.is_empty() {
51 return String::new();
52 }
53 smiles_or_cx
54 .split_once(" |")
55 .map(|(a, _)| a)
56 .unwrap_or(smiles_or_cx)
57 .trim()
58 .to_string()
59}
60
61pub fn apply_cx_by_index(mut molecule: MoleculeIn, source: &str) -> MoleculeIn {
63 let aliases = cx_atom_labels(source);
64 if aliases.is_empty() {
65 return molecule;
66 }
67 for a in &mut molecule.atoms {
68 let idx = a.index as usize;
69 if let Some(Some(label)) = aliases.get(idx) {
70 let label = label.trim();
71 if !label.is_empty() {
72 a.label = Some(label.to_string());
73 }
74 }
75 }
76 molecule
77}
78
79pub fn cx_source<'a>(cxsmiles: Option<&'a str>, smiles: Option<&'a str>) -> Option<&'a str> {
81 for s in [cxsmiles, smiles] {
82 if let Some(t) = s.filter(|x| !x.trim().is_empty()) {
83 return Some(t);
84 }
85 }
86 None
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use crate::scene::{AtomIn, BondIn};
93
94 #[test]
95 fn parses_cx_aliases() {
96 let labels = cx_atom_labels("*C* |$;;R2;$|");
97 assert_eq!(labels.len(), 3);
98 assert!(labels[0].is_none());
99 assert!(labels[1].is_none());
100 assert_eq!(labels[2].as_deref(), Some("R2"));
101 }
102
103 #[test]
104 fn smiles_base_strips_trailer() {
105 assert_eq!(smiles_base("*C* |$;;R2;$|"), "*C*");
106 }
107
108 #[test]
109 fn apply_cx_sets_nonempty_slots() {
110 let mol = MoleculeIn {
111 id: None,
112 atoms: vec![
113 AtomIn {
114 index: 0,
115 element: Some("*".into()),
116 z: Some(0),
117 x: 0.0,
118 y: 0.0,
119 label: Some("*".into()),
120 charge: 0,
121 },
122 AtomIn {
123 index: 1,
124 element: Some("C".into()),
125 z: Some(6),
126 x: 1.0,
127 y: 0.0,
128 label: None,
129 charge: 0,
130 },
131 AtomIn {
132 index: 2,
133 element: Some("*".into()),
134 z: Some(0),
135 x: 2.0,
136 y: 0.0,
137 label: Some("*".into()),
138 charge: 0,
139 },
140 ],
141 bonds: vec![
142 BondIn {
143 index: 0,
144 begin: 0,
145 end: 1,
146 order: 1.0,
147 stereo: None,
148 interior: None,
149 },
150 BondIn {
151 index: 1,
152 begin: 1,
153 end: 2,
154 order: 1.0,
155 stereo: None,
156 interior: None,
157 },
158 ],
159 color: None,
160 atom_shade: None,
161 bond_shade: None,
162 shade_vmin: None,
163 shade_vmax: None,
164 mark_atoms: vec![],
165 mark_bonds: vec![],
166 scale: 1.0,
167 weight: 1.0,
168 };
169 let out = apply_cx_by_index(mol, "*C* |$;;R2;$|");
170 assert_eq!(out.atoms[0].label.as_deref(), Some("*"));
171 assert!(out.atoms[1].label.is_none());
172 assert_eq!(out.atoms[2].label.as_deref(), Some("R2"));
173 }
174
175 #[test]
176 fn empty_and_underscore_edge_cases() {
177 assert!(cx_atom_labels("").is_empty());
178 assert!(cx_atom_labels("CCO").is_empty());
179 assert!(cx_atom_labels("|$no close").is_empty());
180 let labels = cx_atom_labels("*C |$_;$|");
181 assert_eq!(labels[0].as_deref(), Some("_")); let labels2 = cx_atom_labels("*C |$_R;$|");
183 assert_eq!(labels2[0].as_deref(), Some("R"));
184 assert_eq!(smiles_base(""), "");
185 assert_eq!(smiles_base("CCO"), "CCO");
186 assert!(cx_source(None, None).is_none());
187 assert_eq!(cx_source(None, Some(" ")), None);
188 assert_eq!(cx_source(Some("CX"), Some("S")), Some("CX"));
189 }
190}