Intl sv-SE formats negatives with U+2212, which the bundled react-pdf Helvetica/Courier fonts cannot render, so a loss printed as a profit. lib/pdf/number-text.ts pdfNumberText maps U+2212 to an ASCII hyphen and prints negative zero unsigned; routed through financial-statement, kassaflodesanalys, reskontra, momsdeklaration, payslip (fmt, the literal Preliminar skatt sign and the calculation formula text) and operational-report templates, with content-stream regression tests. The K2/K3 arsredovisning templates keep main's formatPdfKronor from #2013. Closes #1982
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { pdfNumberText, pdfText, UNICODE_MINUS } from '../number-text'
|
||||
|
||||
describe('pdfNumberText', () => {
|
||||
it('maps every U+2212 to the ASCII hyphen-minus', () => {
|
||||
const formatted = (-4684.24).toLocaleString('sv-SE', { minimumFractionDigits: 2 })
|
||||
expect(formatted).toContain(UNICODE_MINUS)
|
||||
const safe = pdfNumberText(formatted)
|
||||
expect(safe).not.toContain(UNICODE_MINUS)
|
||||
expect(safe.startsWith('-4')).toBe(true)
|
||||
expect(pdfNumberText(`${UNICODE_MINUS}1 ${UNICODE_MINUS}2`)).toBe('-1 -2')
|
||||
})
|
||||
|
||||
it('prints a negative zero unsigned', () => {
|
||||
expect(pdfNumberText((-0).toLocaleString('sv-SE'))).toBe('0')
|
||||
expect(pdfNumberText(`${UNICODE_MINUS}0,00`)).toBe('0,00')
|
||||
expect(pdfNumberText('-0.00')).toBe('0.00')
|
||||
// A real small negative keeps its sign.
|
||||
expect(pdfNumberText(`${UNICODE_MINUS}0,01`)).toBe('-0,01')
|
||||
})
|
||||
|
||||
it('leaves text without a minus sign untouched', () => {
|
||||
expect(pdfNumberText('20 316')).toBe('20 316')
|
||||
expect(pdfNumberText('')).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('pdfText', () => {
|
||||
it('maps every U+2212 in free text to the ASCII hyphen-minus', () => {
|
||||
expect(pdfText(`bruttolön ${UNICODE_MINUS} skatt ${UNICODE_MINUS} nettoavdrag`)).toBe(
|
||||
'bruttolön - skatt - nettoavdrag'
|
||||
)
|
||||
expect(pdfText(`${UNICODE_MINUS}(full lön ${UNICODE_MINUS} sjuklön + karensavdrag)`)).toBe(
|
||||
'-(full lön - sjuklön + karensavdrag)'
|
||||
)
|
||||
})
|
||||
|
||||
it('does not treat the text as a number', () => {
|
||||
// A formula that happens to start with "-0" keeps its sign.
|
||||
expect(pdfText(`${UNICODE_MINUS}0,5 × lön`)).toBe('-0,5 × lön')
|
||||
expect(pdfText('grundlön + tillägg')).toBe('grundlön + tillägg')
|
||||
expect(pdfText('')).toBe('')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Sign guard for numbers rendered with @react-pdf/renderer.
|
||||
*
|
||||
* `Intl.NumberFormat('sv-SE')` and `toLocaleString('sv-SE')` write negatives
|
||||
* with U+2212 MINUS SIGN, not the ASCII hyphen-minus. The bundled standard
|
||||
* PDF fonts (Helvetica, Courier, Times: WinAnsi encoding) have no glyph for
|
||||
* U+2212, so react-pdf emits an unmapped byte and the viewer draws nothing:
|
||||
* a loss prints as a profit, a credit note as a charge (issue #1982, first
|
||||
* seen on Årets resultat in the årsredovisning PDF).
|
||||
*
|
||||
* Run every locale-formatted amount through this before it reaches a <Text>
|
||||
* that uses a standard font. Embedded TrueType fonts do carry the glyph, but
|
||||
* the ASCII hyphen renders identically there, so applying it everywhere is
|
||||
* safe.
|
||||
*/
|
||||
|
||||
/** Built via fromCharCode so no editor or transport can normalise it away. */
|
||||
export const UNICODE_MINUS = String.fromCharCode(0x2212)
|
||||
|
||||
/** "-0", "-0,00", "-0.00": a negated zero or sub-öre rounding noise. */
|
||||
const NEGATIVE_ZERO = /^-0(?:[.,]0+)?$/
|
||||
|
||||
/**
|
||||
* Locale-formatted number text made safe for a standard PDF font: U+2212
|
||||
* becomes the ASCII hyphen-minus, and a negative zero (which the dropped
|
||||
* glyph used to hide) prints unsigned, as it does on screen.
|
||||
*/
|
||||
export function pdfNumberText(text: string): string {
|
||||
const ascii = text.replaceAll(UNICODE_MINUS, '-')
|
||||
return NEGATIVE_ZERO.test(ascii) ? ascii.slice(1) : ascii
|
||||
}
|
||||
|
||||
/**
|
||||
* Free text (a formula, a label) made safe for a standard PDF font: U+2212
|
||||
* becomes the ASCII hyphen-minus. Unlike pdfNumberText it never touches a
|
||||
* leading "-0", because the text is not a single number.
|
||||
*/
|
||||
export function pdfText(text: string): string {
|
||||
return text.replaceAll(UNICODE_MINUS, '-')
|
||||
}
|
||||
Reference in New Issue
Block a user