Sublinhado: sortBy () com base em vários atributos

115

Estou tentando classificar uma matriz com objetos com base em vários atributos. Ou seja, se o primeiro atributo é o mesmo entre dois objetos, um segundo atributo deve ser usado para comapar os dois objetos. Por exemplo, considere a seguinte matriz:

var patients = [
             [{name: 'John', roomNumber: 1, bedNumber: 1}],
             [{name: 'Lisa', roomNumber: 1, bedNumber: 2}],
             [{name: 'Chris', roomNumber: 2, bedNumber: 1}],
             [{name: 'Omar', roomNumber: 3, bedNumber: 1}]
               ];

Classificando-os pelo roomNumberatributo, eu usaria o seguinte código:

var sortedArray = _.sortBy(patients, function(patient) {
    return patient[0].roomNumber;
});

Isso funciona bem, mas como faço para que 'John' e 'Lisa' sejam classificados corretamente?

Christian R
fonte

Respostas:

250

sortBy diz que é um algoritmo de classificação estável, então você deve ser capaz de classificar pela segunda propriedade primeiro e, em seguida, classificar novamente pela primeira propriedade, assim:

var sortedArray = _(patients).chain().sortBy(function(patient) {
    return patient[0].name;
}).sortBy(function(patient) {
    return patient[0].roomNumber;
}).value();

Quando o segundo sortBydescobrir que John e Lisa têm o mesmo número de quarto, ele os manterá na ordem em que os encontrou, que o primeiro sortBydefiniu como "Lisa, John".

Rory MacLeod
fonte
12
Há uma postagem no blog que expande isso e inclui boas informações sobre a classificação de propriedades ascendentes e descendentes.
Alex C
4
Uma solução mais simples para a classificação em cadeia pode ser encontrada aqui . Para ser justo, parece que a postagem do blog foi escrita depois que essas respostas foram dadas, mas isso me ajudou a descobrir isso depois de tentar usar o código na resposta acima e falhar.
Mike Devenney
1
Tem certeza de que o paciente [0] .name e o paciente [1] .roomNumber devem ter o índice lá? paciente não é uma matriz ...
StinkyCat
O [0]indexador é necessário porque no exemplo original, patientsé uma matriz de matrizes. É também por isso que a "solução mais simples" na postagem do blog mencionada em outro comentário não funcionará aqui.
Rory MacLeod
1
@ac_fire Aqui está um arquivo desse link agora morto: archive.is/tiatQ
lustig
52

Aqui está um truque hacky que às vezes uso nesses casos: combine as propriedades de forma que o resultado seja classificável:

var sortedArray = _.sortBy(patients, function(patient) {
  return [patient[0].roomNumber, patient[0].name].join("_");
});

No entanto, como eu disse, isso é muito hacky. Para fazer isso corretamente, você provavelmente gostaria de usar o sortmétodo principal do JavaScript :

patients.sort(function(x, y) {
  var roomX = x[0].roomNumber;
  var roomY = y[0].roomNumber;
  if (roomX !== roomY) {
    return compare(roomX, roomY);
  }
  return compare(x[0].name, y[0].name);
});

// General comparison function for convenience
function compare(x, y) {
  if (x === y) {
    return 0;
  }
  return x > y ? 1 : -1;
}

Claro, isso classificará seu array no lugar. Se você quiser uma cópia classificada (como _.sortBydaria a você), clone a matriz primeiro:

function sortOutOfPlace(sequence, sorter) {
  var copy = _.clone(sequence);
  copy.sort(sorter);
  return copy;
}

Por causa do tédio, acabei de escrever uma solução geral (para classificar por qualquer número arbitrário de chaves) para isso também: dê uma olhada .

Dan Tao
fonte
Muito obrigado por esta solução acabei usando a segunda uma vez que meus atributos podem ser strings e números. Portanto, não parece haver uma maneira nativa simples de classificar matrizes?
Christian R
3
Por que não basta return [patient[0].roomNumber, patient[0].name];sem o join?
Csaba Toth
1
O link para sua solução geral parece estar quebrado (ou talvez eu não consiga acessá-lo por meio de nosso servidor proxy). Você poderia postar aqui?
Zev Spitz
Além disso, como é que compareos valores do identificador que não são valores primitivos - undefined, nullou objetos simples?
Zev Spitz
Para sua informação, esse hack só funciona se você garantir que o comprimento de str de cada valor seja o mesmo para todos os itens na matriz.
miex
32

