├── .gitattributes ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── index.html ├── package.json └── spec.emu /.gitattributes: -------------------------------------------------------------------------------- 1 | index.html -diff merge=ours 2 | spec.js -diff merge=ours 3 | spec.css -diff merge=ours 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Deploy spec 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v1 12 | with: 13 | node-version: '12.x' 14 | - run: npm install 15 | - run: npm run build 16 | - name: commit changes 17 | uses: elstudio/actions-js-build/commit@v3 18 | with: 19 | commitMessage: "fixup: [spec] `npm run build`" 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Only apps should have lockfiles 40 | yarn.lock 41 | package-lock.json 42 | npm-shrinkwrap.json 43 | pnpm-lock.yaml 44 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ECMA TC39 and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # proposal-array-grouping 2 | 3 | Note: this proposal is now [at stage 4](https://github.com/tc39/proposals/commit/e11ea25d41c8ab28b6632b5d355359dcb528ee6f). See the spec PR here: https://github.com/tc39/ecma262/pull/3176 4 | 5 | A proposal to make grouping of items in an array (and iterables) easier. 6 | 7 | ```js 8 | const array = [1, 2, 3, 4, 5]; 9 | 10 | // `Object.groupBy` groups items by arbitrary key. 11 | // In this case, we're grouping by even/odd keys 12 | Object.groupBy(array, (num, index) => { 13 | return num % 2 === 0 ? 'even': 'odd'; 14 | }); 15 | // => { odd: [1, 3, 5], even: [2, 4] } 16 | 17 | // `Map.groupBy` returns items in a Map, and is useful for grouping 18 | // using an object key. 19 | const odd = { odd: true }; 20 | const even = { even: true }; 21 | Map.groupBy(array, (num, index) => { 22 | return num % 2 === 0 ? even: odd; 23 | }); 24 | // => Map { {odd: true}: [1, 3, 5], {even: true}: [2, 4] } 25 | ``` 26 | 27 | ## Champions 28 | 29 | - Justin Ridgewell ([@jridgewell](https://github.com/jridgewell)) 30 | - Jordan Harband ([@ljharb](https://github.com/ljharb)) 31 | 32 | ## Status 33 | 34 | Current [Stage](https://tc39.es/process-document/): 4 35 | 36 | ## Motivation 37 | 38 | Array grouping is an extremely common operation, best exemplified by 39 | SQL's `GROUP BY` clause and MapReduce programming (which is better 40 | thought of map-group-reduce). The ability to combine like data into 41 | groups allows developers to compute higher order datasets, like the 42 | average age of a cohort or daily LCP values for a webpage. 43 | 44 | Two methods are offered, `Object.groupBy` and `Map.groupBy`. The first 45 | returns a null-prototype object, which allows ergonomic destructuring 46 | and prevents accidental collisions with global Object properties. The 47 | second returns a regular `Map` instance, which allows grouping on 48 | complex key types (imagine a compound key or [tuple]). 49 | 50 | ## Why static methods? 51 | 52 | We've [found][sugar-bug] a web compatibility issue with the name 53 | `Array.prototype.groupBy`. The [Sugar][sugar] library until v1.4.0 54 | conditionally monkey-patches `Array.prototype` with an incompatible 55 | method. By providing a native `groupBy`, these versions of Sugar would 56 | fail to install their implementation, and any sites that depend on their 57 | behavior would break. We've found some 660 origins that use these 58 | versions of the Sugar library. 59 | 60 | We then attempted the name `Array.prototype.group`, but this ran into 61 | [code that uses an array as an arbitrary hashmap][arraymap-bug]. Because 62 | these bugs are exceptionally difficult to detect (it requires devs to 63 | detect and know how to report the bug to us), the committee didn't want 64 | to attempt another prototype method name. Instead we chose to use static 65 | method, which we believe is unorthodox enough to not risk a web 66 | compatibility issue. This also gives us a nice way to support Records 67 | and Tuples in the future. 68 | 69 | ## Polyfill 70 | 71 | - A polyfill is available in the [core-js] library. You can find it in 72 | the [ECMAScript proposals section][core-js-section]. 73 | 74 | ## Related 75 | 76 | - Lodash 77 | - [`_.groupBy`][lodash] ([850k downloads/week][lodash-npm]) 78 | 79 | [tuple]: https://github.com/tc39/proposal-record-tuple 80 | [core-js]: https://github.com/zloirock/core-js 81 | [core-js-section]: https://github.com/zloirock/core-js#array-grouping 82 | [lodash]: https://lodash.com/docs/4.17.15#groupBy 83 | [lodash-npm]: https://www.npmjs.com/package/lodash.groupby 84 | [sugar]: https://sugarjs.com/ 85 | [sugar-bug]: https://github.com/tc39/proposal-array-grouping/issues/37 86 | [arraymap-bug]: https://github.com/tc39/proposal-array-grouping/issues/44 87 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Array Grouping
2691 |

