tags:

views:

138

answers:

4

My OM has a 'product' object.
Each product has a 'manufacturer id' (property, integer).
When I have a list of products to display, the first three are displayed as the 'featured products'.
The list is already sorted in a specific sort order, putting the 'featured' products first in the list.

However, I now need to ensure the featured products in the listing are from different Manufacturers. I want to have a method to call to do this re-sorting. Trying to utilize LINQ to to the querying of the input 'products' and the 'results'

public List<Product> SetFeatures(List<Product> products, int numberOfFeatures)
{
    List<Product> result;

    // ensure the 2nd product is different manufacturer than the first ....

    // ensure the 3rd product is a different manufacturer than the first two... 

    // ... etc ... for the numberOfFeatures

    return result;

}

Thanks in advance.

Clarification:
The original list is in a specific order: the 'best selling', highest first (descending order). The resulting list should remain in this order with the exception of adjusting or moving 'up' items so that the differing manufacturers are seen the top n features.

If the first n (numberOfFeatures) of items all have different manufacturers, then the listing does not need to be altered at all.
e.g. If numberOfFeatures = 3
Product 1 - Manufacturer A (1st feature)
Product 2 - Manufacturer B (2nd feature)
Product 3 - Manufacturer C (3rd feature)
Product 4 - Manufacturer A (...not checked...)
Product 5 - Manufacturer A (...not checked...)

e.g. Case to adjust ... for example ... INPUT List
Product 1 - Manufacturer A
Product 2 - Manufacturer A
Product 3 - Manufacturer B
Product 4 - Manufacturer A
Product 5 - Manufacturer F
(... we would want ...)
Product 1 - Manufacturer A (1st feature)
Product 3 - Manufacturer B (2nd feature ... moved up)
Product 5 - Manufacturer F (3rd feature ... moved up)
Product 2 - Manufacturer A (...pushed down the list...)
Product 4 - Manufacturer A (...pushed down the list...)

+2  A: 
Bryan Watts
I don't think that would guarantee that the rest of the records will stay in the same order, it just grabs the first product from each manufacturer.
R0MANARMY
You're right, I missed that requirement. Editing.
Bryan Watts
And this solution is better than simply looping through the products and splitting them into two buckets (then re-joining them) or are you just trying to see if there's a way to bend Linq to do your bidding?
R0MANARMY
While I appreciate your implication that this is a case of LINQ for LINQ's sake, it may be premature. The problem we are solving is a custom ordering of a sequence: the first products of the first 3 manufacturers sort first, then the rest follow. LINQ supports the concept of ordering a sequence with a pluggable algorithm. As the problem implies a custom sort algorithm, and it can be neatly expressed with minimial imperative logic, that seems like the natural choice. These are the tools intended for this exact job, and anyone familiar with `OrderBy` can understand it.
Bryan Watts
It also does not require the 2 extra lists or the re-join. I aim to write code which says what I'm doing, not how I'm doing it. The `OrderBy` statement says "Order products using a comparison designed for this many featured products." You know what it does as soon as you read it. The intent is clear. To read the loops of your proposed solution, a reader has to mentally compile and run it. Both express a solution to the problem, but one expresses it closer to how people think, and the other closer to how computers think. That is my opinion, anyways :-)
Bryan Watts
Thanks Bryan ... I'm just checking this out ... I posted a 'clarification' on the post - I need the original sort order (ranking) to be maintained ... will you custom comparer result accommodate keeping the original ranking (except for pushing some up/down the list to get the first n items to be different manufacturers) ?
Rob
Yes it will. `OrderBy` preserves the order of equal elements. Look at the bottom of the remarks section here: http://msdn.microsoft.com/en-us/library/bb549422.aspx. The comparer works like this for any 2 products: if there are already 3 manufacturers found, the original order is used. If there are less than 3 manufacturers and either product has a new one, it sorts first. If the new featured product is being compared to an existing featured product, the existing one sorts first. This maintains the original order because the featured products are simply being "bubbled" up.
Bryan Watts
A: 

If I understand you correctly I think this will work for you.

Are you asking what are the first "numberOfFeatures" of "Product"s that have different "ManufacturerId"s using the ordered list "products"?

public List<Product> SetFeatures(List<Product> products, int numberOfFeatures)
{
    return products
        .GroupBy(p => p.ManufacturerId)
        .Take(numberOfFeatures)
        .Select(g => g.First());
}
Jason
Nope, that's what I thought originally too. See the comments on my answer for clarification.
Bryan Watts
+1  A: 

