diff --git a/src/number.ts b/src/number.ts index edb37d8..01f0705 100644 --- a/src/number.ts +++ b/src/number.ts @@ -3,37 +3,58 @@ import { MaskOptions } from './mask' const prepare = (input: string, group: string, decimal: string): string => input.replaceAll(group, '').replace(decimal, '.').replace('..', '.').replace(/[^.\d]/g, '') -const createFormatter = (min: number, max: number, opts: MaskOptions): Intl.NumberFormat => +const plainNumberFormatter = (min: number, max: number): Intl.NumberFormat => + new Intl.NumberFormat('en', { + useGrouping: false, + minimumFractionDigits: min, + maximumFractionDigits: max, + roundingMode: 'trunc' + }) + +const truncNumber = (value: number, max: number): string => + plainNumberFormatter(0, max).format(value) + +const maskFormatter = (min: number, max: number, opts: MaskOptions): Intl.NumberFormat => new Intl.NumberFormat(opts.number?.locale ?? 'en', { minimumFractionDigits: min, maximumFractionDigits: max, roundingMode: 'trunc' }) +const extractNumberFromString = (value: string, masked = true, fraction = 0, opts: MaskOptions): string => { + const numParser = masked ? plainNumberFormatter(0, fraction) : maskFormatter(0, fraction, opts) + const parts = numParser.formatToParts(1000.12) + const group = parts.find((part) => part.type === 'group')?.value ?? ' ' + const decimal = parts.find((part) => part.type === 'decimal')?.value ?? '.' + return prepare(value, group, decimal) +} + export const processNumber = (value: string, masked = true, opts: MaskOptions): string => { const sign = opts.number?.unsigned == null && value.startsWith('-') ? '-' : '' const fraction = opts.number?.fraction ?? 0 - let formatter = createFormatter(0, fraction, opts) + const float = extractNumberFromString(value, masked, fraction, opts) + const floatNum = parseFloat(float) + if (Number.isNaN(floatNum)) return sign + + if (!masked) { + return sign + truncNumber(floatNum, fraction) + } + + let formatter = maskFormatter(0, fraction, opts) const parts = formatter.formatToParts(1000.12) - const group = parts.find((part) => part.type === 'group')?.value ?? ' ' const decimal = parts.find((part) => part.type === 'decimal')?.value ?? '.' - const float = prepare(value, group, decimal) - - if (Number.isNaN(parseFloat(float))) return sign // allow zero at the end const floatParts = float.split('.') if (floatParts[1] != null && floatParts[1].length >= 1) { const min = floatParts[1].length <= fraction ? floatParts[1].length : fraction - formatter = createFormatter(min, fraction, opts) + formatter = maskFormatter(min, fraction, opts) } let result = formatter.format(parseFloat(float)) - if (!masked) { - result = prepare(result, group, decimal) - } else if (fraction > 0 && float.endsWith('.') && !float.slice(0, -1).includes('.')) { + if (fraction > 0 && float.endsWith('.') && !float.slice(0, -1).includes('.')) { // if ends with decimal separator result += decimal } diff --git a/test/input.test.ts b/test/input.test.ts index 11245ee..1582174 100644 --- a/test/input.test.ts +++ b/test/input.test.ts @@ -2147,7 +2147,7 @@ describe('Number mask', () => { new MaskInput(input) await user.type(input, '1234.56') - expect(input).toHaveValue('1 234,56') + expect(input).toHaveValue('123 456') }) test('unsigned number', async () => { diff --git a/test/number.test.ts b/test/number.test.ts index 2057386..fcfb215 100644 --- a/test/number.test.ts +++ b/test/number.test.ts @@ -71,9 +71,9 @@ test('fraction number unmasked', () => { expect(mask.unmasked('1234567.')).toBe('1234567') expect(mask.unmasked('1234567..')).toBe('1234567') expect(mask.unmasked('1234567.1')).toBe('1234567.1') - expect(mask.unmasked('1234567.10')).toBe('1234567.10') - expect(mask.unmasked('1234567.109')).toBe('1234567.10') - expect(mask.unmasked('1234567.0')).toBe('1234567.0') + expect(mask.unmasked('1234567.10')).toBe('1234567.1') + expect(mask.unmasked('1234567.109')).toBe('1234567.1') + expect(mask.unmasked('1234567.0')).toBe('1234567') expect(mask.unmasked('1234567.01')).toBe('1234567.01') expect(mask.unmasked('1234567.019')).toBe('1234567.01') expect(mask.unmasked('1234567.1.')).toBe('1234567.1') @@ -125,9 +125,9 @@ test('russian unmasked number', () => { expect(mask.unmasked('1234567.')).toBe('1234567') expect(mask.unmasked('1234567..')).toBe('1234567') expect(mask.unmasked('1234567.1')).toBe('1234567.1') - expect(mask.unmasked('1234567.10')).toBe('1234567.10') - expect(mask.unmasked('1234567.109')).toBe('1234567.10') - expect(mask.unmasked('1234567.0')).toBe('1234567.0') + expect(mask.unmasked('1234567.10')).toBe('1234567.1') + expect(mask.unmasked('1234567.109')).toBe('1234567.1') + expect(mask.unmasked('1234567.0')).toBe('1234567') expect(mask.unmasked('1234567.01')).toBe('1234567.01') expect(mask.unmasked('1234567.019')).toBe('1234567.01') expect(mask.unmasked('1234567.1.')).toBe('1234567.1') @@ -151,9 +151,9 @@ test('initial russian number', () => { const mask = new Mask({ number: { locale: 'ru', fraction: 2 } }) expect(mask.masked('1234.56')).toBe('1 234,56') - expect(mask.masked('1234,56')).toBe('1 234,56') - expect(mask.masked('1,234.56')).toBe('1,23') - expect(mask.masked('1 234,56')).toBe('1 234,56') + expect(mask.masked('1234,56')).toBe('123 456') + expect(mask.masked('1,234.56')).toBe('1 234,56') + expect(mask.masked('1 234,56')).toBe('123 456') expect(mask.masked('1 234.56')).toBe('1 234,56') expect(mask.masked('1 234.56')).toBe('1 234,56') }) @@ -161,10 +161,32 @@ test('initial russian number', () => { test('initial brazilian number', () => { const mask = new Mask({ number: { locale: 'pt-BR', fraction: 2 } }) - expect(mask.masked('1234.56')).toBe('123.456') - expect(mask.masked('1234,56')).toBe('1.234,56') - expect(mask.masked('1.23')).toBe('123') - expect(mask.masked('1,23')).toBe('1,23') + expect(mask.masked('1234.56')).toBe('1.234,56') + expect(mask.masked('1234,56')).toBe('123.456') + expect(mask.masked('1.23')).toBe('1,23') + expect(mask.masked('1,23')).toBe('123') +}) + +test('handle masked/unmasked for custom locales', () => { + const brMask = new Mask({ number: { locale: 'pt-BR', fraction: 2 } }) + + expect(brMask.unmasked('1.234,56')).toBe('1234.56') + expect(brMask.masked(brMask.unmasked('1.234,56'))).toBe('1.234,56') + expect(brMask.masked(brMask.unmasked('1.234,1'))).toBe('1.234,1') + expect(brMask.masked(brMask.unmasked('1.234,01'))).toBe('1.234,01') + expect(brMask.masked(brMask.unmasked('1.234,10'))).toBe('1.234,1') + expect(brMask.masked(brMask.unmasked('1.234,109'))).toBe('1.234,1') + expect(brMask.masked(brMask.unmasked('1,23'))).toBe('1,23') + + const deMask = new Mask({ number: { locale: 'de', fraction: 3 } }) + + expect(deMask.unmasked('1.234,56')).toBe('1234.56') + expect(deMask.masked(deMask.unmasked('1.234,56'))).toBe('1.234,56') + expect(deMask.masked(deMask.unmasked('1.234,1'))).toBe('1.234,1') + expect(deMask.masked(deMask.unmasked('1.234,01'))).toBe('1.234,01') + expect(deMask.masked(deMask.unmasked('1.234,10'))).toBe('1.234,1') + expect(deMask.masked(deMask.unmasked('1.234,1009'))).toBe('1.234,1') + expect(deMask.masked(deMask.unmasked('1,23'))).toBe('1,23') }) // https://github.com/beholdr/maska/issues/228