Stage 2 Draft / August 1, 2023

Array Grouping

2700 | 2701 | 2702 |

1 Scope

2703 |

2704 | This is the spec text of the Array Grouping proposal in ECMAScript. 2705 |

2706 |
2707 | 2708 | 2709 |

2 Properties of the Object Constructor (20.1.2)

2710 | 2711 | 2712 | 2713 |

2.1 Object.groupBy ( items, callbackfn )

2714 | Note
2715 |

callbackfn should be a function that accepts two arguments. groupBy calls callbackfn once for each element in items, in ascending order, and constructs a new Object of arrays. Each value returned by callbackfn is coerced to a property key, and the associated element is included in the array in the constructed object according to this property key.

2716 |

callbackfn is called with two arguments: the value of the element and the index of the element.

2717 |

The return value of groupBy is an object that does not inherit from %Object.prototype%.

2718 |
2719 |

When the groupBy method is called with two arguments, the following steps are taken:

2720 |
  1. Let groups be ? GroupBy(items, callbackfn, property).
  2. Let obj be OrdinaryObjectCreate(null).
  3. For each Record { [[Key]], [[Elements]] } g of groups, do
    1. Let elements be CreateArrayFromList(g.[[Elements]]).
    2. Perform ! CreateDataPropertyOrThrow(obj, g.[[Key]], elements).
  4. Return obj.
2721 |
2722 |
2723 |
2724 | 2725 | 2726 |

3 Properties of the Map Constructor (24.1.2)

2727 | 2728 | 2729 | 2730 |

3.1 Map.groupBy ( items, callbackfn )

2731 | Note
2732 |

callbackfn should be a function that accepts two arguments. groupBy calls callbackfn once for each element in items, in ascending order, and constructs a new Map of arrays. Each value returned by callbackfn is used as a key in the Map, and the associated element is included in the array in the constructed Map according to this key.

2733 |

callbackfn is called with two arguments: the value of the element and the index of the element.

2734 |

The return value of groupBy is a Map.

2735 |
2736 |

When the groupBy method is called with two arguments, the following steps are taken:

2737 |
  1. Let groups be ? GroupBy(items, callbackfn, zero).
  2. Let map be ! Construct(%Map%).
  3. For each Record { [[Key]], [[Elements]] } g of groups, do
    1. Let elements be CreateArrayFromList(g.[[Elements]]).
    2. Let entry be the Record { [[Key]]: g.[[Key]], [[Value]]: elements }.
    3. Append entry to map.[[MapData]].
  4. Return map.
2738 |
2739 |
2740 |
2741 | 2742 | 2743 |

4 Group By Helpers

2744 | 2745 | 2746 | 2747 |

4.1 GroupBy ( items, callbackfn, keyCoercion )

2748 |

The abstract operation GroupBy takes arguments items (an ECMAScript language value), callbackfn (an ECMAScript language value), and keyCoercion (property or zero) and returns either a normal completion containing a List of Records with fields [[Key]] (an ECMAScript language value) and [[Elements]] (a List of ECMAScript language values), or a throw completion. It performs the following steps when called:

2749 |
  1. Perform ? RequireObjectCoercible(items).
  2. If IsCallable(callbackfn) is false, throw a TypeError exception.
  3. Let groups be a new empty List.
  4. Let iteratorRecord be ? GetIterator(items).
  5. Let k be 0.
  6. Repeat,
    1. If k ≥ 253 - 1, then
      1. Let error be ThrowCompletion(a newly created TypeError object).
      2. Return ? IteratorClose(iteratorRecord, error).
    2. Let next be ? IteratorStep(iteratorRecord).
    3. If next is false, then
      1. Return groups.
    4. Let value be ? IteratorValue(next).
    5. Let key be Completion(Call(callbackfn, undefined, « value, 𝔽(k) »)).
    6. IfAbruptCloseIterator(key, iteratorRecord).
    7. If keyCoercion is property, then
      1. Set key to Completion(ToPropertyKey(key)).
      2. IfAbruptCloseIterator(key, iteratorRecord).
    8. Else,
      1. Assert: keyCoercion is zero.
      2. If key is -0𝔽, set key to +0𝔽.
    9. Perform AddValueToKeyedGroup(groups, key, value).
    10. Set k to k + 1.
