Browse Source

Add bailout for conversion to bigint

There is only a certain string length allowed to convert parsed numbers to bigint.
master
TheoryOfNekomata 1 year ago
parent
commit
545d0a54aa
5 changed files with 126 additions and 18 deletions
  1. +48
    -5
      packages/core/src/converter.ts
  2. +50
    -9
      packages/core/src/exponent.ts
  3. +1
    -0
      packages/core/src/systems/en-US/parse.ts
  4. +5
    -1
      packages/core/test/exponent.test.ts
  5. +22
    -3
      packages/example-web/index.html

+ 48
- 5
packages/core/src/converter.ts View File

@@ -1,12 +1,17 @@
import { enUS } from './systems'; import { enUS } from './systems';
import { StringifySystem } from './common'; import { StringifySystem } from './common';
import { exponentialToNumberString } from './exponent';
import { exponentialToNumberString, extractExponentialComponents, numberToExponential } from './exponent';


/** /**
* Negative symbol. * Negative symbol.
*/ */
const NEGATIVE_SYMBOL = '-' as const; const NEGATIVE_SYMBOL = '-' as const;


/**
* Exponent delimiter.
*/
const EXPONENT_DELIMITER = 'e' as const;

/** /**
* Allowed value type for {@link stringify}. * Allowed value type for {@link stringify}.
*/ */
@@ -104,12 +109,50 @@ export const parse = (value: string, options = {} as ParseOptions) => {
const stringValue = system.combineGroups(groups); const stringValue = system.combineGroups(groups);


switch (type) { switch (type) {
case 'number':
case 'number': {
// Precision might be lost here. Use bigint when not using fractional parts. // Precision might be lost here. Use bigint when not using fractional parts.
if (stringValue.includes(EXPONENT_DELIMITER)) {
const { exponent, integer } = extractExponentialComponents(stringValue);
const exponentValue = Number(exponent);
const integerValue = Number(integer);

const [maxSafeIntegerSignificand, maxSafeIntegerExponent] = Number.MAX_SAFE_INTEGER.toExponential().split('e');
if (
exponentValue >= Number(maxSafeIntegerExponent)
&& integerValue >= Math.floor(Number(maxSafeIntegerSignificand))
) {
// greater than Number.MAX_SAFE_INTEGER
const logger = console;
logger.warn(`Value too large to be produced as number: ${value}`);
logger.warn('Falling back to string...');
return stringValue;
}

const [epsilonSignificand, epsilonExponent] = Number.EPSILON.toExponential().split('e');
if (
exponentValue <= Number(epsilonExponent)
&& integerValue <= Math.floor(Number(epsilonSignificand))
) {
// smaller than Number.EPSILON
const logger = console;
logger.warn(`Value too small to be produced as number: ${value}`);
logger.warn('Falling back to string...');
return stringValue;
}
}
return Number(stringValue); return Number(stringValue);
case 'bigint':
return BigInt(exponentialToNumberString(stringValue));
default:
} case 'bigint': {
const normalizedNumberString = exponentialToNumberString(numberToExponential(stringValue));
try {
return BigInt(normalizedNumberString);
} catch {
const logger = console;
logger.warn(`Value too long to be produced as bigint: ${value}`);
logger.warn('Falling back to string...');
}

return stringValue;
} default:
break; break;
} }




+ 50
- 9
packages/core/src/exponent.ts View File

@@ -3,10 +3,7 @@
*/ */
export type ValidValue = string | number | bigint; export type ValidValue = string | number | bigint;


/**
* Options to use when converting a number to exponential notation.
*/
export interface NumberToExponentialOptions {
interface BaseOptions {
/** /**
* The decimal point character to use. Defaults to ".". * The decimal point character to use. Defaults to ".".
*/ */
@@ -21,6 +18,21 @@ export interface NumberToExponentialOptions {
exponentDelimiter?: string; exponentDelimiter?: string;
} }


/**
* Options to use when converting a number to exponential notation.
*/
export type NumberToExponentialOptions = BaseOptions;

/**
* Options to use when converting exponential notation to a number string.
*/
export interface ExponentialToNumberStringOptions extends BaseOptions {
/**
* The maximum length to represent the return value else the input will be preserved.
*/
maxLength?: number;
}

/** /**
* Default decimal point character. * Default decimal point character.
*/ */
@@ -100,9 +112,9 @@ interface ExponentialComponents {
* @param options - Options to use when extracting components. * @param options - Options to use when extracting components.
* @returns ExponentialComponents The extracted components. * @returns ExponentialComponents The extracted components.
*/ */
const extractExponentialComponents = (
export const extractExponentialComponents = (
value: string, value: string,
options = {} as NumberToExponentialOptions,
options = {} as BaseOptions,
): ExponentialComponents => { ): ExponentialComponents => {
const { const {
decimalPoint = DEFAULT_DECIMAL_POINT, decimalPoint = DEFAULT_DECIMAL_POINT,
@@ -206,18 +218,47 @@ export const numberToExponential = (


export const exponentialToNumberString = ( export const exponentialToNumberString = (
exp: string, exp: string,
options = {} as NumberToExponentialOptions,
options = {} as ExponentialToNumberStringOptions,
) => { ) => {
const { decimalPoint = DEFAULT_DECIMAL_POINT } = options;
const {
decimalPoint = DEFAULT_DECIMAL_POINT,
maxLength = Number.MAX_SAFE_INTEGER,
} = options;
const { integer, fractional, exponent } = extractExponentialComponents(exp, options); const { integer, fractional, exponent } = extractExponentialComponents(exp, options);
const currentDecimalPointIndex = integer.length; const currentDecimalPointIndex = integer.length;
const newDecimalPointIndex = currentDecimalPointIndex + Number(exponent); const newDecimalPointIndex = currentDecimalPointIndex + Number(exponent);
if (newDecimalPointIndex > maxLength) {
// Integer part overflow
//
// Important to bail out as early as possible.
throw new RangeError('Could not represent number in string form.');
}
const fractionalDigits = fractional.length > 0 ? fractional : '0'; const fractionalDigits = fractional.length > 0 ? fractional : '0';
const significantDigits = `${integer}${fractionalDigits}`; const significantDigits = `${integer}${fractionalDigits}`;
const newInteger = significantDigits.slice(0, newDecimalPointIndex).padEnd(newDecimalPointIndex, '0');
if (significantDigits.length > maxLength) {
// Digits overflow
//
// Should we choose to truncate instead of throw an exception?
// Quite tricky when only the fractional part overflows.
throw new RangeError('Could not represent number in string form.');
}
let newInteger: string;
try {
newInteger = significantDigits.slice(0, newDecimalPointIndex)
.padEnd(newDecimalPointIndex, '0');
} catch {
// Error in padEnd(), shadow it.
throw new RangeError('Could not represent number in string form.');
}
const newFractional = significantDigits.slice(newDecimalPointIndex).replace(/0+$/g, ''); const newFractional = significantDigits.slice(newDecimalPointIndex).replace(/0+$/g, '');
if (newFractional.length === 0) { if (newFractional.length === 0) {
return newInteger; return newInteger;
} }
if (newInteger.length + decimalPoint.length + newFractional.length > maxLength) {
// Fractional part overflow
//
// Final length check.
throw new RangeError('Could not represent number in string form.');
}
return `${newInteger}${decimalPoint}${newFractional}`; return `${newInteger}${decimalPoint}${newFractional}`;
}; };

+ 1
- 0
packages/core/src/systems/en-US/parse.ts View File

@@ -121,6 +121,7 @@ const doParseGroupName = (result: DoParseState): DoParseState => {


if (result.groupNameCurrent.startsWith(`${MILLIA_PREFIX}${SHORT_MILLIA_DELIMITER}`)) { if (result.groupNameCurrent.startsWith(`${MILLIA_PREFIX}${SHORT_MILLIA_DELIMITER}`)) {
// short millia // short millia
// FIXME
const matchedMilliaArray = result.groupNameCurrent.match(/^\d+/); const matchedMilliaArray = result.groupNameCurrent.match(/^\d+/);
if (!matchedMilliaArray) { if (!matchedMilliaArray) {
throw new InvalidTokenError(result.groupNameCurrent); throw new InvalidTokenError(result.groupNameCurrent);


+ 5
- 1
packages/core/test/exponent.test.ts View File

@@ -54,9 +54,13 @@ describe('numberToExponential', () => {
it('converts "1e+100" to 1e+100', () => { it('converts "1e+100" to 1e+100', () => {
expect(numberToExponential('1e+100')).toBe('1e+100'); expect(numberToExponential('1e+100')).toBe('1e+100');
}); });

it('converts "12.3e+10" to 1.23e+11', () => {
expect(numberToExponential('12.3e+10')).toBe('1.23e+11');
});
}); });


describe.only('exponentialToNumberString', () => {
describe('exponentialToNumberString', () => {
it('converts 1e+0 to 1', () => { it('converts 1e+0 to 1', () => {
expect(exponentialToNumberString('1e+0')).toBe('1'); expect(exponentialToNumberString('1e+0')).toBe('1');
}); });


+ 22
- 3
packages/example-web/index.html View File

@@ -50,6 +50,10 @@
box-sizing: border-box; box-sizing: border-box;
} }


.main-form > div > textarea:invalid {
color: red;
}

.checkbox > input { .checkbox > input {
position: absolute; position: absolute;
left: -999999px; left: -999999px;
@@ -97,7 +101,10 @@
</label><label class="checkbox"> </label><label class="checkbox">
<input type="checkbox" name="addTensDashes" checked> <input type="checkbox" name="addTensDashes" checked>
<span>Add dashes to tens</span> <span>Add dashes to tens</span>
</label>
</label><!--<label class="checkbox">
<input type="groupDigits" name="groupDigits" checked>
<span>Group digits</span>
</label>-->
</div> </div>
</fieldset> </fieldset>
<div> <div>
@@ -127,19 +134,31 @@
}; };


numberInput.addEventListener('input', (e) => { numberInput.addEventListener('input', (e) => {
e.currentTarget.setCustomValidity('');
nameInput.value = ''; nameInput.value = '';
if (e.currentTarget.value.trim() === '') { if (e.currentTarget.value.trim() === '') {
return; return;
} }
nameInput.value = stringify(e.currentTarget.value, options.stringify);
try {
nameInput.value = stringify(e.currentTarget.value, options.stringify);
} catch {
e.currentTarget.setCustomValidity('Invalid number.');
}
}); });


nameInput.addEventListener('input', (e) => { nameInput.addEventListener('input', (e) => {
e.currentTarget.setCustomValidity('');
numberInput.value = ''; numberInput.value = '';
if (e.currentTarget.value.trim() === '') { if (e.currentTarget.value.trim() === '') {
return; return;
} }
numberInput.value = parse(e.currentTarget.value, options.parse).toString();
try {
numberInput.value = parse(e.currentTarget.value, options.parse)
.toString();
// TODO group digits function from system.
} catch {
e.currentTarget.setCustomValidity('Invalid name.');
}
}); });


addTensDashesCheckbox.addEventListener('change', (e) => { addTensDashesCheckbox.addEventListener('change', (e) => {


Loading…
Cancel
Save