Search anywhere in list, not just the first letter - c#

What I want to do, is to search in a ComboBox, for a word, or a part of a word like this:
For example I have these entries in my combobox:
abc
Abc
Dabc
adbdcd
And when I search for "abc", it should show me every one in the list, except "
adbdcd"
I fill my combobox from a mysql database, so my items are in a "Collection".
I have autocomplete enabled (mode: SuggestAppend, source: ListItems)
This is the code, I am using right now:
private void comboBox1_KeyPress(object sender, KeyPressEventArgs e)
{
comboKeyPressed();
}
private void comboBox1_TextChanged(object sender, EventArgs e)
{
if (comboBox1.Text.Length == 0) comboKeyPressed();
}
private void comboKeyPressed()
{
comboBox1.DroppedDown = true;
object[] originalList = (object[])comboBox1.Tag;
if (originalList == null)
{
// backup original list
originalList = new object[comboBox1.Items.Count];
comboBox1.Items.CopyTo(originalList, 0);
comboBox1.Tag = originalList;
}
// prepare list of matching items
string s = comboBox1.Text.ToLower();
IEnumerable<object> newList = originalList;
if (s.Length > 0)
{
newList = originalList.Where(item => item.ToString().ToLower().Contains(s));
}
// clear list (loop through it, otherwise the cursor would move to the beginning of the textbox...)
while (comboBox1.Items.Count > 0)
{
comboBox1.Items.RemoveAt(0);
}
// re-set list
comboBox1.Items.AddRange(newList.ToArray());
}
The problem with this code, is if I search for "abc" in my example list, "adbdcd" will show up too. And this code randomly crashes my program when I hit backspace in the combobox.

This is the root cause of crash:
while (comboBox1.Items.Count > 0)
{
// this is raising exception if you try to remove the last item
// Check the doc of RemoveAt
comboBox1.Items.RemoveAt(0);
}
use that instead:
comboBox1.Items.Clear();
However what you are trying to achieve is still unclear.
If Text of combox is empty, then nothing will ever happen apart from clearing and re adding the same items to the combo box.
My understanding is that you're trying to duplicate the completion behavior while having it enabled. It can raise exception too (AccessViolationException) since you try to modify the Items collection while the framework tries to do the same.
If you're not happy with the default auto completion behavior, disable it and try to implement it completely inside the comboKeyPressed method.
It means then calling it whenever text is modified.
Modification of your code to make it work (Disable auto completion though):
private void comboBox1_TextChanged(object sender, EventArgs e)
{
comboKeyPressed();
}
private void comboKeyPressed()
{
if (comboBox1.Text == lastFilter)
{
return;
}
object[] originalList = (object[]) comboBox1.Tag;
if (originalList == null)
{
// backup original list
originalList = new object[comboBox1.Items.Count];
comboBox1.Items.CopyTo(originalList, 0);
comboBox1.Tag = originalList;
}
// prepare list of matching items
string s = comboBox1.Text.ToLower();
IEnumerable<object> newList = originalList;
if (s.Length > 0)
{
newList = originalList.Where(item => item.ToString().ToLower().Contains(s));
}
// clear list (loop through it, otherwise the cursor would move to the beginning of the textbox...)
comboBox1.Items.Clear();
// re-set list
comboBox1.Items.AddRange(newList.ToArray());
comboBox1.Select(Text.Length -1, 0);
lastFilter = comboBox1.Text;
comboBox1.DroppedDown = true;
}

