From d5fab3e518b03652d893445438bb6f2308436254 Mon Sep 17 00:00:00 2001 From: francesco Date: Tue, 19 Mar 2024 12:09:05 +0100 Subject: [PATCH 1/7] perf: avoid unnecessary operation for improve performance --- lib/serializer.js | 96 ++++++++++++++++++------------------------- test/fix-604.test.js | 2 +- test/infinity.test.js | 2 +- test/integer.test.js | 2 +- test/required.test.js | 2 +- 5 files changed, 45 insertions(+), 59 deletions(-) diff --git a/lib/serializer.js b/lib/serializer.js index 1ad588a1..5a9f30bd 100644 --- a/lib/serializer.js +++ b/lib/serializer.js @@ -24,41 +24,33 @@ module.exports = class Serializer { } asInteger (i) { - if (typeof i === 'number') { - if (Number.isInteger(i)) { - return '' + i - } - // check if number is Infinity or NaN - // eslint-disable-next-line no-self-compare - if (i === Infinity || i === -Infinity || i !== i) { - throw new Error(`The value "${i}" cannot be converted to an integer.`) - } - return this.parseInteger(i) - } else if (i === null) { + if (i === null) { return '0' } else if (typeof i === 'bigint') { return i.toString() } else { /* eslint no-undef: "off" */ - const integer = this.parseInteger(i) - if (Number.isFinite(integer)) { - return '' + integer - } else { - throw new Error(`The value "${i}" cannot be converted to an integer.`) + i = this.parseInteger(i) + // check if number is Infinity or NaN + // eslint-disable-next-line no-self-compare + if (i === Infinity || i === -Infinity || i !== i) { + throw new Error('The value cannot be converted to an integer.') } + return '' + i } } asNumber (i) { - const num = Number(i) + // fast cast to number + i = +i // check if number is NaN // eslint-disable-next-line no-self-compare - if (num !== num) { - throw new Error(`The value "${i}" cannot be converted to a number.`) - } else if (!Number.isFinite(num)) { + if (i !== i) { + throw new Error('The value cannot be converted to a number.') + } else if (i === Infinity || i === -Infinity) { return 'null' } else { - return '' + num + return '' + i } } @@ -100,9 +92,34 @@ module.exports = class Serializer { } asString (str) { - if (str.length < 42) { - return this.asStringSmall(str) - } else if (str.length < 5000 && STR_ESCAPE.test(str) === false) { + const len = str.length + if (len < 42) { + // magically escape strings for json + // relying on their charCodeAt + // everything below 32 needs JSON.stringify() + // every string that contain surrogate needs JSON.stringify() + // 34 and 92 happens all the time, so we + // have a fast case for them + let result = '' + let last = -1 + let point = 255 + // eslint-disable-next-line + for (var i = 0; i < len; i++) { + point = str.charCodeAt(i) + if ( + point === 0x22 || // '"' + point === 0x5c // '\' + ) { + last === -1 && (last = 0) + result += str.slice(last, i) + '\\' + last = i + } else if (point < 32 || (point >= 0xD800 && point <= 0xDFFF)) { + // The current character is non-printable characters or a surrogate. + return JSON.stringify(str) + } + } + return (last === -1 && ('"' + str + '"')) || ('"' + result + str.slice(last) + '"') + } else if (len < 5000 && STR_ESCAPE.test(str) === false) { // Only use the regular expression for shorter input. The overhead is otherwise too much. return '"' + str + '"' } else { @@ -114,37 +131,6 @@ module.exports = class Serializer { return '"' + str + '"' } - // magically escape strings for json - // relying on their charCodeAt - // everything below 32 needs JSON.stringify() - // every string that contain surrogate needs JSON.stringify() - // 34 and 92 happens all the time, so we - // have a fast case for them - asStringSmall (str) { - const len = str.length - let result = '' - let last = -1 - let point = 255 - - // eslint-disable-next-line - for (var i = 0; i < len; i++) { - point = str.charCodeAt(i) - if ( - point === 0x22 || // '"' - point === 0x5c // '\' - ) { - last === -1 && (last = 0) - result += str.slice(last, i) + '\\' - last = i - } else if (point < 32 || (point >= 0xD800 && point <= 0xDFFF)) { - // The current character is non-printable characters or a surrogate. - return JSON.stringify(str) - } - } - - return (last === -1 && ('"' + str + '"')) || ('"' + result + str.slice(last) + '"') - } - getState () { return this._options } diff --git a/test/fix-604.test.js b/test/fix-604.test.js index ce8c3685..5d95aa3b 100644 --- a/test/fix-604.test.js +++ b/test/fix-604.test.js @@ -20,5 +20,5 @@ const render = fjs(schema) try { render(input) } catch (err) { - t.equal(err.message, 'The value "phone" cannot be converted to a number.') + t.equal(err.message, 'The value cannot be converted to a number.') } diff --git a/test/infinity.test.js b/test/infinity.test.js index 0316b8c1..8d142bb3 100644 --- a/test/infinity.test.js +++ b/test/infinity.test.js @@ -35,7 +35,7 @@ test('Infinite integers', t => { try { stringify(v) } catch (err) { - t.equal(err.message, `The value "${v}" cannot be converted to an integer.`) + t.equal(err.message, 'The value cannot be converted to an integer.') } }) }) diff --git a/test/integer.test.js b/test/integer.test.js index c4a2415a..31e9fb49 100644 --- a/test/integer.test.js +++ b/test/integer.test.js @@ -44,7 +44,7 @@ test('throws on NaN', (t) => { } const stringify = build(schema) - t.throws(() => stringify(NaN), new Error('The value "NaN" cannot be converted to an integer.')) + t.throws(() => stringify(NaN), new Error('The value cannot be converted to an integer.')) }) test('render a float as an integer', (t) => { diff --git a/test/required.test.js b/test/required.test.js index 17144698..ba530b6a 100644 --- a/test/required.test.js +++ b/test/required.test.js @@ -233,7 +233,7 @@ test('required numbers', (t) => { }) t.fail() } catch (e) { - t.equal(e.message, 'The value "aaa" cannot be converted to an integer.') + t.equal(e.message, 'The value cannot be converted to an integer.') t.pass() } }) From ba9de7d0769f98c46e3ea273e49d65e4406fe56a Mon Sep 17 00:00:00 2001 From: francesco Date: Tue, 19 Mar 2024 14:35:05 +0100 Subject: [PATCH 2/7] chore: revert avoid variable declaration --- lib/serializer.js | 18 +++++++++--------- test/fix-604.test.js | 2 +- test/infinity.test.js | 2 +- test/integer.test.js | 2 +- test/required.test.js | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/serializer.js b/lib/serializer.js index 5a9f30bd..d6cadeab 100644 --- a/lib/serializer.js +++ b/lib/serializer.js @@ -30,27 +30,27 @@ module.exports = class Serializer { return i.toString() } else { /* eslint no-undef: "off" */ - i = this.parseInteger(i) + const integer = this.parseInteger(i) // check if number is Infinity or NaN // eslint-disable-next-line no-self-compare - if (i === Infinity || i === -Infinity || i !== i) { - throw new Error('The value cannot be converted to an integer.') + if (integer === Infinity || integer === -Infinity || integer !== integer) { + throw new Error(`The value "${i}" cannot be converted to an integer.`) } - return '' + i + return '' + integer } } asNumber (i) { // fast cast to number - i = +i + const num = +i // check if number is NaN // eslint-disable-next-line no-self-compare - if (i !== i) { - throw new Error('The value cannot be converted to a number.') - } else if (i === Infinity || i === -Infinity) { + if (num !== num) { + throw new Error(`The value "${i}" cannot be converted to a number.`) + } else if (num === Infinity || num === -Infinity) { return 'null' } else { - return '' + i + return '' + num } } diff --git a/test/fix-604.test.js b/test/fix-604.test.js index 5d95aa3b..ce8c3685 100644 --- a/test/fix-604.test.js +++ b/test/fix-604.test.js @@ -20,5 +20,5 @@ const render = fjs(schema) try { render(input) } catch (err) { - t.equal(err.message, 'The value cannot be converted to a number.') + t.equal(err.message, 'The value "phone" cannot be converted to a number.') } diff --git a/test/infinity.test.js b/test/infinity.test.js index 8d142bb3..0316b8c1 100644 --- a/test/infinity.test.js +++ b/test/infinity.test.js @@ -35,7 +35,7 @@ test('Infinite integers', t => { try { stringify(v) } catch (err) { - t.equal(err.message, 'The value cannot be converted to an integer.') + t.equal(err.message, `The value "${v}" cannot be converted to an integer.`) } }) }) diff --git a/test/integer.test.js b/test/integer.test.js index 31e9fb49..c4a2415a 100644 --- a/test/integer.test.js +++ b/test/integer.test.js @@ -44,7 +44,7 @@ test('throws on NaN', (t) => { } const stringify = build(schema) - t.throws(() => stringify(NaN), new Error('The value cannot be converted to an integer.')) + t.throws(() => stringify(NaN), new Error('The value "NaN" cannot be converted to an integer.')) }) test('render a float as an integer', (t) => { diff --git a/test/required.test.js b/test/required.test.js index ba530b6a..17144698 100644 --- a/test/required.test.js +++ b/test/required.test.js @@ -233,7 +233,7 @@ test('required numbers', (t) => { }) t.fail() } catch (e) { - t.equal(e.message, 'The value cannot be converted to an integer.') + t.equal(e.message, 'The value "aaa" cannot be converted to an integer.') t.pass() } }) From 473d26214704354fc2e901b538e34f4d49c2c8c0 Mon Sep 17 00:00:00 2001 From: francesco Date: Tue, 19 Mar 2024 15:15:28 +0100 Subject: [PATCH 3/7] perf: number must be the first case --- lib/serializer.js | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/lib/serializer.js b/lib/serializer.js index d6cadeab..b5d32406 100644 --- a/lib/serializer.js +++ b/lib/serializer.js @@ -24,19 +24,22 @@ module.exports = class Serializer { } asInteger (i) { - if (i === null) { - return '0' - } else if (typeof i === 'bigint') { - return i.toString() - } else { - /* eslint no-undef: "off" */ - const integer = this.parseInteger(i) - // check if number is Infinity or NaN - // eslint-disable-next-line no-self-compare - if (integer === Infinity || integer === -Infinity || integer !== integer) { - throw new Error(`The value "${i}" cannot be converted to an integer.`) + switch (typeof i) { + case 'number': + case 'boolean': + case 'string': + case 'object': { + /* eslint no-undef: "off" */ + const integer = this.parseInteger(i) + // check if number is Infinity or NaN + // eslint-disable-next-line no-self-compare + if (integer === Infinity || integer === -Infinity || integer !== integer) { + throw new Error(`The value "${i}" cannot be converted to an integer.`) + } + return '' + integer } - return '' + integer + case 'bigint': return i.toString() + default: throw new Error(`The value "${i}" cannot be converted to an integer.`) } } From 49e059c2be524781012ba179487d8fff3e61442c Mon Sep 17 00:00:00 2001 From: francesco Date: Tue, 19 Mar 2024 15:32:47 +0100 Subject: [PATCH 4/7] perf: prioritize integer --- lib/serializer.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/serializer.js b/lib/serializer.js index b5d32406..9b84a964 100644 --- a/lib/serializer.js +++ b/lib/serializer.js @@ -29,6 +29,9 @@ module.exports = class Serializer { case 'boolean': case 'string': case 'object': { + if (Number.isInteger(i)) { + return '' + i + } /* eslint no-undef: "off" */ const integer = this.parseInteger(i) // check if number is Infinity or NaN From 16c54b9badd19167159acfef19d3baa3d4172d09 Mon Sep 17 00:00:00 2001 From: francesco Date: Thu, 11 Apr 2024 15:14:53 +0200 Subject: [PATCH 5/7] remove switch Signed-off-by: francesco --- lib/serializer.js | 53 ++++++++++++++++++++--------------------------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/lib/serializer.js b/lib/serializer.js index 9b84a964..8423308b 100644 --- a/lib/serializer.js +++ b/lib/serializer.js @@ -4,7 +4,7 @@ const STR_ESCAPE = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]/ module.exports = class Serializer { - constructor (options) { + constructor(options) { switch (options && options.rounding) { case 'floor': this.parseInteger = Math.floor @@ -23,30 +23,23 @@ module.exports = class Serializer { this._options = options } - asInteger (i) { - switch (typeof i) { - case 'number': - case 'boolean': - case 'string': - case 'object': { - if (Number.isInteger(i)) { - return '' + i - } - /* eslint no-undef: "off" */ - const integer = this.parseInteger(i) - // check if number is Infinity or NaN - // eslint-disable-next-line no-self-compare - if (integer === Infinity || integer === -Infinity || integer !== integer) { - throw new Error(`The value "${i}" cannot be converted to an integer.`) - } - return '' + integer - } - case 'bigint': return i.toString() - default: throw new Error(`The value "${i}" cannot be converted to an integer.`) + asInteger(i) { + if (Number.isInteger(i)) { + return '' + i + } else if (typeof i === 'bigint') { + return i.toString() + } + /* eslint no-undef: "off" */ + const integer = this.parseInteger(i) + // check if number is Infinity or NaN + // eslint-disable-next-line no-self-compare + if (integer === Infinity || integer === -Infinity || integer !== integer) { + throw new Error(`The value "${i}" cannot be converted to an integer.`) } + return '' + integer } - asNumber (i) { + asNumber(i) { // fast cast to number const num = +i // check if number is NaN @@ -60,11 +53,11 @@ module.exports = class Serializer { } } - asBoolean (bool) { + asBoolean(bool) { return bool && 'true' || 'false' // eslint-disable-line } - asDateTime (date) { + asDateTime(date) { if (date === null) return '""' if (date instanceof Date) { return '"' + date.toISOString() + '"' @@ -75,7 +68,7 @@ module.exports = class Serializer { throw new Error(`The value "${date}" cannot be converted to a date-time.`) } - asDate (date) { + asDate(date) { if (date === null) return '""' if (date instanceof Date) { return '"' + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) + '"' @@ -86,7 +79,7 @@ module.exports = class Serializer { throw new Error(`The value "${date}" cannot be converted to a date.`) } - asTime (date) { + asTime(date) { if (date === null) return '""' if (date instanceof Date) { return '"' + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(11, 19) + '"' @@ -97,7 +90,7 @@ module.exports = class Serializer { throw new Error(`The value "${date}" cannot be converted to a time.`) } - asString (str) { + asString(str) { const len = str.length if (len < 42) { // magically escape strings for json @@ -133,15 +126,15 @@ module.exports = class Serializer { } } - asUnsafeString (str) { + asUnsafeString(str) { return '"' + str + '"' } - getState () { + getState() { return this._options } - static restoreFromState (state) { + static restoreFromState(state) { return new Serializer(state) } } From ab473e924f6d767e72cb99b3e059359824a5f0cc Mon Sep 17 00:00:00 2001 From: francesco Date: Thu, 11 Apr 2024 15:17:12 +0200 Subject: [PATCH 6/7] fix format Signed-off-by: francesco --- lib/serializer.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/serializer.js b/lib/serializer.js index 8423308b..dad8074b 100644 --- a/lib/serializer.js +++ b/lib/serializer.js @@ -4,7 +4,7 @@ const STR_ESCAPE = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]/ module.exports = class Serializer { - constructor(options) { + constructor (options) { switch (options && options.rounding) { case 'floor': this.parseInteger = Math.floor @@ -23,7 +23,7 @@ module.exports = class Serializer { this._options = options } - asInteger(i) { + asInteger (i) { if (Number.isInteger(i)) { return '' + i } else if (typeof i === 'bigint') { @@ -39,7 +39,7 @@ module.exports = class Serializer { return '' + integer } - asNumber(i) { + asNumber (i) { // fast cast to number const num = +i // check if number is NaN @@ -53,7 +53,7 @@ module.exports = class Serializer { } } - asBoolean(bool) { + asBoolean (bool) { return bool && 'true' || 'false' // eslint-disable-line } @@ -68,7 +68,7 @@ module.exports = class Serializer { throw new Error(`The value "${date}" cannot be converted to a date-time.`) } - asDate(date) { + asDate (date) { if (date === null) return '""' if (date instanceof Date) { return '"' + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) + '"' @@ -79,7 +79,7 @@ module.exports = class Serializer { throw new Error(`The value "${date}" cannot be converted to a date.`) } - asTime(date) { + asTime (date) { if (date === null) return '""' if (date instanceof Date) { return '"' + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(11, 19) + '"' @@ -90,7 +90,7 @@ module.exports = class Serializer { throw new Error(`The value "${date}" cannot be converted to a time.`) } - asString(str) { + asString (str) { const len = str.length if (len < 42) { // magically escape strings for json @@ -126,15 +126,15 @@ module.exports = class Serializer { } } - asUnsafeString(str) { + asUnsafeString (str) { return '"' + str + '"' } - getState() { + getState () { return this._options } - static restoreFromState(state) { + static restoreFromState (state) { return new Serializer(state) } } From f01f6277bc10c6c0f3dcd42f1534bf928d3d72f9 Mon Sep 17 00:00:00 2001 From: francesco Date: Thu, 11 Apr 2024 15:24:32 +0200 Subject: [PATCH 7/7] fix lint Signed-off-by: francesco --- lib/serializer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/serializer.js b/lib/serializer.js index dad8074b..24917433 100644 --- a/lib/serializer.js +++ b/lib/serializer.js @@ -57,7 +57,7 @@ module.exports = class Serializer { return bool && 'true' || 'false' // eslint-disable-line } - asDateTime(date) { + asDateTime (date) { if (date === null) return '""' if (date instanceof Date) { return '"' + date.toISOString() + '"'