Как сортировать многомерный массив по нескольким столбцам в JavaScript?

Я работаю над этой проблемой весь день без хорошего решения. Google тоже мало помог. У меня есть script, который должен принимать двухмерный массив с неизвестным числом строк/столбцов. script также должен принимать одномерный массив, содержащий список столбцов для сортировки, а другой - порядок сортировки. Вызов будет выглядеть примерно так:

var orderList = {0,4,3,1};
var orderDir = {asc,desc,desc,asc};
dataArr = do2DArraySort(dataArr, orderList, orderDir);

Функция do2DArraySort должна возвращать массив dataArr, отсортированный по первому столбцу (в порядке возрастания), затем по пятому (в порядке убывания), затем по третьему (в порядке убывания), затем по второму (в порядке убывания). Я смог сделать это на два уровня, используя код ниже, но он развалился, как только я попытался добавить третий столбец сортировки. Я понимаю, почему, но я не могу найти хороший способ заставить его работать.

Есть ли стандартный способ сделать это? Может ли кто-нибудь указать мне на хороший script онлайн, который я могу изучить и использовать в качестве шаблона? Или кто-то может предложить модификацию моего кода, чтобы он работал?

Спасибо!

//appends an array content to the original array
function addToArray(originalArray, addArray) {
    if (addArray.length != 0) {
        var curLength = 0;
        curLength = originalArray.length;
        var maxLength = 0;
        maxLength = curLength + addArray.length;  
        var itrerateArray = 0;
        for (var r = curLength; r < maxLength; r++) {   
            originalArray[r] = addArray[itrerateArray];
            itrerateArray++;
        }
    }
}

function do2DArraySort(arrayToBeSorted, sortColumnArray, sortDirectionArray) {
    if (arrayToBeSorted == "undefined" || arrayToBeSorted == "null") return arrayToBeSorted;
    if (arrayToBeSorted.length == 0) return arrayToBeSorted;
    if (sortColumnArray.length == 0) return arrayToBeSorted;
    tempArray = arrayToBeSorted; 
    var totalLength = sortColumnArray.length; 
    for(var m = 0; m < totalLength; m++) {
        if (m == 0) {   
            doBubbleSort(tempArray, tempArray.length, sortColumnArray[m], sortDirectionArray[m]);         
        } else {     
            doMultipleSort(tempArray, sortColumnArray[m], sortColumnArray[m-1], sortDirectionArray[m]);
        }
    } 
    return tempArray;
}

//check if a value exists in a single dimensional array
function checkIfExists(arrayToSearch, valueToSearch) {
    if (arrayToSearch == "undefined" || arrayToSearch == "null") return false;
    if (arrayToSearch.length == 0) return false;
    for (var k = 0; k < arrayToSearch.length; k++) {
        if (arrayToSearch[k] == valueToSearch) return true;
    }
    return false;
}

//sorts an 2D array based on the distinct values of the previous column
function doMultipleSort(sortedArray, currentCol, prevCol, sortDirection) {
    var resultArray = new Array(); 
    var newdistinctValuesArray = new Array();
    //finding distinct previous column values 
    for (var n = 0; n < sortedArray.length; n++) {
        if (checkIfExists(newdistinctValuesArray, sortedArray[n][prevCol]) == false) newdistinctValuesArray.push(sortedArray[n][prevCol]);
    }
    var recCursor = 0;
    var newTempArray = new Array(); var toStoreArray = 0; 
    //for each of the distinct values
    for (var x = 0; x < newdistinctValuesArray.length; x++) {
        toStoreArray = 0;
        newTempArray = new Array();  
        //find the rows with the same previous column value
        for (var y = 0; y < sortedArray.length; y++) {
            if (sortedArray[y][prevCol] == newdistinctValuesArray[x]) {
                newTempArray[toStoreArray] = sortedArray[y];
                toStoreArray++;
            }
        }       //sort the row based on the current column
        doBubbleSort(newTempArray, newTempArray.length, currentCol, sortDirection);
        //append it to the result array
        addToArray(resultArray, newTempArray);
    }
    tempArray = resultArray;
}

