@yoroi/common
Version:
The Common package of Yoroi SDK
155 lines (148 loc) • 5.9 kB
JavaScript
;
import { BigNumber } from 'bignumber.js';
// Local helper to convert BigNumber to string (avoiding circular dependency)
export const asQuantity = value => {
const bn = new BigNumber(value);
if (bn.isNaN() || !bn.isFinite()) {
throw new Error('Invalid quantity');
}
return bn.toString(10);
};
/**
* Parses a number from text input with proper locale support and formatting.
* Handles keyboard-locale mismatch, intermediate states, and preserves formatting.
*
* @param options - Configuration options for parsing
* @returns Object containing display value and parsed quantity
*
* @example
* // With denomination (for tokens)
* parseNumberFromText({
* text: '123.45',
* denomination: 6,
* format: { decimalSeparator: '.' },
* precision: 6
* })
* // Returns: { formattedValue: '123.45', quantity: '123450000' }
*
* @example
* // Without denomination (for unitless numbers)
* parseNumberFromText({
* text: '123.45',
* format: { decimalSeparator: '.' }
* })
* // Returns: { formattedValue: '123.45', quantity: undefined }
*
* @example
* // Without format (formattedValue will be undefined)
* parseNumberFromText({
* text: '123.45',
* denomination: 6
* })
* // Returns: { formattedValue: undefined, quantity: '123450000' }
*/
export const parseNumberFromText = ({
text,
denomination,
format,
precision = denomination ?? 12
}) => {
// Use English locale (dot as decimal separator) for sanitization when format is not provided
const decimalSeparator = format?.decimalSeparator ?? BigNumber.config().FORMAT?.decimalSeparator ?? '.';
// Handle keyboard-locale mismatch FIRST
let normalizedText = text;
if (decimalSeparator === ',' && text.includes('.') && !text.includes(',')) {
// Locale uses comma, but user typed dot (English keyboard)
normalizedText = text.replace('.', ',');
} else if (decimalSeparator === '.' && text.includes(',') && !text.includes('.')) {
// Locale uses dot, but user typed comma (European keyboard)
normalizedText = text.replace(',', '.');
}
// Remove thousands separators (users don't type them, only copy/paste)
// Keep only digits and the correct decimal separator
const invalid = new RegExp(`[^0-9${decimalSeparator}]`, 'g');
let sanitizedInput = normalizedText === '' ? '' : normalizedText.replaceAll(invalid, '');
// Handle multiple decimal separators - keep only the first one
const decimalSeparatorCount = (sanitizedInput.match(new RegExp(`\\${decimalSeparator}`, 'g')) || []).length;
if (decimalSeparatorCount > 1) {
const firstDecimalIndex = sanitizedInput.indexOf(decimalSeparator);
sanitizedInput = sanitizedInput.substring(0, firstDecimalIndex + 1) + sanitizedInput.substring(firstDecimalIndex + 1).replaceAll(decimalSeparator, '');
}
if (sanitizedInput === '') return {
sanitizedInput,
formattedValue: format ? '' : undefined,
numericValue: 0,
quantity: denomination !== undefined ? '0' : undefined
};
let workingInput = sanitizedInput;
if (workingInput.startsWith(decimalSeparator)) {
// Prepend '0' to make it a valid number (e.g., '.45' -> '0.45')
workingInput = `0${workingInput}`;
}
const parts = workingInput.split(decimalSeparator);
let fullDecValue = workingInput;
let value = workingInput;
// Only format if format is provided
let formattedValue;
if (format) {
let fullDecFormat = new BigNumber(fullDecValue.replace(decimalSeparator, '.')).toFormat();
// Convert the formatted result back to use the correct decimal separator
formattedValue = fullDecFormat.replace('.', decimalSeparator);
}
if (parts.length <= 1) {
const bnValue = new BigNumber(value.replace(decimalSeparator, '.')).decimalPlaces(precision);
let quantity;
if (denomination !== undefined) {
const atomic = bnValue.shiftedBy(denomination);
const integerAtomic = atomic.integerValue(BigNumber.ROUND_DOWN);
quantity = asQuantity(integerAtomic);
}
const sanitizedInput = workingInput;
return {
sanitizedInput,
formattedValue: formattedValue?.replace(/[,|.]$/, ''),
numericValue: bnValue.toNumber(),
quantity
};
}
const [int, dec] = parts;
// trailing `1` is to allow the user to type `1.0` without losing the decimal part
if (dec === '') {
// No decimal digits, just format the integer part
fullDecValue = int ?? '';
value = int ?? '';
if (format) {
const fullDecFormat = new BigNumber(fullDecValue).toFormat();
formattedValue = fullDecFormat.replace('.', decimalSeparator);
}
} else {
fullDecValue = `${int}${decimalSeparator}${dec?.slice(0, precision)}1`;
value = `${int}${decimalSeparator}${dec?.slice(0, precision)}`;
if (format) {
const fullDecFormat = new BigNumber(fullDecValue.replace(decimalSeparator, '.')).toFormat();
// Remove trailing '1' and convert decimal separator back
formattedValue = fullDecFormat.slice(0, -1).replace('.', decimalSeparator);
}
}
// Create sanitized input that preserves decimal separator but limits decimal places
let finalSanitizedInput = workingInput;
if (parts.length > 1) {
const [intPart, decPart] = parts;
const limitedDecPart = decPart?.substring(0, precision) ?? '';
finalSanitizedInput = `${intPart ?? ''}${decimalSeparator}${limitedDecPart}`;
}
const bnValue = new BigNumber(value.replace(decimalSeparator, '.')).decimalPlaces(precision);
let quantity;
if (denomination !== undefined) {
const atomic = bnValue.shiftedBy(denomination);
const integerAtomic = atomic.integerValue(BigNumber.ROUND_DOWN);
quantity = asQuantity(integerAtomic);
}
return {
sanitizedInput: finalSanitizedInput,
formattedValue: formattedValue?.replace(/[,|.]$/, ''),
numericValue: bnValue.toNumber(),
quantity
};
};
//# sourceMappingURL=parse-number-from-text.js.map