Determining value jumps in List<T> - c#

I have a class:
public class ShipmentInformation
{
public string OuterNo { get; set; }
public long Start { get; set; }
public long End { get; set; }
}
I have a List<ShipmentInformation> variable called Results.
I then do:
List<ShipmentInformation> FinalResults = new List<ShipmentInformation>();
var OuterNumbers = Results.GroupBy(x => x.OuterNo);
foreach(var item in OuterNumbers)
{
var orderedData = item.OrderBy(x => x.Start);
ShipmentInformation shipment = new ShipmentInformation();
shipment.OuterNo = item.Key;
shipment.Start = orderedData.First().Start;
shipment.End = orderedData.Last().End;
FinalResults.Add(shipment);
}
The issue I have now is that within each grouped item I have various ShipmentInformation but the Start number may not be sequential by x. x can be 300 or 200 based on a incoming parameter. To illustrate I could have
Start = 1, End = 300
Start = 301, End = 600
Start = 601, End = 900
Start = 1201, End = 1500
Start = 1501, End = 1800
Because I have this jump I cannot use the above loop to create an instance of ShipmentInformation and take the first and last item in orderedData to use their data to populate that instance.
I would like some way of identifying a jump by 300 or 200 and creating an instance of ShipmentInformation to add to FinalResults where the data is sequnetial.
Using the above example I would have 2 instances of ShipmentInformation with a Start of 1 and an End of 900 and another with a Start of 1201 and End of 1800

Try the following:
private static IEnumerable<ShipmentInformation> Compress(IEnumerable<ShipmentInformation> shipments)
{
var orderedData = shipments.OrderBy(s => s.OuterNo).ThenBy(s => s.Start);
using (var enumerator = orderedData.GetEnumerator())
{
ShipmentInformation compressed = null;
while (enumerator.MoveNext())
{
var current = enumerator.Current;
if (compressed == null)
{
compressed = current;
continue;
}
if (compressed.OuterNo != current.OuterNo || compressed.End < current.Start - 1)
{
yield return compressed;
compressed = current;
continue;
}
compressed.End = current.End;
}
if (compressed != null)
{
yield return compressed;
}
}
}
Useable like so:
var finalResults = Results.SelectMany(Compress).ToList();

