Xfingine
Xfingine is the computation layer behind the personal-finance tools on sakthipriyan.com.
It is a pure library. Data in, arithmetic, data out. No UI, no I/O, no network, no clock — the same input always produces the same output, on every target.
Input data ──▶ Engine ──▶ Output data
One core written in Rust, shipped three ways:
| Ecosystem | Package | Install |
|---|---|---|
| 🦀 Rust | xfingine |
cargo add xfingine |
| 🟨 JavaScript | xfingine-wasm |
npm i xfingine-wasm |
| 🐍 Python | xfingine |
pip install xfingine |
Motivation
The tools on the site started life as standalone JavaScript files — one .js
per calculator, each with its own copy of the maths tangled up in its own Vue
components and ECharts wiring. That worked, but it meant the arithmetic could
only ever run in a browser, could not be tested independently of the UI, and
would quietly drift between tools.
Xfingine pulls the arithmetic out into a single Rust core:
- Testable. The maths is separated from rendering, so it can be covered by snapshot tests and invariant checks that run in CI.
- Portable. The same engine backs the website (via WASM), notebooks and scripts (via Python), and any Rust program — with no risk of three implementations disagreeing.
- Honest about inflation. Every engine that projects money into the future reports both nominal rupees and rupees discounted to today's value. That is the "RealValue" part, and it is the whole point.
Engines
| Engine | Feature | Status | What it does |
|---|---|---|---|
| 📉 RealValue EMI | emi |
Production Ready | Loan amortization schedules in both nominal and inflation-adjusted rupees. Solves for EMI, tenure, or loan amount. |
More engines from the tools page — RealValue SIP, FX, Portfolio, Family SIP Allocator, IBKR Tax, Emergency Fund — are intended to follow the same shape.
The RealValue EMI Engine
A standard EMI calculator tells you a ₹50L loan at 9% over 20 years costs ₹1.08 crore. That number is misleading: the payment in year 20 is made with rupees worth far less than the payment in year 1.
At 6% inflation, that same loan costs ₹63.9L in today's rupees — the interest bill shrinks from ₹58L to ₹14L once you stop comparing rupees from different decades.
The engine solves for whichever variable you don't know:
mode |
You supply | You get back |
|---|---|---|
emi |
loan amount + tenure | the monthly payment |
tenure |
loan amount + payment | how long it takes |
loanAmount |
payment + tenure | how much you can borrow |
Alongside the headline figures it returns the full month-by-month schedule and, if you give it a start month, a per-calendar-year breakdown.
Usage
Rust
use xfingine::emi::{compute, EmiRequest, YearMonth};
let request = EmiRequest::emi(5_000_000.0, 240, 9.0)
.with_inflation(6.0)
.with_start(YearMonth::new(2026, 1).unwrap());
let result = compute(&request)?;
println!("EMI ₹{}", result.emi); // ₹44986
println!("Nominal cost ₹{}", result.totals.nominal_paid); // ₹10796818
println!("Real cost ₹{}", result.totals.real_paid); // ₹6391327
JavaScript
import init, { compute_emi } from 'xfingine-wasm';
await init();
const result = compute_emi({
mode: 'emi',
loanAmount: 5_000_000,
months: 240,
interestRate: 9,
inflationRate: 6,
start: '2026-01',
});
console.log(result.emi); // 44986
console.log(result.totals.realPaid); // 6391327
console.log(result.years[0].nominalTotal);
Python
import xfingine
result = xfingine.compute_emi({
"mode": "emi",
"loanAmount": 5_000_000,
"months": 240,
"interestRate": 9,
"inflationRate": 6,
"start": "2026-01",
})
print(result["emi"]) # 44986
print(result["totals"]["realPaid"]) # 6391327
Architecture
graph TD
subgraph Core ["xfingine (Rust crate)"]
A["src/emi<br/>RealValue EMI Engine"]
B["src/num<br/>JS-compatible rounding"]
C["src/error<br/>XfingineError"]
A -.->|uses| B
A -.->|returns| C
end
subgraph Targets ["Delivery targets"]
D["wasm/<br/>xfingine-wasm → npm"]
E["python/<br/>xfingine → PyPI"]
F["crates.io"]
end
A --> D
A --> E
A --> F
D --> G["sakthipriyan.com<br/>building-wealth/tools"]
E --> H["notebooks & scripts"]
Each engine lives behind its own Cargo feature, so a WASM bundle only carries the maths it actually uses:
xfingine = { version = "0.1", default-features = false, features = ["emi"] }
Conventions
These hold across all three ecosystems:
- Money out is
i64whole rupees. Engines compute inf64and round at the boundary, because that is what a lender actually debits — no fractional paise drifting across 360 rows. - Rates are percentages, not fractions.
9.0means 9%. - JSON is
camelCaseeverywhere: Rust, WASM, and Python. - No ambient clock. Dates are opt-in. Omit
startand the engine is pure arithmetic with no calendar; supply it and you get dated rows plus a per-year breakdown.
Correctness
The EMI engine was ported from the original realvalue-emi-engine.js and is
verified against it, not merely tested in isolation:
- A differential harness ran the extracted JavaScript maths and the Rust engine over the same inputs and compared every field of every row — 16 hand-picked scenarios (2,691 rows) plus 600 randomized cases (133,686 rows). The output is bit-identical, down to the rounding, with a single deliberate exception noted in CHANGELOG.md.
- Committed snapshots in
tests/data/lock the output in. Unlike Xfina, whose fixtures are real statements containing PII and therefore live outside the repository, these are pure numbers — so they are committed and CI checks them directly. - Invariant tests assert the properties that must hold whatever the numbers are: every row's payment splits exactly into principal and interest, the balance reaches exactly zero, the per-year breakdown covers every payment once, and more inflation always means less real cost.
To re-record snapshots after an intentional change:
UPDATE_EXPECTED=1 cargo test
Development
cargo test --workspace --all-features # tests, including doctests and snapshots
cargo fmt --check # formatting
cargo clippy --workspace --all-targets --all-features -- -D warnings
Building the bindings:
cd wasm && wasm-pack build --target web # → wasm/pkg, the npm package
cd python && maturin develop # → importable xfingine module
See CONTRIBUTING.md for adding a new engine, and AGENTS.md for the project's rules and conventions.
Releasing
Releases are cut with xtask and published by GitHub Actions on tag push:
cargo xtask prepare-release <major|minor|patch> # bumps version, rolls the changelog, opens a branch
# push the branch, open a PR, merge it
cargo xtask tag-release # tags main and pushes
The tag triggers .github/workflows/publish.yml, which publishes to crates.io,
npm, and PyPI in parallel.
License
Licensed under the Apache License, Version 2.0.
Release files for xfingine 0.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| xfingine-0.0.1.tar.gz | 155.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| xfingine-0.0.1-cp39-cp39-macosx_11_0_arm64.whl | CPython 3.9 | CPython 3.9 | macOS 11.0+ ARM64 | Details |
Total release size: 414.1 kB
Release files / xfingine-0.0.1.tar.gz
| Download URL | xfingine-0.0.1.tar.gz |
|---|---|
| Size | 155.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6fb726435457158c7287419d34d248832cf2bb62cd3337e8110861dfd7140ba7
|
|
BLAKE2b-256 checksum How to use checksums |
0c2b1a9d2121e91f42030315815b983369aa665362b0a5efeba81fe7bcaca74c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.15.0
|
Release files / xfingine-0.0.1-cp39-cp39-macosx_11_0_arm64.whl
| Download URL | xfingine-0.0.1-cp39-cp39-macosx_11_0_arm64.whl |
|---|---|
| Size | 258.6 kB |
| Tags | CPython 3.9 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
2d78d7942f779db492e41cb62e3bb4aca09e1910ebbdfb81530426a267bcc2c1
|
|
BLAKE2b-256 checksum How to use checksums |
06753a141a69986726932264997d414d13254f51588b4499f3a2fce9ae339169
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
maturin/1.15.0
|