条件に合うものと合わないものを、1回のループで2つの配列に分けます。 filter() を2回書く方法との違い、条件の否定を書き間違えて件数が合わなくなる罠まで確かめます。
答え
条件は1か所だけ書きます。
example.js
function partition(list, test) {
const yes = [];
const no = [];
for (const item of list) {
(test(item) ? yes : no).push(item);
}
return [yes, no];
}
console.log(partition([1, 2, 3, 4, 5], (n) => n % 2 === 0));
出力書き換えて実行できます
[ [ 2, 4 ], [ 1, 3, 5 ] ]
example.js
function partition(list, test) {
const yes = [];
const no = [];
for (const item of list) {
(test(item) ? yes : no).push(item);
}
return [yes, no];
}
const [passed, failed] = partition(
[{ id: 1, ok: true }, { id: 2, ok: false }],
(item) => item.ok,
);
console.log(passed);
console.log(failed);
出力書き換えて実行できます
[ { id: 1, ok: true } ]
[ { id: 2, ok: false } ]
標準のメソッドはありません。これだけで足ります。
受け取る側は分割代入で書きます。
example.js
const nums = [1, 2, 3, 4, 5];
const even = nums.filter((n) => n % 2 === 0);
const odd = nums.filter((n) => n % 2 !== 0);
console.log(even, odd);
出力書き換えて実行できます
[ 2, 4 ] [ 1, 3, 5 ]
javascript
const users = [
{ name: 'a', age: 20 },
{ name: 'b', age: null },
{ name: 'c', age: 0 },
];
const known = users.filter((user) => user.age);
const unknown = users.filter((user) => !user.age);
console.log(known.length, unknown.length);
1 2
filter() を2回書く方法との違い
example.js
const users = [
{ name: 'a', age: 20 },
{ name: 'b', age: null },
{ name: 'c', age: 0 },
];
const known = users.filter((user) => user.age != null);
const unknown = users.filter((user) => user.age == null);
console.log(known.length, unknown.length);
出力書き換えて実行できます
2 1
example.js
const [even, odd] = [1, 2, 3, 4, 5].reduce(
([yes, no], n) => (n % 2 === 0 ? [[...yes, n], no] : [yes, [...no, n]]),
[[], []],
);
console.log(even, odd);
出力書き換えて実行できます
[ 2, 4 ] [ 1, 3, 5 ]
結果は同じです。 件数が少なく、条件が単純なら、これで構いません。
違いはここです。
|
filter() × 2 |
1回で分ける |
| 走る回数 |
2回 |
1回 |
| 条件の書き場所 |
2か所 |
1か所 |
| 条件が重いとき |
2倍かかる |
そのまま |
| 読みやすさ |
短い |
関数が1つ要る |
問題になるのは条件が2か所にあることです。
いちばんの落とし穴:否定を書き間違える
example.js
function partition(list, test) {
const yes = [];
const no = [];
for (const item of list) {
(test(item) ? yes : no).push(item);
}
return [yes, no];
}
console.log(partition([], (n) => n > 0));
出力書き換えて実行できます
[ [], [] ]
example.js
const isAdult = (user) => user.age >= 18;
function partition(list, test) {
const yes = [];
const no = [];
for (const item of list) {
(test(item) ? yes : no).push(item);
}
return [yes, no];
}
const [adults, minors] = partition(
[{ name: 'a', age: 20 }, { name: 'b', age: 10 }],
isAdult,
);
console.log(adults.map((user) => user.name), minors.map((user) => user.name));
出力書き換えて実行できます
[ 'a' ] [ 'b' ]
age が 0 の人が「年齢が分からない」側に入りました。
0 は偽として扱われるからです。
しかも合計は3件のままなので、数を見ても気づけません。
条件を直すなら、2か所とも直す必要があります。
example.js
const results = await Promise.allSettled([
Promise.resolve(1),
Promise.reject(new Error('だめ')),
Promise.resolve(3),
]);
const succeeded = results.filter((r) => r.status === 'fulfilled').map((r) => r.value);
const failed = results.filter((r) => r.status === 'rejected').map((r) => r.reason.message);
console.log(succeeded);
console.log(failed);
出力書き換えて実行できます
[ 1, 3 ]
[ 'だめ' ]
example.js
const scores = [95, 72, 48, 88];
const grouped = Object.groupBy(scores, (n) => {
if (n >= 90) return 'A';
if (n >= 70) return 'B';
return 'C';
});
console.log(grouped);
出力書き換えて実行できます
{ A: [ 95 ], B: [ 72, 88 ], C: [ 48 ] }
片方だけ直すと、どちらにも入らない要素や、両方に入る要素が出ます。
分ける関数に渡す形なら、条件は1つしかないのでずれようがありません。
!= null は null と undefined の両方に当たる、数少ない「緩い比較を使ってよい」書き方です。
→ なぜ == ではなく === を使うのか
reduce() で書く形
結果は同じですが、勧めません。
1件ごとに配列を作り直しているので、件数が増えると急に遅くなります。
読むのにも時間がかかります。
→ Array.prototype.reduce()
空の配列を渡したとき
空が2つ返ります。 特別扱いは要りません。
受け取った側で「片方が空かどうか」を見るときは、
every() が空配列で true になることに注意してください。
→ every() と some()
条件に名前を付ける
分ける関数を使ういちばんの利点は、条件に名前が付くことです。
isAdult を直したいときに、直す場所は1か所です。
filter() を2回書いていると、否定側も一緒に直さなければなりません。
非同期の結果を分ける
成功と失敗に分ける形は、実務でいちばんよく使います。
Promise.allSettled() は、1つ失敗しても最後まで走ります。
Promise.all() だと最初の失敗で止まるので、この分け方はできません。
→ 複数の非同期処理を同時に走らせる
ここは条件が status の一致なので、書き間違えても fulfilled と rejected のどちらにも入らないだけです。合計が合わなくなるので、否定の書き間違いより気づきやすくなります。
3つ以上に分けたいとき
2つで足りないなら、分けるのではなくまとめるほうに切り替えます。
→ 配列をキーごとにまとめる(グループ化)
まとめ
- 条件は1か所だけ書く。 分ける関数を1つ用意すれば済む
filter() を2回でも結果は同じ。ただし条件が2か所に散る
- 否定を書き間違えると、合計が合ったまま中身がずれる
0 や空文字を条件にそのまま使わない。!= null で書く
reduce() で書けるが、毎回配列を作り直すので勧めない
- 条件に名前を付けると、直す場所が1か所で済む
- 3つ以上なら
Object.groupBy()