Getting inappropriate rank while merging and arranging 2 list - c#

Below is my models:
public class Test
{
public int TestId { get; set; }
public List<VariantsRank> VariantsRanks { get; set; }
}
public class VariantsRank
{
public int VariantId { get; set; }
public string Name { get; set; }
public int Rank { get; set; }
}
I have an existing Test instance which contains the following values for VariantsRanks
VariantId = 10, Name = "V1", Rank = 0
VariantId = 11, Name = "V2", Rank = 1
I then need to merge the following VariantsRank
VariantId = 12, Name = "V3", Rank = 0
VariantId = 13, Name = "V4", Rank = 1
and increment the Rank to produce the following output
VariantId = 10, Name = "V1", Rank = 0
VariantId = 11, Name = "V2", Rank = 1
VariantId = 12, Name = "V3", Rank = 2
VariantId = 13, Name = "V4", Rank = 3
and I use the following code which works correctly (List1 is the original list, and List2 is the list to be merged)
int highestOrder = (List1.VariantsRanks.Max(cpo => cpo.Rank)) + 1;
foreach (var rank in List2.VariantsRanks)
{
var match = List1.VariantsRanks.FirstOrDefault(x => x.VariantId == rank.VariantId);
if (match != null) // found
{
match.Rank = rank.Rank;
}
else
{
rank.Rank = highestOrder;
highestOrder = highestOrder + 1;
List1.VariantsRanks.Add(rank);
}
}
I then need to merge the following VariantsRank to the new list (note the matching VariantId values, but they are in reverse order)
VariantId = 13, Name = "V4", Rank = 0
VariantId = 12, Name = "V3", Rank = 1
so that the output should be
VariantId = 10, Name = "V1", Rank = 0
VariantId = 11, Name = "V2", Rank = 1
VariantId = 13, Name = "V4", Rank = 2
VariantId = 12, Name = "V3", Rank = 3
however the above code instead outputs
VariantId = 10, Name = "V1", Rank = 0
VariantId = 11, Name = "V2", Rank = 1
VariantId = 12, Name = "V3", Rank = 1
VariantId = 13, Name = "V4", Rank = 0
and the Rank values are not incremented correctly
How do I modify the code to ensure that duplicate VariantId are not added, but increment the Rank?

You issue is that in the 2 merge, your adding items with values that match the VariantId in the existing list. This means that you hit the code in the if block, which resets the values of the existing items to the value of the Rank in the model your posting.
For example, in the first iteration of your loop, match is the existing item with VariantID = 13 and your set its Rank to equal the value of rank.Rank which is 0.
You need to first remove any matches from your existing list, and then iterate through the posted values, update their Rank and add to the collection.
You code should be
// Get the VariantId values of the list to be merged
var ids = List2.VariantsRanks.Select(x => x.VariantId);
// Remove any matches from the existing list
List1.VariantsRanks.RemoveAll(x => ids.Contains(x.VariantId));
// Calculate the current highest rank
int highestOrder = (List1.VariantsRanks.Max(x => x.Rank));
foreach (var rank in List2.VariantsRanks)
{
// Update the rank
rank.Rank = ++highestOrder; // pre-increment
// Add to the existing list
List1.VariantsRanks.Add(rank);
}
Based on comments in chat that the the second list might contain items that need to be inserted in the middle of the first list, then the code would need to be
// Get the VariantId's of the first and last items in the list to be merged
var firstID = List2.VariantsRanks.First().VariantId;
var lastID = List2.VariantsRanks.Last().VariantId;
// Get the indexers of those items in the original list
var firstIndex = List1.VariantsRanks.FindIndex(x => x.VariantId == firstID);
var lastIndex = List1.VariantsRanks.FindIndex(x => x.VariantId == lastID);
if (firstIndex > lastIndex) // in case they are in descending order
{
var temp = lastIndex;
lastIndex = firstIndex;
firstIndex = temp;
}
// Remove matches from the original list
for (int i = firstIndex; i < lastIndex + 1; i++)
{
List1.VariantsRanks.RemoveAt(firstIndex);
}
// Inset the items from the list to be merged
for(int i = 0; i < List2.VariantsRanks.Count; i++)
{
List1.VariantsRanks.Insert(firstIndex + i, List2.VariantsRanks[i]);
}
/ Re-number the Rank
for(int i = 0; i < List1.VariantsRanks.Count; i++)
{
List1.VariantsRanks[i].Rank = i;
}
Note, the above will only work if the values of VariantId in the merged list are consecutive (in either ascending or descending order)