So, you want fulltext search.
Where is your data coming from? What kind of data is it? What are the edge cases?
For databases, I like to use Sphinx for fulltext indexing.
For in-memory data, efficient fulltext search algorithms include Suffix Arrays, Suffix Trees, and Patricia Tries. Implementing them can be a fun (and time-consuming) challenge. Or you might find a suitable implementation online. You can find barebones implementations of these algorithms, as well as more polished implementations of fulltext search, such as Lucene.
To give you a sketch of how they work, imagine taking every possible suffix of every word that you store, e.g. needle:
needle
eedle
edle
dle
le
e
Put all these suffixes into an ordered data structure, such as a sorted array or list (for static data) or a B-tree or SortedDictionary (if data is added regularly). After inserting needle, it would contain:
dle
e
edle
eedle
le
needle
Now we can search for any part of a word (e.g. edl) using binary search or better, and see if we have a hit.
To extract more information than just whether or not we have a hit, we could add data to, for example, the values of the SortedDictionary. (We used the suffixes as keys.) Interesting data could be the entire word, or perhaps the original text and location where the word was encountered.

If the number of entries to search from is always low - perhaps no more than a few hundred - then a simple implementation will suffice.
// Select all words that contain our query string
var matchingWords = wordList.Where(word => word.Contains(query));
This is the naive, linear implementation that will become very slow for large data. For small data, though, it is super easy. Just feed the new subset to your combo box.
You might want to add case-insensitivity to the Contains() call, using its optional second parameter.

Related

How to Group and Sort Objects in ObjectListView?

