Skip to content

Commit 519bb4e

Browse files
authored
Merge pull request #268 from reteps/normalized-scientific-notation-fix
feat: add `adaptiveScientific` mode
2 parents 690a56e + 38b2308 commit 519bb4e

4 files changed

Lines changed: 120 additions & 11 deletions

File tree

src/api.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6686,7 +6686,7 @@ These options control how numbers are parsed and serialized.
66866686
```ts
66876687
type NumberSerializationFormat = NumberFormat & {
66886688
fractionalDigits: "auto" | "max" | number;
6689-
notation: "auto" | "engineering" | "scientific";
6689+
notation: "auto" | "engineering" | "scientific" | "adaptiveScientific";
66906690
avoidExponentsInRange: undefined | null | [number, number];
66916691
};
66926692
```
@@ -6703,6 +6703,45 @@ The maximum number of significant digits in serialized numbers.
67036703
67046704
Default: `"auto"`
67056705
6706+
#### NumberSerializationFormat.notation
6707+
6708+
```ts
6709+
notation: "auto" | "engineering" | "scientific" | "adaptiveScientific";
6710+
```
6711+
6712+
Controls how numbers with exponents are formatted:
6713+
6714+
- `"auto"`: Display as decimal when possible, use exponent notation otherwise
6715+
- `"scientific"`: Always use normalized scientific notation (mantissa between 1 and 10)
6716+
- `"engineering"`: Use engineering notation (exponent is a multiple of 3)
6717+
- `"adaptiveScientific"`: Like `"auto"` within the avoid range, but use normalized scientific notation outside
6718+
6719+
Default: `"auto"`
6720+
6721+
#### NumberSerializationFormat.avoidExponentsInRange
6722+
6723+
```ts
6724+
avoidExponentsInRange: undefined | null | [number, number];
6725+
```
6726+
6727+
Specifies a range of exponents where decimal notation is preferred over exponent notation.
6728+
For example, `[-7, 20]` means exponents from -7 to 20 will be displayed as decimals when possible.
6729+
6730+
Default: `[-7, 20]`
6731+
6732+
#### Notation Behavior Summary
6733+
6734+
The table below shows how each notation mode behaves when the exponent is inside or outside the `avoidExponentsInRange`:
6735+
6736+
| Notation | Exponent in Avoid Range | Exponent Outside Avoid Range |
6737+
|----------|-------------------------|------------------------------|
6738+
| `"auto"` | Decimal (e.g., `0.000000142857`) | Exponent, not normalized (e.g., `14285714×10^{-24}`) |
6739+
| `"scientific"` | Scientific notation (e.g., `1.428×10^{-7}`) | Scientific notation (e.g., `1.428×10^{-8}`) |
6740+
| `"engineering"` | Decimal or engineering | Engineering notation (exponent multiple of 3) |
6741+
| `"adaptiveScientific"` | Decimal (e.g., `0.000000142857`) | Scientific notation (e.g., `1.428×10^{-8}`) |
6742+
6743+
Note: `"scientific"` notation ignores `avoidExponentsInRange` and always produces normalized scientific notation.
6744+
67066745
</MemberCard>
67076746
67086747
## Tensors

src/compute-engine/latex-syntax/serialize-number.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -120,15 +120,20 @@ export function serializeNumber(
120120
else if (Number.isNaN(num)) return options.notANumber;
121121

122122
let result: string | undefined = undefined;
123-
if (options.notation === 'engineering')
123+
if (options.notation === 'engineering') {
124124
result = serializeScientificNotationNumber(
125125
num.toExponential(),
126126
options,
127127
3
128128
);
129-
else if (options.notation === 'scientific')
129+
} else if (options.notation === 'scientific') {
130+
result = serializeScientificNotationNumber(num.toExponential(), {
131+
...options,
132+
avoidExponentsInRange: null, // Scientific notation should always use exponents
133+
});
134+
} else if (options.notation === 'adaptiveScientific') {
130135
result = serializeScientificNotationNumber(num.toExponential(), options);
131-
136+
}
132137
return result ?? serializeAutoNotationNumber(num.toString(), options);
133138
}
134139

@@ -171,12 +176,21 @@ export function serializeNumber(
171176
else if (num[0] === '.') num = '0' + num;
172177

173178
let result: string | undefined = undefined;
174-
if (options.notation === 'engineering')
179+
if (options.notation === 'engineering') {
175180
result = serializeScientificNotationNumber(num, options, 3);
176-
else if (options.notation === 'scientific')
181+
} else if (options.notation === 'scientific') {
177182
result = serializeScientificNotationNumber(num, options);
183+
} else if (options.notation === 'adaptiveScientific') {
184+
result = serializeAutoNotationNumber(num, options);
185+
}
178186

179-
return sign + (result ?? serializeAutoNotationNumber(num, options));
187+
return (
188+
sign +
189+
(result ??
190+
serializeAutoNotationNumber(num, {
191+
...options,
192+
}))
193+
);
180194
}
181195

