Compare user's input against prefabricated text chain - c#

I am in my first steps towards creating a very basic structural analysis software using Visual C#.
I decided to make it console-based (no user interface). Therefore the only way to get user's input is through chars and strings.
Imagine the user wants to create a 2D bar element. She would need to specify an initial point, a final point and a name for that bar. I want the syntax to be like follows:
"CREATE bar NAMED (bar_name) FIRST (first_point) LAST (last_point)"
Where:
(bar_name) is the name of the bar, up to the user. Let (object_name)="bar_A" (string type).
(first_point) would be the initial point of the bar. Since we are creating a 2D bar, (first_point) should be a 1x2 vector that the user should enter between parenthesis. For example, (first_point)=(0,0)
(last_point) would be the final point of the bar. Same type and syntax as (first_point).
I am just wondering if there is any easy way to achieve the string comparison task, something like comparing the user's input against a prefabricated command.
Of course without forgetting about user's input cleaning task.
I know there is a huge amount of possible solutions here. Maybe using LINQ. Maybe just using the String object. I just want to know the most efficient way, where efficient means:
The fastest the user's query gets processed, the better;
the less the lines of codes, the better; and
where thorough query sanitizing tasks are made.
This last point is really important since some user's input like this:
"CREATE bar NAMED bar_a FISRT (0,0) LAST (0,1)"
Note that the user commited a typo (FISRT instead of FIRST), and the query shouldn't run.
Thanks

Okay, I created a simple parser that should work good for you and, if the need arises, you can easily expand.
Start off by creating a new Console Application. Add a new class file called Tokenizer.cs. This file was auto generated by my TokenIcer project that I linked to you in the comments above. Make Tokenizer.cs look like this:
public class TokenParser
{
private readonly Dictionary<Tokens, string> _tokens;
private readonly Dictionary<Tokens, MatchCollection> _regExMatchCollection;
private string _inputString;
private int _index;
public enum Tokens
{
UNDEFINED = 0,
CREATE = 1,
FIRST = 2,
LAST = 3,
BAR = 4,
NAMED = 5,
BAR_NAME = 6,
WHITESPACE = 7,
LPAREN = 8,
RPAREN = 9,
COMMA = 10,
NUMBER = 11
}
public string InputString
{
set
{
_inputString = value;
PrepareRegex();
}
}
public TokenParser()
{
_tokens = new Dictionary<Tokens, string>();
_regExMatchCollection = new Dictionary<Tokens, MatchCollection>();
_index = 0;
_inputString = string.Empty;
_tokens.Add(Tokens.CREATE, "[Cc][Rr][Ee][Aa][Tt][Ee]");
_tokens.Add(Tokens.FIRST, "[Ff][Ii][Rr][Ss][Tt]");
_tokens.Add(Tokens.LAST, "[Ll][Aa][Ss][Tt]");
_tokens.Add(Tokens.BAR, "[Bb][Aa][Rr][ \\t]");
_tokens.Add(Tokens.NAMED, "[Nn][Aa][Mm][Ee][Dd]");
_tokens.Add(Tokens.BAR_NAME, "[A-Za-z_][a-zA-Z0-9_]*");
_tokens.Add(Tokens.WHITESPACE, "[ \\t]+");
_tokens.Add(Tokens.LPAREN, "\\(");
_tokens.Add(Tokens.RPAREN, "\\)");
_tokens.Add(Tokens.COMMA, "\\,");
_tokens.Add(Tokens.NUMBER, "[0-9]+");
}
private void PrepareRegex()
{
_regExMatchCollection.Clear();
foreach (KeyValuePair<Tokens, string> pair in _tokens)
{
_regExMatchCollection.Add(pair.Key, Regex.Matches(_inputString, pair.Value));
}
}
public void ResetParser()
{
_index = 0;
_inputString = string.Empty;
_regExMatchCollection.Clear();
}
public Token GetToken()
{
if (_index >= _inputString.Length)
return null;
foreach (KeyValuePair<Tokens, MatchCollection> pair in _regExMatchCollection)
{
foreach (Match match in pair.Value)
{
if (match.Index == _index)
{
_index += match.Length;
return new Token(pair.Key, match.Value);
}
if (match.Index > _index)
{
break;
}
}
}
_index++;
return new Token(Tokens.UNDEFINED, string.Empty);
}
public PeekToken Peek()
{
return Peek(new PeekToken(_index, new Token(Tokens.UNDEFINED, string.Empty)));
}
public PeekToken Peek(PeekToken peekToken)
{
int oldIndex = _index;
_index = peekToken.TokenIndex;
if (_index >= _inputString.Length)
{
_index = oldIndex;
return null;
}
foreach (KeyValuePair<Tokens, string> pair in _tokens)
{
var r = new Regex(pair.Value);
Match m = r.Match(_inputString, _index);
if (m.Success && m.Index == _index)
{
_index += m.Length;
var pt = new PeekToken(_index, new Token(pair.Key, m.Value));
_index = oldIndex;
return pt;
}
}
var pt2 = new PeekToken(_index + 1, new Token(Tokens.UNDEFINED, string.Empty));
_index = oldIndex;
return pt2;
}
}
public class PeekToken
{
public int TokenIndex { get; set; }
public Token TokenPeek { get; set; }
public PeekToken(int index, Token value)
{
TokenIndex = index;
TokenPeek = value;
}
}
public class Token
{
public TokenParser.Tokens TokenName { get; set; }
public string TokenValue { get; set; }
public Token(TokenParser.Tokens name, string value)
{
TokenName = name;
TokenValue = value;
}
}
In Program.cs, make it look like this:
class Program
{
private class Bar
{
public string Name { get; set; }
public int FirstX { get; set; }
public int FirstY { get; set; }
public int LastX { get; set; }
public int LastY { get; set; }
}
static void Main(string[] args)
{
const string commandCreateBar1 = "CREATE bar NAMED bar_a FIRST(5,10) LAST (15,20)";
const string commandCreateBar2 = "CREATE bar NAMED MyFooBar FIRST(25 , 31) LAST (153 ,210)";
const string commandCreateBar3 = "CREATE bar NAMED MySpaceyFooBar FIRST(0,0) LAST (12,39)";
Bar bar1 = ParseCreateBar(commandCreateBar1);
PrintBar(bar1);
Bar bar2 = ParseCreateBar(commandCreateBar2);
PrintBar(bar2);
Bar bar3 = ParseCreateBar(commandCreateBar3);
PrintBar(bar3);
}
private static void PrintBar(Bar bar)
{
Console.WriteLine("A new bar was Created! \"{0}\" ({1}, {2}) ({3}, {4})", bar.Name, bar.FirstX, bar.FirstY, bar.LastX, bar.LastY);
}
private static Bar ParseCreateBar(string commandLine)
{
var bar = new Bar();
var parser = new TokenParser { InputString = commandLine };
Expect(parser, TokenParser.Tokens.CREATE);
Expect(parser, TokenParser.Tokens.BAR);
Expect(parser, TokenParser.Tokens.NAMED);
Token token = Expect(parser, TokenParser.Tokens.BAR_NAME);
bar.Name = token.TokenValue;
Expect(parser, TokenParser.Tokens.FIRST);
Expect(parser, TokenParser.Tokens.LPAREN);
token = Expect(parser, TokenParser.Tokens.NUMBER);
bar.FirstX = int.Parse(token.TokenValue);
Expect(parser, TokenParser.Tokens.COMMA);
token = Expect(parser, TokenParser.Tokens.NUMBER);
bar.FirstY = int.Parse(token.TokenValue);
Expect(parser, TokenParser.Tokens.RPAREN);
Expect(parser, TokenParser.Tokens.LAST);
Expect(parser, TokenParser.Tokens.LPAREN);
token = Expect(parser, TokenParser.Tokens.NUMBER);
bar.LastX = int.Parse(token.TokenValue);
Expect(parser, TokenParser.Tokens.COMMA);
token = Expect(parser, TokenParser.Tokens.NUMBER);
bar.LastY = int.Parse(token.TokenValue);
Expect(parser, TokenParser.Tokens.RPAREN);
return bar;
}
private static Token Expect(TokenParser parser, TokenParser.Tokens expectedToken)
{
EatWhiteSpace(parser);
Token token = parser.GetToken();
if (token != null && token.TokenName != expectedToken)
{
Console.WriteLine("Expected Token " + expectedToken);
Environment.Exit(0);
}
if (token == null)
{
Console.WriteLine("Unexpected end of input!");
Environment.Exit(0);
}
return token;
}
private static void EatWhiteSpace(TokenParser parser)
{
while (parser.Peek() != null && parser.Peek().TokenPeek != null &&
parser.Peek().TokenPeek.TokenName == TokenParser.Tokens.WHITESPACE)
{
parser.GetToken();
}
}
}
As you can see, I created 3 test scenarios. Notice all white space is ignored. If you want to be strict about the white space, you can modify the EatWhiteSpace function to be strict.
If you want, I have a simple expression parser I could throw into this code too, that way you could have commands such as CREATE bar NAMED bar_a FIRST(3+2, 7*8 + 12) LAST (150-100, 12-3*2). I've got a simple expression parser I made a while back using TokenIcer that I can throw in. It can parse any math expression and supports parenthesis, add, subtract, multiply, and divide.

