3

I have Licence plate numbers which I return to UI and I want them ordered in asc order:

So let's say the input is as below:

1/12/13/2
1/12/11/3
1/12/12/2
1/12/12/1

My expected output is:

1/12/11/3
1/12/12/1
1/12/12/2
1/12/13/2

My current code which is working to do this is:

var orderedData = allLicenceNumbers
   .OrderBy(x => x.LicenceNumber.Length)
   .ThenBy(x => x.LicenceNumber)
   .ToList();

However for another input sample as below:

4/032/004/2
4/032/004/9
4/032/004/3/A
4/032/004/3/B
4/032/004/11

I am getting the data returned as:

4/032/004/2
4/032/004/9
4/032/004/11
4/032/004/3/A
4/032/004/3/B

when what I need is:

4/032/004/2
4/032/004/3/A
4/032/004/3/B
4/032/004/9
4/032/004/11

Is there a better way I can order this simply to give correct result in both sample inputs or will I need to write a custom sort?

EDIT

It wont always be the same element on the string.

This could be example input:

2/3/5/1/A
1/4/6/7
1/3/8/9/B
1/3/8/9/A
1/5/6/7

Expected output would be:

1/3/8/9/A
1/3/8/9/B
1/4/6/7
1/5/6/7
2/3/5/1/A
Ctrl_Alt_Defeat
  • 3,933
  • 12
  • 66
  • 116
  • Can you please explain better which part of the number you're expecting to sort? You said DESC, and then posted a first "desired" sample output that looked to me like ASC (11,12,12,13...).. – Caius Jard Nov 28 '17 at 12:57
  • 1
    First you order by Length, so that is why e.g. 4/032/004/11 is before 4/032/004/3/B. You should invert order properties. (OrderBy(x=>x.LicenceNumber).ThenBy(x => x.LicenceNumber.Length) ) – Przemek Marcinkiewicz Nov 28 '17 at 13:01
  • You are looking for *Natural Sort* https://stackoverflow.com/questions/248603/natural-sort-order-in-c-sharp – Dmitry Bychenko Nov 28 '17 at 13:01
  • @CaiusJard - its the entire number I need sorted - smallest to highest - so 1/12/11/3 is considered smaller that 1/1213/2 - so was typo - should have said ASC order for smallest to largest – Ctrl_Alt_Defeat Nov 28 '17 at 13:01
  • 1
    Will it always be 4 numbers followed by an optional letter (or string)? It looks like you'd need to split, parse, and then sort. So a custom comparer would work well here. If possible maybe store the values in a class that separates the values instead of a string. – juharr Nov 28 '17 at 13:15
  • Which comes first, numbers or letters? `1/2/3/4/A` or `1/2/3/4/5` – Idle_Mind Nov 28 '17 at 13:25

3 Answers3

3

You should split your numbers and compare each part with each other. Compare numbers by value and strings lexicographically.

var licenceNumbers = new[]
{
    "4/032/004/2",
    "4/032/004/9",
    "4/032/004/3",
    "4/032/004/3/A",
    "4/032/004/3/B",
    "4/032/004/11"
};

var ordered = licenceNumbers
    .Select(n => n.Split(new[] { '/' }))
    .OrderBy(t => t, new LicenceNumberComparer())
    .Select(t => String.Join("/", t));

Using the following comparer:

