Please critique my class - c#

I've taken a few school classes along time ago on and to be honest i never really understood the concept of classes. I recently "got back on the horse" and have been trying to find some real world application for creating a class.
you may have seen that I'm trying to parse a lot of family tree data that is in an very old and antiquated format called gedcom
I created a Gedcom Reader class to read in the file , process it and make it available as two lists that contain the data that i found necessary to use
More importantly to me is i created a class to do it so I would very much like to get the experts here to tell me what i did right and what i could have done better ( I wont say wrong because the thing works and that's good enough for me)
Class:
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
namespace GedcomReader
{
class Gedcom
{
private string GedcomText = "";
public struct INDI
{
public string ID;
public string Name;
public string Sex;
public string BDay;
public bool Dead;
}
public struct FAM
{
public string FamID;
public string Type;
public string IndiID;
}
public List<INDI> Individuals = new List<INDI>();
public List<FAM> Families = new List<FAM>();
public Gedcom(string fileName)
{
using (StreamReader SR = new StreamReader(fileName))
{
GedcomText = SR.ReadToEnd();
}
ReadGedcom();
}
private void ReadGedcom()
{
string[] Nodes = GedcomText.Replace("0 #", "\u0646").Split('\u0646');
foreach (string Node in Nodes)
{
string[] SubNode = Node.Replace("\r\n", "\r").Split('\r');
if (SubNode[0].Contains("INDI"))
{
Individuals.Add(ExtractINDI(SubNode));
}
else if (SubNode[0].Contains("FAM"))
{
Families.Add(ExtractFAM(SubNode));
}
}
}
private FAM ExtractFAM(string[] Node)
{
string sFID = Node[0].Replace("# FAM", "");
string sID = "";
string sType = "";
foreach (string Line in Node)
{
// If node is HUSB
if (Line.Contains("1 HUSB "))
{
sType = "PAR";
sID = Line.Replace("1 HUSB ", "").Replace("#", "").Trim();
}
//If node for Wife
else if (Line.Contains("1 WIFE "))
{
sType = "PAR";
sID = Line.Replace("1 WIFE ", "").Replace("#", "").Trim();
}
//if node for multi children
else if (Line.Contains("1 CHIL "))
{
sType = "CHIL";
sID = Line.Replace("1 CHIL ", "").Replace("#", "");
}
}
FAM Fam = new FAM();
Fam.FamID = sFID;
Fam.Type = sType;
Fam.IndiID = sID;
return Fam;
}
private INDI ExtractINDI(string[] Node)
{
//If a individual is found
INDI I = new INDI();
if (Node[0].Contains("INDI"))
{
//Create new Structure
//Add the ID number and remove extra formating
I.ID = Node[0].Replace("#", "").Replace(" INDI", "").Trim();
//Find the name remove extra formating for last name
I.Name = Node[FindIndexinArray(Node, "NAME")].Replace("1 NAME", "").Replace("/", "").Trim();
//Find Sex and remove extra formating
I.Sex = Node[FindIndexinArray(Node, "SEX")].Replace("1 SEX ", "").Trim();
//Deterine if there is a brithday -1 means no
if (FindIndexinArray(Node, "1 BIRT ") != -1)
{
// add birthday to Struct
I.BDay = Node[FindIndexinArray(Node, "1 BIRT ") + 1].Replace("2 DATE ", "").Trim();
}
// deterimin if there is a death tag will return -1 if not found
if (FindIndexinArray(Node, "1 DEAT ") != -1)
{
//convert Y or N to true or false ( defaults to False so no need to change unless Y is found.
if (Node[FindIndexinArray(Node, "1 DEAT ")].Replace("1 DEAT ", "").Trim() == "Y")
{
//set death
I.Dead = true;
}
}
}
return I;
}
private int FindIndexinArray(string[] Arr, string search)
{
int Val = -1;
for (int i = 0; i < Arr.Length; i++)
{
if (Arr[i].Contains(search))
{
Val = i;
}
}
return Val;
}
}
}
Implementation:
using System;
using System.Windows.Forms;
using GedcomReader;
namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
string path = #"C:\mostrecent.ged";
string outpath = #"C:\gedcom.txt";
Gedcom GD = new Gedcom(path);
GraphvizWriter GVW = new GraphvizWriter("Family Tree");
foreach(Gedcom.INDI I in GD.Individuals)
{
string color = "pink";
if (I.Sex == "M")
{
color = "blue";
}
GVW.ListNode(I.ID, I.Name, "filled", color, "circle");
if (I.ID == "ind23800")
{MessageBox.Show("stop");}
//"ind23800" [ label="Sarah Mandley",shape="circle",style="filled",color="pink" ];
}
foreach (Gedcom.FAM F in GD.Families)
{
if (F.Type == "par")
{
GVW.ConnNode(F.FamID, F.IndiID);
}
else if (F.Type =="chil")
{
GVW.ConnNode(F.IndiID, F.FamID);
}
}
string x = GVW.SB.ToString();
GVW.SaveFile(outpath);
MessageBox.Show("done");
}
}
I am particularly interested in if anything could be done about the structures i don't know if how i use them in the implementation is the greatest but again it works
Thanks alot