Related

linq order by based on multiple columns and various crietria

I have data arranged in this order in sql.
Now, I want to order this list with both QuestionDataTypeId and DisplayOrder, but want this QuestionDataTypeId = 0 at last. so finally result will be like first row = 6 then 7 then 8 and then 1 to 5.
I want to achieve this with C# and linq.
What I have tried so far?
here is my code but it's not working.
var data = entity
.OrderByDescending(m => m.QuestionDataTypeId == 0)
.ThenBy(m => m.QuestionDataTypeId)
.ThenBy(m => m.DisplayOrder);
I have fixed this with merging 2 different variables sorted separately for QuestionDataTypeId = 0 and all other QuestionDataTypeId, but just want to know what will be the proper linq for this case in single line.
any help would be really appreciated. thanks!
Try replace QuestionDataTypeId where value = 0
.OrderBy(x=>x.QuestionDataTypeId==0?int.MaxValue:x.QuestionDataTypeId)
.ThenBy(t=>t.DisplayOrder)
You can write your own comparer for OrderBy
Sample data structure:
public record Table
{
public Table(int qdtId, int displayOrder, string text)
{
QuestionDataTypeId = qdtId;
DisplayOrder = displayOrder;
Text = text;
}
public int QuestionDataTypeId { get; set; }
public int DisplayOrder { get; set; }
public string Text { get; set; }
}
public class TableComparer : IComparer<Table>
{
public int Compare(Table? x, Table? y)
{
if(x.QuestionDataTypeId!= 0 && y.QuestionDataTypeId!=0 || x.QuestionDataTypeId == 0 && y.QuestionDataTypeId == 0)
{
return y.QuestionDataTypeId.CompareTo(x.QuestionDataTypeId);
}
return x.QuestionDataTypeId == 0 && y.QuestionDataTypeId != 0 ? int.MinValue : int.MaxValue;
}
}
Then in the code:
List<StringConcatBug.Table> list = new()
{
new(0, 1, "Comfortable"),
new(0, 2,"attainable"),
new(0, 3,"recent goal"),
new(0, 4,"comfortable"),
new(2, 2,"Last Name"),
new(3, 3,"Email"),
new(0, 5, "feeling"),
new(1, 1, "First Name"),
};
var ordered = list.OrderByDescending(t=>t,new TableComparer());
foreach(var v in ordered) { Console.WriteLine(v);}
Output
Table { QuestionDataTypeId = 1, DisplayOrder = 1, Text = First Name }
Table { QuestionDataTypeId = 2, DisplayOrder = 2, Text = Last Name }
Table { QuestionDataTypeId = 3, DisplayOrder = 3, Text = Email }
Table { QuestionDataTypeId = 0, DisplayOrder = 1, Text = Comfortable }
Table { QuestionDataTypeId = 0, DisplayOrder = 2, Text = attainable }
Table { QuestionDataTypeId = 0, DisplayOrder = 3, Text = recent goal }
Table { QuestionDataTypeId = 0, DisplayOrder = 4, Text = comfortable }
Table { QuestionDataTypeId = 0, DisplayOrder = 5, Text = feeling }
I usually use this algorithm
var mult=100000; // You can select a different number depending how many records of
// the same type you expecting; Numbers should not overlap
var data = entity
.OrderBy(m => (m.QuestionDataTypeId*mult + m.DisplayOrder))
.....

How to programmatically determine tab index of fields on a form based on their X,Y coordinates?

