nx_money_ledger.nx source
↩ module page · 119 lines · 5646 B
1// nx_money_ledger.nx -- the SOVEREIGN data-in path (the needle-mover the reality-check identified). Parses a
2// user-owned CSV statement export (date,description,amount) into a real transaction ledger: sums income (inflows),
3// and auto-categorizes each outflow (needs/wants/savings) by merchant keyword + flags recurring drains -- emitting
4// exactly the array nx_spend_analyze consumes. NO Plaid, NO third-party data broker: the person's OWN export, on
5// their machine. Integer-exact (cents). Rules are keyword-based (should migrate to a config table). license_tier: ORIGINAL
6import "nx_syscalls.nx"
7import "nx_spend_analyze.nx" // SP_FIELDS + SP_NEED/WANT/SAVE (emit compatible rows) + the sp_ analyzers
8
9// parse a signed dollar string [start,end) like "-1500.00" / "3000" / "5.5" -> cents (i64). Non-numeric -> 0.
10func lg_parse_amount(s: *u8, start: i64, end: i64) -> i64 {
11 var i: i64 = start
12 var neg: i64 = 0
13 if i < end { if s[i] == (45 as u8) { neg = 1; i = i + 1 } }
14 var dollars: i64 = 0
15 var cents: i64 = 0
16 var seen_dot: i64 = 0
17 var cdigits: i64 = 0
18 while i < end {
19 let ch: i64 = s[i] as i64
20 if ch == 46 { seen_dot = 1 }
21 else {
22 if ch >= 48 { if ch <= 57 {
23 if seen_dot == 0 { dollars = dollars * 10 + (ch - 48) }
24 else { if cdigits < 2 { cents = cents * 10 + (ch - 48); cdigits = cdigits + 1 } }
25 } }
26 }
27 i = i + 1
28 }
29 if cdigits == 1 { cents = cents * 10 }
30 var total: i64 = dollars * 100 + cents
31 if neg == 1 { total = 0 - total }
32 return total
33}
34
35// does needle (c-string) occur in hay[start,end)? case-sensitive (feed upper-case descriptions).
36func lg_contains(hay: *u8, start: i64, end: i64, needle: *u8) -> i64 {
37 var nlen: i64 = 0; while needle[nlen] != (0 as u8) { nlen = nlen + 1 }
38 if nlen == 0 { return 0 }
39 var i: i64 = start
40 while i + nlen <= end {
41 var j: i64 = 0; var hit: i64 = 1
42 while j < nlen { if hay[i+j] != needle[j] { hit = 0 } j = j + 1 }
43 if hit == 1 { return 1 }
44 i = i + 1
45 }
46 return 0
47}
48
49// classify an outflow description into need / want / save (keyword rules; unknown -> discretionary want).
50func lg_class_of(d: *u8, s: i64, e: i64) -> i64 {
51 if lg_contains(d, s, e, "RENT" as *u8) == 1 { return SP_NEED }
52 if lg_contains(d, s, e, "MORTGAGE" as *u8) == 1 { return SP_NEED }
53 if lg_contains(d, s, e, "GROCER" as *u8) == 1 { return SP_NEED }
54 if lg_contains(d, s, e, "WHOLE FOODS" as *u8) == 1 { return SP_NEED }
55 if lg_contains(d, s, e, "SAFEWAY" as *u8) == 1 { return SP_NEED }
56 if lg_contains(d, s, e, "UTILIT" as *u8) == 1 { return SP_NEED }
57 if lg_contains(d, s, e, "ELECTRIC" as *u8) == 1 { return SP_NEED }
58 if lg_contains(d, s, e, "WATER" as *u8) == 1 { return SP_NEED }
59 if lg_contains(d, s, e, "INSUR" as *u8) == 1 { return SP_NEED }
60 if lg_contains(d, s, e, "SAVING" as *u8) == 1 { return SP_SAVE }
61 if lg_contains(d, s, e, "401K" as *u8) == 1 { return SP_SAVE }
62 if lg_contains(d, s, e, "IRA CONTRIB" as *u8) == 1 { return SP_SAVE }
63 return SP_WANT
64}
65
66// recurring merchant? (subscriptions, rent, insurance, utilities, savings transfers)
67func lg_is_recurring(d: *u8, s: i64, e: i64) -> i64 {
68 if lg_contains(d, s, e, "RENT" as *u8) == 1 { return 1 }
69 if lg_contains(d, s, e, "MORTGAGE" as *u8) == 1 { return 1 }
70 if lg_contains(d, s, e, "NETFLIX" as *u8) == 1 { return 1 }
71 if lg_contains(d, s, e, "SPOTIFY" as *u8) == 1 { return 1 }
72 if lg_contains(d, s, e, "HULU" as *u8) == 1 { return 1 }
73 if lg_contains(d, s, e, "SUBSCRIP" as *u8) == 1 { return 1 }
74 if lg_contains(d, s, e, "MEMBERSHIP" as *u8) == 1 { return 1 }
75 if lg_contains(d, s, e, "INSUR" as *u8) == 1 { return 1 }
76 if lg_contains(d, s, e, "UTILIT" as *u8) == 1 { return 1 }
77 if lg_contains(d, s, e, "SAVING" as *u8) == 1 { return 1 }
78 if lg_contains(d, s, e, "401K" as *u8) == 1 { return 1 }
79 return 0
80}
81
82// parse a CSV (date,description,amount per line) into sp_out (outflows, SP-format) + income_out[0] (sum inflows).
83// returns the number of outflow rows. Header lines (non-numeric amount) are auto-skipped (amount parses to 0).
84func lg_parse_csv(csv: *u8, len: i64, sp_out: *i64, max: i64, income_out: *i64) -> i64 {
85 var i: i64 = 0
86 var k: i64 = 0
87 income_out[0] = 0
88 while i < len {
89 var le: i64 = i; var run: i64 = 1
90 while run == 1 { if le >= len { run = 0 } else { if csv[le] == (10 as u8) { run = 0 } else { le = le + 1 } } }
91 var c1: i64 = 0 - 1; var c2: i64 = 0 - 1; var p: i64 = i
92 while p < le { if csv[p] == (44 as u8) { if c1 == (0 - 1) { c1 = p } c2 = p } p = p + 1 }
93 if c1 >= 0 { if c2 > c1 {
94 let amt: i64 = lg_parse_amount(csv, c2 + 1, le)
95 if amt > 0 { income_out[0] = income_out[0] + amt }
96 else { if amt < 0 {
97 if k < max {
98 sp_out[k*SP_FIELDS+0] = 0 - amt
99 sp_out[k*SP_FIELDS+1] = lg_class_of(csv, c1 + 1, c2)
100 sp_out[k*SP_FIELDS+2] = lg_is_recurring(csv, c1 + 1, c2)
101 k = k + 1
102 }
103 } }
104 } }
105 i = le + 1
106 }
107 return k
108}
109
110// convenience: read a statement CSV file into a buffer (real use = the user's own export on their machine).
111func lg_load_file(path: *u8, buf: *u8, cap: i64) -> i64 {
112 let fd: i64 = sys_openat_rd(path)
113 if fd < 0 { return 0 }
114 var o: i64 = 0
115 var r: i64 = sys_read(fd, buf, cap)
116 while r > 0 { o = o + r; r = sys_read(fd, ((buf as i64) + o) as *u8, cap - o) }
117 sys_close(fd)
118 return o
119}