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

feat(array): Object.groupBy 静的メソッドを追加 #1749

Merged
merged 14 commits into from
Jul 20, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
- run: npm install
- run: npm run build
- name: Deploy
Expand Down
18 changes: 9 additions & 9 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
node-version: [18]
node-version: [20]
os: [macOS-latest, windows-latest, ubuntu-latest]
name: "Build on Node.js: ${{ matrix.node-version }} OS: ${{ matrix.os }}"
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: "Node.js ${{ matrix.node-version }}"
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
Expand All @@ -29,12 +29,12 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20]
node-version: [20, 22.5.1]
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

https://nodejs.org/en/blog/release/v22.5.1
22.5.0は動かないため。

name: "Test on Node.js ${{ matrix.node-version }}"
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: "Node.js ${{ matrix.node-version }}"
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
Expand All @@ -43,10 +43,10 @@ jobs:
runs-on: ubuntu-latest
name: E2E
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
- run: npm ci
- run: npm run e2e
2 changes: 1 addition & 1 deletion .node-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v20.11.1
v22.4.1
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,13 @@ IssueやPull Requestについては、次のページを参照してください

npm install

Node.js v20.11.1以上とnpm 10.2.4以上が必要です
Node.js v22.4.1以上とnpm 10.8.2以上が必要です

```
$ node -v
v20.11.1
v22.4.1
$ npm -v
10.2.4
10.8.2
```
Comment on lines -82 to 89
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

book.jsonはまた別で更新。テストがNode.js 22じゃないと全てはpassしないので(20でもignoreされるだけで動きはする)


