29

I have an array of following strings:

['5.5.1', '4.21.0', '4.22.0', '6.1.0', '5.1.0', '4.5.0'] 

...etc.

I need a solution that will give me following ordered result

['4.5.0', '4.21.0', '4.22.0', '5.1.0', '5.5.1', '6.1.0'].

I tried to implement a sort so it first sorts by the numbers in the first position, than in case of equality, sort by the numbers in the second position (after the first dot), and so on...

I tried using sort() and localeCompare(), but if I have elements '4.5.0' and '4.11.0', I get them sorted as ['4.11.0','4.5.0'], but I need to get ['4.5.0','4.11.0'].

How can I achieve this?

Toto
  • 89,455
  • 62
  • 89
  • 125
user3921420
  • 463
  • 1
  • 5
  • 10

16 Answers16

62

You could prepend all parts to fixed size strings, then sort that, and finally remove the padding again.

var arr = ['5.5.1', '4.21.0', '4.22.0', '6.1.0', '5.1.0', '4.5.0'];
arr = arr.map( a => a.split('.').map( n => +n+100000 ).join('.') ).sort()
         .map( a => a.split('.').map( n => +n-100000 ).join('.') );

console.log(arr)

Obviously you have to choose the size of the number 100000 wisely: it should have at least one more digit than your largest number part will ever have.

With regular expression

The same manipulation can be achieved without having to split & join, when you use the callback argument to the replace method:

var arr = ['5.5.1', '4.21.0', '4.22.0', '6.1.0', '5.1.0', '4.5.0'];
arr = arr.map( a => a.replace(/\d+/g, n => +n+100000 ) ).sort()
         .map( a => a.replace(/\d+/g, n => +n-100000 ) );

console.log(arr)

Defining the padding function once only

As both the padding and its reverse functions are so similar, it seemed a nice exercise to use one function f for both, with an extra argument defining the "direction" (1=padding, -1=unpadding). This resulted in this quite obscure, and extreme code. Consider this just for fun, not for real use:

var arr = ['5.5.1', '4.21.0', '4.22.0', '6.1.0', '5.1.0', '4.5.0'];
arr = (f=>f(f(arr,1).sort(),-1)) ((arr,v)=>arr.map(a=>a.replace(/\d+/g,n=>+n+v*100000)));

console.log(arr);

Use the sort compare callback function

You could use the compare function argument of sort to achieve the same:

arr.sort( (a, b) => a.replace(/\d+/g, n => +n+100000 )
                     .localeCompare(b.replace(/\d+/g, n => +n+100000 )) );

But for larger arrays this will lead to slower performance. This is because the sorting algorithm will often need to compare a certain value several times, each time with a different value from the array. This means that the padding will have to be executed multiple times for the same number. For this reason, it will be faster for larger arrays to first apply the padding in the whole array, then use the standard sort, and then remove the padding again.

But for shorter arrays, this approach might still be the fastest. In that case, the so-called natural sort option -- that can be achieved with the extra arguments of localeCompare -- will be more efficient than the padding method:

var arr = ['5.5.1', '4.21.0', '4.22.0', '6.1.0', '5.1.0', '4.5.0'];
arr = arr.sort( (a, b) => a.localeCompare(b, undefined, { numeric:true }) );

console.log(arr);

More about the padding and unary plus

To see how the padding works, look at the intermediate result it generates:

[ "100005.100005.100001", "100004.100021.100000", "100004.100022.100000", 
  "100006.100001.100000", "100005.100001.100000" ]

Concerning the expression +n+100000, note that the first + is the unary plus and is the most efficient way to convert a string-encoded decimal number to its numerical equivalent. The 100000 is added to make the number have a fixed number of digits. Of course, it could just as well be 200000 or 300000. Note that this addition does not change the order the numbers will have when they would be sorted numerically.

The above is just one way to pad a string. See this Q&A for some other alternatives.

