1use chematic_core::Molecule;
8
9use crate::error::MolParseError;
10use crate::mol2000::{MolMetadata, parse_mol};
11
12pub struct SdfReader<'a> {
18 remaining: &'a str,
19 current_mol_num: usize,
20}
21
22impl<'a> SdfReader<'a> {
23 pub fn new(input: &'a str) -> Self {
25 Self {
26 remaining: input,
27 current_mol_num: 0,
28 }
29 }
30}
31
32impl<'a> Iterator for SdfReader<'a> {
33 type Item = Result<(Molecule, MolMetadata), MolParseError>;
34
35 fn next(&mut self) -> Option<Self::Item> {
36 while let Some(rest) = self
39 .remaining
40 .strip_prefix("\r\n")
41 .or_else(|| self.remaining.strip_prefix('\n'))
42 {
43 self.remaining = rest;
44 }
45
46 if self.remaining.is_empty() {
47 return None;
48 }
49
50 self.current_mol_num += 1;
51
52 let mut byte_offset = 0usize;
58 let (end_byte, after_delim) = loop {
59 let rest = &self.remaining[byte_offset..];
60 match rest.find('\n') {
61 Some(nl) => {
62 let line = rest[..nl].trim_end_matches('\r');
63 if line == "$$$$" {
64 break (byte_offset, &self.remaining[byte_offset + nl + 1..]);
65 }
66 byte_offset += nl + 1;
67 }
68 None => {
69 if rest.trim_end_matches('\r') == "$$$$" {
71 break (byte_offset, "");
72 }
73 break (self.remaining.len(), "");
74 }
75 }
76 };
77
78 let mol_block = &self.remaining[..end_byte];
79 self.remaining = after_delim;
80
81 if mol_block.trim().is_empty() {
82 return self.next();
84 }
85
86 Some(parse_mol(mol_block))
87 }
88}
89
90pub fn parse_sdf(input: &str) -> Result<Vec<(Molecule, MolMetadata)>, MolParseError> {
94 SdfReader::new(input).collect()
95}
96
97pub struct SdfRecord {
103 pub mol: Molecule,
105 pub name: String,
107 pub properties: Vec<(String, String)>,
110}
111
112pub struct SdfRecordReader<'a> {
117 remaining: &'a str,
118}
119
120impl<'a> SdfRecordReader<'a> {
121 pub fn new(input: &'a str) -> Self {
123 Self { remaining: input }
124 }
125}
126
127impl<'a> Iterator for SdfRecordReader<'a> {
128 type Item = Result<SdfRecord, MolParseError>;
129
130 fn next(&mut self) -> Option<Self::Item> {
131 while let Some(rest) = self
133 .remaining
134 .strip_prefix("\r\n")
135 .or_else(|| self.remaining.strip_prefix('\n'))
136 {
137 self.remaining = rest;
138 }
139
140 if self.remaining.is_empty() {
141 return None;
142 }
143
144 let mut byte_offset = 0usize;
146 let (end_byte, after_delim) = loop {
147 let rest = &self.remaining[byte_offset..];
148 match rest.find('\n') {
149 Some(nl) => {
150 let line = rest[..nl].trim_end_matches('\r');
151 if line == "$$$$" {
152 break (byte_offset, &self.remaining[byte_offset + nl + 1..]);
153 }
154 byte_offset += nl + 1;
155 }
156 None => {
157 if rest.trim_end_matches('\r') == "$$$$" {
158 break (byte_offset, "");
159 }
160 break (self.remaining.len(), "");
161 }
162 }
163 };
164
165 let block = &self.remaining[..end_byte];
166 self.remaining = after_delim;
167
168 if block.trim().is_empty() {
169 return self.next();
170 }
171
172 let (mol, meta) = match parse_mol(block) {
176 Ok(pair) => pair,
177 Err(e) => return Some(Err(e)),
178 };
179
180 let data_part = block
182 .find("M END")
183 .map(|pos| &block[pos + 6..]) .unwrap_or("");
185 let properties = parse_sd_fields(data_part);
186
187 Some(Ok(SdfRecord { mol, name: meta.name, properties }))
188 }
189}
190
191fn parse_sd_fields(data: &str) -> Vec<(String, String)> {
196 let mut fields = Vec::new();
197 let mut current_key: Option<String> = None;
198 let mut current_value_lines: Vec<&str> = Vec::new();
199
200 for raw_line in data.lines() {
201 let line = raw_line.trim_end_matches('\r');
202
203 if let Some(key) = parse_sd_field_header(line) {
204 if let Some(k) = current_key.take() {
206 fields.push((k, current_value_lines.join("\n")));
207 current_value_lines.clear();
208 }
209 current_key = Some(key);
210 } else if line.is_empty() {
211 if let Some(k) = current_key.take() {
213 fields.push((k, current_value_lines.join("\n")));
214 current_value_lines.clear();
215 }
216 } else if current_key.is_some() {
217 current_value_lines.push(line);
218 }
219 }
220 if let Some(k) = current_key {
222 fields.push((k, current_value_lines.join("\n")));
223 }
224
225 fields
226}
227
228fn parse_sd_field_header(line: &str) -> Option<String> {
230 let rest = line.strip_prefix('>')?;
233 let rest = rest.trim();
234 let inner = rest.strip_prefix('<')?.strip_suffix('>')?;
235 Some(inner.to_string())
236}
237
238#[cfg(test)]
243mod tests {
244 use super::*;
245
246 const MOL_A: &str = "\
247mol_a
248 chematic
249
250 2 1 0 0 0 0 0 0 0 0 0 V2000
251 0.0000 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
252 1.0000 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
253 1 2 1 0
254M END
255";
256
257 const MOL_B: &str = "\
258mol_b
259 chematic
260
261 3 2 0 0 0 0 0 0 0 0 0 V2000
262 0.0000 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
263 1.0000 0.0000 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0
264 2.0000 0.0000 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0
265 1 2 1 0
266 2 3 2 0
267M END
268";
269
270 fn two_mol_sdf() -> String {
271 format!("{MOL_A}$$$$\n{MOL_B}$$$$\n")
272 }
273
274 #[test]
275 fn test_sdf_reader_two_molecules() {
276 let sdf = two_mol_sdf();
277 let results: Vec<_> = SdfReader::new(&sdf).collect();
278 assert_eq!(results.len(), 2);
279 let (mol_a, meta_a) = results[0].as_ref().expect("mol_a parse");
280 let (mol_b, meta_b) = results[1].as_ref().expect("mol_b parse");
281 assert_eq!(mol_a.atom_count(), 2);
282 assert_eq!(mol_a.bond_count(), 1);
283 assert_eq!(meta_a.name, "mol_a");
284 assert_eq!(mol_b.atom_count(), 3);
285 assert_eq!(mol_b.bond_count(), 2);
286 assert_eq!(meta_b.name, "mol_b");
287 }
288
289 #[test]
290 fn test_parse_sdf_all() {
291 let sdf = two_mol_sdf();
292 let mols = parse_sdf(&sdf).expect("parse_sdf");
293 assert_eq!(mols.len(), 2);
294 }
295
296 #[test]
297 fn test_sdf_reader_single_molecule_no_delimiter() {
298 let results: Vec<_> = SdfReader::new(MOL_A).collect();
300 assert_eq!(results.len(), 1);
301 let (mol, _) = results[0].as_ref().expect("parse");
302 assert_eq!(mol.atom_count(), 2);
303 }
304
305 #[test]
306 fn test_sdf_reader_stops_on_error() {
307 let bad_sdf = format!("{MOL_A}$$$$\nbad\n prog\n\n X Y\nM END\n$$$$\n");
309 let result = parse_sdf(&bad_sdf);
310 assert!(result.is_err());
311 }
312
313 #[test]
314 fn test_sdf_reader_empty_input() {
315 let results: Vec<_> = SdfReader::new("").collect();
316 assert_eq!(results.len(), 0);
317 }
318
319 #[test]
320 fn test_sdf_reader_names_preserved() {
321 let sdf = two_mol_sdf();
322 let mols = parse_sdf(&sdf).expect("parse");
323 assert_eq!(mols[0].1.name, "mol_a");
324 assert_eq!(mols[1].1.name, "mol_b");
325 }
326
327 #[test]
328 fn test_sdf_with_data_fields() {
329 let sdf_with_data = format!(
331 "{MOL_A}> <MW>\n44.0\n\n$$$$\n"
332 );
333 let results: Vec<_> = SdfReader::new(&sdf_with_data).collect();
334 assert_eq!(results.len(), 1);
335 let (mol, _) = results[0].as_ref().expect("parse");
336 assert_eq!(mol.atom_count(), 2);
337 }
338}