C# - Can't write list into the txt file - c#

I have a problem with write list into txt file. If I run my Save method it makes only blank txt file. I fill this list from txt file and it works fine so I'm sure it isn't empty(I can see my appointments in calendar). There are my methods.
EDIT
Ok, I know where issue is. _appointments list in Load isn't the same of _appointments list in Save. I don't know why. I don't have any other lists. It's the same but it isn't :/
public bool Load()
{
DateTime start = new DateTime(2000,01,01);
CultureInfo enUS = new CultureInfo("en-US");
int length = 0;
string screenDiscription = "";
bool occursOnDate = false;
string line;
int i = 1;
StreamReader sr = new StreamReader("appointments.txt");
if (!File.Exists("appointments.txt"))
{
return false;
}
while ((line = sr.ReadLine()) != null)
{
if (i % 4 == 1)
{
start = DateTime.ParseExact(line, "ddMMyyyy HHmm", enUS);
}
if (i % 4 == 2)
{
length = int.Parse(line);
}
if (i % 4 == 3)
{
screenDiscription = line;
}
if (i % 4 == 0)
{
Appointment appointment = new Appointment(start, length, screenDiscription, occursOnDate);
_appointments.Add(appointment);
}
i++;
}
sr.Close();
return true;
}
public bool Save()
{
StreamWriter sw = new StreamWriter("appointments.txt");
if (File.Exists("appointments.txt"))
{
foreach(IAppointment item in _appointments)
{
sw.WriteLine(item.Start);
sw.WriteLine(item.Length);
sw.WriteLine(item.DisplayableDescription);
sw.WriteLine(" ");
}
sw.Close();
return true;
}
else
{
File.Create("appointments.txt");
foreach (IAppointment item in _appointments)
{
sw.WriteLine(item.Start);
sw.WriteLine(item.Length);
sw.WriteLine(item.DisplayableDescription);
sw.WriteLine(" ");
}
sw.Close();
return true;
}
}

I have refactored your Save method, but I'm unable to test it as I don't have your IAppointment and Appointment:
public void Save()
{
var builder = new StringBuilder()
foreach (IAppointment item in _appointments)
{
builder.AppendLine(item.Start);
builder.AppendLine(item.Length);
builder.AppendLine(item.DisplayableDescription);
builder.AppendLine(" ");
}
File.WriteAllText("appointments.txt", builder.ToString());
}
Note a few things here: I think your bool return type is superfluous since the method always returns true on all code paths; thus I've changed it to void. Also, I'm using a StringBuilder to construct the file contents, and then the built-in File.WriteAllText method which abstracts out the IO operations you would normally have to mess around with for opening the stream, the stream writer, closing, etc.
I'm not sure if this will resolve your issue because, as I stated, I can't test it and I'm not sure what exactly is wrong with the code you have, but at the very least it's probably a lot cleaner and easier to work with.

Related

Counting/sorting characters in a text file