trincot
  • 317,000
  • 35
  • 244
  • 286
  • Instead of `+part + 100000`, I did a regular string padding: `'0'.repeat(8 - part.length) + part`. This way you also support versions like "1.0a". – Gras Double Dec 20 '17 at 23:21
  • Ah ah, IE lacks String.prototype.repeat()… because of this, you'll have to instead do: `'00000000'.substr(0, 8 - part.length) + part` – Gras Double Dec 20 '17 at 23:40
  • Also discovered this method fails on `['1.1', '1.01']`… – Gras Double Dec 21 '17 at 01:11
  • @GrasDouble, that `['1.1', '1.01']` is an interesting case. This could get confusing: `['4.21.0', '4.5.0', '4.13.0', '4.06.0']`... There are at least two ways to interpret an ascending sequence for this: (1) by numerical value: `['4.5.0', '4.06.0', '4.13.0', '4.21.0']`, or (2) by character comparison: `['4.06.0', '4.13.0', '4.21.0', '4.5.0']` and in the OP's example 4.21 was expected to to be higher than 4.5, which is according to logic (1). I suppose one would have to choose by which logic (1 or 2) the order should be generated. – trincot Dec 21 '17 at 15:55
  • Yes, this case is tricky, both to interpret and to programmatically parse. That's probably why "semantic versioning" forbids it. Still, if support for it is needed, I have [published a function](https://stackoverflow.com/questions/7717109/how-can-i-compare-arbitrary-version-numbers/47917031#47917031) that supports it. – Gras Double Dec 22 '17 at 04:31
  • Hi @trincot, If some of values have format like this 'v2.10.0', how can I sort the list? – aylin Feb 28 '22 at 14:27
  • @aylin, there are many ways to do that: for example, remove the "v" first and append it again after the sort. If you have trouble implementing it, ask a question about it, and show your code, input, expected output, and where it goes wrong. – trincot Feb 28 '22 at 15:29
10

If you are looking for a npm package to compare two semver version, https://www.npmjs.com/package/compare-versions is the one.

Then you can sort version like this:

// ES6/TypeScript
import compareVersions from 'compare-versions';

var versions = ['5.5.1', '4.21.0', '4.22.0', '6.1.0', '5.1.0', '4.5.0'];
var sorted = versions.sort(compareVersions);
Anton Matosov
  • 1,685
  • 1
  • 20
  • 19
Tim Yao
  • 1,077
  • 1
  • 13
  • 17
5

You could split the strings and compare the parts.

function customSort(data, order) {

  function isNumber(v) {
    return (+v).toString() === v;
  }

  var sort = {
    asc: function (a, b) {
      var i = 0,
        l = Math.min(a.value.length, b.value.length);

      while (i < l && a.value[i] === b.value[i]) {
        i++;
      }
      if (i === l) {
        return a.value.length - b.value.length;
      }
      if (isNumber(a.value[i]) && isNumber(b.value[i])) {
        return a.value[i] - b.value[i];
      }
      return a.value[i].localeCompare(b.value[i]);
    },
    desc: function (a, b) {
      return sort.asc(b, a);
    }
  }
  var mapped = data.map(function (el, i) {
    return {
      index: i,
      value: el.split('')
    };
  });

  mapped.sort(sort[order] || sort.asc);
  return mapped.map(function (el) {
    return data[el.index];
  });
}

var array = ['5.5.1', '4.21.0', '4.22.0', '6.1.0', '5.1.0'];

console.log('sorted array asc', customSort(array));
console.log('sorted array desc ', customSort(array, 'desc'));
console.log('original array ', array);
.as-console-wrapper { max-height: 100% !important; top: 0; }
totymedli
  • 29,531
  • 22
  • 131
  • 165
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
2

You can check in loop if values are different, return difference, else continue

var a=['5.5.1', '4.21.0', '4.22.0', '6.1.0', '5.1.0', '4.5.0'];

a.sort(function(a,b){
  var a1 = a.split('.');
  var b1 = b.split('.');
  var len = Math.max(a1.length, b1.length);
  
  for(var i = 0; i< len; i++){
    var _a = +a1[i] || 0;
    var _b = +b1[i] || 0;
    if(_a === _b) continue;
    else return _a > _b ? 1 : -1
  }
  return 0;
})

