pkg-components
Version:
252 lines (211 loc) • 6.39 kB
text/typescript
import { type IntlConfig } from '../CurrencyInputProps'
import { cleanValue } from './cleanValue'
import { escapeRegExp } from './escapeRegExp'
import { getSuffix } from './getSuffix'
export interface FormatValueOptions {
/**
* Value to format
*/
value: string | undefined
/**
* Decimal separator
*
* Default = '.'
*/
decimalSeparator?: string
/**
* Group separator
*
* Default = ','
*/
groupSeparator?: string
/**
* Turn off separators
*
* This will override Group separators
*
* Default = false
*/
disableGroupSeparators?: boolean
/**
* Intl locale currency config
*/
intlConfig?: IntlConfig
/**
* Specify decimal scale for padding/trimming
*
* Eg. 1.5 -> 1.50 or 1.234 -> 1.23
*/
decimalScale?: number
/**
* Prefix
*/
prefix?: string
/**
* Suffix
*/
suffix?: string
}
/**
* Format value with decimal separator, group separator and prefix
*/
export const formatValue = (options: FormatValueOptions): string => {
const {
value: _value,
decimalSeparator,
intlConfig,
decimalScale,
prefix = '',
suffix = ''
} = options
if (_value === '' || _value === undefined) {
return ''
}
if (_value === '-') {
return '-'
}
const isNegative = new RegExp(`^\\d?-${prefix ? `${escapeRegExp(prefix)}?` : ''}\\d`).test(
_value
)
let value =
decimalSeparator !== '.'
? replaceDecimalSeparator(_value, decimalSeparator, isNegative)
: _value
if (decimalSeparator && decimalSeparator !== '-' && value.startsWith(decimalSeparator)) {
value = '0' + value
}
const { locale, currency, ...formatOptions } = intlConfig || {}
const defaultNumberFormatOptions = {
...formatOptions,
minimumFractionDigits: decimalScale || 0,
maximumFractionDigits: 20
}
const numberFormatter = intlConfig
? new Intl.NumberFormat(locale, {
...defaultNumberFormatOptions,
...(currency && { style: 'currency', currency })
})
: new Intl.NumberFormat(undefined, defaultNumberFormatOptions)
const parts = numberFormatter.formatToParts(Number(value))
let formatted = replaceParts(parts, options)
// Does intl formatting add a suffix?
const intlSuffix = getSuffix(formatted, { ...options })
// Include decimal separator if user input ends with decimal separator
const includeDecimalSeparator = _value.slice(-1) === decimalSeparator ? decimalSeparator : ''
const [, decimals] = value.match(RegExp('\\d+\\.(\\d+)')) || []
// Keep original decimal padding if no decimalScale
if (decimalScale === undefined && decimals && decimalSeparator) {
if (formatted.includes(decimalSeparator)) {
formatted = formatted.replace(
RegExp(`(\\d+)(${escapeRegExp(decimalSeparator)})(\\d+)`, 'g'),
`$1$2${decimals}`
)
} else {
if (intlSuffix && !suffix) {
formatted = formatted.replace(intlSuffix, `${decimalSeparator}${decimals}${intlSuffix}`)
} else {
formatted = `${formatted}${decimalSeparator}${decimals}`
}
}
}
if (suffix && includeDecimalSeparator) {
return `${formatted}${includeDecimalSeparator}${suffix}`
}
if (intlSuffix && includeDecimalSeparator) {
return formatted.replace(intlSuffix, `${includeDecimalSeparator}${intlSuffix}`)
}
if (intlSuffix && suffix) {
return formatted.replace(intlSuffix, `${includeDecimalSeparator}${suffix}`)
}
return [formatted, includeDecimalSeparator, suffix].join('')
}
/**
* Before converting to Number, decimal separator has to be .
*/
const replaceDecimalSeparator = (
value: string,
decimalSeparator: FormatValueOptions['decimalSeparator'],
isNegative: boolean
): string => {
let newValue = value
if (decimalSeparator && decimalSeparator !== '.') {
newValue = newValue.replace(RegExp(escapeRegExp(decimalSeparator), 'g'), '.')
if (isNegative && decimalSeparator === '-') {
newValue = `-${newValue.slice(1)}`
}
}
return newValue
}
const replaceParts = (
parts: Intl.NumberFormatPart[],
{
prefix,
groupSeparator,
decimalSeparator,
decimalScale,
disableGroupSeparators = false
}: Pick<
FormatValueOptions,
'prefix' | 'groupSeparator' | 'decimalSeparator' | 'decimalScale' | 'disableGroupSeparators'
>
): string => {
return parts
.reduce(
(prev, { type, value }, i) => {
if (i === 0 && prefix) {
if (type === 'minusSign') {
return [value, prefix]
}
if (type === 'currency') {
return [...prev, prefix]
}
return [prefix, value]
}
if (type === 'currency') {
return prefix ? prev : [...prev, value]
}
if (type === 'group') {
return !disableGroupSeparators
? [...prev, groupSeparator !== undefined ? groupSeparator : value]
: prev
}
if (type === 'decimal') {
if (decimalScale !== undefined && decimalScale === 0) {
return prev
}
return [...prev, decimalSeparator !== undefined ? decimalSeparator : value]
}
if (type === 'fraction') {
return [...prev, decimalScale !== undefined ? value.slice(0, decimalScale) : value]
}
return [...prev, value]
},
['']
)
.join('')
}
/**
* Converts a formatted string with group and decimal separators into a float number.
*
* @param value - The formatted string input, e.g., "$ 1.234,56"
* @param decimalSeparator - The character used as decimal separator, e.g., ','
* @param groupSeparator - The character used as group/thousands separator, e.g., '.'
* @returns The parsed float value or null if input is invalid
*/
export function parseFormattedFloat (value: string | number): number {
// if (!value || typeof value !== 'string') return null
const options = {
decimalSeparator: ',',
groupSeparator: '.',
allowDecimals: true,
decimalsLimit: 20,
allowNegativeValue: true,
disableAbbreviations: false
}
const cleaned = cleanValue({ value: value as string, ...options })
const normalized = (typeof options.decimalSeparator === 'string' && options.decimalSeparator !== '')
? cleaned.replace(options.decimalSeparator, '.')
: cleaned
const num = parseFloat(normalized)
return isNaN(num) ? 0 : num
}