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
の方がしっくりきますね…。あとで変更しておきます!