C# Read string from CSV file and plot line graph - c#

Currently, I am able to read data from multiple CSV file and plot line graph using windows form application. However, now I need to plot a line graph based on a CSV file's section name (3rd column of csv file).
Modified/New CSV file: (Added the Section Name column)
Values,Sector,Name
5.55,1024,red
5.37,1536,red
5.73,2048,blue
5.62,2560,.blue
5.12,3072,.yellow
...
Based on the Section Name column, my line graph need to be plotted accordingly in a Single line and different sections must be plotted with different colors, including the legends shown at the side of the graph must be shown based on the different section names.
1 csv file = 1 Series. But there are same section names in a csv file (csv file example shown above, e.g. red for the 1st 20lines). Same section names = same color. If I open 2 or more csv files = 2 Series. Each Series will have different colors due to different section names in the csv file.
I am quite new with programming, and would really appreciate someone could help me by editing from my code.
Thank you.

Updated code:
GraphDemo (Form):
List<Read> rrList = new List<Read>();
void openToolStripMenuItem_Click(object sender, EventArgs e)
{
OpenFileDialog ff = new OpenFileDialog();
Read rr;
ff.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); //"C:\\";
ff.Filter = "csv files (*.csv)|*.csv|All files (*.*)|*.*";
ff.Multiselect = true;
ff.FilterIndex = 1;
ff.RestoreDirectory = true;
if (ff.ShowDialog() == DialogResult.OK)
{
try
{
rrList.Clear();
foreach (String file in ff.FileNames) //if ((myStream = ff.OpenFile()) != null)
{
rr = new Read(file);
rrList.Add(rr);
}
//Populate the ComboBoxes
if (rrList.Count > 0)
{
string[] header = rrList[0].header; //header of first file
xBox.DataSource = header;
yBox.DataSource = header.Clone(); //without Clone the 2 comboboxes link together!
}
if (yBox.Items.Count > 1) yBox.SelectedIndex = 1; //select second item
}
catch (Exception err)
{
//Inform the user if we can't read the file
MessageBox.Show(err.Message);
}
}
}
private void button1_Click(object sender, EventArgs e)
{
Plot.Draw(rrList, xBox, yBox, chart);
}
class Read:
public class Read
{
public int nLines { get; private set; }
public int nColumns { get; private set; }
public string[] header { get; private set; }
public float[,] data { get; private set; }
public string fileName { get; set; }
public string[] section { get; private set; }
public Read(string file)
{
string[] pieces;
fileName = Path.GetFileName(file);
string[] lines = File.ReadAllLines(file); // read all lines
if (lines == null || lines.Length < 2) return; //no data in file
header = lines[0].Split(','); //first line is header
nLines = lines.Length - 1; //first line is header
nColumns = header.Length;
//read the numerical data and section name from the file
data = new float[nLines, nColumns - 1]; // *** 1 less than nColumns as last col is sectionName
section = new string[nLines]; // ***
for (int i = 0; i < nLines; i++)
{
pieces = lines[i + 1].Split(','); // first line is header
if (pieces.Length != nColumns) { MessageBox.Show("Invalid data at line " + (i + 2) + " of file " + fileName); return; }
for (int j = 0; j < nColumns - 1; j++)
{
float.TryParse(pieces[j], out data[i, j]); //data[i, j] = float.Parse(pieces[j]);
}
section[i] = pieces[nColumns - 1]; //last item is section
}
}
}
class Plot:
public class Plot
{
//public Plot() { } //no constructor required as we use a static class to be called
public static void Draw(List<Read> rrList, ComboBox xBox, ComboBox yBox, Chart chart) //***
{
int indX = xBox.SelectedIndex;
int indY = yBox.SelectedIndex;
chart.Series.Clear(); //ensure that the chart is empty
chart.Legends.Clear();
Legend myLegend = chart.Legends.Add("myLegend");
myLegend.Title = "myTitle";
//define a set of colors to be used for sections
Color[] colors = new Color[] { Color.Black, Color.Blue, Color.Red, Color.Green, Color.Magenta, Color.DarkCyan, Color.Chocolate, Color.DarkMagenta };
//use a Dictionary to keep iColor of each section
// key=sectionName, value=iColor (color index in our colors array)
var sectionColors = new Dictionary<string, int>();
int i = 0;
int iColor = -1, maxColor = -1;
foreach (Read rr in rrList)
{
float[,] data = rr.data;
int nLines = rr.nLines;
int nColumns = rr.nColumns;
string[] header = rr.header;
chart.Series.Add("Series" + i);
chart.Series[i].ChartType = SeriesChartType.Line;
//chart.Series[i].LegendText = rr.fileName;
chart.Series[i].IsVisibleInLegend = false; //hide default item from legend
chart.ChartAreas[0].AxisX.LabelStyle.Format = "{F2}";
chart.ChartAreas[0].AxisX.Title = header[indX];
chart.ChartAreas[0].AxisY.Title = header[indY];
for (int j = 0; j < nLines; j++)
{
int k = chart.Series[i].Points.AddXY(data[j, indX], data[j, indY]);
string curSection = rr.section[j];
if (sectionColors.ContainsKey(curSection))
{
iColor = sectionColors[curSection];
}
else
{
maxColor++;
iColor = maxColor; sectionColors[curSection] = iColor;
}
chart.Series[i].Points[k].Color = colors[iColor];
}
i++; //series#
} //end foreach rr
//fill custom legends based on sections/colors
foreach (var x in sectionColors)
{
string section = x.Key;
iColor = x.Value;
myLegend.CustomItems.Add(colors[iColor], section); //new LegendItem()
}
}
}

