c# Sorting with multiple conditions - c#

Bear with me, I will try to not make this too complicated. I have a list of products. A product looks like this:
{
"id": 3797,
"title": "Canon EOS 100D Digital SLR Camera with 18-55 IS STM Lens, HD 1080p, 18MP, 3\" LCD Touch Screen",
"shortTitle": "Canon EOS 100D Black",
"brand": "Canon",
"model": "EOS 100D",
"colour": "Black",
"gtin": "8714574602721",
"image": "http://piiick.blob.core.windows.net/images/Canon-EOS-100D-18-55-Black-8714574602721.png",
"type": "Digital SLR",
"lensFocalLength": "18-55",
"lensType": "IS STM",
"lensMount": "EF/EF-S",
"maxAperture": "999",
"connectivity": "",
"shootingModes": "Scene Intelligent Auto (Stills and Movie), No Flash, Creative Auto, Portrait, Landscape, Close-up, Sports, SCN(Kids, Food, Candlelight, Night Portrait, Handheld Night Scene, HDR Backlight Control), Program AE , Shutter priority AE, Aperture priority AE, Manual (Stills and Movie)",
"weight": 410.0,
"width": 116.8,
"height": 90.7,
"depth": 69.4,
"digitalZoom": "N/A",
"opticalZoom": "N/A",
"waterproof": false,
"maxVideoResolution": "1920 x 1080",
"sensorType": "CMOS",
"sensorSize": "22.3 x 14.9 mm",
"continuousShootingSpeed": "4",
"iso": "1600",
"style": "traditional",
"designer": "",
"dateAnnounced": "10/06/2008",
"focusPoints": 7
}
But to help explain this I will simplify it.
I am trying to create a sort method that will allow sorting on multiple columns.
For example, I have this method:
/// <summary>
/// Performs an advanced sort on products using sortations
/// </summary>
/// <param name="products">The products to sort</param>
/// <param name="sortations">The sortation list</param>
public void AdvancedSort(List<JObject> products, List<Sortation> sortations)
{
// Get our value types
GetTypesFromOperator(sortations);
// Sort our products by the sortations
products.Sort((a, b) =>
{
foreach (var sortation in sortations)
{
// Get our values
var fieldName = sortation.Field;
var x = a.SelectToken(fieldName).ToString();
var y = b.SelectToken(fieldName).ToString();
var type = sortation.Type;
// If both values are the same, skip the rest of this iteration
if (x.Equals(y)) return 0;
// If we have an expression
if (!string.IsNullOrEmpty(sortation.Expression))
{
// If we are checking for between
if (sortation.Operator == "><")
{
// Get our values
var values = sortation.Expression.Split(',');
// If we have 2 values
if (values.Length == 2)
return CompareBetween(values, x, y);
}
// If we are checking booleans
if (sortation.Operator == "===")
return CompareBoolean(sortation.Expression);
// If we are checking equals
if (sortation.Operator == "=")
return y.Equals(sortation.Expression) ? 1 : -1;
// If we are checking like
if (sortation.Operator == "%")
return y.Equals(sortation.Expression, StringComparison.OrdinalIgnoreCase) ? 1 : -1;
}
// If we get this far, do a switch on sortation types
switch (sortation.Type)
{
case SortationValueType.Boolean:
return Boolean.Parse(x) ? -1 : 1;
case SortationValueType.Number:
return CompareNumeric(x, y, sortation.Direction);
default:
return string.CompareOrdinal(x, y);
}
}
// If nothing matches, return 0
return 0;
});
}
It is trying to sort based on property value types and what I call sortations.
A sortation is just a class that stipulates the operator, expression and field to query.
This is an example of a sortation:
// Add importance, expert and sponsored
sortations.Add(new Sortation
{
Field = "importance",
Operator = "<"
});
Now, we can have an number of different sortations which are dynamically added. I need to be able to sort the products by these sortations.
I have created a simple test:
[Test]
public void SortBySilverColourSilverShouldBeTop()
{
// Assemble
var json = "[{ colour: 'Black', importance: '150' }, { colour: 'Black', importance: '150' }, { colour: 'Black', importance: '150' }, { colour: 'Silver', importance: '150' }, { colour: 'Black', importance: '149' }, { colour: 'Black', importance: '149' }, { colour: 'Black', importance: '149' }, { colour: 'Silver', importance: '149' }]";
var products = JsonConvert.DeserializeObject<List<JObject>>(json);
var sortations = new List<Sortation>() { new Sortation() { Field = "colour", Operator = "=", Expression = "Silver" }, new Sortation() { Field = "importance", Operator = "<" } };
// Act
_sortProvider.AdvancedSort(products, sortations);
var result = products[0].SelectToken("colour").ToString();
// Assert
Assert.That(result, Is.EqualTo("Silver"));
}
I would expect with that, that I would get this output:
[
{ colour:'Silver', Importance:'150'},
{ colour:'Black', Importance:'150' },
{ colour:'Black', Importance:'150' },
{ colour:'Black', Importance:'150' },
{ colour:'Silver', Importance:'149' },
{ colour:'Black', Importance:'149' },
{ colour:'Black', Importance:'149' },
{ colour:'Black', Importance:'149' }
]
but instead I get this output:
[
{ colour:'Silver', Importance:'150' },
{ colour:'Silver', Importance:'149' },
{ colour:'Black', Importance:'150' },
{ colour:'Black', Importance:'150' },
{ colour:'Black', Importance:'150' },
{ colour:'Black', Importance:'149' },
{ colour:'Black', Importance:'149' },
{ colour:'Black', Importance:'149' }
]
Can someone help me fix my sort method?

