JavaScriptでネストした配列内オブジェクトの値で要素を並び替える
実装時、調べるのに時間がかかったのでメモです。
[
{
"id": 1,
"state": {
"displayOrder": 0
},
},
{
"id": 2,
"state": {
"displayOrder": -1
},
"children": [
{
"id": 6,
"state": {
"displayOrder": 1
}
},
{
"id":7,
"state": {
"displayOrder": 0
}
},
]
},
{
"id": 3,
"state": {
"displayOrder": 1
},
"children": [
{
"id": 8,
"state": {
"displayOrder": 1
}
},
{
"id": 9,
"state": {
"displayOrder": null
}
},
{
"id": 10,
"state": {
"displayOrder": 0
}
},
]
},
{
"id": 4,
"state": {
"displayOrder": null
},
},
{
"id": 5,
"state": {
"displayOrder": 2
},
},
]
上記のような配列をdisplayOrderの値で昇順に並び替える。ただしdisplayOrderがnull(あるいは空値やundefind、false)だったオブジェクトは最後尾につける。
下記のようにchildren以下も同様に処理する。
[
{
"id": 2,
"state": {
"displayOrder": -1
},
"children": [
{
"id": 7,
"state": {
"displayOrder": 0
}
},
{
"id": 6,
"state": {
"displayOrder": 1
}
}
]
},
{
"id": 1,
"state": {
"displayOrder": 0
}
},
{
"id": 3,
"state": {
"displayOrder": 1
},
"childrenren": [
{
"id": 10,
"state": {
"displayOrder": 0
}
},
{
"id": 8,
"state": {
"displayOrder": 1
}
},
{
"id": 9,
"state": {
"displayOrder": null
}
},
]
},
{
"id": 5,
"state": {
"displayOrder": 2
}
},
{
"id": 4,
"state": {
"displayOrder": null
}
}
]
sort()とNumber.isFinite()で並び替える
数字の並び順を変更するのにsort()を使用し、値がnull(あるいは空値やundefind、false)かどうかの判定にNumber.isFinite()を使用した関数を作る。
ちなみにNumber.isFinite()は、厳密には値が有限数であるかどうかを判断するものです。有限数であればtrueを、そうでなければfalseを返します。
displayOrderの値が数値であれば比較して昇順に、そうでなければ後ろにソートされるようにしました。
const orderSort = (array) => array.sort((a, b) => {
const aOrder = a.state.displayOrder
const bOrder = b.state.displayOrder
return !Number.isFinite(aOrder) ? 1 : !Number.isFinite(bOrder) ? -1 : aOrder === bOrder ? 0 : aOrder - bOrder;
})
まず親オブジェクトから並び替える
const array = [
{
"id": 1,
"state": {
"displayOrder": 0
},
},
{
"id": 2,
"state": {
"displayOrder": -1
},
"children": [...]
},
{
"id": 3,
"state": {
"displayOrder": 1
},
"children": [...]
},
{
"id": 4,
"state": {
"displayOrder": null
},
},
{
"id": 5,
"state": {
"displayOrder": 2
},
},
]
const sortArray = orderSort(array)
console.log(sortArray)
/* [
{
"id": 2,
"state": {
"displayOrder": -1
},
"children": [...]
},
{
"id": 1,
"state": {
"displayOrder": 0
}
},
{
"id": 3,
"state": {
"displayOrder": 1
},
"children": [...]
},
{
"id": 5,
"state": {
"displayOrder": 2
}
},
{
"id": 4,
"state": {
"displayOrder": null
}
}
] */
in演算子でプロパティの有無を確認してからchildren以下を並び替える
childrenプロパティを持たないオブジェクトもあるのでin演算子で有無を確認する。
sortArray.map((item) => 'children' in item && orderSort(item.children))
console.log(sortArray)
/*
[
{
...,
"children": [
{
"id": 7,
"state": {
"displayOrder": 0
}
},
{
"id": 6,
"state": {
"displayOrder": 1
}
}
]
},
...,
{
...,
"children": [
{
"id": 10,
"state": {
"displayOrder": 0
}
},
{
"id": 8,
"state": {
"displayOrder": 1
}
}
{
"id": 9,
"state": {
"displayOrder": null
}
},
]
},
...,
...
]
*/
新しい配列を作るわけではないのでmap()ではなくforEach()でも同じ結果になる。
sortArray.forEach((item) => 'children' in item && orderSort(item.children))
まとめて書いてみる
const array = [
{
"id": 1,
"state": {
"displayOrder": 0
},
},
{
"id": 2,
"state": {
"displayOrder": -1
},
"children": [
{
"id": 6,
"state": {
"displayOrder": 1
}
},
{
"id":7,
"state": {
"displayOrder": 0
}
},
]
},
{
"id": 3,
"state": {
"displayOrder": 1
},
"children": [
{
"id": 8,
"state": {
"displayOrder": 1
}
},
{
"id": 9,
"state": {
"displayOrder": null
}
},
{
"id": 10,
"state": {
"displayOrder": 0
}
},
]
},
{
"id": 4,
"state": {
"displayOrder": null
},
},
{
"id": 5,
"state": {
"displayOrder": 2
},
},
]
const orderSort = (array) => array.sort((a, b) => {
const aOrder = a.state.displayOrder
const bOrder = b.state.displayOrder
return !Number.isFinite(aOrder) ? 1 : !Number.isFinite(bOrder) ? -1 : aOrder === bOrder ? 0 : aOrder - bOrder;
})
orderSort(array).map((item) => 'children' in item && orderSort(item.children))
console.log(array)
/*
[
{
"id": 2,
"state": {
"displayOrder": -1
},
"children": [
{
"id": 7,
"state": {
"displayOrder": 0
}
},
{
"id": 6,
"state": {
"displayOrder": 1
}
}
]
},
{
"id": 1,
"state": {
"displayOrder": 0
}
},
{
"id": 3,
"state": {
"displayOrder": 1
},
"children": [
{
"id": 10,
"state": {
"displayOrder": 0
}
},
{
"id": 8,
"state": {
"displayOrder": 1
}
}
{
"id": 9,
"state": {
"displayOrder": null
}
},
]
},
{
"id": 5,
"state": {
"displayOrder": 2
}
},
{
"id": 4,
"state": {
"displayOrder": null
}
}
]
*/
以上です。
ネストされた配列の操作は、かなりややこしい印象です。もっと他にスマートな方法があるかもしれません。
コメントいただいた方法を元に書き換えてみる
コメントありがとうございます。うれしいです。
いただいた内容を元に、自分のアイデアと組み合わせて、こんな方法もあるかなと思い書き直してみました。
//null、undefind、false、空値をNumber.MAX_VALUEに変換する関数
const convert = (x) => (Number.isFinite(x) ? x : Number.MAX_VALUE)
//比較して並び替える関数
const compare = (x) => x.sort((a, b) => convert(a.state.displayOrder) - convert(b.state.displayOrder))
//forEach()で実行
compare(array).forEach((item) => item?.children && compare(item.children))
//map()で実行
const sortedArray = compare(array).map((item) => ({
...item,
children: item?.children && compare(item.children),
}))
やっていることは変わらないのですが、よりシンプルになったかなと思います。
分割代入を利用してさらに簡潔にしてみる
分割代入を利用すると、さらに簡潔に処理できることが分かりました。再帰的な処理も含めて書き直してみます。
//null、undefind、false、空値をNumber.MAX_VALUEに変換する関数
const convert = (x) => (Number.isFinite(x) ? x : Number.MAX_VALUE)
//比較して並び替える関数
const compare = (x) => x.sort((a, b) => convert(a.state.displayOrder) - convert(b.state.displayOrder))
// map()で分割代入を利用して並び替えを実行
const deepSort = (array) =>
compare(array).map(({ children, ...item }) => ({
...item,
...(children && { children: deepSort(children) }),
}));
console.log(deepSort(array));
分割代入で{ children, ...item }というオブジェクトを作成。そこにchildrenが存在しなかったら、そのまま並び替え処理を行い、存在した場合はdeepSort()関数で再帰的にソートされた配列を返します。
分割代入をmap()で使用して、一部を抜き出して処理できることを知れたのが一番の収穫でした。大変勉強になりました。
Discussion
はじめまして。
記事を拝見し、私も考えてみました。
in-placeで(元の配列の中身を上書きしながら)ソートするバージョンと、元の配列の中身は変更せずソートされた配列を返すバージョンを書いてみました。
compareFn
sort in-place
sort non in-place
基本的にはthktさんのやり方と同じですが、
displayOrderの値は論理和(||)でNumber.MAX_VALUEにして扱う(意味を考えると本来はInfinityにして扱う方がよいかもしれないが、引き算結果がNaNになってめんどくさいので妥協)orderSort(array).map((item) => 'child' in item && orderSort(item.child))はソートされたarrayではなくchildの配列を返し直感的ではないので、何も返さないArray.prototype.forEach()を使う。どうせArray.prototype.sort()はin-placeでソートするので値は返さなくていいかな、とchildが存在するかわからないのでオプショナルチェーンを使ってsort()を呼ぶArray.prototype.map()とスプレッド構文で処理するあたりを変えてみました。
1.に関して、falsyではなく
nullかundefinedにしかならないならnull合体演算子(??)も使えますあとは非常に瑣末ですが、
childは配列なのでchildrenの方がより実態に則した名前になるかと。あんまりスマートになりませんでしたが(笑)、一案ということで……
コメントありがとうございます!
オプショナルチェーン初めて知りました。これ使うとスマートな感じがしますね。
map()のスプレッド構文での処理や、in-placeとnon in-placeで分けて考えるのも勉強になりました。displayOrderCompareでの処理の部分は、今回のdisplayOrderの0は数値として扱いたかったので、論理和よりnull合体演算子(これも初めて知りました)がベターな感じですね。childのご指摘もたしかにchildrenの方がしっくりきますね…。あとで変更しておきます!