diff --git a/CHANGELOG.md b/CHANGELOG.md index 93169b083a3e..84d2887e7695 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/README.md b/README.md index 93a5bb4e2597..07a8703eab89 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 }; +} + +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 diff --git a/packages/core-js-compat/src/data.mjs b/packages/core-js-compat/src/data.mjs index 028931ab0ad3..7b2067d97b47 100644 --- a/packages/core-js-compat/src/data.mjs +++ b/packages/core-js-compat/src/data.mjs @@ -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': { @@ -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': { diff --git a/packages/core-js-compat/src/modules-by-versions.mjs b/packages/core-js-compat/src/modules-by-versions.mjs index 1561da520c00..d1c41b5892cf 100644 --- a/packages/core-js-compat/src/modules-by-versions.mjs +++ b/packages/core-js-compat/src/modules-by-versions.mjs @@ -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', ], }; diff --git a/packages/core-js-pure/override/modules/esnext.typed-array.group-by.js b/packages/core-js-pure/override/modules/esnext.typed-array.group-by.js new file mode 100644 index 000000000000..8b1a393741c9 --- /dev/null +++ b/packages/core-js-pure/override/modules/esnext.typed-array.group-by.js @@ -0,0 +1 @@ +// empty diff --git a/packages/core-js/features/array/group-by.js b/packages/core-js/features/array/group-by.js new file mode 100644 index 000000000000..fcb65664a586 --- /dev/null +++ b/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'); diff --git a/packages/core-js/features/array/index.js b/packages/core-js/features/array/index.js index 8ee27f26c67d..5b7887d959a4 100644 --- a/packages/core-js/features/array/index.js +++ b/packages/core-js/features/array/index.js @@ -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'); diff --git a/packages/core-js/features/array/virtual/group-by.js b/packages/core-js/features/array/virtual/group-by.js new file mode 100644 index 000000000000..19c82816a7ea --- /dev/null +++ b/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; diff --git a/packages/core-js/features/array/virtual/index.js b/packages/core-js/features/array/virtual/index.js index c64feb68d304..a99fca643068 100644 --- a/packages/core-js/features/array/virtual/index.js +++ b/packages/core-js/features/array/virtual/index.js @@ -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; diff --git a/packages/core-js/features/instance/group-by.js b/packages/core-js/features/instance/group-by.js new file mode 100644 index 000000000000..2482595563e4 --- /dev/null +++ b/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; +}; diff --git a/packages/core-js/features/typed-array/group-by.js b/packages/core-js/features/typed-array/group-by.js new file mode 100644 index 000000000000..3c41657328a8 --- /dev/null +++ b/packages/core-js/features/typed-array/group-by.js @@ -0,0 +1 @@ +require('../../modules/esnext.typed-array.group-by'); diff --git a/packages/core-js/features/typed-array/index.js b/packages/core-js/features/typed-array/index.js index 1c07f749114d..b96658e776c1 100644 --- a/packages/core-js/features/typed-array/index.js +++ b/packages/core-js/features/typed-array/index.js @@ -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; diff --git a/packages/core-js/internals/array-group-by.js b/packages/core-js/internals/array-group-by.js new file mode 100644 index 000000000000..4b58c0b9f7d2 --- /dev/null +++ b/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); + else target[key] = [value]; + } + if (specificConstructor) { + Constructor = specificConstructor(O); + if (Constructor !== Array) { + for (key in target) target[key] = arrayFromConstructorAndList(Constructor, target[key]); + } + } return target; +}; diff --git a/packages/core-js/modules/esnext.array.group-by.js b/packages/core-js/modules/esnext.array.group-by.js new file mode 100644 index 000000000000..6e82b72f86d6 --- /dev/null +++ b/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'); diff --git a/packages/core-js/modules/esnext.typed-array.group-by.js b/packages/core-js/modules/esnext.typed-array.group-by.js new file mode 100644 index 000000000000..7b42ccd8ecad --- /dev/null +++ b/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); +}); diff --git a/packages/core-js/proposals/array-grouping.js b/packages/core-js/proposals/array-grouping.js new file mode 100644 index 000000000000..00a5c6fc396d --- /dev/null +++ b/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'); diff --git a/packages/core-js/stage/1.js b/packages/core-js/stage/1.js index af1220a5e4dc..2907a863af48 100644 --- a/packages/core-js/stage/1.js +++ b/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'); diff --git a/tests/commonjs.js b/tests/commonjs.js index 34bc1d649926..78ce13d901ab 100644 --- a/tests/commonjs.js +++ b/tests/commonjs.js @@ -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'); @@ -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'); @@ -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'); @@ -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); @@ -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'); diff --git a/tests/pure/esnext.array.group-by.js b/tests/pure/esnext.array.group-by.js new file mode 100644 index 000000000000..252e8490b7dd --- /dev/null +++ b/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'); +}); diff --git a/tests/tests/esnext.array.group-by.js b/tests/tests/esnext.array.group-by.js new file mode 100644 index 000000000000..c3fe51c91e35 --- /dev/null +++ b/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'); +}); diff --git a/tests/tests/esnext.typed-array.group-by.js b/tests/tests/esnext.typed-array.group-by.js new file mode 100644 index 000000000000..36afda100c18 --- /dev/null +++ b/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"); + } +}); +