In my mind your basic problem is that as soon as the first sortation hits a match it returns the sort order value to the sort function and the rest of the sortations don't matter at all. I believe what you need to do is swap the foreach and the products.sort call, like so:
// Get our value types
GetTypesFromOperator(sortations);
foreach (var sortation in sortations)
{
// Sort our products by the sortations
products.Sort((a, b) =>
{
// Get our values
var fieldName = sortation.Field;
var x = a.SelectToken(fieldName).ToString();
var y = b.SelectToken(fieldName).ToString();
var type = sortation.Type;
....
....
}
}
This will take longer, but each sortation will be considered. Keep in mind that the last sortation in the list will be the most significant.

It looks like you've got them backwards - it puts all the "Silver"s first, then ordering by importance second.
Try swapping the order of your sortations:
var sortations = new List<Sortation>() {
new Sortation() { Field = "importance", Operator = "<" },
new Sortation() { Field = "colour", Operator = "=", Expression = "Silver" }
};

Related

Returning top 50% of list whilst evenly splitting rows between object property

Suppose I have the following object
public string Quote { get; set; }
public string FirstName { get; set; }
I have a List of this object, sample data as:
I am trying to return a new list which will evenly distribute between each FirstName (Same amount) and return 50% of the rows.
In the above Example, I have 20 rows, returning half gives 10. There are 4 different FirstNames to which John = 2, Mark = 2, Phil = 2, bob = 2 - There are 2 slots remaining which 2 Different random names are chosen.
So how do i do the grouping so the FirstNames are evenly distributed? and is the 50% taken first or last?
New to LINQ and filtering through data so help is appreciated :)
LinQ is not the most efficient way As we will be ieterating multiple time the same collection in roder to get basic count etc.
But lets do it Step by step.
Data generation:
That part was missing from the original question. You can change the repatition of name adding duplicate values in names collection.
// Data generation, Tuple because Im lazy.
var names = new[] { "Toto", "John", "John", "John", "Foo", "Foo", "Bar" };
var datas = Enumerable.Range(0, 100)
.Select(x => (Id: x + 1, Name: names[x % names.Length]));
Basic Count:
As we are given a percentage we need to know how many element it represent.
// Input value
var wantedPercent = 10;
// Counting, may be more efficient to iterate that list only once doing grouping and both count in one go
var totalCount = datas.Count();
var distinctNames = datas.Select(x => x.Name).Distinct().Count();
int wantedSize = (int)Math.Ceiling(totalCount * (wantedPercent / 100.0));
int maxGroupSize = wantedSize / distinctNames; // Integer division : 1.9999 -> 1
Grouping:
This is the core issue. We need to group by on name.
And for each element of those group we will number them based on maxsize of a group.
If that number exceed the max size we keep them for remaining slots.
var grpDatas = datas.GroupBy(x => x.Name)
.SelectMany(g =>
g.Select((x, i) =>
new { Item =x, newGroupingKey = i < maxGroupSize}
)
)
.GroupBy(x=> x.newGroupingKey);
Populate :
Let's take the item of the first group.
var result = grpDatas.First(x=> x.Key)
.Select(g=> g.Item)
.ToList();
You can have a remaining slot even if wantedSize / distinctNames is round.
Because one group size may be lower than the Max Allowed Size.
var remainingSlot = wantedSize - result.Count();
if (remainingSlot > 0){
var random = new Random();
var shuffledGroup2 = grpDatas.First(x => !x.Key)
.Select(g => g.Item)
.OrderBy(a => random.Next());
var remaining = shuffledGroup2.Take(remainingSlot);
result.AddRange(remaining);
}
Result : Live Demo
[
{ "Id": 1, "Name": "Toto" },
{ "Id": 8, "Name": "Toto" },
{ "Id": 2, "Name": "John" },
{ "Id": 3, "Name": "John" },
{ "Id": 5, "Name": "Foo" },
{ "Id": 6, "Name": "Foo" },
{ "Id": 7, "Name": "Bar" },
{ "Id": 14, "Name": "Bar" },
{ "Id": 83, "Name": "Foo" }, // random
{ "Id": 85, "Name": "Toto" } // random
]

