Binary searching array to display row containing string field - c#

I have a button which should take a 'surname' string input, search a directory array for the 'record' structure correlating to that surname, and output that record to a listview. The directory consists of many rows of 3 string record structures: surname, forename, extcode.
After days of scouring similar SO questions and other sources, I've pieced together the methods which seem to fit my problem best, but my primary search method is not, for some reason, referencing my secondary method. Can you tell me why you think this is, whether you think it will work if I manage to get the reference working, and if not, any alternative suggestions?
FYI my my if statement is a little unruly as my project requires that the surname be case insensitive and accept partial string matches.
private void SearchSurname()
{
Array.Sort(directory, (x, y) => String.Compare(x.surname, y.surname));
ClearForm();
int surnameIndex = Array.BinarySearch(directory, txtSurname.Text);
if (directory[surnameIndex].surname.ToUpper().Substring(0, txtSurname.Text.Length).Contains(txtSurname.Text.ToUpper()))
{
ListViewItem record = new ListViewItem();
// Send each field in current record to single listview item
record.Text = (Convert.ToString(txtSurname.Text));
record.SubItems.Add(Convert.ToString(txtForename.Text));
record.SubItems.Add(Convert.ToString(txtExtCode.Text));
// Display new record listview item in listview
lvDirectory.Items.Add(record);
}
}
public int BinarySearch(string[] directory, string searchTerm)
{
int first = 0;
int last = directory.Length - 1;
int position = -1;
bool found = false;
int compCount = 0;
searchTerm = txtSurname.Text;
while (found != true && first <= last)
{
int middle = (first + last) / 2;
if (string.Compare(directory[middle], searchTerm, true) == 0)
{
found = true;
position = middle;
compCount++;
}
else if (string.Compare(directory[middle], searchTerm, true) > 0)
{
last = middle;
compCount++;
}
else
{
first = middle;
compCount++;
}
}
return position;
}
Edit: Updated code per Olivier Jacot-Descombes' answer:
private void SearchSurname()
{
// Sort directory alphabetically by surname
Array.Sort(directory, (x, y) => String.Compare(x.surname, y.surname));
ClearForm();
// In directory, find line index of search term
int surnameIndex = BinarySearch(directory, txtSurname.Text);
ListViewItem record = new ListViewItem();
// Send each field in current record to single listview item
record.Text = (Convert.ToString(directory[surnameIndex].surname));
record.SubItems.Add(Convert.ToString(directory[surnameIndex].forename));
record.SubItems.Add(Convert.ToString(directory[surnameIndex].extCode));
// Display new record listview item in listview
lvDirectory.Items.Add(record);
}
private int BinarySearch(record[] directory, string searchTerm)
{
int first = 0;
int last = directory.Length - 1;
int surnameIndex = -1;
bool indexFound = false;
// While index not found and there are still points in the array to check
while (indexFound != true && first < last)
{
int middle = (first + last) / 2;
// If surname field in middle record of directory array matches the search term
if (string.Compare(directory[middle].surname, searchTerm, true) == 0)
{
// Index found!
indexFound = true;
surnameIndex = middle;
MessageBox.Show("If 1");
}
// If surname field in middle record of directory array is higher, alphabetically, than the search term
else if(string.Compare(directory[middle].surname, searchTerm, true) > 0)
{
// The next search will be between the first and the current middle records of the array
last = middle;
MessageBox.Show("If 2");
}
// If surname field in middle record of directory array is lower, alphabetically, than the search term
else
{
// The next search will be between the current middle and the highest records of the array
first = middle;
MessageBox.Show("If 3");
}
}
return surnameIndex;
}

Your SearchSurname method calls Array.BinarySearch, which is a static method of the Array Class. If you wanted to call your own method, you would have to write:
int surnameIndex = BinarySearch(directory, txtSurname.Text); // Without "Array."
You are using case-insensitive comparison through the 3rd parameter of String.Compare being true.
You could use
if (string.StartsWith(directory[middle], searchTerm,
StringComparison.CurrentCultureIgnoreCase))
To search only for beginning of names. But there is a problem if several names match. E.g. You are searching for "smit" and you have "Smith" and "Smithy" in the array. Therefore, you would have to test the entries before and after the found match, and return all matching ones as possible results.

Related

Printing list with additional text

