Sorting strings by specified order - c#

I need to find out how to sort some data by pre-defined pattern.
Lets say I have some strings, which represents product informafion, e. g.
Product1, red, 70/n
Product6, blue, 90/n
Product3, red, 50/n
Product9, white, 33/n
I separated these strings by coma string split and stored them at different Arrays (name, color, price) and then DataTable with same columns.
I can order created rows by color using :
DataView.sort = "color"
or by LINQ with
DataRow[] dr = table.Select().OrderBy(u=>u[color]).ToArray();
DataTable sortedtable = dr.CopyToDataTable();
However this is just simple sorting, asc/desc, based on alphabet.
I would like to achieve sorting with pre-defined pattern. In example the item order would be defined by colors in order red, black, blue, white.
Is there anything simple I could do? I think this is possible with checking each row color and comparing it with predefined color list, then building new Array / DataTable based on this order. However I feel that this is weak approach.

You could store the order in another collection and then use IndexOf:
var colorOrderList = new List<string>{"red", "black", "blue", "white"};
table = table.AsEnumerable()
.OrderBy(row => colorOrderList.IndexOf(row.Field<string>("color")))
.CopyToDataTable();

You can define n ordering array like
var order = new [] { "red", "blue", "white"};
and then use IndexOf
DataRow.Select().OrderBy(u=>Array.IndexOf(order, u[color]))

Use IComparable to make a custom sort order.
https://support.microsoft.com/en-ca/help/320727/how-to-use-the-icomparable-and-icomparer-interfaces-in-visual-c

Products.OrderBy(u => u == "Red" ? 0 : u == "Black" ? 1 : u == "Blue" ? 2 : 3)
has the advantage that it should be translatable to a SQL statement so that the database server can do the sorting.

You can use IComaparable. First create a custom class which can accommodate your product details. Make this class implement the IComparable interface.
public class ProductDetails : IComparable
{
public int ProductId { get; set; }
public int CompareTo(object obj)
{
ProductDetails prodDetails = obj as ProductDetails;
if (obj == null) return 1;
if (prodDetails != null)
{
if (this.ProductId < prodDetails.ProductId) return 1;
else
return 0;
}
else {
return 0;
}
}
}

Related

How to sort a list so that the same changes would happen in another list?

I have a question:
For example I have 2 lists:
1: Banana Apple Orange
2: Yellow Red Orange
I want it to list.sort
so it will be:
Apple Banana Orange
But in the same time I want the SAME changes happening inside of the Yellow red orange list.
So it would be like this:
Apple Banana Orange
Red Yellow Orange
I didnt try this because I literally have no idea how to do this and all this is just on the planning board
You should really start using classes which encapsulate these information in one entity, for example:
public class Fruit
{
public string Name {get; set;}
public string Color {get; set;}
}
Then it's easy and readable:
List<Fruit> fruits = new()
{
new Fruit{ Name = "Banana", Color = "Yellow" },
new Fruit{ Name = "Apple", Color = "Red" },
new Fruit{ Name = "Orange", Color = "Orange" }
};
var orderedFruits = fruits.OrderBy(f => f.Name);
If you want to sort the original list, simply append ToList ad re-assign it:
fruits = fruits.OrderBy(f => f.Name).ToList();
If you really must use two separate lists which depend on each other, you could use Zip:
List<string> fruitNames = new() { "Banana", "Apple", "Orange" };
List<string> fruitColors = new() { "Yellow", "Red", "Orange" };
List<(string Name, string Color)> orderedFruits = fruitNames
.Zip(fruitColors, (n, c) => (Name: n, Color: c))
.OrderBy(x => x.Name)
.ToList();
fruitNames = orderedFruits.Select(x => x.Name).ToList();
fruitColors = orderedFruits.Select(x => x.Color).ToList();
If for some reason it is problematic to merge the lists, say you have a large list of large structs for example, an alternative can be to use a proxy list with indices for sorting:
var fruits = ...
var colors = ...
var proxy = Enumerable.Range(0, fruits.Length);
var sortedProxy = proxy.OrderBy(i => fruits[i]);
var sortedColors = sortedProxy.Select(i => colors[i]).ToList();
This lets you sort the indices according to the order defined by one list, and then you can use this ordering to create an ordered list for any list of the same length. Note that this example leaves the fruit list unordered.
You can reduce allocations by creating a custom comparer that does the i => fruits[i]-part so that you can use List.Sort instead of OrderBy.
You could try to zip the two list, making a list of pairs.
Then you can apply a custom sort on this list of pair, sorting pairs only relatively to the first element.
Once the sort is done, you can unzip the list of pairs to get back your two lists, sorted the same way.

Remove DataGridView cells containing duplicate values