Sei que estou atrasado para a festa, mas queria acrescentar isso para quem precisa de uma solução mais limpa e rápida que as já sugeridas. Você pode encadear chamadas sortBy na ordem da propriedade menos importante para a propriedade mais importante. No código a seguir, crio uma nova matriz de pacientes classificados por Nome em RoomNumber a partir da matriz original chamada pacientes .

var sortedPatients = _.chain(patients)
  .sortBy('Name')
  .sortBy('RoomNumber')
  .value();
Mike Devenney
fonte
4
Mesmo que você esteja atrasado, você ainda está correto :) Obrigado!
Allan Jikamu de
3
Legal, muito limpo.
Jason Turan,
11

btw seu inicializador para pacientes é um pouco estranho, não é? por que você não inicializa esta variável como isto -como um verdadeiro array de objetos -você pode fazer isso usando _.flatten () e não como um array de arrays de um único objeto, talvez seja um problema de digitação):

var patients = [
        {name: 'Omar', roomNumber: 3, bedNumber: 1},
        {name: 'John', roomNumber: 1, bedNumber: 1},
        {name: 'Chris', roomNumber: 2, bedNumber: 1},
        {name: 'Lisa', roomNumber: 1, bedNumber: 2},
        {name: 'Kiko', roomNumber: 1, bedNumber: 2}
        ];

Classifiquei a lista de maneira diferente e coloquei Kiko na cama de Lisa; apenas para se divertir e ver que mudanças seriam feitas ...

var sorted = _(patients).sortBy( 
                    function(patient){
                       return [patient.roomNumber, patient.bedNumber, patient.name];
                    });

inspecione classificado e você verá isso

[
{bedNumber: 1, name: "John", roomNumber: 1}, 
{bedNumber: 2, name: "Kiko", roomNumber: 1}, 
{bedNumber: 2, name: "Lisa", roomNumber: 1}, 
{bedNumber: 1, name: "Chris", roomNumber: 2}, 
{bedNumber: 1, name: "Omar", roomNumber: 3}
]

então minha resposta é: use uma matriz em sua função de retorno de chamada, isso é bastante semelhante à resposta de Dan Tao , apenas esqueci a junção (talvez porque eu removi a matriz de matrizes de item único :))
Usando sua estrutura de dados, então seria :

var sorted = _(patients).chain()
                        .flatten()
                        .sortBy( function(patient){
                              return [patient.roomNumber, 
                                     patient.bedNumber, 
                                     patient.name];
                        })
                        .value();

e um teste seria interessante ...

zobidafly
fonte
Sério, essa é a resposta
Radek Duchoň
7

Nenhuma dessas respostas é ideal como método de propósito geral para o uso de vários campos em uma classificação. Todas as abordagens acima são ineficientes, pois exigem a classificação da matriz várias vezes (o que, em uma lista grande o suficiente, pode tornar as coisas muito lentas) ou geram grandes quantidades de objetos de lixo que a VM precisará limpar (e, por fim, desacelerar o programa desativado).

Esta é uma solução que é rápida, eficiente, permite fácil classificação reversa e pode ser usada com underscoreou lodash, ou diretamente comArray.sort

A parte mais importante é o compositeComparatormétodo, que pega uma matriz de funções de comparador e retorna uma nova função de comparador composto.

/**
 * Chains a comparator function to another comparator
 * and returns the result of the first comparator, unless
 * the first comparator returns 0, in which case the
 * result of the second comparator is used.
 */
function makeChainedComparator(first, next) {
  return function(a, b) {
    var result = first(a, b);
    if (result !== 0) return result;
    return next(a, b);
  }
}

/**
 * Given an array of comparators, returns a new comparator with
 * descending priority such that
 * the next comparator will only be used if the precending on returned
 * 0 (ie, found the two objects to be equal)
 *
 * Allows multiple sorts to be used simply. For example,
 * sort by column a, then sort by column b, then sort by column c
 */
function compositeComparator(comparators) {
  return comparators.reduceRight(function(memo, comparator) {
    return makeChainedComparator(comparator, memo);
  });
}

Você também precisará de uma função de comparador para comparar os campos que deseja classificar. A naturalSortfunção criará um comparador para um determinado campo. Escrever um comparador para classificação reversa também é trivial.