## Usage
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"license": "MIT",
"version": "4.0.0",
"description": "📖 JavaScript Primer - 迷わないための入門書",
"packageManager": "npm@10.2.4",
"packageManager": "npm@10.8.2+sha512.c7f0088c520a46596b85c6f8f1da943400199748a0f7ea8cb8df75469668dc26f6fb3ba26df87e2884a5ebe91557292d0f3db7d0929cdb4f14910c3032ac81fb",
"type": "module",
"directories": {
"test": "test"
Expand Down
55 changes: 53 additions & 2 deletions source/basic/array/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -978,7 +978,7 @@ console.log(totalValue); // => 6
そのため、できる限り変数を`const`で宣言したい場合には`reduce`メソッドは有用です。
一方で、`reduce`メソッドは可読性があまりよくないため、コードの意図が伝わりにくいというデメリットもあります。

`reduce`メソッドには利点と可読性のトレードオフがありますが、利用する場合は`reduce`メソッドを扱う処理を関数で囲むなど処理の意図がわかるように工夫をする必要があります
`reduce`メソッドには利点と可読性のトレードオフがありますが、利用する場合は`reduce`メソッドを扱う処理を関数にするといった処理の意図がわかるように工夫をする必要があります

{{book.console}}
```js
Expand All @@ -991,6 +991,55 @@ function sum(array) {
console.log(sum(array)); // => 6
```

### [ES2024] `Object.groupBy`静的メソッド {#object-group-by}

`Array.prototype.reduce`メソッドを使うことで、配列から数値やオブジェクトなど任意の値を作成できます。

先ほどは配列の合計の数値を計算する例でしたが、配列からオブジェクトを作成することもできます。
配列からオブジェクトを作成したいユースケースとして、配列の要素を条件によってグループ分けしたいケースがあります。
たとえば、数値からなる配列の要素を奇数と偶数の配列に分けたい場合などです。

`Array.prototype.reduce`メソッドを使って、数値からなる配列を奇数と偶数に分けるコードは次のようになります。

{{book.console}}
```js
const array = [1, 2, 3, 4, 5];
const grouped = array.reduce((accumulator, currentValue) => {
// 2で割った余りが0なら偶数(even)、そうでないなら奇数(odd)
const key = currentValue % 2 === 0 ? "even" : "odd";
if (!accumulator[key]) {
accumulator[key] = [];
}
// グループ分けしたキーの配列に要素を追加
accumulator[key].push(currentValue);
return accumulator;
}, {});
console.log(grouped.even); // => [2, 4]
console.log(grouped.odd); // => [1, 3, 5]
```

しかし、`reduce`メソッドは使い方がやや複雜であるため、可能なら避けたほうが読みやすいコードとなりやすいです。
ES2024では、`Object.groupBy`静的メソッドが追加され、配列からグループ分けしたオブジェクトを作成できるようになっています。

`Object.groupBy`静的メソッド[^1]は、第1引数に配列を、第2引数にグループ分けの条件を返すコールバック関数を渡します。
Copy link
Contributor

Choose a reason for hiding this comment

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

Object.groupBy の第一引数は配列に限らずiterableならなんでも処理することができます1。このページが配列の解説なので意図的にこのような表現にしているのかもしれませんが、もし本全体の設計としてここでiterableについて言及するのが自然だと判断できるようであれば、そのようにしたほうが良いかもしれません。

Footnotes

  1. https://tc39.es/ecma262/#sec-groupby の 4

Copy link
Collaborator Author

@azu azu Jul 20, 2024

Choose a reason for hiding this comment

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

ありがとうございます。

proposal-array-groupingでもArray限定で始まってたのでそこまで意識してなかったですが、色々考えてみてここではiterableについて触れる必要性はあまりないかなと思いました。

現実的な理由としては、配列以外のiterableを渡すユースケースがあまり思いつかない(setはあるかもしれないが)のと、章の構成的に他のiterableがこの章より前に説明されてるものが配列以外はないことなどがあります。(文字列はあるが実用性がない)

image

iterable対応といった場合にも、無限iteratorには対応してないので、ちょっと中途半端な気がしています。

// infinity iterator
const infinityIterator = function*() {
  let i = 0;
  while (true) {
    yield i++;
  }
};
const grouped = Object.groupBy(infinityIterator(), value => {
  if (value % 2 === 0) {
    return 'even';
  }
  return 'odd';
});
console.log(grouped);

なので、具体的にはArrayとMap/Setを受け付けるという感じになる気がしますが、 Object.groupBy(map, …) はここだと書けない感じがします。そのため、おそらく配列の章におく場合は省略するのが妥当かなという感じがしました。

おそらくIterator Helpersが入った時にIteratorが本格的に意識される気がするので、その時にiterable関係を章を独立させたり考え直すのがいいんじゃなかなと思ってます。

iterableを扱う同様のものとして Object.fromEntriesArray.from がありますが、これもiterableを扱うものというよりは、ユースケース(Map to Object、Array-like to Array)がメインな感じがしますね。

💭 iterableといったときに無限と有限があんまり意識できないので、MDNはリストとか言ったり、コレクションとかいうみたいな整理も合わせて必要そう。iteratorだけで切り出すとちょっと難しい部分かもしれない。

Copy link
Collaborator Author

@azu azu Jul 20, 2024

Choose a reason for hiding this comment

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

サンプルコードとしてiterableを受け付けることは省くのが妥当な気はするけど(書籍の目的外になる)、シグネチャの説明として"配列を含むiterableなオブジェクト"てきな表現にするのはありかもしれない。

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

#1754 で対応しました

Copy link
Contributor

Choose a reason for hiding this comment

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

ありがとうございます。おっしゃるとおり、現実的にはObject.groupByに渡される値は配列がメインですので、もし言及されるとしても軽く触れる程度で良いと思います。

第2引数のコールバック関数が返した値をキーとして、配列の要素をグループ分けしたオブジェクトが作成されます。

先ほどのコードを`Object.groupBy`静的メソッドを使って書き換えると、次のようになります。

{{book.console}}
<!-- doctest:meta:{ "ECMAScript": "2024" } -->
```js
const array = [1, 2, 3, 4, 5];
const grouped = Object.groupBy(array, (currentValue) => {
// currentValueが偶数なら"even"、そうでないなら"odd"の配列に追加される
return currentValue % 2 === 0 ? "even" : "odd";
});
console.log(grouped.even); // => [2, 4]
console.log(grouped.odd); // => [1, 3, 5]
Comment on lines +1032 to +1038
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

node
Welcome to Node.js v22.4.1.
Type ".help" for more information.
> assert.deepStrictEqual({}, {})
undefined
> assert.deepStrictEqual(Object.create(null), {})
Uncaught AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
+ actual - expected

+ [Object: null prototype] {}
- {}
    at REPL16:1:8
    at ContextifyScript.runInThisContext (node:vm:136:12)
    at REPLServer.defaultEval (node:repl:598:22)
    at bound (node:domain:432:15)
    at REPLServer.runBound [as eval] (node:domain:443:12)
    at REPLServer.onLine (node:repl:927:10)
    at REPLServer.emit (node:events:532:35)
    at REPLServer.emit (node:domain:488:12)
    at [_onLine] [as _onLine] (node:internal/readline/interface:416:12)
    at [_line] [as _line] (node:internal/readline/interface:887:18) {
  generatedMessage: true,
  code: 'ERR_ASSERTION',
  actual: [Object: null prototype] {},
  expected: {},
  operator: 'deepStrictEqual'
}

Object.groupByは Object.create(null)を返すのプロパティ同士の比較にした。
https://tc39.es/proposal-array-grouping/#sec-object.groupby

```

`Object.groupBy`静的メソッドを使うことで、配列からグループ分けしたオブジェクトを簡潔に作成できます。

## [コラム] Array-likeオブジェクト {#array-like}

配列のように扱えるが配列ではないオブジェクトのことを、**Array-likeオブジェクト**と呼びます。
Expand Down Expand Up @@ -1030,7 +1079,7 @@ function myFunc() {
myFunc("a", "b", "c");
```

Array-likeオブジェクトは配列のようで配列ではないというもどかしさを持つオブジェクトです。`Array.from`メソッド<sup>[ES2015]</sup>を使うことでArray-likeオブジェクトを配列に変換して扱うことができます。一度配列に変換してしまえばArrayメソッドも利用できます。
Array-likeオブジェクトは配列のようで配列ではないというもどかしさを持つオブジェクトです。`Array.from`静的メソッド<sup>[ES2015]</sup>を使うことでArray-likeオブジェクトを配列に変換して扱うことができます。一度配列に変換してしまえばArrayメソッドも利用できます。

{{book.console}}
```js
Expand Down Expand Up @@ -1123,3 +1172,5 @@ console.log(versionNames); // => ["ECMAScript 1", "ECMAScript 2", "ECMAScript 3"
[Lodash]: https://lodash.com/ "Lodash"
[Immutable.js]: https://immutable-js.com/ "Immutable.js"
[Arrayについてのドキュメント]: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array

[^1]: `Array.prototype.groupBy`メソッドのようなArrayのメソッドではないのは、同じメソッド名を実装するウェブサイトが多く存在しており後方互換性がなかったためです。
13 changes: 8 additions & 5 deletions test/markdown-doc-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ const sourceDir = path.join(__dirname, "..", "source");
/**
* 指定したECMAScriptバージョンをmetaにもつコードは実行環境によってはサポートされてないので無視する
* 最新版のNodeでは無視しない
* @type {string[]}
* @type {string[]} サポートしてないECMAScriptバージョン
*/
const IgnoredECMAScriptVersions = (() => {
if (semver.cmp(process.version, ">=", "20.0.0")) {
if (semver.cmp(process.version, ">=", "22.0.0")) {
return []; // すべて通る前提
}
if (semver.cmp(process.version, ">=", "20.0.0")) {
return ["2024"]; // Object.groupByがサポートされていない
}
if (semver.cmp(process.version, ">=", "18.0.0")) {
return ["2023"]; // Array.prototype.withがサポートされていない
}
Expand All @@ -35,7 +38,7 @@ const IgnoredECMAScriptVersions = (() => {
// Top-Level await をサポートしていない
return ["2021", "2022"];
}
return ["2017", "2018", "2019", "2020", "2021", "2022"];
return ["2017", "2018", "2019", "2020", "2021", "2022", "2023", "2024"];
})();
/**
* Markdownファイルの CodeBlock に対してdoctestを行う
Expand All @@ -55,7 +58,7 @@ describe("doctest:md", function() {
]);
files.forEach(filePath => {
const normalizeFilePath = filePath.replace(sourceDir, "");
describe(`${normalizeFilePath}`, function () {
describe(`${normalizeFilePath}`, function() {
const content = fs.readFileSync(filePath, "utf-8");
const parsedCodes = parse({
filePath,
Expand All @@ -66,7 +69,7 @@ describe("doctest:md", function() {
parsedCodes.forEach((parsedCode, index) => {
const codeValue = parsedCode.code;
const testCaseName = codeValue.slice(0, 32).replace(/[\r\n]/g, "_");
it(dirName + ": " + testCaseName, function () {
it(dirName + ": " + testCaseName, function() {
return test({
...parsedCode,
code: toTestCode(parsedCode.code)
Expand Down