2750 |
2751 |
2752 | 2753 | 2754 | 2755 |

4.2 AddValueToKeyedGroup ( groups, key, value )

2756 |

The abstract operation AddValueToKeyedGroup takes arguments groups (a List of Records that have [[Key]] and [[Elements]] fields), key (an ECMAScript language value), and value (an ECMAScript language value) and returns unused. It performs the following steps when called:

2757 |
  1. For each Record { [[Key]], [[Elements]] } g of groups, do
    1. If SameValue(g.[[Key]], key) is true, then
      1. Assert: exactly one element of groups meets this criteria.
      2. Append value to g.[[Elements]].
      3. Return unused.
  2. Let group be the Record { [[Key]]: key, [[Elements]]: « value » }.
  3. Append group to groups.
  4. Return unused.
2758 |
2759 |
2760 |
2761 |

A Copyright & Software License

2762 | 2763 |

Copyright Notice

2764 |

© 2023 Justin Ridgewell, Jordan Harband

2765 | 2766 |

Software License

2767 |

All Software contained in this document ("Software") is protected by copyright and is being made available under the "BSD License", included below. This Software may be subject to third party rights (rights from parties other than Ecma International), including patent rights, and no licenses under such third party rights are granted under this license even if the third party concerned is a member of Ecma International. SEE THE ECMA CODE OF CONDUCT IN PATENT MATTERS AVAILABLE AT https://ecma-international.org/memento/codeofconduct.htm FOR INFORMATION REGARDING THE LICENSING OF PATENT CLAIMS THAT ARE REQUIRED TO IMPLEMENT ECMA INTERNATIONAL STANDARDS.

2768 | 2769 |

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

2770 | 2771 |
    2772 |
  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. 2773 |
  3. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  4. 2774 |
  5. Neither the name of the authors nor Ecma International may be used to endorse or promote products derived from this software without specific prior written permission.
  6. 2775 |
2776 | 2777 |

THIS SOFTWARE IS PROVIDED BY THE ECMA INTERNATIONAL "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ECMA INTERNATIONAL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

2778 | 2779 |
2780 |
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "proposal-array-grouping", 4 | "description": "A proposal to make grouping of array items easier", 5 | "scripts": { 6 | "start": "npm run build-loose -- --watch", 7 | "build": "npm run build-loose -- --strict", 8 | "build-loose": "ecmarkup --verbose spec.emu index.html" 9 | }, 10 | "homepage": "https://github.com/tc39/proposal-array-grouping#readme", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/tc39/proposal-array-grouping.git" 14 | }, 15 | "license": "MIT", 16 | "devDependencies": { 17 | "ecmarkup": "^17.0.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /spec.emu: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
  7 | title: Array Grouping
  8 | status: proposal
  9 | stage: 4
 10 | contributors: Justin Ridgewell, Jordan Harband
 11 | location: https://tc39.es/proposal-array-grouping/
 12 | 
13 | 14 | 15 |

Scope

16 |

17 | This is the spec text of the Array Grouping proposal in ECMAScript. 18 |

19 |
20 | 21 | 22 |

Properties of the Object Constructor (20.1.2)

23 | 24 | 25 | 26 |

Object.groupBy ( _items_, _callbackfn_ )

27 | 28 |

_callbackfn_ should be a function that accepts two arguments. `groupBy` calls _callbackfn_ once for each element in _items_, in ascending order, and constructs a new Object of arrays. Each value returned by _callbackfn_ is coerced to a property key, and the associated element is included in the array in the constructed object according to this property key.

29 |

_callbackfn_ is called with two arguments: the value of the element and the index of the element.

30 |

The return value of `groupBy` is an object that does not inherit from %Object.prototype%.

31 |
32 |

When the `groupBy` method is called with two arguments, the following steps are taken:

33 | 34 | 1. Let _groups_ be ? GroupBy(_items_, _callbackfn_, ~property~). 35 | 1. Let _obj_ be OrdinaryObjectCreate(*null*). 36 | 1. For each Record { [[Key]], [[Elements]] } _g_ of _groups_, do 37 | 1. Let _elements_ be CreateArrayFromList(_g_.[[Elements]]). 38 | 1. Perform ! CreateDataPropertyOrThrow(_obj_, _g_.[[Key]], _elements_). 39 | 1. Return _obj_. 40 | 41 |
42 |
43 |
44 | 45 | 46 |