I am trying to group my list of objects in an ObjectListView.
The ObjectListView should group the objects based on the first column but then have that same column sorted based on a custom sort.
How do I do that? I have read through the documentation for ObjectListView:
http://objectlistview.sourceforge.net/cs/gettingStarted.html#gettingstarted
So far, I have implemented my custom sort but I am not sure how to trigger the grouping? Remember that I am trying to group on the first column but then apply a custom sort.
My custom sort relis on the BeforeSorting event:
// after initializing components
olv.BeforeSorting += olv_BeforeSorting;
Then...
private void olv_BeforeSorting(object sender,BrightIdeasSoftware.BeforeSortingEventArgs e)
{
olvDataSource.Sort((x, y) => x.Group.ID.CompareTo(y.Group.ID));
e.Handled = true;
}
The ObjectListView displays my ordered object list but it is not grouped together. Each object displays on its own row without a group heading.
How do I group my objects after sorting them?
You can force the grouping column as follows:
olv.ShowGroups = true;
olv.AlwaysGroupByColumn = olvColumn1;
If you want to show one value in the column and group by a different one you can use GroupByKeyGetter
olvColumn1.GroupKeyGetter = GroupKeyGetter;
Delegate would be something like:
private object GroupKeyGetter(object rowObject)
{
var o = rowObject as MyClass;
if(o == null)
return "unknown";
return o.ID;
}
Some stuff doesn't take affect till you call
olv.RebuildColumns();
Always Sort By (Arbitrary Function)
If you want to force sorting on some custom logic you can use ListViewItemSorter in the BeforeSorting event. This is similar to registering a CustomSorter (but that doesn't seem to work when ShowGroups is true).
olv.BeforeSorting += olv_BeforeSorting;
Then
private void olv_BeforeSorting(object sender, BrightIdeasSoftware.BeforeSortingEventArgs e)
{
//example sort based on the last letter of the object name
var s = new OLVColumn();
s.AspectGetter = (o) => ((MyClass)o).Name.Reverse().First();
this.olv.ListViewItemSorter = new ColumnComparer(
s, SortOrder.Descending, e.ColumnToSort, e.SortOrder);
e.Handled = true;
}
I am sharing this for anyone that might come across here looking for a way to apply a custom sort on groups within an ObjectListView.
There might be better ways to do this, but this way worked for me.
colFirst.GroupFormatter = (BrightIdeasSoftware.OLVGroup group, BrightIdeasSoftware.GroupingParameters parms) =>
{
ObjectA a = (OjectA)group.Key;
/* Add any processing code that you need */
group.Task = " . . . ";
group.Header = "Special Name: " + a.Name;
group.Subtitle = $("Object A: {a.Index}, Total Water Consumption: {a.WaterConsumption}");
// This is what is going to be used as a comparable in the GroupComparer below
group.Id = a.ID;
// This will create the iComparer that is needed to create the custom sorting of the groups
parms.GroupComparer = Comparer<BrightIdeasSoftware.OLVGroup>.Create((x, y) => (x.GroupId.CompareTo(y.GroupId)));
};
The OLVColumn.GroupFormatter is lightly explained here:
http://objectlistview.sourceforge.net/cs/recipes.html#how-do-i-put-an-image-next-to-a-group-heading
This works and is basically what is described in the cookbook here http://objectlistview.sourceforge.net/cs/recipes.html?highlight=sort#how-can-i-change-the-ordering-of-groups-or-rows-within-a-group
First subscribe to the olv BeforeCreatingGroups event.
Then create a custom sort comparator in the event handler. In this case for a group matching "Turtles" it will push to the end of the sort, but you can obviously have however much convoluted logic you want in there.
private void Olv_BeforeCreatingGroups(object sender, CreateGroupsEventArgs e)
{
e.Parameters.GroupComparer = Comparer<BrightIdeasSoftware.OLVGroup>.Create(
(x, y) => (
x.Header == "Turtles" ? 1
: x.GroupId.CompareTo(y.GroupId)
)
);
}
This is what I used initially since it's what was in the cookbook. But I ended up switching to something more like Marwan's answer because that one creates a space to reconfigure the group headers themselves.

GetDirectories - Retrieve only a few folders from the Directory

New to c# and ASP.net, I am working on retrieving Directories and have done so.
{
public partial class WebForm1 : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
ListItem item;
ListItem item
string folderLocation = #"\\serv5007i\TeamCityDeploy\Trunk Production Build\Current\bin\Runtime";
int startSize = folderLocation.Length+1;
string[] fileNames = Directory.GetDirectories(folderLocation);
foreach (string fileName in fileNames)
{
item = new ListItem();
item.Value = item.Text = "Add " + fileName.Substring(startSize);
CheckBoxList1.Items.Add(item);
CheckBoxList2.Items.Add(item);
CheckBoxList3.Items.Add(item);
}
}
}
}
So I have the output as the Directory of 15 or so folders. Is it possible to return only 10 and then 5 in another div.
So basically I have 15 folders being returned, but I need to have the bottom 5 under a different heading to the others. My apologies if I'm not being clear. Beginner!
You can control the output, when you execute Directory.GetDirectories you'll receive a collection. I believe in this instance, you'll receive a string array. This can be manipulated however you want:
Loop Example's:
foreach(string directory in directories)
{
// Enumerate over all items within the collection.
}
for(int index = 0; index < directories.Length; index++)
{
// Will enumerate until index == directories
// If you make index five, it would start at position six of the array.
// since they're zero based. But you can manipulate how you want.
}
do
{
index++
// Perform an action. Based on the while.
}
while(index != directories.Length);
while(index != directories)
{
index++;
// Perform action until equal.
}
The downfall to these approaches, are you're manipulating a integer for a starting position or ending position. Which can create confusion in the code. The other approach would be Linq, which similar to the above as far as iteration but will make the code a bit more expressive.
Linq Example's:
var filtered = directories.Take(10); // Take the first ten.
var filtered = directories.Skip(5); // Skip the first five.
var filtered = directories.Where(path => new DirectoryInfo(path).Name.Contains("Name")); // If directory names contain, return on that.
You could also do:
var filter = Directory.EnumerateDirectories(path)
.Where(directory => directory.Name.Contains("Sample"))
.Take(10);
So the initial line will automatically enumerate the directories within the provided path, you filter based on name, then take the first ten.
You can tackle this problem a lot of different ways, that is why narrowing it down would be more helpful.
"Go not to the Elves for counsel, for they will say both no and yes."
Update:
A full Linq example would be one of these two approaches:
var directories = Directory.GetDirectories(path);
var filtered = directories.Skip(5);
Or you could do it in a single line.
var filtered = Directory.EnumerateDirectories(path).Skip(5).Take(5);

Searching a access database for a part of a word

