1

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

nikkop
  • 63
  • 8
  • 3
    Are you saying you want to sort by the result of each sum, or by the value of the first number in each sum (which for the data shown would give the same result)? – nnnnnn Mar 14 '17 at 07:57
  • @nnnnnn The value should be treated as strings, so no math operations at all. The issue now is that "$100 + $9" is being treated as a lower value than "$2 + $0.5" which is not what I'm trying to achieve. – nikkop Mar 14 '17 at 08:01
  • 2
    @nikkop: That doesn't answer nnnnnn's question, and doesn't really make sense. You can't (reasonably) get the order you want by treating them as strings. You either A) Extract the numeric value of the first operand and sort by that, or B) Do the sum and sort by the result. Which you do depends on what end result you want. As nnnnnn said, both give the sample result above (but not all inputs would). – T.J. Crowder Mar 14 '17 at 08:02
  • If you really want to treat the strings as strings and implicitly sort by the first number in the string, you want to use natural sort. That would make this question a possible duplicate of [javascript natural sort](http://stackoverflow.com/questions/14599321/javascript-natural-sort) – Just a student Mar 14 '17 at 08:05
  • 1
    @T.J.Crowder Sorry if I misunderstood the question. However, doing the sum and then sorting 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. – nikkop Mar 14 '17 at 08:10

3 Answers3

5

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);
Pranav C Balan
  • 113,687
  • 23
  • 165
  • 188
  • Thanks for this code. However I don't wish to sort on the sum as it will yield unwanted results (please check the original post where I updated with information) – nikkop Mar 14 '17 at 08:15
  • Thanks a lot but unfortunately your need code outputs a weird order: `["$0.9 + $0.1", "$20 + $2", "$0.7 + $0.4", "$5 + $0.5", "$0 + $0.01", "$100 + $9", "$1 + $1", "$2 + $0.5"]` – nikkop Mar 14 '17 at 08:29
2

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);
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
2

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.

trincot
  • 317,000
  • 35
  • 244
  • 286
  • Added a variant that will not order by the sum, but by the individual values. – trincot Mar 14 '17 at 08:20
  • Works like a charm! The output is as the desired one. Thanks a bunch for the description, I learned a lot from it. – nikkop Mar 14 '17 at 08:36