If you want something that probably has terrible performance and is impossible to understand, but only uses out-of-the box LINQ, I think this might do it.
var orderedData = item.OrderBy(x => x.Start);
orderedData
.SelectMany(x =>
Enumerable
.Range(x.Start, 1 + x.End - x.Start)
.Select(n => new { time = n, info = x))
.Select((x, i) => new { index = i, time = x.time, info = x.info } )
.GroupBy(t => t.time - t.info)
.Select(g => new ShipmentInformation {
OuterNo = g.First().Key,
Start = g.First().Start(),
End = g.Last().End });
My brain hurts.
(Edit for clarity: this just replaces what goes inside your foreach loop. You can make it even more horrible by putting this inside a Select statement to replace the foreach loop, like in rich's answer.)

How about this?
List<ShipmentInfo> si = new List<ShipmentInfo>();
si.Add(new ShipmentInfo(orderedData.First()));
for (int index = 1; index < orderedData.Count(); ++index)
{
if (orderedData.ElementAt(index).Start ==
(si.ElementAt(si.Count() - 1).End + 1))
{
si[si.Count() - 1].End = orderedData.ElementAt(index).End;
}
else
{
si.Add(new ShipmentInfo(orderedData.ElementAt(index)));
}
}
FinalResults.AddRange(si);

Another LINQ solution would be to use the Except extension method.
EDIT: Rewritten in C#, includes composing the missing points back into Ranges:
class Program
{
static void Main(string[] args)
{
Range[] l_ranges = new Range[] {
new Range() { Start = 10, End = 19 },
new Range() { Start = 20, End = 29 },
new Range() { Start = 40, End = 49 },
new Range() { Start = 50, End = 59 }
};
var l_flattenedRanges =
from l_range in l_ranges
from l_point in Enumerable.Range(l_range.Start, 1 + l_range.End - l_range.Start)
select l_point;
var l_min = 0;
var l_max = l_flattenedRanges.Max();
var l_allPoints =
Enumerable.Range(l_min, 1 + l_max - l_min);
var l_missingPoints =
l_allPoints.Except(l_flattenedRanges);
var l_lastRange = new Range() { Start = l_missingPoints.Min(), End = l_missingPoints.Min() };
var l_missingRanges = new List<Range>();
l_missingPoints.ToList<int>().ForEach(delegate(int i)
{
if (i > l_lastRange.End + 1)
{
l_missingRanges.Add(l_lastRange);
l_lastRange = new Range() { Start = i, End = i };
}
else
{
l_lastRange.End = i;
}
});
l_missingRanges.Add(l_lastRange);
foreach (Range l_missingRange in l_missingRanges) {
Console.WriteLine("Start = " + l_missingRange.Start + " End = " + l_missingRange.End);
}
Console.ReadKey(true);
}
}
class Range
{
public int Start { get; set; }
public int End { get; set; }
}

Related

How to convert nested for loop with if condition to .Net Linq?

I am working on a function which will modify the input payload properties. The payload contains nodes and each node contains list of features. I need to remove some specific features match condition and also modify each node window property start and end time. I have written the function using traditional nested for loop, but struggling to convert it to Linq function. Anyone has idea how to convert this nested for loop function to a Linq function?
private void ApplyTransformation(InputPayload payload, int startTime = 8, int endTime = 15)
{
var nodes = payload.Nodes;
for (var i = 0; i < nodes.Count(); ++i)
{
var node = nodes[i];
var features = node.Features;
for (var j = 0; j < features.Count(); ++j)
{
var feature = features[j];
if (feature.NodeFeatureTypeID
== FeatureTypeEnum.FEATURE_A
|| feature.FeatureTypeID == FeatureTypeEnum.FEATURE_B
|| feature.FeatureTypeID == FeatureTypeEnum.FEATURE_C
|| feature.FeatureTypeID == FeatureTypeEnum.FEATURE_D
)
{
features.RemoveAt(j);
}
}
var windows = node.Windows;
for (var k = 0; k < windows.Count(); ++k)
{
var window = windows[k];
if (window.NodeFunctionTypeID == FeatureTypeEnum.MM_HOURS) continue;
window.StartHour = new TimeSpan(startTime, 0, 0);
window.EndHour = new TimeSpan(endTime, 0, 0);
}
}
}
Let's do it in parts. This first code of yours removes features that are in a list,
for (var j = 0; j < features.Count(); ++j)
{
var feature = features[j];
if (feature.NodeFeatureTypeID
== FeatureTypeEnum.FEATURE_A
|| feature.FeatureTypeID == FeatureTypeEnum.FEATURE_B
|| feature.FeatureTypeID == FeatureTypeEnum.FEATURE_C
|| feature.FeatureTypeID == FeatureTypeEnum.FEATURE_D
)
{
features.RemoveAt(j);
}
}
We need to convert that to "keep features not in a list"
var discard = new [] {FeatureTypeEnum.FEATURE_A, FeatureTypeEnum.FEATURE_B, FeatureTypeEnum.FEATURE_C, FeatureTypeEnum.FEATURE_D };
node.Features = node.Features.Where(f => !discard.Contains(f)).ToArray();
This part of your code skips if function type is a certain kind, and zeroes timespans if the function type is not a certain kind:
if (window.NodeFunctionTypeID == FeatureTypeEnum.MM_HOURS) continue;
window.StartHour = new TimeSpan(startTime, 0, 0);
window.EndHour = new TimeSpan(endTime, 0, 0);
Which might be better as a loop that operates on just those things we are interested in (we want to modify only those Windows where the nodefuctionTypeId is not Hours):
foreach(var window in node.Windows.Where(w => w.NodeFunctionTypeID != FeatureTypeEnum.MM_HOURS){
window.StartHour = new TimeSpan.FromHous(startTime);
window.EndHour = new TimeSpan.FromHours(endTime);
}
Meaning the whole is:
private void ApplyTransformation(InputPayload payload, int startTime = 8, int endTime = 15)
{
var discard = new [] {FeatureTypeEnum.FEATURE_A, FeatureTypeEnum.FEATURE_B, FeatureTypeEnum.FEATURE_C, FeatureTypeEnum.FEATURE_D };
foreach (var node in payload.Nodes)
{
node.Features = node.Features.Where(f => !discard.Contains(f)).ToArray();
foreach(var window in node.Windows.Where(w => w.NodeFunctionTypeID != FeatureTypeEnum.MM_HOURS){
window.StartHour = TimeSpan.FromHous(startTime);
window.EndHour = TimeSpan.FromHours(endTime);
}
}
}
I don't think I'd convert it all to a Linq form, as it would make a mess; linq queries should not have side effects (modify the objects the query iterates over) so particularly the second part where the time spans are being zeroed would have to become an operation where everything about the window was being copied over to a new Window if you wanted to LINQ it.
If a window is literally just a time span pair then it's not so bad, or if you want to provide a constructor that takes an existing Window and a startTime and endTime:
public Window(Window copyFrom, int startTime, int endTime){
this.X = copyFrom.X;
this.Y = copyFrom.Y;
...
this.StartHour = TimeSpan.FromHours(strtTime);
this.EndHour = TimeSpan.FromHours(endTime);
}
Then maybe your method could become some linq:
foreach (var node in payload.Nodes)
{
node.Features = node.Features.Where(f => !discard.Contains(f)).ToArray();
node.Windows = node.Windows.Select(w => w.NodeFunctionTypeID == FeatureTypeEnum.MM_HOURS ? w : new Window(w, startTime, endTime).ToArray();
}
..but I don't know if I would try for a wholesale replacement of the entire nodes list, for reasons even based on the name of the method: ApplyTransformations sounds like it means "take this list of nodes and change bits of them" not "take this list of nodes and give me a new list with some or all of the nodes replaced or modified" - that sort of behavior in code could wreck something else, if the calling code is expecting a tweak and the object it sends (or objects within it) are swapped out for new ones
Using a linq query for the second part would make things messier, better to just have a for loop
Something like this should work:
var nodes = payload.Nodes;
nodes.Features = nodes.Features.Where(f => !(
f.NodeFeatureTypeID == FeatureTypeEnum.FEATURE_A
|| feature.FeatureTypeID == FeatureTypeEnum.FEATURE_B
|| feature.FeatureTypeID == FeatureTypeEnum.FEATURE_C
|| feature.FeatureTypeID == FeatureTypeEnum.FEATURE_D
)
);
foreach(var window in nodes.Windows)
{
if (window.NodeFunctionTypeID != FeatureTypeEnum.MM_HOURS)
{
window.StartHour = new TimeSpan(startTime, 0, 0);
window.EndHour = new TimeSpan(endTime, 0, 0);
}
}
Pls have look at below code snippet to avoid nested loop using linq.
public class Employee
{
public string Name { get; set; }
public List<EmployeeProject> EmployeeProject { get; set; }
}
public class EmployeeProject
{
public string ProjectName { get; set; }
public string ClientName { get; set; }
}
/// <summary>
/// Object mapping
/// </summary>
/// <returns></returns>
public static List<Employee> CreateObject()
{
var employeeList = new List<Employee>();
var employee = new Employee();
var employeeProjectList = new List<EmployeeProject>();
employee.Name = "John";
var employeeProject = new EmployeeProject();
employeeProject.ProjectName = "Chrome";
employeeProject.ClientName = "Google";
employeeProjectList.Add(employeeProject);
employeeProject = new EmployeeProject();
employeeProject.ProjectName = "WhatsApp";
employeeProject.ClientName = "Meta";
employeeProjectList.Add(employeeProject);
employee.EmployeeProject = employeeProjectList;
employeeList.Add(employee);
employee.Name = "Alex";
employeeProjectList = new List<EmployeeProject>();
employeeProject = new EmployeeProject();
employeeProject.ProjectName = "Chrome2";
employeeProject.ClientName = "Google2";
employeeProjectList.Add(employeeProject);
employeeProject = new EmployeeProject();
employeeProject.ProjectName = "WhatsApp2";
employeeProject.ClientName = "Meta2";
employeeProjectList.Add(employeeProject);
employee.EmployeeProject = employeeProjectList;
employeeList.Add(employee);
return employeeList;
}
/// <summary>
/// Linq function
/// </summary>
public static void LinqFunctionForNestedQuery()
{
var employeeObject = CreateObject();
var result1 = employeeObject.Select(x =>
{
x.EmployeeProject = x.EmployeeProject.Select(y =>
{
y.ProjectName.Contains("Chrome");
return y;
}).ToList();
return x;
});
}
To maintain readability of the code and to make it simpler , I have modified the code.
Step1 : Replace for with foreach if you can
Step2 : Replace foreach with linq
Important tip. Resharper helps you with code suggestions.
Solution
private void ApplyTransformation(InputPayload payload, int startTime = 8, int endTime = 15)
{
foreach (var node in payload.Nodes)
{
var features = node.Features;
foreach (var feature in features.Where(feature => feature.NodeFeatureTypeID == FeatureTypeEnum.FEATURE_A ||
feature.FeatureTypeID == FeatureTypeEnum.FEATURE_B ||
feature.FeatureTypeID == FeatureTypeEnum.FEATURE_C ||
feature.FeatureTypeID == FeatureTypeEnum.FEATURE_D))
{
features.Remove(feature);
}
var windows = node.Windows;
foreach (var window in windows
.Where(window => window.NodeFunctionTypeID != FeatureTypeEnum.MM_HOURS))
{
window.StartHour = new TimeSpan(startTime, 0, 0);
window.EndHour = new TimeSpan(endTime, 0, 0);
}
}
}

Merge interval with merge distance in C#

Given a list of intervals [start, end] I have to merge them based on a specified merge distance. The intervals are arriving in a particular order, and as they arrive it should merge them according to the specified merge distance as each interval is received. Some of these intervals will be removed (in the arrival stream they will be marked as removed) – in that situation I've to treat the original interval as if it never existed. Example:
Merge distance is 7 – in the following example the input is arriving in order
I've tried the following algorithm to merge them but my Output isn't coming like the above example. Can somebody assists me, what I'm missing here!
Here is my code.
class Program
{
static void Main(string[] args)
{
var intervals = new List<Interval>
{
new Interval
{
start = 1,
end = 20
},
new Interval
{
start = 55,
end = 58
},
new Interval
{
start = 60,
end = 89
},
new Interval
{
start = 15,
end = 31
},
new Interval
{
start = 10,
end = 15
},
new Interval
{
start = 1,
end = 20
}
};
var mergedIntervals = Merge(intervals, 7);
foreach (var item in mergedIntervals)
{
Console.WriteLine($"[{item.start}, {item.end}]");
}
Console.ReadKey();
}
public static List<Interval> Merge(List<Interval> intervals, int mergeDistance)
{
var result = new List<Interval>();
for (int i = 0; i < intervals.Count; i++)
{
var newInterval = new Interval(intervals[i].start, intervals[i].end);
//while (i < intervals.Count - 1 && newInterval.end >= intervals[i + 1].start)
while (i < intervals.Count - 1 && newInterval.end <= mergeDistance) // intervals[i + 1].start)
{
newInterval.end = Math.Max(newInterval.end, intervals[i + 1].end);
i++;
}
result.Add(newInterval);
}
return result;
}
}
public class Interval
{
public int start { get; set; }
public int end { get; set; }
public Interval()
{
}
public Interval(int start, int end)
{
this.start = start;
this.end = end;
}
}
Can you please try this code, working demo code here
class Program
{
static void Main(string[] args)
{
var intervals = new List<Interval>
{
new Interval
{
start = 1,
end = 20,
isAdded = true
},
new Interval
{
start = 55,
end = 58,
isAdded = true
},
new Interval
{
start = 60,
end = 89,
isAdded = true
},
new Interval
{
start = 15,
end = 31,
isAdded = true
},
new Interval
{
start = 10,
end = 15,
isAdded = true
},
new Interval
{
start = 1,
end = 20,
isAdded = false
}
};
var mergedIntervals = Merge(intervals, 7);
foreach (var item in mergedIntervals)
{
Console.WriteLine($"[{item.start}, {item.end}]");
}
Console.ReadKey();
}
public static List<Interval> Merge(List<Interval> intervals, int mergeDistance)
{
var result = new List<IntervalGroup>();
var group = new IntervalGroup();
foreach (var item in intervals)
{
group = result.Where(c => c.Groups.Any(g =>
Math.Abs(g.end - item.start) <= mergeDistance ||
Math.Abs(g.end - item.end) <= mergeDistance)).FirstOrDefault();
if (group != null && item.isAdded)
{
group.Groups.Add(item);
}
else if(item.isAdded)
{
group = new IntervalGroup();
group.Groups = new List<Interval>();
result.Add(group);
group.Groups.Add(item);
}
else if(item.isAdded == false)
{
group.Groups.Remove(group.Groups.Where(c => c.start == item.start && c.end == item.end).First());
}
}
var finalResult = result.Select(s => new Interval { start = s.Groups.Min(min => min.start), end = s.Groups.Max(min => min.end) });
return finalResult.ToList();
}
}
public class Interval
{
public int start { get; set; }
public int end { get; set; }
public bool isAdded { get; set; }
public Interval()
{
}
public Interval(int start, int end, bool isAdded)
{
this.start = start;
this.end = end;
this.isAdded = isAdded;
}
}
public class IntervalGroup
{
public List<Interval> Groups { get; set; }
}
When a problem is beyond your grasp, it is very helpful to break it up into smaller pieces and code the bits that you do understand. Here's how I break it down, step by step.
First, I think it would be convenient to be able to check the length of an interval, so let's add a property.
class Interval
{
/* Prior code */
public int Length => this.end - this.start;
Now let's write a method that merges two intervals:
class Interval
{
/* Prior code */
static public Interval Merge(Interval a, Interval b )
{
return new Interval(Math.Min(a.start, b.start), Math.Max(a.end, b.end));
}
Now we need to write the code that decides if two intervals are capable of being merged. The prototype could look like this:
static public bool CanMerge(Interval a, Interval b, int mergeDistance)
What logic do we need inside? Well, we could check for overlaps and check the merge distance from both ends, but I know a shortcut. Given a merge A + B = C, the merge is allowed if and only if the length of C is less than or equal to the sum of A + B + the merge distance. So we can write this:
class Interval
{
/* Prior code */
static public bool CanMerge(Interval a, Interval b, int mergeDistance)
{
var merged = Merge(a,b);
var canMerge = merged.Length <= a.Length + b.Length + mergeDistance;
return canMerge;
}
From there you can add to a list by checking for mergeable items. Note that recursion is required because the act of merging an interval could result in another interval becoming mergeable.
void AddToList(List<Interval> list, Interval newInterval, int mergeDistance)
{
var target = list.FirstOrDefault( x => Interval.CanMerge(x, newInterval, mergeDistance) );
if (target == null)
{
list.Add(newInterval);
return;
}
list.Remove(target);
AddToList(list, Interval.Merge(target, newInterval), mergeDistance);
}

Filter products with ElasticSearch concat a lot of filter

I've been trying to filter products with Elasticsearch for a few hours, unfortunately to no avail.
I need to find products that belong to certain categories and at the same time have selected several brands and one size.
Help :(
json screen
querycontainer build method
private QueryContainer CreateOrQueryFromFilter(QueryContainer queryContainer, SortedSet<string> filter, string fieldName)
{
if (filter != null && filter.Count > 0)
{
foreach (var item in filter)
{
queryContainer |= new TermQuery()
{
Name = fieldName + "named_query",
Boost = 1.1,
Field = fieldName,
Value = item
};
}
}
return queryContainer;
}
and search method
public ResultModel SearchRequest(RequestModel r)
{
string key = string.Format("search-{0}-{1}", r.CacheKey + "-" + ProductCatalog.Model.Extension.StringHelper.UrlFriendly(r.SearchText), r.Prefix);
node = new Uri("http://xxxx:9200/");
settings = new ConnectionSettings(node);
settings.DisableDirectStreaming();
settings.DefaultIndex("products");
client = new ElasticClient(settings);
// return AppCache.Get(key, () =>
// {
DateTime start = DateTime.Now;
ResultModel result = new ResultModel(r.Canonical, r.RouteObject);
if (!string.IsNullOrEmpty(r.Prefix))
{
result.Prefix = r.Prefix;
}
QueryContainer c = new QueryContainer();
if (r.CategoryFilterChilds != null && r.CategoryFilterChilds.Count > 0)
{
var a1 = new SortedSet<string>(r.CategoryFilterChilds.Select(a => (string)a.ToString()));
c = CreateOrQueryFromFilter(c, a1, "categories");
}
else
{
if (r.CategoryFilterRoots != null && r.CategoryFilterRoots.Count > 0)
{
var a1 = new SortedSet<string>(r.CategoryFilterRoots.Select(a => (string)a.ToString()));
c = CreateOrQueryFromFilter(c, a1, "categories");
}
else
{
// null
}
}
var filters = new AggregationDictionary();
if (r.IsBrandFilter)
{
c = CreateOrQueryFromFilter(c, r.SelectedBrands, "brands");
}
if (r.IsColorFilter)
{
c = CreateOrQueryFromFilter(c, r.SelectedBrands, "colors");
}
int skip = (r.Page * r.PageSize) - r.PageSize;
ISearchRequest r2 = new SearchRequest("products");
r2.From = 1;
r2.Size = r.PageSize;
r2.Query = c;
string[] Fields = new[] { "brands", "shopId" };
AggregationBase aggregations = null;
foreach (string sField in Fields)
{
var termsAggregation = new TermsAggregation("agg_" + sField)
{
Field = sField,
Size = 120,
Order = new List<TermsOrder> { TermsOrder.TermDescending }
};
if (aggregations == null)
{
aggregations = termsAggregation;
}
else
{
aggregations &= termsAggregation;
}
}
r2.Aggregations = aggregations;
var c2 = client.Search<ShopProductElastic>(r2);
var ShopsBuf = (Nest.BucketAggregate)(c2.Aggregations["agg_brands"]);
var ShopsCount = ShopsBuf.Items.Count();
var results = c2;
result.BrandsRequest = new SortedDictionary<string, int>();
foreach (Nest.KeyedBucket<object> item in ShopsBuf.Items)
{
result.BrandsRequest.Add((string)item.Key, (int)(item.DocCount ?? 0));
}
result.CategorySelected = r.CategoryCurrent;
result.TotalCount = 10;
var costam = results.Documents.ToList();
var targetInstance = Mapper.Map<List<Products>>(costam);
result.Products = targetInstance;
result.Page = r.Page;
result.PageSize = r.PageSize;
result.IsBrandFilter = r.IsBrandFilter;
result.IsColorFilter = r.IsColorFilter;
result.IsPatternFilter = r.IsPatternFilter;
result.IsSeasonFilter = r.IsSeasonFilter;
result.IsShopFilter = r.IsShopFilter;
result.IsSizeFilter = r.IsSizeFilter;
result.IsStyleFilter = r.IsStyleFilter;
result.IsTextileFilter = r.IsTextileFilter;
DateTime stop = DateTime.Now;
result.SearchTime = stop - start;
result.SearchText = r.SearchText;
return result;
// }, TimeSpan.FromHours(8));
}
I have all products that have a given brand or categories. However, i need all products of a selected brand in selected categories

C# - Merge list items into one item based on some matching values

I have a list of items (List<Tasks> tasks), like this one:
Id Action Source Target
------- --------- -------- ---------
1 Save 12 18
4 Save 18 21
7 Save 21 23
6 Save 23 25
10 Save 25 27
16 Save 29 31
0 Edit 31 37
What I want to do, is to merge the rows that have the same (Source and Target) and the same Action. For example, what I need at the end should look like this:
Id Action Source Target
------- --------- -------- ---------
22 Save 12 27
16 Save 29 31
0 Edit 31 37
Which means, all the items that have the same Action(in my case here Save) should be merged to one row/item but only in case the Target value of the upper item is equal to the Source value of the follower item. A follower is the lower item.
For example, upper is item Id = 1 and lower/follower is item Id = 4. So the record that follows.
Is there any way to do this with linq avoiding the too many foreach loops?
Maybe something like Hierarchies with CTE in SQL. But I'm still not finding the correct syntax, so that's why I didn't paste any code.
Thanks in advance!
If you want "LINQ" solution, you can use Aggregate like this:
var result = tasks.Aggregate(new List<Item>(), (acc, current) =>
{
if (acc.Count > 0)
{
var prev = acc[acc.Count - 1];
if (prev.Action == current.Action && prev.Target == current.Source)
{
// update previous target
prev.Target = current.Target;
}
// otherwise just add
else acc.Add(current);
}
else acc.Add(current);
return acc;
});
It starts with empty List as accumulator, and feeds items one by one. Then we just add items to accumulator if they do not match criteria, and if they do match - we update previous item instead.
Take a look at MoreLinq. There is a function named Segment which splits the sequence into subsequences based on some condition:
var grouped = tasks
.GroupBy(t => t.Action, (k, g) => g
.Segment((s, f, a) => s.Source != f.Target)
.Select(c => new
{
c.First().Source,
c.Last().Target,
Action = k
})));
So the sequence is divided and a new subsequence is created on each adjacent pair when s.Source != f.Target (f is first element and s is second in a pair).
Something like a CTE query. First select seed nodes (no records pointing to the SOURCE) then in the do-wile loop change Targets to get the end target of each chain. No previous order is required.
public class Tasks
{
public int Id;
public string Action;
public int Source;
public int Target;
}
static void Main(string[] args)
{
List<Tasks> tasks = new List<Tasks>{
new Tasks{Id=1,Action="Save",Source= 12,Target=18},
new Tasks{Id=4,Action="Save",Source= 18,Target=21},
new Tasks{Id=7,Action="Save",Source= 21,Target=23},
new Tasks{Id=6,Action="Save",Source= 23,Target=25},
new Tasks{Id=10,Action="Save",Source= 25,Target=27},
new Tasks{Id=16,Action="Save",Source= 29,Target=31},
new Tasks{Id=0,Action="Edit",Source= 31,Target=37},
};
var collectTasks = (from t in tasks
where !tasks.Any(t1 => (t1.Target == t.Source)&&(t1.Action == t.Action)&&(t1.Id!=t.Id))
select t).ToList();
foreach (var ct in collectTasks)
{
do{
var t1 = from t in tasks where ((ct.Target == t.Source)&&(ct.Action == t.Action)&&(ct.Id!=t.Id)) select t;
if (t1.Count() == 0) { break; }
ct.Target = t1.First().Target;
} while (true);
}
foreach (var t in collectTasks)
{
Console.WriteLine("Action = {0}, Source = {1}, Target = {2}", t.Action, t.Source, t.Target);
}
}
You need just one loop using index to access list items, starting from 1 and combining current item with previous if their Action match and Target of previous item is equal to Source of current:
for (int i = 1; i < items.Count; i++)
if (items[i].Action == items[i - 1].Action && items[i].Source == items[i - 1].Target)
{
items[i - 1].Target = items[i].Target;
items.RemoveAt(i);
i--; // to have same index after i++
}
That would not change Id, so it will be 1 and not 22 as you wrote.
Try following :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication33
{
class Program
{
static void Main(string[] args)
{
List<Task> tasks = new List<Task>() {
new Task() { Id = 1, Action = "Save", Source = 12, Target = 18},
new Task() { Id = 4, Action = "Save", Source = 18, Target = 21},
new Task() { Id = 7, Action = "Save", Source = 21, Target = 23},
new Task() { Id = 6, Action = "Save", Source = 23, Target = 25},
new Task() { Id = 10, Action = "Save", Source = 25, Target = 27},
new Task() { Id = 16, Action = "Save", Source = 29, Target = 31},
new Task() { Id = 0, Action = "Edit", Source = 31, Target = 37}
};
for(int i = tasks.Count - 1; i >= 0; i--)
{
int source = tasks[i].Source;
List<int> match = tasks.Select((x, index) => new { x = x, i = index }).Where(x => (x.x.Target == source) && (tasks[i].Action == tasks[x.i].Action)).Select(x => x.i).ToList();
if (match.Count > 0)
{
tasks[match[0]].Target = tasks[i].Target;
tasks.RemoveAt(i);
}
}
}
}
public class Task
{
public int Id { get; set; }
public string Action { get; set; }
public int Source { get; set; }
public int Target { get; set; }
}
}
This procedure will do what you need in one-pass, preserving the "Id" values of items that can't be merged.
You'll see it works on an ordered list so strictly speaking there is some LINQ-hidden activity there; but generally speaking you'll find that LINQ queries incur an overhead which make then always slower than for-loops over arrays.
private List<Task> Merge(List<Task> list)
{
var result = new List<Task>();
var maxId = list.Max(x => x.Id) + 1;
// order list for this algo to work
var listO = list.OrderByDescending(x => x.Action).ThenBy(x => x.Source).ToArray();
// create seed and counter
Task seed = listO[0];
var ctr = 0;
for (var i = 0; i < listO.Length - 1; i++)
{
if (listO[i + 1].Source == listO[i].Target && listO[i + 1].Action == listO[i].Action && listO[i + 1].Action == seed.Action)
{
// if the next is the last, merge it now
if (i + 1 == listO.Length - 1)
result.Add(new Task() { Id = maxId++, Action = seed.Action, Source = seed.Source, Target = listO[i].Target });
// the next item qualifies for merge, move to next
ctr++;
continue;
}
// next item does not qualify for merge, merge what we have or just add the item if ctr == 0
result.Add(ctr == 0 ? seed : new Task() { Id = maxId++, Action = seed.Action, Source = seed.Source, Target = listO[i].Target });
// reset seed record + counter
seed = listO[i+1];
ctr = 0;
// if the next item is the last, it belongs in the list as is
if (i + 1 == listO.Length - 1) result.Add(seed);
}
return result;
}

How to find the next element in a generic List?

This is my Generic List:
public class TagType
{
public string FieldTag;
public int Position;
}
List<TagType<dynamic>> TagList = new List<TagType<dynamic>>();
TagList.Add(new TagType<dynamic>() { FieldTag = "ID", Position = posIdStr });
TagList.Add(new TagType<dynamic>() { FieldTag = "PT", Position = posPtStr });
TagList.Add(new TagType<dynamic>() { FieldTag = "ID", Position = posIdStr });
TagList.Add(new TagType<dynamic>() { FieldTag = "EC", Position = posECStr });
I am trying to get Position value for the FieldTag that comes after (for eg: PT).
How do I do this?
You find the index of PT and add 1? (but remember to check that the index + 1 < the length of the List)
// Find the index of PT
int ix = TagList.FindIndex(x => x.FieldTag == "PT");
// index found
if (ix != -1)
{
// Check that index + 1 < the length of the List
if (ix + 1 < TagList.Count)
{
var position = TagList[ix + 1]; // Add 1
}
}
Unfortunately each time you do a search you will have to iterate over the list, find the field tag you are looking for, then go to the next element and get the position value. e..: An O(n) for lookup solution:
private static object SearchPosition(List<TagType<object>> tagList, string fieldTag)
{
var i = tagList.FindIndex(x => x.FieldTag == "PT");
if (i >= 0 && i < tagList.Count)
{
return tagList[i + 1].Position;
}
}
And test:
[Test]
public void FieldTagTest()
{
var res = SearchPosition(_tagList, "PT");
res.ToString().Should().Be("ID2");
}
If your list does not change often, you should build a Dictionary<string,int> with the FieldTag as Keyand the list index position as value. Ofcourse each time you modify the list you would need to build this index again.
An O(1) solution is:
private static object SearchPositionUsingIndex(List<TagType<object>> tagList, string fieldTag)
{
// You would save this index, and build it only once,
// or rebuild it whenver something changes.
// you could implement custom index modifications.
var index = BuildIndex(tagList);
int i;
if (!index.TryGetValue(fieldTag, out i)) return null;
if (i + 1 >= tagList.Count) return null;
return tagList[i + 1].Position;
}
private static Dictionary<string, int> BuildIndex(List<TagType<object>> tagList)
{
var index = new Dictionary<string, int>();
for (int i = 0; i < tagList.Count; i++)
{
var tag = tagList[i];
if (!index.ContainsKey(tag.FieldTag)) index.Add(tag.FieldTag, i);
}
return index;
}
And test:
[Test]
public void FieldTagTestUsingIndex()
{
var res = SearchPositionUsingIndex(_tagList, "PT");
res.ToString().Should().Be("ID2");
}
Or you could use a 1 line LINQ method, which is also O(n):
[Test]
public void FieldTagTestLinq()
{
var res = SearchUsingLinq();
res.ToString().Should().Be("ID2");
}
private object SearchUsingLinq()
{
var p = _tagList.SkipWhile(x => x.FieldTag != "PT").Skip(1).FirstOrDefault();
return p != null ? p.Position : null;
}
TestSetup
public class SO29047477
{
private List<TagType<object>> _tagList;
[SetUp]
public void TestSetup()
{
_tagList = new List<TagType<dynamic>>();
_tagList.Add(new TagType<dynamic>() { FieldTag = "ID", Position = "ID1"});
_tagList.Add(new TagType<dynamic>() { FieldTag = "PT", Position = "PT1" });
_tagList.Add(new TagType<dynamic>() { FieldTag = "ID", Position = "ID2" });
_tagList.Add(new TagType<dynamic>() { FieldTag = "EC", Position = "EC1" });
}
}
If you want to get next element's Position after each item with FieldTag PT, then you can solve it in one or two lines with LINQ:
var resultTag = TagList.SkipWhile(x => x.FieldTag != "PT").Skip(1).FirstOrDefault();
var resultPosition = resultTag == null ? 0 : resultTag.Position;
Additional:
If you want to cast it to int then just cast it explicitly.
var resultTag = TagList.SkipWhile(x => x.FieldTag != "PT").Skip(1).FirstOrDefault();
int resultPosition = resultTag == null ? 0 : (int)resultTag.Position;
public Position? GetNextPosition(string FieldTagVal)
{
bool returnNext = false;
foreach(TagType t in TagList)
{
if (returnNext) return t.Position;
if (t.FieldTag == FieldTagVal) returnNext = true;
}
return null;
}

Categories