1use chematic_core::Molecule;
8
9use crate::error::MolParseError;
10use crate::mol2000::{MolMetadata, parse_mol, parse_mol_with_coords};
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 {
106 pub mol: Molecule,
108 pub meta: MolMetadata,
110 pub coords: Vec<(f64, f64)>,
113 pub properties: std::collections::HashMap<String, String>,
116}
117
118pub struct SdfRecordReader<'a> {
123 remaining: &'a str,
124}
125
126impl<'a> SdfRecordReader<'a> {
127 pub fn new(input: &'a str) -> Self {
129 Self { remaining: input }
130 }
131}
132
133impl<'a> Iterator for SdfRecordReader<'a> {
134 type Item = Result<SdfRecord, MolParseError>;
135
136 fn next(&mut self) -> Option<Self::Item> {
137 while let Some(rest) = self
139 .remaining
140 .strip_prefix("\r\n")
141 .or_else(|| self.remaining.strip_prefix('\n'))
142 {
143 self.remaining = rest;
144 }
145
146 if self.remaining.is_empty() {
147 return None;
148 }
149
150 let mut byte_offset = 0usize;
152 let (end_byte, after_delim) = loop {
153 let rest = &self.remaining[byte_offset..];
154 match rest.find('\n') {
155 Some(nl) => {
156 let line = rest[..nl].trim_end_matches('\r');
157 if line == "$$$$" {
158 break (byte_offset, &self.remaining[byte_offset + nl + 1..]);
159 }
160 byte_offset += nl + 1;
161 }
162 None => {
163 if rest.trim_end_matches('\r') == "$$$$" {
164 break (byte_offset, "");
165 }
166 break (self.remaining.len(), "");
167 }
168 }
169 };
170
171 let block = &self.remaining[..end_byte];
172 self.remaining = after_delim;
173
174 if block.trim().is_empty() {
175 return self.next();
176 }
177
178 let (mol, meta, coords) = match parse_mol_with_coords(block) {
180 Ok(triple) => triple,
181 Err(e) => return Some(Err(e)),
182 };
183
184 let data_part = block
186 .find("M END")
187 .map(|pos| &block[pos + 6..])
188 .unwrap_or("");
189 let properties: std::collections::HashMap<String, String> =
190 parse_sd_fields(data_part).into_iter().collect();
191
192 Some(Ok(SdfRecord {
193 mol,
194 meta,
195 coords,
196 properties,
197 }))
198 }
199}
200
201fn parse_sd_fields(data: &str) -> Vec<(String, String)> {
206 let mut fields = Vec::new();
207 let mut current_key: Option<String> = None;
208 let mut current_value_lines: Vec<&str> = Vec::new();
209
210 for raw_line in data.lines() {
211 let line = raw_line.trim_end_matches('\r');
212
213 if let Some(key) = parse_sd_field_header(line) {
214 if let Some(k) = current_key.take() {
216 fields.push((k, current_value_lines.join("\n")));
217 current_value_lines.clear();
218 }
219 current_key = Some(key);
220 } else if line.is_empty() {
221 if let Some(k) = current_key.take() {
223 fields.push((k, current_value_lines.join("\n")));
224 current_value_lines.clear();
225 }
226 } else if current_key.is_some() {
227 current_value_lines.push(line);
228 }
229 }
230 if let Some(k) = current_key {
232 fields.push((k, current_value_lines.join("\n")));
233 }
234
235 fields
236}
237
238fn parse_sd_field_header(line: &str) -> Option<String> {
240 let rest = line.strip_prefix('>')?;
243 let rest = rest.trim();
244 let inner = rest.strip_prefix('<')?.strip_suffix('>')?;
245 Some(inner.to_string())
246}
247
248#[cfg(test)]
253mod tests {
254 use super::*;
255
256 const MOL_A: &str = "\
257mol_a
258 chematic
259
260 2 1 0 0 0 0 0 0 0 0 0 V2000
261 0.0000 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
262 1.0000 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
263 1 2 1 0
264M END
265";
266
267 const MOL_B: &str = "\
268mol_b
269 chematic
270
271 3 2 0 0 0 0 0 0 0 0 0 V2000
272 0.0000 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
273 1.0000 0.0000 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0
274 2.0000 0.0000 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0
275 1 2 1 0
276 2 3 2 0
277M END
278";
279
280 fn two_mol_sdf() -> String {
281 format!("{MOL_A}$$$$\n{MOL_B}$$$$\n")
282 }
283
284 #[test]
285 fn test_sdf_reader_two_molecules() {
286 let sdf = two_mol_sdf();
287 let results: Vec<_> = SdfReader::new(&sdf).collect();
288 assert_eq!(results.len(), 2);
289 let (mol_a, meta_a) = results[0].as_ref().expect("mol_a parse");
290 let (mol_b, meta_b) = results[1].as_ref().expect("mol_b parse");
291 assert_eq!(mol_a.atom_count(), 2);
292 assert_eq!(mol_a.bond_count(), 1);
293 assert_eq!(meta_a.name, "mol_a");
294 assert_eq!(mol_b.atom_count(), 3);
295 assert_eq!(mol_b.bond_count(), 2);
296 assert_eq!(meta_b.name, "mol_b");
297 }
298
299 #[test]
300 fn test_parse_sdf_all() {
301 let sdf = two_mol_sdf();
302 let mols = parse_sdf(&sdf).expect("parse_sdf");
303 assert_eq!(mols.len(), 2);
304 }
305
306 #[test]
307 fn test_sdf_reader_single_molecule_no_delimiter() {
308 let results: Vec<_> = SdfReader::new(MOL_A).collect();
310 assert_eq!(results.len(), 1);
311 let (mol, _) = results[0].as_ref().expect("parse");
312 assert_eq!(mol.atom_count(), 2);
313 }
314
315 #[test]
316 fn test_sdf_reader_stops_on_error() {
317 let bad_sdf = format!("{MOL_A}$$$$\nbad\n prog\n\n X Y\nM END\n$$$$\n");
319 let result = parse_sdf(&bad_sdf);
320 assert!(result.is_err());
321 }
322
323 #[test]
324 fn test_sdf_reader_empty_input() {
325 let results: Vec<_> = SdfReader::new("").collect();
326 assert_eq!(results.len(), 0);
327 }
328
329 #[test]
330 fn test_sdf_reader_names_preserved() {
331 let sdf = two_mol_sdf();
332 let mols = parse_sdf(&sdf).expect("parse");
333 assert_eq!(mols[0].1.name, "mol_a");
334 assert_eq!(mols[1].1.name, "mol_b");
335 }
336
337 #[test]
338 fn test_sdf_with_data_fields() {
339 let sdf_with_data = format!("{MOL_A}> <MW>\n44.0\n\n$$$$\n");
341 let results: Vec<_> = SdfReader::new(&sdf_with_data).collect();
342 assert_eq!(results.len(), 1);
343 let (mol, _) = results[0].as_ref().expect("parse");
344 assert_eq!(mol.atom_count(), 2);
345 }
346
347 #[test]
348 fn test_sdf_reader_reports_truncated_large_count_record() {
349 let bad_sdf = "\
350max_atoms
351 chematic
352
353999 0 0 0 0 0 0 0 0 0 0 V2000
354$$$$
355";
356 let mut reader = SdfReader::new(bad_sdf);
357 assert!(matches!(
358 reader.next(),
359 Some(Err(MolParseError::UnexpectedEnd))
360 ));
361 assert!(reader.next().is_none());
362 assert!(matches!(
363 parse_sdf(bad_sdf),
364 Err(MolParseError::UnexpectedEnd)
365 ));
366 }
367}