Properties of the Map Constructor (24.1.2)

47 | 48 | 49 | 50 |

Map.groupBy ( _items_, _callbackfn_ )

51 | 52 |

_callbackfn_ should be a function that accepts two arguments. `groupBy` calls _callbackfn_ once for each element in _items_, in ascending order, and constructs a new Map of arrays. Each value returned by _callbackfn_ is used as a key in the Map, and the associated element is included in the array in the constructed Map according to this key.

53 |

_callbackfn_ is called with two arguments: the value of the element and the index of the element.

54 |

The return value of `groupBy` is a Map.

55 |
56 |

When the `groupBy` method is called with two arguments, the following steps are taken:

57 | 58 | 1. Let _groups_ be ? GroupBy(_items_, _callbackfn_, ~zero~). 59 | 1. Let _map_ be ! Construct(%Map%). 60 | 1. For each Record { [[Key]], [[Elements]] } _g_ of _groups_, do 61 | 1. Let _elements_ be CreateArrayFromList(_g_.[[Elements]]). 62 | 1. Let _entry_ be the Record { [[Key]]: _g_.[[Key]], [[Value]]: _elements_ }. 63 | 1. Append _entry_ to _map_.[[MapData]]. 64 | 1. Return _map_. 65 | 66 |
67 |
68 |
69 | 70 | 71 |

Group By Helpers

72 | 73 | 74 | 75 |

76 | GroupBy ( 77 | _items_: an ECMAScript language value, 78 | _callbackfn_: an ECMAScript language value, 79 | _keyCoercion_: ~property~ or ~zero~, 80 | ): either a normal completion containing a List of Records with fields [[Key]] (an ECMAScript language value) and [[Elements]] (a List of ECMAScript language values), or a throw completion 81 |

82 |
83 |
84 | 85 | 1. Perform ? RequireObjectCoercible(_items_). 86 | 1. If IsCallable(_callbackfn_) is *false*, throw a *TypeError* exception. 87 | 1. Let _groups_ be a new empty List. 88 | 1. Let _iteratorRecord_ be ? GetIterator(_items_). 89 | 1. Let _k_ be 0. 90 | 1. Repeat, 91 | 1. If _k_ ≥ 253 - 1, then 92 | 1. Let _error_ be ThrowCompletion(a newly created *TypeError* object). 93 | 1. Return ? IteratorClose(_iteratorRecord_, _error_). 94 | 1. Let _next_ be ? IteratorStep(_iteratorRecord_). 95 | 1. If _next_ is *false*, then 96 | 1. Return _groups_. 97 | 1. Let _value_ be ? IteratorValue(_next_). 98 | 1. Let _key_ be Completion(Call(_callbackfn_, *undefined*, « _value_, 𝔽(_k_) »)). 99 | 1. IfAbruptCloseIterator(_key_, _iteratorRecord_). 100 | 1. If _keyCoercion_ is ~property~, then 101 | 1. Set _key_ to Completion(ToPropertyKey(_key_)). 102 | 1. IfAbruptCloseIterator(_key_, _iteratorRecord_). 103 | 1. Else, 104 | 1. Assert: _keyCoercion_ is ~zero~. 105 | 1. If _key_ is *-0*𝔽, set _key_ to *+0*𝔽. 106 | 1. Perform AddValueToKeyedGroup(_groups_, _key_, _value_). 107 | 1. Set _k_ to _k_ + 1. 108 | 109 |
110 |
111 | 112 | 113 | 114 |

115 | AddValueToKeyedGroup ( 116 | _groups_: a List of Records that have [[Key]] and [[Elements]] fields, 117 | _key_: an ECMAScript language value, 118 | _value_: an ECMAScript language value, 119 | ): ~unused~ 120 |

121 |
122 |
123 | 124 | 1. For each Record { [[Key]], [[Elements]] } _g_ of _groups_, do 125 | 1. If SameValue(_g_.[[Key]], _key_) is *true*, then 126 | 1. Assert: exactly one element of _groups_ meets this criteria. 127 | 1. Append _value_ to _g_.[[Elements]]. 128 | 1. Return ~unused~. 129 | 1. Let _group_ be the Record { [[Key]]: _key_, [[Elements]]: « _value_ » }. 130 | 1. Append _group_ to _groups_. 131 | 1. Return ~unused~. 132 | 133 |
134 |
135 |
136 | --------------------------------------------------------------------------------