combining two LINQ expressions into one - c#

In this case, I have two different LINQ expressions to get count from Products for two different conditions. I was just curious if there could be anyway of retrieving these two counts from one LINQ expression?
class Program
{
static void Main(string[] args)
{
List<Product> Products = new List<Product>()
{
new Product() { ID = 1 },
new Product() { ID = 2 },
new Product() { ID = 3 },
new Product() { ID = 4 },
new Product() { ID = 5 },
new Product() { ID = 6 }
};
int all = Products.Count();
int some = Products.Where(x => x.ID < 2).Count();
}
}
public class Product
{
public int ID { get; set; }
}

Using Aggregate you can avoid iterating through your collection twice:
var result = Products.Aggregate(new {a=0, s=0},(p,c) =>
{
return new { a = p.a + 1, s = c.ID < 2 ? p.s + 1 : p.s };
});
Now result.a == 6 and result.s == 2
You can, of course, create a class to hold your result if you want instead of using an anonymous type, and it works much the same way. That maybe easier to deal with if you have to return it from a function, for example.
So you could do something like:
public class CountResult
{
public int All { get; set; }
public int Some { get; set; }
}
public CountResult GetMyCount(IEnumerable<Product> products)
{
return products.Aggregate(new CountResult(), (p,c) =>
{
p.All++;
if (c.ID < 2) // or whatever you condition might be
{
p.Some++;
}
return p;
});
}

You can do this using a Tuple<int, int>:
var result = new Tuple<int, int>(Products.Count, Products.Where(x => x.ID < 2).Count());
And plese remark the use of Products.Count property which has O(1) complexity instead of O(N), so you don't have to worry at all about the performance of this implementation. For further reading you can check this post.

Related

Joining two lists of object optimization

I am looking for a way of optimizing my LINQ query.
Classes:
public class OffersObject
{
public List<SingleFlight> Flights { get; set; }
public List<Offer> Offers { get; set; } = new List<Offer>();
}
public class SingleFlight
{
public int Id { get; set; }
public string CarrierCode { get; set; }
public string FlightNumber { get; set; }
}
public class Offer
{
public int ProfileId { get; set; }
public List<ExtraOffer> ExtraOffers { get; set; } = new List<ExtraOffer>();
}
public class ExtraOffer
{
public List<int> Flights { get; set; }
public string Name { get; set; }
}
Sample object:
var sampleObject = new OffersObject
{
Flights = new List<SingleFlight>
{
new SingleFlight
{
Id = 1,
CarrierCode = "KL",
FlightNumber = "1"
},
new SingleFlight
{
Id = 2,
CarrierCode = "KL",
FlightNumber = "2"
}
},
Offers = new List<Offer>
{
new Offer
{
ProfileId = 41,
ExtraOffers = new List<ExtraOffer>
{
new ExtraOffer
{
Flights = new List<int>{1},
Name = "TEST"
},
new ExtraOffer
{
Flights = new List<int>{2},
Name = "TEST"
},
new ExtraOffer
{
Flights = new List<int>{1,2},
Name = "TEST"
}
}
}
}
};
Goal of LINQ query:
List of:
{ int ProfileId, string CommercialName, List<string> fullFlightNumbers }
FullFlightNumber should by created by "Id association" of a flight. It is created like: {CarrierCode} {FlightNumber}
What I have so far (works correctly, but not the fastest way I guess):
var result = sampleObject.Offers
.SelectMany(x => x.ExtraOffers,
(a, b) => {
return new
{
ProfileId = a.ProfileId,
Name = b.Name,
FullFlightNumbers = b.Flights.Select(f => $"{sampleObject.Flights.FirstOrDefault(fl => fl.Id == f).CarrierCode} {sampleObject.Flights.First(fl => fl.Id == f).FlightNumber}").ToList()
};
})
.ToList();
Final note
The part that looks wrong to me is:
.Select(f => $"{sampleObject.Flights.FirstOrDefault(fl => fl.Id == f)?.CarrierCode} {sampleObject.Flights.FirstOrDefault(fl => fl.Id == f)?.FlightNumber}").ToList()
I am basically looking for a way of "joining" those two lists of the OffersObject by Flight's Id.
Any tips appreciated.
If there will only be a few flights defined in sampleObject.Flights, a sequential search using a numeric key is hard to beat.
However, if the number of flights times the number of offers is substantial (1000s or more), I would suggest loading the list of flights into a dictionary with Id as the key for efficient lookup. Something like:
var flightLookup = sampleObject.Flights.ToDictionary(f => f.Id);
And then calculate your FullFlightNumbers as
FullFlightNumbers = b.Flights
.Select(flightId => {
flightLookup.TryGetValue(flightId, out SingleFlight flight);
return $"{flight?.CarrierCode} {flight?.FlightNumber}";
})
.ToList()
TryGetValue above will quietly return a null value for flight if no match is found. If you know that a match will always be present, the lookup cold alternately be coded as:
SingleFlight flight = flightLookup[flightId];
The above also uses a statement lambda. In short, lambda functions can have either expression or statement blocks as bodies. See the C# reference for more information.
I'd suggest replacing the double .FirstOrDefault() approach with .IntersectBy(). It is available in the System.Linq namespace, starting from .NET 6.
.IntersectBy() basically filters sampleObject.Flights by matching the flight ID for each flight in sampleObject with flight IDs in ExtraOffers.Flights.
In the code below, fl => fl.Id is the key selector for sampleObject.Flights (i.e. fl is a SingleFlight).
var result = sampleObject.Offers
.SelectMany(x => x.ExtraOffers,
(a, b) => {
return new
{
ProfileId = a.ProfileId,
Name = b.Name,
FullFlightNumbers = sampleObject.Flights
.IntersectBy(b.Flights, fl => fl.Id)
.Select(fl => fl.FullFlightNumber) // alternative 1
//.Select(fl => $"{fl.CarrierCode} {fl.FlightNumber}") // alternative 2
.ToList()
};
})
.ToList();
In my suggestion I have added the property FullFlightNumber to SingleFlight so that the Linq statement looks slightly cleaner:
public class SingleFlight
{
public int Id { get; set; }
public string CarrierCode { get; set; }
public string FlightNumber { get; set; }
public string FullFlightNumber => $"{CarrierCode} {FlightNumber}";
}
If defining SingleFlight.FullFlightNumber is not possible/desirable for you, the second alternative in the code suggestion can be used instead.
Example fiddle here.