console.log(a)
Rajesh
  • 24,354
  • 5
  • 48
  • 79
  • Not Math.max, because then you run out of bounds on the shorter list. Use min and then `return b1.length - a1.length` instead of zero. – Josef Kufner Jul 24 '21 at 22:38
1

Though slightly late this would be my solution;

var arr = ["5.1.1","5.1.12","5.1.2","3.7.6","2.11.4","4.8.5","4.8.4","2.10.4"],
 sorted = arr.sort((a,b) => {var aa = a.split("."),
                                 ba = b.split(".");
                             return +aa[0] < +ba[0] ? -1
                                                    : aa[0] === ba[0] ? +aa[1] < +ba[1] ? -1
                                                                                        : aa[1] === ba[1] ? +aa[2] < +ba[2] ? -1
                                                                                                                            : 1
                                                                                                          : 1
                                                                      : 1;
                            });
 console.log(sorted);
Redu
  • 25,060
  • 6
  • 56
  • 76
1

Here's a solution I developed based on @trincot's that will sort by semver even if the strings aren't exactly "1.2.3" - they could be i.e. "v1.2.3" or "2.4"

function sortSemVer(arr, reverse = false) {
    let semVerArr = arr.map(i => i.replace(/(\d+)/g, m => +m + 100000)).sort(); // +m is just a short way of converting the match to int
    if (reverse)
        semVerArr = semVerArr.reverse();

    return semVerArr.map(i => i.replace(/(\d+)/g, m => +m - 100000))
}

console.log(sortSemVer(["1.0.1", "1.0.9", "1.0.10"]))
console.log(sortSemVer(["v2.1", "v2.0.9", "v2.0.12", "v2.2"], true))
Gillespie
  • 5,780
  • 3
  • 32
  • 54
  • ^^ also works fine with minus instead of a dot at he end : ['3.35.0-1', '3.35.0-0', '3.34.0-0'] . Thank you Gillespie! – thomasL Aug 29 '22 at 16:04
0

This seems to work provided there are only digits between the dots:

var a = ['5.5.1', '4.21.0', '4.22.0', '6.1.0', '5.1.0', '4.5.0']

a = a.map(function (x) {
    return x.split('.').map(function (x) {
        return parseInt(x)
    })
}).sort(function (a, b) {
    var i = 0, m = a.length, n = b.length, o, d
    o = m < n ? n : m
    for (; i < o; ++i) {
        d = (a[i] || 0) - (b[i] || 0)
        if (d) return d
    }
    return 0
}).map(function (x) {
    return x.join('.')
})
Thejaka Maldeniya
  • 1,076
  • 1
  • 10
  • 20
0
'use strict';

var arr = ['5.1.2', '5.1.1', '5.1.1', '5.1.0', '5.7.2.2'];

Array.prototype.versionSort = function () {

    var arr = this;

    function isNexVersionBigger (v1, v2) {
        var a1 = v1.split('.');
        var b2 = v2.split('.');
        var len = a1.length > b2.length ? a1.length : b2.length;
        for (var k = 0; k < len; k++) {
            var a = a1[k] || 0;
            var b = b2[k] || 0;
            if (a === b) {
                continue;
            } else

                return b < a;
        }
    }

    for (var i = 0; i < arr.length; i++) {
        var min_i = i;
        for (var j = i + 1; j < arr.length; j++) {
            if (isNexVersionBigger(arr[i], arr[j])) {
                min_i = j;
            }
        }
        var temp = arr[i];
        arr[i] = arr[min_i];
        arr[min_i] = temp;
    }
    return arr;
}

console.log(arr.versionSort());
Denys Medvediev
  • 1,160
  • 3
  • 16
  • 32
0

This solution accounts for version numbers that might not be in the full, 3-part format (for example, if one of the version numbers is just 2 or 2.0 or 0.1, etc).