I have an access database that contains columns such as Title, Director, And Year.
I have access database connected to the program. I am able to search the database and print out the title of the movie to a listBox only if the title contains the one word. For example, if I search for the movie "psycho" the movie will display in the listBox.
Now I'm wondering how I can search for a particular word like "the" and print all the movie titles with "the" in it to the listBox.
At that point, the text book I'm using fails to go into anything deeper.
Maybe I'm not using the right method for what I'm trying to do.
private void searchButton_Click(object sender, EventArgs e)
{
listBox1.Items.Clear();
string lowerCaseData = textBox1.Text.ToLower();
if (titleButton.Checked)
{
var query =
from m in this.moviesDataSet.Movies
where m.Title == lowerCaseData
select m;
foreach (var m in query)
{
listBox1.Items.Add(m.Title);
}
}
}
private void Form1_Load(object sender, EventArgs e)
{
// TODO: This line of code loads data into the 'moviesDataSet.Movies' table. You can move, or remove it, as needed.
this.moviesTableAdapter.Fill(this.moviesDataSet.Movies);
}
Instead of exact match check for Contains
Depending on the LINQ provider, Contains check should be translated to SQL LIKE statement which will be case insensitive, so your check should be:
where m.Title.Contains(lowerCaseData)
However for in-memory collections, Contains would perform a case sensitive match. In that case String.IndexOf can be used like:
where m.Title.IndexOf(lowerCaseData, StringComparison.OrdinalIgnoreCase) >= 0)
Your where query is doing an exact match. Use Contains instead
where m.Title.ToLower().Contains(lowerCaseData)

Check that all values of an array have been used

