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

Add array grouping proposal #959

Merged
merged 4 commits into from Jul 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Expand Up @@ -4,6 +4,9 @@
- [`Array` filtering stage 1 proposal](https://github.com/tc39/proposal-array-filtering):
- `Array.prototype.filterReject` replaces `Array.prototype.filterOut`
- `%TypedArray%.prototype.filterReject` replaces `%TypedArray%.prototype.filterOut`
- Added [`Array` grouping stage 1 proposal](https://github.com/tc39/proposal-array-grouping):
- `Array.prototype.groupBy`
- `%TypedArray%.prototype.groupBy`
- Work with symbols made stricter: some missed before cases of methods that should throw an error on symbols now works as they should
- Handling `@@toPrimitive` in some cases of `ToPrimitive` internal logic made stricter
- Fixed work of `Request` with polyfilled `URLSearchParams`, [#965](https://github.com/zloirock/core-js/issues/965)
Expand Down
22 changes: 22 additions & 0 deletions README.md
Expand Up @@ -112,6 +112,7 @@ Promise.resolve(32).then(x => console.log(x)); // => 32
- [`.of` and `.from` methods on collection constructors](#of-and-from-methods-on-collection-constructors)
- [`compositeKey` and `compositeSymbol`](#compositekey-and-compositesymbol)
- [`Array` filtering](#array-filtering)
- [`Array` grouping](#array-grouping)
- [`Array` deduplication](#array-deduplication)
- [Getting last item from `Array`](#getting-last-item-from-array)
- [`Number.range`](#numberrange)
Expand Down Expand Up @@ -2292,6 +2293,27 @@ core-js/features/typed-array/filter-reject
```js
[1, 2, 3, 4, 5].filterReject(it => it % 2); // => [2, 4]
````
##### [`Array` grouping](#https://github.com/tc39/proposal-array-grouping)[⬆](#index)
Modules [`esnext.array.group-by`](https://github.com/zloirock/core-js/blob/master/packages/core-js/modules/esnext.array.group-by.js) and [`esnext.typed-array.group-by`](https://github.com/zloirock/core-js/blob/master/packages/core-js/modules/esnext.typed-array.group-by.js).
```js
class Array {
groupBy(callbackfn: (value: any, index: number, target: any) => key, thisArg?: any): { [key]: Array<mixed> };
}

class %TypedArray% {
groupBy(callbackfn: (value: number, index: number, target: %TypedArray%) => key, thisArg?: any): { [key]: %TypedArray% };
}
```
[*CommonJS entry points:*](#commonjs-api)
```
core-js/proposals/array-grouping
core-js(-pure)/features/array(/virtual)/group-by
core-js/features/typed-array/group-by
```
[*Examples*](http://es6.zloirock.ru/#log(%5B1%2C%202%2C%203%2C%204%2C%205%5D.groupBy(it%20%3D%3E%20it%20%25%202))%3B%20%2F%2F%20%3D%3E%20%7B%201%3A%20%5B1%2C%203%2C%205%5D%2C%200%3A%20%5B2%2C%204%5D%20%7D):
```js
[1, 2, 3, 4, 5].groupBy(it => it % 2); // => { 1: [1, 3, 5], 0: [2, 4] }
````
##### [Array deduplication](https://github.com/tc39/proposal-array-unique)[⬆](#index)
Modules [`esnext.array.unique-by`](https://github.com/zloirock/core-js/blob/master/packages/core-js/modules/esnext.array.unique-by.js) and [`esnext.typed-array.unique-by`](https://github.com/zloirock/core-js/blob/master/packages/core-js/modules/esnext.typed-array.unique-by.js)
```js
Expand Down
4 changes: 4 additions & 0 deletions packages/core-js-compat/src/data.mjs
Expand Up @@ -1425,6 +1425,8 @@ export const data = {
},
'esnext.array.find-last-index': {
},
'esnext.array.group-by': {
},
'esnext.array.is-template-object': {
},
'esnext.array.last-index': {
Expand Down Expand Up @@ -1683,6 +1685,8 @@ export const data = {
},
'esnext.typed-array.find-last-index': {
},
'esnext.typed-array.group-by': {
},
'esnext.typed-array.unique-by': {
},
'esnext.weak-map.delete-all': {
Expand Down
2 changes: 2 additions & 0 deletions packages/core-js-compat/src/modules-by-versions.mjs
Expand Up @@ -97,6 +97,8 @@ export default {
],
3.16: [
'esnext.array.filter-reject',
'esnext.array.group-by',
'esnext.typed-array.filter-reject',
'esnext.typed-array.group-by',
],
};
@@ -0,0 +1 @@
// empty
4 changes: 4 additions & 0 deletions packages/core-js/features/array/group-by.js
@@ -0,0 +1,4 @@
require('../../modules/esnext.array.group-by');
var entryUnbind = require('../../internals/entry-unbind');

module.exports = entryUnbind('Array', 'groupBy');
1 change: 1 addition & 0 deletions packages/core-js/features/array/index.js
Expand Up @@ -6,6 +6,7 @@ require('../../modules/esnext.array.filter-out');
require('../../modules/esnext.array.filter-reject');
require('../../modules/esnext.array.find-last');
require('../../modules/esnext.array.find-last-index');
require('../../modules/esnext.array.group-by');
require('../../modules/esnext.array.is-template-object');
require('../../modules/esnext.array.last-item');
require('../../modules/esnext.array.last-index');
Expand Down
4 changes: 4 additions & 0 deletions packages/core-js/features/array/virtual/group-by.js
@@ -0,0 +1,4 @@
require('../../../modules/esnext.array.group-by');
var entryVirtual = require('../../../internals/entry-virtual');

module.exports = entryVirtual('Array').groupBy;
1 change: 1 addition & 0 deletions packages/core-js/features/array/virtual/index.js
Expand Up @@ -6,6 +6,7 @@ require('../../../modules/esnext.array.filter-out');
require('../../../modules/esnext.array.filter-reject');
require('../../../modules/esnext.array.find-last');
require('../../../modules/esnext.array.find-last-index');
require('../../../modules/esnext.array.group-by');
require('../../../modules/esnext.array.unique-by');

module.exports = parent;
8 changes: 8 additions & 0 deletions packages/core-js/features/instance/group-by.js
@@ -0,0 +1,8 @@
var groupBy = require('../array/virtual/group-by');

var ArrayPrototype = Array.prototype;

module.exports = function (it) {
var own = it.groupBy;
return it === ArrayPrototype || (it instanceof Array && own === ArrayPrototype.groupBy) ? groupBy : own;
};
1 change: 1 addition & 0 deletions packages/core-js/features/typed-array/group-by.js
@@ -0,0 +1 @@
require('../../modules/esnext.typed-array.group-by');
1 change: 1 addition & 0 deletions packages/core-js/features/typed-array/index.js
Expand Up @@ -6,6 +6,7 @@ require('../../modules/esnext.typed-array.filter-out');
require('../../modules/esnext.typed-array.filter-reject');
require('../../modules/esnext.typed-array.find-last');
require('../../modules/esnext.typed-array.find-last-index');
require('../../modules/esnext.typed-array.group-by');
require('../../modules/esnext.typed-array.unique-by');

module.exports = parent;
33 changes: 33 additions & 0 deletions packages/core-js/internals/array-group-by.js
@@ -0,0 +1,33 @@
var bind = require('../internals/function-bind-context');
var IndexedObject = require('../internals/indexed-object');
var toObject = require('../internals/to-object');
var toLength = require('../internals/to-length');
var toPropertyKey = require('../internals/to-property-key');
var objectCreate = require('../internals/object-create');
var arrayFromConstructorAndList = require('../internals/array-from-constructor-and-list');

var push = [].push;

module.exports = function ($this, callbackfn, that, specificConstructor) {
var O = toObject($this);
var self = IndexedObject(O);
var boundFunction = bind(callbackfn, that, 3);
var target = objectCreate(null);
var length = toLength(self.length);
var index = 0;
var Constructor, key, value;
for (;length > index; index++) {
value = self[index];
key = toPropertyKey(boundFunction(value, index, O));
// in some IE10 builds, `hasOwnProperty` returns incorrect result on integer keys
// but since it's a `null` prototype object, we can safely use `in`
if (key in target) push.call(target[key], value);

Choose a reason for hiding this comment

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

Nit: I switched a List type in the current spec, but I don't know how you implement list (it may just be a normal array?) But I think this exposes set operations in an incorrect order?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Lists are unobservable, so it's optimized for direct adding plain arrays to the null prototype object. If it's a custom array constructor or a typed array, they are created on a second loop pass and all observable operations evaluated in the correct order. The only observable difference with the current spec draft is an optimization of getting @@species constructor.

Copy link
Owner Author

@zloirock zloirock Jul 28, 2021

Choose a reason for hiding this comment

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

Sure, it's possible to define setters on Array.prototype or Object.prototype and observe it, but I think that it's an acceptable limitation. This is a limitation of all polyfills of features that use List that I know. It's possible to use arrays with null prototype or objects with null prototype, but I don't think that it's worth it.

else target[key] = [value];
}
if (specificConstructor) {
Constructor = specificConstructor(O);
if (Constructor !== Array) {
for (key in target) target[key] = arrayFromConstructorAndList(Constructor, target[key]);
}
} return target;
};
16 changes: 16 additions & 0 deletions packages/core-js/modules/esnext.array.group-by.js
@@ -0,0 +1,16 @@
'use strict';
var $ = require('../internals/export');
var $groupBy = require('../internals/array-group-by');
var arraySpeciesConstructor = require('../internals/array-species-constructor');
var addToUnscopables = require('../internals/add-to-unscopables');

// `Array.prototype.groupBy` method
// https://github.com/tc39/proposal-array-grouping
$({ target: 'Array', proto: true }, {
groupBy: function groupBy(callbackfn /* , thisArg */) {
var thisArg = arguments.length > 1 ? arguments[1] : undefined;
return $groupBy(this, callbackfn, thisArg, arraySpeciesConstructor);
}
});

addToUnscopables('groupBy');
14 changes: 14 additions & 0 deletions packages/core-js/modules/esnext.typed-array.group-by.js
@@ -0,0 +1,14 @@
'use strict';
var ArrayBufferViewCore = require('../internals/array-buffer-view-core');
var $groupBy = require('../internals/array-group-by');
var typedArraySpeciesConstructor = require('../internals/typed-array-species-constructor');

var aTypedArray = ArrayBufferViewCore.aTypedArray;
var exportTypedArrayMethod = ArrayBufferViewCore.exportTypedArrayMethod;

// `%TypedArray%.prototype.groupBy` method
// https://github.com/tc39/proposal-array-grouping
exportTypedArrayMethod('groupBy', function groupBy(callbackfn /* , thisArg */) {
var thisArg = arguments.length > 1 ? arguments[1] : undefined;
return $groupBy(aTypedArray(this), callbackfn, thisArg, typedArraySpeciesConstructor);
});
3 changes: 3 additions & 0 deletions packages/core-js/proposals/array-grouping.js
@@ -0,0 +1,3 @@
// https://github.com/tc39/proposal-array-grouping
require('../modules/esnext.array.group-by');
require('../modules/esnext.typed-array.group-by');
1 change: 1 addition & 0 deletions packages/core-js/stage/1.js
@@ -1,4 +1,5 @@
require('../proposals/array-filtering');
require('../proposals/array-grouping');
require('../proposals/array-last');
require('../proposals/array-unique');
require('../proposals/collection-methods');
Expand Down
10 changes: 10 additions & 0 deletions tests/commonjs.js
Expand Up @@ -71,6 +71,7 @@ for (PATH of ['core-js-pure', 'core-js']) {
ok(typeof load('features/array/filter-reject') === 'function');
ok(typeof load('features/array/flat') === 'function');
ok(typeof load('features/array/flat-map') === 'function');
ok(typeof load('features/array/group-by') === 'function');
ok(typeof load('features/array/some') === 'function');
ok(typeof load('features/array/every') === 'function');
ok(typeof load('features/array/reduce') === 'function');
Expand Down Expand Up @@ -106,6 +107,7 @@ for (PATH of ['core-js-pure', 'core-js']) {
ok(typeof load('features/array/virtual/filter-reject') === 'function');
ok(typeof load('features/array/virtual/flat') === 'function');
ok(typeof load('features/array/virtual/flat-map') === 'function');
ok(typeof load('features/array/virtual/group-by') === 'function');
ok(typeof load('features/array/virtual/some') === 'function');
ok(typeof load('features/array/virtual/every') === 'function');
ok(typeof load('features/array/virtual/reduce') === 'function');
Expand Down Expand Up @@ -970,6 +972,7 @@ for (PATH of ['core-js-pure', 'core-js']) {
load('proposals/accessible-object-hasownproperty');
load('proposals/array-filtering');
load('proposals/array-find-from-last');
load('proposals/array-grouping');
load('proposals/array-is-template-object');
load('proposals/array-last');
load('proposals/array-unique');
Expand Down Expand Up @@ -1182,6 +1185,12 @@ for (PATH of ['core-js-pure', 'core-js']) {
ok(typeof instanceFilterReject([]) === 'function');
ok(instanceFilterReject([]).call([1, 2, 3], it => it % 2).length === 1);

const instanceGroupBy = load('features/instance/group-by');
ok(typeof instanceGroupBy === 'function');
ok(instanceGroupBy({}) === undefined);
ok(typeof instanceGroupBy([]) === 'function');
ok(instanceGroupBy([]).call([1, 2, 3], it => it % 2)[1].length === 2);

let instanceFindIndex = load('features/instance/find-index');
ok(typeof instanceFindIndex === 'function');
ok(instanceFindIndex({}) === undefined);
Expand Down Expand Up @@ -1710,6 +1719,7 @@ load('features/typed-array/fill');
load('features/typed-array/filter');
load('features/typed-array/filter-out');
load('features/typed-array/filter-reject');
load('features/typed-array/group-by');
load('features/typed-array/find');
load('features/typed-array/find-index');
load('features/typed-array/find-last');
Expand Down
36 changes: 36 additions & 0 deletions tests/pure/esnext.array.group-by.js
@@ -0,0 +1,36 @@
import { STRICT } from '../helpers/constants';

import Symbol from 'core-js-pure/features/symbol';
import groupBy from 'core-js-pure/features/array/group-by';
import getPrototypeOf from 'core-js-pure/features/object/get-prototype-of';

QUnit.test('Array#groupBy', assert => {
assert.isFunction(groupBy);
let array = [1];
const context = {};
groupBy(array, function (value, key, that) {
assert.same(arguments.length, 3, 'correct number of callback arguments');
assert.same(value, 1, 'correct value in callback');
assert.same(key, 0, 'correct index in callback');
assert.same(that, array, 'correct link to array in callback');
assert.same(this, context, 'correct callback context');
}, context);
assert.same(getPrototypeOf(groupBy([], it => it)), null, 'null proto');
assert.deepEqual(groupBy([1, 2, 3], it => it % 2), { 1: [1, 3], 0: [2] }, '#1');
assert.deepEqual(
groupBy([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], it => `i${ it % 5 }`),
{ i1: [1, 6, 11], i2: [2, 7, 12], i3: [3, 8], i4: [4, 9], i0: [5, 10] },
'#2',
);
assert.deepEqual(groupBy(Array(3), it => it), { undefined: [undefined, undefined, undefined] }, '#3');
if (STRICT) {
assert.throws(() => groupBy(null, () => { /* empty */ }), TypeError);
assert.throws(() => groupBy(undefined, () => { /* empty */ }), TypeError);
}
array = [1];
// eslint-disable-next-line object-shorthand -- constructor
array.constructor = { [Symbol.species]: function () {
return { foo: 1 };
} };
assert.same(groupBy(array, Boolean).true.foo, 1, '@@species');
});
39 changes: 39 additions & 0 deletions tests/tests/esnext.array.group-by.js
@@ -0,0 +1,39 @@
import { STRICT } from '../helpers/constants';

const { getPrototypeOf } = Object;

QUnit.test('Array#groupBy', assert => {
const { groupBy } = Array.prototype;
assert.isFunction(groupBy);
assert.arity(groupBy, 1);
assert.name(groupBy, 'groupBy');
assert.looksNative(groupBy);
assert.nonEnumerable(Array.prototype, 'groupBy');
let array = [1];
const context = {};
array.groupBy(function (value, key, that) {
assert.same(arguments.length, 3, 'correct number of callback arguments');
assert.same(value, 1, 'correct value in callback');
assert.same(key, 0, 'correct index in callback');
assert.same(that, array, 'correct link to array in callback');
assert.same(this, context, 'correct callback context');
}, context);
assert.same(getPrototypeOf([].groupBy(it => it)), null, 'null proto');
assert.deepEqual([1, 2, 3].groupBy(it => it % 2), { 1: [1, 3], 0: [2] }, '#1');
assert.deepEqual(
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].groupBy(it => `i${ it % 5 }`),
{ i1: [1, 6, 11], i2: [2, 7, 12], i3: [3, 8], i4: [4, 9], i0: [5, 10] },
'#2',
);
assert.deepEqual(Array(3).groupBy(it => it), { undefined: [undefined, undefined, undefined] }, '#3');
if (STRICT) {
assert.throws(() => groupBy.call(null, () => { /* empty */ }), TypeError);
assert.throws(() => groupBy.call(undefined, () => { /* empty */ }), TypeError);
}
array = [1];
// eslint-disable-next-line object-shorthand -- constructor
array.constructor = { [Symbol.species]: function () {
return { foo: 1 };
} };
assert.same(array.groupBy(Boolean).true.foo, 1, '@@species');
});
42 changes: 42 additions & 0 deletions tests/tests/esnext.typed-array.group-by.js
@@ -0,0 +1,42 @@
import { DESCRIPTORS, GLOBAL, TYPED_ARRAYS } from '../helpers/constants';

const { getPrototypeOf } = Object;

if (DESCRIPTORS) QUnit.test('%TypedArrayPrototype%.groupBy', assert => {
// we can't implement %TypedArrayPrototype% in all engines, so run all tests for each typed array constructor
for (const name in TYPED_ARRAYS) {
const TypedArray = GLOBAL[name];
const { groupBy } = TypedArray.prototype;
assert.isFunction(groupBy, `${ name }::groupBy is function`);
assert.arity(groupBy, 1, `${ name }::groupBy arity is 1`);
assert.name(groupBy, 'groupBy', `${ name }::groupBy name is 'groupBy'`);
assert.looksNative(groupBy, `${ name }::groupBy looks native`);
const array = new TypedArray([1]);
const context = {};
array.groupBy(function (value, key, that) {
assert.same(arguments.length, 3, 'correct number of callback arguments');
assert.same(value, 1, 'correct value in callback');
assert.same(key, 0, 'correct index in callback');
assert.same(that, array, 'correct link to array in callback');
assert.same(this, context, 'correct callback context');
}, context);

assert.same(getPrototypeOf(new TypedArray([1]).groupBy(it => it)), null, 'null proto');
assert.ok(new TypedArray([1]).groupBy(it => it)[1] instanceof TypedArray, 'instance');
assert.deepEqual(
new TypedArray([1, 2, 3]).groupBy(it => it % 2),
{ 1: new TypedArray([1, 3]), 0: new TypedArray([2]) },
'#1',
);
assert.deepEqual(new TypedArray([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]).groupBy(it => `i${ it % 5 }`), {
i1: new TypedArray([1, 6, 11]),
i2: new TypedArray([2, 7, 12]),
i3: new TypedArray([3, 8]),
i4: new TypedArray([4, 9]),
i0: new TypedArray([5, 10]),
}, '#2');

assert.throws(() => groupBy.call([0], () => { /* empty */ }), "isn't generic");
}
});