nx_doc_extract.nx source
↩ module page · 155 lines · 6296 B
1// nx_doc_extract.nx -- R1 of THE NISHI DOCUMENT-INTELLIGENCE arc: READ a document's text into STRUCTURED
2// DATA. The "consuming documents -> structured data" leg (the other leg, making documents, is nx_fin_dispute
3// / the emitters). Sovereign + deterministic: a tolerant line/field parser (no OCR/ML dependency) that turns
4// an itemized medical-bill statement into structured line-items (date, CPT code, units, billed cents) that
5// flow straight into nx_fin_audit -- so a scanned/typed bill becomes auditable structured data with ZERO
6// manual entry. This is the format-specific deterministic rung; the research corpus (knowledge/fetched/
7// doc_*.raw: layout-analysis, IE, NER, classification) grounds the roadmap to arbitrary-layout + classified
8// extraction. A line is a line-item iff it begins with a MM/DD/YYYY date AND carries a 5-digit code AND a
9// $amount; headers/totals/blank lines are skipped by construction. No floats (exact cents), no hardware
10// writes. license_tier: ORIGINAL
11import "nx_syscalls.nx"
12
13// digit value 0..9, or -1 if not an ASCII digit.
14func dx_dig(c: i64) -> i64 {
15 if c < 0x30 { return 0 - 1 }
16 if c > 0x39 { return 0 - 1 }
17 return c - 0x30
18}
19
20// parse "MM/DD/YYYY" (length 10) at line[s..] -> YYYYMMDD, or -1.
21func dx_tok_date(line: *u8, s: i64, l: i64) -> i64 {
22 if l != 10 { return 0 - 1 }
23 if line[s + 2] != (0x2f as u8) { return 0 - 1 }
24 if line[s + 5] != (0x2f as u8) { return 0 - 1 }
25 let m1: i64 = dx_dig(line[s]); let m2: i64 = dx_dig(line[s + 1])
26 let d1: i64 = dx_dig(line[s + 3]); let d2: i64 = dx_dig(line[s + 4])
27 let y1: i64 = dx_dig(line[s + 6]); let y2: i64 = dx_dig(line[s + 7])
28 let y3: i64 = dx_dig(line[s + 8]); let y4: i64 = dx_dig(line[s + 9])
29 if m1 < 0 { return 0 - 1 }
30 if m2 < 0 { return 0 - 1 }
31 if d1 < 0 { return 0 - 1 }
32 if d2 < 0 { return 0 - 1 }
33 if y1 < 0 { return 0 - 1 }
34 if y2 < 0 { return 0 - 1 }
35 if y3 < 0 { return 0 - 1 }
36 if y4 < 0 { return 0 - 1 }
37 let mm: i64 = m1 * 10 + m2
38 let dd: i64 = d1 * 10 + d2
39 let yyyy: i64 = y1 * 1000 + y2 * 100 + y3 * 10 + y4
40 return yyyy * 10000 + mm * 100 + dd
41}
42
43// parse a pure integer token at line[s..s+l); -1 if any non-digit.
44func dx_tok_int(line: *u8, s: i64, l: i64) -> i64 {
45 if l <= 0 { return 0 - 1 }
46 var v: i64 = 0
47 var i: i64 = 0
48 while i < l {
49 let d: i64 = dx_dig(line[s + i])
50 if d < 0 { return 0 - 1 }
51 v = v * 10 + d
52 i = i + 1
53 }
54 return v
55}
56
57// 1 if the token is exactly 5 digits (a CPT-shaped code).
58func dx_is5(line: *u8, s: i64, l: i64) -> i64 {
59 if l != 5 { return 0 }
60 var i: i64 = 0
61 while i < 5 { if dx_dig(line[s + i]) < 0 { return 0 } i = i + 1 }
62 return 1
63}
64
65// parse a "$D,DDD.CC" money token -> cents, or -1 (requires leading '$' and at least one digit).
66func dx_tok_money(line: *u8, s: i64, l: i64) -> i64 {
67 if l < 2 { return 0 - 1 }
68 if line[s] != (0x24 as u8) { return 0 - 1 }
69 var dollars: i64 = 0
70 var cc: i64 = 0
71 var sawdig: i64 = 0
72 var dot: i64 = 0
73 var ccd: i64 = 0
74 var i: i64 = 1
75 while i < l {
76 let c: i64 = line[s + i]
77 if c == 0x2e { dot = 1 }
78 else {
79 if c == 0x2c { dot = dot }
80 else {
81 let d: i64 = dx_dig(c)
82 if d < 0 { return 0 - 1 }
83 if dot == 0 { dollars = dollars * 10 + d; sawdig = 1 }
84 else { if ccd < 2 { cc = cc * 10 + d; ccd = ccd + 1 } }
85 }
86 }
87 i = i + 1
88 }
89 if sawdig == 0 { return 0 - 1 }
90 if ccd == 1 { cc = cc * 10 }
91 return dollars * 100 + cc
92}
93
94// tokenize a line into (start,len) pairs on whitespace runs; returns token count (capped).
95func dx_tokenize(line: *u8, lstart: i64, llen: i64, tstart: *i64, tlen: *i64, cap: i64) -> i64 {
96 var nt: i64 = 0
97 var i: i64 = 0
98 while i < llen {
99 let c: i64 = line[lstart + i]
100 if c == 0x20 { i = i + 1 }
101 else { if c == 0x09 { i = i + 1 }
102 else {
103 let ts: i64 = i
104 var go: i64 = 1
105 while go == 1 {
106 if i >= llen { go = 0 }
107 else { let c2: i64 = line[lstart + i]; if c2 == 0x20 { go = 0 } else { if c2 == 0x09 { go = 0 } else { i = i + 1 } } }
108 }
109 if nt < cap { tstart[nt] = lstart + ts; tlen[nt] = i - ts; nt = nt + 1 }
110 } }
111 }
112 return nt
113}
114
115// EXTRACT structured line-items from bill/statement text. Writes parallel arrays out_codes/out_dates/
116// out_units/out_billed; returns the number of line-items found.
117func dx_extract_bill(text: *u8, n: i64, out_codes: *i64, out_dates: *i64, out_units: *i64, out_billed: *i64, cap: i64) -> i64 {
118 let tstart: *i64 = sys_mmap(8 * 128) as *i64
119 let tlen: *i64 = sys_mmap(8 * 128) as *i64
120 var cnt: i64 = 0
121 var i: i64 = 0
122 while i < n {
123 // line = [i, j)
124 var j: i64 = i
125 var adv: i64 = 1
126 while adv == 1 { if j >= n { adv = 0 } else { if text[j] == (10 as u8) { adv = 0 } else { j = j + 1 } } }
127 let llen: i64 = j - i
128 let nt: i64 = dx_tokenize(text, i, llen, tstart, tlen, 128)
129 if nt >= 3 {
130 let d: i64 = dx_tok_date(text, tstart[0], tlen[0])
131 if d >= 0 {
132 var cpt: i64 = 0 - 1
133 var cptpos: i64 = 0 - 1
134 var t: i64 = 1
135 while t < nt {
136 if cptpos < 0 { if dx_is5(text, tstart[t], tlen[t]) == 1 { cpt = dx_tok_int(text, tstart[t], tlen[t]); cptpos = t } }
137 t = t + 1
138 }
139 var units: i64 = 0 - 1
140 if cptpos >= 0 {
141 t = cptpos + 1
142 while t < nt { if units < 0 { let v: i64 = dx_tok_int(text, tstart[t], tlen[t]); if v >= 0 { units = v } } t = t + 1 }
143 }
144 var amt: i64 = 0 - 1
145 t = 0
146 while t < nt { let mm: i64 = dx_tok_money(text, tstart[t], tlen[t]); if mm >= 0 { amt = mm } t = t + 1 }
147 if cpt >= 0 { if units >= 0 { if amt >= 0 { if cnt < cap {
148 out_dates[cnt] = d; out_codes[cnt] = cpt; out_units[cnt] = units; out_billed[cnt] = amt; cnt = cnt + 1
149 } } } }
150 }
151 }
152 i = j + 1
153 }
154 return cnt
155}