diff --git a/doc/api/url.md b/doc/api/url.md index 4a9f0361d196e9..d81c31d126dedd 100755 --- a/doc/api/url.md +++ b/doc/api/url.md @@ -777,6 +777,21 @@ Returns an ES6 Iterator over the names of each name-value pair. Remove any existing name-value pairs whose name is `name` and append a new name-value pair. +#### urlSearchParams.sort() + +Sort all existing name-value pairs in-place by their names. Sorting is done +with a [stable sorting algorithm][], so relative order between name-value pairs +with the same name is preserved. + +This method can be used, in particular, to increase cache hits. + +```js +const params = new URLSearchParams('query[]=abc&type=search&query[]=123'); +params.sort(); +console.log(params.toString()); + // Prints query%5B%5D=abc&query%5B%5D=123&type=search +``` + #### urlSearchParams.toString() * Returns: {String} @@ -872,3 +887,4 @@ console.log(myURL.origin); [`Map`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map [`array.toString()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toString [WHATWG URL]: #url_the_whatwg_url_api +[stable sorting algorithm]: https://en.wikipedia.org/wiki/Sorting_algorithm#Stability diff --git a/lib/internal/url.js b/lib/internal/url.js index 2e3fec18fbae65..7d7d434aa0daf1 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -766,6 +766,35 @@ class URLSearchParams { } } +// for merge sort +function merge(out, start, mid, end, lBuffer, rBuffer) { + const sizeLeft = mid - start; + const sizeRight = end - mid; + var l, r, o; + + for (l = 0; l < sizeLeft; l++) + lBuffer[l] = out[start + l]; + for (r = 0; r < sizeRight; r++) + rBuffer[r] = out[mid + r]; + + l = 0; + r = 0; + o = start; + while (l < sizeLeft && r < sizeRight) { + if (lBuffer[l] <= rBuffer[r]) { + out[o++] = lBuffer[l++]; + out[o++] = lBuffer[l++]; + } else { + out[o++] = rBuffer[r++]; + out[o++] = rBuffer[r++]; + } + } + while (l < sizeLeft) + out[o++] = lBuffer[l++]; + while (r < sizeRight) + out[o++] = rBuffer[r++]; +} + defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', { append(name, value) { if (!this || !(this instanceof URLSearchParams)) { @@ -897,6 +926,51 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', { update(this[context], this); }, + sort() { + const a = this[searchParams]; + const len = a.length; + if (len <= 2) { + return; + } + + // arbitrary number found through testing + if (len < 100) { + // Simple stable in-place insertion sort + // Derived from v8/src/js/array.js + for (var i = 2; i < len; i += 2) { + var curKey = a[i]; + var curVal = a[i + 1]; + var j; + for (j = i - 2; j >= 0; j -= 2) { + if (a[j] > curKey) { + a[j + 2] = a[j]; + a[j + 3] = a[j + 1]; + } else { + break; + } + } + a[j + 2] = curKey; + a[j + 3] = curVal; + } + } else { + // Bottom-up iterative stable merge sort + const lBuffer = new Array(len); + const rBuffer = new Array(len); + for (var step = 2; step < len; step *= 2) { + for (var start = 0; start < len - 2; start += 2 * step) { + var mid = start + step; + var end = mid + step; + end = end < len ? end : len; + if (mid > end) + continue; + merge(a, start, mid, end, lBuffer, rBuffer); + } + } + } + + update(this[context], this); + }, + // https://heycam.github.io/webidl/#es-iterators // Define entries here rather than [Symbol.iterator] as the function name // must be set to `entries`. diff --git a/test/parallel/test-whatwg-url-searchparams-sort.js b/test/parallel/test-whatwg-url-searchparams-sort.js new file mode 100644 index 00000000000000..179eafba602c53 --- /dev/null +++ b/test/parallel/test-whatwg-url-searchparams-sort.js @@ -0,0 +1,84 @@ +'use strict'; + +const common = require('../common'); +const {URL, URLSearchParams} = require('url'); +const { test, assert_array_equals } = common.WPT; + +/* eslint-disable */ +/* WPT Refs: + https://github.com/w3c/web-platform-tests/blob/5903e00e77e85f8bcb21c73d1d7819fcd04763bd/url/urlsearchparams-sort.html + License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html +*/ +[ + { + "input": "z=b&a=b&z=a&a=a", + "output": [["a", "b"], ["a", "a"], ["z", "b"], ["z", "a"]] + }, + { + "input": "\uFFFD=x&\uFFFC&\uFFFD=a", + "output": [["\uFFFC", ""], ["\uFFFD", "x"], ["\uFFFD", "a"]] + }, + { + "input": "ffi&🌈", // 🌈 > code point, but < code unit because two code units + "output": [["🌈", ""], ["ffi", ""]] + }, + { + "input": "é&e\uFFFD&e\u0301", + "output": [["e\u0301", ""], ["e\uFFFD", ""], ["é", ""]] + }, + { + "input": "z=z&a=a&z=y&a=b&z=x&a=c&z=w&a=d&z=v&a=e&z=u&a=f&z=t&a=g", + "output": [["a", "a"], ["a", "b"], ["a", "c"], ["a", "d"], ["a", "e"], ["a", "f"], ["a", "g"], ["z", "z"], ["z", "y"], ["z", "x"], ["z", "w"], ["z", "v"], ["z", "u"], ["z", "t"]] + } +].forEach((val) => { + test(() => { + let params = new URLSearchParams(val.input), + i = 0 + params.sort() + for(let param of params) { + assert_array_equals(param, val.output[i]) + i++ + } + }, "Parse and sort: " + val.input) + + test(() => { + let url = new URL("?" + val.input, "https://example/") + url.searchParams.sort() + let params = new URLSearchParams(url.search), + i = 0 + for(let param of params) { + assert_array_equals(param, val.output[i]) + i++ + } + }, "URL parse and sort: " + val.input) +}) +/* eslint-enable */ + +// Tests below are not from WPT. +;[ + { + 'input': 'z=a&=b&c=d', + 'output': [['', 'b'], ['c', 'd'], ['z', 'a']] + } +].forEach((val) => { + test(() => { + const params = new URLSearchParams(val.input); + let i = 0; + params.sort(); + for (const param of params) { + assert_array_equals(param, val.output[i]); + i++; + } + }, 'Parse and sort: ' + val.input); + + test(() => { + const url = new URL(`?${val.input}`, 'https://example/'); + url.searchParams.sort(); + const params = new URLSearchParams(url.search); + let i = 0; + for (const param of params) { + assert_array_equals(param, val.output[i]); + i++; + } + }, 'URL parse and sort: ' + val.input); +});