Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

url: add urlSearchParams.sort() #11098

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions benchmark/url/url-searchparams-sort.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use strict';
const common = require('../common.js');
const URLSearchParams = require('url').URLSearchParams;

const inputs = {
empty: '',
sorted: 'a&b&c&d&e&f&g&h&i&j&k&l&m&n&o&p&q&r&s&t&u&v&w&x&y&z',
almostsorted: 'a&b&c&d&e&f&g&i&h&j&k&l&m&n&o&p&q&r&s&t&u&w&v&x&y&z',
reversed: 'z&y&x&w&v&u&t&s&r&q&p&o&n&m&l&k&j&i&h&g&f&e&d&c&b&a',
random: 'm&t&d&c&z&v&a&n&p&y&u&o&h&l&f&j&e&q&b&i&s&x&k&w&r&g',
// 8 parameters
short: 'm&t&d&c&z&v&a&n',
// 88 parameters
long: 'g&r&t&h&s&r&d&w&b&n&h&k&x&m&k&h&o&e&x&c&c&g&e&b&p&p&s&n&j&b&y&z&' +
'u&l&o&r&w&a&u&l&m&f&j&q&p&f&e&y&e&n&e&l&m&w&u&w&t&n&t&q&v&y&c&o&' +
'k&f&j&i&l&m&g&j&d&i&z&q&p&x&q&q&d&n&y&w&g&i&v&r'
};

function getParams(str) {
const out = [];
for (const key of str.split('&')) {
out.push(key, '');
}
return out;
}

const bench = common.createBenchmark(main, {
type: Object.keys(inputs),
n: [1e6]
}, {
flags: ['--expose-internals']
});

function main(conf) {
const searchParams = require('internal/url').searchParamsSymbol;
const input = inputs[conf.type];
const n = conf.n | 0;
const params = new URLSearchParams();
const array = getParams(input);

var i;
bench.start();
for (i = 0; i < n; i++) {
params[searchParams] = array.slice();
params.sort();
}
bench.end(n);
}
16 changes: 16 additions & 0 deletions doc/api/url.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An example may be helpful here.


```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}
Expand Down Expand Up @@ -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
75 changes: 75 additions & 0 deletions lib/internal/url.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -1183,3 +1257,4 @@ exports.domainToUnicode = domainToUnicode;
exports.encodeAuth = encodeAuth;
exports.urlToOptions = urlToOptions;
exports.formatSymbol = kFormat;
exports.searchParamsSymbol = searchParams;
84 changes: 84 additions & 0 deletions test/parallel/test-whatwg-url-searchparams-sort.js
Original file line number Diff line number Diff line change
@@ -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);
});