diff --git a/src/schema/core/float.ts b/src/schema/core/float.ts index 56146a48..59312b11 100644 --- a/src/schema/core/float.ts +++ b/src/schema/core/float.ts @@ -13,7 +13,7 @@ export const floatNaN: ScalarTag = { : str[0] === '-' ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY, - stringify: stringifyNumber + stringify: node => stringifyNumber(node) } export const floatExp: ScalarTag = { @@ -41,5 +41,5 @@ export const float: ScalarTag = { node.minFractionDigits = str.length - dot - 1 return node }, - stringify: stringifyNumber + stringify: node => stringifyNumber(node) } diff --git a/src/schema/core/int.ts b/src/schema/core/int.ts index a5bcd2ce..6ecaf325 100644 --- a/src/schema/core/int.ts +++ b/src/schema/core/int.ts @@ -35,7 +35,7 @@ export const int: ScalarTag = { tag: 'tag:yaml.org,2002:int', test: /^[-+]?[0-9]+$/, resolve: (str, _onError, opt) => intResolve(str, 0, 10, opt), - stringify: stringifyNumber + stringify: node => stringifyNumber(node) } export const intHex: ScalarTag = { diff --git a/src/schema/yaml-1.1/float.ts b/src/schema/yaml-1.1/float.ts index 88ea3baf..cf814d68 100644 --- a/src/schema/yaml-1.1/float.ts +++ b/src/schema/yaml-1.1/float.ts @@ -13,7 +13,7 @@ export const floatNaN: ScalarTag = { : str[0] === '-' ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY, - stringify: stringifyNumber + stringify: node => stringifyNumber(node, '1.1') } export const floatExp: ScalarTag = { @@ -25,7 +25,7 @@ export const floatExp: ScalarTag = { resolve: (str: string) => parseFloat(str.replace(/_/g, '')), stringify(node) { const num = Number(node.value) - return isFinite(num) ? num.toExponential() : stringifyNumber(node) + return isFinite(num) ? num.toExponential() : stringifyNumber(node, '1.1') } } @@ -43,5 +43,5 @@ export const float: ScalarTag = { } return node }, - stringify: stringifyNumber + stringify: node => stringifyNumber(node, '1.1') } diff --git a/src/schema/yaml-1.1/int.ts b/src/schema/yaml-1.1/int.ts index 68b6450a..68a85053 100644 --- a/src/schema/yaml-1.1/int.ts +++ b/src/schema/yaml-1.1/int.ts @@ -72,7 +72,7 @@ export const int: ScalarTag = { test: /^[-+]?[0-9][0-9_]*$/, resolve: (str: string, _onError: unknown, opt: ParseOptions) => intResolve(str, 0, 10, opt), - stringify: stringifyNumber + stringify: node => stringifyNumber(node, '1.1') } export const intHex: ScalarTag = { diff --git a/src/stringify/stringifyNumber.ts b/src/stringify/stringifyNumber.ts index 6b9b7dcd..66dfde04 100644 --- a/src/stringify/stringifyNumber.ts +++ b/src/stringify/stringifyNumber.ts @@ -1,14 +1,18 @@ import type { Scalar } from '../nodes/Scalar.ts' -export function stringifyNumber({ - format, - minFractionDigits, - tag, - value -}: Scalar): string { +export function stringifyNumber( + { format, minFractionDigits, source, tag, value }: Scalar, + version: '1.1' | '1.2' = '1.2' +): string { if (typeof value === 'bigint') return String(value) const num = typeof value === 'number' ? value : Number(value) - if (!isFinite(num)) return isNaN(num) ? '.nan' : num < 0 ? '-.inf' : '.inf' + if (!isFinite(num)) { + if (typeof value === 'number' && source) { + const source_ = version === '1.1' ? source.replace(/_/g, '') : source + if (parseFloat(source_) === num) return source + } + return isNaN(num) ? '.nan' : num < 0 ? '-.inf' : '.inf' + } let n = Object.is(value, -0) ? '-0' : JSON.stringify(value) if ( !format && diff --git a/tests/doc/types.ts b/tests/doc/types.ts index ddda9ff0..89416a82 100644 --- a/tests/doc/types.ts +++ b/tests/doc/types.ts @@ -14,7 +14,7 @@ import { YAMLMap, YAMLSeq } from 'yaml' -import { seqTag, stringifyString, stringTag } from 'yaml/util' +import { seqTag, stringifyNumber, stringifyString, stringTag } from 'yaml/util' import { source } from '../_utils.ts' const parseDocument = ( @@ -262,6 +262,48 @@ describe('number types', () => { expect(doc.value[5]).not.toHaveProperty('minFractionDigits') }) }) + + describe('overflowing values keep their source (#660)', () => { + test('float overflowing to Infinity round-trips via its source', () => { + expect(String(parseDocument('gitsha: 61e9540'))).toBe('gitsha: 61e9540\n') + }) + + test('negative overflow keeps its source', () => { + expect(String(parseDocument('n: -61e9540'))).toBe('n: -61e9540\n') + }) + + test('literal .inf / .nan are still emitted as such', () => { + expect(String(parseDocument('n: .inf'))).toBe('n: .inf\n') + expect(String(parseDocument('n: -.inf'))).toBe('n: -.inf\n') + expect(String(parseDocument('n: .nan'))).toBe('n: .nan\n') + }) + + test('a programmatic Infinity with finite source is emitted as .inf', () => { + const doc = parseDocument('n: 1') + doc.set('n', Infinity) + expect(String(doc)).toBe('n: .inf\n') + }) + + test('editing the value of a node parsed from a non-finite source is reflected', () => { + const doc = parseDocument('n: 61e9540') + const node = doc.get('n') as Scalar + node.value = -Infinity + expect(String(doc)).toBe('n: -.inf\n') + }) + + test('the source is used only while it still resolves to the value', () => { + const node = new Scalar(Infinity) + node.source = '61e9540' + expect(stringifyNumber(node)).toBe('61e9540') + node.source = '5' + expect(stringifyNumber(node)).toBe('.inf') + }) + + test('a YAML 1.1 float with underscores that overflows keeps its source', () => { + const doc = parseDocument('n: 6_1e9540', { version: '1.1' }) + expect(String(doc)).toBe('n: 6_1e9540\n') + }) + }) }) test('Indented sequence with sequence values (#2)', () => {