0

Say we have two List<T>. The first is a list of sales totals:

class SalesTotals
{
    public Guid EmpID { get; set; }
    public string EmpName { get; set; }
    public decimal? TotalSales { get; set; }
}

Then we have another list of sales by year:

class YearlySales
{
    public Guid EmpID { get; set; }
    public short SalesYear { get; set; }
    public decimal? YearlyTotals { get; set; }
}

These are used together to create a "cross tab" report which lists the total sales by each employee, followed by a column for each year with the related yearly sales. It would look something like this:

| Name | Total | 2018 | 2017 | 2016 |
+------+-------+------+------+------+
|  Joe |    70 |   20 |      |   50 |
|  Sam |    60 |   30 |   20 |   10 |
| Fred |    50 |   30 |      |   20 |
|  Bob |    40 |   10 |   15 |   15 |

By default, the report is sorted by TotalSales (no problem). But if we want to sort by an individual year, things get trickier. Sorted by 2017 (then by total):

| Name | Total | 2018 | 2017 | 2016 |
+------+-------+------+------+------+
|  Sam |    60 |   30 |   20 |   10 |
|  Bob |    40 |   10 |   15 |   15 |
|  Joe |    70 |   20 |      |   50 |
| Fred |    50 |   30 |      |   20 |

I assume we want to (Left) Join these two List<T>s on EmpID, where SalesYear == <year to sort by> then OrderBy YearlyTotals, TotalSales (since YearlyTotals might not exist for a given year, and we still want some type of order in that case). So we also have to consider that there might not be a record for that year to join with (so it needs to be a left join).

If I were writing SQL it would look something like this:

SELECT ST.EmpID, ST.EmpName, ST.TotalSales
FROM SalesTotals AS ST
LEFT JOIN YearlySales AS YS ON ST.EmpID=YS.EmpID
WHERE YS.SalesYear=@SortBySalesYear OR YS.SalesYear IS NULL
ORDER BY YS.YearlySales DESC, ST.TotalSales DESC

I'm not good enough with Linq (yet) to be able to figure this out. In fact, I was able to get virtually no where (maybe trying to do too much at once, perhaps I need to break it down in to individual steps, and not search for the one liner).

So, is there a way to do this with Linq? Or should I be attempting some other type of approach?

Note: All I need is an "in place" sort here. I don't need/want a different type of List<T> returned here, just a sorted List<SalesTotals>.

Edit: I prefer the Linq "Query Syntax" as it is more intuitive to me (strong SQL background). So I prefer an answer using Query Syntax as opposed to Method Syntax.

Edit: Here is a test case setup:

