C# Code Optimization GroupBy - c#

I have a couple of problems with my below code.
var groupedItems = inputFiles.GroupBy(q => q.Name.ToLower().Split('_').ElementAt(2));
string currentNo = ////value retreived from someMethod;
if (string.IsNullOrEmpty(currentNo))
{
if (groupedItems.Count() > 1)
{
foreach (var group in groupedItems)
{
foreach (var groupedItem in group)
{
ErrorFile(groupedItem);
}
}
}
else if (groupedItems.Count() == 1)
{
ProcessFile();
}
}
else
{
foreach (var group in groupedItems.Where(x => x.Key != currentNo))
{
foreach (var groupedItem in group)
{
ErrorFile(groupedItem);
}
}
}
There is repetitive code with nested foreach loops. I am looking at possibility of optimization
How do I handle when Split and ElementAt(2) return errors. I still need to call ErrorFile() method even if I am unable to Split by _.

Hard to understand what your code is really doing without more context, but this should be about right:
static void SomeMethod(IEnumerable<File> inputFiles)
{
var groupedItems = inputFiles.GroupBy
(f => splitAndFindElement(f.Name, '_', 2, string.Empty));
string currentNo = //whatever;
if (string.IsNullOrEmpty(currentNo))
{
if (groupedItems.Count() > 1)
{
foreach (var item in groupedItems.SelectMany(g => g))
{
ErrorFile(item);
}
}
else if (groupedItems.Count() == 1)
{
ProcessFile();
}
}
else
{
foreach (var item in groupedItems.Where(g => g.Key != currentNo).SelectMany(g => g))
{
ErrorFile(item);
}
}
}
And the helper method:
static string splitAndFindElement(string input, char splitter, int index, string resultOnFail)
{
var succesful = false;
string[] words = null;
if (input != null)
{
words = input.Split(splitter);
succesful = words.Length > index;
}
return succesful ? words[index] : resultOnFail;
}
The trick here is to group files with key "" if they can't be split. This will ensure that they will be processed with ErrorFile either becuase there is more than one grouping or because the key does not equal currentNo. I'm assuming here that Name can't end in "_".
Also, SelectMany is used to flatten enumerables of enumerables and avoid nested loops.

We can optimize repeated loops as following, and to handle error use try catch
var groupedItems;
try
{
groupedItems= inputFiles.GroupBy(q => q.Name.ToLower().Split('_').ElementAt(2));
string currentNo = ////value retreived from someMethod;
if (string.IsNullOrEmpty(currentNo) && groupedItems.Count() == 1)
{
ProcessFile();
}
else
{
foreach (var group in groupedItems.Where(x => string.IsNullOrEmpty(currentNo) || x.Key != currentNo))
{
foreach (var groupedItem in group)
{
ErrorFile(groupedItem);
}
}
}
}
catch
{
ErrorFile(groupedItem);
}

Related

Change loop to Linq C#