182196
/**
@@ -315,8 +329,10 @@ function serializeAutoNotationNumber(
315329

316330
// Is there is an exponent...
317331
let exp = 0;
332+
let originalExp = 0; // Save the original exponent for avoid range check
318333
if (m?.[1] && m[2]) {
319334
exp = parseInt(m[2]);
335+
originalExp = exp;
320336
valString = m[1];
321337
}
322338

@@ -338,11 +354,11 @@ function serializeAutoNotationNumber(
338354
fractionalPart = '';
339355
}
340356

341-
// Check if the exponent is in a range to be avoided
342357
const avoid = options.avoidExponentsInRange;
343358
if (exp !== 0 && avoid) {
344-
if (exp >= avoid[0] && exp <= avoid[1]) {
345-
// We want to avoid an exponent, so we'll padd the whole part
359+
// Use the original exponent (before fractional part adjustment) for the avoid check
360+
if (originalExp >= avoid[0] && originalExp <= avoid[1]) {
361+
// We want to avoid an exponent, so we'll pad the whole part
346362
// with zeros and adjust the exponent
347363
[wholePart, fractionalPart] = toDecimalNumber(
348364
wholePart,

src/compute-engine/latex-syntax/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -670,7 +670,7 @@ export type NumberSerializationFormat = NumberFormat & {
670670
* Default: `"auto"`
671671
*/
672672
fractionalDigits: 'auto' | 'max' | number;
673-
notation: 'auto' | 'engineering' | 'scientific'; // @todo: add | 'percent'
673+
notation: 'auto' | 'engineering' | 'scientific' | 'adaptiveScientific'; // @todo: add | 'percent'
674674
avoidExponentsInRange:
675675
| undefined
676676
| null

test/compute-engine/latex-syntax/numbers.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,60 @@ describe('SERIALIZATION OF NUMBERS', () => {
381381
);
382382
});
383383

384+
test('scientific notation within avoidExponentsInRange', () => {
385+
const result = ce.box(1 / 7000000).toLatex({
386+
notation: 'scientific',
387+
});
388+
expect(result).toMatchInlineSnapshot(
389+
`1.428\\,571\\,428\\,571\\,428\\,5\\cdot10^{-7}`
390+
);
391+
});
392+
393+
test('scientific notation outside avoidExponentsInRange', () => {
394+
const result = ce.box(1 / 70000000).toLatex({
395+
notation: 'scientific',
396+
});
397+
expect(result).toMatchInlineSnapshot(
398+
`1.428\\,571\\,428\\,571\\,428\\,6\\cdot10^{-8}`
399+
);
400+
});
401+
402+
test('auto notation within avoidExponentsInRange', () => {
403+
const result = ce.box(1 / 7000000).toLatex({
404+
notation: 'auto',
405+
});
406+
expect(result).toMatchInlineSnapshot(
407+
`0.000\\,000\\,142\\,857\\,142\\,857\\,142\\,85`
408+
);
409+
});
410+
411+
test('auto notation outside avoidExponentsInRange', () => {
412+
const result = ce.box(1 / 70000000).toLatex({
413+
notation: 'auto',
414+
});
415+
expect(result).toMatchInlineSnapshot(
416+
`14\\,285\\,714\\,285\\,714\\,286\\cdot10^{-24}`
417+
);
418+
});
419+
420+
test('adaptiveScientific notation within avoidExponentsInRange', () => {
421+
const result = ce.box(1 / 7000000).toLatex({
422+
notation: 'adaptiveScientific',
423+
});
424+
expect(result).toMatchInlineSnapshot(
425+
`0.000\\,000\\,142\\,857\\,142\\,857\\,142\\,85`
426+
);
427+
});
428+
429+
test('adaptiveScientific notation outside avoidExponentsInRange', () => {
430+
const result = ce.box(1 / 70000000).toLatex({
431+
notation: 'adaptiveScientific',
432+
});
433+
expect(result).toMatchInlineSnapshot(
434+
`1.428\\,571\\,428\\,571\\,428\\,6\\cdot10^{-8}`
435+
);
436+
});
437+
384438
test('Number with repeating pattern', () => {
385439
const format = (num: string, p: string) =>
386440
ce.box({ num }).toLatex({

0 commit comments

Comments
 (0)