Ответ 1

Литерал массива [] предпочтительнее new Array. Обозначение {0,4,3,1} недопустимо и должно быть [0,4,3,1].

Есть ли необходимость изобретать колесо? Два массива можно объединить, используя:

originalArray = originalArray.concat(addArray);

Элементы могут быть добавлены до конца, используя:

array.push(element);

Массивы имеют метод сортировки массива. По умолчанию он отсортирован численно:

// sort elements numerically
var array = [1, 3, 2];
array.sort(); // array becomes [1, 2, 3]

Массивы также могут быть отменены. Продолжая предыдущий пример:

array = array.reverse(); //yields [3, 2, 1]

Чтобы обеспечить пользовательскую сортировку, вы можете передать необязательный аргумент функции array.sort():

array = [];
array[0] = [1, "first element"];
array[1] = [3, "second element"];
array[2] = [2, "third element"];
array.sort(function (element_a, element_b) {
    return element_a[0] - element_b[0];
});
/** array becomes (in order):
 * [1, "first element"]
 * [2, "third element"]
 * [3, "second element"]
 */

Элементы сохраняют свое положение, если элемент равен другому элементу. Используя это, вы можете комбинировать несколько алгоритмов сортировки. Вы должны применить свои настройки сортировки в обратном порядке, поскольку последний вид имеет приоритет над предыдущими. Чтобы отсортировать массив ниже по первому столбцу (по убыванию), а затем по второму столбцу (по возрастанию):

array = [];
array.push([1, 2, 4]);
array.push([1, 3, 3]);
array.push([2, 1, 3]);
array.push([1, 2, 3]);
// sort on second column
array.sort(function (element_a, element_b) {
    return element_a[1] - element_b[1];
});
// sort on first column, reverse sort
array.sort(function (element_a, element_b) {
    return element_b[0] - element_a[0];
});
/** result (note, 3rd column is not sorted, so the order of row 2+3 is preserved)
 * [2, 1, 3]
 * [1, 2, 4] (row 2)
 * [1, 2, 3] (row 3)
 * [1, 3, 3]
 */

Чтобы сортировать латинские строки (например, английский, немецкий, голландский), используйте String.localeCompare:

array.sort(function (element_a, element_b) {
    return element_a.localeCompare(element_b);
});

Чтобы отсортировать дату из объекта Date, используйте их представление в миллисекундах:

array.sort(function (element_a, element_b) {
    return element_a.getTime() - element_b.getTime();
});

Вы можете применить эту функцию сортировки ко всем типам данных, просто следуйте правилам:

x является результатом сравнения двух значений, которые должны быть возвращены функцией, переданной в array.sort.

  • x < 0: element_a должен появиться до element_b
  • x = 0: element_a и element_b равны, элементы не меняются местами
  • x > 0: element_a должен появиться после element_b

Ответ 2

Я бы предложил написать функцию более высокого порядка, которая принимает аргументы orderList и orderDir в качестве аргументов и возвращает функцию компаратора, которая может быть передана непосредственно в Array # sort. Таким образом, вы можете попробовать различные реализации (которые, например, торгуют простотой для производительности).

Этот непроверенный код демонстрирует идею:

var getComparator = function(orderList, orderDir) {
  var len = orderList.length; // XXX: assume == orderDir.length
  return function(a, b) {
    var cmp, ax, bx, i;
    for (i=0; i<len; i++) { # For each field and direction...
      ax = a[orderList[i]];
      bx = b[orderList[i]];
      cmp = ax.localeCompare(bx); # compare elements...
      if (cmp != 0) { # if not equal then indicate order...
        return (orderDir[i]=='asc') ? -1 : 1;
      }
    }
    return 0; # otherwise, indicate equality.
  };
};
dataArr.sort(getComparator(orderList, orderDir));

Обратите внимание, что вам нужно быть осторожным в использовании "localeCompare" против вычитания для строк и чисел, поэтому, возможно, этот аспект может быть параметризован и для функции getComparator.