Quick thoughts:
Nested types should not be visible.
ValueTypes (structs) should be immutable.
Fields (class variables) should not be public. Expose them via properties instead.
Check passed arguments for invalid values, like null.

It might be more readable. It's hard to read and understand.
You may study SOLID principles (http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod)
Robert C. Martin gave good presentation on Oredev 2008 about clean code (http://www.oredev.org/topmenu/video/agile/robertcmartincleancodeiiifunctions.4.5a2d30d411ee6ffd2888000779.html)
Some recomended books to read about code readability:
Kent Beck "Implemetation patterns"
Robert C Martin "Clean Code" Robert C
Martin "Agile Principles, Patterns
and Practices in C#"

I suggest you check this place out: http://refactormycode.com/.
For some quick things, your naming is the biggest thing I would start to change.
No need to use ALL-CAPS or abbreviated terms.
Also, FxCop will help with a lot of suggested changes. For example, FindIndexinArray would be named FindIndexInArray.
EDIT:
I don't know if this is a bug in your code or by-design, but in FindIndexinArray, you don't break from your loop once you find a match. Do you want the first (break) or last (no break) match in the array?

Related

fastest starts with search algorithm

I need to implement a search algorithm which only searches from the start of the string rather than anywhere within the string.
I am new to algorithms but from what I can see it seems as though they go through the string and find any occurrence.
I have a collection of strings (over 1 million) which need to be searched everytime the user types a keystroke.
EDIT:
This will be an incremental search. I currently have it implemented with the following code and my searches are coming back ranging between 300-700ms from over 1 million possible strings. The collection isnt ordered but there is no reason it couldnt be.
private ICollection<string> SearchCities(string searchString) {
return _cityDataSource.AsParallel().Where(x => x.ToLower().StartsWith(searchString)).ToArray();
}
I've adapted the code from this article from Visual Studio Magazine that implements a Trie.
The following program demonstrates how to use a Trie to do fast prefix searching.
In order to run this program, you will need a text file called "words.txt" with a large list of words. You can download one from Github here.
After you compile the program, copy the "words.txt" file into the same folder as the executable.
When you run the program, type a prefix (such as prefix ;)) and press return, and it will list all the words beginning with that prefix.
This should be a very fast lookup - see the Visual Studio Magazine article for more details!
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace ConsoleApp1
{
class Program
{
static void Main()
{
var trie = new Trie();
trie.InsertRange(File.ReadLines("words.txt"));
Console.WriteLine("Type a prefix and press return.");
while (true)
{
string prefix = Console.ReadLine();
if (string.IsNullOrEmpty(prefix))
continue;
var node = trie.Prefix(prefix);
if (node.Depth == prefix.Length)
{
foreach (var suffix in suffixes(node))
Console.WriteLine(prefix + suffix);
}
else
{
Console.WriteLine("Prefix not found.");
}
Console.WriteLine();
}
}
static IEnumerable<string> suffixes(Node parent)
{
var sb = new StringBuilder();
return suffixes(parent, sb).Select(suffix => suffix.TrimEnd('$'));
}
static IEnumerable<string> suffixes(Node parent, StringBuilder current)
{
if (parent.IsLeaf())
{
yield return current.ToString();
}
else
{
foreach (var child in parent.Children)
{
current.Append(child.Value);
foreach (var value in suffixes(child, current))
yield return value;
--current.Length;
}
}
}
}
public class Node
{
public char Value { get; set; }
public List<Node> Children { get; set; }
public Node Parent { get; set; }
public int Depth { get; set; }
public Node(char value, int depth, Node parent)
{
Value = value;
Children = new List<Node>();
Depth = depth;
Parent = parent;
}
public bool IsLeaf()
{
return Children.Count == 0;
}
public Node FindChildNode(char c)
{
return Children.FirstOrDefault(child => child.Value == c);
}
public void DeleteChildNode(char c)
{
for (var i = 0; i < Children.Count; i++)
if (Children[i].Value == c)
Children.RemoveAt(i);
}
}
public class Trie
{
readonly Node _root;
public Trie()
{
_root = new Node('^', 0, null);
}
public Node Prefix(string s)
{
var currentNode = _root;
var result = currentNode;
foreach (var c in s)
{
currentNode = currentNode.FindChildNode(c);
if (currentNode == null)
break;
result = currentNode;
}
return result;
}
public bool Search(string s)
{
var prefix = Prefix(s);
return prefix.Depth == s.Length && prefix.FindChildNode('$') != null;
}
public void InsertRange(IEnumerable<string> items)
{
foreach (string item in items)
Insert(item);
}
public void Insert(string s)
{
var commonPrefix = Prefix(s);
var current = commonPrefix;
for (var i = current.Depth; i < s.Length; i++)
{
var newNode = new Node(s[i], current.Depth + 1, current);
current.Children.Add(newNode);
current = newNode;
}
current.Children.Add(new Node('$', current.Depth + 1, current));
}
public void Delete(string s)
{
if (!Search(s))
return;
var node = Prefix(s).FindChildNode('$');
while (node.IsLeaf())
{
var parent = node.Parent;
parent.DeleteChildNode(node.Value);
node = parent;
}
}
}
}
A couple of thoughts:
First, your million strings need to be ordered, so that you can "seek" to the first matching string and return strings until you no longer have a match...in order (seek via C# List<string>.BinarySearch, perhaps). That's how you touch the least number of strings possible.
Second, you should probably not try to hit the string list until there's a pause in input of at least 500 ms (give or take).
Third, your queries into the vastness should be async and cancelable, because it's certainly going to be the case that one effort will be superseded by the next keystroke.
Finally, any subsequent query should first check that the new search string is an append of the most recent search string...so that you can begin your subsequent seek from the last seek (saving lots of time).
I suggest using linq.
string x = "searchterm";
List<string> y = new List<string>();
List<string> Matches = y.Where(xo => xo.StartsWith(x)).ToList();
Where x is your keystroke search text term, y is your collection of strings to search, and Matches is the matches from your collection.
I tested this with the first 1 million prime numbers, here is the code adapted from above:
Stopwatch SW = new Stopwatch();
SW.Start();
string x = "2";
List<string> y = System.IO.File.ReadAllText("primes1.txt").Split(' ').ToList();
y.RemoveAll(xo => xo == " " || xo == "" || xo == "\r\r\n");
List <string> Matches = y.Where(xo => xo.StartsWith(x)).ToList();
SW.Stop();
Console.WriteLine("matches: " + Matches.Count);
Console.WriteLine("time taken: " + SW.Elapsed.TotalSeconds);
Console.Read();
Result is:
matches: 77025
time taken: 0.4240604
Of course this is testing against numbers and I don't know whether linq converts the values before, or if numbers make any difference.

Loop through list, match word call description from list

I am having a problem constructing a loop WHICH will COMPARE a VAR(userSelection) against the name ITEMS in my LIST(Listings). The goal is that if userSelection MATCHES name, getDescription will Console.WriteLine the GetDefinition and display the definition of the word in the list matched. Most of my code is working, and i've been working on this assignment for a week.
I'm very much a newb, please assume I know nothing. All help is appreciated. I think this would be a while loop, but i've played with all the loops now and am lost and confused. I'm a newb, please use small words and be as detailed as you can afford to be. It's greatly appreciated. Thank you.
My C# Program:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;// Needed for Streaming...
using System.IO;// Needed for Streaming...
namespace a090___StreamReader_DictionarySearch
{
class Program
{
private const String FILE_NAME = "dictionary.txt";//establish text file instance
public void Play()
{
do
{
DisplayTitle();
List<Listing> items = LoadListings();//create a list of WordDef objects
Console.Write(string.Join(" | ", items.Select(x => x.GetName()))); //Console.Write("\b \b");// Backspace would also of worked
DisplayText("\n\nPlease enter a word from the selections about to see it's definition");// Nice use of PROMPT
String userSelection = Console.ReadLine().ToLower();//Capture input
//loop through all of the listings, and compare each one to userSelection
//Then once it equals print definition
bool found = false;
foreach (Listing item in items)
{
if (userSelection == item.GetName())
{
Console.WriteLine("You selected: " + userSelection +
"\nWhich Means: " + item.GetDefinition());
found = true;
break;
}
}
if (!found)
{ Console.WriteLine("I'm sorry, I don't have a match for that."); }
} while (PlayAgain());
Salutation();
}
//ToolBox -- my program specific tools
public List<Listing> LoadListings()//load entries display as list
{
StreamReader fileIn = new StreamReader(FILE_NAME);
List<Listing> entry = new List<Listing>();
//loop through every line of the file
while (!fileIn.EndOfStream)
{
String line = fileIn.ReadLine();
String[] pieces = line.Split(':');
if (pieces.Length < 1) continue;//error handling - set to length of text items
Listing myListing = new Listing(pieces[0], pieces[1]);
entry.Add(myListing);
}
fileIn.Close(); return entry;
}
//MaxBox -- my useful tools
public void DisplayText(String StringNameIs)
{ Console.WriteLine(StringNameIs); }//Where are we?
public Boolean PlayAgain()
{
Console.Write("\n\nDo you select again? (y)es or (n)o: ");
String command = Console.ReadLine().ToLower();
if (command == "y" || command == "yes") return true;
return false;
}
public void Salutation()
{ Console.Clear(); Console.WriteLine("Ti Do - oh - oh Ti Do -- So Do!"); } //The last line from the 'do-re-mi' song from the Sound of Music
public void DisplayTitle()
{ Console.Clear(); Console.WriteLine(">>>-- A Dictionary of Sounds --<<< \n"); } //Announce Our Program
static void Main(string[] args)
{
Program DictionaryLookup = new Program();
DictionaryLookup.Play();
Console.Read();
}
}
}
My Class
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace a090___StreamReader_DictionarySearch
{
class Listing
{
private String name;
private String definition;
public Listing(String name, String definition)
{ this.name = name;
this.definition = definition;}
public String GetName() {return name;}
public String GetDefinition() {return definition; }
}
}
My Text File
Doe: a deer, a female deer
Ray: a drop of golden sun
Me: a name I call myself
Far: a long, long way to run
Sew: a needle pulling thread
La: a note to follow Sew
Tea: a drink with jam and bread
This is untested, but should work, using your existing code.
Assuming each "name" (doe, ray, etc) only occurs once (which they do), then you can use Linq's "SingleOrDefault", which will return null if no match is found.
var selection = items.SingleOrDefault(x => x.GetName() == userSelection);
if (selection == null)
Console.WriteLine("I'm sorry, I don't have a match for that.");
else
Console.WriteLine("You selected: " + userSelection +
"\nWhich Means: " + selection.GetDefinition());
To ignore case during comparison, try modifying the above:
... items.SingleOrDefault(x => String.Compare(x.GetName(), userSelection, true));
There are a number of other things you could change here, but perhaps it won't matter for your assignment. For example, I'd eliminate the private variables in your Listing class and change the public "get" methods into properties:
public String Name { get; private set; }
public String Definition { get; private set; }
Substitute the code
while (true)
{
if (userSelection == name)
{Console.WriteLine("You selected: " + Listing.userSelection() +
"\nWhich Means: " + Listing.items.GetDefinition());}
}
else { Console.WriteLine("I'm sorry, I don't have a match for that."); }
with this
bool found = false;
foreach (Listing item in items)
{
if (userSelection == item.GetName().ToLower())
{
Console.WriteLine("You selected: " + userSelection +
"\nWhich Means: " + item.GetDefinition());
found = true;
break;
}
}
if (!found)
{ Console.WriteLine("I'm sorry, I don't have a match for that."); }
I have use the foreach statement; with this statement you can iterate all items of your collection.
Inside the foreach loop I check if the element Name is equal with the user input, if match I set a bool variable indicating element found.
After the loop if element not found print the message.
NB Obviously this code can be written more concisely with LINQ.

Removing quotes in file helpers

I have a .csv file(I have no control over the data) and for some reason it has everything in quotes.
"Date","Description","Original Description","Amount","Type","Category","Name","Labels","Notes"
"2/02/2012","ac","ac","515.00","a","b","","javascript://"
"2/02/2012","test","test","40.00","a","d","c",""," "
I am using filehelpers and I am wondering what the best way to remove all these quotes would be? Is there something that says "if I see quotes remove. If no quotes found do nothing"?
This messes with the data as I will have "\"515.00\"" with unneeded extra quotes(especially since I want in this case it to be a decimal not a string".
I am also not sure what the "javascript" is all about and why it was generated but this is from a service I have no control over.
edit
this is how I consume the csv file.
using (TextReader textReader = new StreamReader(stream))
{
engine.ErrorManager.ErrorMode = ErrorMode.SaveAndContinue;
object[] transactions = engine.ReadStream(textReader);
}
You can use the FieldQuoted attribute described best on the attributes page here. Note that the attribute can be applied to any FileHelpers field (even if it type Decimal). (Remember that the FileHelpers class describes the spec for your import file.. So when you mark a Decimal field as FieldQuoted, you are saying in the file, this field will be quoted.)
You can even specify whether or not the quotes are optional with
[FieldQuoted('"', QuoteMode.OptionalForBoth)]
Here is a console application which works with your data:
class Program
{
[DelimitedRecord(",")]
[IgnoreFirst(1)]
public class Format1
{
[FieldQuoted]
[FieldConverter(ConverterKind.Date, "d/M/yyyy")]
public DateTime Date;
[FieldQuoted]
public string Description;
[FieldQuoted]
public string OriginalDescription;
[FieldQuoted]
public Decimal Amount;
[FieldQuoted]
public string Type;
[FieldQuoted]
public string Category;
[FieldQuoted]
public string Name;
[FieldQuoted]
public string Labels;
[FieldQuoted]
[FieldOptional]
public string Notes;
}
static void Main(string[] args)
{
var engine = new FileHelperEngine(typeof(Format1));
// read in the data
object[] importedObjects = engine.ReadString(#"""Date"",""Description"",""Original Description"",""Amount"",""Type"",""Category"",""Name"",""Labels"",""Notes""
""2/02/2012"",""ac"",""ac"",""515.00"",""a"",""b"","""",""javascript://""
""2/02/2012"",""test"",""test"",""40.00"",""a"",""d"",""c"","""","" """);
// check that 2 records were imported
Assert.AreEqual(2, importedObjects.Length);
// check the values for the first record
Format1 customer1 = (Format1)importedObjects[0];
Assert.AreEqual(DateTime.Parse("2/02/2012"), customer1.Date);
Assert.AreEqual("ac", customer1.Description);
Assert.AreEqual("ac", customer1.OriginalDescription);
Assert.AreEqual(515.00, customer1.Amount);
Assert.AreEqual("a", customer1.Type);
Assert.AreEqual("b", customer1.Category);
Assert.AreEqual("", customer1.Name);
Assert.AreEqual("javascript://", customer1.Labels);
Assert.AreEqual("", customer1.Notes);
// check the values for the second record
Format1 customer2 = (Format1)importedObjects[1];
Assert.AreEqual(DateTime.Parse("2/02/2012"), customer2.Date);
Assert.AreEqual("test", customer2.Description);
Assert.AreEqual("test", customer2.OriginalDescription);
Assert.AreEqual(40.00, customer2.Amount);
Assert.AreEqual("a", customer2.Type);
Assert.AreEqual("d", customer2.Category);
Assert.AreEqual("c", customer2.Name);
Assert.AreEqual("", customer2.Labels);
Assert.AreEqual(" ", customer2.Notes);
}
}
(Note, your first line of data seems to have 8 fields instead of 9, so I marked the Notes field with FieldOptional).
Here’s one way of doing it:
string[] lines = new string[]
{
"\"Date\",\"Description\",\"Original Description\",\"Amount\",\"Type\",\"Category\",\"Name\",\"Labels\",\"Notes\"",
"\"2/02/2012\",\"ac\",\"ac\",\"515.00\",\"a\",\"b\",\"\",\"javascript://\"",
"\"2/02/2012\",\"test\",\"test\",\"40.00\",\"a\",\"d\",\"c\",\"\",\" \"",
};
string[][] values =
lines.Select(line =>
line.Trim('"')
.Split(new string[] { "\",\"" }, StringSplitOptions.None)
.ToArray()
).ToArray();
The lines array represents the lines in your sample. Each " character must be escaped as \" in C# string literals.
For each line, we start off by removing the first and last " characters, then proceed to split it into a collection of substrings, using the "," character sequence as the delimiter.
Note that the above code will not work if you have " characters occurring naturally within your values (even if escaped).
Edit: If your CSV is to be read from a stream, all your need to do is:
var lines = new List<string>();
using (var streamReader = new StreamReader(stream))
while (!streamReader.EndOfStream)
lines.Add(streamReader.ReadLine());
The rest of the above code would work intact.
Edit: Given your new code, check whether you’re looking for something like this:
for (int i = 0; i < transactions.Length; ++i)
{
object oTrans = transactions[i];
string sTrans = oTrans as string;
if (sTrans != null &&
sTrans.StartsWith("\"") &&
sTrans.EndsWith("\""))
{
transactions[i] = sTrans.Substring(1, sTrans.Length - 2);
}
}
I have the same predicament and I replace the quotes when I load the value into my list object:
using System;
using System.Collections.Generic;
using System.IO;
using System.Windows.Forms;
namespace WindowsFormsApplication6
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
LoadCSV();
}
private void LoadCSV()
{
List<string> Rows = new List<string>();
string m_CSVFilePath = "<Path to CSV File>";
using (StreamReader r = new StreamReader(m_CSVFilePath))
{
string row;
while ((row = r.ReadLine()) != null)
{
Rows.Add(row.Replace("\"", ""));
}
foreach (var Row in Rows)
{
if (Row.Length > 0)
{
string[] RowValue = Row.Split(',');
//Do something with values here
}
}
}
}
}
}
This code might help which I developed:
using (StreamReader r = new StreamReader("C:\\Projects\\Mactive\\Audience\\DrawBalancing\\CSVFiles\\Analytix_ABC_HD.csv"))
{
string row;
int outCount;
StringBuilder line=new StringBuilder() ;
string token="";
char chr;
string Eachline;
while ((row = r.ReadLine()) != null)
{
outCount = row.Length;
line = new StringBuilder();
for (int innerCount = 0; innerCount <= outCount - 1; innerCount++)
{
chr=row[innerCount];
if (chr != '"')
{
line.Append(row[innerCount].ToString());
}
else if(chr=='"')
{
token = "";
innerCount = innerCount + 1;
for (; innerCount < outCount - 1; innerCount++)
{
chr=row[innerCount];
if(chr=='"')
{
break;
}
token = token + chr.ToString();
}
if(token.Contains(",")){token=token.Replace(",","");}
line.Append(token);
}
}
Eachline = line.ToString();
Console.WriteLine(Eachline);
}
}

