1

I have a list<job>called _joblist that is populated from a deserialized json on form load, with job being a class:

     public class Job
        {
            public string Jobname { get; set; }
            public string State { get; set; }
            public string City { get; set; }
            public string Status = "●";
            public int COT { get; set; }
            public string Customer { get; set; }
        }

the problem I have with the default list.sort is that it sorts "ASCIIbetically" where I would want it to sort alphanumerically specifically based on the public string Customer variable, for example expected output:

job1
job2
job3
job11
job21

actual output:

job1
job11
job2
job21
job3

on similar questions I've seen people suggest using LINQ and list.orderby, my issue with this is all the examples I've seen were for a list<string> or string[], and as I'm relatively new to c# I'm not sure how to do this in this specific example. Apologies if the question is unclear or if I'm wrong on any terminology, like I said i'm relatively new to this and happy to provide clarification if requested, thanks in advance.

Blindy
  • 65,249
  • 10
  • 91
  • 131
  • job1 job2 job3 all Customers will have 'job' in prefix ? or this is just an example ? – hanan Jul 06 '20 at 18:41
  • 1
    How is it supposed to know about the special integer value embedded in the string? Either make it explicate via a property and sort on that, or write a custom comparer type that understands the encoding and sorts values per your specifications. – asawyer Jul 06 '20 at 18:44
  • how about `1job`, `2job`? – Mohammed Sajid Jul 06 '20 at 18:47
  • @hanan I used the word job as just an example, the point is i'm looking to sort a string containing both numbers and letters, i.e job1 etc, but alphanumerically. – zjackson1123 Jul 06 '20 at 18:48
  • @Sajid I should've been more clear when talking about what I was previously using to sort the list. I'd been using `list.sort((x,y) => string.compare(x.Customer, y.Customer);` which gave me the "actual output" in my question, sorting with 11 being before 2 in the list and so on. – zjackson1123 Jul 06 '20 at 18:54
  • There are plenty of answers on how to perform natural sort. – Alexei Levenkov Jul 06 '20 at 19:01
  • @AlexeiLevenkov could you perhaps link one that you think would work in my specific situation? I've seen a few but like I stated in my question I've not found one that works for a list created from a class like mine. – zjackson1123 Jul 06 '20 at 19:06
  • 1
    https://stackoverflow.com/questions/3716831/sorting-liststring-in-c-sharp but as you've said it that is not going to help you anyway. – Alexei Levenkov Jul 06 '20 at 19:25

4 Answers4

2

can you just do

    List<Job> jobs = new List<Job>();
    jobs.Add("job1");
    jobs.Add("job234");
    jobs.Add("job2");
    jobs.Add("job123");
    jobs.OrderBy(a=>a.Name.Length).ThenBy(a=>a.Name).Dump();

public static class JobExtension {
    public static void Add(this List<Job> jobs, string name){
        jobs.Add(new Job{Name = name});
    }
}

public class Job {
    public string Name { get; set; }
}

it is simple, but it works for your case

if you want something more sophisticated, you need to write your own IComparer

enter image description here