MongoDB in C# - what is the right way to perform multiple filtering

I have a MongoDB collection of Persons (person_Id, person_name, person_age)
I want to return an object that contains:
number_of_all_persons
number_of_all_persons with age < 20
number_of_all_persons with age >= 20 && age <= 40
number_of_all_persons with age > 40
What is the right way to do it in Mongo using C#?
Should I run 4 different Filters to achieve result?
You can use Aggregation.
const lowerRange = { $lt: ["$person_age", 20] };
const middleRange = { $and: [{ $gte: ["$person_age", 20]}, { $lte: ["$person_age", 40] }] };
// const upperRange = { $gt: ["$person_age", 40] };
db.range.aggregate([
{
$project: {
ageRange: {
$cond: {
if: lowerRange,
then: "lowerRange",
else: {
$cond: {
if: middleRange,
then: "middleRange",
else: "upperRange"
}
}
}
}
}
},
{
$group: {
_id: "$ageRange",
count: { $sum: 1 }
}
}
]);
Here the total count is not included as you can calculate it from the count of the ranges. If you have more that 3 ranges, a better idea would be to pass an array of $cond statements to the project stage, as nesting multiple if/else statements starts to get harder to maintain. Here is a sample:
$project: {
"ageRange": {
$concat: [
{ $cond: [ { $lt: ["$person_age", 20] }, "0-19", ""] },
{ $cond: [ { $and: [ { $gte: ["$person_age", 20] }, { $lte: ["$person_age", 40] } ] }, "20-40", ""] },
{ $cond: [ { $gt: ["$person_age", 40] }, "41+", ""] }
]
}
}
You have several possible solutions here.
Filter with mongo
Filter with Linq
Looks like you are having an "and" filter here. You can do your filtering as described here: Link to filtering
Or, depending on how large your persons collection is, you could do query for all objects and filter with linq:
var collection.FindAll().Where(x => (x.age < 20) && (x.age >= 20 && x.age <= 40) ...)
The above is not syntax correct but I hope you get the idea behind it.
My approach would be creating a filter builder and and adding it to the find method call.
var highExamScoreFilter = Builders<BsonDocument>.Filter.ElemMatch<BsonValue>(
"scores", new BsonDocument { { "type", "exam" },
{ "score", new BsonDocument { { "$gte", 95 } } }
});
var highExamScores = collection.Find(highExamScoreFilter).ToList();
Hope this helps somehow.

