code wiki / (root) / nx_chem_report_csv.nx

nx_chem_report_csv.nx source

↩ module page · 166 lines · 7094 B

1// nx_chem_report_csv.nx -- C6.1: CSV output format for lab analysts 2// who work in Excel. 3// 4// Output: one row per match. Columns: 5// sample_id, peak_mz, rt_seconds, intensity, compound_id, name, 6// regulatory, delta_ppm, citation 7// 8// CSV escaping (RFC 4180-style): 9// - Field containing comma, quote, or newline -> wrap in double quotes 10// - Internal " -> "" (doubled) 11// - Other fields: bare 12// 13// Compose nx_chem_report_json's emit_* helpers (emit_str/emit_int/ 14// emit_q4) for type-format consistency with the JSON emitter. 15 16import "nx_chem.nx" 17import "nx_chem_molecule.nx" 18import "nx_chem_adulterant_db.nx" 19import "nx_chem_peak_list.nx" 20import "nx_chem_report_json.nx" 21const K_MAGIC_10000: i64 = 10000 22 23// ================================================================= 24// Emit a CSV field that needs escaping (RFC 4180): if the field 25// contains comma, quote, or newline, wrap in "..." with internal 26// " doubled. Otherwise emit bare. Returns bytes written. 27// ================================================================= 28func nx_chem_emit_csv_field(buf: *u8, offset: nx_int, src: *u8) -> nx_int { 29 // Scan src for special chars 30 var needs_quote: nx_int = 0 31 var i: nx_int = 0 32 var done_s: nx_int = 0 33 while done_s == 0 { 34 let c: nx_int = src[i] as nx_int 35 if c == 0 { done_s = 1 } 36 else { 37 if c == 44 { needs_quote = 1 } // ',' 38 if c == 34 { needs_quote = 1 } // '"' 39 if c == 10 { needs_quote = 1 } // '\n' 40 if c == 13 { needs_quote = 1 } // '\r' 41 i = i + 1 42 } 43 } 44 var w: nx_int = 0 45 if needs_quote == 1 { 46 buf[offset + w] = 0x22; w = w + 1 // opening " 47 var j: nx_int = 0 48 var done_c: nx_int = 0 49 while done_c == 0 { 50 let c2: nx_int = src[j] as nx_int 51 if c2 == 0 { done_c = 1 } 52 else { 53 if c2 == 34 { 54 buf[offset + w] = 0x22; w = w + 1 55 buf[offset + w] = 0x22; w = w + 1 56 } 57 else { 58 buf[offset + w] = src[j]; w = w + 1 59 } 60 j = j + 1 61 } 62 } 63 buf[offset + w] = 0x22; w = w + 1 // closing " 64 } 65 if needs_quote == 0 { 66 var k: nx_int = 0 67 var done_b: nx_int = 0 68 while done_b == 0 { 69 let c3: nx_int = src[k] as nx_int 70 if c3 == 0 { done_b = 1 } 71 else { 72 buf[offset + w] = src[k]; w = w + 1 73 k = k + 1 74 } 75 } 76 } 77 return w 78} 79 80// ================================================================= 81// Emit the CSV column header row + newline. 82// C7.3: includes confidence_pct as 10th column. 83// ================================================================= 84func nx_chem_emit_csv_header(buf: *u8, offset: nx_int) -> nx_int { 85 let hdr: *u8 = "sample_id,peak_mz,rt_seconds,intensity,compound_id,name,regulatory,delta_ppm,confidence_pct,citation\n" as *u8 86 return nx_chem_emit_str(buf, offset, hdr) 87} 88 89// ================================================================= 90// Emit one CSV row for a single match. Returns bytes written. 91// ================================================================= 92func nx_chem_emit_csv_row(buf: *u8, offset: nx_int, sample_id: *u8, p: *PeakObservation, db: *AdulterantDB, r: *MatchResult) -> nx_int { 93 // C4.3 fix: r.entry_id is the compound's user-supplied id (not array 94 // index). For seed compounds id == index, but loaded compounds have 95 // arbitrary ids (e.g., 200). Use entry_by_id helper for both. 96 let ee: *AdulterantEntry = nx_chem_adulterant_entry_by_id(db, r.entry_id) 97 var w: nx_int = 0 98 w = w + nx_chem_emit_csv_field(buf, offset + w, sample_id) 99 buf[offset + w] = 0x2C; w = w + 1 100 w = w + nx_chem_emit_q4(buf, offset + w, p.mz_q4) 101 buf[offset + w] = 0x2C; w = w + 1 102 w = w + nx_chem_emit_int(buf, offset + w, p.rt_q3 / 1000) 103 buf[offset + w] = 0x2C; w = w + 1 104 w = w + nx_chem_emit_int(buf, offset + w, p.intensity_q4 / K_MAGIC_10000) 105 buf[offset + w] = 0x2C; w = w + 1 106 w = w + nx_chem_emit_int(buf, offset + w, r.entry_id) 107 buf[offset + w] = 0x2C; w = w + 1 108 w = w + nx_chem_emit_csv_field(buf, offset + w, ee.name_ptr) 109 buf[offset + w] = 0x2C; w = w + 1 110 w = w + nx_chem_emit_csv_field(buf, offset + w, nx_chem_regulatory_str(ee.regulatory)) 111 buf[offset + w] = 0x2C; w = w + 1 112 w = w + nx_chem_emit_int(buf, offset + w, r.delta_ppm_q1 / 10) 113 buf[offset + w] = 0x2C; w = w + 1 114 w = w + nx_chem_emit_int(buf, offset + w, r.confidence_pct) 115 buf[offset + w] = 0x2C; w = w + 1 116 w = w + nx_chem_emit_csv_field(buf, offset + w, ee.citation_ptr) 117 buf[offset + w] = 0x0A; w = w + 1 118 return w 119} 120 121// ================================================================= 122// Emit complete CSV report for one PeakList: header + one row per 123// match. If peaks_n == 0 or no matches, only the header is emitted. 124// Returns bytes written. 125// ================================================================= 126func nx_chem_emit_report_csv(buf: *u8, peaks: *PeakList, db: *AdulterantDB, sample_id: *u8, tolerance_ppm: nx_int) -> nx_int { 127 var pos: nx_int = 0 128 pos = pos + nx_chem_emit_csv_header(buf, pos) 129 var pi: nx_int = 0 130 while pi < peaks.n { 131 let p: *PeakObservation = ((peaks.peaks as nx_int) + (pi * NX_PEAK_OBS_BYTES)) as *PeakObservation 132 let outs: *MatchResult = (sys_mmap((4 * NX_MATCH_RESULT_BYTES) as i64)) as *MatchResult 133 let nm: nx_int = nx_chem_adulterant_db_lookup_with_rt(db, p.mz_q4, p.rt_q3, tolerance_ppm, outs, 4) 134 var mi: nx_int = 0 135 while mi < nm { 136 let rr: *MatchResult = ((outs as nx_int) + (mi * NX_MATCH_RESULT_BYTES)) as *MatchResult 137 pos = pos + nx_chem_emit_csv_row(buf, pos, sample_id, p, db, rr) 138 mi = mi + 1 139 } 140 pi = pi + 1 141 } 142 buf[pos] = 0 143 return pos 144} 145 146// ================================================================= 147// Append CSV rows (no header) for one PeakList -- used in batch mode 148// where header is emitted once. Returns bytes written. 149// ================================================================= 150func nx_chem_emit_report_csv_rows_only(buf: *u8, peaks: *PeakList, db: *AdulterantDB, sample_id: *u8, tolerance_ppm: nx_int) -> nx_int { 151 var pos: nx_int = 0 152 var pi: nx_int = 0 153 while pi < peaks.n { 154 let p: *PeakObservation = ((peaks.peaks as nx_int) + (pi * NX_PEAK_OBS_BYTES)) as *PeakObservation 155 let outs: *MatchResult = (sys_mmap((4 * NX_MATCH_RESULT_BYTES) as i64)) as *MatchResult 156 let nm: nx_int = nx_chem_adulterant_db_lookup_with_rt(db, p.mz_q4, p.rt_q3, tolerance_ppm, outs, 4) 157 var mi: nx_int = 0 158 while mi < nm { 159 let rr: *MatchResult = ((outs as nx_int) + (mi * NX_MATCH_RESULT_BYTES)) as *MatchResult 160 pos = pos + nx_chem_emit_csv_row(buf, pos, sample_id, p, db, rr) 161 mi = mi + 1 162 } 163 pi = pi + 1 164 } 165 return pos 166}