Sorry if this is a stupid noob question. I'm doing a very small project for my girlfriend - a list of countries and she has to enter their capitals (obscure countries, mind you) . Since I'm a total beginner, I had to resort to using two arrays, one for countries and the other for capitals, with matching indexes. That way it's easy to check for the right answer and I don't have to parse any text files or use any data-bases. I'm using random numbers to make it more interesting. To stop the program from generating the same countries over and over again, I'm using a List of integers that keeps tracks of what indexes have already been used and regenerates the number if the list contains the previous one. Pretty basic stuff. Surprisingly, it all works.
But I'm having a problem. How do I check that I've run out of countries, basically? :) I can't simply check the List size against my countries array, since List probably includes more values than the array, and if (taken.Equals(Countries.Length)) doesn't seem to work. Or I can't find the right place in the code to put this check.
Sorry if this is simple, but I can't seem to find a proper solution.
EDIT
Wow, what an amazing community. During the short walk from Starbucks to my place I get dozens of quality answers which cover a huge array of design techniques. This is so great! Thank you everyone! Obviously, the question has been answered but I will post the code for you, if anyone has any additional comments.
// JUST A TEST FOR NOW, 13 COUNTRIES
string[] Countries = {"Belgium", "France", "The Netherlands", "Spain", "Monaco", "Belarus", "Germany",
"Portugal", "Ukraine", "Russia", "Sweden", "Denmark", "South Africa"};
string[] Capitals = {"Brussels", "Paris", "Amsterdam", "Madrid", "Monaco", "Minsk", "Berlin",
"Lisbon", "Kiev", "Moscow", "Stockholm", "Copenhagen", "Pretoria"};
Random number = new Random();
List<int> taken = new List<int>();
int index;
int score = 0;
private int Generate()
{
while (true) {
index = number.Next(0, Countries.Length);
if (taken.Contains(index)) continue;
// THIS IS WHAT I WAS INITIALLY TRYING TO DO
if (taken.Equals(Countries.Length)) {
MessageBox.Show("Game over!");
return -1;
}
return index;
}
}
private void Form1_Load(object sender, EventArgs e)
{
index = Generate();
taken.Add(index);
label1.Text = Countries[index];
label3.Text = "0 out of " + Countries.Length.ToString();
}
private void button1_Click(object sender, EventArgs e)
{
if (textBox1.Text.Trim() == Capitals[index].ToString()) {
label2.Text = "You win!";
index = Generate();
taken.Add(index);
label1.Text = Countries[index];
textBox1.Clear();
label3.Text = ++score + " out of " + Countries.Length.ToString();
}
else {
label2.Text = "Wrong!";
textBox1.Clear();
}
}
}
}
To stop the program from generating the same countries over and over again, I'm using a List of integers that keeps tracks of what indexes have already been used and regenerates the number if the list contains the previous one.
...
How do I check that I've run out of countries, basically?
You might want to consider an alternative approach, as this is going to be quite expensive and overly complicated.
Instead of trying to add one country at random, checking against ones you've already added, you could just make the entire list of countries, then perform a shuffle ("random sort") on the collection. This way, you'll get all of the countries in one shot in a random order.
Instead of using two arrays, or an array and a list, let's introduce something of C# 4.0 that actually looks and is easy to use and seems to be made for this type of assignments.
Follow this code with your eyes and specifically look how these "anonymous types" are used in the end. It makes life real easy.
// initialize your array like so,
// now you can access your items as countries[1].name and countries[1].city
// and you will never have to worry about having too much cities or countries
// PLUS: they're always together!
var countries = new [] {
new { name = "The Netherlands", city = "Amsterdam"},
new { name = "Andorra", city = "Vaduz" },
new { name = "Madagascar", city = "Antananarivo"}
};
// randomize by shuffling (see http://stackoverflow.com/questions/375351/most-efficient-way-to-randomly-sort-shuffle-a-list-of-integers-in-c-sharp/375446#375446)
Random random = new Random();
for (int i = 0; i < countries.Length; i += 1)
{
int swapIndex = random.Next(i, countries.Length);
if (swapIndex != i)
{
var temp = countries[i];
countries[i] = countries[swapIndex];
countries[swapIndex] = temp;
}
}
// go through all your items in the array using foreach
// so you don't have to worry about having too much items
foreach(var item in countries)
{
// show your girlfriend the country, something like
string inputString = DisplayCountry(item.country);
if(inputString == item.city)
{
ShowMessage("we are happy, you guessed right!");
}
}
// at the end of the foreach-loop you've automatically run out of countries
DisplayScore(to-your-girlfriend);
Note: you can easily expand on this anonymous types by adding whether or not that particular country/city pair was guessed right and make a subsequent test with the ones she failed.
You could use a HashSet<int> to keep track of indexes that have been used. This won't accept duplicate values. The Add method returns a boolean that indicates whether the value was already in the list:
if (hashSet.Add(index))
DisplayValue(index);
else
//regenerate
But I would probably use your existing stragegy, but backwards: create a list pre-filled with values from 0 to Count - 1. Pick indexes from this list, removing them as you use them. This is logically similar to Reed Copsey's suggestion of sorting, but probably requires less change to your existing code.
var availableIndexes = new List<int>(Enumerable.Range(0, countryCount));
var random = new Random();
while (availableIndexes.Count > 0)
{
var index = availableIndexes[Random.Next(0, availableIndexes.Count)];
DisplayValue(index);
availableIndexes.Remove(index);
}
You can use a key/value pair, like a Dictionary<string, string> to store your countries and capitals. Then iterate through the collection using a random LINQ orderby clause:
Dictionary<string, string> Countries = new Dictionary<int, string>();
// populate your collection of countries
foreach(var country in Countries.OrderBy(c => Guid.NewGuid()))
{
Console.WriteLine("Key: {0} Value: {1}", country.Key, country.Value);
}
Create a Country class and a Capital class.
Then model your classes to use a Dictionary<TKey, TValue> Generic Collection so that you declare the generic Dictionary object as:
Dictionary<Country, Capital>
where Country is the key and Capital is its value.
For MSDN reference to Dictionary and its sample usage, you can follow below link:
http://msdn.microsoft.com/en-us/library/xfhwa508.aspx
As you keep using Countries and Capitals, add them to above Dictionary instance after checking for their existence in the Dictionary instance, if any of them do exist then either popup an info message or a warning.
Quick and dirty, not necessarily efficient or secure.
Dictionary<string, string> countriesAndCapitals = new Dictionary<string, string>()
{
{ "Afghanistan", "Kabul" },
{ "Albania", "Tirane" },
{ "Algeria","Algers" },
{ "Andorra", "Andorra la Vella" } //etc, etc
};
foreach (var countryCapital in countriesAndCapitals.OrderBy(f => Guid.NewGuid()))
{
Console.WriteLine(countryCapital.Key + " " + countryCapital.Value);
}
It seems like what you need is a different type of data structure, two sets of lists would work fine but it is complicated for nothing. I suggest looking into the dictionary list type.
Dictionary<string,string> countryList = new Dictionary<string,string>();
countryList.Add("Canada","Ottawa");
countryList.Add("Thailand","Bankok");
etc...
You could then iterate through the list while a boolean value sees whether or not there was a hit. More info on Dictionary list type.
Why don't you remove the items from the list that you used? Then you don't have conflicts. Then you check states.Count() > 0.
The quickest thing I can think to do is to use the Distinct() call on your list. Then your count of items in the list can be compared to your array's count to see if all have been used.
if(myUsedList.Distinct().Count() < myArray.Count) { ... }