class SalesTotals
{
    public int EmpID { get; set; }
    public string EmpName { get; set; }
    public decimal? TotalSales { get; set; }
}
class YearlySales
{
    public int EmpID { get; set; }
    public short SalesYear { get; set; }
    public decimal? YearlyTotals { get; set; }
}
class TestSort
{
    public TestSort()
    {
        var st = new List<SalesTotals>
        {
            new SalesTotals() { EmpID = 1, EmpName = "Joe", TotalSales = 70 },
            new SalesTotals() { EmpID = 2, EmpName = "Sam", TotalSales = 60 },
            new SalesTotals() { EmpID = 3, EmpName = "Fred", TotalSales = 50 },
            new SalesTotals() { EmpID = 4, EmpName = "Bob", TotalSales = 40 }
        };

        var ys = new List<YearlySales>
        {
            new YearlySales() { EmpID = 1, SalesYear = 2018, YearlyTotals = 20 },
            new YearlySales() { EmpID = 2, SalesYear = 2018, YearlyTotals = 30 },
            new YearlySales() { EmpID = 3, SalesYear = 2018, YearlyTotals = 30 },
            new YearlySales() { EmpID = 4, SalesYear = 2018, YearlyTotals = 10 },
            new YearlySales() { EmpID = 2, SalesYear = 2017, YearlyTotals = 20 },
            new YearlySales() { EmpID = 4, SalesYear = 2017, YearlyTotals = 15 },
            new YearlySales() { EmpID = 1, SalesYear = 2016, YearlyTotals = 10 },
            new YearlySales() { EmpID = 2, SalesYear = 2016, YearlyTotals = 15 },
            new YearlySales() { EmpID = 3, SalesYear = 2016, YearlyTotals = 50 },
            new YearlySales() { EmpID = 4, SalesYear = 2016, YearlyTotals = 20 }
        };

        st = SortByYear(st, ys, 2017);
    }
    private List<SalesTotals> SortByYear(List<SalesTotals> salesTotals, List<YearlySales> yearlySales, short sortYear)
    {
        // return sorted salesTotals by sortYear using both salesTotals and yearlySales joined on EmpID
    }
}         
Scuzzlebutt
  • 494
  • 9
  • 18
  • Can you provide examples of the data in the two lists that you use to generate the given results? – 15ee8f99-57ff-4f92-890c-b56153 May 21 '18 at 15:26
  • Possible duplicate of https://stackoverflow.com/questions/3404975/left-outer-join-in-linq – paparazzo May 21 '18 at 15:27
  • Saw that question, and it helps, but it didn't address sorting, and didn't seem picky about return type. – Scuzzlebutt May 21 '18 at 15:49
  • 1
    Requirement to sort in-place contradicts requirement to use LINQ (whether query syntax or not). – Evk May 21 '18 at 16:23
  • @Evk I am aware of that, but Linq seems to be the easiest to implement at this point. I was clarifying that I needed "in place" sort here to emphasize that I wanted to "get back" an exact copy of the List sorted, not some other type of List. This would also open the door for other methods of achieving the goal (without the use of Linq) – Scuzzlebutt May 21 '18 at 16:41
  • 1
    Your second class won't compile because a member name and the class name are the same? – NetMage May 21 '18 at 17:35
  • Yea I just saw that too... Adding a test case for you that I actually wrote in VS this time =) – Scuzzlebutt May 21 '18 at 17:37

3 Answers3

2

Rather than convert the SQL directly, I think it is a little clearer to break the query into two parts.

First, find the YearlySales for the year to sort by:

var sortYearSales = from ys in yearlySales
                    where ys.SalesYear == SortBySalesYear
                    select ys;

Then you can left join on that and sort (since ys might by null, I used the null conditional member acecss operator):

var orderedSalesTotals = (from st in salesTotals
                          join ys in sortYearSales on st.EmpID equals ys.EmpID into ysj
                          from ys in ysj.DefaultIfEmpty()
                          orderby ys?.YearSales descending, st.TotalSales descending
                          select st).ToList();

Note: I changed the name of the YearlySales member to YearSales since the C# compiler complained about the member and class having the same name.

You can do it in a single query, but you must either nest the first query into the second, or use lambda syntax in the query:

var orderedSalesTotals = (from st in salesTotals
                          join ys in yearlySales on st.EmpID equals ys.EmpID into ysj
                          from ys in ysj.Where(y => y.SalesYear == SortBySalesYear).DefaultIfEmpty()
                          orderby ys?.YearSales descending, st.TotalSales descending
                          select st).ToList();
NetMage
  • 26,163
  • 3
  • 34
  • 55
  • Ka POW! That knocked it out of the park! Would have been nice to have it all in one go, but this will do fine. +1 on the null conditional. All I did was add in an `ys == null` ahead of the other orderby clause members to get nulls to show up last, and wala. Thx very much! – Scuzzlebutt May 21 '18 at 18:12
  • 1
    @Scuzzlebutt No problem. I added a single query example but it requires mixing lambda syntax or nesting queries do to do it (to be where, that is what you are doing in SQL). – NetMage May 21 '18 at 18:57
1

You can write it pretty much the same way you would in SQL!

        var results = from t in totals
            join y in years on t.EmpID equals y.EmpID into groupedTable
                      from p in groupedTable.DefaultIfEmpty()
                      where y == null || y.SalesYear == year
                      orderby y.SalesYear, t.TotalSales descending
                      select t;

Quick note: Joins in LINQ are by default inner joins. If you want an outer join, you have to use a DefaultIfEmpty() call.