function naturalSort(field) {
  return function(a, b) {
    var c1 = a[field];
    var c2 = b[field];
    if (c1 > c2) return 1;
    if (c1 < c2) return -1;
    return 0;
  }
}

(Todo o código até agora é reutilizável e pode ser mantido no módulo utilitário, por exemplo)

Em seguida, você precisa criar o comparador composto. Para nosso exemplo, seria assim:

var cmp = compositeComparator([naturalSort('roomNumber'), naturalSort('name')]);

Isso classificará pelo número do quarto, seguido pelo nome. Adicionar critérios de classificação adicionais é trivial e não afeta o desempenho da classificação.

var patients = [
 {name: 'John', roomNumber: 3, bedNumber: 1},
 {name: 'Omar', roomNumber: 2, bedNumber: 1},
 {name: 'Lisa', roomNumber: 2, bedNumber: 2},
 {name: 'Chris', roomNumber: 1, bedNumber: 1},
];

// Sort using the composite
patients.sort(cmp);

console.log(patients);

Retorna o seguinte

[ { name: 'Chris', roomNumber: 1, bedNumber: 1 },
  { name: 'Lisa', roomNumber: 2, bedNumber: 2 },
  { name: 'Omar', roomNumber: 2, bedNumber: 1 },
  { name: 'John', roomNumber: 3, bedNumber: 1 } ]

A razão pela qual eu prefiro este método é que ele permite uma classificação rápida em um número arbitrário de campos, não gera muito lixo ou executa concatenação de strings dentro da classificação e pode ser facilmente usado para que algumas colunas sejam classificadas de forma reversa enquanto as colunas de ordem usam natural ordenar.

Andrew Newdigate
fonte
2

Talvez os motores de underscore.js ou apenas Javascript sejam diferentes agora de quando essas respostas foram escritas, mas consegui resolver isso apenas retornando um array das chaves de classificação.

var input = [];

for (var i = 0; i < 20; ++i) {
  input.push({
    a: Math.round(100 * Math.random()),
    b: Math.round(3 * Math.random())
  })
}

var output = _.sortBy(input, function(o) {
  return [o.b, o.a];
});

// output is now sorted by b ascending, a ascending

Em ação, consulte este violino: https://jsfiddle.net/mikeular/xenu3u91/

Mike K
fonte
2

Basta retornar uma matriz de propriedades com as quais deseja classificar:

Sintaxe ES6

var sortedArray = _.sortBy(patients, patient => [patient[0].name, patient[1].roomNumber])

Sintaxe ES5

var sortedArray = _.sortBy(patients, function(patient) { 
    return [patient[0].name, patient[1].roomNumber]
})

Isso não tem efeitos colaterais na conversão de um número em string.

Lucky Soni
fonte
1

Você pode concatenar as propriedades que deseja classificar no iterador:

return [patient[0].roomNumber,patient[0].name].join('|');

ou algo equivalente.

NOTA: Uma vez que você está convertendo o atributo numérico roomNumber em uma string, você teria que fazer algo se tivesse números de quartos> 10. Caso contrário, 11 virá antes de 2. Você pode preencher com zeros à esquerda para resolver o problema, ou seja, 01 em vez de 1

Mark Sherretta
fonte
1

Acho melhor você usar em _.orderByvez de sortBy:

_.orderBy(patients, ['name', 'roomNumber'], ['asc', 'desc'])
ZhangYi
fonte
4
Tem certeza de que orderBy está em sublinhado? Não consigo ver nos documentos ou no meu arquivo .d.ts.
Zachary Dow
1
Não há orderBy em sublinhado.
AfroMogli de
1
_.orderByfunciona, mas é um método da biblioteca lodash, não sublinhado: lodash.com/docs/4.17.4#orderBy Lodash é principalmente um substituto imediato para o sublinhado, portanto, pode ser apropriado para o OP.
Mike K
0

Se você estiver usando Angular, poderá usar seu filtro de número no arquivo html em vez de adicionar qualquer manipulador JS ou CSS. Por exemplo:

  No fractions: <span>{{val | number:0}}</span><br>

Nesse exemplo, se val = 1234567, será exibido como

  No fractions: 1,234,567

Exemplo e mais orientações em: https://docs.angularjs.org/api/ng/filter/number

sucata
fonte