Tokenization is one way to go, but if you aren't planning on supporting way too many commands and parameters, you should look at Regexes.
Regex regex = new Regex(#"^CREATE bar NAMED (?<BarName>[A-Za-z0-9-_]*) FIRST (?<FirstPoint>\([0-9]+\|[0-9]+\)) LAST (?<LastPoint>\([0-9]+\|[0-9]+\)$");
Match match = regex.Match("create bar named bar_a first (0,0) last (0,1)", RegexOptions.IgnoreCase);
if (match.Success)
{
var name = match.Groups["BarName"].Value;
// and so on for other matches
}

Related

Problem in databinding Array data to DataGridView in c#

I have been binding short data to DataGridView in C# Winforms. However, I need to bind long string array with size 75 to DataGridView. My data list class consists of 6 individual variables with get and set and array of string which I have defined get and set properties. The individual variables are displayed but the array of strings is not displayed in DataGridView. In debug, I checked the data source of DataGridView and it seems ok. How can I display binded array in gridview.
Below is my source code to populate DataGridView named Logview
public void populateLogData(string path)
{
StreamReader sr = null;
BindingList<LogList> bindLogList;
BindingSource bLogsource = new BindingSource();
List<LogList> loglist = new List<LogList>();
try
{
Logview.DataSource = null;
Logview.Rows.Clear();
Logview.Columns.Clear();
Logview.AutoGenerateColumns = true;
if (File.Exists(path))
{
try
{
sr = new StreamReader(path);
StringBuilder readline = new StringBuilder(sr.ReadLine());
if (readline.ToString() != null && readline.ToString() != "")
{
readline = new StringBuilder(sr.ReadLine());
while (readline.ToString() != null && readline.ToString() != "")
{
string[] subdata = readline.ToString().Split(',');
LogList tloglist = new LogList(subdata[0], subdata[1], subdata[2], subdata[3], subdata[4], subdata[5], max_index);
for (int i = 6; i < subdata.Length; i++)
tloglist.setPartList(i-6, subdata[i]);
loglist.Add(new LogList(subdata, subdata.Length));
readline = new StringBuilder(sr.ReadLine());
}
}
bindLogList = new BindingList<LogList>(loglist);
bLogsource.DataSource = bindLogList;
Logview.AutoGenerateColumns = true;
Logview.DataSource = bindLogList;
Logview.Columns[0].Width = 140; // project name
Logview.Columns[1].Width = 140; // data/time
Logview.Columns[2].Width = 90;
Logview.Columns[3].Width = 90;
Logview.Columns[4].Width = 90;
Logview.Columns[5].Width = 90;
// max_index is set from another part of code
for(int i = 0; i <= max_index; i++)
{
int counter = 6 + i;
Logview.Columns.Add(headertext[i], headertext[i]);
Logview.Columns[counter].Width = 90;
Logview.Columns[counter].HeaderText = headertext[i];
}
}
catch (IOException io)
{
MessageBox.Show("Error: Cannot Open log file.");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
finally
{
if (sr != null) sr.Close();
}
}
else
{
MessageBox.Show("Log file not found \n" + path);
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
finally
{
GC.Collect();
}
}
Below is LogList class
class LogList
{
const int max_size = 100;
private string[] holdList;
public string project { get; set; }
public string date_time { get; set; }
public string Qty { get; set; }
public string Pass { get; set; }
public string Fail { get; set; }
public string Result { get; set; }
public string[] partlist
{
get
{
return holdList;
}
set
{
holdList = value;
}
}
public LogList(string project, string date_time, string Qty, string Pass, string Fail, string Result, int partsize)
{
this.project = project;
this.date_time = date_time;
this.Qty = Qty;
this.Pass = Pass;
this.Fail = Fail;
this.Result = Result;
partlist = new string[partsize+1];
}
public void setPartList(int size, string getValue)
{
partlist[size] = getValue;
}
}
Project, date/time, Qty, Pass, Fail, Result is displayed. But partlist array is not displayed.
To supplement IVSoftware’s answer, below is an example using two grids in a master-detail scenario.
One issue I would have with your current approach, is that it uses an Array for the “parts list.” Currently this is a string array, and that isn’t going to work if we want to display it in a grid. Fortunately, there are a few easy ways we can get the data to display as we want.
One simple solution is to create a “wrapper” Class for the string. I will call this Class Part. I added a simple int ID property and the string PartName property. You could easily leave out the ID and have a simple string wrapper. This simple Class may look something like…
public class Part {
public int ID { get; set; }
public string PartName { get; set; }
}
This should allow the data to display correctly in the grid using just about any construct like an array, list etc.… So, we “could” change your current code to use an array of Part objects like…
Part[] Parts = new Parts[X];
And this would work, however, if we use an array and we know for sure that each LogItem may have a different number of parts in its PartsList, then we will have to manage the array sizes. So, a BindingList of Part objects will simplify this. The altered LogList (LogItem) Class is below…
public class LogItem {
public BindingList<Part> PartsList { get; set; }
public string Project { get; set; }
public string Date_Time { get; set; }
public string Qty { get; set; }
public string Pass { get; set; }
public string Fail { get; set; }
public string Result { get; set; }
public LogItem(string project, string date_Time, string qty, string pass, string fail, string result) {
Project = project;
Date_Time = date_Time;
Qty = qty;
Pass = pass;
Fail = fail;
Result = result;
PartsList = new BindingList<Part>();
}
}
So given the updated Classes, this should simplify things and we will use the same DataSource for both grids. This DataSource for the “master” grid will be a BindingList of LogItem objects. In the “detail” grid, we simply need to point it’s DataMember property to the PartsList property of the currently selected LogItem. And this would look something like…
dgvLogs.DataSource = LogsBL;
if (LogsBL.Count > 0) {
dgvParts.DataMember = "PartsList";
dgvParts.DataSource = LogsBL;
}
Below is the code to test the Classes above in a master-detail scenario with two grids. Create a new winform solution and drop two (2) DataGridViews on the form. The grid on the left is dgvLogs and the grid on the right is dgvParts.
public void populateLogData(string path) {
BindingList<LogItem> LogsBL = new BindingList<LogItem>();
string currentLine;
if (File.Exists(path)) {
try {
using (StreamReader sr = new StreamReader(path)) {
LogItem tempLogItem;
currentLine = sr.ReadLine(); // <- header row - ignoring
currentLine = sr.ReadLine();
while (currentLine != null) {
if (!string.IsNullOrEmpty(currentLine)) {
string[] splitArray = currentLine.Split(',');
if (splitArray.Length >= 6) {
tempLogItem = new LogItem(splitArray[0], splitArray[1], splitArray[2], splitArray[3], splitArray[4], splitArray[5]);
for (int i = 6; i < splitArray.Length; i++) {
tempLogItem.PartsList.Add(new Part { ID = i, PartName = splitArray[i] });
}
LogsBL.Add(tempLogItem);
}
else {
Debug.WriteLine("DataRead Error: Not enough items to make a LogItem: " + currentLine);
}
}
else {
Debug.WriteLine("DataRead Empty row");
}
currentLine = sr.ReadLine();
}
}
dgvLogs.DataSource = LogsBL;
if (LogsBL.Count > 0) {
dgvParts.DataMember = "PartsList";
dgvParts.DataSource = LogsBL;
}
}
catch (IOException io) {
MessageBox.Show("Error: Cannot Open log file.");
}
catch (Exception ex) {
MessageBox.Show(ex.Message + " Stacktrace- " + ex.StackTrace);
}
}
else {
MessageBox.Show("Log file not found \n" + path);
}
}
And some test data…
H1,h2,h3,h4,h5,h6,h7,h8
Model: LMG600N_IF_2blablas,2022-9-6,112,61,51,Fail,p1,p3,p4,p5,p6
1,2022-9-6,2112,621,251,Pass,px4,px5,px6,px1,px2,px3
data1,2022-9-7,3456,789,123,Fail,z3,z3,z4
Model: LMG600N_IF_2blablas,2022-9-6,112,61,51,Fail
Model: LMG600N_IF_2blablas,2022-9-6,112,61,51,Fail,p1,p3,p4,p5,p6,p7,p8,p99
BadData Model: LMG600N_IF_2blablas,2022-9-6,112,61
Moxxxdel: LMG600N_IF_2blablas,2022-9-6,11x2,6x1,5x1,Fail
Hope this helps and makes sense.
Your data list class consists of 6 individual variables with get and set, and an array of string. Your question is about the variables are displayed but the array of strings is not.
Here's what has worked for me (similar to the excellent suggestion by JohnG) for displaying the string array. What I'm doing here is taking a DataGridView and dropping in my main form without changing any settings (other than to Dock it). Given the default settings, the LogList class (shown here in a minimal reproducible example of 1 variable and 1 array of strings) is defined with a public string property named PartList and with this basic implementation:
class LogList
{
public LogList(string product, string[] partList)
{
Product = product;
_partList = partList;
}
public string Product { get; set; }
private string[] _partList;
public string PartList => string.Join(",", _partList);
}
To autoconfigure the DataGridView with Product and PartList columns, here is an example initializer method that sets the DataSource and adds the first three items as a test:
// Set data source property once. Clear it, Add to it, but no reason to nullify it.
BindingList<LogList> DataSource { get; } = new BindingList<LogList>();
private void InitDataGridView()
{
dataGridView1.DataSource = DataSource;
// Auto config columns by adding at least one Record.
DataSource.Add(
new LogList(
product: "LMG450",
// Four parts
partList: new string[]
{
"PCT2000",
"WCT100",
"ZEL-0812LN",
"EN61000-3-3/-11",
}
));
DataSource.Add(
new LogList(
product: "LMG600N",
// Three parts
partList: new string[]
{
"LTC2280",
"BMS6815",
"ZEL-0812LN",
}
));
DataSource.Add(
new LogList(
product: "Long Array",
// 75 parts
partList: Enumerable.Range(1, 75).Select(x => $"{ x }").ToArray()
));
// Use string indexer to access columns for formatting purposes.
dataGridView1
.Columns[nameof(LogList.Product)]
.AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells;
dataGridView1
.Columns[nameof(LogList.PartList)]
.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
}
After running this code, the DGV looks like this:
With the mouse hovered over the item all 75 "parts" can be viewed.
One last thing - I notice you have some methods to assign a new partList[] of perhaps change an individual part at a specified index. (I didn't show them in the minimal sample but for sure you'll want things like that). You probably know this but make sure to call dataGridView1.Refresh after altering properties of an existing row/LogList object so that the view will reflect the changes.
I hope there's something here that offers a few ideas to achieve the outcome you want.

How to struct an object to represent a list of topics?

I'm coding at C# and I'm trying to make an OOP representation of a list of topics. I tried countless approaches but I still not being able to reach the desired result.
I want to make a method later that will output it like:
1) Text
1.1) Text
2) Text
2.1) Text
2.2) Text
2.2.1) Text
2.2.2) Text
2.3) Text
3) Text
3.1) Text
When needed to get a single topic, I would like to create a method calling my object like:
private string GetSingleTopic()
{
return $"{Topic.Numerator}) {Topic.Text}"
}
EXAMPLES
Example 1
I would be able to instantiate the object such as:
var variable = new TopicObject
{
"TitleA",
"TitleB",
"TitleC"
}
/* --- OUTPUT ---
1) TitleA
2) TitleB
3) TitleC
--- OUTPUT --- */
Example 2
Be able to instantiate the object such as:
var variable = new TopicObject
{
"TitleA",
"TitleB",
"TitleC":
{
"TitleD":
{
"TitleE"
},
"TitleF":
{
"TitleG",
"TitleH"
}
}
}
/* --- OUTPUT ---
1) TitleA
2) TitleB
3) TitleC
3.1) TitleD
3.1.2) TitleE
3.2) TitleF
3.2.1) TitleG
3.2.2) TitleH
--- OUTPUT --- */
My Approach
This, was one of my many approaches. I couldn't use it because I can't initialize the inner topic List in the way i mentioned, like an hierarchy.
But the structure is pretty similar to what I want to achieve so I decided to put here as an example.
public abstract class TopicBase
{
public List<Topic> Topics { get; set; } // optional
protected TopicBase() { Topics = new List<Topic>(); }
protected TopicBase(List<Topic> topics) { Topics = topics; }
public TopicBase AddTopic(string topicText)
{
var test = new Topic(topicText);
Topics.Add(test);
return this;
}
}
public class Topic
{
public Topic(string text)
{
Numerator++;
Text = text;
}
public int Numerator { get; }
public string Text { get; }
}
public class TopicLevel1 : TopicBase { }
public class TopicLevel2 : TopicBase { }
public class TopicLevel3 : TopicBase { }
Let's start by defining a data structure that can hold the topics:
public class Topics<T> : List<Topic<T>> { }
public class Topic<T> : List<Topic<T>>
{
public T Value { get; private set; }
public Topic(T value, params Topic<T>[] children)
{
this.Value = value;
if (children != null)
this.AddRange(children);
}
}
That allows us to write this code:
var topics = new Topics<string>()
{
new Topic<string>("TitleA"),
new Topic<string>("TitleB"),
new Topic<string>("TitleC",
new Topic<string>("TitleD",
new Topic<string>("TitleE")),
new Topic<string>("TitleF",
new Topic<string>("TitleF"),
new Topic<string>("TitleH")))
};
That matches your "Example 2" data.
To output the result we add two ToOutput methods.
To Topics<T>:
public IEnumerable<string> ToOutput(Func<T, string> format)
=> this.SelectMany((t, n) => t.ToOutput(0, $"{n + 1}", format));
To Topic<T>:
public IEnumerable<string> ToOutput(int depth, string prefix, Func<T, string> format)
{
yield return $"{new string(' ', depth)}{prefix}) {format(this.Value)}";
foreach (var child in this.SelectMany((t, n) => t.ToOutput(depth + 1, $"{prefix}.{n + 1}", format)))
{
yield return child;
}
}
Now I can run this code:
foreach (var line in topics.ToOutput(x => x))
{
Console.WriteLine(line);
}
That gives me:
1) TitleA
2) TitleB
3) TitleC
3.1) TitleD
3.1.1) TitleE
3.2) TitleF
3.2.1) TitleF
3.2.2) TitleH
If the goal is to have some structure that will help with the output of the topic hierarchy, you already have it (and may even be able to simplify it more).
For example, here's an almost-minimal Topic to get what you want:
public class Topic
{
public string Title { get; set; }
public List<Topic> SubTopics { get; private set; } = new();
public Topic() : this("DocRoot") { }
public Topic(string title) => Title = title;
public void AddTopics(List<Topic> subTopics) => SubTopics.AddRange(subTopics);
public void AddTopics(params Topic[] subTopics) => SubTopics.AddRange(subTopics);
public override string ToString() => Title;
}
That is, you have a Topic that can have SubTopics (aka children) and that's all you need.
With that, we can build your second example:
var firstLevelTopics = new List<Topic>();
for (var c = 'A'; c < 'D'; ++c)
{
firstLevelTopics.Add(new Topic(c.ToString()));
}
var cTopic = firstLevelTopics.Last();
cTopic.AddTopics(
new Topic
{
Title = "D",
SubTopics = { new Topic("E") }
},
new Topic
{
Title = "F",
SubTopics = { new Topic("G"), new Topic("H") }
});
Now, imagine if we had a function to print the hierarchy from the list of top-level topics. I'm leaving the final detail for yourself in case this is homework.
PrintTopics(firstLevelTopics);
static void PrintTopics(List<Topic> topics, string prefix = "")
{
// For the simple case, we can just loop and print...
for (var i = 0; i < topics.Count; ++i)
{
var topic = topics[i];
var level = i + 1;
Console.WriteLine($"{prefix}{level}) {topic}");
// ...but, if we want to print the children, we need more.
// Make a recursive call to print the SubTopics
// PrintTopics(<What goes here?>);
}
}