SomeGuy
  • 485
  • 3
  • 12
  • I think I'm mostly getting tripped up on the left join part, and returning the correct return type `List`. This looks promising... Trying on my end... – Scuzzlebutt May 21 '18 at 15:48
  • I'm going to keep working on it, but right off the bat, the `yearly.SalesYear == null` doesn't work because its a `short` (non nullable) type. To be clear, YearlySales doesn't always include a record for that year, so if there is no sales data for that year, the record won't exist. – Scuzzlebutt May 21 '18 at 16:11
  • @Scuzzlebutt whoops, that should say where yearly == null. Fixed. – SomeGuy May 21 '18 at 16:18
  • Ahh, nice, I get it. Still working on it... Compiler doesn't like `into tempTbl` after the `orderby` clause... (a query body must end with a select or group clause)... I want to select just the SalesTotals, but I don't think I can do that just yet... Need to handle DefaultIfEmpty() for the YearlySales... Getting closer, I'll keep working... – Scuzzlebutt May 21 '18 at 16:34
  • sorry, I'm pretty rusty at the query syntax and only just stepped in front of a compiler. I'll take a look. – SomeGuy May 21 '18 at 16:59
  • 1
    Recall that `OR` in C# is `||` or `|`. – Eric Lippert May 21 '18 at 17:06
  • 2
    That feeling when you accidentally use SQL syntax in C# and then Eric Lippert himself sees it. Oof. – SomeGuy May 21 '18 at 17:48
  • I caught it right away too, but wasn't going to say anything (I knew what you meant!) – Scuzzlebutt May 21 '18 at 17:49
-1

Kind of works. Need to put in a null for sales

List<YearlySale> YearlySales = new List<YearlySale>() { new YearlySale() { EmpID = 1, Sales = 700, Year = 2018 },
                                                        new YearlySale() { EmpID = 1, Sales = 600, Year = 2017 },
                                                        new YearlySale() { EmpID = 1, Sales = 500, Year = 2016 },
                                                        new YearlySale() { EmpID = 2, Sales = 400, Year = 2018 },
                                                        new YearlySale() { EmpID = 2, Sales = null, Year = 2017 },
                                                        new YearlySale() { EmpID = 2, Sales = 300, Year = 2016 }
                                                        };
List<SalesTotal> SalesTotals = new List<SalesTotal>() { new SalesTotal() { EmpID = 1, EmpName = "stan", TotalSales  = 1800 },
                                                        new SalesTotal() { EmpID = 2, EmpName = "sally", TotalSales = 700 }

                                                        };
var q = from s in SalesTotals
        join y18 in YearlySales 
            on s.EmpID equals y18.EmpID
        join y17 in YearlySales
            on s.EmpID equals y17.EmpID
        join y16 in YearlySales
            on s.EmpID equals y16.EmpID
        where y18.Year == 2018
        where y17.Year == 2017
        where y16.Year == 2016
        select new { SalesTotal = s, Year18 = y18 == null ? 0 : y18.Year, YearS18 = y18 == null ? 0 : y18.Sales
                                   , Year17 = y17 == null ? 0 : y17.Year, YearS17 = y17 == null ? 0 : y17.Sales
                                   , Year16 = y16 == null ? 0 : y16.Year, YearS16 = y16 == null ? 0 : y16.Sales
                    };
foreach (var v in q.OrderBy(x => x.SalesTotal.EmpID))
{
    Debug.WriteLine($"{v.SalesTotal.EmpID} {v.SalesTotal.EmpName} {v.SalesTotal.TotalSales} {v.YearS18} as y18 {v.YearS17} as y17  {v.YearS16} as y16" );
}
paparazzo
  • 44,497
  • 23
  • 105
  • 176
  • The YearlySales object with `Sales = null` won't exist in the `List`. If there is no sales data for that year, there will be no record. Also, I'm not interested in getting a "cross tab" report back. I just want the original `List` returned, but sorted. So no need (or desire) to include the `YearlySales` detail in the output. Was hoping to be able to sort in one swoop, otherwise I'd be up to three "copies" of the List in memory right? (probably not an issue, I'm at most a few hundred items) – Scuzzlebutt May 21 '18 at 17:19
  • You can insert a null. If you did not want a cross presentation then why is that how you formatted the question and asked for "cross tab"? – paparazzo May 21 '18 at 17:22
  • I very clearly stated that I wanted a `List` returned (in the original question). I showed what the result of the sort would look like to help clarify what the ultimate goal was. Even the SQL I wrote included only the SalesTotal fields... – Scuzzlebutt May 21 '18 at 17:46