Remove all occurrences of a particular key from a JSON response in C#

I have a JSON string from which I want to eliminate all the occurrences of a given key.
JSON I have:
string requstBody =
{
"payLoad": [
{
"BaseVersionId_": 9,
"VersionId_": 10,
"AssetCollateralLink": [
{
"AssetId": 137,
"BaseVersionId_": 9,
"VersionId_": 10
},
{
"AssetId": 136,
"BaseVersionId_": 0,
"VersionId_": 1
}
],
"CollateralProvider": [],
"AdvCollateralAllocation": [
{
"LinkId": 91,
"IsDeleted_": false,
"BaseVersionId_": 1,
"VersionId_": 2
}
]
}
]
}
I want to eliminate keys "BaseVersionID_" and "VersionId_" as follows:
string requstBody =
{
"payLoad": [
{
"AssetCollateralLink": [
{
"AssetId": 137
},
{
"AssetId": 136
}
],
"CollateralProvider": [],
"AdvCollateralAllocation": [
{
"LinkId": 91,
"IsDeleted_": false
}
]
}
]
}
I used JObject.Remove(); as follows
JObject sampleObj1 = new JObject();
sampleObj1 = JsonHelper.JsonParse(requestBody);
sampleObj1.Remove("BaseVersionId_");
but able to remove the keys under payLoad Hierarchy only.
How do I remove all the occurrences of the Key.
The required properties can be removed from the Json, simply with Linq:
var jsonObj = JObject.Parse(requestBody);
jsonObj.SelectToken("payLoad[0]").SelectToken("AdvCollateralAllocation")
.Select(jt => (JObject)jt)
.ToList()
.ForEach(r =>
r
.Properties()
.ToList()
.ForEach(e =>
{
if (e.Name == "BaseVersionId_" || e.Name == "VersionId_")
e.Remove();
}));
The resultant jsonObj will be without the BaseVersionId_ and VersionId_ names as well as their values.
I'd use JsonPath as such:
var toRemove = jsonObject
.SelectTokens("$.payLoad.[*].AssetCollateralLink.[*]..BaseVersionId_")
.Concat(stuff.SelectTokens("$.payLoad.[*].AssetCollateralLink.[*]..VersionId_"))
.Concat(stuff.SelectTokens("$.payLoad.[*].AdvCollateralAllocation.[*]..VersionId_"))
.Concat(stuff.SelectTokens("$.payLoad.[*].AdvCollateralAllocation.[*]..BaseVersionId_"))
.ToList();
for (int i = toRemove.Count - 1; i >= 0; i--)
{
toRemove[i].Parent?.Remove();
}

How to find total number of lowest level of elements in a JSON object?

