You log a 1,200 yen expense at 12:30 a.m. on the first of the month, and the screen's "October total" goes up instead of November's. In record-keeping apps, this is the kind of bug that tends to go unnoticed longest.
Apps that let you enter an amount, save it, and sum by month usually come out of a tool like Rork looking fine on the first try. Working and adding up correctly at month-end are different things, though. Here are three lines worth adding to the first prompt, checked in order: how money is stored, where a month begins, and how input is cleaned.
One caveat up front: I only verified the code below on Node 22. What Rork actually generates will vary, so please treat this as a yardstick for reading generated code, not as a drop-in.
1. Store money as integer yen
Decimals add tiny errors as you sum. In my run, 0.1 + 0.2 + 0.3 gave 0.6000000000000001, not 0.6. Summing 1980.1, 320.2 and 99.9 happens to look like exactly 2400.2, but that is luck; with more entries, stray digits show up somewhere.
For yen you never need decimals. Add this to the first prompt:
Store amounts as integer yen (number). No decimals. Format with thousands separators only when displaying.For foreign currencies, the same idea means storing the smallest unit (cents) as an integer.
2. Build the month key from the local date
Generated code often groups by month with toISOString().slice(0, 7), which reads the month from UTC. On a device in Japan, an expense at 00:30 on November 1:
const spent = new Date("2026-11-01T00:30:00+09:00");
spent.toISOString().slice(0, 7); // "2026-10" <- the UTC month
spent.getMonth() + 1; // 11 <- the device's local month (TZ=Asia/Tokyo)Before 9 a.m. Japan time it is still the previous day in UTC, so only entries logged early on the first of the month land in the previous month. If your totals are off by exactly one entry at month-end, this is the usual shape.
Build the monthly aggregation key from the device's local date (getFullYear / getMonth). Never use toISOString for aggregation.3. Clean the input once, before saving
What people type into an amount field is not always digits. Passing it through Number and parseInt splits like this:
| Input | Number | parseInt |
|---|---|---|
| 1,200 | NaN | 1 |
| 1200 (full-width) | NaN | NaN |
| 1200円 | NaN | 1200 |
| (empty) | 0 | NaN |
| 12.5 | 12.5 | 12 |
The one to watch is "1,200" becoming 1 under parseInt: no error, just a 1 yen entry saved. An empty field becoming 0 under Number works the same way, quietly piling up 0 yen expenses.
One function for all three, and the test I ran
// ledger.mjs
// Money is integer yen. Input strings are normalized here, once.
export function parseYen(input) {
const half = String(input)
.replace(/[0-9]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 0xfee0))
.replace(/[,、,\s円¥¥]/g, "");
if (!/^\d{1,9}$/.test(half)) return null; // empty, decimal, or mixed text is rejected
return Number(half);
}
// Month key comes from the local date (toISOString is UTC, so avoid it)
export function monthKey(date) {
const m = String(date.getMonth() + 1).padStart(2, "0");
return `${date.getFullYear()}-${m}`;
}
export function totalsByMonth(entries) {
const totals = {};
for (const e of entries) {
const key = monthKey(new Date(e.spentAt));
totals[key] = (totals[key] ?? 0) + e.amountYen;
}
return totals;
}// ledger.test.mjs
import assert from "node:assert/strict";
import { parseYen, totalsByMonth } from "./ledger.mjs";
assert.equal(parseYen("1,200"), 1200);
assert.equal(parseYen("1200"), 1200);
assert.equal(parseYen("1200円"), 1200);
assert.equal(parseYen(" ¥1,200 "), 1200);
assert.equal(parseYen(""), null);
assert.equal(parseYen("12.5"), null);
const entries = [
{ spentAt: "2026-10-31T23:50:00+09:00", amountYen: 980 },
{ spentAt: "2026-11-01T00:30:00+09:00", amountYen: 1200 },
{ spentAt: "2026-11-15T12:00:00+09:00", amountYen: 300 },
];
assert.deepEqual(totalsByMonth(entries), { "2026-10": 980, "2026-11": 1500 });Everything passed with TZ=Asia/Tokyo node ledger.test.mjs.
The surprise: the same test fails under another time zone
Running it with TZ=UTC failed at the aggregation step: 2026-10 came out as 2180 and 2026-11 as 300. The 1,200 yen entry from 00:30 on November 1 (Japan time) is October 31, 15:30 on a UTC device.
The function isn't wrong; a month is defined by where the person lives, and that is the intended behavior. But it will confuse you when a test that passes on your laptop fails on an overseas server or in CI. Pinning TZ at the top of the test was the cheapest fix I found.
Three lines to paste into the first prompt
Store amounts as integer yen (number); no decimals.
Build monthly aggregation keys from the device's local date; never use toISOString for aggregation.
For amount input, strip full-width digits, commas and "円", check that only digits remain, and if not, don't save and return to the field.Fixing these later means editing screens that were already generated, so putting them in the first prompt saves rounds. For adding requests in small steps, see writing prompts that add one feature at a time, and for screen-level instructions, UI generation prompt patterns.
For apps that handle numbers, decide how to hold them and where to cut them first. As an indie developer I'd draw that line the same way for any app. A good next step is to take whatever record-keeping app you have now, and check which month an entry made before 9 a.m. on the first landed in.