The custom sort function I wrote is probably mostly what you're looking for, it just needs an array of objects in the format {"major":X, "minor":X, "revision":X}:

var versionArr = ['5.5.1', '4.21.0', '4.22.0', '6.1.0', '5.1.0', '4.5.0'];
var versionObjectArr = [];
var finalVersionArr = [];
/*
split each version number string by the '.' and separate them in an
object by part (major, minor, & revision).  If version number is not
already in full, 3-part format, -1 will represent that part of the
version number that didn't exist.  Push the object into an array that
can be sorted.
*/
for(var i = 0; i < versionArr.length; i++){
  var splitVersionNum = versionArr[i].split('.');
  var versionObj = {};
  switch(splitVersionNum.length){
    case 1:
      versionObj = {
        "major":parseInt(splitVersionNum[0]),
        "minor":-1,
        "revision":-1
      };
      break;
    case 2:
      versionObj = {
        "major":parseInt(splitVersionNum[0]),
        "minor":parseInt(splitVersionNum[1]),
        "revision":-1
      };
      break;
    case 3:
      versionObj = {
        "major":parseInt(splitVersionNum[0]),
        "minor":parseInt(splitVersionNum[1]),
        "revision":parseInt(splitVersionNum[2])
      };
  }
  versionObjectArr.push(versionObj);
}

//sort objects by parts, going from major to minor to revision number.
versionObjectArr.sort(function(a, b){
  if(a.major < b.major) return -1;
  else if(a.major > b.major) return 1;
  else {
    if(a.minor < b.minor) return -1;
    else if(a.minor > b.minor) return 1;
    else {
      if(a.revision < b.revision) return -1;
      else if(a.revision > b.revision) return 1;
    }
  }
});

/*
loops through sorted object array to recombine it's version keys to match the original string's value.  If any trailing parts of the version
number are less than 0 (i.e. they didn't exist so we replaced them with
-1) then leave that part of the version number string blank. 
*/
for(var i = 0; i < versionObjectArr.length; i++){
  var versionStr = "";
  for(var key in versionObjectArr[i]){
    versionStr = versionObjectArr[i].major;
    versionStr += (versionObjectArr[i].minor < 0 ? '' : "." + versionObjectArr[i].minor);
    versionStr += (versionObjectArr[i].revision < 0 ? '' : "." + versionObjectArr[i].revision);
  }
  finalVersionArr.push(versionStr);
}
console.log('Original Array: ',versionArr);
console.log('Expected Output: ',['4.5.0', '4.21.0', '4.22.0', '5.1.0', '5.5.1', '6.1.0']);
console.log('Actual Output: ', finalVersionArr);
HaulinOats
  • 3,628
  • 4
  • 21
  • 24
0

Inspired from the accepted answer, but ECMA5-compatible, and with regular string padding (see my comments on the answer):

function sortCallback(a, b) {

    function padParts(version) {
        return version
            .split('.')
            .map(function (part) {
                return '00000000'.substr(0, 8 - part.length) + part;
            })
            .join('.');
    }

    a = padParts(a);
    b = padParts(b);

    return a.localeCompare(b);
}

Usage:

['1.1', '1.0'].sort(sortCallback);
Gras Double
  • 15,901
  • 8
  • 56
  • 54
0

const arr = ["5.1.1","5.1.12","5.1.2","3.7.6","2.11.4","4.8.5","4.8.4","2.10.4"];
const sorted = arr.sort((a,b) => {
  const ba = b.split('.');
  const d = a.split('.').map((a1,i)=>a1-ba[i]);
  return d[0] ? d[0] : d[1] ? d[1] : d[2]
});

console.log(sorted);
kuboon
  • 9,557
  • 3
  • 42
  • 32
0
  • sort 1.0a notation correct
  • use native localeCompare to sort 1.090 notation

function log(label,val){
  document.body.append(label,String(val).replace(/,/g," - "),document.createElement("BR"));
}

