JavaScriptで複数のキーでオブジェクトを並び替えるプログラムです。
例えば、
const fruitsArr = [
{name: 'ぶどう', price: 5000, weight: 1000},
{name: 'もも', price: 3000, weight: 2000},
{name: 'りんご', price: 5000, weight: 5000},
{name: 'バナナ', price: 1500, weight: 1000},
{name: 'メロン', price: 5000, weight: 1200},
{name: 'マンゴー', price: 10000, weight: 900},
{name: 'みかん', price: 1500, weight: 5000},
]
このような配列を、priceの昇順、同じpriceであればweightの昇順で並び変えます。
compareByAttr
関数を作成し、このように書くとシンプルです。compareByAttr
は下で実装します。
fruitsArr.sort((o1, o2) =>
compareByAttr(o1, o2, ['price', 'weight'])
)
console.log(fruitsArr)
//=> [
// {name: "バナナ", price: 1500, weight: 1000},
// {name: "みかん", price: 1500, weight: 5000},
// {name: "もも", price: 3000, weight: 2000},
// {name: "ぶどう", price: 5000, weight: 1000},
// {name: "メロン", price: 5000, weight: 1200},
// {name: "りんご", price: 5000, weight: 5000},
// {name: "マンゴー", price: 10000, weight: 900}
// ]
compareByAttrと関連する関数の作成
上記コードのcompareByAttr
関数と関連する関数の定義と実装です。
const zipLongest = (...arrays) => {
const length = Math.max(...(arrays.map(arr => arr.length)))
return new Array(length).fill().map((_, i) => arrays.map(arr => arr[i]))
}
const compareArr = (arr1, arr2) => {
const difference = zipLongest(arr1, arr2).find(([v1, v2]) => v1 !== v2)
if (!difference) {
return 0
}
if (difference[0] === undefined) return -1
if (difference[1] === undefined) return 1
if (difference[0] > difference[1]) {
return 1
}
return -1
}
const compareByAttr = (o1, o2, attrs) => {
const o1Values = attrs.map(attr => o1[attr])
const o2Values = attrs.map(attr => o2[attr])
return compareArr(o1Values, o2Values)
}
コードの解説
compareByAttr
compareByAttr
関数は、2つのオブジェクト(o1
、o2
)と、1つの配列(attrs
)を受け取ります。配列には、比較する対象のキーが、優先度の高い順番に並べられています。
まず、各オブジェクトからattrs
で指定されたキーを抜き出して配列にします。
以下のように動作します。
const main = () => {
const o1 = {name: 'ぶどう', price: 5000, weight: 1000}
const o2 = {name: 'みかん', price: 1500, weight: 5000}
compareByAttr(o1, o2, ['price', 'weight'])
}
const compareByAttr = (o1, o2, attrs) => {
const o1Values = attrs.map(attr => o1[attr])
//=> [5000, 1000]
const o2Values = attrs.map(attr => o2[attr])
//=> [1500, 5000]
}
main()
このようにして得られた結果をcompareArr
関数に渡してその結果を返します。
const main = () => {
const o1 = {name: 'ぶどう', price: 5000, weight: 1000}
const o2 = {name: 'みかん', price: 1500, weight: 5000}
compareByAttr(o1, o2, ['price', 'weight'])
}
const compareByAttr = (o1, o2, attrs) => {
const o1Values = attrs.map(attr => o1[attr])
//=> [5000, 1000]
const o2Values = attrs.map(attr => o2[attr])
//=> [1500, 5000]
return compareArr(o1Values, o2Values)
//=> return compareArr([5000, 1000], [1500, 5000])
}
main()
zipLongest
compareArr
関数の解説をする前に、zipLongest
関数を解説します。この関数は、各配列の同じインデックスの要素をまとめます。各配列の長さが異なる場合には、一番長い配列の長さになります。未定義値はundefined
になります。
const a1 = [1, 2, 3]
const a2 = ['Jan', 'Feb', 'Mar']
zipLongest(a1, a2)
//=> [[1, "Jan"], [2, "Feb"], [3, "Mar"]]
const a3 = [1, 2, 3]
const a4 = ['Jan', 'Feb', 'Mar', 'Apr', 'May']
zipLongest(a3, a4)
//=> [[1, "Jan"], [2, "Feb"], [3, "Mar"], [undefined, "Apr"], [undefined, "May"]]
zipLongest
関数では、レスト構文を使用して引数全てをarrays
に格納しています。
最終的に作成する配列の長さは、各配列の最大値なので、Math.max
関数を使用して長さを求めます。
最後に、各配列の同じインデックスの値をまとめます。
const zipLongest = (...arrays) => {
const length = Math.max(...(arrays.map(arr => arr.length)))
return new Array(length).fill().map((_, i) => arrays.map(arr => arr[i]))
}
もっと詳細なコードの解説は参考リンクを参照してください。
compareArr
compareArr
関数は、2つの配列(arr1,
とarr2
)を受け取り、配列を最初から値を比べていき、arr1
の値の方が大きければ1
、arr2
の値の方が大きければ-1
を返します。同じ値であれば次の値を見ます。全て同じ値であれば0
を返します。
最初の値をみて、違うならばその値同士で判定、同じならば次の値を見る、という操作は、"最初に違う値が出たところで、大小を判定する"と言い換えることができます。そこで、配列を最初から見ていって、違う値が出たところを取得します。
まず、zipLongest
関数で2つの配列の同じインデックスをまとめましょう。
const main = () => {
const arr1 = [100, 500]
const arr2 = [100, 300]
compareArr(arr1, arr2)
}
const compareArr = (arr1, arr2) => {
zipLongest(arr1, arr2)
//=> [[100, 100], [500, 300]]
}
main()
次に、最初に違う値になる時のarr1
、arr2
の値を取得します。Array.prototype.find()
メソッドを使用します。このメソッドは、渡された関数を満たす配列内の最初の要素の値を返します。
find()
メソッドに([v1, v2]) => v1 !== v2
を渡し、値が等しくない最初の値を取得します。
const main = () => {
const arr1 = [100, 500]
const arr2 = [100, 300]
compareArr(arr1, arr2)
}
const compareArr = (arr1, arr2) => {
const difference = zipLongest(arr1, arr2).find(([v1, v2]) => v1 !== v2)
//=> [[100, 100], [500, 300]].find(([v1, v2]) => v1 !== v2)
//=> [500, 300]
}
main()
違う値が見つからない場合(全て同じ値)のときは、undefined
になります。
const main = () => {
const arr1 = [100, 500]
const arr2 = [100, 500]
compareArr(arr1, arr2)
}
const compareArr = (arr1, arr2) => {
const difference = zipLongest(arr1, arr2).find(([v1, v2]) => v1 !== v2)
//=> undefined
}
main()
difference
がundefined
の場合は等しいので0
を返します。
それ以外の場合はarr1
の値(difference[0]
)の値が大きければ1
、そうでなければ-1
を返します。
ただし、片方がundefined
の場合は大小比較ができないので、先に判定しておきます。undefind
は並び替えで先に来るようにします。(参照: JavaScriptでundefinedの大小比較はつねにfalse。その理由を、仕様を引用して解説する - Qiita)
const main = () => {
const arr1 = [100, 500]
const arr2 = [100, 300]
compareArr(arr1, arr2)
}
const compareArr = (arr1, arr2) => {
const difference = zipLongest(arr1, arr2).find(([v1, v2]) => v1 !== v2)
//=> [500, 300]
if (!difference) {
return 0
}
if (difference[0] === undefined) return -1
if (difference[1] === undefined) return 1
if (difference[0] > difference[1]) {
// この例では1がreturnされる
return 1
}
return -1
}
main()
まとめ
もう一度全てのコードをコメント付きで書いておきます。コード量は多いですが、呼び出し側はすっきりします。
また、個々の関数はそれ自体で独立性が高く、他の用途でも有用です。
/**
* 各配列の同じインデックスの要素をまとめます。
* 各配列の長さが異なる場合には、一番長い配列の長さになります。
* 未定義値はundefinedになります。
*
* @param {...Array} arrays
*/
const zipLongest = (...arrays) => {
const length = Math.max(...(arrays.map(arr => arr.length)))
return new Array(length).fill().map((_, i) => arrays.map(arr => arr[i]))
}
/**
* 2つの配列を比較します。
* 配列の要素をインデックスの小さい順に比較し、arr1の要素の方が大きければ1を返し、arr2の要素の方が大きければ-1を返します。
* 全ての値が等しいときは、0を返します。
*
* @param {Array} arr1 比較する配列1個目
* @param {Array} arr2 比較する配列2個目
*/
const compareArr = (arr1, arr2) => {
const difference = zipLongest(arr1, arr2).find(([v1, v2]) => v1 !== v2)
if (!difference) {
return 0
}
if (difference[0] === undefined) return -1
if (difference[1] === undefined) return 1
if (difference[0] > difference[1]) {
return 1
}
return -1
}
/**
* 与えられたキーによってオブジェクトの大小を判定します。
* 与えられたキーに対応する値が、o1の方が大きい場合には1、o2の方が大きい場合には-1を返します。
* 等しい場合には0を返します。
*
* @param {Object} o1 比較対象の1個目のオブジェクト
* @param {Object} o2 比較対象の2個目のオブジェクト
* @param {Array} attrs 比較する対象のキー。優先度の高い順番に並べる
*/
const compareByAttr = (o1, o2, attrs) => {
const o1Values = attrs.map(attr => o1[attr])
const o2Values = attrs.map(attr => o2[attr])
return compareArr(o1Values, o2Values)
}
// --- 以下、使用方法と実行結果 ---
const fruitsArr = [
{name: 'ぶどう', price: 5000, weight: 1000},
{name: 'もも', price: 3000, weight: 2000},
{name: 'りんご', price: 5000, weight: 5000},
{name: 'バナナ', price: 1500, weight: 1000},
{name: 'メロン', price: 5000, weight: 1200},
{name: 'マンゴー', price: 10000, weight: 900},
{name: 'みかん', price: 1500, weight: 5000},
]
// priceの昇順、weightの昇順の優先順で並び替え
fruitsArr.sort((o1, o2) =>
compareByAttr(o1, o2, ['price', 'weight'])
)
console.log(fruitsArr)
//=>[
// {name: "バナナ", price: 1500, weight: 1000},
// {name: "みかん", price: 1500, weight: 5000},
// {name: "もも", price: 3000, weight: 2000},
// {name: "ぶどう", price: 5000, weight: 1000},
// {name: "メロン", price: 5000, weight: 1200},
// {name: "りんご", price: 5000, weight: 5000},
// {name: "マンゴー", price: 10000, weight: 900}
// ]
// priceの降順、weightの降順の優先順で並び替え
fruitsArr.sort((o1, o2) =>
-1 * compareByAttr(o1, o2, ['price', 'weight'])
)
//=> [
// {name: "マンゴー", price: 10000, weight: 900},
// {name: "りんご", price: 5000, weight: 5000},
// {name: "メロン", price: 5000, weight: 1200},
// {name: "ぶどう", price: 5000, weight: 1000},
// {name: "もも", price: 3000, weight: 2000},
// {name: "みかん", price: 1500, weight: 5000},
// {name: "バナナ", price: 1500, weight: 1000}
まとめ その2
"javascript ソート 複数キー"で検索すると、以下のようなコードが出てきます。
fruitsArr.sort((o1,o2) => {
if(o1.price > o2.price) return 1;
if(o1.price < o2.price) return -1;
if(o1.weight > o2.weight) return 1;
if(o1.weight < o2.weight) return -1;
return 0;
});
もちろん、これでも動作します。ただ、price
だけで4回も書かなければいけないことが面倒ですし、変更時に漏れも発生します。
また、この程度であればバグの入り込む余地も少なく、レビューもそれほど大変ではありません。しかし、数が大きくなってきたときに、今回のような実装も検討してはいかがでしょうか。
あとがき
Pythonだと、2つ(やそれ以上)のキーで並び替える時には、以下のようにシンプルに書けます。
list1 = [
{'name': 'ぶどう', 'price': 5000, 'weight': 1000},
# 中略
{'name': 'みかん', 'price': 1500, 'weight': 5000},
]
sorted(list1, key=itemgetter('price', 'weight'))
JavaScriptでも似たように書けないか試行錯誤したところ、上記のコードにたどりつきました。
参考リンク
- JavaScriptでPython風のzip_longest関数を実装する - Javaエンジニア、React+Redux+Firebaseでアプリを作る
-
JavaScript つい忘れてしまう配列のソート方法 - Qiita
(プログラムの例で、同じようにフルーツの名前、価格、重さを使用していますが全くの偶然です。不快に思いましたら申し訳ございません) - (Tips)オブジェクトのソート。複数の数値プロパティ - Qiita