I have a Json as below
{
"name": "xxxx",
"type": "ccc",
"properties": {
"serialNumber": {
"value": "24-66292"
},
"documentLinks": {
"productManuals": {
"54868484": {
"url": {
"value": "xxxx"
},
"productName": {
"value": "R02400"
}
}
},
"keystringFiles": {
"60050588": {
"url": {
"value": "http://se-s-0010052.de.abb.com/stage/wc/!control.controller?action=view_document&doc_id=60050588"
},
"name": {
"value": "24-66292_160.kxt"
},
"fileSize": {
"value": 0.87
},
"addedDate": {
"value": "2012-01-19"
},
"addedBy": {
"value": "Loader"
}
}
}
}
},
"variables":{
"temperature" :{
"dataType": "number"
},
"modes" :{
"normal":{
"dataType": "string"
},
"fast":{
"dataType": "string"
}
}
}
}
I need to count the total number of elements in this excluding the root level elements. In this example it's like under "properties" below are the number of elements
serialNumber
documentLinks->productManuals->54868484->url
documentLinks->productManuals->54868484->productName
documentLinks->keystringFiles->60050588->url
documentLinks->keystringFiles->60050588->name
documentLinks->keystringFiles->60050588->fileSize
documentLinks->keystringFiles->60050588->addedDate
documentLinks->keystringFiles->60050588->addedBy
Under "variables"
temperature
modes->normal
modes->fast
Hence total number of elements is
8+3 = 11
I was trying multiple things as below, but I am not able to find the best logic which serves the purpose.
var ob = JObject.Parse(json);
var propCount = JObject.Parse(json).Root
.SelectTokens("properties")
.SelectMany(t => t.Children().OfType<JProperty>().Select(p => p.Name))
.ToArray();
var varCount = JObject.Parse(json).Root
.SelectTokens("variables")
.SelectMany(t => t.Children().OfType<JProperty>().Select(p => p.Name))
.ToArray();
int propCount = 0;
int variableCount = 0;
foreach (var prop in propCount)
{
propCount += ob["properties"][prop].Children().Count();
}
foreach (var variable in varCount)
{
variableCount += ob["variables"][variable].Children().Count();
}
var total = propCount + variableCount;
I think, based on what I understand, you have 11. You're missing properties->serialNumber in your count
private int GetLowestLevelCount(JToken obj)
{
int counter = 0;
if (obj.HasValues) // Checks if token has children.
{
foreach (var children in obj.Children())
{
counter += GetLowestLevelCount(children);
}
}
else // Lowest-level elem.
{
counter +=1;
}
return counter;
}
Use:
var ob = JObject.Parse(json);
var mainKeys = ob.Children().Select(x => ((JProperty)x).Name);
int lowestLevel = 0;
foreach (var mainKey in mainKeys)
{
var a = ob[mainKey];
if (a.HasValues) //Don't bother with top-level elements
{
foreach (var c in a.Children())
{
lowestLevel += GetLowestLevelCount(c);
}
}
}
Console.WriteLine(lowestLevel);

Grouping Linq and Converting columns to rows

I have a list that will currently return something like this. the Att column can be anything because a user can enter in a Att and Value at anytime.
var attr_vals = (from mpav in _db.MemberProductAttributeValues
select mpav);
Results
Id Att Value
1 Color Blue
1 Size 30
1 Special Slim
3 Color Blue
4 Size 30
2 Special Slim
2 Random Foo Foo
The conversion I am looking for would be similar to this
Converted results
Id Color Size Special Random
1 Blue 30 Slim null
2 null null null Foo Foo
3 Blue null null null
4 null 52 null null
Class looks like this so far.
public class MyMappProducts
{
public int? id { get; set; }
public Dictionary<string, string> Attributes { get; set; }
string GetAttribute(string aName)
{
return Attributes[aName];
}
void setAttribute(string aName, string aValue)
{
Attributes[aName] = aValue;
}
}
So giving that your list of attributes might change, creating a class with each attribute as a property would not be good as you'll have to know all the attributes before hand, thus working with a dictionary is easier.
Here's a way of doing what you want (Note that the missing attributes of each row aren't present in the dictionary):
var list = new List<AttributeValue>
{
new AttributeValue(1, "Color", "Blue"),
new AttributeValue(1, "Size", "30"),
new AttributeValue(1, "Special", "Slim"),
new AttributeValue(3, "Color", "Blue"),
new AttributeValue(4, "Size", "30"),
new AttributeValue(2, "Special", "Slim"),
new AttributeValue(2, "Random", "Foo Foo")
};
// First we groupby the id and then for each group (which is essentialy a row now)
// we'll create a new MyMappProducts containing the id and its attributes
var result = list.GroupBy(av => av.Id)
.Select(g => new MyMappProducts
{
id = g.Key,
Attributes = g.ToDictionary(av => av.Attribute, av => av.Value)
})
.ToList();
This results in (pretty printed):
[
{
"id": 1,
"Attributes": {
"Color": "Blue",
"Size": "30",
"Special": "Slim"
}
},
{
"id": 3,
"Attributes": {
"Color": "Blue"
}
},
{
"id": 4,
"Attributes": {
"Size": "30"
}
},
{
"id": 2,
"Attributes": {
"Special": "Slim",
"Random": "Foo Foo"
}
}
]

Categories