I am trying to write a program that reads a text file, sorts it by character, and keeps track of how many times each character appears in the document. This is what I have so far.
class Program
{
static void Main(string[] args)
{
CharFrequency[] Charfreq = new CharFrequency[128];
try
{
string line;
System.IO.StreamReader file = new System.IO.StreamReader(#"C:\Users\User\Documents\Visual Studio 2013\Projects\Array_Project\wap.txt");
while ((line = file.ReadLine()) != null)
{
int ch = file.Read();
if (Charfreq.Contains(ch))
{
}
}
file.Close();
Console.ReadLine();
}
catch (Exception e)
{
Console.WriteLine("The process failed: {0}", e.ToString());
}
}
}
My question is, what should go in the if statement here?
I also have a Charfrequency class, which I'll include here in case it is helpful/necessary that I include it (and yes, it is necessary that I use an array versus a list or arraylist).
public class CharFrequency
{
private char m_character;
private long m_count;
public CharFrequency(char ch)
{
Character = ch;
Count = 0;
}
public CharFrequency(char ch, long charCount)
{
Character = ch;
Count = charCount;
}
public char Character
{
set
{
m_character = value;
}
get
{
return m_character;
}
}
public long Count
{
get
{
return m_count;
}
set
{
if (value < 0)
value = 0;
m_count = value;
}
}
public void Increment()
{
m_count++;
}
public override bool Equals(object obj)
{
bool equal = false;
CharFrequency cf = new CharFrequency('\0', 0);
cf = (CharFrequency)obj;
if (this.Character == cf.Character)
equal = true;
return equal;
}
public override int GetHashCode()
{
return m_character.GetHashCode();
}
public override string ToString()
{
String s = String.Format("'{0}' ({1}) = {2}", m_character, (byte)m_character, m_count);
return s;
}
}
Have a look at this post.
https://codereview.stackexchange.com/questions/63872/counting-the-number-of-character-occurrences
It uses LINQ to achieve your goal
You shouldn't use Contains
first you need to initialize your Charfreq array:
CharFrequency[] Charfreq = new CharFrequency[128];
for (int i = 0; i < Charferq.Length; i++)
{
Charfreq[i] = new CharFrequency((char)i);
}
try
then you can
int ch;
// -1 means that there are no more characters to read,
// otherwise ch is the char read
while ((ch = file.Read()) != -1)
{
CharFrequency cf = new CharFrequency((char)ch);
// This works because CharFrequency overloads the
// Equals method, and the Equals method checks only
// for the Character property of CharFrequency
int ix = Array.IndexOf(Charfreq, cf);
// if there is the "right" charfrequency
if (ix != -1)
{
Charfreq[ix].Increment();
}
}
Note that this isn't the way I would write the program. This is the minimum changes needed to make your program working.
As a sidenote, this program will count the "frequency" of ASCII characters (characters with code <= 127)
CharFrequency cf = new CharFrequency('\0', 0);
cf = (CharFrequency)obj;
And this is an useless initialization:
CharFrequency cf = (CharFrequency)obj;
is enough, otherwise you are creating a CharFrequency just to discard it the line below.
A dictionary is well suited for a task like this. You didn't say which character set and encoding the file was in. So, because Unicode is so common, let's assume the Unicode character set and UTF-8 encoding. (After all, it is the default for .NET, Java, JavaScript, HTML, XML,….) If that's not the case then read the file using the applicable encoding and fix your code because you currently are using UTF-8 in your StreamReader.
Next comes iterating across the "characters". And then incrementing the count for a "character" in the dictionary as it is seen in the text.
Unicode does have a few complex features. One is combining characters, where a base character can be overlaid with diacritics etc. Users view such combinations as one "character", or, as Unicode calls them, graphemes. Thankfully, .NET gives is the StringInfo class that iterates over them as a "text element."
So, if you think about it, using an array would be quite difficult. You'd have to build your own dictionary on top of your array.
The example below uses a Dictionary and is runnable using a LINQPad script. After it creates the dictionary, it orders and dumps it with a nice display.
var path = Path.GetTempFileName();
// Get some text we know is encoded in UTF-8 to simplify the code below
// and contains combining codepoints as a matter of example.
using (var web = new WebClient())
{
web.DownloadFile("http://superuser.com/questions/52671/which-unicode-characters-do-smilies-like-%D9%A9-%CC%AE%CC%AE%CC%83-%CC%83%DB%B6-consist-of", path);
}
// since the question asks to analyze a file
var content = File.ReadAllText(path, Encoding.UTF8);
var frequency = new Dictionary<String, int>();
var itor = System.Globalization.StringInfo.GetTextElementEnumerator(content);
while (itor.MoveNext())
{
var element = (String)itor.Current;
if (!frequency.ContainsKey(element))
{
frequency.Add(element, 0);
}
frequency[element]++;
}
var histogram = frequency
.OrderByDescending(f => f.Value)
// jazz it up with the list of codepoints in each text element
.Select(pair =>
{
var bytes = Encoding.UTF32.GetBytes(pair.Key);
var codepoints = new UInt32[bytes.Length/4];
Buffer.BlockCopy(bytes, 0, codepoints, 0, bytes.Length);
return new {
Count = pair.Value,
textElement = pair.Key,
codepoints = codepoints.Select(cp => String.Format("U+{0:X4}", cp) ) };
});
histogram.Dump(); // For use in LINQPad

reading a .txt file and displaying numbers from the file

I am trying to read a .txt file using c# and displaying its contents but I am getting error as IndexOutOfRangeException with error code as 0xc000013a.
Here's my code:
static void Main(string[] args)
{
StreamReader sStreamReader = new StreamReader("d:\\TEST.txt");
while (!sStreamReader.EndOfStream)
{
string sLine = "";
if (sLine != null)
{
sLine = sStreamReader.ReadLine();
if (sLine != null)
{
string[] rows = sLine.Split(",".ToCharArray());
double a = Convert.ToDouble(rows[1]);
Console.Write(a);
int b = Convert.ToInt32(rows[3]);
Console.WriteLine(b);
Console.WriteLine();
}
}
}
}
my text file is as follows:
1,2,3,4,5,6,7
1,2,3,4,5,6,7
5,6,2,7,3,8,4
3,4,3,4,3
5,3,23,12
12,30000,12,99
I would change it to the following:
static void Main(string[] args)
{
// StreamReader is IDisposable which should be wrapped in a using statement
using (StreamReader reader = new StreamReader(#"d:\TEST.txt"))
{
while (!reader.EndOfStream)
{
string line = reader.ReadLine();
// make sure we have something to work with
if (String.IsNullOrEmpty(line)) continue;
string[] cols = line.Split(',');
// make sure we have the minimum number of columns to process
if (cols.Length < 4) continue;
double a = Convert.ToDouble(cols[1]);
Console.Write(a);
int b = Convert.ToInt32(cols[3]);
Console.WriteLine(b);
Console.WriteLine();
}
}
}
Some notes here:
StreamReader implements IDisposable, so you should wrap it in a using clause so that it is properly disposed of.
Don't name things like "sLine". That form of Hungarian is commonly recognized as seriously bad practice. Even Microsoft says don't do it.
You're dealing with columns, not rows. So that variable should be named appropriately.
Always test to make sure you have all of the columns you need before blindly accessing them.
Normally, I wouldn't use Convert.ToDouble or Convert.ToInt32. It's much safer to use TryParse to make sure it was able to convert. The code you have will blow if cols[1] and cols[3] had non-numeric data.
You can use the # symbol in front of a string to tell the compiler that it doesn't need to be escaped.
It's much cleaner to simply "continue" a loop instead of wrapping it in a if statement.
Setting a String variable to a blank string then immediately setting it to some other value causes the blank to stay in memory for the entire scope. In other words, it's wasting memory. Granted, in this case it's a micro-optimization, but it never hurts to use best practices all of the time.
Have you considered checking for row.Length before accessing row[1] and row[3]
I suspect your empty lines are the problem
Here is how you can do it simpler:
string[] lines = File.ReadAllLines("d:\\TEST.txt");
foreach (var line in lines.Where(line => line.Length > 0))
{
string[] numbers = line.Split(',');
// It checks whether numbers.Length is greater than
// 3 because if maximum index used is 3 (numbers[3])
// than the array has to contain at least 4 elements
if (numbers.Length > 3)
{
double a = Convert.ToDouble(numbers[1]);
Console.Write(a);
int b = Convert.ToInt32(numbers[3]);
Console.Write(b);
Console.WriteLine();
}
}
You should consider to use :
if (!string.IsNullOrEmpty(sLine))
instead of
if (sLine != null)
You have this exceptions because some lines are empty.
However, here is a way you should write your code when using a StreamReader :
using(var reader = new StreamReader(#"d:\\TEST.txt"))
{
string line;
while ((line= reader.ReadLine()) != null)
{
if (string.IsNullOrEmpty(line)) continue;
var rows = line.Split(",".ToCharArray());
var a = Convert.ToDouble(rows[1]);
Console.Write(a);
var b = Convert.ToInt32(rows[3]);
Console.WriteLine(b);
Console.WriteLine();
}
}
Regards,
Kévin

Clearing a line of a txt file by the line ID

I have looked all over for the answer to this, but I can't find it anywhere. I need to be able to clear a line from a txt file by the last integer in the line (the ID number), but I have no idea how to do that. Please help? Basically I was thinking that I need to find the last integer, and if it does not equal to the input, then it would move to the next line until it finds the right integer. Then that line is cleared. Here is some of my code that obviously doesn't work:
public static void TicID(CommandArgs args)
{
if (args.Parameters.Count == 1)
{
if (i == 1)
{
try
{
string idToDelete = args.Parameters[0];
StreamReader idreader = new StreamReader("Tickets.txt");
StreamWriter iddeleter = new StreamWriter("Tickets.txt");
string id = Convert.ToString(idreader.Read());
string line = null;
while (idreader.Peek() >= 0)
{
if (String.Compare(id, idToDelete) == 0)
{
iddeleter.WriteLine(line);
}
else
{
idreader.ReadLine();
}
}
}
The most straightforward way to delete lines is to write the lines that should not be deleted:
var idToDelete = "1";
var path = #"C:\Temp\Test.txt";
var lines = File.ReadAllLines(path);
using (var writer = new StreamWriter(path, false)) {
for (var i = 0; i < lines.Length; i++) {
var line = lines[i];
//assuming it's a CSV file
var cols = line.Split(',');
var id = cols[cols.Length - 1];
if (id != idToDelete) {
writer.WriteLine(line);
}
}
}
This is the LINQ-way:
var lines = from line in File.ReadAllLines(path)
let cols = line.Split(',')
let id = cols[cols.Length - 1]
where id != idToDelete
select line;
File.WriteAllLines(path, lines);
Create a StreamReader object and read line by line into a string array or something like that using StreamReader's instance method ReadLine() and now find your line of choice in your text array and delete it.
Important note:
do { /*see description above*/ } while (streamReader.Peak() != -1);

Manipulating lines of data

I have millions of lines generated from data updated every second which look like this:
104500 4783
104501 8930
104502 21794
104503 21927
104505 5746
104506 9968
104509 5867
104510 46353
104511 7767
104512 4903
The column on the left represents time (hhmmss format), and the column on the right is data which is updated second-by-second. As you can see however, it isn't actually second-by-second, and there are some missing times (10:45:04, 10:45:07, 10:45:08 are missing in this example). My goal is to add in the missing seconds, and to use the data from the previous second for that missing second, like this:
104500 4783
104501 8930
104502 21794
104503 21927
104504 21927 --
104505 5746
104506 9968
104507 9968 --
104508 9968 --
104509 5867
104510 46353
104511 7767
104512 4903
I don't want the "--" in the result, I just put those there to mark the added lines. So far I've tried to accomplish this using StreamReader and StreamWriter, but it doesn't seem like they're going to get me what I want. I'm a newbie programmer and a newbie to C#, so if you could just point me in the right direction, that would be great. I'm really just wondering if this is even possible to do in C#...I've spent a lot of time on MSDN and here on SO looking for a solution to this, but so far haven't found any.
Edit: The lines are in a text file, and I want to store the newly created data in a new text file.
There are a few things you need to put together.
Read a file line-by-line: See here: Reading a Text File One Line at a Time
Writing a file line-by-line : StreamWriter.WriteLine
Keep track of the last read line. (Just use a variable in your while loop where you read the lines)
Check whether there is a gap. Maybe by parsing the first column (string.Split) using TimeSpan.Parse. If there is a gap then write the last read line, incrementing the timespan.
ok, here is the whole shooting match, tested and working against your test data:
public void InjectMissingData()
{
DataLine lastDataLine = null;
using (var writer = new StreamWriter(File.Create("c:\\temp\\out.txt")))
{
using (var reader = new StreamReader("c:\\temp\\in.txt"))
{
while (!reader.EndOfStream)
{
var dataLine = DataLine.Parse(reader.ReadLine());
while (lastDataLine != null && dataLine.Occurence - lastDataLine.Occurence > TimeSpan.FromSeconds(1))
{
lastDataLine = new DataLine(lastDataLine.Occurence + TimeSpan.FromSeconds(1), lastDataLine.Data);
writer.WriteLine(lastDataLine.Line);
}
writer.WriteLine(dataLine.Line);
lastDataLine = dataLine;
}
}
}
}
public class DataLine
{
public static DataLine Parse(string line)
{
var timeString = string.Format("{0}:{1}:{2}", line.Substring(0, 2), line.Substring(2, 2),
line.Substring(4, 2));
return new DataLine(TimeSpan.Parse(timeString), long.Parse(line.Substring(7, line.Length - 7).Trim()));
}
public DataLine(TimeSpan occurence, long data)
{
Occurence = occurence;
Data = data;
}
public TimeSpan Occurence { get; private set; }
public long Data { get; private set; }
public string Line
{
get { return string.Format("{0}{1}{2} {3}",
Occurence.Hours.ToString().PadLeft(2, Char.Parse("0")),
Occurence.Minutes.ToString().PadLeft(2, Char.Parse("0")),
Occurence.Seconds.ToString().PadLeft(2, Char.Parse("0")),
Data); }
}
}
In adition to all answers, considering that you are talking about a huge files, consider use of MemoryMappedFiles, can read here to see how to use them from C#.
This is not performance improvement, but memory improvement definetely is.
So far as inserting new entries between certain ones goes, I would advise reading in the text file into separated lines, and then storing them in a List. That way, you can use the Insert(...) method to insert your new lines. From there, you can write the lines back into the file.
When reading the lines, you can use either of the static helper methods in the System.IO.File class: ReadAllText and ReadAllLines.
Note: I've added links to the MSDN Documentation for each of the methods and classes I've mentioned, since you said you are new to C# and programming in general.
String prevTime;
String prevData;
while(String line = myStreamReader.ReadLine())
{
String[] parts = line.Split(new Char[] { ' ' });
String time = parts[0];
String data = parts[1];
Int32 iPrevTime = Int32.Parse(prevTime);
Int32 iCurrentTime = Int32.Parse(time);
// May need to loop here if you're missing more than one second
if(iCurrentTime > iPrevTime + 1)
AddData((iPrevTime + 1).ToString(), prevData);
AddData(time, data);
prevTime = time;
prevData = data;
}
Here is some pseudo-code to get you started. I think you will want this type of algorithm.
Here's some rough code for you. I'm not properly disposing everything, it's just to get you started.
DateTime lastTime;
string lastValue = null;
StreamReader reader = File.OpenText("path");
StreamWriter writer = new StreamWriter(File.OpenWrite("newPath"));
while (!reader.EndOfStream)
{
string[] lineData = reader.ReadLine().Split(' ');
DateTime currentTime = DateTime.Parse(lineData[0]);
string value = lineData[1];
if (lastValue != null)
{
while (lastTime < currentTime.AddSeconds(-1))
{
lastTime = lastTime.AddSeconds(1);
writer.WriteLine("{0} {1}", lastTime, lastValue);
}
}
writer.WriteLine("{0} {1}", currentTime, value);
lastTime = currentTime;
lastValue = value;
}
This assumes the times are never more than a second apart. If that assumption is wrong, it's easy enough to modify the below so it writes the lastValue in a loop for each second missing.
Update I missed in your example that it can in fact miss multiple seconds. I changed the example below to address that.
using (StreamReader reader = OpenYourInputFile())
using (StreamWriter writer = OpenYourOutputFile())
{
TimeSpan? lastTime;
TimeSpan currentTime, maxDiff = TimeSpan.FromSeconds(1);
string lastValue, currentline, currentValue, format = "{0:hhmmss} {1}";
while( (currentLine = reader.ReadLine()) != null)
{
string[] s = currentLine.Split(' ');
currentTime = DateTime.ParseExact("hhmmss", s[0] CultureInfo.InvariantCulture).TimeOfDay;
currentValue = s[1];
if (lastTime.HasValue && currentTime - lastTime.Value > maxDiff)
{
for(int x = 1; x <= (currentTime - lastTime).Seconds; x++) writer.WriteLine(string.Format(format, DateTime.Today.Add(lastTime).AddSeconds(x), lastValue);
}
writer.WriteLine(string.Format(format, DateTime.Today.Add(currentTime), currentValue);
lastTime = currentTime;
lastValue = currentValue;
}
}
string line;//The line that is read.
string previousLine = "0 0";
int prevTime = 0;
//These "using"'s are so that the resources they use will be freed when the block ( i.e. {} ) is finished.
using (System.IO.StreamReader originalFile = new System.IO.StreamReader("c:\\users\\Me\\t.txt"))
using (System.IO.StreamWriter newFile = new System.IO.StreamWriter("c:\\users\\Me\\t2.txt"))
{
while ((line = originalFile.ReadLine()) != null)
{
//"Split" changes the words in "line" (- that are separated by a space) to an array.
//"Parse" takes the first in that array (by using "[0]") and changes it into an integer.
int time = int.Parse(line.Split(' ')[0]);
while (prevTime != 0 && time > ++prevTime) newFile.WriteLine(prevTime.ToString() + " " + previousLine.Split(' ')[1]);
previousLine = line;
prevTime = time;
newFile.WriteLine(line);
}
}

CSV string handling

Typical way of creating a CSV string (pseudocode):
Create a CSV container object (like a StringBuilder in C#).
Loop through the strings you want to add appending a comma after each one.
After the loop, remove that last superfluous comma.
Code sample:
public string ReturnAsCSV(ContactList contactList)
{
StringBuilder sb = new StringBuilder();
foreach (Contact c in contactList)
{
sb.Append(c.Name + ",");
}
sb.Remove(sb.Length - 1, 1);
//sb.Replace(",", "", sb.Length - 1, 1)
return sb.ToString();
}
I like the idea of adding the comma by checking if the container is empty, but doesn't that mean more processing as it needs to check the length of the string on each occurrence?
I feel that there should be an easier/cleaner/more efficient way of removing that last comma. Any ideas?
You could use LINQ to Objects:
string [] strings = contactList.Select(c => c.Name).ToArray();
string csv = string.Join(",", strings);
Obviously that could all be done in one line, but it's a bit clearer on two.
Your code not really compliant with full CSV format. If you are just generating CSV from data that has no commas, leading/trailing spaces, tabs, newlines or quotes, it should be fine. However, in most real-world data-exchange scenarios, you do need the full imlementation.
For generation to proper CSV, you can use this:
public static String EncodeCsvLine(params String[] fields)
{
StringBuilder line = new StringBuilder();
for (int i = 0; i < fields.Length; i++)
{
if (i > 0)
{
line.Append(DelimiterChar);
}
String csvField = EncodeCsvField(fields[i]);
line.Append(csvField);
}
return line.ToString();
}
static String EncodeCsvField(String field)
{
StringBuilder sb = new StringBuilder();
sb.Append(field);
// Some fields with special characters must be embedded in double quotes
bool embedInQuotes = false;
// Embed in quotes to preserve leading/tralining whitespace
if (sb.Length > 0 &&
(sb[0] == ' ' ||
sb[0] == '\t' ||
sb[sb.Length-1] == ' ' ||
sb[sb.Length-1] == '\t' ))
{
embedInQuotes = true;
}
for (int i = 0; i < sb.Length; i++)
{
// Embed in quotes to preserve: commas, line-breaks etc.
if (sb[i] == DelimiterChar ||
sb[i]=='\r' ||
sb[i]=='\n' ||
sb[i] == '"')
{
embedInQuotes = true;
break;
}
}
// If the field itself has quotes, they must each be represented
// by a pair of consecutive quotes.
sb.Replace("\"", "\"\"");
String rv = sb.ToString();
if (embedInQuotes)
{
rv = "\"" + rv + "\"";
}
return rv;
}
Might not be world's most efficient code, but it has been tested. Real world sucks compared to quick sample code :)
Why not use one of the open source CSV libraries out there?
I know it sounds like overkill for something that appears so simple, but as you can tell by the comments and code snippets, there's more than meets the eye. In addition to handling full CSV compliance, you'll eventually want to handle both reading and writing CSVs... and you may want file manipulation.
I've used Open CSV on one of my projects before (but there are plenty of others to choose from). It certainly made my life easier. ;)
Don't forget our old friend "for". It's not as nice-looking as foreach but it has the advantage of being able to start at the second element.
public string ReturnAsCSV(ContactList contactList)
{
if (contactList == null || contactList.Count == 0)
return string.Empty;
StringBuilder sb = new StringBuilder(contactList[0].Name);
for (int i = 1; i < contactList.Count; i++)
{
sb.Append(",");
sb.Append(contactList[i].Name);
}
return sb.ToString();
}
You could also wrap the second Append in an "if" that tests whether the Name property contains a double-quote or a comma, and if so, escape them appropriately.
You could instead add the comma as the first thing inside your foreach.
if (sb.Length > 0) sb.Append(",");
You could also make an array of c.Name data and use String.Join method to create your line.
public string ReturnAsCSV(ContactList contactList)
{
List<String> tmpList = new List<string>();
foreach (Contact c in contactList)
{
tmpList.Add(c.Name);
}
return String.Join(",", tmpList.ToArray());
}
This might not be as performant as the StringBuilder approach, but it definitely looks cleaner.
Also, you might want to consider using .CurrentCulture.TextInfo.ListSeparator instead of a hard-coded comma -- If your output is going to be imported into other applications, you might have problems with it. ListSeparator may be different across different cultures, and MS Excel at the very least, honors this setting. So:
return String.Join(
System.Globalization.CultureInfo.CurrentCulture.TextInfo.ListSeparator,
tmpList.ToArray());
I like the idea of adding the comma by checking if the container is empty, but doesn't that mean more processing as it needs to check the length of the string on each occurrence?
You're prematurely optimizing, the performance hit would be negligible.
Just a thought, but remember to handle comma's and quotation marks (") in the field values, otherwise your CSV file may break the consumers reader.
I wrote a small class for this in case someone else finds it useful...
public class clsCSVBuilder
{
protected int _CurrentIndex = -1;
protected List<string> _Headers = new List<string>();
protected List<List<string>> _Records = new List<List<string>>();
protected const string SEPERATOR = ",";
public clsCSVBuilder() { }
public void CreateRow()
{
_Records.Add(new List<string>());
_CurrentIndex++;
}
protected string _EscapeString(string str)
{
return string.Format("\"{0}\"", str.Replace("\"", "\"\"")
.Replace("\r\n", " ")
.Replace("\n", " ")
.Replace("\r", " "));
}
protected void _AddRawString(string item)
{
_Records[_CurrentIndex].Add(item);
}
public void AddHeader(string name)
{
_Headers.Add(_EscapeString(name));
}
public void AddRowItem(string item)
{
_AddRawString(_EscapeString(item));
}
public void AddRowItem(int item)
{
_AddRawString(item.ToString());
}
public void AddRowItem(double item)
{
_AddRawString(item.ToString());
}
public void AddRowItem(DateTime date)
{
AddRowItem(date.ToShortDateString());
}
public static string GenerateTempCSVPath()
{
return Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString().ToLower().Replace("-", "") + ".csv");
}
protected string _GenerateCSV()
{
StringBuilder sb = new StringBuilder();
if (_Headers.Count > 0)
{
sb.AppendLine(string.Join(SEPERATOR, _Headers.ToArray()));
}
foreach (List<string> row in _Records)
{
sb.AppendLine(string.Join(SEPERATOR, row.ToArray()));
}
return sb.ToString();
}
public void SaveAs(string path)
{
using (StreamWriter sw = new StreamWriter(path))
{
sw.Write(_GenerateCSV());
}
}
}
I've used this method before. The Length property of StringBuilder is NOT readonly so subtracting it by one means truncate the last character. But you have to make sure your length is not zero to start with (which would happen if your list is empty) because setting the length to less than zero is an error.
public string ReturnAsCSV(ContactList contactList)
{
StringBuilder sb = new StringBuilder();
foreach (Contact c in contactList)
{
sb.Append(c.Name + ",");
}
if (sb.Length > 0)
sb.Length -= 1;
return sb.ToString();
}
I use CSVHelper - it's a great open-source library that lets you generate compliant CSV streams one element at a time or custom-map your classes:
public string ReturnAsCSV(ContactList contactList)
{
StringBuilder sb = new StringBuilder();
using (StringWriter stringWriter = new StringWriter(sb))
{
using (var csvWriter = new CsvHelper.CsvWriter(stringWriter))
{
csvWriter.Configuration.HasHeaderRecord = false;
foreach (Contact c in contactList)
{
csvWriter.WriteField(c.Name);
}
}
}
return sb.ToString();
}
or if you map then something like this: csvWriter.WriteRecords<ContactList>(contactList);
How about some trimming?
public string ReturnAsCSV(ContactList contactList)
{
StringBuilder sb = new StringBuilder();
foreach (Contact c in contactList)
{
sb.Append(c.Name + ",");
}
return sb.ToString().Trim(',');
}
How about tracking whether you are on the first item, and only add a comma before the item if it is not the first one.
public string ReturnAsCSV(ContactList contactList)
{
StringBuilder sb = new StringBuilder();
bool isFirst = true;
foreach (Contact c in contactList) {
if (!isFirst) {
// Only add comma before item if it is not the first item
sb.Append(",");
} else {
isFirst = false;
}
sb.Append(c.Name);
}
return sb.ToString();
}

Categories