public class LicenceNumberComparer: IComparer<string[]>
{ 
    public int Compare(string[] a, string[] b)
    {
        var len = Math.Min(a.Length, b.Length);
        for(var i = 0; i < len; i++)
        {
            var aIsNum = int.TryParse(a[i], out int aNum);
            var bIsNum = int.TryParse(b[i], out int bNum);
            if (aIsNum && bIsNum)
            {
                if (aNum != bNum)
                {
                    return aNum - bNum;
                }
            }
            else
            {
                var strCompare = String.Compare(a[i], b[i]);
                if (strCompare != 0)
                {
                    return strCompare;
                }
            }
        }
        return a.Length - b.Length;
    }
}
hansmaad
  • 18,417
  • 9
  • 53
  • 94
  • hansmaad - this looks really good - ill give that a go and mark as accepted if it works - cheers – Ctrl_Alt_Defeat Nov 28 '17 at 13:24
  • though is there a typo here - .Select(n => n.Split(new[] { '/' })) should it be n.LicenceNumber.Split? – Ctrl_Alt_Defeat Nov 28 '17 at 13:30
  • @Ctrl_Alt_Defeat `licenceNumbers` in my example is an array of strings. I edit my example code. If it's a list of different objects in your case, it could be `n.LicenceNumber.Split` of course. – hansmaad Nov 28 '17 at 13:34
  • yes it is a property in an list of licence plate objects for me so not sure this will work as I need to return a list of those objects but with the select projection now it is a list of strings. Sorry - should have made that more clear in question – Ctrl_Alt_Defeat Nov 28 '17 at 13:41
  • _it is a property in an list of licence plate objects_ You can implement the [IComparable()](https://msdn.microsoft.com/en-us/library/4d7sx9hd(v=vs.110).aspx) interface for that class and then just tell the List<> to sort and it will work. Use hansmadd's logic in your implementation. – Idle_Mind Nov 28 '17 at 13:49
  • @Ctrl_Alt_Defeat You could also do the split in the comparer and measure if it is performant enough for you. If your sort is getting slow, try to extract as many operations as possible from the `Compare` method into a preceding projection. – hansmaad Nov 28 '17 at 13:50
1

If we can assume that

  1. Number plate constist of several (one or more) parts separated by '/', e.g. 4, 032, 004, 2
  2. Each part is not longer than some constant value (3 in the code below)
  3. Each part consist of either digits (e.g. 4, 032) or non-digits (e.g. A, B)

We can just PadLeft each number plate's digit part with 0 in order to compare not "3" and "11" (and get "3" > "11") but padded "003" < "011":

  var source = new string[] {
    "4/032/004/2",
    "4/032/004/9",
    "4/032/004/3/A",
    "4/032/004/3/B",
    "4/032/004/11",
  };

  var ordered = source
    .OrderBy(item => string.Concat(item
       .Split('/')                            // for each part
       .Select(part => part.All(char.IsDigit) // we either
           ? part.PadLeft(3, '0') // Pad digit parts e.g. 3 -> 003, 11 -> 011 
           : part)));             // ..or leave it as is

  Console.WriteLine(string.Join(Environment.NewLine, ordered));

Outcome:

4/032/004/2
4/032/004/3/A
4/032/004/3/B
4/032/004/9
4/032/004/11
Dmitry Bychenko
  • 180,369
  • 20
  • 160
  • 215
  • I don't think you need to bother with the `part.All(char.IsDigit)` bit - the strings "00A" and "00B" sort just the same as "A" and "B" do. This is why I didn't bother checking this in my logic - the last example in my answer is essentially identical to this one (I completely forgot about `PadLeft`, and padded the string manually) – Caius Jard Nov 28 '17 at 15:48
  • @Caius Jard: There is problem with `ZZ` and `ABC` strings: we don't want `0ZZ` – Dmitry Bychenko Nov 28 '17 at 17:37
  • Do they occur though? – Caius Jard Nov 28 '17 at 17:46
  • @Caius Jard: I don't know; in my own country (Russia) licence car plates can have different lengths, like `123TE78` (civil) and `0123AET` (military) numbers. The very case I tried to solve with `char.IsDigit` – Dmitry Bychenko Nov 28 '17 at 19:17
0

You seem to be wanting to sort on the fourth element of the string (delimited by /) in numeric rather than string mode.. ?

You can make a lambda more involved/multi-statement by putting it like any other method code block, in { }

var orderedData = allLicenceNumbers
 .OrderBy(x => 
  { 
   var t = x.Split('/');
   if(t.Length<4)
     return -1;
   else{
     int o = -1;
     int.TryParse(t[3], out o);
     return o;
  }
 )
 .ToList();

If you're after sorting on more elements of the string, you might want to look at some alternative logic, perhaps if the first part of the string will always be in the form N/NNN/NNN/??/?, then do:

var orderedData = allLicenceNumbers
 .OrderBy(w => w.Remove(9)) //the first 9 are always in the form N/NNN/NNN
 .ThenBy(x =>               //then there's maybe a number that should be parsed
  {  
   var t = x.Split('/');
   if(t.Length<4)
     return -1;
   else{
     int o = -1;
     int.TryParse(t[3], out o);
     return o;
  }
 )
 .ThenBy(y => y.Substring(y.LastIndexOf('/'))) //then there's maybe A or B..
 .ToList();

Ultimately, it seems that more and more outliers will be thrown into the mix, so you're just going to have to keep inventing rules to sort with..

Either that or change your strings to standardize everything (int an NNN/NNN/NNN/NNN/NNA format for example), and then sort as strings..

var orderedData = allLicenceNumbers
 .OrderBy(x =>               
  {  
   var t = x.Split('/');
   for(int i = 0; i < t.Length; i++) //make all elements in the form NNN
   {
     t[i] = "000" + t[i];
     t[i] = t[i].Substring(t[i].Length - 3);
   }
   return string.Join(t, "/");
  }
 )
 .ToList();

Mmm.. nasty!

Caius Jard
  • 72,509
  • 5
  • 49
  • 80
  • should have maybe used another exmaple in the question - it wont always be the fourth element of the string - ill add an edit to question – Ctrl_Alt_Defeat Nov 28 '17 at 13:04
  • Problem with keep shifting the goalposts is it makes it hard to provide an answer that keeps covering all cases – Caius Jard Nov 28 '17 at 13:21