My DataGridView looks like this:
How to clear the text of duplicate cells in the DataGridView Rows?
I tried below but it's clearing all values of Cells[0].
string duplicateValue = dataGridView1.Rows[0].Cells[0].Value.ToString();
for (int i = 1; i < dataGridView1.Rows.Count; i++)
{
if (dataGridView1.Rows[i].Cells[0].Value.ToString() == duplicateValue)
{
dataGridView1.Rows[i].Cells[0].Value = string.Empty;
}
else
{
dataGridView1.Rows[i].Cells[0].Value = duplicateValue;
}
}
One way to achieve this would be to use a HashSet as follows:
var valuesFound = new HashSet<string>();
for (int i = 0; i < dataGridView1.Rows.Count; i++)
{
string cellText = dataGridView1.Rows[i].Cells[0].Value.ToString();
// Attempt to add the value to the HashSet. If it fails, then it's a duplicate.
if (!valuesFound.Add(cellText))
{
dataGridView1.Rows[i].Cells[0].Value = string.Empty;
}
}
Or if you prefer LINQ, you could do something like this:
var duplicateCells = dataGridView1.Rows.OfType<DataGridViewRow>()
.Select(r => r.Cells[0])
.GroupBy(c => c.Value.ToString())
.SelectMany(g => g.Skip(1))
.ToList();
duplicateCells.ForEach(c => c.Value = string.Empty);
Short answer:
How to clear the text of duplicate cells in the DataGridView Rows?
Apparent you consider some products to be the same. Alas you forgot to say when two products are equal. Is Product [Apple, UK, 1] equal to [Apple, UK, 2]? And if so, which one do you want to show?
Or do you want to show the sum: [Apple, UK, 3]?
And what about: [Apple, Ireland, 1]? Is that the same as [Apple, UK, 1]?
Clearly you need a method that says: this product equals that product, but that one is a different product.
For this we'll have to create an equality comparer.
class Product
{
public Name {get; set;}
public string Country {get; set;}
public int Quantity {get; set;}
...
}
IEqualityComparer<Product> productComparer = ... // TODO: implement
Once you've got this, you can get rid of duplicates:
IEnumerable<Product> productsWithDuplicates = ...
IEnumerable<Product> noDuplicates = productsWithDuplicates.Distinct(productComparer);
Or if you want to combine [Apple, UK, 1] and [Apple, UK, 2] to show the sum [Apple, UK, 3], use groupBy to make groups:
IEnumerable<Product> productsToDisplay = productsWithDuplicates
.GroupBy(product => new {product.Name, product.Country}
(key, productsWithThisKey) => new Product
{
Name = key.Name,
Country = key.Country,
Quantity = productWithThisKey.Select(product => product.Quantity).Sum(),
},
productComparer);
So the solution depends on when two products are equal, and what you want to show if you've found equal produts.
Equality Comparer for Products
class ProductComparer : EqualityComparer<Product>()
{
public static IEqualityComparer<Product> NameCountry {get;} = new ProductComparer();
public override bool Equals(Product x, Product y)
{
if (x == null) return y == null; // true if both null, false if x null, but y not
if (y == null) return false; // because x not null
if (object.ReferenceEquals(x, y) return true;
// define equality, for instance:
return x.Name == y.Name && x.Country == y.Country;
}
If you want case insensitive, add a property:
private static IEqualityComparer<string> NameComparer {get; } = StringComparer.InvariantIgnoreCase;
private static IEqualityComparer<string> CountryComparer {get;} = ...
And in Equals:
return NameComparer.Equals(x.Name, y.Name)
&& CountryComparer.Equals(x.Country, y.Country);
Now if you later decide that you want to be case sensitive when comparing Countries, or maybe want to use the current culture, you'll only have to change this on one location.
The use of the comparers, makes changing easier, but also your code: you don't have to check for null names and countries, that is handled by the comparers.
GetHashCode: only requirement: if x equals y, return same GetHashCode. if not equal, you are free to return whatever you want, but it is more efficient if you return different hashcode.
public override int GetHashCode(Product product)
{
if (product == null) return 47855249; // just a number
return NameComparer.GetHashCode(product.Name)
^ CountryComparer.GetHashCode(product.Country);
}
There's room for improvement
It is usually not a good idea to intertwine your model with the view of your model. If in future you want to change how your data is displayed, for instance you want to show it in a ListBox, or in a Graph, you'll have to change a lot.
Besides, if you have separated your model from the way that it is displayed, it will be a lot easier to unit test your model. To test your view, you won't need your original data, you can test with edge conditions, like an empty datagridview,
First of all, you need a method to fetch the products from your model:
private IEnumerable<Productm> FetchProducts(...) {...}
So now you have a unit testable method that fetches the products. The nice thing is that you even hid where you get this information from: it can be from a database, or an XML file, or even from the internet: your Form doesn't know, and doesn't have to know. Totally designed for change.
Using visual studio designer you have defined columns. Every column shows exactly the value of one property. Which property the column shows is defined in property DataGridViewColumn.DataPropertyName
columnName.DataPropertyName = nameof(Product.Name);
columnCountry.DatapropertyName = nameof(Product.Country);
...
To show the fetched Products, use property DataGridView.DataSource. If you assign a List<Product>, then changes that the operator makes (add / remove rows, change cells) are not reflected in the List. If you want to automatically update the changes that the operator made, use a BindingList
public BindingList<Product> DisplayedProducts
{
get => (BindingList<Product>)this.datagridView1.DataSource;
set => this.datagridView1.DataSource = value;
}
private IEqualityComparer<Product> ProductComparer {get;} = ProductComparer.NameCountry;
public void InitProductDisplay()
{
IEnumerable<Product> productsToDisplay = this.FetchProducts()
.Distinct(productComparer);
// or if you want to show the totals: use the GroupBy described above
this.DisplayedProducts = new BindingList<Product>(productsToDisplay.ToList());
}
Nice! If you don't want to compare on NameCountry, but differently, or if you want to Compare using current culture, if you want to show the totals of the quantity, or even if you want to show it in a graph instead of a table: there is only one place you need to change.
Now every change that the operator makes: add / remove / change is reflected in your BindingList, even if the rows are sorted.
For instance, if the operator indicates that he finished editing by clicking a button:
private void OnButtonOk_Clicked(object sender, ...)
{
var displayedProducts = this.DisplayedProducts;
// find out which products are added / removed / changed
this.ProcessEditedProducts(displayedProducts);
}
If you need to do something with selected rows, consider to add the follwing:
private Product CurrentProduct => (Product)(this.datagridView1.CurrentRow?.DataBoundItem);
private IEnumerable<Product> SelectedProducts = this.datagridView1.SelectedRows
.Cast<DataGridViewrow>()
.Select(row => row.DataBoundItem)
.Cast<Product>();

Deleting an array element while moving others down?

I'm really new to programming, so take this with a grain of salt.
I've made 2 arrays that correspond to eachother; One is a Name array and one is a Phone Number array. The idea is that the spot [1] in NameArray corresponds to spot [1] in the PhoneArray. In other words, I need to keep these 'pairings' in tact.
I'm trying to make a function that deletes one of the spots in the array, and shifts everything down one, as to fill the space left empty by the deleted element.
namearray = namearray.Where(f => f != iNum).ToArray();
is what I've tried, with iNum being the number corresponding to the element marked for deletion in the array.
I've also tried converting it to a list, removing the item, then array-ing it again.
var namelist = namearray.ToList();
var phonelist = phonearray.ToList();
namelist.Remove(txtName.Text);
phonelist.Remove(txtPhone.Text);
namearray = namelist.ToArray();
phonearray = phonelist.ToArray();
lbName.Items.Clear();
lbPhone.Items.Clear();
lbName.Items.AddRange(namearray);
lbPhone.Items.AddRange(phonearray);
with txtName.Text and txtPhone.Text being the strings for deletion in the corresponding list boxes.
Can someone suggest a better way to do it / What I'm doing wrong / How to fix?
Thanks guys
-Zack
A better way would be to have an array of a class that contains a Name and Phone Number object:
public class PersonData
{
public string Name { get; set; }
public string Phone { get; set; }
}
public PersonData[] data;
That way, instead of keeping two arrays in sync, it's one array with all the appropriate data.
Try a loop through both arrays, moving the values of each down an index each time.
Start the loop at the index of the value you want to delete. So you would find the IndexOf(T) the value you want, storing it as deleteIndex and run the loop starting from that index.
When you hit the end of the array, set the last value as null or string.Empty (depending what value type the array holds).
A bit like this:
var deleteIndex = namearray.IndexOf("TheStringYouWantToDelete");
for (int i = deleteIndex; i < namearray.Length; i++)
{
if (i == namearray.Length - 1) // The "last" item in the array.
{
namearray[i] = string.Empty; // Or null, or your chosen "empty" value.
phonearray[i] = string.Empty; // Or null, or your chosen "empty" value.
}
else
{
namearray[i] = namearray[i+1];
phonearray[i] = phonearray[i+1];
}
}
This will work for deleting and moving values 'down' in index.
You could also rewrite the code for moving them the other way, as it would work similarly.
Reordering them completely? Different ball game...
Hope this helps.
If the namearray and phonearray contain strings and you know the index of the element to remove (iNum) then you need to use the overload of the Where extension that takes a second parameter, the index of the current element in the evaluation
namearray = namearray.Where((x, y) => y != iNum).ToArray();
However the suggestion to use classes for your task is the correct one. Namearray and Phonearray (and whatever else you need to handle in future) are to be thought as properties of a Person class and instead of using arrays use a List<Person>
public class Person
{
public string FirstName {get; set;}
public string LastName {get; set;}
public string Phone {get; set;}
}
List<Person> people = new List<Person>()
{
{new Person() {FirstName="Steve", LastName="OHara", Phone="123456"}},
{new Person() {FirstName="Mark", LastName="Noname", Phone="789012"}}
};
In this scenarion removing an item knowing the LastName could be written as
people = people.Where(x => x.LastName != "OHara").ToList();
(or as before using the index in the list of the element to remove)
people = people.Where((x, y) => y != iNum).ToArray();
The other answers provide some better design suggestions, but if you're using ListBoxes and want to stick with arrays, you can do this to synchronize them:
int idx = lbName.Items.IndexOf(txtName.Text);
if (idx > -1)
{
lbName.Items.RemoveAt(idx);
lbPhone.Items.RemoveAt(idx);
}
namearray = lbName.Items.Cast<string>().ToArray<string>();
phonearray = lbPhone.Items.Cast<string>().ToArray<string>();
Use a dictionary instead.
Dictionary<string, string> phoneBook = new Dictionary<string, string>();
phoneBook["Foo"] = "1234567890";
phoneBook["Bar"] = "0987654321";
phoneBook.Remove("Bar");

Select items from List<>

I have this List defined as property :
List<string> colors= new List<string>();
colors.Add("Red");
colors.Add("Blue");
colors.Add("Green");
colors.Add("Black");
And I have this function:
private List<string> getColors(string colorName , List<string> headers)
{
List<string> list2return = colors(return all colors except red and black);
return list2return ;
}
My question is how can I select from list all items except red and black?
Like this?:
colors.Where(c => !c.Equals("Red") && !c.Equals("Black")).ToList()
Or, if you need it to be case-insensitive:
colors.Where(c =>
!c.Equals("Red", StringComparison.InvariantCultureIgnoreCase) &&
!c.Equals("Black", StringComparison.InvariantCultureIgnoreCase)
).ToList()
(Though it's not really clear why that method has parameters which aren't being used. Or how it has access to the colors variable in the first place, since that really doesn't look like a class-level member.)
Another way is like this:
colors.Except(new[] { "Red", "Black" });

How to sort a collection of an object that has a predecessor field?

Suppose that I have a Color class
class Color
{
int id;
string name;
int? predecessorId;
Color(_id, _name, _predecessorId)
{
id = _id;
name = _name;
predecessorId = _predecessorId;
}
}
The purpose of the predecessor ID is that I can have a collection of colors and have them sorted arbitrarily. (I did not design this data structure.)
Suppose I have the following seven colors:
var colors = new []
{
new Color(0, "red", null),
new Color(1, "orange", 0),
new Color(2, "yellow", 1),
new Color(3, "green", 2),
new Color(4, "blue", 3),
new Color(5, "indigo", 4),
new Color(6, "violet", 5)
}
Now, suppose that I receive data from an external source. The data looks like the above, but the colors don't necessarily come in the array in the order that the predecessorId field. However, I will always assume that the predecessorId values always form an unbroken linked list.
Given an array of seven of these color objects, how do I sort them so that the first one is the one with no predecessor, the second one has the first one as its predecessor, and so on.
I am well aware that there are any number of ways to skin this cat, but I am using C# .net and I would like to use the built-in framework classes as much as possible to avoid writing more code that must be maintained.
Pick the first (and hopefully unique) color by finding the one with predecessorId == null. Then find the next by indexing a dictionary of predecessorId to color until no more matches are found:
var current = colors.Single(c => c._predecessorId == null);
var sorted = new List<Color> { current };
var colorsByPredecessorId = colors.Where(c => c._predecessorId.HasValue)
.ToDictionary(c => c._predecessorId.Value, c => c);
while (colorsByPredecessorId.ContainsKey(current._id))
{
current = colorsByPredecessorId[current._id];
sorted.Add(current);
}
return sorted;
I posted the same thing as ryanyuyu but deleted it as he was a few seconds faster, but seeing as it seems to do what you want and he hasn't submitted it as answer yet, I will:
var orderedColors = colors.OrderBy(x => x.predecessorId);
Which provides the output like so:
So I'm not sure what you mean in your comment
solution here would only work if we could guarantee that the predecessorIds are always in numeric order. If I changed the predecessor IDs to reverse the order, it wouldn't work anymore
It doesn't matter what order the source data is in. That's the point of OrderBy. It puts the collection in the order you dictate. Unless you can clarify on the requirements, this should suffice.

Categories

Resources