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}