diff --git a/src/tag/scalar/float_core.ts b/src/tag/scalar/float_core.ts index 9293717b..b7a18312 100644 --- a/src/tag/scalar/float_core.ts +++ b/src/tag/scalar/float_core.ts @@ -50,8 +50,20 @@ const floatCoreTag = defineScalarTag('tag:yaml.org,2002:float', { // ('.inf'/'.nan' start with '.'). implicitFirstChars: ['-', '+', '.', ...'0123456789'], resolve: resolveYamlFloat, - identify: (object) => Object.prototype.toString.call(object) === '[object Number]' && - (object % 1 !== 0 || Object.is(object, -0)), + identify: (object) => + // No ancient boxed numbers support + typeof object === 'number' && + ( + // We land here all numbers, not handled (declined) by !!int `.identify` + // The same condition as for !!int, but reversed. + + // Filter out integers... + !Number.isInteger(object) || + // but allow negative zero + Object.is(object, -0) || + // and integers with exponential form + object.toString(10).indexOf('e') >= 0 + ), represent: representYamlFloat }) diff --git a/src/tag/scalar/float_json.ts b/src/tag/scalar/float_json.ts index 8c28e557..ad222630 100644 --- a/src/tag/scalar/float_json.ts +++ b/src/tag/scalar/float_json.ts @@ -56,8 +56,20 @@ const floatJsonTag = defineScalarTag('tag:yaml.org,2002:float', { // Superset of source.charAt(0) over all matched inputs: optional '-' or digit. implicitFirstChars: ['-', ...'0123456789'], resolve: resolveYamlFloat, - identify: (object) => Object.prototype.toString.call(object) === '[object Number]' && - (object % 1 !== 0 || Object.is(object, -0)), + identify: (object) => + // No ancient boxed numbers support + typeof object === 'number' && + ( + // We land here all numbers, not handled (declined) by !!int `.identify` + // The same condition as for !!int, but reversed. + + // Filter out integers... + !Number.isInteger(object) || + // but allow negative zero + Object.is(object, -0) || + // and integers with exponential form + object.toString(10).indexOf('e') >= 0 + ), represent: representYamlFloat }) diff --git a/src/tag/scalar/float_yaml11.ts b/src/tag/scalar/float_yaml11.ts index ec4b616b..6fd27e64 100644 --- a/src/tag/scalar/float_yaml11.ts +++ b/src/tag/scalar/float_yaml11.ts @@ -57,8 +57,20 @@ const floatYaml11Tag = defineScalarTag('tag:yaml.org,2002:float', { // ('.inf'/'.nan' start with '.'). implicitFirstChars: ['-', '+', '.', ...'0123456789'], resolve: resolveYamlFloat, - identify: (object) => Object.prototype.toString.call(object) === '[object Number]' && - (object % 1 !== 0 || Object.is(object, -0)), + identify: (object) => + // No ancient boxed numbers support + typeof object === 'number' && + ( + // We land here all numbers, not handled (declined) by !!int `.identify` + // The same condition as for !!int, but reversed. + + // Filter out integers... + !Number.isInteger(object) || + // but allow negative zero + Object.is(object, -0) || + // and integers with exponential form + object.toString(10).indexOf('e') >= 0 + ), represent: representYamlFloat }) diff --git a/src/tag/scalar/int_core.ts b/src/tag/scalar/int_core.ts index 573091c9..e097e8bf 100644 --- a/src/tag/scalar/int_core.ts +++ b/src/tag/scalar/int_core.ts @@ -53,8 +53,13 @@ const intCoreTag = defineScalarTag('tag:yaml.org,2002:int', { // Superset of source.charAt(0) over all matched inputs: optional sign + decimal digit. implicitFirstChars: ['-', '+', ...'0123456789'], resolve: resolveYamlInteger, - identify: (object) => Object.prototype.toString.call(object) === '[object Number]' && - (object % 1 === 0 && !Object.is(object, -0)), + identify: (object) => + // No ancient boxed numbers support + Number.isInteger(object) && + // Negative zero => !!float + !Object.is(object, -0) && + // Exponential form => !!float, round-trip for !!int 1e21 will be broken + object.toString(10).indexOf('e') < 0, represent: (object: number) => object.toString(10) }) diff --git a/src/tag/scalar/int_json.ts b/src/tag/scalar/int_json.ts index c4d9eddf..393292c9 100644 --- a/src/tag/scalar/int_json.ts +++ b/src/tag/scalar/int_json.ts @@ -48,8 +48,13 @@ const intJsonTag = defineScalarTag('tag:yaml.org,2002:int', { // Superset of source.charAt(0) over all matched inputs: optional '-' or digit. implicitFirstChars: ['-', ...'0123456789'], resolve: resolveYamlInteger, - identify: (object) => Object.prototype.toString.call(object) === '[object Number]' && - (object % 1 === 0 && !Object.is(object, -0)), + identify: (object) => + // No ancient boxed numbers support + Number.isInteger(object) && + // Negative zero => !!float + !Object.is(object, -0) && + // Exponential form => !!float, round-trip for !!int 1e21 will be broken + object.toString(10).indexOf('e') < 0, represent: (object: number) => object.toString(10) }) diff --git a/src/tag/scalar/int_yaml11.ts b/src/tag/scalar/int_yaml11.ts index 84263338..a4c30326 100644 --- a/src/tag/scalar/int_yaml11.ts +++ b/src/tag/scalar/int_yaml11.ts @@ -47,8 +47,13 @@ const intYaml11Tag = defineScalarTag('tag:yaml.org,2002:int', { // Superset of source.charAt(0) over all matched inputs: optional sign + decimal digit. implicitFirstChars: ['-', '+', ...'0123456789'], resolve: resolveYamlInteger, - identify: (object) => Object.prototype.toString.call(object) === '[object Number]' && - (object % 1 === 0 && !Object.is(object, -0)), + identify: (object) => + // No ancient boxed numbers support + Number.isInteger(object) && + // Negative zero => !!float + !Object.is(object, -0) && + // Exponential form => !!float, round-trip for !!int 1e21 will be broken + object.toString(10).indexOf('e') < 0, represent: (object: number) => object.toString(10) }) diff --git a/test/core/tags/int.test.mjs b/test/core/tags/int.test.mjs index 5a4247fe..68c26487 100644 --- a/test/core/tags/int.test.mjs +++ b/test/core/tags/int.test.mjs @@ -39,6 +39,27 @@ describe('tags/int', () => { assert.deepStrictEqual(load(dump(expected, { schema }), { schema }), expected) }) + it(`${name} round-trip of large integers`, () => { + // Integers at or above 1e21 stringify in exponential notation + // ('1e+21'), which is not valid `!!int` text. They must round-trip + // through the float tag rather than being dumped as `!!int '1e+21'`. + + // Should round-trip as !!int + assert.strictEqual(dump(1e20, { schema }), '100000000000000000000\n') + assert.strictEqual(load(dump(1e20, { schema }), { schema }), 1e20) + assert.strictEqual(dump(-1e20, { schema }), '-100000000000000000000\n') + assert.strictEqual(load(dump(-1e20, { schema }), { schema }), -1e20) + + // Should round-trip as !!float + assert.strictEqual(dump(1e21, { schema }), '1.e+21\n') + assert.strictEqual(load(dump(1e21, { schema }), { schema }), 1e21) + assert.strictEqual(dump(-1e21, { schema }), '-1.e+21\n') + assert.strictEqual(load(dump(-1e21, { schema }), { schema }), -1e21) + + assert.strictEqual(load(dump(Number.MAX_VALUE, { schema }), { schema }), Number.MAX_VALUE) + assert.strictEqual(load(dump(-Number.MAX_VALUE, { schema }), { schema }), -Number.MAX_VALUE) + }) + it(`${name} fail explicit tag`, () => { assert.throws(() => load('!!int 1.5', { schema }), /cannot resolve/) })