const sortVersions = (
  x,
  v = s => s.match(/[a-z]|\d+/g).map(c => c==~~c ? String.fromCharCode(97 + c) : c)
) => x.sort((a, b) => (a + b).match(/[a-z]/) 
                             ? v(b) < v(a) ? 1 : -1 
                             : a.localeCompare(b, 0, {numeric: true}))

let v=["1.90.1","1.090","1.0a","1.0.1","1.0.0a","1.0.0b","1.0.0.1","1.0a"];
log(' input : ',v);
log('sorted: ',sortVersions(v));
log('no dups:',[...new Set(sortVersions(v))]);
Herohtar
  • 5,347
  • 4
  • 31
  • 41
Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49
0

This can be in an easier way using the sort method without hardcoding any numbers and in a more generic way.

enter code here

var arr = ['5.1.2', '5.1.1', '5.1.1', '5.1.0', '5.7.2.2'];

splitArray = arr.map(elements => elements.split('.'))

//now lets sort based on the elements on the corresponding index of each array

//mapped.sort(function(a, b) {
//  if (a.value > b.value) {
//    return 1;
//  }
//  if (a.value < b.value) {
//    return -1;
//  }
//  return 0;
//});

//here we compare the first element with the first element of the next version number and that is [5.1.2,5.7.2] 5,5 and 1,7 and 2,2 are compared to identify the smaller version...In the end use the join() to get back the version numbers in the proper format.

sortedArray = splitArray.sort((a, b) => {
  for (i in a) {
    if (parseInt(a[i]) < parseInt(b[i])) {
      return -1;
      break
    }
    if (parseInt(a[i]) > parseInt(b[i])) {
      return +1;
      break
    } else {
      continue
    }
  }
}).map(p => p.join('.'))

sortedArray = ["5.1.0", "5.1.1", "5.1.1", "5.1.2", "5.7.2.2"]
0

In ES6 you can go without regex.

const versions = ["0.4", "0.11", "0.4.1", "0.4", "0.4.2", "2.0.1","2", "0.0.1", "0.2.3"];

const splitted = versions.map(version =>
    version
        .split('.')
        .map(i => +i))
        .map(i => {
           let items;
           if (i.length === 1) {
             items = [0, 0]
             i.push(...items)
           }
           if (i.length === 2) {
             items = [0]
             i.push(...items)
           }

           return i
        })
        .sort((a, b) => {
          for(i in a) {
            if (a[i] < b[i]) {
              return -1;
            }
            if (a[i] > b[i]) {
              return +1;
            }
        }
    })
    .map(item => item.join('.'))

const sorted = [...new Set(splitted)]
Maksym Koshyk
  • 468
  • 5
  • 8
-1

If ES6 I do this:

versions.sort((v1, v2) => {
  let [, major1, minor1, revision1 = 0] = v1.match(/([0-9]+)\.([0-9]+)(?:\.([0-9]+))?/);
  let [, major2, minor2, revision2 = 0] = v2.match(/([0-9]+)\.([0-9]+)(?:\.([0-9]+))?/);
  if (major1 != major2) return parseInt(major1) - parseInt(major2);
  if (minor1 != minor2) return parseInt(minor1) - parseInt(major2);
  return parseInt(revision1) - parseInt(revision2);
});
Trenskow
  • 3,783
  • 1
  • 29
  • 35
-3
**Sorted Array Object by dotted version value**

    var sampleData = [
      { name: 'Edward', value: '2.1.2' },
      { name: 'Sharpe', value: '2.1.3' },
      { name: 'And', value: '2.2.1' },
      { name: 'The', value: '2.1' },
      { name: 'Magnetic', value: '2.2' },
      { name: 'Zeros', value: '0' },
      { name: 'Zeros', value: '1' }
    ];
    arr = sampleData.map( a => a.value).sort();
    var requireData = [];

    arr.forEach(function(record, index){    
      var findRecord = sampleData.find(arr => arr.value === record);
        if(findRecord){
          requireData.push(findRecord);
        }
      });
    console.log(requireData);

    [check on jsfiddle.net][1]
    [1]: https://jsfiddle.net/jx3buswq/2/

    It is corrected now!!!