String Replacements for Word Merge

using asp.net 4
we do a lot of Word merges at work. rather than using the complicated conditional statements of Word i want to embed my own syntax. something like:
Dear Mr. { select lastname from users where userid = 7 },
Your invoice for this quarter is: ${ select amount from invoices where userid = 7 }.
......
ideally, i'd like this to get turned into:
string.Format("Dear Mr. {0}, Your invoice for this quarter is: ${1}", sqlEval[0], sqlEval[1]);
any ideas?
Well, I don't really recommend rolling your own solution for this, however I will answer the question as asked.
First, you need to process the text and extract the SQL statements. For that you'll need a simple parser:
/// <summary>Parses the input string and extracts a unique list of all placeholders.</summary>
/// <remarks>
/// This method does not handle escaping of delimiters
/// </remarks>
public static IList<string> Parse(string input)
{
const char placeholderDelimStart = '{';
const char placeholderDelimEnd = '}';
var characters = input.ToCharArray();
var placeHolders = new List<string>();
string currentPlaceHolder = string.Empty;
bool inPlaceHolder = false;
for (int i = 0; i < characters.Length; i++)
{
var currentChar = characters[i];
// Start of a placeholder
if (!inPlaceHolder && currentChar == placeholderDelimStart)
{
currentPlaceHolder = string.Empty;
inPlaceHolder = true;
continue;
}
// Start of a placeholder when we already have one
if (inPlaceHolder && currentChar == placeholderDelimStart)
throw new InvalidOperationException("Unexpected character detected at position " + i);
// We found the end marker while in a placeholder - we're done with this placeholder
if (inPlaceHolder && currentChar == placeholderDelimEnd)
{
if (!placeHolders.Contains(currentPlaceHolder))
placeHolders.Add(currentPlaceHolder);
inPlaceHolder = false;
continue;
}
// End of a placeholder with no matching start
if (!inPlaceHolder && currentChar == placeholderDelimEnd)
throw new InvalidOperationException("Unexpected character detected at position " + i);
if (inPlaceHolder)
currentPlaceHolder += currentChar;
}
return placeHolders;
}
Okay, so that will get you a list of SQL statements extracted from the input text. You'll probably want to tweak it to use properly typed parser exceptions and some input guards (which I elided for clarity).
Now you just need to replace those placeholders with the results of the evaluated SQL:
// Sample input
var input = "Hello Mr. {select firstname from users where userid=7}";
string output = input;
var extractedStatements = Parse(input);
foreach (var statement in extractedStatements)
{
// Execute the SQL statement
var result = Evaluate(statement);
// Update the output with the result of the SQL statement
output = output.Replace("{" + statement + "}", result);
}
This is obviously not the most efficient way to do this, but I think it sufficiently demonstrates the concept without muddying the waters.
You'll need to define the Evaluate(string) method. This will handle executing the SQL.
I just finished building a proprietary solution like this for a law firm here.
I evaluated a product called Windward reports. It's a tad pricy, esp if you need a lot of copies, but for one user it's not bad.
it can pull from XML or SQL data sources (or more if I remember).
Might be worth a look (and no I don't work for 'em, just evaluated their stuff)
You might want to check out the razor engine project on codeplex
http://razorengine.codeplex.com/
Using SQL etc within your template looks like a bad idea. I'd suggest you make a ViewModel for each template.
The Razor thing is really easy to use. Just add a reference, import the namespace, and call the Parse method like so:
(VB guy so excuse syntax!)
MyViewModel myModel = new MyViewModel("Bob",150.00); //set properties
string myTemplate = "Dear Mr. #Model.FirstName, Your invoice for this quarter is: #Model.InvoiceAmount";
string myOutput = Razor.Parse(myTemplate, myModel);
Your string can come from anywhere - I use this with my templates stored in a database, you could equally load it from files or whatever. It's very powerful as a view engine, you can do conditional stuff, loops, etc etc.
i ended up rolling my own solution but thanks. i really dislike if statements. i'll need to refactor them out. here it is:
var mailingMergeString = new MailingMergeString(input);
var output = mailingMergeString.ParseMailingMergeString();
public class MailingMergeString
{
private string _input;
public MailingMergeString(string input)
{
_input = input;
}
public string ParseMailingMergeString()
{
IList<SqlReplaceCommand> sqlCommands = new List<SqlReplaceCommand>();
var i = 0;
const string openBrace = "{";
const string closeBrace = "}";
while (string.IsNullOrWhiteSpace(_input) == false)
{
var sqlReplaceCommand = new SqlReplaceCommand();
var open = _input.IndexOf(openBrace) + 1;
var close = _input.IndexOf(closeBrace);
var length = close != -1 ? close - open : _input.Length;
var newInput = _input.Substring(close + 1);
var nextClose = newInput.Contains(openBrace) ? newInput.IndexOf(openBrace) : newInput.Length;
if (i == 0 && open > 0)
{
sqlReplaceCommand.Text = _input.Substring(0, open - 1);
_input = _input.Substring(open - 1);
}
else
{
sqlReplaceCommand.Command = _input.Substring(open, length);
sqlReplaceCommand.PlaceHolder = openBrace + i + closeBrace;
sqlReplaceCommand.Text = _input.Substring(close + 1, nextClose);
sqlReplaceCommand.NewInput = _input.Substring(close + 1);
_input = newInput.Contains(openBrace) ? sqlReplaceCommand.NewInput : string.Empty;
}
sqlCommands.Add(sqlReplaceCommand);
i++;
}
return sqlCommands.GetParsedString();
}
internal class SqlReplaceCommand
{
public string Command { get; set; }
public string SqlResult { get; set; }
public string PlaceHolder { get; set; }
public string Text { get; set; }
protected internal string NewInput { get; set; }
}
}
internal static class SqlReplaceExtensions
{
public static string GetParsedString(this IEnumerable<MailingMergeString.SqlReplaceCommand> sqlCommands)
{
return sqlCommands.Aggregate("", (current, replaceCommand) => current + (replaceCommand.PlaceHolder + replaceCommand.Text));
}
}

