From ff06ea67033cd60d7cd7000dd47021b6c9fc0437 Mon Sep 17 00:00:00 2001 From: Alexander Shabunevich Date: Sun, 12 May 2024 20:11:08 +0300 Subject: [PATCH] feat: number mask --- packages/maska/src/mask.ts | 10 +++++++++ packages/maska/src/number.ts | 43 ++++++++++++++++++++++++++++++++++++ packages/maska/src/parser.ts | 14 ++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 packages/maska/src/number.ts diff --git a/packages/maska/src/mask.ts b/packages/maska/src/mask.ts index c70e68e..8738e75 100644 --- a/packages/maska/src/mask.ts +++ b/packages/maska/src/mask.ts @@ -1,13 +1,21 @@ import { MaskTokens, tokens } from './tokens' +import { processNumber } from './number' export type MaskType = string | string[] | ((input: string) => string) | null +interface MaskNumber { + locale?: string + fraction?: number + unsigned?: boolean +} + export interface MaskOptions { mask?: MaskType tokens?: MaskTokens tokensReplace?: boolean eager?: boolean reversed?: boolean + number?: MaskNumber } export class Mask { @@ -109,6 +117,8 @@ export class Mask { } private process (value: string, maskRaw: string | null, masked = true): string { + if (this.opts.number != null) return processNumber(value, masked, this.opts) + if (maskRaw == null) return value const memoKey = `v=${value},mr=${maskRaw},m=${masked ? 1 : 0}` diff --git a/packages/maska/src/number.ts b/packages/maska/src/number.ts new file mode 100644 index 0000000..33cc6a3 --- /dev/null +++ b/packages/maska/src/number.ts @@ -0,0 +1,43 @@ +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 => + new Intl.NumberFormat(opts.number?.locale ?? 'en', { + minimumFractionDigits: min, + maximumFractionDigits: max, + // @ts-expect-error + roundingMode: 'trunc' + }) + +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 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 (float === '' || Number.isNaN(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) + } + + 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 ends with decimal separator + result += decimal + } + + return sign + result +} diff --git a/packages/maska/src/parser.ts b/packages/maska/src/parser.ts index 2e3f59b..1505389 100644 --- a/packages/maska/src/parser.ts +++ b/packages/maska/src/parser.ts @@ -25,6 +25,20 @@ export const parseInput = ( opts.tokens = parseTokens(input.dataset.maskaTokens) } + const number: MaskOptions['number'] = {} + if (input.dataset.maskaNumberLocale != null) { + number.locale = input.dataset.maskaNumberLocale + } + if (input.dataset.maskaNumberFraction != null) { + number.fraction = parseInt(input.dataset.maskaNumberFraction) + } + if (input.dataset.maskaNumberUnsigned != null) { + number.unsigned = parseBool(input.dataset.maskaNumberUnsigned) + } + if (Object.values(number).length > 0) { + opts.number = number + } + return opts }