I need to figure out the proper tabbing order of HTML form fields based on their absolute X, Y coordinates on the page. We use X, Y from the bottom-left corner of the div (page container) in which they are placed.
For example, in the image below, the numbers inside the boxes indicate the final tabIndex order I would expect as the result should the boxes overlap at all on the Y axis; the lowest X axis value would win and the Y axis wouldn't matter at all. If there's no overlap, then the highest Y axis value wins.
Context
Basically when filling out a PDF form, the natural tab index should be left to right even if box #3 is a little higher than box #2; you'd still want to fill it out left to right. However, since box #1 is on a completely different X plane than the other boxes (regardless if it's further right than the rest) it should still logically come before the other boxes when filling out a form. You wouldn't go across and then up.
The fields are in a C# object with X and Y properties. (pseudocode below)
var fields = new List<TestFieldModel>()
{
new TestFieldModel()
{
ExpectedOrderNumberResult = 3,
PageNumber = 1,
X = 7,
Y = 6,
Width = 5,
Height = 2
},
new TestFieldModel()
{
ExpectedOrderNumberResult = 4,
PageNumber = 1,
X = 14,
Y = 4,
Width = 5,
Height = 3
},
new TestFieldModel()
{
ExpectedOrderNumberResult = 1,
PageNumber = 1,
X = 17,
Y = 9,
Width = 5,
Height = 3
},
new TestFieldModel()
{
ExpectedOrderNumberResult = 2,
PageNumber = 1,
X = 2,
Y = 5,
Width = 4,
Height = 2
}
};
My answer is very similar to yours. The main differences are
not using a boolean to break out of two loops.
In the "double break" scenario I prefer the maligned goto. It's subjective, but for me, is clearer because it avoids a check in the outer loop.
replacing the if statements on recalculating group bounds with Math.Min/Max. This shorthand expresses your intent more clearly.
calling Group.Add() for every field; there is no need to initialize the list of fields in a group differently if you have an empty list to being with
Psuedo C#:
foreach (var f in fields)
{
foreach (var g in groups)
{
if (g.VerticallyOverlapsWith(f))
{
g.Add(f);
goto NEXT_FIELD;
}
}
// no overlap detected, so make a new group
var newGroup = new Group();
newGroup.Add(f);
groups.Add(newGroup)
NEXT_FIELD :;
}
class Group
{
void AddField(Field f)
{
_group.Add(f);
_yTop = Max(f.Top, _yTop);
_yBottom = Min(f.Bottom, _yBottom);
}
List<Field> _group = new List<Field>();
int _yTop = int.MinValue();
int _yBottom = int.MaxValue();
}
At this point you have your groups. You now have to sort groups descending then by fields ascending (which you have done).
A couple of design points.
this does not address fields that overlap with fields in multiple groups, causing unnecessarily large groups. If you get into that, your tabbing order could get a bit unexpected (from the user's point of view). If you expect these
weird overlaps you'd be better off using a clustering algorithm or a "tolerance" function rather than a simple "overlap with first overlapping group" function (VerticallyOverlapsWith does the latter).
you can avoid the sort steps at the end by making the adds insert into an ordered collection in each case.
Ok this is how I was able to solve this:
[Fact]
public void GetFieldsOrderTest()
{
var fields = new List<TestFieldModel>()
{
new TestFieldModel()
{
ExpectedOrderNumberResult = 3,
PageNumber = 1,
X = 7,
Y = 6,
Width = 5,
Height = 2
},
new TestFieldModel()
{
ExpectedOrderNumberResult = 4,
PageNumber = 1,
X = 14,
Y = 4,
Width = 5,
Height = 3
},
new TestFieldModel()
{
ExpectedOrderNumberResult = 1,
PageNumber = 1,
X = 17,
Y = 9,
Width = 5,
Height = 3
},
new TestFieldModel()
{
ExpectedOrderNumberResult = 2,
PageNumber = 1,
X = 2,
Y = 5,
Width = 4,
Height = 2
}
};
var pageCount = 1;
for (var i = 1; i <= pageCount; i++)
{
var groups = new List<Group>();
foreach (var field in fields.Where(f => f.PageNumber == i))
{
var needsNewGroup = true;
foreach (var group in groups)
{
var fieldTop = field.Y + field.Height;
var fieldBottom = field.Y;
if ((fieldTop <= group.Top && fieldTop >= group.Bottom) || (fieldBottom >= group.Bottom && fieldBottom <= group.Top))
{
if (fieldTop > group.Top)
{
group.Top = fieldTop;
}
if (fieldBottom < group.Bottom)
{
group.Bottom = fieldBottom;
}
group.GroupFields.Add(field);
needsNewGroup = false;
break;
}
}
if (needsNewGroup)
{
var group = new Group
{
Top = field.Y + field.Height,
Bottom = field.Y,
GroupFields = new List<TestFieldModel>
{
field
}
};
groups.Add(group);
}
}
var groupFields = groups.OrderByDescending(g => g.Top).Select(g => g.GroupFields.OrderBy(gf => gf.X).ToList()).ToList();
fields = groupFields.Aggregate((acc, list) => acc.Concat(list).ToList());
fields.Should().HaveCount(4, "Count is not 4: " + fields.Count);
fields[0].ExpectedOrderNumberResult.Should().Be(1, $"Expected 1. Got {fields[0].ExpectedOrderNumberResult}.");
fields[1].ExpectedOrderNumberResult.Should().Be(2, $"Expected 2. Got {fields[1].ExpectedOrderNumberResult}.");
fields[2].ExpectedOrderNumberResult.Should().Be(3, $"Expected 3. Got {fields[2].ExpectedOrderNumberResult}.");
fields[3].ExpectedOrderNumberResult.Should().Be(4, $"Expected 4. Got {fields[3].ExpectedOrderNumberResult}.");
}
}
public class Group
{
public List<TestFieldModel> GroupFields { get; set; }
public int Top { get; set; }
public int Bottom { get; set; }
}
public class TestFieldModel
{
public int ExpectedOrderNumberResult { get; set; }
public int PageNumber { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public int X { get; set; }
public int Y { get; set; }
}

linq Contains but less

I have a list to search a table,
List<long> searchListIds = new List<long>();
searchListIds.Add(1);
searchListIds.Add(2);
List<long> searchListFieldValues = new List<long>();
searchListFieldValues.Add(100);
searchListFieldValues.Add(50);
and my query is:
var adsWithRelevantadFields =
from adField in cwContext.tblAdFields
group adField by adField.adId into adAdFields
where searchListIds.All(i => adAdFields.Select(co => co.listId).Contains(i))
&& searchListFieldValues.All(i => adAdFields.Select(co => co.listFieldValue).Contains(i))
select adAdFields.Key;
everything is ok, but now: i need to get all records that meet less than searchListFieldValues. i mean:
all adId that have (listId == 1)&(listFieldValue <100) AND (listId == 2)&(listFieldValue <50)
contains part must change to something like contains-less
example:
cwContext.tblAdFields:
id 1 2 3 4 5 6 7
adId 1 2 1 2 3 3 3
listId 1 1 2 2 1 2 3
listfieldValue 100 100 50 50 100 49 10
Now if I want to get (listId == 1)&(listFieldValue ==100) AND (listId == 2)&(listFieldValue ==50) my code works, and return id adId: 1,2
but I can't get
all adId that have (listId == 1)&(listFieldValue ==100) AND (listId == 2)&(listFieldValue <50)
it must return 3
You should try changing Contains to Any, but I'm not sure if LINQ to Entities will translate it correctly into proper SQL statement.
var adsWithRelevantadFields =
from adField in cwContext.tblAdFields
group adField by adField.adId into adAdFields
where searchListIds.All(i => adAdFields.Select(co => co.listId).Contains(i))
&& searchListFieldValues.All(i => adAdFields.Select(co => co.listFieldValue).Any(x => x < i))
select adAdFields.Key;
Here is a full example that should work if I understood you correctly:
class Program
{
static void Main(string[] args)
{
List<int> searchListIds = new List<int>
{
1,
2,
};
List<int> searchListFieldValues = new List<int>
{
100,
50,
};
List<Tuple<int, int>> searchParameters = new List<Tuple<int,int>>();
for (int i = 0; i < searchListIds.Count; i++)
{
searchParameters.Add(new Tuple<int,int>(searchListIds[i], searchListFieldValues[i]));
}
List<AdField> adFields = new List<AdField>
{
new AdField(1, 1, 1, 100),
new AdField(2, 2, 1, 100),
new AdField(3, 1, 2, 50),
new AdField(4, 2, 2, 50),
new AdField(5, 3, 1, 100),
new AdField(6, 3, 2, 49),
new AdField(7, 3, 3, 10)
};
var result = adFields.Where(af => searchParameters.Any(sp => af.ListId == sp.Item1 && af.ListFieldValue < sp.Item2)).Select(af => af.AdId).Distinct();
foreach (var item in result)
{
Console.WriteLine(item);
}
Console.Read();
}
public class AdField
{
public int Id { get; private set; }
public int AdId { get; private set; }
public int ListId { get; private set; }
public int ListFieldValue { get; private set; }
public AdField(int id, int adId, int listId, int listFieldValue)
{
Id = id;
AdId = adId;
ListId = listId;
ListFieldValue = listFieldValue;
}
}
}
First, you're probably looking for functionality of Any() instead of Contains(). Another thing is that if your search criteria consists of two items - use one list of Tuple<int,int> instead of two lists. In this case you will e able to efficiently search by combination of listId and fieldValue:
var result = from adField in cwContext.tblAdFields
where searchParams.Any(sp => adField.listId == sp.Item1 && adField.listFieldValue < sp.Item2)
group adField by adField.adId into adAdFields
select adAdField.Key;

Can someone help me ordering this list?

I am not the best programmer, so need some help to order this list. I had a few stabs at it, but still getting some cases which are wrong.
Essentially the list is the following:
#, ID, PreceedingID
A, 1 , 0
B, 2 , 3
C, 3 , 1
D, 4 , 2
I want to order it so that the list follows the preceeding id. The first item will always have the preceeding ID of 0.
#, ID, PreceedingID
A, 1 , 0
C, 3 , 1
B, 2 , 3
D, 4 , 2
Do you think you can help?
Thanks!
How about:
var data = new[] {
new Row{ Name = "A", ID = 1, PreceedingID = 0},
new Row{ Name = "B", ID = 2, PreceedingID = 3},
new Row{ Name = "C", ID = 3, PreceedingID = 1},
new Row{ Name = "D", ID = 4, PreceedingID = 2},
};
var byLastId = data.ToDictionary(x => x.PreceedingID);
var newList = new List<Row>(data.Length);
int lastId = 0;
Row next;
while (byLastId.TryGetValue(lastId, out next))
{
byLastId.Remove(lastId); // removal avoids infinite loops
newList.Add(next);
lastId = next.ID;
}
After this, newList has the data in the desired order.
In the above, Row is:
class Row
{
public string Name { get; set; }
public int ID { get; set; }
public int PreceedingID { get; set; }
}
But obviously substitute for your own type.
You can use for example dictionary to sort it:
Dictionary<..> d = new Dictionary<..>()
foreach(var el in list){
d[el.PreceedingID] = el; //put data to dict by PreecedingID
}
List<..> result = new List<..>();
int prec = 0; //get first ID
for(int i = 0; i < list.Length; ++i){
var actEl = d[prec]; //get next element
prec = actEl.ID; //change prec id
result.Add(actEl); //put element into result list
}

Using LINQ to count value frequency

I have a table
ID|VALUE
VALUE is an integer field with possible values between 0 and 4. How can I query the count of each value?
Ideally the result should be an array with 6 elements, one for the count of each value and the last one is the total number of rows.
This simple program does just that:
class Record
{
public int Id { get; set; }
public int Value { get; set; }
}
class Program
{
static void Main(string[] args)
{
List<Record> records = new List<Record>()
{
new Record() { Id = 1, Value = 0},
new Record() { Id = 2, Value = 1 },
new Record() { Id = 3, Value = 2 },
new Record() { Id = 4, Value = 3 },
new Record() { Id = 5, Value = 4 },
new Record() { Id = 6, Value = 2 },
new Record() { Id = 7, Value = 3 },
new Record() { Id = 8, Value = 1 },
new Record() { Id = 9, Value = 0 },
new Record() { Id = 10, Value = 4 }
};
var query = from r in records
group r by r.Value into g
select new {Count = g.Count(), Value = g.Key};
foreach (var v in query)
{
Console.WriteLine("Value = {0}, Count = {1}", v.Value, v.Count);
}
}
}
Output:
Value = 0, Count = 2
Value = 1, Count = 2
Value = 2, Count = 2
Value = 3, Count = 2
Value = 4, Count = 2
Slightly modified version to return an Array with only the count of values:
int[] valuesCounted = (from r in records
group r by r.Value
into g
select g.Count()).ToArray();
Adding the rows count in the end:
valuesCounted = valuesCounted.Concat(new[] { records.Count()}).ToArray();
Here is how you would get the number of rows for each value of VALUE, in order:
var counts =
from row in db.Table
group row by row.VALUE into rowsByValue
orderby rowsByValue.Key
select rowsByValue.Count();
To get the total number of rows in the table, you can add all of the counts together. You don't want the original sequence to be iterated twice, though; that would cause the query to be executed twice. Instead, you should make an intermediate list first:
var countsList = counts.ToList();
var countsWithTotal = countsList.Concat(new[] { countsList.Sum() });

Categories