Edit turns out Distinct() is not required, the revised code:

Is this what you want?

var result = 
        products
            .GroupBy(x => x.Id)
            .Take(numberOfFeatures)
            .Select(x => x.First())
            .Union(products);
return result.ToList();

Note that GroupBy will have the correct sequence and Union will also

Cornelius
This won't work because GroupBy changes the order of the products, it's the same as Jason's and Bryan Watt's solution, and doesn't work in the exact same way.
R0MANARMY
@ROMANARMY Yes but the later products' (that the union is done by) order will not have changed and the order will be correct. The group by is just used for the _featured_ products. Try it will sample data to see for yourself.
Cornelius
Is there any guarantee that `Distinct()` will pick the first featured member of the list and not the one further down?
Gabe
@gabe `Distinct()` keeps the first element it finds in a sequence, since the IEnumerable can only be traversed with the `Enumerator` and the `Enumerator` enumerates the list in sequence. In _PLinq_ it could be that it is different not sure, but this is not _PLinq_.
Cornelius
@Cornelius, I understand GroupBy doesn't change the order of the original collection. The problem is it most likely re-arranges the recommendation. For example Product that used to be at the top of the list could be attached to the **last** manufacturer in the grouping. Thus it wouldn't show as a recommended product even though it was at the top of the product list.
R0MANARMY
@gabe as for guarantee not sure if MS has one but it does make a good question http://stackoverflow.com/questions/2475472/does-distinct-preserve-always-take-the-first-element-in-the-list @ROMANARYMY from the msdn _The IGrouping(TKey, TElement) objects are yielded in an order based on the order of the elements in source that produced the first key of each IGrouping(TKey, TElement). Elements in a grouping are yielded in the order they appear in source._ http://msdn.microsoft.com/en-us/library/bb534501.aspx
Cornelius
I think you should be using `Concat` instead of `Union` because Union is essentially the same thing as Concat().Distinct().
Gabe
@gabe turns out only `Union` is required because union preserve the sequence: http://msdn.microsoft.com/en-us/library/bb341731.aspx
Cornelius
This algorithm is `O(2N + numberOfFeatures)`, meaning it will process all the products twice, plus `numberOfFeatures` groups. There are also a lot of comparisons happening for the `GroupBy` and `Union`. This is a viable way to solve the problem, just not the most optimized.
Bryan Watts
Thanks ... I'm just checking this out ... I posted a 'clarification' on the post. I need the original sort order (ranking) to be maintained ... will the 'group by' result be sorted in a specific order?
Rob
@Rob, looks like Cornelius is correct and the order should be preserved.
R0MANARMY
+2  A: 

I think Bryan encapsulated the sorting logic pretty well and it's not something I thought of doing. I'd like to present my take on it anyway using a Foo example.

        List<Foo> foos = new List<Foo>()
    {
        new Foo() { Baz = 1, Blah = "A"},
        new Foo() { Baz = 2, Blah = "A"},
        new Foo() { Baz = 3, Blah = "B"},
        new Foo() { Baz = 4, Blah = "B"},
        new Foo() { Baz = 5, Blah = "B"},
        new Foo() { Baz = 6, Blah = "C"},
        new Foo() { Baz = 7, Blah = "C"},
        new Foo() { Baz = 8, Blah = "D"},
        new Foo() { Baz = 9, Blah = "A"},
        new Foo() { Baz = 10, Blah = "B"},
    };

    var query = foos.Distinct(new FooComparer()).Take(3).ToList();
    var theRest = foos.Except(query);

    query.AddRange(theRest);

FooComparer being

    public class FooComparer : IEqualityComparer<Foo>
{
    public bool Equals(Foo x, Foo y)
    {
        return x.Blah == y.Blah;
    }

    public int GetHashCode(Foo obj)
    {
        return obj.Blah.GetHashCode();
    }
}

You get (Baz) 1, 3, and 6 shifted to top, the remaining in their original order afterwards.

Anthony Pegram
Anthony, thanks.This seems to have gotten me where I need to be. I'll do a little more a/b testing, but looks good so far.thanks
Rob
There are some good thought processes in each of these answers. I'm glad to contribute to them.
Anthony Pegram
+1 Interesting approach. It reads well. I optimized for the least number of iterations and comparisons, leading me to think about things at the individual comparison level. Your approach frames the solution as a query, which is arguably clearer and worth the O(3N) cost.
Bryan Watts