diff --git a/lib/lexers/lexNumber.ts b/lib/lexers/lexNumber.ts index 2b04dae..b4490a6 100644 --- a/lib/lexers/lexNumber.ts +++ b/lib/lexers/lexNumber.ts @@ -6,34 +6,35 @@ const COLON = 58; // : function advDigits (str: string, pos: number): number { const start = pos; - do { + while (pos < str.length) { const c = str.charCodeAt(pos); if (c < 48 || c > 57) { // 0-9 break; } pos++; } - while (pos < str.length); return pos - start; } -// \d+(\.\d+)?(?:[eE][+-]?\d+)? +// (?:\d+(\.\d*)?|\.\d+)(?:[eE][+-]?\d+)? export function lexNumber (str: string, pos: number): Token | undefined { const start = pos; - // integer + // integer part, optional when there is a fraction part (.5) const lead = advDigits(str, pos); - if (!lead) { return; } pos += lead; - // optional fraction part + // fraction part, optional when there is an integer part (5.) const c0 = str.charCodeAt(pos); + let frac = 0; if (c0 === 46) { // . pos++; - const frac = advDigits(str, pos); - if (!frac) { return; } + frac = advDigits(str, pos); pos += frac; } + if (!frac && !lead) { + return; + } // optional exponent part const c1 = str.charCodeAt(pos); if (c1 === 69 || c1 === 101) { // E e diff --git a/lib/tokenize.spec.ts b/lib/tokenize.spec.ts index 7b24adb..bd825c3 100644 --- a/lib/tokenize.spec.ts +++ b/lib/tokenize.spec.ts @@ -511,6 +511,94 @@ describe('lexer', () => { ]); }); + test('decimals with no integer part', () => { + isTokens('=.5', [ + { type: FX_PREFIX, value: '=' }, + { type: NUMBER, value: '.5' } + ]); + isTokens('=A1*.95', [ + { type: FX_PREFIX, value: '=' }, + { type: REF_RANGE, value: 'A1' }, + { type: OPERATOR, value: '*' }, + { type: NUMBER, value: '.95' } + ]); + isTokens('=.5E-1', [ + { type: FX_PREFIX, value: '=' }, + { type: NUMBER, value: '.5E-1' } + ]); + isTokensNeg('=-.5', [ + { type: FX_PREFIX, value: '=' }, + { type: NUMBER, value: '-.5' } + ]); + // unary minus after a binary operator + isTokensNeg('=A1*-.95', [ + { type: FX_PREFIX, value: '=' }, + { type: REF_RANGE, value: 'A1' }, + { type: OPERATOR, value: '*' }, + { type: NUMBER, value: '-.95' } + ]); + isTokens('=A1*-.95', [ + { type: FX_PREFIX, value: '=' }, + { type: REF_RANGE, value: 'A1' }, + { type: OPERATOR, value: '*' }, + { type: OPERATOR, value: '-' }, + { type: NUMBER, value: '.95' } + ]); + // a lone period is not a number + isTokens('=.', [ + { type: FX_PREFIX, value: '=' }, + { type: UNKNOWN, value: '.' } + ]); + }); + + test('a decimal with no integer part still yields to a following : or !', () => { + // the tail guard applies to the new form as to any number + for (const expr of [ '=.5:A1', '=.5!A1' ]) { + expect(tokenize(expr).filter(t => t.type === NUMBER)).toEqual([]); + } + }); + + test('decimals with no fraction part', () => { + isTokens('=1.*2', [ + { type: FX_PREFIX, value: '=' }, + { type: NUMBER, value: '1.' }, + { type: OPERATOR, value: '*' }, + { type: NUMBER, value: '2' } + ]); + isTokens('=(1.)', [ + { type: FX_PREFIX, value: '=' }, + { type: OPERATOR, value: '(' }, + { type: NUMBER, value: '1.' }, + { type: OPERATOR, value: ')' } + ]); + isTokens('=1.E5', [ + { type: FX_PREFIX, value: '=' }, + { type: NUMBER, value: '1.E5' } + ]); + isTokens('=1.e-2', [ + { type: FX_PREFIX, value: '=' }, + { type: NUMBER, value: '1.e-2' } + ]); + // Excel reads `=1.` as 1, at the end of the input like anywhere else + isTokens('=1.', [ + { type: FX_PREFIX, value: '=' }, + { type: NUMBER, value: '1.' } + ]); + }); + + test('dangling exponents are not numbers', () => { + // Excel refuses `=1E` and `=1.5e+` at entry + isTokens('=1E', [ + { type: FX_PREFIX, value: '=' }, + { type: UNKNOWN, value: '1E' } + ]); + isTokens('=1.5e+', [ + { type: FX_PREFIX, value: '=' }, + { type: UNKNOWN, value: '1.5e' }, + { type: OPERATOR, value: '+' } + ]); + }); + test('scientific notation', () => { isTokens('=1E-1', [ { type: FX_PREFIX, value: '=' },