nikkop
nikkop

Reputation: 63

Sorting "advanced" numeric strings by numeric order

Imagine having this array:

var fees = [
    '$0.9 + $0.1',
    '$20 + $2',
    '$0.7 + $0.4',
    '$5 + $0.5',
    '$0 + $0.01',
    '$100 + $9',
    '$1 + $1',
    '$2 + $0.5'
];

How would I, with vanilla JavaScript, go about sorting these string values in numeric ascending order?

The desired output after sorting:

['$0 + $0.01', '$0.7 + $0.4', '$0.9 + $0.1', '$1 + $1', '$2 + $0.5', '$5 + $0.5', '$20 + $2', '$100 + $9']

I tried the following:

function mySort(a, b) {
    return ((a < b) ? -1 : ((a > b) ? 1 : 0));
}

But that simply outputs:

["$0 + $0.01", "$0.7 + $0.4", "$0.9 + $.1", "$1 + $1", "$100 + $9", "$2 + $0.5", "$20 + $2", "$5 + $0.5"]`

Is this possible in a neat and logical way?

I don't wish to get the sum as it will yield unwanted results. Consider the example of "$0.9 + $0.1" and "$0.7 + $0.4". "$0.9 + $0.1" will be a lower value as the sum is 1, but I would like to sort so that "$0.7 + $0.4" is a lower value instead. So basically the wish is to sort on the first number ascending, and if the first number is the same between two values, then sort those on the second number

Upvotes: 1

Views: 83

Answers (3)

Pranav C Balan
Pranav C Balan

Reputation: 115212

Sort based on the sum of numbers in string.

var fees = ['$0.9 + $.1', '$20 + $2', '$5 + $0.5', '$0 + $0.01', '$100 + $9', '$1 + $1', '$2 + $0.5'];

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

function getSum(str) {
  return str
    // remove the $ and space
    .replace(/[^\d+.]/g, '')
    //split by + symbol
    .split('+')
    // get the sum
    .reduce(function(sum, s) {
      // parse and add with sum
      return sum + (Number(s) || 0);
      // set inital value as sum
    }, 0)
}

console.log(fees);


You can speed up the process by using an additional object which holds the sum.

var fees = ['$0.9 + $.1', '$20 + $2', '$5 + $0.5', '$0 + $0.01', '$100 + $9', '$1 + $1', '$2 + $0.5'];

var ref = fees.reduce(function(obj, str) {
  // define object property if not defined
  obj[str] = str in obj || str
    // remove the $ and space
    .replace(/[^\d+.]/g, '')
    //split by + symbol
    .split('+')
    // get the sum
    .reduce(function(sum, s) {
      // parse and add with sum
      return sum + (Number(s) || 0);
      // set inital value as sum
    }, 0);
  // return the object reference
  return obj;
  // set initial value as an empty objecct
}, {})

fees.sort(function(a, b) {
  return ref[a] - ref[b];
})



console.log(fees);


UPDATE: Since you had updated the question you need to compare the individual parts.

var fees = ['$0.9 + $.1', '$20 + $2', '$5 + $0.5', '$0 + $0.01', '$100 + $9', '$1 + $1', '$2 + $0.5'];

fees.sort(function(a, b) {
  // get numbers from a
  var arrA = a.replace(/[^\d.+]/g, '').split('+');
  // get numbers from b
  var arrB = b.replace(/[^\d.+]/g, '').split('+');

  // generate sort value
  return arrA[0] - arrB[0] || arrA[1] - arrB[1];
})

console.log(fees);

Upvotes: 5

trincot
trincot

Reputation: 350147

I would use this three-step algorithm:

  1. add to each of the elements the two values to be sorted by
  2. sort by those values
  3. drop that extra information again

Here is the ES6 code.

var result = fees.map( s => [s].concat(s.match(/[\d.]+/g).map(Number)) )
                .sort( (a, b) => a[1] - b[1] || a[2] - b[2])
                .map( a => a[0] );

The s.match() call will produce an array with two matches (strings). The map() call will turn those into numbers, and concat() will add those two numbers to form a triplet with the original string.

The sorting callback will sort by the first number (which is at index 1), and if equal (then the difference is 0), the second number will be used for sorting.

The final map will take the original string from the triplets, thus dropping the extra information that was used for sorting.

In the snippet below I have added one element to your sample data ($5.1 + $0.1), which will show the difference with the output you would get if sorted by sums (which you do not want, as indicated in the question's update).

var fees = [
    '$0.9 + $.1',
    '$20 + $2',
    '$5 + $0.5',
    '$5.1 + $0.1', // smaller sum than previous, but larger first value
    '$0 + $0.01',
    '$100 + $9',
    '$1 + $1',
    '$2 + $0.5'
];

// 1. add the sum to each of the elements
// 2. sort by the sums
// 3. drop the sums
var result = fees.map( s => [s].concat(s.match(/[\d.]+/g).map(Number)) )
                .sort( (a, b) => a[1] - b[1] || a[2] - b[2])
                .map( a => a[0] );

console.log(result);

This way of doing it will apply the regular expression to each string only once, which is not (always) the case when you do this on-the-fly within a sort callback. The sort algorithm will in general have to compare the same value several times, so you gain performance when moving logic out of the sort callback where possible.

Upvotes: 2

Nina Scholz
Nina Scholz

Reputation: 386560

You could get the parts of the strings, add them and take the delta of two elements for sorting.

var fees = ['$0.9 + $.1', '$20 + $2', '$5 + $0.5', '$0 + $0.01', '$100 + $9', '$1 + $1', '$2 + $0.5'];

fees.sort(function (a, b) {
    function getValues(s) {
        return s.match(/([0-9.]+)/g).map(Number);
    }
    
    var aa = getValues(a),
        bb = getValues(b);
    return aa[0] + aa[1] - (bb[0] + bb[1]);
});

console.log(fees);

Upvotes: 2

Related Questions