nx_money.nx source
↩ module page · 91 lines · 3726 B
1// nx_money.nx -- R1 of THE NISHI FINANCIAL ECOSYSTEM engine: the NO-FLOAT money substrate.
2// Money is exact i64 MINOR UNITS (cents). Rates are fixed-point scaled by MNY_RATE_SCALE (1e9).
3// All arithmetic is integer -- floats are WRONG for money (0.1 + 0.2 != 0.3), so this is correctness,
4// not doctrine. Provides exact add/sub/neg, rounded division (floor / half-up / banker's half-even),
5// scaled-rate application (interest), APR->periodic, percent constructor, and [-]D.CC formatting.
6// Foundation for the ledger (R2), debt/amortization (R4), audit (R5), optimizer (R7).
7// PRECONDITION on mny_apply_rate: |amount * rate_scaled| < 2^63 (true for all realistic personal-finance
8// amounts; 128-bit-safe widening for extreme values is the R1b hardening rung). license_tier: ORIGINAL
9import "nx_syscalls.nx"
10
11const MNY_RATE_SCALE: i64 = 1000000000
12const RND_FLOOR: i64 = 0
13const RND_HALF_UP: i64 = 1
14const RND_HALF_EVEN: i64 = 2
15
16func mny_add(a: i64, b: i64) -> i64 { return a + b }
17func mny_sub(a: i64, b: i64) -> i64 { return a - b }
18func mny_neg(a: i64) -> i64 { return 0 - a }
19
20// Floor division toward -infinity (den > 0).
21func mny_div_floor(num: i64, den: i64) -> i64 {
22 var q: i64 = num / den
23 let r: i64 = num - q * den
24 if r < 0 { q = q - 1 }
25 return q
26}
27
28// Rounded integer division num/den (den > 0) under mode. half-up rounds .5 away from zero;
29// half-even rounds .5 to the even neighbour (banker's). Sign-symmetric via magnitude.
30func mny_div_round(num: i64, den: i64, mode: i64) -> i64 {
31 if mode == RND_FLOOR { return mny_div_floor(num, den) }
32 var neg: i64 = 0
33 var x: i64 = num
34 if x < 0 { neg = 1; x = 0 - x }
35 let q: i64 = x / den
36 let r: i64 = x - q * den
37 var inc: i64 = 0
38 let twice: i64 = 2 * r
39 if mode == RND_HALF_UP { if twice >= den { inc = 1 } }
40 if mode == RND_HALF_EVEN {
41 if twice > den { inc = 1 }
42 else { if twice == den { if (q % 2) == 1 { inc = 1 } } }
43 }
44 var qq: i64 = q + inc
45 if neg == 1 { qq = 0 - qq }
46 return qq
47}
48
49// Apply a fixed-point rate (scaled by MNY_RATE_SCALE) to a money amount, exactly rounded.
50// interest = round(amount * rate / SCALE).
51func mny_apply_rate(amount: i64, rate_scaled: i64, mode: i64) -> i64 {
52 return mny_div_round(amount * rate_scaled, MNY_RATE_SCALE, mode)
53}
54
55// Convert an annual fixed-point rate to a monthly one (banker's rounding).
56func mny_rate_monthly(apr_scaled: i64) -> i64 {
57 return mny_div_round(apr_scaled, 12, RND_HALF_EVEN)
58}
59
60// Build a scaled rate from a percent expressed as the fraction pct_num/pct_den
61// (e.g. 6.5% = mny_rate_from_pct(65, 10)). rate = (pct_num / pct_den) / 100 * SCALE.
62func mny_rate_from_pct(pct_num: i64, pct_den: i64) -> i64 {
63 return mny_div_round(pct_num * MNY_RATE_SCALE, pct_den * 100, RND_HALF_EVEN)
64}
65
66// Write a base-10 integer (>= 0) to out at pos; returns new pos.
67func mny_wdec(out: *u8, pos: i64, v: i64) -> i64 {
68 if v == 0 { out[pos] = 0x30 as u8; return pos + 1 }
69 let tmp: *u8 = sys_mmap(24)
70 var m: i64 = v
71 var k: i64 = 0
72 while m > 0 { tmp[k] = (0x30 + (m % 10)) as u8; m = m / 10; k = k + 1 }
73 var p: i64 = pos
74 var i: i64 = k - 1
75 while i >= 0 { out[p] = tmp[i]; p = p + 1; i = i - 1 }
76 return p
77}
78
79// Format money (cents) as "[-]D.CC" into out; returns byte length.
80func mny_format(amount: i64, out: *u8) -> i64 {
81 var p: i64 = 0
82 var x: i64 = amount
83 if x < 0 { out[p] = 0x2d as u8; p = p + 1; x = 0 - x }
84 let dollars: i64 = x / 100
85 let cents: i64 = x - dollars * 100
86 p = mny_wdec(out, p, dollars)
87 out[p] = 0x2e as u8; p = p + 1
88 out[p] = (0x30 + (cents / 10)) as u8; p = p + 1
89 out[p] = (0x30 + (cents % 10)) as u8; p = p + 1
90 return p
91}