Edit:
So currently I am creating a struct:
struct Coordinate
{
public string Name;
public int HP;
public int EXP;
public Coordinate(string name, int hp, int exp)
{
this.Name = name;
this.HP = hp;
this.EXP = exp;
}
}
}
I have also created this list:
var myList = new List<Coordinate>();
And this is a code i have been working on:
{Console.Write("name:");
string name = Console.ReadLine();
Console.Write("hp:");
int hp = Convert.ToInt32(Console.ReadLine());
Console.Write("exp:");
int exp = Convert.ToInt32(Console.ReadLine());
Coordinate point = new Coordinate(name, hp, exp);
Console.WriteLine("Pokemon's name: "+point.Name);
Console.WriteLine(point.HP); //output: hp inputed
Console.WriteLine(point.EXP); //output: 20
myList.Add(point);
}
else if (menu == '6'){ //need help here, want to print list
}
How to I add the list so that when I input 6, I want to write this:
Pokemon's name = Charmander
Pokemon's HP = 20
Pokemon's EXP = 40
This is probably incorrect:
if (myDictionary.Count <= 0)
{
myDictionary.Add("Pokemon's Name", name);
myDictionary.Add("Pokemon's HP", hp.ToString());
myDictionary.Add("Pokemon's EXP", exp.ToString());
myDictionary.Add("Pokemon's Skill", name);
Console.WriteLine("Pokemon has been added!");
}
First of all, it will only run the code if the dictionary is empty, so trying to add another pokemon will not do anything.
Secondly, it adds the pokemons properties as separate values in the dictionary. So if you remove the Count <= 0 check it will fail since keys are constants and have already been added. Regular dictionaries can only have a single value for a specific key.
You probably want to do the following:
Introduce a Pokemon-class with name, hp, exp and skill properties.
Replace myDictionary with a regular list of pokemons.
Remove the Count <= 0 check
If you only want to allow a single pokemon with a specific name you can keep the dictionary and change it to myDictionary[myPokemonName] = myPokemonObject. This will add a new pokemon, or overwrite any existing pokemon with the same name.