You can separate the data by the section column and use the section names as index into the Series collection instead of using i.
Best use the section name as the Series.Name. I suggest using a data class containing the two numbers and the string and collect them in a List<Dataclass>. Then create Series for the distinct sections. Then loop over them..
Here are a few code examples:
Define a class for your data:
public class Data3
{
public int N1 { get; set;}
public double N2 { get; set;}
public string S1 { get; set;}
public Data3(double n2, int n1, string s1)
{
N1 = n1; N2 = n2; S1 = s1;
}
}
Pick your own names! Optional but always recommended: Add a nice ToString() overload!
Declare a class level varible:
List<Data3> data = new List<Data3>();
During the read collect the data there:
data.Add(new Data3(Convert.ToDouble(pieces[1]), Convert.ToInt32(pieces[0]), pieces[2]));
To plot the chart first create the Series:
var sections= data.Select(x => x.S1).Distinct<string>();
foreach (string s in sections)
chart.Series.Add(new Series(s) { ChartType = SeriesChartType.Line });
Then plot the data; the series can be indexed by their Names:
foreach (var d in data) chart.Series[d.S1].Points.AddXY(d.N1, d.N2);
I left out the nitty gritty of integrating the code into your application; if you run into issues, do show the new code by editing your question!
A few notes:
When in doubt always create a class to hold your data
When in doubt always choose classes over structures
When in doubt always choose List<T> over arrays
Always try to break your code down to small chunks with helpful names.
Example: To read all the data in a csv file create a function to do so:
public void AppendCsvToDataList(string file, List<Data3> list)
{
if (File.Exists(file))
{
var lines = File.ReadAllLines(file);
for (int l = 1; l < lines.Length; l++)
{
var pieces = lines[l].Split(',');
list.Add(new Data3(Convert.ToInt32(pieces[1]),
Convert.ToDouble(pieces[0]), pieces[2]));
}
}
}

Related

How could I avoid instantiating using the new keyword?