Ответ 3

Основываясь на отличном ответе Lekensteyn, я разработал следующее решение для моих нужд. Я еще не прошел полное тестирование QA и не знаю, является ли это совершенным (на самом деле, я уверен, что это не так), но я надеюсь, что другие могут извлечь из этого пользу и использовать его для их потребностей. Я опубликую обновление, если бы были сделаны важные изменения.

function do2DArraySort(dataArr, orderList, orderDir) {
    for (x=orderList.length-1; x >= 0; x--) {
        if (orderDir[x] == 'asc') {
            dataArr.sort(sortMethodFunctionAsc);
        } else {
            dataArr.sort(sortMethodFunctionDesc);
        }
    }

    return dataArr;
}

function sortMethodFunctionAsc(a, b) {
    if ((IsNumeric(a[orderList[x]]) && IsNumeric(b[orderList[x]])) || (IsDate(a[orderList[x]]) && IsDate(b[orderList[x]]))) {
        return a[orderList[x]] - b[orderList[x]];
    } else {
        if (a[orderList[x]].toString() > b[orderList[x]].toString()) {
            return 1;
        } else if (a[orderList[x]].toString() < b[orderList[x]].toString()) {
            return -1;
        } else {
            return 0;
        }
    }
}

function sortMethodFunctionDesc(a, b) {
    if ((IsNumeric(a[orderList[x]]) && IsNumeric(b[orderList[x]])) || (IsDate(a[orderList[x]]) && IsDate(b[orderList[x]]))) {
        return b[orderList[x]] - a[orderList[x]];
    } else {
        if (a[orderList[x]].toString() < b[orderList[x]].toString()) {
            return 1;
        } else if (a[orderList[x]].toString() > b[orderList[x]].toString()) {
            return -1;
        } else {
            return 0;
        }
    }
}


function IsNumeric(input) {
    return (input - 0) == input && input.length > 0;
}

function IsDate(testValue) {
    var returnValue = false;
    var testDate;
    try {
        testDate = new Date(testValue);
        if (!isNaN(testDate)) {
            returnValue = true;
        } else {
            returnValue = false;
        }
    }
    catch (e) {
        returnValue = false;
    }
    return returnValue;
}

Ответ 4

var arr = [27, 2, 4, 13]
arr.sort();

устанавливает arr как [13, 2, 27, 4], потому что массивы сортируются как строки по умолчанию в JavaScript

arr.sort(function (a, b) {
    return a - b;
});

устанавливает arr как [2, 4, 13, 27], сортируя численно вперед.

arr.sort(function (a, b) {
    return b - a;
});

устанавливает arr как [27, 13, 4, 2], сортируя численно в обратном порядке.

var marr = [[]];
marr.shift();

marr.push(["frog", 4, 27, 13]);
marr.push(["frog", 11, 5, 12]);
marr.push(["cat", 16, 3, 5]);
marr.push(["dog", 11, 7, 21]);
marr.push(["cat", 16, 21, 6]);
marr.push(["dog", 10, 280, 5]);
marr.push(["dog", 10, 32, 5]);

marr.sort();

устанавливает marr следующим образом, сортируя строки массива по столбцам в порядке строк.

["cat", 16, 21, 6]
["cat", 16, 3, 5]
["dog", 10, 280, 5]
["dog", 10, 32, 5]
["dog", 11, 7, 21]
["frog", 11, 5, 12]
["frog", 4, 27, 13]

Вызов сортировки по столбцу позволяет сортировать по одному столбцу. сортировать строки по 3-му столбцу как число..

marr.sort(function (a, b) {
    return a[2] - b[2];
});

["cat", 16, 3, 5]
["frog", 11, 5, 12]
["dog", 11, 7, 21]
["cat", 16, 21, 6]
["frog", 4, 27, 13]
["dog", 10, 32, 5]
["dog", 10, 280, 5]

затем сортируйте 4-й столбец в обратном порядке как число.

