ec27228a8e
Em dashes (—) and en dashes (–) had spread across comments, docs, tests, and a few UI strings, reading as AI-generated boilerplate rather than house style. Replaced each with punctuation matching its context: colon for explanatory clauses, comma for asides, plain hyphen for numeric/legal ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for paired-dash asides. messages/en.json and messages/sv.json were fixed by hand together to keep sv/en in sync. Left untouched where the dash is the functional subject rather than decorative punctuation: date-range-parser.ts's separator regex, charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the agent system-prompt files that already instruct against em dashes, and a golden iXBRL test fixture compared byte-for-byte. Also fixes two bugs surfaced along the way: an off-by-one in ApiKeysPanel's scope-label split (a leftover from an earlier partial pass), and a charset-repair test that had lost the literal en-dash it exists to verify. Regenerated the agent atom seed migration (skills:generate) since 27 SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes, with an explicit carve-out for the functional-dash cases above. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
104 lines
3.0 KiB
TypeScript
104 lines
3.0 KiB
TypeScript
import type { JournalEntryLine } from '@/types'
|
||
|
||
export interface CorrectionLineInput {
|
||
account_number: string
|
||
debit_amount: string | number
|
||
credit_amount: string | number
|
||
}
|
||
|
||
export interface AccountRow {
|
||
account_number: string
|
||
original: number
|
||
storno: number
|
||
correction: number
|
||
delta: number
|
||
/**
|
||
* True when this account appears on at least one (4-digit) corrected line.
|
||
* Lets the UI tell apart an account the user removed from the rättelse, which
|
||
* the storno then zeroes (delta = −original), from one that was never part of
|
||
* the correction at all. Without this distinction a removed account renders as
|
||
* a bare "-", reading as "unchanged" when it is in fact being drained.
|
||
*/
|
||
correctionPresent: boolean
|
||
}
|
||
|
||
function toNumber(v: string | number | null | undefined): number {
|
||
if (v == null) return 0
|
||
const n = typeof v === 'string' ? parseFloat(v) : v
|
||
return Number.isFinite(n) ? n : 0
|
||
}
|
||
|
||
function round2(n: number): number {
|
||
return Math.round(n * 100) / 100
|
||
}
|
||
|
||
/**
|
||
* Build per-account diff rows: original net, storno (= −original), proposed
|
||
* correction net, and förändring (= storno + correction = correction − original).
|
||
*
|
||
* Net per row is debit − credit. Accounts appearing only on one side still get
|
||
* a row, so the user sees account swaps clearly (old account drains to zero,
|
||
* new account picks up the value).
|
||
*
|
||
* Corrected lines with account_number.length !== 4 are skipped: those are
|
||
* incomplete user input mid-edit, not real proposals.
|
||
*/
|
||
export function buildCorrectionRows(
|
||
original: JournalEntryLine[],
|
||
corrected: CorrectionLineInput[]
|
||
): AccountRow[] {
|
||
const map = new Map<string, AccountRow>()
|
||
|
||
const ensure = (acc: string): AccountRow => {
|
||
let row = map.get(acc)
|
||
if (!row) {
|
||
row = {
|
||
account_number: acc,
|
||
original: 0,
|
||
storno: 0,
|
||
correction: 0,
|
||
delta: 0,
|
||
correctionPresent: false,
|
||
}
|
||
map.set(acc, row)
|
||
}
|
||
return row
|
||
}
|
||
|
||
for (const line of original) {
|
||
if (!line.account_number) continue
|
||
const net = toNumber(line.debit_amount) - toNumber(line.credit_amount)
|
||
const row = ensure(line.account_number)
|
||
row.original += net
|
||
row.storno -= net
|
||
}
|
||
|
||
for (const line of corrected) {
|
||
if (!line.account_number || line.account_number.length !== 4) continue
|
||
const net = toNumber(line.debit_amount) - toNumber(line.credit_amount)
|
||
const row = ensure(line.account_number)
|
||
row.correction += net
|
||
row.correctionPresent = true
|
||
}
|
||
|
||
for (const row of map.values()) {
|
||
row.original = round2(row.original)
|
||
row.storno = round2(row.storno)
|
||
row.correction = round2(row.correction)
|
||
row.delta = round2(row.storno + row.correction)
|
||
}
|
||
|
||
return Array.from(map.values()).sort((a, b) =>
|
||
a.account_number.localeCompare(b.account_number)
|
||
)
|
||
}
|
||
|
||
export function formatSignedAmount(n: number): string {
|
||
if (n === 0) return '-'
|
||
const abs = Math.abs(n).toLocaleString('sv-SE', {
|
||
minimumFractionDigits: 2,
|
||
maximumFractionDigits: 2,
|
||
})
|
||
return n > 0 ? `+${abs}` : `−${abs}`
|
||
}
|