Nate
Nate

Reputation: 31

sort js array by two closest fields

i'd like to sort an array by two fields while providing a desired number to sort them by. i tried using lodash but haven't got the desired results.

for example

const data = [
  {name: 'first', score: 22, average: 59},
  {name: 'second', score: 34, average: 83},
  {name: 'third', score: 40, average: 24},
  {name: 'fourth', score: 29, average: 49},
  {name: 'fifth', score: 23, average: 55}
];
// call function like this
sortByTwoFields({field:'score', number:21}, {field:'average', number:50}); 

the desired result would be

const result = [
  {name: 'fifth', score: 23, average: 55},
  {name: 'fourth', score: 29, average: 49},
  {name: 'first', score: 22, average: 59},
  {name: 'third', score: 40, average: 24},
  {name: 'second', score: 34, average: 83}
];

any ideas would be greatly appreciated

Upvotes: 3

Views: 135

Answers (4)

tokland
tokland

Reputation: 67900

You can as easily make it generic by sorting by any number of fields, not just two:

function sortByFields(objs, fields) {
  const diffToFields = obj =>
    _(fields).map(f => Math.abs(obj[f] - f.number)).sum();
  return _(objs).sortBy(diffToFields).value();
}

const sortedData = sortByFields(data, [
  {field: 'score', number: 21},
  {field: 'average', number: 50},
]);

Upvotes: 0

Koushik Chatterjee
Koushik Chatterjee

Reputation: 4175

How about simply use _.sortBy of lodash??

_.sortBy(data, [a=> Math.abs(a.score - 21), a=> Math.abs(a.average - 50)])

Isn't this enough?

EDIT

Well, Yes! if you need this sorting (execution) from one place and with only (exactly that 2) 2 nested fields, then this ole liner is ok, but if you are calling from several places and with diff fields (nested) with closest value, then you can wrap this in a function, prepare the itarees dynamically and pass it to sortBy.

Here is a working example for that:

var data = [
  {name: 'first', score: 22, average: 59},
  {name: 'second', score: 34, average: 83},
  {name: 'second', score: 34, average: 80},
  {name: 'third', score: 40, average: 24},
  {name: 'fourth', score: 29, average: 49},
  {name: 'fifth', score: 23, average: 55}
];

function sortByClosest(arr, ...fields) {
    //this will create array of callbacks, similar to what user in first example
    let iteratees = fields.map(f => o => Math.abs(f.number - o[f.field]));
    return _.sortBy(arr, iteratees);
}

//pass data for "arr" argument, and all other
//sort key/closest-value for fields array argument
var res = sortByClosest(data, {field:'score', number:21},{field:'average',number:50});

console.log(res);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script>

However key, closest value can be passes in more shorter format like {score: 21, average: 50}, in that case the logic to generate iteratees needs to adjust accordingly, so it will became:

_.map(fields, (v,k)=> o=> Math.abs(v - o[k]))

And you need to call it with a single object (as shown above), here is the call for that:

sortByClosest(data, {score: 21, average: 50});

Upvotes: 1

frobinsonj
frobinsonj

Reputation: 1167

I don't believe your expected result is correct as the fourth has a smaller total difference.

Anyway, the function will work out the total difference between the values and the numbers given and then sort by that total.

function sortByTwoFields(data, field1, field2) {
  data.sort(function (a, b) {
    // Work out difference of two fields and add them together to get the total
    var totalDifferenceA = Math.abs(field1.number - a[field1.field]) + Math.abs(field2.number - a[field2.field]);
    var totalDifferenceB = Math.abs(field1.number - b[field1.field]) + Math.abs(field2.number - b[field2.field]);
    
    // If a has a smaller total then b it should come first
    return totalDifferenceA - totalDifferenceB;
  });
}

const data = [
  {name: 'first', score: 22, average: 59},
  {name: 'second', score: 34, average: 83},
  {name: 'third', score: 40, average: 24},
  {name: 'fourth', score: 29, average: 49},
  {name: 'fifth', score: 23, average: 55}
];

sortByTwoFields(data, {field: 'score', number: 21}, {field: 'average', number: 50});
console.log(data);

Upvotes: 0

Aplet123
Aplet123

Reputation: 35540

It appears that your desired result is slightly off as fourth has a total difference of 9 while first has a total difference of 10. The function below is called like sortByTwoFields(data, {field:'score', number:21}, {field:'average', number:50}). You can add a weight attribute to an object if you want to prioritize it, otherwise it defaults to 1.

function sortByTwoFields (data, info1, info2) {
    if (! ("weight" in info1)) {
        info1.weight = 1;
    }
    if (! ("weight" in info2)) {
        info2.weight = 1;
    }
    return data.sort(function (a, b) {
        var adiff1 = a[info1.field] - info1.number;
        var adiff2 = a[info2.field] - info2.number;
        var bdiff1 = b[info1.field] - info1.number;
        var bdiff2 = b[info2.field] - info2.number;
        return Math.abs(adiff1) * info1.weight + Math.abs(adiff2) * info2.weight - Math.abs(bdiff1) * info1.weight - Math.abs(bdiff2) * info2.weight;
    });
}

Upvotes: 0

Related Questions