Trying to pass objects to an array and then search the array to return the object [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 5 years ago.
Improve this question
So I'm trying to create a program that will allow me to enter object information to an array and then search that array for pieces of the object and return the object.
At this point I'm not sure if my objects are being put in to the array and if my calls are correct.
Ideally I want to populate the array of objects, then when the menu displays choose a search option. I will search the array of objects for a match and display the object(s) to that match.
I'm not sure where my mistake is as this is not returning my search options. Assistance finding it would be great. Thank you!
static void Main(string[] args)
{
Greeting();
WriteLine();
School [] newStudent = new School[4];
string firstName;
string lastName;
string major;
int id;
double gpa;
for (int i = 0; i < newStudent.Length; i++)
{
GetInfo(out firstName, out lastName, out major);
id = GetId();
gpa = GetGpa();
WriteLine();
newStudent[i] = new School(lastName, firstName, major, id, gpa);
}
DisplayMenu();
int menuOpt = GetChoice();
DoChoice(menuOpt, newStudent);
}
static void Greeting()
{
WriteLine("Student Input");
}
static void GetInfo(out string lastn, out string firstn, out string mjr)
{
Write("Enter student's last name: ");
lastn = ReadLine();
Write("Enter student's first name: ");
firstn = ReadLine();
Write("Enter student's major: ");
mjr = ReadLine();
}
static int GetId()
{
Write("Enter student's ID number: ");
int inum = int.Parse(ReadLine());
while (inum <= 0)
{
Write("ID number must be greater than 0. Please enter student's ID number: ");
inum = int.Parse(ReadLine());
}
return inum;
}
static double GetGpa()
{
Write("Enter student's GPA: ");
double sgpa = double.Parse(ReadLine());
while (sgpa < 0.0 || sgpa > 4.0)
{
Write("That's not a valid GPA. Try Again: ");
sgpa = double.Parse(ReadLine());
}
return sgpa;
}
static void DisplayMenu()
{
WriteLine("1. Search by Last Name");
WriteLine("2. Search by First Name");
WriteLine("3. Search by Major");
WriteLine("4. Quit");
}
static int GetChoice()
{
Write("What is your choice? ");
int select = int.Parse(ReadLine());
while (select < 1 || select > 4)
{
Write("That's not a valid choice. Try Again: ");
select = int.Parse(ReadLine());
}
return select;
}
static void DoChoice(int choice, params School [] nStudent)
{
switch (choice)
{
case 1:
FindLastName(nStudent);
break;
case 2:
break;
//more options will go here at a later date with their matching methods like case 1
}
}
static void FindLastName(params School [] findLName)
{
Write("What is the last name? ");
string findL = ReadLine();
int pos = Array.IndexOf(findLName, findL);
if (pos > -1)
{
PrintStudent(findLName[pos]);
}
else
{
Write("No results found with that last name {0}.", findL);
}
}
static void PrintStudent(School student1)
{
Write(student1.ToString());
}
class School
{
private string lname;
private string fname;
private double gpa;
private string major;
private int id;
public School (string ln, string fn, string maj, int ident, double grade)
{
lname = ln;
fname = fn;
major = maj;
id = ident;
gpa = grade;
}
public string Lname
{
get
{
return lname;
}
set
{
lname = value;
}
}
public string Fname
{
get
{
return fname;
}
set
{
fname = value;
}
}
public string Major
{
get
{
return major;
}
set
{
major = value;
}
}
public override string ToString()
{
string student = lname + ", " + fname + "; " + id + " " + major + " " + gpa.ToString("D2");
return student;
}
UPDATE!: So I have confirmed that my array is populating my objects, however I'm wondering if those values are being passed properly to my methods.
Currently trying to search the array of objects with my FindLastName() method is only returning the else statement as if the values in the array do not exist.
UPDATE 2: After making some suggested changes, I'm about 80% confident my error lies within this method.
static void FindLastName(params School [] findLName)
{
for (int i = 0; i < findLName.Length; i++)
{
WriteLine(findLName[i]);
}
// The above loop proves that the objects exist within the array after being passed to the method.
Write("What is the last name? ");
string findL = ReadLine();
School foundStudent = null;
for (int i = 0; i < findLName.Length; i++)
{
if (findLName[i].Lname == findL)
{
foundStudent = findLName[i];
break;
}
}
if (foundStudent != null)
{
PrintStudent(foundStudent);
}
else
{
Write("No results found with that last name {0}.", findL);
}
// When I run this it returns the else statement even though I am using a last name that exists within the array
It looks to me like you are passing the parameters to GetInfo() in the wrong order, which ends up swapping the person's first name with their last name. So then later, when you're searching for someone by their last name, you will never get a match!
The function signature looks like this (with lastName as the FIRST argument):
static void GetInfo(out string lastn, out string firstn, out string mjr)
But you are calling it like this (with lastName as the SECOND argument):
GetInfo(out firstName, out lastName, out major);
So to resolve this issue, you just need to swap the positions of firstName and lastName in the line above.
Also, I just noticed that you are trying to format the GPA as a date, which will not work. You might consider changing this line:
gpa.ToString("D2")
to this (if you want to show one decimal place):
gpa.ToString("0.0")
You could utilise LINQ to make this search nice and easy with something like this:
string searchValue = PutSearchValueInHere();
School[] selectedStudentArray = schoolArray.Where(school=>school.SearchField == searchValue).ToArray();
School selectedStudent = selectedStudentArray[0]; //Assuming you used a primary key as your search field
Essentially how this works is you pick a field (likely a unique student ID) and a property of your class (eg .StudentID) then pass it into the extention method .Where of your array as a lambda expression. It will then return any items where the lambda evaluates to true.
The expression school=>school.SearchField == searchValue essentially means "Does the search value equal the SearchField of this school object, yes/no?" and it repeats this operation for every item in your array.
Next, the .ToArray() simply tells it to store the results as a new array.
After you've done this, assuming only one student fits the criteria, you are free to select the first item in the array. However, it might be worth checking to see if this is empty or not.
As an after-thought, I really can't help but feel that you should be storing student information in a SQL database and accessing it through an ODBC link, but that's something different entirely.
Based on the concepts it looks like you've learned so far, you probably want to change your logic a little bit in your search algorithm.
What you can do is create a Boolean variable to track whether or not you found any students, and start it off with false
Then loop through your array, one student at a time, and compare the last name to the one we're searching for
If we find the student, output their details and set our Boolean variable to true.
Outside of the loop, we can now just check to see if our variable is false, and if it is we can write the message that no students were found:
The reason using IndexOf wasn't working like you wanted it to is that you have to pass an object of the type stored in the array, and it has to be an exact match to one in the array (the Compare method of the class would return true).
...
static void FindLastName(params School[] findLName)
{
Write("What is the last name? ");
string findL = ReadLine();
// Create a variable to track if we find a match
bool foundStudent = false;
// Search through each array item
for (int i = 0; i < findLName.Length; i++)
{
// See if this item's last name is a match
if (findLName[i].LName == findL)
{
// If it is, set our variable to this student and break out of the for loop
PrintStudent(findLName[i]);
foundStudent = true;
}
}
// If our variable is false (or "not true" in the syntax here), we tell the user
if (!foundStudent)
{
Write("No results found with that last name {0}.", findL);
}
}

Pass string value created in one function as parameter to function

I have a function called ValidColumns that, when finished will contain a string of id values joined by the pipe character "|."
private bool ValidColumns(string[] curGlobalAttr, int LineCount)
{
//test to see if there is more than 1 empty sku mod field in the imported file
string SkuMod = GetValue(curGlobalAttr, (int)GlobalAttrCols.SKU);
if(SkuMod =="")
ids += string.Join("|", OptionId);
}
What I want to do is take the ids string and pass it as a reference into another function to check to see if it contains duplicate values:
protected bool CheckForDuplicates(ref string ids)
{
bool NoDupes = true;
string[] idSet = ids.Split('|');
for (int i = 1; i <= idSet.Length - 1; i++)
{
if (idSet[i].ToString() == idSet[i - 1].ToString()) { NoDupes = false; }
}
return NoDupes;
}
But I am not sure how to do it properly? It seems so easy but I feel like I'm making this out to be much harder than it needs to be.
if (idSet[i].ToString() == idSet[i - 1].ToString())
You're only checking each value against the previous value. That would work if the values were sorted, but an easier method would just be to get a distinct list and check the lengths:
return (idSet.Length == idSet.Distinct().Count());
The second part of D Stanley's answer is what you want
public bool CheckForDuplicates(string value)
{
string[] split = value.Split('|');
return split.Length != split.Distinct().ToArray().Length;
}
You need a nested loop to do this which will look at each item and compare it to another item. This also will eliminate checking it against itself so it does not always return false.
In this we will track what we have already checked and only compare the new items against items after that slowly shrinking the second loop down in size to eliminate duplicate checks.
bool noDupes = true;
int currentItem = 0;
int counter = 0;
string[] idSet = ids.Split('|');
while(currentItem < idSet.Count)
{
counter = currentItem + 1;
while(counter < idSet.Count)
{
if(idSet[currentItem].ToUpper() == idSet[counter].ToUpper())
{
noDupes = false;
return noDupes;
}
counter ++;
}
currentItem ++;
}
return noDupes;
Edit:
It was pointed out an option I posted as an answer would always return false so I removed that option and adjusted this option to be more robust and easier to step through logically.
Hope this helps :)

C# How to find out if array includes string twice

i want to check if a string array includes a string more than one time.
for example
string[] array = new string[]{"A1","A2","A3"};
string item = "A1";
look for item in array, include item just once return false
string[] array = new string[]{"A1","A2","A3","A1"};
string item = "A1";
return true
Any insight would be greatly appreciated.
If you want to know whether or not a particular item is repeated more than once, you can get the count of the item and check if it is bigger than 1:
bool isRepeated = array.Count(x => x == item) > 1;
Or, you can do it more efficiently with a HashSet:
bool isRepeated = false;
var set = new HashSet<int>();
foreach(var x in array)
{
if(x == item && !set.Add(x))
{
isRepeated = true;
break;
}
}

how do i pass a single char value to a string variable after increment from a linQ record data

internal char SubMeasurement = 'a';
internal string GetLast;
private void CreateSub()
{
SFCDataContext SFC = new SFCDataContext();
try
{
var CheckRecordSub = SFC.Systems_SettingsMeasurements.Where(r => r.RelationData == txtNO.Text)
.Select(t => new { CODE = t.No });
int count = 0; int total = 0;
string[] row = new string[CheckRecordSub.Count()];
foreach (var r in CheckRecordSub)
{
row[count] = r.CODE;
GetLast = r.CODE;
count++;
total = count;
}
if (txtNO.Text == GetLast)
{
MessageBox.Show(SubMeasurement.ToString()); <-- Msg Box doesn't Work
}
else
{
SubMeasurement = Convert.ToChar(GetLast);
SubMeasurement++; <-- Error
MessageBox.Show(SubMeasurement.ToString()); <-- Msg Box doesn't Work
}
}
catch (Exception) { }
}
I have a record data which is when a user tries to pick a "Sub" option instead of a "Header" the process tries to check for a record last sub record and takes that record put it in a char and increment it and then place it back to a string variable at this code i just use messagebox to check if i get the last record and if it increments it if its a Header it just take the default value of the "SubMeasurement" 'a' for a start. but its not working that way please help.
It's error of course it is. Because char can not increase in normal, if you really need it let try some kind of difference:
int asciiCode = ((int)SubMeasurement) + 1;
SubMeasurement = (char)asciiCode;

Categories