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
248pub struct SdfFileReader<R: std::io::BufRead> {
259 reader: R,
260 done: bool,
261}
262
263impl<R: std::io::BufRead> SdfFileReader<R> {
264 pub fn new(reader: R) -> Self {
266 Self {
267 reader,
268 done: false,
269 }
270 }
271}
272
273impl<R: std::io::BufRead> Iterator for SdfFileReader<R> {
274 type Item = Result<SdfRecord, MolParseError>;
275
276 fn next(&mut self) -> Option<Self::Item> {
277 if self.done {
278 return None;
279 }
280
281 let mut block = String::with_capacity(2048);
282 let mut line = String::new();
283
284 loop {
285 line.clear();
286 match self.reader.read_line(&mut line) {
287 Err(e) => {
288 self.done = true;
289 return Some(Err(MolParseError::Io(e.to_string())));
290 }
291 Ok(0) => {
292 self.done = true;
294 if block.trim().is_empty() {
295 return None;
296 }
297 break;
299 }
300 Ok(_) => {
301 let trimmed = line.trim_end_matches(['\r', '\n']);
302 if trimmed == "$$$$" {
303 break;
304 }
305 block.push_str(&line);
306 }
307 }
308 }
309
310 if block.trim().is_empty() {
311 return self.next();
313 }
314
315 let (mol, meta, coords) = match parse_mol_with_coords(&block) {
317 Ok(triple) => triple,
318 Err(e) => return Some(Err(e)),
319 };
320
321 let data_part = block
322 .find("M END")
323 .map(|pos| &block[pos + 6..])
324 .unwrap_or("");
325 let properties: std::collections::HashMap<String, String> =
326 parse_sd_fields(data_part).into_iter().collect();
327
328 Some(Ok(SdfRecord {
329 mol,
330 meta,
331 coords,
332 properties,
333 }))
334 }
335}
336
337#[cfg(test)]
342mod tests {
343 use super::*;
344
345 const MOL_A: &str = "\
346mol_a
347 chematic
348
349 2 1 0 0 0 0 0 0 0 0 0 V2000
350 0.0000 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
351 1.0000 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
352 1 2 1 0
353M END
354";
355
356 const MOL_B: &str = "\
357mol_b
358 chematic
359
360 3 2 0 0 0 0 0 0 0 0 0 V2000
361 0.0000 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
362 1.0000 0.0000 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0
363 2.0000 0.0000 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0
364 1 2 1 0
365 2 3 2 0
366M END
367";
368
369 fn two_mol_sdf() -> String {
370 format!("{MOL_A}$$$$\n{MOL_B}$$$$\n")
371 }
372
373 #[test]
374 fn test_sdf_reader_two_molecules() {
375 let sdf = two_mol_sdf();
376 let results: Vec<_> = SdfReader::new(&sdf).collect();
377 assert_eq!(results.len(), 2);
378 let (mol_a, meta_a) = results[0].as_ref().expect("mol_a parse");
379 let (mol_b, meta_b) = results[1].as_ref().expect("mol_b parse");
380 assert_eq!(mol_a.atom_count(), 2);
381 assert_eq!(mol_a.bond_count(), 1);
382 assert_eq!(meta_a.name, "mol_a");
383 assert_eq!(mol_b.atom_count(), 3);
384 assert_eq!(mol_b.bond_count(), 2);
385 assert_eq!(meta_b.name, "mol_b");
386 }
387
388 #[test]
389 fn test_parse_sdf_all() {
390 let sdf = two_mol_sdf();
391 let mols = parse_sdf(&sdf).expect("parse_sdf");
392 assert_eq!(mols.len(), 2);
393 }
394
395 #[test]
396 fn test_sdf_reader_single_molecule_no_delimiter() {
397 let results: Vec<_> = SdfReader::new(MOL_A).collect();
399 assert_eq!(results.len(), 1);
400 let (mol, _) = results[0].as_ref().expect("parse");
401 assert_eq!(mol.atom_count(), 2);
402 }
403
404 #[test]
405 fn test_sdf_reader_stops_on_error() {
406 let bad_sdf = format!("{MOL_A}$$$$\nbad\n prog\n\n X Y\nM END\n$$$$\n");
408 let result = parse_sdf(&bad_sdf);
409 assert!(result.is_err());
410 }
411
412 #[test]
413 fn test_sdf_reader_empty_input() {
414 let results: Vec<_> = SdfReader::new("").collect();
415 assert_eq!(results.len(), 0);
416 }
417
418 #[test]
419 fn test_sdf_reader_names_preserved() {
420 let sdf = two_mol_sdf();
421 let mols = parse_sdf(&sdf).expect("parse");
422 assert_eq!(mols[0].1.name, "mol_a");
423 assert_eq!(mols[1].1.name, "mol_b");
424 }
425
426 #[test]
427 fn test_sdf_with_data_fields() {
428 let sdf_with_data = format!("{MOL_A}> <MW>\n44.0\n\n$$$$\n");
430 let results: Vec<_> = SdfReader::new(&sdf_with_data).collect();
431 assert_eq!(results.len(), 1);
432 let (mol, _) = results[0].as_ref().expect("parse");
433 assert_eq!(mol.atom_count(), 2);
434 }
435
436 #[test]
437 fn test_sdf_reader_reports_truncated_large_count_record() {
438 let bad_sdf = "\
439max_atoms
440 chematic
441
442999 0 0 0 0 0 0 0 0 0 0 V2000
443$$$$
444";
445 let mut reader = SdfReader::new(bad_sdf);
446 assert!(matches!(
447 reader.next(),
448 Some(Err(MolParseError::UnexpectedEnd))
449 ));
450 assert!(reader.next().is_none());
451 assert!(matches!(
452 parse_sdf(bad_sdf),
453 Err(MolParseError::UnexpectedEnd)
454 ));
455 }
456
457 #[test]
458 fn test_sdf_file_reader_streaming() {
459 use std::io::{BufReader, Cursor};
461 let sdf = two_mol_sdf();
462 let cursor = Cursor::new(sdf.into_bytes());
463 let records: Vec<_> = SdfFileReader::new(BufReader::new(cursor))
464 .filter_map(|r| r.ok())
465 .collect();
466 assert_eq!(records.len(), 2);
467 assert_eq!(records[0].mol.atom_count(), 2); assert_eq!(records[1].mol.atom_count(), 3); }
470
471 #[test]
472 fn test_sdf_file_reader_skips_empty_block() {
473 use std::io::{BufReader, Cursor};
474 let sdf = format!("$$$$\n{MOL_A}$$$$\n");
475 let cursor = Cursor::new(sdf.into_bytes());
476 let records: Vec<_> = SdfFileReader::new(BufReader::new(cursor))
477 .filter_map(|r| r.ok())
478 .collect();
479 assert_eq!(records.len(), 1);
480 }
481
482 #[test]
483 fn test_sdf_file_reader_malformed_record_yields_err() {
484 use std::io::{BufReader, Cursor};
485
486 let malformed = "broken\n prog\n\n NOTNUM 0 0 V2000\nM END\n";
488 let sdf = format!("{MOL_A}$$$$\n{malformed}$$$$\n{MOL_B}$$$$\n");
489 let cursor = Cursor::new(sdf.into_bytes());
490 let results: Vec<_> = SdfFileReader::new(BufReader::new(cursor)).collect();
491
492 assert_eq!(results.len(), 3);
494 assert!(results[0].is_ok(), "first record should be ok");
495 assert!(
496 results[1].is_err(),
497 "second record should be err (malformed)"
498 );
499 assert!(results[2].is_ok(), "third record should be ok");
500 }
501}