sowen
  • 1,090
  • 9
  • 28
  • 1
    Where would "z" end up with your comparer? – Blindy Jul 06 '20 at 18:56
  • Trying to implement something similar to this now, could you explain what you meant by the .Dump in `jobs.OrderBy(a=>a.Name.Length).ThenBy(a=>a.Name).Dump();` I don't understand where that part is coming from. – zjackson1123 Jul 06 '20 at 18:59
  • 2
    @zjackson1123 sowen is using Linqpad. – asawyer Jul 06 '20 at 19:08
  • 1
    @zjackson1123 just delete it. It is convenient method from [LINQPad](https://www.linqpad.net/) to dump results into it's results window. – Guru Stron Jul 06 '20 at 19:20
2

Here's a generic comparer that sorts strings by splitting them into numbers and strings, and then sorts the strings with the strings, and the numbers with the numbers (as numbers, not strings), which should work with any kind of mixes of numbers and strings, however many or long.

using System;
using System.Diagnostics;
using System.Linq;
using System.Collections.Generic;
using System.Collections;
using System.Text.RegularExpressions;
                    
public class Program
{
    static void Main()
    {
        var arr = new[] { "job0", "job1", "job11", "job2", "z0", "z1", "z11", "z2", "0", "1", "11", "111", "2", "21", "a0b11", "a0b111", "a0b2", "a0b20" };

        var tmp = arr.Select(name => (orig: name, parts: GetParts(Regex.Match(name, @"^([a-zA-Z ]+)(?:(\d+)([a-zA-Z ]+))*(\d+)?$|^(\d+)(?:([a-zA-Z ]+)(\d+))*([a-zA-Z ]+)?$")))).ToList();
        tmp.Sort(new AlphaNumericalComparer());

        foreach(var item in tmp.Select(x => x.orig)) Console.Write(item + ", ");
    }

    static object GetObject(string s) => Regex.IsMatch(s, @"^\d+$") ? (object)Convert.ToInt32(s) : s;

    static IEnumerable<object> GetParts(Match m) =>
        char.IsDigit(m.Groups.Cast<Group>().Skip(1).First(m => m.Success).Value[0]) 
            ? Enumerable.Repeat((object)"", 1).Concat(m.Groups.Cast<Group>().Skip(1).Where(m => m.Success).Select(m => GetObject(m.Value)).ToList())
            : m.Groups.Cast<Group>().Skip(1).Where(m => m.Success).Select(m => GetObject(m.Value)).ToList();

    class AlphaNumericalComparer : IComparer<(string name, IEnumerable<object> parts)>
    {
        public int Compare((string name, IEnumerable<object> parts) left, (string name, IEnumerable<object> parts) right)
        {
            foreach(var lr in left.parts.Cast<IComparable>().Zip(right.parts.Cast<IComparable>()))
            {
                var cmp = lr.Item1.CompareTo(lr.Item2);
                if(cmp != 0) return cmp;
            }

            return 0;
        }
    }
}

The result of running that program is, as you would expect:

0, 1, 2, 11, 21, 111, a0b2, a0b11, a0b20, a0b111, job0, job1, job2, job11, z0, z1, z2, z11
Blindy
  • 65,249
  • 10
  • 91
  • 131
1

if job3 must be before job11, I'm wondering if it would not be easier to change the customer name convention to something like job03, job11

the idea is to format the job id on two digit

Chris
  • 1,122
  • 1
  • 12
  • 14
  • This was a really unique workaround that I'd never considered, thanks for the contribution. I see this natural sort issue being common and this is probably the most concise workaround I've seen. – zjackson1123 Jul 07 '20 at 13:26
1

Others already gave you the solution. Since you seemed to be new to LINQ, I thought it might be useful to provide some background information on how LINQ works, and how to use Lambda expressions.

I see that you have two questions:

  • Given a sequence of Jobs, how do I order them by a specific property?
  • If I have two string to compare, how do I specify that I want Job3 before Job11

How to sort by the value of a property using LINQ

A sequence of Jobs, is every object that implements IEnumerable<Job>, like your List<Job>, but it could also be an array, a database query, some information to be fetched from the internet, anything that implements IEnumerable<Job>. Usually all classes that represent a sequence of similar objects implement this interface

LINQ is nothing more than a set of methods that take an IEnumerable of something as input and returns some specified output. LINQ will never change the input, it can only extract data from it.

One of the LINQ methods is Enumerable.OrderBy. This method has a parameter keySelector. This parameter selects the key by which you want to order. You want to order by ascending value of Customer, so your keySelector should tell the method to use property Customer.

The keySelector has a format of Func<TSource, TKey>. Your input sequence is a sequence of Jobs, so TSource is Job. The func is a description of a function that has as input a Job, and as output the key that you want to sort by. This would be something like:

string GetOrderByKey(Job job)
{
    return job.Customer;
}

Note: the return value is a string, so everywhere that you see TKey, you should think of string.

Luckily, since the introduction of Lambda expressions this has become a lot less typing:

IEnumerable<Job> jobs = ...
var jobsOrderedByCustomer = jobs.OrderBy(job => job.Customer);

In words:
We have a sequence of Jobs, and for every job object in this sequence, return a key with a value of job.Customer. Order the Jobs by this returned key.

The first time you see lambda expressions, it might seem a bit intimidating. Usually it helps if you use plural nouns for the collections (jobs), and singular nouns for elements of the collection (job).

The phrase job => ... would mean: for every job in the collection of jobs do ...

The part after the => doesn't have to be simple, it can be a block:

job =>
{
    if (String.IsNullOrEmpty(status))
       return job.Company;
    else
       return job.City;
}

So if you need to provide a Func<TSource, TKey>, you can write x => ... where x is considered an input element of type TSource, and ... is the return value of the function, which should be of type TKey

I don't want standard ordering

If you use the overload of Enumerable.OrderBy without a comparer, the orderby method uses a default comparer for type TKey.

So if your keySelector returns an int, the standard comparer for ints is used, if your keySelector returns a string, the standard string compare is used, which is StringComparer.Ordinal.

You don't want the standard comparison, you want a comparer that says that Job3 should come before Job11. You need to provide a comparer. This comparer should implement IComparer<TKey>, where TKey is the return value of your keySelector, in this case a string.

class CompanyNameComparer : IComparer<string>
{
    public public int Compare (string x, string y)
    {
         // TODO implement
    }
}

The implementation should return a number that indicates whether x should come first (< 0), or y (> 0), or that you don't care (==0).

var orderedJobs = jobs.Orderby(job => job.Company, new CompanyNameComparer());

Others already gave you a solution for the comparer, so I won't repeat it. What might help you to get on track using LINQ, is The standard LINQ operators

Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116
  • Thank you very much for this contribution, this helped me to understand what i'm actually doing a lot better, if I hadn't already accepted an answer I would absolutely use this. – zjackson1123 Jul 07 '20 at 11:15