C# Finding duplicate properties inside a list of objects that contains another list

I've got a list of objects, let's call the objects People. Each person has a list of vacation days they've requested. I'm trying to create a Linq query to find out how many people have requested the same vacation day. I'm not having any luck. Any advice, or perhaps a shove in the right direction?
You could create classes like this:
public class Person
{
public List<int> Vacations { get; set; }
public string Name { get; set; }
}
public class Vacation
{
public List<string> People { get; set; }
public int Day { get; set; }
public int PeopleCount => People?.Count ?? 0;
}
and then get a list of people who have booked each day like this:
public void Test()
{
var people = new List<Person>()
{
new Person() { Name = "Person1", Vacations = new List<int>() { 1, 15, 200, 364 } },
new Person() { Name = "Person2", Vacations = new List<int>() { 1, 15, 110, 210 } },
new Person() { Name = "Person3", Vacations = new List<int>() { 1, 15, 200 , 210} }
};
var vacations =
Enumerable.Range(0, 365)
.Select(d => new Vacation()
{
Day = d,
People = people.Where(p
=> p.Vacations.Contains(d)).Select(p => p.Name).ToList()
})
.ToList();
}
This should help:
class Customer
{
List<Vacation> vacationDays {get; set;}
}
public class Vacation : IEquatable<Vacation>
{
public string Name { get; set; }
public int VacationId { get; set; }
public override string ToString()
{
return "ID: " + VacationId + " Name: " + Name;
}
public override bool Equals(object obj)
{
if (obj == null) return false;
Vacation objAsVacation = obj as Vacation;
if (objAsVacation == null) return false;
else return Equals(objAsVacation);
}
public override int GetHashCode()
{
return VacationId;
}
public bool Equals(Vacation other)
{
if (other == null) return false;
return (this.VacationId.Equals(other.VacationId));
}
// Should also override == and != operators.
}
Now, you can use SelectMany here:
https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.selectmany?view=netframework-4.7.2
More on Contains here:
https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.contains?view=netframework-4.7.2
Start by flattening the list. This code will create a single, non-nested list containing one row for each person/date:
var flatList = people.SelectMany
(
p => p.VacationDays.Select( d => new { Person = p, Date = d } )
);
Then you can filter any way you want quite trivially:
var lookFor = DateTime.Parse("1/1/2019");
var entriesForJan01 = flatList.Where( f => f.Date == lookFor );
Click this link for a working example on DotNetFiddle
This can be done in many ways using LINQ, but I would like to suggest a more efficient solution if you want to do the lookup for multiple dates. My solution uses a Dictionary<DateTime,int> which acts as a counter for the days we have encountered. Thanks to the fact that Dictionary lookup is in constant time complexity (O(1)), this solution will be very efficient when you need to check the number of occurrences for multiple dates or even for all dates.
var dateOccurrences = new Dictionary<DateTime, int>();
foreach (var vacationDate in people.SelectMany(p => p.Vacations))
{
//check if we already have this date in the dictionary
if (!dateOccurrences.TryGetValue(vacationDate.Date, out int previousOccurrences))
{
//never seen before
previousOccurrences = 0;
}
//add one occurrence
dateOccurrences[vacationDate] = previousOccurrences + 1;
}
Now for lookup just use TryGetValue again. Alternatively, you can foreach over all the dictionary entries:
foreach (var pair in dateOccurrences)
{
Console.WriteLine(pair.Key);
Console.WriteLine(pair.Value);
}