Parse a text file into instances of a class based on keywords c#

I have a text file that contains a list of points:
POINT:
TYPE 5,
OBJECT ID 2,
DEVICE TYPE CAT,
TAG 'ADDRESS-1',
DESCRIPTION 'kitty',
UNITS 'Lb',
POINT:
TYPE 5,
OBJECT ID 2,
DEVICE TYPE CAT,
TAG 'ADDRESS-2',
DESCRIPTION 'orange kitty',
UNITS 'Lb',
POINT:
TYPE 2,
OBJECT ID 3,
DEVICE TYPE DOG,
TAG 'ADDRESS-5',
DESCRIPTION 'brown dog',
UNITS 'Lb',
From this, I want to create instances (in this case, 2) of my class 'Cat' that contain the tag and description in this text file(and then put them in a list of Cats). I only want to take the description and tag from points of Type 5 (those are the cats).
I'm not sure what the best approach is to get the strings I want. I need to search the entire file for all points of type 5, then for each of those points, take the description and tag and add it to a new Cat.
public static void Main()
{
string line;
List<Cat> catList = new List<Cat>();
StreamReader file = new StreamReader(#"C:\Config\pets.txt");
while((line = file.ReadLine()) != null)
{
string[] words = line.Split(',');
catList.Add(new Cat cat1)
}}
I ended up doing it this way:
public static List<List<string>> Parse()
{
string filePath = #"C:\Config\pets.txt";
string readText = File.ReadAllText(filePath);
string[] stringSeparators = new string[] { "POINT:" }; //POINT is the keyword the text will be split on
string[] result;
result = readText.Split(stringSeparators, StringSplitOptions.None);
List<List<string>> catData = new List<List<string>>();
//split the text into an list of pieces
List<string> tags = new List<string>(); //tags go here
List<string> descriptions = new List<string>(); //descriptions go here
foreach (string s in result)
{
if (s.Contains("TYPE 5")) //TYPE 5 = CAT
{
string[] parts = s.Split(','); //split the cat by commas
string chop = "'"; //once tags and descriptions have been found, only want to keep what is inside single quotes ie 'orange kitty'
foreach (string part in parts)
{
if (part.Contains("TAG"))
{
int startIndex = part.IndexOf(chop);
int endIndex = part.LastIndexOf(chop);
int length = endIndex - startIndex + 1;
string path = part.Substring(startIndex, length);
tag = tag.Replace(chop, string.Empty);
tags.Add(tag);
//need to create instance of Cat with this tag
}
if (part.Contains("DESCRIPTION"))
{
int startIndex = part.IndexOf(chop);
int endIndex = part.LastIndexOf(chop);
int length = endIndex - startIndex + 1;
string description = part.Substring(startIndex, length);
description = description.Replace(chop, string.Empty);
descriptions.Add(description);
//need to add description to Cat instance that matches associated tag
}
}
}
}
catData.Add(tags);
catData.Add(descriptions);
return catData;
What I would do is create a class that represents the fields you want to capture. For this example, I'm capturing all the fields, but you can customize it how you want. I called this class "Animal", since it seems the "points" represent an animal.
Then I would add a static Parse method to the class that will return an instance of an Animal based on some input string. This method will parse the input string and attempt to set the relevant properties of the Animal object based on the values in the string.
I also added a ToString() override on the class so we have some way of displaying the output:
class Animal
{
public int Type { get; set; }
public int ObjectId { get; set; }
public string DeviceType { get; set; }
public string Tag { get; set; }
public string Description { get; set; }
public string Units { get; set; }
/// <summary>
/// Parses an input string and returns an Animal based
/// on any property values found in the string
/// </summary>
/// <param name="input">The string to parse for property values</param>
/// <returns>An animal instance with specified properties</returns>
public static Animal Parse(string input)
{
var result = new Animal();
if (string.IsNullOrWhiteSpace(input)) return result;
// Parse input string and set fields accordingly
var keyValueParts = input
.Split(new [] {','}, StringSplitOptions.RemoveEmptyEntries)
.Select(kvp => kvp.Trim());
foreach (var keyValuePart in keyValueParts)
{
if (keyValuePart.StartsWith("Type",
StringComparison.OrdinalIgnoreCase))
{
int type;
var value = keyValuePart.Substring("Type".Length).Trim();
if (int.TryParse(value, out type))
{
result.Type = type;
}
}
else if (keyValuePart.StartsWith("Object Id",
StringComparison.OrdinalIgnoreCase))
{
int objectId;
var value = keyValuePart.Substring("Object Id".Length).Trim();
if (int.TryParse(value, out objectId))
{
result.ObjectId = objectId;
}
}
else if (keyValuePart.StartsWith("Device Type",
StringComparison.OrdinalIgnoreCase))
{
var value = keyValuePart.Substring("Device Type".Length).Trim();
result.DeviceType = value;
}
else if (keyValuePart.StartsWith("Tag",
StringComparison.OrdinalIgnoreCase))
{
var value = keyValuePart.Substring("Tag".Length).Trim();
result.Tag = value;
}
else if (keyValuePart.StartsWith("Description",
StringComparison.OrdinalIgnoreCase))
{
var value = keyValuePart.Substring("Description".Length).Trim();
result.Description = value;
}
else if (keyValuePart.StartsWith("Units",
StringComparison.OrdinalIgnoreCase))
{
var value = keyValuePart.Substring("Units".Length).Trim();
result.Units = value;
}
}
return result;
}
public override string ToString()
{
// Return a string that describes this animal
var animalProperties = new StringBuilder();
animalProperties.Append($"Type = {Type}, Object Id = {ObjectId}, ");
animalProperties.Append($"Device Type = {DeviceType}, Tag = {Tag}, ");
animalProperties.Append($"Description = {Description}, Units = {Units}");
return animalProperties.ToString();
}
}
Now that we have an object that can create itself from a string, we just need to read in the file contents, split it on the "Point:" keyword, and then pass each string to the Animal class to get our instances.
I added some System.Linq clause to filter on animals that have Type = 5 (which you said are all cats), since you said that was the only animal you were interested in. Of course you could remove this to get all animals, or replace it with "Dog" to get the dogs, etc..:
private static void Main()
{
var filePath = #"f:\public\temp\temp.txt";
// Read all file contents and split it on the word "Point:"
var fileContents = Regex
.Split(File.ReadAllText(filePath), "Point:", RegexOptions.IgnoreCase)
.Where(point => !string.IsNullOrWhiteSpace(point))
.Select(point => point.Trim());
// Get all animals that are cats from the results
var catList = fileContents
.Select(Animal.Parse)
.Where(animal => animal.Type == 5)
.ToList();
// Output results
catList.ForEach(Console.WriteLine);
// Wait for input before closing
Console.WriteLine("\nDone!\nPress any key to exit...");
Console.ReadKey();
}
Output

Working with CSV file

I've been working and trying to solve this problem for maybe a whole week, which at this point I am wondering if I can solve it without it diving even deeper into the C# language, and I'm quite fairly new to C#, as well as working with CSV files and sorting and organizing them, so I'm fairly inexperienced into the whole spectrum of this.
I'm trying to sort a CSV file alphabetically, hide items that need to be hidden and have them have depth levels based on their parents, child and grandchild elements.
I've been successful with a couple of them, and written somewhat working code, but I don't know how to sort them alphabetically and give them the proper depth layer based on the parent and child they belong to.
Here's the mockup CSV that I've been trying to organize:
ID;MenuName;ParentID;isHidden;LinkURL
1;Company;NULL;False;/company
2;About Us;1;False;/company/aboutus
3;Mission;1;False;/company/mission
4;Team;2;False;/company/aboutus/team
5;Client 2;10;False;/references/client2
6;Client 1;10;False;/references/client1
7;Client 4;10;True;/references/client4
8;Client 5;10;True;/references/client5
10;References;NULL;False;/references
I've delimited the items by the semicolon, I've displayed the items that needs to be shown, but I fail to sort them like I should.
The sorting should look like this:
Company
About Us
Team
Mission
References
Client 1
Client 2
I've tried to sort them or display them in that order by getting the index of the slash, but what the code reproduces is not how it should be displayed, and, it looks like this:
Company
About Us
Mission
Team
Client 2
Client 1
References
In the other try, where I recursively match their parent id with the id, the console display looks like this:
Company
About Us
Mission
Team
Client 2
Client 1
References
I've tried solving this with a friend, and, even he doesn't know how to approach this problem, since this code should work on a different file that uses different parent ids.
On top of all this, I am unable to index them to an array, because there's only index of 0 or the index is based on their letters or crashes the console if I enter the index position of 1.
Here's the code for the first part where I fail to sort them:
class Program
{
static void Main(string[] args)
{
StreamReader sr = new StreamReader(#"Navigation.csv");
string data = sr.ReadLine();
while (data != null)
{
string[] rows = data.Split(';');
int id;
int parentId;
bool ids = Int32.TryParse(rows[0], out id);
string name = rows[1];
bool pIds = Int32.TryParse(rows[2], out parentId);
string isHidden = rows[3];
string linkUrl = rows[4];
string[] splitted = linkUrl.Split('/');
if (isHidden == "False")
{
List<CsvParentChild> pIdCid = new List<CsvParentChild>()
{
new CsvParentChild(id, parentId, name, linkUrl)
};
}
data = sr.ReadLine();
}
}
}
class CsvParentChild
{
public int Id;
public int ParentId;
public string Name;
public string LinkUrl;
public List<CsvParentChild> Children = new List<CsvParentChild>();
public CsvParentChild(int id, int parentId, string name, string linkUrl)
{
Id = id;
ParentId = parentId;
Name = name;
LinkUrl = linkUrl;
string[] splitted = linkUrl.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
if (splitted.Length == 1)
{
Console.WriteLine($". { name }");
}
else if (splitted.Length == 2)
{
Console.WriteLine($".... { name }");
}
else if (splitted.Length == 3)
{
Console.WriteLine($"....... { name }");
}
}
}
And here's for the second part:
class Program
{
static void Main(string[] args)
{
// Get the path for the file
const string filePath = #"../../Navigation.csv";
// Read the file
StreamReader sr = new StreamReader(File.OpenRead(filePath));
string data = sr.ReadLine();
while (data != null)
{
string[] rows = data.Split(';');
ListItems lis = new ListItems();
int id;
int parentId;
// Get the rows/columns from the Csv file
bool ids = Int32.TryParse(rows[0], out id);
string name = rows[1];
bool parentIds = Int32.TryParse(rows[2], out parentId);
string isHidden = rows[3];
string linkUrl = rows[4];
// Split the linkUrl so that we get the position of the
// elements based on their slash
string [] splitted = linkUrl.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
// If item.isHidden == "False"
// then display the all items whose state is set to false.
// If the item.isHidden == "True", then display the item
// whose state is set to true.
if (isHidden == "False")
{
// Set the items
ListItems.data = new List<ListItems>()
{
new ListItems() { Id = id, Name = name, ParentId = parentId },
};
// Make a new instance of ListItems()
ListItems listItems = new ListItems();
// Loop through the CSV data
for (var i = 0; i < data.Count(); i++)
{
if (splitted.Length == 1)
{
listItems.ListThroughItems(i, i);
}
else if (splitted.Length == 2)
{
listItems.ListThroughItems(i, i);
}
else
{
listItems.ListThroughItems(i, i);
}
}
}
// Break out of infinite loop
data = sr.ReadLine();
}
}
public class ListItems
{
public int Id { get; set; }
public string Name { get; set; }
public int ParentId { get; set; }
public static List<ListItems> data = null;
public List<ListItems> Children = new List<ListItems>();
// http://stackoverflow.com/a/36250045/7826856
public void ListThroughItems(int id, int level)
{
Id = id;
// Match the parent id with the id
List<ListItems> children = data
.Where(p => p.ParentId == id)
.ToList();
foreach (ListItems child in children)
{
string depth = new string('.', level * 4);
Console.WriteLine($".{ depth } { child.Name }");
ListThroughItems(child.Id, level + 1);
}
}
}
}
For each item, you need to construct a kind of "sort array" consisting of ids. The sort array consists of the ids of the item's ancestors in order from most distant to least distant. For "Team", our sort array is [1, 2, 4].
Here are the sort arrays of each item:
[1]
[1, 2]
[1, 3]
[1, 2, 4]
[10, 5]
[10, 6]
[10, 7]
[10, 8]
[10]
Once you have this, sorting the items is simple. When comparing two "sort arrays", start with the numbers in order in each array. If they are different, sort according to the value of the first number and you're done. If they are the same, look at the second number. If there is no second number, then sort by the length of the arrays, i.e., nothing comes before something.
Applying this algorithm, we get:
[1]
[1, 2]
[1, 2, 4]
[1, 3]
[10]
[10, 5]
[10, 6]
[10, 7]
[10, 8]
After that, hide the items based on the flag. I leave that to you because it's so simple. Depth is easy: It's the length of the sort array.
My Application was compiled and produced the following output with your data:
Company
About Us
Team
Mission
References
Client 1
Client 2
Client 4
Client 5
I would attempt to use object relation to create your tree like structure.
The main difficulty with the question is that parents don't matter. Children do.
So at some point in your code, you will need to reverse the hierarchy; Parsing Children first but reading their Parents first to create the output.
The roots of our tree are the data entries without parents.
Parsing
This should be pretty self explanatory, we have a nice class with a constructor that parses the input array and stores the data in it's properties.
We store all the rows in a list. After we are done with this, we pretty much converted the list, but no sorting happened at all.
public partial class csvRow
{
// Your Data
public int Id { get; private set; }
public string MenuName { get; private set; }
public int? ParentId { get; private set; }
public bool isHidden { get; private set; }
public string LinkURL { get; private set; }
public csvRow(string[] arr)
{
Id = Int32.Parse(arr[0]);
MenuName = arr[1];
//Parent Id can be null!
ParentId = ToNullableInt(arr[2]);
isHidden = bool.Parse(arr[3]);
LinkURL = arr[4];
}
private static int? ToNullableInt(string s)
{
int i;
if (int.TryParse(s, out i))
return i;
else
return null;
}
}
static void Main(string[] args)
{
List<csvRow> unsortedRows = new List<csvRow>();
// Read the file
const string filePath = #"Navigation.csv";
StreamReader sr = new StreamReader(File.OpenRead(filePath));
string data = sr.ReadLine();
//Read each line
while (data != null)
{
var dataSplit = data.Split(';');
//We need to avoid parsing the first line.
if (dataSplit[0] != "ID" )
{
csvRow lis = new csvRow(dataSplit);
unsortedRows.Add(lis);
}
// Break out of infinite loop
data = sr.ReadLine();
}
sr.Dispose();
//At this point we got our data in our List<csvRow> unsortedRows
//It's parsed nicely. But we still need to sort it.
//So let's get ourselves the root values. Those are the data entries that don't have a parent.
//Please Note that the main method continues afterwards.
Creating our Tree Strukture and Sorting the items
We start by defining Children and a public ChildrenSorted property that returns them sorted. That's actually allsorting we are doing, it's alot easier to sort than to work recursively.
We also need a function that add's children. It will pretty much filter the input and find all the rows where row.parentId = this.ID.
The last one is the function that defines our output and allows us to get something we can print into the console.
public partial class csvRow
{
private List<csvRow> children = new List<csvRow>();
public List<csvRow> ChildrenSorted
{
get
{
// This is a quite neet way of sorting, isn't it?
//Btw this is all the sorting we are doing, recursion for win!
return children.OrderBy(row => row.MenuName).ToList();
}
}
public void addChildrenFrom(List<csvRow> unsortedRows)
{
// Add's only rows where this is the parent.
this.children.AddRange(unsortedRows.Where(
//Avoid running into null errors
row => row.ParentId.HasValue &&
//Find actualy children
row.ParentId == this.Id &&
//Avoid adding a child twice. This shouldn't be a problem with your data,
//but why not be careful?
!this.children.Any(child => child.Id == row.Id)));
//And this is where the magic happens. We are doing this recursively.
foreach (csvRow child in this.children)
{
child.addChildrenFrom(unsortedRows);
}
}
//Depending on your use case this function should be replaced with something
//that actually makes sense for your business logic, it's an example on
//how to read from a recursiv structure.
public List<string> FamilyTree
{
get
{
List<string> myFamily = new List<string>();
myFamily.Add(this.MenuName);
//Merges the Trees with itself as root.
foreach (csvRow child in this.ChildrenSorted)
{
foreach (string familyMember in child.FamilyTree)
{
//Adds a tab for all children, grandchildren etc.
myFamily.Add("\t" + familyMember);
}
}
return myFamily;
}
}
}
Adding Items to the Tree and accessing them
This is the second part of my main function, where we actually work with our data (Right after sr.Dispose();)
var roots = unsortedRows.Where(row => row.ParentId.HasValue == false).
OrderBy(root => root.MenuName).ToList();
foreach (csvRow root in roots)
{
root.addChildrenFrom(unsortedRows);
}
foreach (csvRow root in roots)
{
foreach (string FamilyMember in root.FamilyTree)
{
Console.WriteLine(FamilyMember);
}
}
Console.Read();
}
Entire Sourcecode (Visual Studio C# Console Application)
You can use this to test, play around and learn more about recursive structures.
Copyright 2017 Eldar Kersebaum
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
namespace ConsoleApplication49
{
class Program
{
static void Main(string[] args)
{
List<csvRow> unsortedRows = new List<csvRow>();
const string filePath = #"Navigation.csv";
StreamReader sr = new StreamReader(File.OpenRead(filePath));
string data = sr.ReadLine();
while (data != null)
{
var dataSplit = data.Split(';');
//We need to avoid parsing the first line.
if (dataSplit[0] != "ID" )
{
csvRow lis = new csvRow(dataSplit);
unsortedRows.Add(lis);
}
// Break out of infinite loop
data = sr.ReadLine();
}
sr.Dispose();
var roots = unsortedRows.Where(row => row.ParentId.HasValue == false).
OrderBy(root => root.MenuName).ToList();
foreach (csvRow root in roots)
{
root.addChildrenFrom(unsortedRows);
}
foreach (csvRow root in roots)
{
foreach (string FamilyMember in root.FamilyTree)
{
Console.WriteLine(FamilyMember);
}
}
Console.Read();
}
}
public partial class csvRow
{
// Your Data
public int Id { get; private set; }
public string MenuName { get; private set; }
public int? ParentId { get; private set; }
public bool isHidden { get; private set; }
public string LinkURL { get; private set; }
public csvRow(string[] arr)
{
Id = Int32.Parse(arr[0]);
MenuName = arr[1];
ParentId = ToNullableInt(arr[2]);
isHidden = bool.Parse(arr[3]);
LinkURL = arr[4];
}
private static int? ToNullableInt(string s)
{
int i;
if (int.TryParse(s, out i))
return i;
else
return null;
}
private List<csvRow> children = new List<csvRow>();
public List<csvRow> ChildrenSorted
{
get
{
return children.OrderBy(row => row.MenuName).ToList();
}
}
public void addChildrenFrom(List<csvRow> unsortedRows)
{
this.children.AddRange(unsortedRows.Where(
row => row.ParentId.HasValue &&
row.ParentId == this.Id &&
!this.children.Any(child => child.Id == row.Id)));
foreach (csvRow child in this.children)
{
child.addChildrenFrom(unsortedRows);
}
}
public List<string> FamilyTree
{
get
{
List<string> myFamily = new List<string>();
myFamily.Add(this.MenuName);
foreach (csvRow child in this.ChildrenSorted)
{
foreach (string familyMember in child.FamilyTree)
{
myFamily.Add("\t" + familyMember);
}
}
return myFamily;
}
}
}
}

How can I match custom PropertyGrid BrowsableAttributes entries?

I have a class(KeywordProperties) with this code :
public class KeywordProperties
{
[DisplayMode("0-1,0-2,0-3,1-1,1-2,1-3,1-6,1-9,1-10,1-11,1-12,2-1,2-2,2-3,2-9,2-10,2-12,3-1,3-2,3-3,3-10,3-12,4-13,5,6")]
public string Onvaan { get; set; }
[DisplayMode("0-1,0-2,0-3,1-1,1-2,1-3,1-6,1-9,1-10,1-11,1-12,2-1,2-2,2-3,2-9,2-10,2-12,3-1,3-2,3-3,3-10,3-12,4-13,5,6")]
public string MozooKolli { get; set; }
[DisplayMode("0-10,1-10,3-10,3-12,5,6")]
public string EsmeDars { get; set; }
[DisplayMode("0-1,1-1,2-1,2-2,3-1,6")]
public string Sokhanraan { get; set; }
[DisplayMode("0-10,1-2,2-1,2-10,3-10,6")]
public string Modares { get; set; }
}
And I have another for check attributes :
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class DisplayModeAttribute : Attribute
{
private readonly string mode;
public DisplayModeAttribute(string mode)
{
this.mode = mode ?? "";
}
public override bool Match(object obj)
{
var other = obj as DisplayModeAttribute;
if (other == null) return false;
if (other.mode == mode) return true;
// allow for a comma-separated match, in either direction
if (mode.IndexOf(',') >= 0)
{
string[] tokens = mode.Split(',');
if (Array.IndexOf(tokens, other.mode) >= 0) return true;
}
else if (other.mode.IndexOf(',') >= 0)
{
string[] tokens = other.mode.Split(',');
if (Array.IndexOf(tokens, mode) >= 0) return true;
}
return false;
}
}
I want display properties in propertygrid with this code :
String Code = "":
KeywordProperties Kp = new KeywordProperties();
propertygrid1.SelectedObject = Kp;
propertygrid1.BrowsableAttributes = new AttributeCollection(new DisplayModeAttribute(Code));
When Code vlue is "0-1" or "5" or ...(single value), I can see my properties.
But, when use "0-1,1-2" for Code, I can't see any thing in my properygrid.
How can I see these data :
1- All properties that have code 0-1 and code 1-2 :
result is :Onvaan,MozooKolli
2- All properties that have code 0-1 or code 1-2 :
result is : Onvaan,MozooKolli,Sokhanraan,Modares
It appears that your code only matches DisplayModeAttributes when both have a single value, or one contains a single value and the other contains multiple values; it won't match them when both contain multiple values, unless the list of values are identical.
To use your code as-is, you could change the way you populate PropertyGrid.BrowsableAttributes:
propertygrid1.BrowsableAttributes = new AttributeCollection(
new DisplayModeAttribute("0-1"),
new DisplayModeAttribute("1-2")
// etc.
);
Alternatively, to fix your matching code, you could replace it with something like:
public override bool Match(object obj)
{
var other = obj as DisplayModeAttribute;
if (other == null)
return false;
if (other.mode == mode)
return true;
string[] modes = mode.Split(',');
string[] others = other.mode.Split(',');
var matches = modes.Intersect(others);
return matches.Count() > 0;
}
This uses the LINQ Intersect method, which returns the elements that two lists have in common.

Categories

Resources