From 7648875f3ff7abb8fb7f2716d971feb8b5f194fc Mon Sep 17 00:00:00 2001 From: Peter Stenger Date: Wed, 7 Jan 2026 18:25:32 -0500 Subject: [PATCH 1/9] Fix normalization, add tests --- .../latex-syntax/serialize-number.ts | 24 +++++++++++-- .../latex-syntax/linear-algebra.test.ts | 2 -- .../latex-syntax/numbers.test.ts | 35 ++++++++++++++++++- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/src/compute-engine/latex-syntax/serialize-number.ts b/src/compute-engine/latex-syntax/serialize-number.ts index e946b333..bb06f3ab 100755 --- a/src/compute-engine/latex-syntax/serialize-number.ts +++ b/src/compute-engine/latex-syntax/serialize-number.ts @@ -227,9 +227,27 @@ function serializeScientificNotationNumber( if (!fraction) fraction = ''; while (whole.startsWith('0')) whole = whole.substring(1); if (!whole) { - // .123 -> 0.123e+0 - // .0123 -> 0.0123e+0 - valString = sign + '0.' + fraction + 'e+0'; + // Normalize: move first non-zero digit to whole part + // Count leading zeros in fraction to calculate negative exponent + let leadingZeros = 0; + while (leadingZeros < fraction.length && fraction[leadingZeros] === '0') + leadingZeros++; + + if (leadingZeros >= fraction.length) { + // All zeros - the number is 0 + valString = sign + '0e+0'; + } else { + // .0123 -> 1.23e-2 (leadingZeros=1, exponent = -(leadingZeros+1) = -2) + const firstNonZero = fraction[leadingZeros]; + const restOfFraction = fraction.slice(leadingZeros + 1); + const exponent = -(leadingZeros + 1); + valString = + sign + + firstNonZero + + (restOfFraction ? '.' + restOfFraction : '') + + 'e' + + exponent; + } } else { // 1.234 -> 1.234e+0 // 12.345 -> 1.2345e+1 diff --git a/test/compute-engine/latex-syntax/linear-algebra.test.ts b/test/compute-engine/latex-syntax/linear-algebra.test.ts index 58726759..2bcbe70f 100644 --- a/test/compute-engine/latex-syntax/linear-algebra.test.ts +++ b/test/compute-engine/latex-syntax/linear-algebra.test.ts @@ -3,8 +3,6 @@ import { engine as ce } from '../../utils'; const m4: Expression = ['List', ['List', 1, 2], ['List', 3, 4]]; -const v1 = ['Vector', 5, 7, 0, -1]; - describe('Parsing environments', () => { it('should parse a pmatrix', () => { const result = ce.parse('\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}'); diff --git a/test/compute-engine/latex-syntax/numbers.test.ts b/test/compute-engine/latex-syntax/numbers.test.ts index b21e2813..296c2ee1 100755 --- a/test/compute-engine/latex-syntax/numbers.test.ts +++ b/test/compute-engine/latex-syntax/numbers.test.ts @@ -1,4 +1,4 @@ -import { SerializeLatexOptions } from '../../../src/compute-engine/latex-syntax/public.ts'; +import { SerializeLatexOptions } from '../../../src/compute-engine/latex-syntax/types'; import { exprToString, engine as ce } from '../../utils'; function parse(s: string) { @@ -381,6 +381,39 @@ describe('SERIALIZATION OF NUMBERS', () => { ); }); + test('Scientific notation with string numbers', () => { + const formatBigNum = (num: string) => + ce.box({ num }).toLatex({ + notation: 'scientific', + avoidExponentsInRange: null, + exponentProduct: '\\times', + }); + + expect(formatBigNum('0.0123')).toMatchInlineSnapshot(`1.23\\times10^{-2}`); + expect(formatBigNum('0.00000014285714285714285')).toMatchInlineSnapshot( + `1.428\\,571\\,428\\,571\\,428\\,5\\times10^{-7}` + ); + + expect(formatBigNum('0.1')).toMatchInlineSnapshot(`10^{-1}`); + expect(formatBigNum('0.0001')).toMatchInlineSnapshot(`10^{-4}`); + expect(formatBigNum('0.00123')).toMatchInlineSnapshot(`1.23\\times10^{-3}`); + + expect(formatBigNum('14285714285714285')).toMatchInlineSnapshot( + `1.428\\,571\\,428\\,571\\,428\\,5\\times10^{16}` + ); + }); + + test('Normalized scientific notation', () => { + const result = ce.box(1 / 7000000).toLatex({ + notation: 'scientific', + avoidExponentsInRange: null, + exponentProduct: '\\times', + }); + expect(result).toMatchInlineSnapshot( + `1.428\\,571\\,428\\,571\\,428\\,5\\times10^{-7}` + ); + }); + test('Number with repeating pattern', () => { const format = (num: string, p: string) => ce.box({ num }).toLatex({ From 2fd7357d900d4e811c0a69410725218a23e835a7 Mon Sep 17 00:00:00 2001 From: Peter Stenger Date: Mon, 12 Jan 2026 17:38:46 -0500 Subject: [PATCH 2/9] Revert "Fix normalization, add tests" This reverts commit 7648875f3ff7abb8fb7f2716d971feb8b5f194fc. --- .../latex-syntax/serialize-number.ts | 24 ++----------- .../latex-syntax/linear-algebra.test.ts | 2 ++ .../latex-syntax/numbers.test.ts | 35 +------------------ 3 files changed, 6 insertions(+), 55 deletions(-) diff --git a/src/compute-engine/latex-syntax/serialize-number.ts b/src/compute-engine/latex-syntax/serialize-number.ts index bb06f3ab..e946b333 100755 --- a/src/compute-engine/latex-syntax/serialize-number.ts +++ b/src/compute-engine/latex-syntax/serialize-number.ts @@ -227,27 +227,9 @@ function serializeScientificNotationNumber( if (!fraction) fraction = ''; while (whole.startsWith('0')) whole = whole.substring(1); if (!whole) { - // Normalize: move first non-zero digit to whole part - // Count leading zeros in fraction to calculate negative exponent - let leadingZeros = 0; - while (leadingZeros < fraction.length && fraction[leadingZeros] === '0') - leadingZeros++; - - if (leadingZeros >= fraction.length) { - // All zeros - the number is 0 - valString = sign + '0e+0'; - } else { - // .0123 -> 1.23e-2 (leadingZeros=1, exponent = -(leadingZeros+1) = -2) - const firstNonZero = fraction[leadingZeros]; - const restOfFraction = fraction.slice(leadingZeros + 1); - const exponent = -(leadingZeros + 1); - valString = - sign + - firstNonZero + - (restOfFraction ? '.' + restOfFraction : '') + - 'e' + - exponent; - } + // .123 -> 0.123e+0 + // .0123 -> 0.0123e+0 + valString = sign + '0.' + fraction + 'e+0'; } else { // 1.234 -> 1.234e+0 // 12.345 -> 1.2345e+1 diff --git a/test/compute-engine/latex-syntax/linear-algebra.test.ts b/test/compute-engine/latex-syntax/linear-algebra.test.ts index 2bcbe70f..58726759 100644 --- a/test/compute-engine/latex-syntax/linear-algebra.test.ts +++ b/test/compute-engine/latex-syntax/linear-algebra.test.ts @@ -3,6 +3,8 @@ import { engine as ce } from '../../utils'; const m4: Expression = ['List', ['List', 1, 2], ['List', 3, 4]]; +const v1 = ['Vector', 5, 7, 0, -1]; + describe('Parsing environments', () => { it('should parse a pmatrix', () => { const result = ce.parse('\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}'); diff --git a/test/compute-engine/latex-syntax/numbers.test.ts b/test/compute-engine/latex-syntax/numbers.test.ts index 296c2ee1..b21e2813 100755 --- a/test/compute-engine/latex-syntax/numbers.test.ts +++ b/test/compute-engine/latex-syntax/numbers.test.ts @@ -1,4 +1,4 @@ -import { SerializeLatexOptions } from '../../../src/compute-engine/latex-syntax/types'; +import { SerializeLatexOptions } from '../../../src/compute-engine/latex-syntax/public.ts'; import { exprToString, engine as ce } from '../../utils'; function parse(s: string) { @@ -381,39 +381,6 @@ describe('SERIALIZATION OF NUMBERS', () => { ); }); - test('Scientific notation with string numbers', () => { - const formatBigNum = (num: string) => - ce.box({ num }).toLatex({ - notation: 'scientific', - avoidExponentsInRange: null, - exponentProduct: '\\times', - }); - - expect(formatBigNum('0.0123')).toMatchInlineSnapshot(`1.23\\times10^{-2}`); - expect(formatBigNum('0.00000014285714285714285')).toMatchInlineSnapshot( - `1.428\\,571\\,428\\,571\\,428\\,5\\times10^{-7}` - ); - - expect(formatBigNum('0.1')).toMatchInlineSnapshot(`10^{-1}`); - expect(formatBigNum('0.0001')).toMatchInlineSnapshot(`10^{-4}`); - expect(formatBigNum('0.00123')).toMatchInlineSnapshot(`1.23\\times10^{-3}`); - - expect(formatBigNum('14285714285714285')).toMatchInlineSnapshot( - `1.428\\,571\\,428\\,571\\,428\\,5\\times10^{16}` - ); - }); - - test('Normalized scientific notation', () => { - const result = ce.box(1 / 7000000).toLatex({ - notation: 'scientific', - avoidExponentsInRange: null, - exponentProduct: '\\times', - }); - expect(result).toMatchInlineSnapshot( - `1.428\\,571\\,428\\,571\\,428\\,5\\times10^{-7}` - ); - }); - test('Number with repeating pattern', () => { const format = (num: string, p: string) => ce.box({ num }).toLatex({ From 41e8986a489b8941886ec706c6b9b87d3d062ae7 Mon Sep 17 00:00:00 2001 From: Peter Stenger Date: Mon, 12 Jan 2026 17:48:33 -0500 Subject: [PATCH 3/9] Try fix again --- .../latex-syntax/serialize-number.ts | 31 ++++++------------- .../latex-syntax/numbers.test.ts | 22 +++++++++++++ 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/compute-engine/latex-syntax/serialize-number.ts b/src/compute-engine/latex-syntax/serialize-number.ts index e946b333..f849be9e 100755 --- a/src/compute-engine/latex-syntax/serialize-number.ts +++ b/src/compute-engine/latex-syntax/serialize-number.ts @@ -329,28 +329,17 @@ function serializeAutoNotationNumber( fractionalPart = m[2]; } - // If we have some fractional digits *and* an exponent, we need to - // adjust the whole part to include the fractional part. - // 1.23e4 -> 123e2 - if (exp !== 0 && fractionalPart) { - wholePart += fractionalPart; - exp -= fractionalPart.length; - fractionalPart = ''; - } - - // Check if the exponent is in a range to be avoided const avoid = options.avoidExponentsInRange; - if (exp !== 0 && avoid) { - if (exp >= avoid[0] && exp <= avoid[1]) { - // We want to avoid an exponent, so we'll padd the whole part - // with zeros and adjust the exponent - [wholePart, fractionalPart] = toDecimalNumber( - wholePart, - fractionalPart, - exp - ); - exp = 0; - } + + if (exp !== 0 && avoid && exp >= avoid[0] && exp <= avoid[1]) { + // We want to avoid an exponent, so we'll pad the whole part + // with zeros and adjust the exponent + [wholePart, fractionalPart] = toDecimalNumber( + wholePart, + fractionalPart, + exp + ); + exp = 0; } const exponent = formatExponent(exp.toString(), options); diff --git a/test/compute-engine/latex-syntax/numbers.test.ts b/test/compute-engine/latex-syntax/numbers.test.ts index b21e2813..06658590 100755 --- a/test/compute-engine/latex-syntax/numbers.test.ts +++ b/test/compute-engine/latex-syntax/numbers.test.ts @@ -381,6 +381,28 @@ describe('SERIALIZATION OF NUMBERS', () => { ); }); + test('auto notation within avoidExponentsInRange', () => { + // This is inside `avoidExponentsInRange`, so + // we expect a decimal number. + const result = ce.box(1 / 7000000).toLatex({ + notation: 'auto', + }); + expect(result).toMatchInlineSnapshot( + `0.000\\,000\\,142\\,857\\,142\\,857\\,142\\,85` + ); + }); + + test('auto notation outside avoidExponentsInRange', () => { + // This is outside `avoidExponentsInRange`, so + // we expect a number in normalized scientific notation. + const result = ce.box(1 / 70000000).toLatex({ + notation: 'auto', + }); + expect(result).toMatchInlineSnapshot( + `1.428\\,571\\,428\\,571\\,428\\,6\\cdot10^{-8}` + ); + }); + test('Number with repeating pattern', () => { const format = (num: string, p: string) => ce.box({ num }).toLatex({ From bc9ab82d892d81d65f438a8b571c98326784a6d4 Mon Sep 17 00:00:00 2001 From: Peter Stenger Date: Mon, 12 Jan 2026 17:49:43 -0500 Subject: [PATCH 4/9] diff --- .../latex-syntax/serialize-number.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/compute-engine/latex-syntax/serialize-number.ts b/src/compute-engine/latex-syntax/serialize-number.ts index f849be9e..670fb1be 100755 --- a/src/compute-engine/latex-syntax/serialize-number.ts +++ b/src/compute-engine/latex-syntax/serialize-number.ts @@ -331,15 +331,17 @@ function serializeAutoNotationNumber( const avoid = options.avoidExponentsInRange; - if (exp !== 0 && avoid && exp >= avoid[0] && exp <= avoid[1]) { - // We want to avoid an exponent, so we'll pad the whole part - // with zeros and adjust the exponent - [wholePart, fractionalPart] = toDecimalNumber( - wholePart, - fractionalPart, - exp - ); - exp = 0; + if (exp !== 0 && avoid) { + if (exp >= avoid[0] && exp <= avoid[1]) { + // We want to avoid an exponent, so we'll pad the whole part + // with zeros and adjust the exponent + [wholePart, fractionalPart] = toDecimalNumber( + wholePart, + fractionalPart, + exp + ); + exp = 0; + } } const exponent = formatExponent(exp.toString(), options); From 3d484546a8d8fcb010737cdd2ba02584e6b88233 Mon Sep 17 00:00:00 2001 From: Peter Stenger Date: Mon, 12 Jan 2026 17:50:00 -0500 Subject: [PATCH 5/9] diff --- src/compute-engine/latex-syntax/serialize-number.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/compute-engine/latex-syntax/serialize-number.ts b/src/compute-engine/latex-syntax/serialize-number.ts index 670fb1be..eef81748 100755 --- a/src/compute-engine/latex-syntax/serialize-number.ts +++ b/src/compute-engine/latex-syntax/serialize-number.ts @@ -330,7 +330,6 @@ function serializeAutoNotationNumber( } const avoid = options.avoidExponentsInRange; - if (exp !== 0 && avoid) { if (exp >= avoid[0] && exp <= avoid[1]) { // We want to avoid an exponent, so we'll pad the whole part From 817d9ffe8b19c6b6a5d12a89f4153648b339ed41 Mon Sep 17 00:00:00 2001 From: Peter Stenger Date: Tue, 13 Jan 2026 11:43:56 -0500 Subject: [PATCH 6/9] wip --- src/api.md | 2 +- .../latex-syntax/serialize-number.ts | 75 +++++++++++++++---- src/compute-engine/latex-syntax/types.ts | 2 +- .../latex-syntax/numbers.test.ts | 42 +++++++++-- 4 files changed, 101 insertions(+), 20 deletions(-) diff --git a/src/api.md b/src/api.md index 99fb7d03..da57a019 100644 --- a/src/api.md +++ b/src/api.md @@ -6686,7 +6686,7 @@ These options control how numbers are parsed and serialized. ```ts type NumberSerializationFormat = NumberFormat & { fractionalDigits: "auto" | "max" | number; - notation: "auto" | "engineering" | "scientific"; + notation: "auto" | "engineering" | "scientific" | "adaptiveScientific"; avoidExponentsInRange: undefined | null | [number, number]; }; ``` diff --git a/src/compute-engine/latex-syntax/serialize-number.ts b/src/compute-engine/latex-syntax/serialize-number.ts index eef81748..85c9c14a 100755 --- a/src/compute-engine/latex-syntax/serialize-number.ts +++ b/src/compute-engine/latex-syntax/serialize-number.ts @@ -120,16 +120,30 @@ export function serializeNumber( else if (Number.isNaN(num)) return options.notANumber; let result: string | undefined = undefined; - if (options.notation === 'engineering') + if (options.notation === 'engineering') { result = serializeScientificNotationNumber( num.toExponential(), - options, + { ...options, denormalizeScientificNotation: true }, 3 ); - else if (options.notation === 'scientific') - result = serializeScientificNotationNumber(num.toExponential(), options); - - return result ?? serializeAutoNotationNumber(num.toString(), options); + } else if (options.notation === 'scientific') { + result = serializeScientificNotationNumber(num.toExponential(), { + ...options, + denormalizeScientificNotation: false, + }); + } else if (options.notation === 'adaptiveScientific') { + result = serializeScientificNotationNumber(num.toExponential(), { + ...options, + denormalizeScientificNotation: false, + }); + } + return ( + result ?? + serializeAutoNotationNumber(num.toString(), { + ...options, + denormalizeScientificNotation: true, + }) + ); } num = num.toLowerCase().replace(/[\u0009-\u000d\u0020\u00a0]/g, ''); @@ -171,12 +185,32 @@ export function serializeNumber( else if (num[0] === '.') num = '0' + num; let result: string | undefined = undefined; - if (options.notation === 'engineering') - result = serializeScientificNotationNumber(num, options, 3); - else if (options.notation === 'scientific') - result = serializeScientificNotationNumber(num, options); + if (options.notation === 'engineering') { + result = serializeScientificNotationNumber( + num, + { ...options, denormalizeScientificNotation: false }, + 3 + ); + } else if (options.notation === 'scientific') { + result = serializeScientificNotationNumber(num, { + ...options, + denormalizeScientificNotation: false, + }); + } else if (options.notation === 'adaptiveScientific') { + result = serializeAutoNotationNumber(num, { + ...options, + denormalizeScientificNotation: false, + }); + } - return sign + (result ?? serializeAutoNotationNumber(num, options)); + return ( + sign + + (result ?? + serializeAutoNotationNumber(num, { + ...options, + denormalizeScientificNotation: true, + })) + ); } /** @@ -190,7 +224,9 @@ export function serializeNumber( */ function serializeScientificNotationNumber( valString: string, - options: NumberSerializationFormat, + options: NumberSerializationFormat & { + denormalizeScientificNotation: boolean; + }, expMultiple = 1 ): string | undefined { // For '7' returns '7e+0' @@ -306,7 +342,9 @@ function serializeScientificNotationNumber( function serializeAutoNotationNumber( valString: string, - options: NumberSerializationFormat + options: NumberSerializationFormat & { + denormalizeScientificNotation: boolean; + } ): string { let m = valString.match(/^(.*)[e|E]([-+]?[0-9]+)$/i); // if valString === '-1234567.89e-123' @@ -329,17 +367,28 @@ function serializeAutoNotationNumber( fractionalPart = m[2]; } + // If we have some fractional digits *and* an exponent, we need to + // adjust the whole part to include the fractional part. + // 1.23e4 -> 123e2 + if (exp !== 0 && fractionalPart) { + wholePart += fractionalPart; + exp -= fractionalPart.length; + fractionalPart = ''; + } + const avoid = options.avoidExponentsInRange; if (exp !== 0 && avoid) { if (exp >= avoid[0] && exp <= avoid[1]) { // We want to avoid an exponent, so we'll pad the whole part // with zeros and adjust the exponent + console.log('avoid b4', wholePart, fractionalPart, exp); [wholePart, fractionalPart] = toDecimalNumber( wholePart, fractionalPart, exp ); exp = 0; + console.log('avoid', wholePart, fractionalPart, exp); } } diff --git a/src/compute-engine/latex-syntax/types.ts b/src/compute-engine/latex-syntax/types.ts index efde767b..c33f53ee 100755 --- a/src/compute-engine/latex-syntax/types.ts +++ b/src/compute-engine/latex-syntax/types.ts @@ -670,7 +670,7 @@ export type NumberSerializationFormat = NumberFormat & { * Default: `"auto"` */ fractionalDigits: 'auto' | 'max' | number; - notation: 'auto' | 'engineering' | 'scientific'; // @todo: add | 'percent' + notation: 'auto' | 'engineering' | 'scientific' | 'adaptiveScientific'; // @todo: add | 'percent' avoidExponentsInRange: | undefined | null diff --git a/test/compute-engine/latex-syntax/numbers.test.ts b/test/compute-engine/latex-syntax/numbers.test.ts index 06658590..1a147792 100755 --- a/test/compute-engine/latex-syntax/numbers.test.ts +++ b/test/compute-engine/latex-syntax/numbers.test.ts @@ -381,23 +381,55 @@ describe('SERIALIZATION OF NUMBERS', () => { ); }); + test('scientific notation within avoidExponentsInRange', () => { + const result = ce.box(1 / 7000000).toLatex({ + notation: 'scientific', + }); + expect(result).toMatchInlineSnapshot( + `14\\,285\\,714\\,285\\,714\\,285\\cdot10^{-23}` + ); + }); + + test('scientific notation outside avoidExponentsInRange', () => { + const result = ce.box(1 / 70000000).toLatex({ + notation: 'scientific', + }); + expect(result).toMatchInlineSnapshot( + `1.428\\,571\\,428\\,571\\,428\\,6\\cdot10^{-8}` + ); + }); + test('auto notation within avoidExponentsInRange', () => { - // This is inside `avoidExponentsInRange`, so - // we expect a decimal number. const result = ce.box(1 / 7000000).toLatex({ notation: 'auto', }); expect(result).toMatchInlineSnapshot( - `0.000\\,000\\,142\\,857\\,142\\,857\\,142\\,85` + `14\\,285\\,714\\,285\\,714\\,285\\cdot10^{-23}` ); }); test('auto notation outside avoidExponentsInRange', () => { - // This is outside `avoidExponentsInRange`, so - // we expect a number in normalized scientific notation. const result = ce.box(1 / 70000000).toLatex({ notation: 'auto', }); + expect(result).toMatchInlineSnapshot( + `14\\,285\\,714\\,285\\,714\\,286\\cdot10^{-24}` + ); + }); + + test('adaptiveScientific notation within avoidExponentsInRange', () => { + const result = ce.box(1 / 7000000).toLatex({ + notation: 'adaptiveScientific', + }); + expect(result).toMatchInlineSnapshot( + `14\\,285\\,714\\,285\\,714\\,285\\cdot10^{-23}` + ); + }); + + test('adaptiveScientific notation outside avoidExponentsInRange', () => { + const result = ce.box(1 / 70000000).toLatex({ + notation: 'adaptiveScientific', + }); expect(result).toMatchInlineSnapshot( `1.428\\,571\\,428\\,571\\,428\\,6\\cdot10^{-8}` ); From 50c21c81fcd854ea8b4b73935ee4e24445fdc17e Mon Sep 17 00:00:00 2001 From: Peter Stenger Date: Tue, 13 Jan 2026 11:55:59 -0500 Subject: [PATCH 7/9] update behavior --- src/compute-engine/latex-syntax/serialize-number.ts | 11 +++++++---- test/compute-engine/latex-syntax/numbers.test.ts | 6 +++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/compute-engine/latex-syntax/serialize-number.ts b/src/compute-engine/latex-syntax/serialize-number.ts index 85c9c14a..b799c29a 100755 --- a/src/compute-engine/latex-syntax/serialize-number.ts +++ b/src/compute-engine/latex-syntax/serialize-number.ts @@ -129,6 +129,7 @@ export function serializeNumber( } else if (options.notation === 'scientific') { result = serializeScientificNotationNumber(num.toExponential(), { ...options, + avoidExponentsInRange: null, // Scientific notation should always use exponents denormalizeScientificNotation: false, }); } else if (options.notation === 'adaptiveScientific') { @@ -188,12 +189,13 @@ export function serializeNumber( if (options.notation === 'engineering') { result = serializeScientificNotationNumber( num, - { ...options, denormalizeScientificNotation: false }, + { ...options, denormalizeScientificNotation: true }, 3 ); } else if (options.notation === 'scientific') { result = serializeScientificNotationNumber(num, { ...options, + avoidExponentsInRange: null, // Scientific notation should always use exponents denormalizeScientificNotation: false, }); } else if (options.notation === 'adaptiveScientific') { @@ -353,8 +355,10 @@ function serializeAutoNotationNumber( // Is there is an exponent... let exp = 0; + let originalExp = 0; // Save the original exponent for avoid range check if (m?.[1] && m[2]) { exp = parseInt(m[2]); + originalExp = exp; valString = m[1]; } @@ -378,17 +382,16 @@ function serializeAutoNotationNumber( const avoid = options.avoidExponentsInRange; if (exp !== 0 && avoid) { - if (exp >= avoid[0] && exp <= avoid[1]) { + // Use the original exponent (before fractional part adjustment) for the avoid check + if (originalExp >= avoid[0] && originalExp <= avoid[1]) { // We want to avoid an exponent, so we'll pad the whole part // with zeros and adjust the exponent - console.log('avoid b4', wholePart, fractionalPart, exp); [wholePart, fractionalPart] = toDecimalNumber( wholePart, fractionalPart, exp ); exp = 0; - console.log('avoid', wholePart, fractionalPart, exp); } } diff --git a/test/compute-engine/latex-syntax/numbers.test.ts b/test/compute-engine/latex-syntax/numbers.test.ts index 1a147792..645e5433 100755 --- a/test/compute-engine/latex-syntax/numbers.test.ts +++ b/test/compute-engine/latex-syntax/numbers.test.ts @@ -386,7 +386,7 @@ describe('SERIALIZATION OF NUMBERS', () => { notation: 'scientific', }); expect(result).toMatchInlineSnapshot( - `14\\,285\\,714\\,285\\,714\\,285\\cdot10^{-23}` + `1.428\\,571\\,428\\,571\\,428\\,5\\cdot10^{-7}` ); }); @@ -404,7 +404,7 @@ describe('SERIALIZATION OF NUMBERS', () => { notation: 'auto', }); expect(result).toMatchInlineSnapshot( - `14\\,285\\,714\\,285\\,714\\,285\\cdot10^{-23}` + `0.000\\,000\\,142\\,857\\,142\\,857\\,142\\,85` ); }); @@ -422,7 +422,7 @@ describe('SERIALIZATION OF NUMBERS', () => { notation: 'adaptiveScientific', }); expect(result).toMatchInlineSnapshot( - `14\\,285\\,714\\,285\\,714\\,285\\cdot10^{-23}` + `0.000\\,000\\,142\\,857\\,142\\,857\\,142\\,85` ); }); From 587a055d372ee0f1621cca4c39db1cdd4125a67d Mon Sep 17 00:00:00 2001 From: Peter Stenger Date: Tue, 13 Jan 2026 12:05:43 -0500 Subject: [PATCH 8/9] add section about notation --- src/api.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/api.md b/src/api.md index da57a019..9977dbe4 100644 --- a/src/api.md +++ b/src/api.md @@ -6703,6 +6703,45 @@ The maximum number of significant digits in serialized numbers. Default: `"auto"` +#### NumberSerializationFormat.notation + +```ts +notation: "auto" | "engineering" | "scientific" | "adaptiveScientific"; +``` + +Controls how numbers with exponents are formatted: + +- `"auto"`: Display as decimal when possible, use exponent notation otherwise +- `"scientific"`: Always use normalized scientific notation (mantissa between 1 and 10) +- `"engineering"`: Use engineering notation (exponent is a multiple of 3) +- `"adaptiveScientific"`: Like `"auto"` within the avoid range, but use normalized scientific notation outside + +Default: `"auto"` + +#### NumberSerializationFormat.avoidExponentsInRange + +```ts +avoidExponentsInRange: undefined | null | [number, number]; +``` + +Specifies a range of exponents where decimal notation is preferred over exponent notation. +For example, `[-7, 20]` means exponents from -7 to 20 will be displayed as decimals when possible. + +Default: `[-7, 20]` + +#### Notation Behavior Summary + +The table below shows how each notation mode behaves when the exponent is inside or outside the `avoidExponentsInRange`: + +| Notation | Exponent in Avoid Range | Exponent Outside Avoid Range | +|----------|-------------------------|------------------------------| +| `"auto"` | Decimal (e.g., `0.000000142857`) | Exponent, not normalized (e.g., `14285714×10^{-24}`) | +| `"scientific"` | Scientific notation (e.g., `1.428×10^{-7}`) | Scientific notation (e.g., `1.428×10^{-8}`) | +| `"engineering"` | Decimal or engineering | Engineering notation (exponent multiple of 3) | +| `"adaptiveScientific"` | Decimal (e.g., `0.000000142857`) | Scientific notation (e.g., `1.428×10^{-8}`) | + +Note: `"scientific"` notation ignores `avoidExponentsInRange` and always produces normalized scientific notation. + ## Tensors From 38b2308264202da7e9eaba6cebbfa41b55b161a2 Mon Sep 17 00:00:00 2001 From: Peter Stenger Date: Tue, 13 Jan 2026 12:11:03 -0500 Subject: [PATCH 9/9] cleanup --- .../latex-syntax/serialize-number.ts | 42 ++++--------------- 1 file changed, 8 insertions(+), 34 deletions(-) diff --git a/src/compute-engine/latex-syntax/serialize-number.ts b/src/compute-engine/latex-syntax/serialize-number.ts index b799c29a..3b13bc74 100755 --- a/src/compute-engine/latex-syntax/serialize-number.ts +++ b/src/compute-engine/latex-syntax/serialize-number.ts @@ -123,28 +123,18 @@ export function serializeNumber( if (options.notation === 'engineering') { result = serializeScientificNotationNumber( num.toExponential(), - { ...options, denormalizeScientificNotation: true }, + options, 3 ); } else if (options.notation === 'scientific') { result = serializeScientificNotationNumber(num.toExponential(), { ...options, avoidExponentsInRange: null, // Scientific notation should always use exponents - denormalizeScientificNotation: false, }); } else if (options.notation === 'adaptiveScientific') { - result = serializeScientificNotationNumber(num.toExponential(), { - ...options, - denormalizeScientificNotation: false, - }); + result = serializeScientificNotationNumber(num.toExponential(), options); } - return ( - result ?? - serializeAutoNotationNumber(num.toString(), { - ...options, - denormalizeScientificNotation: true, - }) - ); + return result ?? serializeAutoNotationNumber(num.toString(), options); } num = num.toLowerCase().replace(/[\u0009-\u000d\u0020\u00a0]/g, ''); @@ -187,22 +177,11 @@ export function serializeNumber( let result: string | undefined = undefined; if (options.notation === 'engineering') { - result = serializeScientificNotationNumber( - num, - { ...options, denormalizeScientificNotation: true }, - 3 - ); + result = serializeScientificNotationNumber(num, options, 3); } else if (options.notation === 'scientific') { - result = serializeScientificNotationNumber(num, { - ...options, - avoidExponentsInRange: null, // Scientific notation should always use exponents - denormalizeScientificNotation: false, - }); + result = serializeScientificNotationNumber(num, options); } else if (options.notation === 'adaptiveScientific') { - result = serializeAutoNotationNumber(num, { - ...options, - denormalizeScientificNotation: false, - }); + result = serializeAutoNotationNumber(num, options); } return ( @@ -210,7 +189,6 @@ export function serializeNumber( (result ?? serializeAutoNotationNumber(num, { ...options, - denormalizeScientificNotation: true, })) ); } @@ -226,9 +204,7 @@ export function serializeNumber( */ function serializeScientificNotationNumber( valString: string, - options: NumberSerializationFormat & { - denormalizeScientificNotation: boolean; - }, + options: NumberSerializationFormat, expMultiple = 1 ): string | undefined { // For '7' returns '7e+0' @@ -344,9 +320,7 @@ function serializeScientificNotationNumber( function serializeAutoNotationNumber( valString: string, - options: NumberSerializationFormat & { - denormalizeScientificNotation: boolean; - } + options: NumberSerializationFormat ): string { let m = valString.match(/^(.*)[e|E]([-+]?[0-9]+)$/i); // if valString === '-1234567.89e-123'