Cache only parts of an object

I'm trying to achieve a super-fast search, and decided to rely heavily on caching to achieve this. The order of events is as follows;
1) Cache what can be cached (from entire database, around 3000 items)
2) When a search is performed, pull the entire result set out of the cache
3) Filter that result set based on the search criteria. Give each search result a "relevance" score.
4) Send the filtered results down to the database via xml to get the bits that can't be cached (e.g. prices)
5) Display the final results
This is all working and going at lightning speed, but in order to achieve (3) I've given each result a "relevance" score. This is just a member integer on each search result object. I iterate through the entire result set and update this score accordingly, then order-by it at the end.
The problem I am having is that the "relevance" member is retaining this value from search to search. I assume this is because what I am updating is a reference to the search results in the cache, rather than a new object, so updating it also updates the cached version. What I'm looking for is a tidy solution to get around this. What I've come up with so far is either;
a) Clone the cache when i get it.
b) Create a seperate dictionary to store relevances in and match them up at the end
Am I missing a really obvious and clean solution or should i go down one of these routes? I'm using C# and .net.
Hopefully it should be obvious from the description what I'm getting at, here's some code anyway; this first one is the iteration through the cached results in order to do the filtering;
private List<QuickSearchResult> performFiltering(string keywords, string regions, List<QuickSearchResult> cachedSearchResults)
{
List<QuickSearchResult> filteredItems = new List<QuickSearchResult>();
string upperedKeywords = keywords.ToUpper();
string[] keywordsArray = upperedKeywords.Split(' ');
string[] regionsArray = regions.Split(',');
foreach (var item in cachedSearchResults)
{
//Check for keywords
if (keywordsArray != null)
{
if (!item.ContainsKeyword(upperedKeywords, keywordsArray))
continue;
}
//Check for regions
if (regionsArray != null)
{
if (!item.IsInRegion(regionsArray))
continue;
}
filteredItems.Add(item);
}
return filteredItems.OrderBy(t=> t.Relevance).Take(_maxSearchResults).ToList<QuickSearchResult>();
}
and here is an example of the "IsInRegion" method of the QuickSearchResult object;
public bool IsInRegion(string[] regions)
{
int relevanceScore = 0;
foreach (var region in regions)
{
int parsedRegion = 0;
if (int.TryParse(region, out parsedRegion))
{
foreach (var thisItemsRegion in this.Regions)
{
if (thisItemsRegion.ID == parsedRegion)
relevanceScore += 10;
}
}
}
Relevance += relevanceScore;
return relevanceScore > 0;
}
And basically if i search for "london" i get a score of "10" the first time, "20" the second time...
If you use the NetDataContractSerializer to serialize your objects in the cache, you could use a [DataMember] attribute to control what gets serialized and what doesn't. For instance, you could store your temporarary calculated relevance value in a field that is not serialized.

Categories