diff --git a/src/api.md b/src/api.md index 99fb7d03..9977dbe4 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]; }; ``` @@ -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 diff --git a/src/compute-engine/latex-syntax/serialize-number.ts b/src/compute-engine/latex-syntax/serialize-number.ts index e946b333..3b13bc74 100755 --- a/src/compute-engine/latex-syntax/serialize-number.ts +++ b/src/compute-engine/latex-syntax/serialize-number.ts @@ -120,15 +120,20 @@ 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, 3 ); - else if (options.notation === 'scientific') + } else if (options.notation === 'scientific') { + result = serializeScientificNotationNumber(num.toExponential(), { + ...options, + avoidExponentsInRange: null, // Scientific notation should always use exponents + }); + } else if (options.notation === 'adaptiveScientific') { result = serializeScientificNotationNumber(num.toExponential(), options); - + } return result ?? serializeAutoNotationNumber(num.toString(), options); } @@ -171,12 +176,21 @@ export function serializeNumber( else if (num[0] === '.') num = '0' + num; let result: string | undefined = undefined; - if (options.notation === 'engineering') + if (options.notation === 'engineering') { result = serializeScientificNotationNumber(num, options, 3); - else if (options.notation === 'scientific') + } else if (options.notation === 'scientific') { result = serializeScientificNotationNumber(num, options); + } else if (options.notation === 'adaptiveScientific') { + result = serializeAutoNotationNumber(num, options); + } - return sign + (result ?? serializeAutoNotationNumber(num, options)); + return ( + sign + + (result ?? + serializeAutoNotationNumber(num, { + ...options, + })) + ); } /** @@ -315,8 +329,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]; } @@ -338,11 +354,11 @@ function serializeAutoNotationNumber( 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 + // 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 [wholePart, fractionalPart] = toDecimalNumber( wholePart, 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 b21e2813..645e5433 100755 --- a/test/compute-engine/latex-syntax/numbers.test.ts +++ b/test/compute-engine/latex-syntax/numbers.test.ts @@ -381,6 +381,60 @@ describe('SERIALIZATION OF NUMBERS', () => { ); }); + test('scientific notation within avoidExponentsInRange', () => { + const result = ce.box(1 / 7000000).toLatex({ + notation: 'scientific', + }); + expect(result).toMatchInlineSnapshot( + `1.428\\,571\\,428\\,571\\,428\\,5\\cdot10^{-7}` + ); + }); + + 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', () => { + 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', () => { + 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( + `0.000\\,000\\,142\\,857\\,142\\,857\\,142\\,85` + ); + }); + + 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}` + ); + }); + test('Number with repeating pattern', () => { const format = (num: string, p: string) => ce.box({ num }).toLatex({