2

While I don't have a hard requirement for this, I'd like to manipulate all elements of a list except for the last one. For example, assume a class Cars

public class Car {
  public string Name { get; set; }
  public List<Tire> Tires { get; set; }
}

var cars = _db.GetAllCars().Select(x => new Car {
  Name = x.Name + ", ",
  Tires = x.Tires
});

This works, except the last element in the list will have a comma, which I don't want ... but I still want the last item in the list. I cannot so a simple string.Join as I'm passing this to a view which has a lot of template code around the Name element. Is what I'm trying to do possible?

chum of chance
  • 6,200
  • 10
  • 46
  • 74
  • 1
    This is the View/ViewModel's responsibilty, not the model's. – It'sNotALie. Jul 22 '13 at 17:40
  • My solution with `Take` isn't good. Did not realise you wanted to get the last one. @newStackExchangeInstance is right. – Simon Belanger Jul 22 '13 at 17:46
  • I agree, this work is actually being done in a mapper but I didn't want to obfuscate it. My actual code looks more like _db.GetAllCars().Select(_carMapper.Map); – chum of chance Jul 22 '13 at 17:57
  • This is the subject of [Eric Lippert's "comma-quibbling" challenge](http://stackoverflow.com/questions/788535/eric-lipperts-challenge-comma-quibbling-best-answer). – Raymond Chen Jul 22 '13 at 19:02

4 Answers4

4

Try this:

var cars = _db.GetAllCars().Reverse().Select((x, i) => new Car {
    Name = x.Name + (i == 0 ? "" : ", "),
    Tires = x.Tires
}).Reverse();
Krzysztof
  • 15,900
  • 2
  • 46
  • 76
  • Interesting aspect: does the double reverse show up in SQL code? – Pleun Jul 22 '13 at 18:27
  • 1
    @Pleun Well, from the perspective of the DB you need to have an OrderBy call for reversing to make sense; as it is the order is undefined in SQL unless there is a relevant clause; sets are unordered. It's only when you have gotten the result set in C# that you have any order at all. If there *is* an orderby then you can just swap the ascending/decending to get a reverse. The `Select` method won't translate into SQL as it stands, so you'll need to have an `AsEnumerable` in there (probably before the `Reverse`) for this to actually run. Note that the first reverse isn't needed, just the last. – Servy Jul 22 '13 at 18:37
  • Actually result from db migth be ordered if there is index. If order is not a clue, first reverse is not required obviously. But you should notice that in question there is nothing about sql, so `_db` might be in memory db, or anything else - in that case both reverse calls are expected. – Krzysztof Jul 22 '13 at 19:51
2

Improved version of Lolo (give points there)

var cars = _db.GetAllCars().Select((x, i) => new Car {
    Name = (i == 0 ? "" : ", ") + x.Name  ,
    Tires = x.Tires
});
Pleun
  • 8,856
  • 2
  • 30
  • 50
0

What about:

var allCars = _db.GetAllCars();
var cars = allCars.Select((x, i) => new Car { //where i is the index
   Name = x.Name + (i != allCars.Count - 1 ? ", " : string.Empty), //or "." if you need a comma
   Tires = x.Tires
});

EDIT:

As said @newStackExchangeInstance this works if _db.GetAllCars() returns an ICollection, otherwise you need to turn it in a list:

var allCars = _db.GetAllCars().ToList();

Or just use the Count() extension method.

Omar
  • 16,329
  • 10
  • 48
  • 66
  • Yes and if it doesn't return an `ICollection` use `ToList()`. – Omar Jul 22 '13 at 17:55
  • `Count` is still expensive, and so is an O(n) iteration to turn it into a `List`. Just use `Count()`, it checks if it's an `ICollection`. Also cache the count. – It'sNotALie. Jul 22 '13 at 17:58
  • The `Count()` extension method checks if it is an `ICollection` and if it isn't it iterates. So there are no differences. – Omar Jul 22 '13 at 18:02
  • Sure, but it's better than just blindly converting to a List – It'sNotALie. Jul 22 '13 at 18:03
  • @newStackExchangeInstance Using the `Count` extension method here would result in executing two database queries, one to get the count, one to get the items. You don't need two queries, you only need one. You should either find a solution that doesn't require knowing the count first, or convert the results to a list; the alternative is quite expensive. – Servy Jul 22 '13 at 18:17
-1

If you really want to manipulate it right after retrieved from data source:

var allCars = _db.GetAllCars().Select(x => new Car {
    Name = x.Name,
    Tires = x.Tires
}).ToList();

var cars = allCars.Take(allCars.Count() - 1).Select(x => new Car
{
    Name = x.Name + ", ",
    Tires = x.Tires
}).Concat(new[] { new Car {
    Name = allCars.Last().Name,
    Tires = allCars.Last().Tires
}});
Fung
  • 3,508
  • 2
  • 26
  • 33
  • this is iterating the query four times (the main query in which you call `Take`, and then there is the `Count` and the two `Last` calls), which means executing four database queries when one is needed. – Servy Jul 22 '13 at 18:19
  • @Servy All subsequent query are operating on `allCars`, you can make a `ToList()` call first to query once. See Edit. – Fung Jul 23 '13 at 01:58
  • Yes, I know. My point was that materializing the query into a collection is *very* important, and you hadn't done it. The fix is simple enough, but the consequences for gettting it wrong are significant. Note that one problem with this fix though is that you're forced to bring the entire query results into memory, all at once, you can't stream the results as could be done with a more effective (although possibly complex) answer. – Servy Jul 23 '13 at 15:31