marr.sort(function (a, b) {
    return b[3] - a[3];
});

["dog", 11, 7, 21]
["frog", 4, 27, 13]
["frog", 11, 5, 12]
["cat", 16, 21, 6]
["cat", 16, 3, 5]
["dog", 10, 32, 5]
["dog", 10, 280, 5]

затем сортируйте 2-й столбец по возрастанию как число

marr.sort(function (a, b) {
    return a[1] - b[1];
});

["frog", 4, 27, 13]
["dog", 10, 32, 5]
["dog", 10, 280, 5]
["dog", 11, 7, 21]
["frog", 11, 5, 12]
["cat", 16, 21, 6]
["cat", 16, 3, 5]

Обратите внимание, что каждый раз, когда вы сортируете, сохраняется предыдущий порядок, где новый столбец совпадает с последовательной строкой.

Теперь вы можете свернуть в альфа-сортировку первого столбца

// asc
marr.sort(function (a, b) {
    return (a[0] < b[0]) ? -1 : 1;
});

["cat", 16, 21, 6]
["cat", 16, 3, 5]
["dog", 10, 32, 5]
["dog", 10, 280, 5]
["dog", 11, 7, 21]
["frog", 4, 27, 13]
["frog", 11, 5, 12]

// desc
marr.sort(function (a, b) {
    return (a[0] > b[0]) ? -1 : 1;
});

["frog", 4, 27, 13]
["frog", 11, 5, 12]
["dog", 10, 32, 5]
["dog", 10, 280, 5]
["dog", 11, 7, 21]
["cat", 16, 21, 6]
["cat", 16, 3, 5]

отсортировать все числовые столбцы в цикле desc: 4, 3, 2, затем отсортировать 1-й столбец asc как строку

for (var colid = 3; colid > 0; colid--) {
    marr.sort(function (a, b) {
        return (b[colid] - a[colid]);
    });
}

// 1st row as string asc
marr.sort(function (a, b) {
    return (a[0] < b[0]) ? -1 : 1;
});

["cat", 16, 21, 6]
["cat", 16, 3, 5]
["dog", 11, 7, 21]
["dog", 10, 280, 5]
["dog", 10, 32, 5]
["frog", 11, 5, 12]
["frog", 4, 27, 13]

объединить эти сортировки.. более логичным образом, в порядке того, какой столбец вы хотите отсортировать и как вы хотите отсортировать в первую очередь

// c1 asc, c2 desc, c3 asc, c4 asc
marr.sort(function (a, b) {
    return (a[0] < b[0]) ? -1 : (a[0] == b[0]) ?
        (b[1] - a[1]) || (a[2] - b[2]) || (a[3] - b[3]) : 1;
});

["cat", 16, 3, 5]
["cat", 16, 21, 6]
["dog", 11, 7, 21]
["dog", 10, 32, 5]
["dog", 10, 280, 5]
["frog", 11, 5, 12]
["frog", 4, 27, 13]

Ответ 5

На этот вопрос уже есть хорошие ответы, я хотел бы добавить короткие функции для обработки решения, основанного на сортировке нескольких ключей, по адресу https://stackoverflow.com/users/2279116/shinobi.

// sort function handle for multiple keys
const sortCols  = (a, b, attrs) => Object.keys(attrs)
    .reduce((diff, k) =>  diff == 0 ? attrs[k](a[k], b[k]) : diff, 0);

Давайте возьмем следующий пример

const array = [
    [1, 'hello', 4],
    [1, 'how', 3],
    [2, 'are', 3],
    [1, 'hello', 1],
    [1, 'hello', 3]
];

array.sort((a, b) => sortCols(a, b, { 
   0: (a, b) => a - b, 
   1: (a, b) => a.localeCompare(b), 
   2: (a, b) => b - a
}))

Выход будет следующим.

[ 1, "hello", 4 ]​
[ 1, "hello", 3 ]​
[ 1, "hello", 1 ]​
[ 1, "how", 3 ]
​[ 2, "are", 3 ]