One of my function in C# required me to filter some value.So, I try it by using a lot of loop in it. It works but doesn't look like effecient.Any idea on how to convert code below to LinQ?
Any help is appreciated.
var object1 = JsonConvert.DeserializeObject<List<string>>(object1json);
foreach (var item1 in table1)
{
if (item1.Code == InputCode)
{
for (int i = 0; i < object1.Count(); i++)
{
tempData temp = new tempData();
foreach (var item2 in item1.List)
{
if (item2.Code == object1[i])
{
temp.Code = item2.Code;
temp.Description = item2.Description;
}
}
if(temp.Code != null || temp.Description != null)
final.Add(temp);
}
}
}
If you want your code to be more efficient, as pointed out in the comments, converting it to Linq isn't really going to help. It's still the same logic, just written in a different way. If you're going for readability, it can be improved with just a few changes:
foreach (var item1 in table1.Where(i => i.Code == InputCode))
{
foreach (var code in object)
{
// This could be SingleOrDefault, I don't know if you have duplicates in the list or not
var item2 = item1.List.LastOrDefault(i => i.Code == code);
if(item2 != null)
{
final.Add(new tempData
{
Code = item2.Code,
Description = item2.Description,
});
}
}
}
If you convert the whole thing to Linq:
var final = table1.Where(i => i.Code == InputCode)
.SelectMany(item1 => object.Select(code => item1.List.LastOrDefault(i => i.Code == code))
.Where(item2 => item2 != null)
.Select(item2 => new tempData
{
Code = item2.Code,
Description = item2.Description,
})
.ToList();
Personally, I prefer the first option, as it's a bit easier to read.
I guess what you post is sample code instead of actual code otherwise it would be better to avoid keyword like object in C#. Anyway,
var final = table1.Where(item1 => item1.Code == InputCode)
.SelectMany(item1 => item1.List)
.Where(item2 => #object.Contains(item2.Code))
.Where(temp => temp.Code != null || temp.Description != null)
.Select(item2 => new tempData()
{
Code = item2.Code,
Description = item2.Description
});

To make a linq with a multiple foreach

I have several foreach chained.
I would like to know if it is possible to do a linq synthesis of these foreach.
foreach(var item in parameter)
{
foreach (var worksheets in item.Item6.Worksheets)
{
var worksheetName = worksheets.Value.Name;
foreach (var dynamicRange in worksheets.Value.DynamicRanges)
{
var dynamicRangeRowStartIndex = dynamicRange.RowStartIndex;
var dynamicRangeColumnStartIndex = dynamicRange.ColumnStartIndex;
foreach (var node in dynamicRange.Nodes)
{
var nodeDirection = node.Direction;
foreach (var rule in node.Rules)
{
reportWorkbook.Worksheets.Values.First(c => c.Name == worksheetName).DynamicRanges.First(c => c.RowStartIndex == dynamicRangeRowStartIndex && c.ColumnStartIndex == dynamicRangeColumnStartIndex)
.NodeByDirection(nodeDirection).Rules[ruleCount].DefinitionRule.ContextItem = item.Item6.Worksheets.Values.First(c => c.Name == worksheetName).DynamicRanges.First(c => c.RowStartIndex == dynamicRangeRowStartIndex && c.ColumnStartIndex == dynamicRangeColumnStartIndex)
.NodeByDirection(nodeDirection).Rules[ruleCount].DefinitionRule.ContextItem;
ruleCount++;
}
ruleCount = 0;
}
}
}
}
Do you have any idea how to do this?
Thank you

Merge two or more T in List<T> based on condition

I have the below class:
public class FactoryOrder
{
public string Text { get; set; }
public int OrderNo { get; set; }
}
and collection holding the list of FactoryOrders
List<FactoryOrder>()
here is the sample data
FactoryOrder("Apple",20)
FactoryOrder("Orange",21)
FactoryOrder("WaterMelon",42)
FactoryOrder("JackFruit",51)
FactoryOrder("Grapes",71)
FactoryOrder("mango",72)
FactoryOrder("Cherry",73)
My requirement is to merge the Text of FactoryOrders where orderNo are in sequence and retain the lower orderNo for the merged FactoryOrder
- so the resulting output will be
FactoryOrder("Apple Orange",20) //Merged Apple and Orange and retained Lower OrderNo 20
FactoryOrder("WaterMelon",42)
FactoryOrder("JackFruit",51)
FactoryOrder("Grapes mango Cherry",71)//Merged Grapes,Mango,cherry and retained Lower OrderNo 71
I am new to Linq so not sure how to go about this. Any help or pointers would be appreciated
As commented, if your logic depends on consecutive items so heavily LINQ is not the easiest appoach. Use a simple loop.
You could order them first with LINQ: orders.OrderBy(x => x.OrderNo )
var consecutiveOrdernoGroups = new List<List<FactoryOrder>> { new List<FactoryOrder>() };
FactoryOrder lastOrder = null;
foreach (FactoryOrder order in orders.OrderBy(o => o.OrderNo))
{
if (lastOrder == null || lastOrder.OrderNo == order.OrderNo - 1)
consecutiveOrdernoGroups.Last().Add(order);
else
consecutiveOrdernoGroups.Add(new List<FactoryOrder> { order });
lastOrder = order;
}
Now you just need to build the list of FactoryOrder with the joined names for every group. This is where LINQ and String.Join can come in handy:
orders = consecutiveOrdernoGroups
.Select(list => new FactoryOrder
{
Text = String.Join(" ", list.Select(o => o.Text)),
OrderNo = list.First().OrderNo // is the minimum number
})
.ToList();
Result with your sample:
I'm not sure this can be done using a single comprehensible LINQ expression. What would work is a simple enumeration:
private static IEnumerable<FactoryOrder> Merge(IEnumerable<FactoryOrder> orders)
{
var enumerator = orders.OrderBy(x => x.OrderNo).GetEnumerator();
FactoryOrder previousOrder = null;
FactoryOrder mergedOrder = null;
while (enumerator.MoveNext())
{
var current = enumerator.Current;
if (mergedOrder == null)
{
mergedOrder = new FactoryOrder(current.Text, current.OrderNo);
}
else
{
if (current.OrderNo == previousOrder.OrderNo + 1)
{
mergedOrder.Text += current.Text;
}
else
{
yield return mergedOrder;
mergedOrder = new FactoryOrder(current.Text, current.OrderNo);
}
}
previousOrder = current;
}
if (mergedOrder != null)
yield return mergedOrder;
}
This assumes FactoryOrder has a constructor accepting Text and OrderNo.
Linq implementation using side effects:
var groupId = 0;
var previous = Int32.MinValue;
var grouped = GetItems()
.OrderBy(x => x.OrderNo)
.Select(x =>
{
var #group = x.OrderNo != previous + 1 ? (groupId = x.OrderNo) : groupId;
previous = x.OrderNo;
return new
{
GroupId = group,
Item = x
};
})
.GroupBy(x => x.GroupId)
.Select(x => new FactoryOrder(
String.Join(" ", x.Select(y => y.Item.Text).ToArray()),
x.Key))
.ToArray();
foreach (var item in grouped)
{
Console.WriteLine(item.Text + "\t" + item.OrderNo);
}
output:
Apple Orange 20
WaterMelon 42
JackFruit 51
Grapes mango Cherry 71
Or, eliminate the side effects by using a generator extension method
public static class IEnumerableExtensions
{
public static IEnumerable<IList<T>> MakeSets<T>(this IEnumerable<T> items, Func<T, T, bool> areInSameGroup)
{
var result = new List<T>();
foreach (var item in items)
{
if (!result.Any() || areInSameGroup(result[result.Count - 1], item))
{
result.Add(item);
continue;
}
yield return result;
result = new List<T> { item };
}
if (result.Any())
{
yield return result;
}
}
}
and your implementation becomes
var grouped = GetItems()
.OrderBy(x => x.OrderNo)
.MakeSets((prev, next) => next.OrderNo == prev.OrderNo + 1)
.Select(x => new FactoryOrder(
String.Join(" ", x.Select(y => y.Text).ToArray()),
x.First().OrderNo))
.ToList();
foreach (var item in grouped)
{
Console.WriteLine(item.Text + "\t" + item.OrderNo);
}
The output is the same but the code is easier to follow and maintain.
LINQ + sequential processing = Aggregate.
It's not said though that using Aggregate is always the best option. Sequential processing in a for(each) loop usually makes for better readable code (see Tim's answer). Anyway, here's a pure LINQ solution.
It loops through the orders and first collects them in a dictionary having the first Id of consecutive orders as Key, and a collection of orders as Value. Then it produces a result using string.Join:
Class:
class FactoryOrder
{
public FactoryOrder(int id, string name)
{
this.Id = id;
this.Name = name;
}
public int Id { get; set; }
public string Name { get; set; }
}
The program:
IEnumerable<FactoryOrder> orders =
new[]
{
new FactoryOrder(20, "Apple"),
new FactoryOrder(21, "Orange"),
new FactoryOrder(22, "Pear"),
new FactoryOrder(42, "WaterMelon"),
new FactoryOrder(51, "JackFruit"),
new FactoryOrder(71, "Grapes"),
new FactoryOrder(72, "Mango"),
new FactoryOrder(73, "Cherry"),
};
var result = orders.OrderBy(t => t.Id).Aggregate(new Dictionary<int, List<FactoryOrder>>(),
(dir, curr) =>
{
var prevId = dir.SelectMany(d => d.Value.Select(v => v.Id))
.OrderBy(i => i).DefaultIfEmpty(-1)
.LastOrDefault();
var newKey = dir.Select(d => d.Key).OrderBy(i => i).LastOrDefault();
if (prevId == -1 || curr.Id - prevId > 1)
{
newKey = curr.Id;
}
if (!dir.ContainsKey(newKey))
{
dir[newKey] = new List<FactoryOrder>();
}
dir[newKey].Add(curr);
return dir;
}, c => c)
.Select(t => new
{
t.Key,
Items = string.Join(" ", t.Value.Select(v => v.Name))
}).ToList();
As you see, it's not really straightforward what happens here, and chances are that it performs badly when there are "many" items, because the growing dictionary is accessed over and over again.
Which is a long-winded way to say: don't use Aggregate.
Just coded a method, it's compact and quite good in terms of performance :
static List<FactoryOrder> MergeValues(List<FactoryOrder> dirtyList)
{
FactoryOrder[] temp1 = dirtyList.ToArray();
int index = -1;
for (int i = 1; i < temp1.Length; i++)
{
if (temp1[i].OrderNo - temp1[i - 1].OrderNo != 1) { index = -1; continue; }
if(index == -1 ) index = dirtyList.IndexOf(temp1[i - 1]);
dirtyList[index].Text += " " + temp1[i].Text;
dirtyList.Remove(temp1[i]);
}
return dirtyList;
}

Error while removing item from collection

Getting error Collection was modified; enumeration operation may not execute.
var toUpdateItm = MC_CRM_T001A.ItemDetails
.Where(X => X.CatNo == SelectedCRM_T001A.CatNo);
foreach (var itm in toUpdateItm)
{
int x = MC_CRM_T001A.PartDetails.IndexOf(MC_CRM_T001A.PartDetails
.Where(X => X.cat_item_id == itm.id)
.FirstOrDefault()
);
if (x >= 0 && x!=null)
{
MC_CRM_T001A.PartDetails.RemoveAt(x);
}
}
foreach (var itm in toUpdateItm)
{
if (itm.CatNo == SelectedCRM_T001A.CatNo)
{
MC_CRM_T001A.ItemDetails.Remove(itm);
}
}
You can't modify the list you're looping over. Change for foreach calls to foreach (var itm in toUpdateItem.ToList()), which will create a copy of the list, instead.
Also, you can express this code more cleanly without all the IndexOf stuff:
var toUpdateItm = MC_CRM_T001A.ItemDetails.Where(X => X.CatNo == SelectedCRM_T001A.CatNo).ToList();
foreach (var itm in toUpdateItm.ToList())
{
var item = MC_CRM_T001A.PartDetails.FirstOrDefault(X => X.cat_item_id == itm.id);
if (item != null) { MC_CRM_T001A.PartDetails.Remove(item); }
if (itm.CatNo == SelectedCRM_T001A.CatNo) { MC_CRM_T001A.ItemDetails.Remove(itm);
}
You don't need two loops either.

Getting the sum of a column based on column name

I need to get the sum of columns based on the name of the column. Currently, I'm using an IF ELSE block to take care of it, but I'm hoping there is a more automatic method for getting this sort of thing done.
What works:
foreach (var day in bydates)
{
var bymile_bydate = bymile.Where(x => x.Date == day).ToList();
foreach (var r in results)
{
var name = r.name;
if (name.Equals("TotalIssues"))
{
r.data.Add(bymile_bydate.Sum(x => x.TotalIssues).Value);
}
else if (name.Equals("TotalCritical"))
{
r.data.Add(bymile_bydate.Sum(x => x.TotalCritical).Value);
}
}
}
How I'd like to get it working:
foreach (var day in bydates)
{
var bymile_bydate = bymile.Where(x => x.Date == day).ToList();
foreach (var r in results)
{
r.data.Add(bymile_bydate.Sum(x=> x.(r.name)).Value);
}
}
So anyway way of doing this?
C# doesn't have good support for accessing members whose names aren't known at compile time. What I'll often do in this situation is have a dictionary of names to delegates that return the properties:
// assuming your objects are of type ClassX and your properties are decimals
static Dictionary<string, Func<ClassX, decimal>> PropertyLookup =
new Dictionary<string, Func<ClassX, decimal>>
{ { "TotalIssues", x => x.TotalIssues },
{ "TotalCritical", x => x.TotalCritical },
};
foreach (var day in bydates)
{
var bymile_bydate = bymile.Where(x => x.Date == day).ToList();
foreach (var r in results)
{
var name = r.name;
r.data.Add(bymile_bydate.Sum(PropertyLookup[name]).Value);
}
}
If you don't want to have to define the property names ahead of time, you can use reflection to get the delegates. Here's an implementation that caches the delegates in a dictionary like the previous solution:
// assuming your objects are of type ClassX and your properties are decimals
static Dictionary<string, Func<ClassX, decimal>> PropertyLookup =
new Dictionary<string, Func<ClassX, decimal>>();
foreach (var day in bydates)
{
var bymile_bydate = bymile.Where(x => x.Date == day).ToList();
foreach (var r in results)
{
var name = r.name;
if (!PropertyLookup.ContainsKey(name))
PropertyLookup[name] = (Func<ClassX, decimal>)Delegate.CreateDelegate(
typeof(Func<ClassX, decimal>),
typeof(ClassX).GetProperty(name).GetGetMethod());
r.data.Add(bymile_bydate.Sum(PropertyLookup[name]).Value);
}
}
Without reflection, you can't refer to the property name by a string value. And reflection would be a poor choice here. I'd recommend creating a property on the class that contains your totals that does the following:
public int? RelevantTotal
{
get
{
switch (this.name)
{
case "TotalIssues":
return this.TotalIssues;
case "TotalCritical":
return this.TotalCritical;
default:
return 0;
}
}
}
The when you loop through the results, call that property instead:
foreach (var day in bydates)
{
var bymile_bydate = bymile.Where(x => x.Date == day).ToList();
foreach (var r in results)
{
r.data.Add(bymile_bydate.Sum(x => r.RelevantTotal).Value);
}
}

Categories

Resources