I'm just a student trying to get better.
Right now I am developing an application which reads from Excel. For that I have a class, DataReader.
public class DataReader : IDataReader
{
Workbook workbook;
public DataReader()
{
workbook = Globals.Factory.GetVstoObject(Globals.ThisAddIn.Application.ActiveWorkbook);
}
public Worksheet GetWorksheetByName(string name)
{
Worksheet sheet = (Worksheet)Globals.Factory.GetVstoObject(workbook.Worksheets[name]);
return sheet;
}
}
and this class I am calling from multiple places. Here's an example of how I'm I'm instantiating it in one class
public List<ConfigModel> Instances { get; set; }
public IDataReader dr;
public Worksheet sheet;
public PopulateConfigModel()
{
Instances = new List<ConfigModel>();
dr = new DataReader();
sheet = dr.GetWorksheetByName("Your_Data_Sheet");
}
and here is another example
public class RangeCreator : IRangeCreator
{
IDataReader dr;
Worksheet sheet;
public Xcl.Range GetDestinationRange()
{
dr = new DataReader();
DataHandler dh = new DataHandler();
sheet = dr.GetWorksheetByName("Your_CSV_File");
string range = "A" + ((dh.GetLastRow(sheet) + 1) + ":A" + (dh.GetLastRow(sheet) + 3));
Xcl.Range rng = sheet.Range[range];
return rng;
}
Note that I also calls new DataHandler in that method.
I don't think this is good practice but I don't have a better solution so what's the work around for instantiating this class multiple places? I guess I could go with DI but then I'd have to DI both the DataReader and DataHandler in multiple places and I'm not sure that's a better solution.
I don't know,
UPDATE:
public void BeginProcess()
{
dh = new DataHandler1();
//Returns a List<List<int>> with all used boxsizes {{ 5,1 }, { 15, 3 }} & startCell for each of those boxes eg {{ "B6" }, { "B27" }}
ListHandler lh = dh.GetListHandlerData();
//Holds a list of instances
IPopulateConfigModel pc = new PopulateConfigModel();
//For hver boks vi har i dokumentet
for (int i = 0; i < lh.cellAddresses.Count; i++)
{
//Adds list of instances to list of models
models.Add(pc.PopulateInstances(lh.boxSizes[i], lh.cellAddresses[i]));
}
ConfigModelHandler cm = new ConfigModelHandler(models);
cm.StartModelProcessing(dh);
}
Now I am injecting the DataHandler as a parameter in StartModelProcessing
public void StartModelProcessing(IDataHandler1 dh)
{
IFileSaver fileSaver = new FileSaver();
Worksheet sheet = dr.GetWorksheetByName("Your_CSV_File");
//Each model - each with their own sheet
for (int i = 0; i < models.Count; i++)
{
//Contains all instances of an object
string[][] instances = new string[models[i].Count][];
string filePath = Directory.GetCurrentDirectory() + #"\" + models[i][0].TemplateName + ".csv";
//Clears our sheet so we're ready for a new set of instances
dh.ClearWorksheet(sheet);
//Each instance - sharing their worksheet
for (int j = 0; j < models[i].Count; j++)
{
Xcl.Range rngPopulated = GetPopulatedSourceRange(models[i][j]);
string[] rows = new string[3];
//Get our data from our populated range to our string array. It's in the form of:
/*
* one string array = one instance
* rows[0] = TEMPLATE
* rows[1] = Properties
* rows[2] = Variables
*/
for (int ti = 0; ti < rngPopulated.Rows.Count; ti++)
{
for (int tu = 1; tu < rngPopulated.Rows[ti + 1].Cells.Count; tu++)
{
rows[ti] += rngPopulated.Rows[ti + 1].Cells[tu].Value2 + ",";
}
}
instances[j] = rows;
Xcl.Range testRange = rangeGetter.GetDestinationRange(dh);
//We aint using this sheet - it's only if the user will change and save it manually but only last instance of last document is showing
di.PopulateCsvFile(rows, testRange);
}
fileSaver.SaveFile(instances, filePath);
}
}
and as you can see, injecting it as a parameter again in the rangeGetter.GetDestRange method.
and here's the method
public Xcl.Range GetDestinationRange(IDataHandler1 dh)
{
dr = new DataReader1();
sheet = dr.GetWorksheetByName("Your_CSV_File");
string range = "A" + ((dh.GetLastRow(sheet) + 1) + ":A" + (dh.GetLastRow(sheet) + 3));
Xcl.Range rng = sheet.Range[range];
return rng;
}
One way is to make DataReader class as singleton pattern like:
class DataReader
{
private static DataReader instance = null;
private DataReader()
{
//workbook goes here...
}
public static DataReader Instance
{
get
{
if (instance == null)
instance = new DataReader();
return instance;
}
}
}
And then you can call the method like:
...
sheet = DataReader.Instance.GetWorksheetByName("Your_CSV_File");
Here you not need to use the new keyword every time
please understand
this is very "sudo code" and just here to illiterate that what is more important than trying to avoid "new" is how code is consumed by other code.
your code is hard to comment on as the whole picture cannot be seen... (what belongs to what)
If you don't understand what I'm trying to show below then i will delete, but if you do then great! I have ommited a lot of code to make it easier to understand... well hopefully
class Main
{
DataReader1 _dr;
DataHandler _dh;
public Main()
{
_dr = new DataReader1();
_dh = new DataHandler();
BeginProcess();
}
public void BeginProcess()
{
//...code ommitted
ConfigModelHandler cm = new ConfigModelHandler(models);
cm.StartModelProcessing();
}
public void StartModelProcessing(IDataHandler1 dh)
{
Worksheet sheet = dr.GetWorksheetByName("Your_CSV_File");
//Each model - each with their own sheet
for (int i = 0; i < models.Count; i++)
{
for (int j = 0; j < models[i].Count; j++)
{
Worksheet sheetInner = dr.GetWorksheetByName("Your_CSV_File");
//do stuff with sheet
var range = GetDestinationRange(sheetInner);
}
fileSaver.SaveFile(instances, filePath);
}
}
public Xcl.Range GetDestinationRange(Worksheet sheet)
{
var lastRow = dh.GetLastRow(sheet);
string range = "A" + ((lastRow + 1) + ":A" + (lastRow + 3));
Xcl.Range rng = sheet.Range[range];
return rng;
}
}

Applying Algorithm to multiple files

I'm currently working on a project where I have to choose a file and then sort it along with other files. The files are just filled with numbers but each file has linked data. So the first number in a file is linked to the first number in the second file and so on. I Currently have code that allows me to read a file and display the file unsorted and sorted using the bubble sort. I am not sure how I would be able to apply this same principle to multiple files at once. So that I could choose a file and then sort it in line with the same method I have for a single file.
Currently, the program loads and asks the user to choose between 1 and 2. 1 Shows the code unsorted and 2 shows the code sorted. I can read in the files but the problem is sorting and displaying in order. Basically How do I sort multiple files that are linked together. What steps do I need to take to do this?
An example of one file:
4
28
77
96
An example of the second file:
66.698
74.58
2.54
48.657
Any help would be appreciated.
Thanks
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
public class Program
{
public Program()
{
}
public void ReadFile(int [] numbers)
{
string path = "Data1/Day_1.txt";
StreamReader sr = new StreamReader(path);
for (int index = 0; index < numbers.Length; index++)
{
numbers[index] = Convert.ToInt32(sr.ReadLine());
}
sr.Close(); // closes file when done
}
public void SortArray(int[] numbers)
{
bool swap;
int temp;
do
{
swap = false;
for (int index = 0; index < (numbers.Length - 1); index++)
{
if (numbers[index] > numbers[index + 1])
{
temp = numbers[index];
numbers[index] = numbers[index + 1];
numbers[index + 1] = temp;
swap = true;
}
}
} while (swap == true);
}
public void DisplayArray(int[] numbers)
{
for(int index = 0; index < numbers.Length; index++)
{
Console.WriteLine("{0}", numbers[index]);
}
}
}
The main is in another file to keep work organised:
using System;
public class FileDemoTest
{
public static void Main(string[] args)
{
int[] numbers = new int[300];
Program obj = new Program();
int operation = 0;
Console.WriteLine("1 or 2 ?");
operation = Convert.ToInt32(Console.ReadLine());
// call the read from file method
obj.ReadFile(numbers);
if (operation == 1)
{
//Display unsorted values
Console.Write("Unsorted:");
obj.DisplayArray(numbers);
}
if (operation == 2)
{
//sort numbers and display
obj.SortArray(numbers);
Console.Write("Sorted: ");
obj.DisplayArray(numbers);
}
}
}
What I would do is create a class that will hold the values from file 1 and file 2. Then you can populate a list of these classes by reading values from both files. After that, you can sort the list of classes on either field, and the relationships will be maintained (since the two values are stored in a single object).
Here's an example of the class that would hold the file data:
public class FileData
{
public int File1Value { get; set; }
public decimal File2Value { get; set; }
/// <summary>
/// Provides a friendly string representation of this object
/// </summary>
/// <returns>A string showing the File1 and File2 values</returns>
public override string ToString()
{
return $"{File1Value}: {File2Value}";
}
}
Then you can create a method that reads both files and creates and returns a list of these objects:
public static FileData[] GetFileData(string firstFilePath, string secondFilePath)
{
// These guys hold the strongly typed version of the string values in the files
int intHolder = 0;
decimal decHolder = 0;
// Get a list of ints from the first file
var fileOneValues = File
.ReadAllLines(firstFilePath)
.Where(line => int.TryParse(line, out intHolder))
.Select(v => intHolder)
.ToArray();
// Get a list of decimals from the second file
var fileTwoValues = File
.ReadAllLines(secondFilePath)
.Where(line => decimal.TryParse(line, out decHolder))
.Select(v => decHolder)
.ToArray();
// I guess the file lengths should match, but in case they don't,
// use the size of the smaller one so we have matches for all items
var numItems = Math.Min(fileOneValues.Count(), fileTwoValues.Count());
// Populate an array of new FileData objects
var fileData = new FileData[numItems];
for (var index = 0; index < numItems; index++)
{
fileData[index] = new FileData
{
File1Value = fileOneValues[index],
File2Value = fileTwoValues[index]
};
}
return fileData;
}
Now, we need to modify your sorting method to work on a FileData array instead of an int array. I also added an argument that, if set to false, will sort on the File2Data field instead of File1Data:
public static void SortArray(FileData[] fileData, bool sortOnFile1Data = true)
{
bool swap;
do
{
swap = false;
for (int index = 0; index < (fileData.Length - 1); index++)
{
bool comparison = sortOnFile1Data
? fileData[index].File1Value > fileData[index + 1].File1Value
: fileData[index].File2Value > fileData[index + 1].File2Value;
if (comparison)
{
var temp = fileData[index];
fileData[index] = fileData[index + 1];
fileData[index + 1] = temp;
swap = true;
}
}
} while (swap);
}
And finally, you can display the non-sorted and sorted lists of data. Note I added a second question to the user if they choose "Sorted" where they can decide if it should be sorted by File1 or File2:
private static void Main()
{
Console.WriteLine("1 or 2 ?");
int operation = Convert.ToInt32(Console.ReadLine());
var fileData = GetFileData(#"f:\public\temp\temp1.txt", #"f:\public\temp\temp2.txt");
if (operation == 1)
{
//Display unsorted values
Console.WriteLine("Unsorted:");
foreach (var data in fileData)
{
Console.WriteLine(data);
}
}
if (operation == 2)
{
Console.WriteLine("Sort on File1 or File2 (1 or 2)?");
operation = Convert.ToInt32(Console.ReadLine());
//sort numbers and display
SortArray(fileData, operation == 1);
Console.WriteLine("Sorted: ");
foreach (var data in fileData)
{
Console.WriteLine(data);
}
}
Console.Write("\nDone!\nPress any key to exit...");
Console.ReadKey();
}
If you wanted to use an int to decide which field to sort on, you could do something like the following:
public static void SortArray(FileData[] fileData, int sortFileNumber = 1)
{
bool swap;
do
{
swap = false;
for (int index = 0; index < (fileData.Length - 1); index++)
{
bool comparison;
// Set our comparison to the field sortFileNumber
if (sortFileNumber == 1)
{
comparison = fileData[index].File1Value > fileData[index + 1].File1Value;
}
else if (sortFileNumber == 2)
{
comparison = fileData[index].File2Value > fileData[index + 1].File2Value;
}
else // File3Value becomes default for anything else
{
comparison = fileData[index].File3Value > fileData[index + 1].File3Value;
}
if (comparison)
{
var temp = fileData[index];
fileData[index] = fileData[index + 1];
fileData[index + 1] = temp;
swap = true;
}
}
} while (swap);
}

Reading text file until a space and storing several values

I am creating an application to read and display Pokemon stats. Currently, I have 6 txt files, one for each stat. I have 6 arrays reading each txt file and displaying each stat in a label.
I want to condense this information into a single text file, splitting each stat by a " " to keep each Pokemon's data on a single line.
Current code if it helps explain the idea better:
using System;
using System.Windows.Forms;
using System.IO;
namespace Pokedex
{
public partial class Pokedex : Form
{
public Pokedex()
{
InitializeComponent();
}
//Read stat data from text files into string arrays
string[] HP = File.ReadAllLines("HP.txt");
string[] Atk = File.ReadAllLines("Atk.txt");
string[] Def = File.ReadAllLines("Def.txt");
string[] SpAtk = File.ReadAllLines("SpAtk.txt");
string[] SpDef = File.ReadAllLines("SpDef.txt");
string[] Spe = File.ReadAllLines("Spe.txt");
private void cbxPokemon_SelectedIndexChanged(object sender, EventArgs e)
{
//Get array index of currently selected Pokemon
int index = cbxPokemon.SelectedIndex;
//Get integer values out of the string arrays for appropriate use
int intHP = int.Parse(HP[index]);
int intAtk = int.Parse(Atk[index]);
int intDef = int.Parse(Def[index]);
int intSpAtk = int.Parse(SpAtk[index]);
int intSpDef = int.Parse(SpDef[index]);
int intSpe = int.Parse(Spe[index]);
//Update labels with stat values in string forme, could also assign intStat.ToString()
lblDexNum.Text = (index + 1).ToString("d3");
lblHP.Text = HP[index];
lblAtk.Text = Atk[index];
lblDef.Text = Def[index];
lblSpAtk.Text = SpAtk[index];
lblSpDef.Text = SpDef[index];
lblSpe.Text = Spe[index];
lblBST.Text = (intHP + intAtk + intDef + intSpAtk + intSpDef + intSpe).ToString();
//Update bar width based on stat value
barHP.Width = intHP;
barAtk.Width = intAtk;
barDef.Width = intDef;
barSpAtk.Width = intSpAtk;
barSpDef.Width = intSpDef;
barSpe.Width = intSpe;
//Disable Previous and Next buttons when they cannot be used
if (index == 0) { btnPrev.Enabled = false; }
else { btnPrev.Enabled = true; }
if (index == cbxPokemon.Items.Count - 1) { btnNext.Enabled = false; }
else { btnNext.Enabled = true; }
}
private void btnPrev_Click(object sender, EventArgs e)
{
cbxPokemon.SelectedIndex -= 1;
}
private void btnNext_Click(object sender, EventArgs e)
{
cbxPokemon.SelectedIndex += 1;
}
}
}
Ideally, the file would contain the 6 stats, for example "100 90 80 70 60 50" on each line, with each of those valued being stored.
Is there a simple way to read until a space, store that value, and keep doing that until the end of the line?
Yes, this is very simple to do. You can use the String.Split() method to break apart the lines into parsable pieces.
You can read the stats file in the same way you are now, except you would only need to read one file instead of six:
string[] lines = File.ReadAllLines("PokemonStats.txt");
Then, inside your cbxPokemon_SelectedIndexChanged method, you can retrieve the stats for a Pokemon like this:
//Get integer values out of the string for appropriate use
string line = lines[index];
string[] parts = line.Split(' ');
int intHP = int.Parse(parts[0]);
int intAtk = int.Parse(parts[1]);
int intDef = int.Parse(parts[2]);
int intSpAtk = int.Parse(parts[3]);
int intSpDef = int.Parse(parts[4]);
int intSpe = int.Parse(parts[5]);
Of course this assumes there will always be exactly 6 integer stats per line and they will all be in a specific order.
Try something like this:
static void Main(string[] args)
{
//list to store stats
List<string[]> pokemonStats = new List<string[]>();
//get a reader on the file
using (StreamReader reader = new StreamReader("TextFile1.txt"))
{
//while we still have lines to read
while (!reader.EndOfStream)
{
//get the line of stats
string line = reader.ReadLine();
//split it on the ' ' character and store it in our list of pokemon stats
pokemonStats.Add(line.Split(' '));
}
}
//we have them all so do something, like print to screen
foreach (string[] pokemon in pokemonStats)
{
foreach (string stat in pokemon)
Console.Write(stat + " ");
Console.WriteLine();
}
Console.ReadLine();
}
Where TextFile1.txt contains; which also happens to be the output...
1 1 1 1 1 1
2 2 2 2 2 2
3 3 3 3 3 3
Ideally, the file would contain the 6 stats, for example "100 90 80 70 60 50" on each line, with each of those valued being stored.
So this solution is actually very simple, assuming you have a file that looks like this:
100
90
80
70
60
50
You could write code that looks like this:
public string[] ReadStats(string fileName)
{
return File.ReadAllLines(fileName);
}
This would return:
[ "100", "90", "80", "70", "60", "50" ]
This is obviously an over-simplified version that does not checking on whether the file exists, whether there are more than 6 lines, etc. But it gets the idea across, and you could enhance it to suit your needs.
The answer to your main question:
Is there a simple way to read until a space, store that value, and keep doing that until the end of the line?
This is a bit more complicated, but this should work:
public IEnumerable<string[]> ReadAllPokemonStats(string fileName)
{
List<string[]> allPokemon = new List<string[]>();
string[] allStats = new string[6];
var allText = File.ReadAllText(fileName);
int nextStatIndex = 0;
string thisStat;
for(int i=0; i < allText.Length; i++)
{
var nextChar = allText[i];
if(nextChar == ' ')
{
allStats[nextStatIndex] = thisStat;
nextStatIndex++;
continue;
}
if(nextChar == '\r')
{
allPokemon.Add(allStats);
nextStatIndex = 0;
allStats = new string[6];
continue;
}
thisStat += nextChar.ToString();
}
return allPokemon;
}
Again, the above code is not perfect, for example it reads the entire file into memory so there's a potential attack vector there. It also does not ensure that the stats are actually numeric, but neither did your code (it would just blow up on the int.Parse()). But again, it gives you the general idea. You can see this approach is actually a lot more complex that reading each stat line by line.
I am thinking that each file is a one column file that represents one type of stat. If this is correct, you can try this:
Perhaps creating a class the represents all the stats that you are loading and then treat that as an array or a dictionary to fetch the proper stat or to create a single stat file.
public class PokemonStat
{
public int Hp { get; set; }
public int Atk { get; set; }
public int Def { get; set; }
public int SpAtk { get; set; }
public int SpDef { get; set; }
public int Spe { get; set; }
}
Then in your executing file:
var newPokemanData = new Dictionary<int, PokemonStat>();
var fileNames = new string[] { "Hp.txt", "Atk.txt", "Def.txt", "SpAtk.txt", "SpDef.txt", "Spe.txt" }
foreach (var fileName in fileNames)
{
var lineNumber = 0;
using (var stream = new StreamReader(fileName))
{
while (!stream.EndOfStream)
{
var singleStat = stream.ReadLine();
if (!newPokemanData.Keys.Contains(lineNumber))
{
newPokemanData.Add(lineNumber, new PokemonStat());
}
switch(fileName)
{
case "Hp.txt":
newPokemanData[lineNumber].Hp = int.Parse(singleStat);
break;
case "Atk.txt":
newPokemanData[lineNumber].Atk = int.Parse(singleStat);
break;
case "Def.txt":
newPokemanData[lineNumber].Def = int.Parse(singleStat);
break;
case "SpAtk.txt":
newPokemanData[lineNumber].SpAtk = int.Parse(singleStat);
break;
case "SpDef.txt":
newPokemanData[lineNumber].SpDef = int.Parse(singleStat);
break;
case "Spe.txt":
newPokemanData[lineNumber].Spe = int.Parse(singleStat);
break;
default:
Console.WriteLine("Error");
break;
}
lineNumber++;
}
}
}
using (var unifiedStats = new StreamWriter("unifieldFile.txt"))
{
foreach (var line in newPokemanData.Keys)
{
//write to a file
unifiedStats.WriteLine(newPokemanData[line].Hp.ToString() + " " +
newPokemanData[line].Atk.ToString() + " " +
newPokemanData[line].Def.ToString() + " " +
newPokemanData[line].SpAtk.ToString() + " " +
newPokemanData[line].SpDef.ToString() + " " +
newPokemanData[line].Spe.ToString() + " "
);
}
}
//

C# Create high score text file and reorder the results

I'm trying to create a text file containing the players' scores of a hangman game. The structure of the text file should follow the order: number. name score (e.g. 1. Helen 2500). I tried to split the line so that I can introduce the data into a specific array for name and score so that I can compare the results and reorder them (the numbers remain the same: 1, 2, 3, etc.) but it doesn't work. I don't get errors but the building stops at this function because of an incorrect use of the array v[]. What do you suggest me to do to make it work?
[code]
private void New_Score(int score)
{
int k=0, i;
char[] sep = new char[] { ' ', '\n', '.' };
string line, aux1, aux2;
string n=null, s=null;
n = textBox1.Text;
s = Convert.ToString(score);
string[] part=null, nr=null, name=null, result=null;
file_path = #"D:\Visual Studio 2005\Projects\WindowsApplication2\WindowsApplication2\Resources\HighScore.txt";
StreamReader f = new StreamReader(file_path);
while ((line = f.ReadLine()) != null)
{
part = null;
v = null;
part = line.Split(sep);
i=0;
foreach(string c in part)
{
v[i]= c;
i++;
}
nr[k] = v[0];
name[k] = v[1];
result[k] = v[2];
}
for (i = 0; i < k; i++)
if (string.CompareOrdinal(s,result[i]) == 1)
{
aux1 = s;
s = result[i];
result[i] = aux1;
aux2 = n;
n = name[i];
name[i] = aux2;
}
for (i = 0; i < k; i++)
{
line = nr[i] + ". " + name[i] + " " + result[i] + "\n";
File.WriteAllText(file_path, line);
}
}
[/code]
I would personally abstract the code a bit more, but if you don't want to add any more classes or really do anything outside of the method, here's what I'd do:
Parse high scores in as a List<Tuple<int, string, int>>
Add the new score
Use either list.Sort() or LINQ to sort the list
Write the list out to the file.
It's a lot cleaner and way more readable than what you have in the question.
Despite the fact this goes entirely against the saying about fishing and eating which I strongly support, I took the liberty of making a few improvements by completely rewriting your code.
First of all, I got rid of storing the position of the player in the text file. This isn't efficient, since when you add a player with the highest score (rendering him #1), you'll have to re-number everyone else that's present in the file at that point.
So the resulting file looks like this:
Foo 123
Qux 714
Bar 456
Baz 999
The main() method looks like this:
var scores = ReadScoresFromFile("Highscores.txt");
scores.ForEach(s => Console.WriteLine(s));
Console.ReadKey();
Then there's the Highscore class:
class Highscore
{
public String Name { get; set; }
public int Position { get; set; }
public int Score { get; set; }
public Highscore(String data)
{
var d = data.Split(' ');
if (String.IsNullOrEmpty(data) || d.Length < 2)
throw new ArgumentException("Invalid high score string", "data");
this.Name = d[0];
int num;
if (int.TryParse(d[1], out num))
{
this.Score = num;
}
else
{
throw new ArgumentException("Invalid score", "data");
}
}
public override string ToString()
{
return String.Format("{0}. {1}: {2}", this.Position, this.Name, this.Score);
}
}
You see a Highscore populates itself based on the line from the Highscore file it's fed. I populate the list of scores using this method:
static List<Highscore> ReadScoresFromFile(String path)
{
var scores = new List<Highscore>();
using (StreamReader reader = new StreamReader(path))
{
String line;
while (!reader.EndOfStream)
{
line = reader.ReadLine();
try
{
scores.Add(new Highscore(line));
}
catch (ArgumentException ex)
{
Console.WriteLine("Invalid score at line \"{0}\": {1}", line, ex);
}
}
}
return SortAndPositionHighscores(scores);
}
And finally some sorting and position assigment:
static List<Highscore> SortAndPositionHighscores(List<Highscore> scores)
{
scores = scores.OrderByDescending(s => s.Score).ToList();
int pos = 1;
scores.ForEach(s => s.Position = pos++);
return scores.ToList();
}
Resulting in:
1. Baz: 999
2. Qux: 714
3. Bar: 456
4. Foo: 123
There is no reason to store number, line position can serve to that purpose. Even better would be serialize List with score objects to for example XML (if you want to keep human readable score file), to avoid line parsing. But if you want to store to plain text here is a simple example :
private void New_Score(int score, string name)
{
string filename = "scores.txt";
List<string> scoreList;
if (File.Exists(filename))
scoreList = File.ReadAllLines(filename).ToList();
else
scoreList = new List<string>();
scoreList.Add(name + " " + score.ToString());
var sortedScoreList = scoreList.OrderByDescending(ss => int.Parse(ss.Substring(ss.LastIndexOf(" ") + 1)));
File.WriteAllLines(filename, sortedScoreList.ToArray());
}
And later when displaying results add order number in front, something like this:
int xx = 1;
List<string> scoreList = File.ReadAllLines(filename).ToList();
foreach (string oneScore in scoreList)
{
Console.WriteLine(xx.ToString() + ". " + oneScore);
xx++;
}
Seems like a complicated way to store a simple list of high scores. Why don't you try the following.
Define a simple object to hold a player's score.
[Serializable]
public class HighScore
{
public string PlayerName { get; set; }
public int Score { get; set; }
}
Make sure you mark it with the [Serializable] attribute.
Let's quickly create a list of high scores for a couple of players.
var highScores = new List<HighScore>()
{
new HighScore { PlayerName = "Helen", Score = 1000 },
new HighScore { PlayerName = "Christophe", Score = 2000 },
new HighScore { PlayerName = "Ruben", Score = 3000 },
new HighScore { PlayerName = "John", Score = 4000 },
new HighScore { PlayerName = "The Last Starfighter", Score = 5000 }
};
Now you can use the BinaryFormatter to serialize the scores and save them to a local file.
using (var fileStream = new FileStream(#"C:\temp\scores.dat", FileMode.Create, FileAccess.Write))
{
var formatter = new BinaryFormatter();
formatter.Serialize(fileStream, highScores);
}
Later you can load the high scores from this files in a similar manner.
using (var fileStream = new FileStream(#"C:\temp\scores.dat", FileMode.Open, FileAccess.Read))
{
var formatter = new BinaryFormatter();
highScores = (List<HighScore>) formatter.Deserialize(fileStream);
}
If you want to sort them you can implement the IComparable interface on the HighScore type.
[Serializable]
public class HighScore : IComparable
{
//...
public int CompareTo(object obj)
{
var otherScore = (HighScore) obj;
if (Score == otherScore.Score)
return 0;
if (Score < otherScore.Score)
return 1;
return -1;
}
}
Now you can just call the Sort(...) on your generic List collection.
highScores.Sort();
And voila the scores are sorted in a descending order.
foreach(var score in highScores)
{
Console.WriteLine(String.Format("{0}: {1} points", score.PlayerName, score.Score));
}
Or even more simple, just use LINQ to sort the high scores.
var sortedScores = highScores.OrderByDescending(s => s.Score).ToList();

getting the best record from a file

I have a file with the following text inside
mimi,m,70
tata,f,60
bobo,m,100
soso,f,30
I did the reading from file thing and many many other methods and functions, but how I can get the best male name and his grade according to the grade.
here is the code I wrote. Hope it's not so long
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
namespace practice_Ex
{
class Program
{
public static int[] ReadFile(string FileName, out string[] Name, out char[] Gender)
{
Name = new string[1];
int[] Mark = new int[1];
Gender = new char[1];
if (File.Exists(FileName))
{
FileStream Input = new FileStream(FileName, FileMode.Open, FileAccess.Read);
StreamReader SR = new StreamReader(Input);
string[] Current;
int Counter = 0;
string Str = SR.ReadLine();
while (Str != null)
{
Current = Str.Split(',');
Name[Counter] = Current[0];
Mark[Counter] = int.Parse(Current[2]);
Gender[Counter] = char.Parse(Current[1].ToUpper());
Counter++;
Array.Resize(ref Name, Counter + 1);
Array.Resize(ref Mark, Counter + 1);
Array.Resize(ref Gender, Counter + 1);
Str = SR.ReadLine();
}
}
return Mark;
}
public static int MostFreq(int[] M, out int Frequency)
{
int Counter = 0;
int Frequent = 0;
Frequency = 0;
for (int i = 0; i < M.Length; i++)
{
Counter = 0;
for (int j = 0; j < M.Length; j++)
if (M[i] == M[j])
Counter++;
if (Counter > Frequency)
{
Frequency = Counter;
Frequent = M[i];
}
}
return Frequent;
}
public static int Avg(int[] M)
{
int total = 0;
for (int i = 0; i < M.Length; i++)
total += M[i];
return total / M.Length;
}
public static int AvgCond(char[] G, int[] M, char S)
{
int total = 0;
int counter = 0;
for (int i = 0; i < G.Length; i++)
if (G[i] == S)
{
total += M[i];
counter++;
}
return total / counter;
}
public static int BelowAvg(int[] M, out int AboveAvg)
{
int Bcounter = 0;
AboveAvg = 0;
for (int i = 0; i < M.Length; i++)
{
if (M[i] < Avg(M))
Bcounter++;
else
AboveAvg++;
}
return Bcounter;
}
public static int CheckNames(string[] Name, char C)
{
C = char.Parse(C.ToString().ToLower());
int counter = 0;
string Str;
for (int i = 0; i < Name.Length - 1; i++)
{
Str = Name[i].ToLower();
if (Str[0] == C || Str[Str.Length - 1] == C)
counter++;
}
return counter;
}
public static void WriteFile(string FileName, string[] Output)
{
FileStream FS = new FileStream(FileName, FileMode.OpenOrCreate, FileAccess.Write);
StreamWriter SW = new StreamWriter(FS);
for (int i = 0; i < Output.Length; i++)
SW.WriteLine(Output[i]);
}
static void Main(string[] args)
{
int[] Mark;
char[] Gender;
string[] Name;
string[] Output = new string[8];
int Frequent, Frequency, AvgAll, MaleAvg, FemaleAvg, BelowAverage, AboveAverage, NamesCheck;
Mark = ReadFile("c:\\IUST1.txt", out Name, out Gender);
Frequent = MostFreq(Mark, out Frequency);
AvgAll = Avg(Mark);
MaleAvg = AvgCond(Gender, Mark, 'M');
FemaleAvg = AvgCond(Gender, Mark, 'F');
BelowAverage = BelowAvg(Mark, out AboveAverage);
NamesCheck = CheckNames(Name, 'T');
Output [0]= "Frequent Mark = " + Frequent.ToString();
Output [1]= "Frequency = " + Frequency.ToString();
Output [2]= "Average Of All = " + AvgAll.ToString();
Output [3]= "Average Of Males = " + MaleAvg.ToString();
Output [4]= "Average Of Females = " + FemaleAvg.ToString();
Output [5]= "Below Average = " + BelowAverage.ToString();
Output [6]= "Above Average = " + AboveAverage.ToString();
Output [7]= "Names With \"T\" = " + NamesCheck.ToString();
WriteFile("c:\\Output.txt", Output);
}
}
}
Well, I like LINQ (update: excluded via comments) for querying, especially if I can do it without buffering the data (so I can process a huge file efficiently). For example below (update: removed LINQ); note the use of iterator blocks (yield return) makes this fully "lazy" - only one record is held in memory at a time.
This also shows separation of concerns: one method deals with reading a file line by line; one method deals with parsing a line into a typed data record; one (or more) method(s) work with those data record(s).
using System;
using System.Collections.Generic;
using System.IO;
enum Gender { Male, Female, Unknown }
class Record
{
public string Name { get; set; }
public Gender Gender { get; set; }
public int Score { get; set; }
}
static class Program
{
static IEnumerable<string> ReadLines(string path)
{
using (StreamReader reader = File.OpenText(path))
{
string line;
while ((line = reader.ReadLine()) != null)
{
yield return line;
}
}
}
static IEnumerable<Record> Parse(string path)
{
foreach (string line in ReadLines(path))
{
string[] segments = line.Split(',');
Gender gender;
switch(segments[1]) {
case "m": gender = Gender.Male; break;
case "f": gender = Gender.Female; break;
default: gender = Gender.Unknown; break;
}
yield return new Record
{
Name = segments[0],
Gender = gender,
Score = int.Parse(segments[2])
};
}
}
static void Main()
{
Record best = null;
foreach (Record record in Parse("data.txt"))
{
if (record.Gender != Gender.Male) continue;
if (best == null || record.Score > best.Score)
{
best = record;
}
}
Console.WriteLine("{0}: {1}", best.Name, best.Score);
}
}
The advantage of writing things as iterators is that you can easily use either streaming or buffering - for example, you can do:
List<Record> data = new List<Record>(Parse("data.txt"));
and then manipulate data all day long (assuming it isn't too large) - useful for multiple aggregates, mutating data, etc.
This question asks how to find a maximal element by a certain criterion. Combine that with Marc's LINQ part and you're away.
In the real world, of course, these would be records in a database, and you would use one line of SQL to select the best record, ie:
SELECT Name, Score FROM Grades WHERE Score = MAX(Score)
(This returns more than one record where there's more than one best record, of course.) This is an example of the power of using the right tool for the job.
I think the fastest and least-code way would be to transform the txt to xml and then use Linq2Xml to select from it. Here's a link.
Edit: That might be more work than you'd like to do. Another option is to create a class called AcademicRecord that has properties for the persons name gender etc. Then when you read the file, add to a List for each line in the file. Then use a Sort predicate to sort the list; the highest record would then be the first one in the list. Here's a link.
Your assignment might have different requirements, but if you only want to get "best male name and grade" from a file you described, a compact way is:
public String FindRecord()
{
String[] lines = File.ReadAllLines("MyFile.csv");
Array.Sort(lines, CompareByBestMaleName);
return lines[0];
}
int SortByBestMaleName(String a, String b)
{
String[] ap = a.Split();
String[] bp = b.Split();
// Always rank male higher
if (ap[1] == "m" && bp[1] == "f") { return 1; }
if (ap[1] == "f" && bp[1] == "m") { return -1; }
// Compare by score
return int.Parse(ap[2]).CompareTo(int.Parse(bp[2]));
}
Note that this is neither fast nor robust.

Categories