search text file using c# and display the line number and the complete line that contains the search keyword

I require help to search a text file (log file) using c# and display the line number and the complete line that contains the search keyword.
This is a slight modification from: http://msdn.microsoft.com/en-us/library/aa287535%28VS.71%29.aspx
int counter = 0;
string line;
// Read the file and display it line by line.
System.IO.StreamReader file = new System.IO.StreamReader("c:\\test.txt");
while((line = file.ReadLine()) != null)
{
if ( line.Contains("word") )
{
Console.WriteLine (counter.ToString() + ": " + line);
}
counter++;
}
file.Close();
Bit late to the game on this one, but happened across this post and thought I'd add an alternative answer.
foreach (var match in File.ReadLines(#"c:\LogFile.txt")
.Select((text, index) => new { text, lineNumber = index+ 1 })
.Where(x => x.text.Contains("SEARCHWORD")))
{
Console.WriteLine("{0}: {1}", match.lineNumber, match.text);
}
This uses:
File.ReadLines, which eliminates the need for a StreamReader, and it also plays nicely with LINQ's Where clause to return a filtered set of lines from a file.
The overload of Enumerable.Select that returns each element's index, which you can then add 1 to, to get the line number for the matching line.
Sample Input:
just a sample line
another sample line
first matching SEARCHWORD line
not a match
...here's aSEARCHWORDmatch
SEARCHWORD123
asdfasdfasdf
Output:
3: first matching SEARCHWORD line
5: ...here's aSEARCHWORDmatch
6: SEARCHWORD123
To export do Excel you can use the CSV file format, like the Pessimist wrote. If you are uncertain about what to write, try entering some data in MS Excel and click on "Save As" option in the Menu and choose CSV as file type.
Take care when writing a CSV file format as in some languages the default for separating values is not the comma. In brazilian portuguese, for example, the default is comma as decimal separator, dot as thousands separator and semicolon for separating values. Mind the culture when writing that.
The other alternative is using horizontal tabs as separators. Experiment to write a string, press the TAB key and then another string and paste it into Microsoft Excel. It is the default separator in that program.
If you're using an ad-hoc solution to your specific problem, either alternatives can be used without much thinking. If you are programming something to be used by other persons (or in other environments), mind the culture specific differences.
Oh, I've just remembered now: you can write a Spreadsheet using XML, you can do that with only the .NET package. I've done that years ago with C# .NET 2.0
I had a requirement where I needed to search through a list of directories looking for particular file types, containing a specific search terms but excluding other terms.
For example let's say you wanted to look through C:\DEV and only find .cs files that have terms "WriteLine" and "Readline" but not the term "hello".
I decided to write a small c# utility to do just this:
This is how you call it:
class Program
{
//Syntax:
//FileSearch <Directory> EXT <ext1> <ext2> LIKE <TERM1> <TERM2> NOT <TERM3> <TERM4>
//Example:
//Search for all files recursively in C:\Dev with an extension of cs that contain either "WriteLine" or "Readline" but not "hello"
//FileSearch C:\DEV EXT .cs LIKE "WriteLine" "ReadLine" NOT "hello"
static void Main(string[] args)
{
if (args.Length == 0)
{
Console.WriteLine("FileSearch <Directory> EXT <EXT1> LIKE <TERM1> <TERM2> NOT <TERM3> <TERM4>");
return;
}
Search s = new Search(args);
s.DoSearch();
}
}
This is the implementation:
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
class Hit
{
public string File { get; set; }
public int LineNo { get; set; }
public int Pos { get; set; }
public string Line { get; set; }
public string SearchTerm { get; set; }
public void Print()
{
Console.WriteLine(File);
Console.Write("(" + LineNo + "," + Pos + ") ");
Console.WriteLine(Line);
}
}
class Search
{
string rootDir;
List<string> likeTerms;
List<string> notTerms;
List<string> extensions;
List<Hit> hitList = new List<Hit>();
//FileSearch <Directory> EXT .CS LIKE "TERM1" "TERM2" NOT "TERM3" "TERM4"
public Search(string[] args)
{
this.rootDir = args[0];
this.extensions = ParseTerms("EXT", "LIKE", args);
this.likeTerms = ParseTerms("LIKE", "NOT", args);
this.notTerms = ParseTerms("NOT", "", args);
Print();
}
public void Print()
{
Console.WriteLine("Search Dir:" + rootDir);
Console.WriteLine("Extensions:");
foreach (string s in extensions)
Console.WriteLine(s);
Console.WriteLine("Like Terms:");
foreach (string s in likeTerms)
Console.WriteLine(s);
Console.WriteLine("Not Terms:");
foreach (string s in notTerms)
Console.WriteLine(s);
}
private List<string> ParseTerms(string keyword, string stopword, string[] args)
{
List<string> list = new List<string>();
bool collect = false;
foreach (string arg in args)
{
string argu = arg.ToUpper();
if (argu == stopword)
break;
if (argu == keyword)
{
collect = true;
continue;
}
if(collect)
list.Add(arg);
}
return list;
}
private void SearchDir(string dir)
{
foreach (string file in Directory.GetFiles(dir, "*.*"))
{
string extension = Path.GetExtension(file);
if (extension != null && extensions.Contains(extension))
SearchFile(file);
}
foreach (string subdir in Directory.GetDirectories(dir))
SearchDir(subdir);
}
private void SearchFile(string file)
{
using (StreamReader sr = new StreamReader(file))
{
int lineNo = 0;
while (!sr.EndOfStream)
{
int pos = 0;
string term = "";
string line = sr.ReadLine();
lineNo++;
//Look through each likeTerm
foreach(string likeTerm in likeTerms)
{
pos = line.IndexOf(likeTerm, StringComparison.OrdinalIgnoreCase);
if (pos >= 0)
{
term = likeTerm;
break;
}
}
//If found make sure not in the not term
if (pos >= 0)
{
bool notTermFound = false;
//Look through each not Term
foreach (string notTerm in notTerms)
{
if (line.IndexOf(notTerm, StringComparison.OrdinalIgnoreCase) >= 0)
{
notTermFound = true;
break;
}
}
//If not term not found finally add to hitList
if (!notTermFound)
{
Hit hit = new Hit();
hit.File = file;
hit.LineNo = lineNo;
hit.Pos = pos;
hit.Line = line;
hit.SearchTerm = term;
hitList.Add(hit);
}
}
}
}
}
public void DoSearch()
{
SearchDir(rootDir);
foreach (Hit hit in hitList)
{
hit.Print();
}
}
}

Categories