Code review of this portion of Linq Filters

I am a newbie of c #, I would like to know if I can remove the for each and do a single operation with Linq. I would like to return an IEnumerable with already filtered. is it possible to do this? Every suggestion is welcome, thank you very much
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Linq
{
class Oggetto
{
public int Id { get; set; }
public string MyProperty { get; set; }
public int Deleted { get; set; }
}
class Program
{
static void Main(string[] args)
{
IEnumerable<Oggetto> lista = new List<Oggetto> {
new Oggetto(){ Id = 1, MyProperty = "Propr1", Deleted = 0 },
new Oggetto(){ Id = 1, MyProperty = "Propr2", Deleted = 1 },
new Oggetto(){ Id = 2, MyProperty = "Prop3", Deleted = 0 },
new Oggetto(){ Id = 3, MyProperty = "Propr4", Deleted = 0 },
new Oggetto(){ Id = 3, MyProperty = "Prop5", Deleted = 1 }
};
foreach (var item in lista.Where(x => x.Deleted == 1).GroupBy(x => x.Id).Select(g => g.First()))
{
item.MyProperty = string.Join(",", lista.Where(t => t.Id == item.Id).Select(x => x.MyProperty).ToArray());
Console.WriteLine(item.Id);
Console.WriteLine(item.MyProperty);
}
Console.ReadLine();
}
}
}
You can use projection for this.
var orderedList = lista.GroupBy(x => x.Id)
.Where(x => x.Any(y => y.Deleted == 1))
.Select(x => new Oggetto
{
Id = x.Key, MyProperty = string.Join(",", x.Select(v => v.MyProperty))
});
foreach (var item in orderedList)
{
Console.WriteLine(item.Id);
Console.WriteLine(item.MyProperty);
}
Anyway, as #Alex said you shoud replace Deleted field type to bool and as said by #Marco Salerno start programming in English you'll not regret.
First of all I would avoid the groupBy statement. This is a lot of unneded overhead. You can use distinct instead. This will give you all the IDs you need to know.
var ids = lista.Where(x => x.Deleted).Select(x => x.Id).Distinct();
You can then select all the elements that you need with:
var items = ids.Select(i => lista.Where(x => x.Id == i));
which results in a List of Lists. For the ease of use I would convert this to a Dictionary<K, V> (int this case it's Dictionary<long, List<string>> as a final step:
var dictionary = items.ToDictionary(l => l.First().Id, l => l.Select(o => o.MyProperty).ToList());
You now got a "nice and filtered" collection you can use any way you like (or just output it)
foreach (var item in dictionary)
{
Console.WriteLine($"Id: {item.Key}");
Console.WriteLine($"Properties: {string.Join(", ", item.Value)}");
}
I also changed your class a little bit to:
class Oggetto
{
public int Id { get; set; }
public string MyProperty { get; set; }
// bool instead of int - Deleted has only 2 states
public bool Deleted { get; set; }
}
First of all STOP programming in Italian, start doing it in English.
Anyway, this should be a better approach:
class Program
{
static void Main(string[] args)
{
List<Item> items = new List<Item> {
new Item{ Id = 1, MyProperty = "Propr1", Deleted = 0 },
new Item{ Id = 1, MyProperty = "Propr2", Deleted = 1 },
new Item{ Id = 2, MyProperty = "Prop3", Deleted = 0 },
new Item{ Id = 3, MyProperty = "Propr4", Deleted = 0 },
new Item{ Id = 3, MyProperty = "Prop5", Deleted = 1}
};
foreach (IGrouping<int,Item> group in items.GroupBy(x => x.Id).ToList())
{
List<Item> groupItems = group.ToList();
Item deletedItem = groupItems.Where(x => x.Deleted == 1).FirstOrDefault();
if(deletedItem != null)
{
deletedItem.MyProperty = string.Join(",", groupItems.Select(x => x.MyProperty).ToArray());
Console.WriteLine(deletedItem.Id);
Console.WriteLine(deletedItem.MyProperty);
}
}
}
}
class Item
{
public int Id { get; set; }
public string MyProperty { get; set; }
public int Deleted { get; set; }
}

Excluding items from a list that exist in another list

I have a list for example List<string> ListProviderKeys that has some values in it.
I also have a second list from a class below, for example List<ChangesSummary> SecondList;
public class ChangesSummary
{
public string TableName { get; set; }
public string ProviderKey { get; set; }
public string ProviderAdrsKey { get; set; }
public string ProviderSpecialtyKey { get; set; }
public string FieldName{ get; set; }
}
Imagine the values that first list holds is the same kind of values we put in ProviderKey field in the second list.
Now What I want is to trim down the second list to only have values that their ProviderKey IS NOT already in the first list.
How Can I do that? I know the operator Except but not sure how to apply it in this situation!
The best I can think of is :
A) Create dictionary and use its fast lookups
B) Use LINQ .Where method with .ContainsKey() on this dictionary which internally uses Hashtable and performs quick lookups.
This should reduce search complexity to almost O(1) rather than O(N) ro worse (when we use LINQ .Where() with .Any() or .Contains() and that leads to nested loops).
From MSDN page :
The Dictionary generic class provides a mapping from a set of keys to
a set of values. Each addition to the dictionary consists of a value
and its associated key. Retrieving a value by using its key is very
fast, close to O(1), because the Dictionary class is implemented as a
hash table.
So what we can do is :
Dictionary<string, string> dict = ListProviderKeys.ToDictionary(s => s);
var newList = SecondList.Where(e => !dict.ContainsKey(e.ProviderKey)).ToList();
Here is a very simple, short, but complete example illustrating it and also testing its performance :
class Person
{
public int Id { get; set; }
}
class Program
{
static void Main(string[] args)
{
List<int> ints = new List<int>();
List<Person> People = new List<Person>(1000);
for (int i = 0; i < 7500; i++)
{
ints.Add(i);
ints.Add(15000 - i - 1);
}
for (int i = 0; i < 45000; i++)
People.Add(new Person() { Id = i });
Stopwatch s = new Stopwatch();
s.Start();
// code A (feel free to uncomment it)
//Dictionary<int, int> dict = ints.ToDictionary(p => p);
//List<Person> newList = People.Where(p => !dict.ContainsKey(p.Id)).ToList();
// code B
List<Person> newList = People.Where(p => !ints.Contains(p.Id)).ToList();
s.Stop();
Console.WriteLine(s.ElapsedMilliseconds);
Console.WriteLine("Number of elements " + newList.Count);
Console.ReadKey();
}
On release mode results are :
Both code A & code B outputs 30 000 elements but :
It took more than 2000 ms with code B and only 5 ms with code A
public class Programm
{
public static void Main()
{
List<ChangesSummary> summaries = new List<ChangesSummary>();
summaries.Add(new ChangesSummary()
{
FieldName = "1",
ProviderKey = "Test1",
});
summaries.Add(new ChangesSummary()
{
FieldName = "2",
ProviderKey = "Test2",
});
summaries.Add(new ChangesSummary()
{
FieldName = "3",
ProviderKey = "Test3",
});
List<string> listProviderKeys = new List<string>();
listProviderKeys.Add("Test1");
listProviderKeys.Add("Test3");
var res = summaries.Where(x => !listProviderKeys.Contains(x.ProviderKey));
res.ToList().ForEach(x => Console.WriteLine(x.ProviderKey));
Console.ReadLine();
}
}
public class ChangesSummary
{
public string TableName { get; set; }
public string ProviderKey { get; set; }
public string ProviderAdrsKey { get; set; }
public string ProviderSpecialtyKey { get; set; }
public string FieldName { get; set; }
}
I think in this case simple Where would be easier and more readable to apply.
var first = new List<string> { "a" };
var second = new List<ChangesSummary>()
{
new ChangesSummary() { ProviderKey = "a" },
new ChangesSummary() { ProviderKey = "b" }
};
var result = second.Where(item => !first.Contains(item.ProviderKey));
// result
// .ToList()
// .ForEach(item => Console.WriteLine(item.ProviderKey));
I believe this will work:
List<ChangesSummary> ExceptionList = SecondList.
Where(x => !ListProviderKeys.Any(key => x.ProviderKey == key)).ToList();

Group by with multiple columns using lambda

How can I group by with multiple columns using lambda?
I saw examples of how to do it using linq to entities, but I am looking for lambda form.
var query = source.GroupBy(x => new { x.Column1, x.Column2 });
I came up with a mix of defining a class like David's answer, but not requiring a Where class to go with it. It looks something like:
var resultsGroupings = resultsRecords.GroupBy(r => new { r.IdObj1, r.IdObj2, r.IdObj3})
.Select(r => new ResultGrouping {
IdObj1= r.Key.IdObj1,
IdObj2= r.Key.IdObj2,
IdObj3= r.Key.IdObj3,
Results = r.ToArray(),
Count = r.Count()
});
private class ResultGrouping
{
public short IdObj1{ get; set; }
public short IdObj2{ get; set; }
public int IdObj3{ get; set; }
public ResultCsvImport[] Results { get; set; }
public int Count { get; set; }
}
Where resultRecords is my initial list I'm grouping, and its a List<ResultCsvImport>. Note that the idea here to is that, I'm grouping by 3 columns, IdObj1 and IdObj2 and IdObj3
if your table is like this
rowId col1 col2 col3 col4
1 a e 12 2
2 b f 42 5
3 a e 32 2
4 b f 44 5
var grouped = myTable.AsEnumerable().GroupBy(r=> new {pp1 = r.Field<int>("col1"), pp2 = r.Field<int>("col2")});
Further to aduchis answer above - if you then need to filter based on those group by keys, you can define a class to wrap the many keys.
return customers.GroupBy(a => new CustomerGroupingKey(a.Country, a.Gender))
.Where(a => a.Key.Country == "Ireland" && a.Key.Gender == "M")
.SelectMany(a => a)
.ToList();
Where CustomerGroupingKey takes the group keys:
private class CustomerGroupingKey
{
public CustomerGroupingKey(string country, string gender)
{
Country = country;
Gender = gender;
}
public string Country { get; }
public string Gender { get; }
}
class Element
{
public string Company;
public string TypeOfInvestment;
public decimal Worth;
}
class Program
{
static void Main(string[] args)
{
List<Element> elements = new List<Element>()
{
new Element { Company = "JPMORGAN CHASE",TypeOfInvestment = "Stocks", Worth = 96983 },
new Element { Company = "AMER TOWER CORP",TypeOfInvestment = "Securities", Worth = 17141 },
new Element { Company = "ORACLE CORP",TypeOfInvestment = "Assets", Worth = 59372 },
new Element { Company = "PEPSICO INC",TypeOfInvestment = "Assets", Worth = 26516 },
new Element { Company = "PROCTER & GAMBL",TypeOfInvestment = "Stocks", Worth = 387050 },
new Element { Company = "QUASLCOMM INC",TypeOfInvestment = "Bonds", Worth = 196811 },
new Element { Company = "UTD TECHS CORP",TypeOfInvestment = "Bonds", Worth = 257429 },
new Element { Company = "WELLS FARGO-NEW",TypeOfInvestment = "Bank Account", Worth = 106600 },
new Element { Company = "FEDEX CORP",TypeOfInvestment = "Stocks", Worth = 103955 },
new Element { Company = "CVS CAREMARK CP",TypeOfInvestment = "Securities", Worth = 171048 },
};
//Group by on multiple column in LINQ (Query Method)
var query = from e in elements
group e by new{e.TypeOfInvestment,e.Company} into eg
select new {eg.Key.TypeOfInvestment, eg.Key.Company, Points = eg.Sum(rl => rl.Worth)};
foreach (var item in query)
{
Console.WriteLine(item.TypeOfInvestment.PadRight(20) + " " + item.Points.ToString());
}
//Group by on multiple column in LINQ (Lambda Method)
var CompanyDetails =elements.GroupBy(s => new { s.Company, s.TypeOfInvestment})
.Select(g =>
new
{
company = g.Key.Company,
TypeOfInvestment = g.Key.TypeOfInvestment,
Balance = g.Sum(x => Math.Round(Convert.ToDecimal(x.Worth), 2)),
}
);
foreach (var item in CompanyDetails)
{
Console.WriteLine(item.TypeOfInvestment.PadRight(20) + " " + item.Balance.ToString());
}
Console.ReadLine();
}
}

Categories

Resources