code wiki / (root) / nx_money_ledger.nx

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}