目录
介绍
您是否曾经遇到过这样的情况:您必须处理结构化表,但数据库不是项目的一部分,甚至是某种过度设计的解决方案?在这里CvsFileProcessor可能是一个解决方案。您可以从文件中读取数据,也可以在内存中创建结构化表,Select数据,通过Insert、Update和Delete方法操作数据,最后将实际内容保存到文件中。
为了更加灵活,最好能够将列的内容作为任何平面数据类型(如:string, byte, int, double, bool等)进行处理。该类提供了一个接口方法来定义结构化表的唯一键。请记住,多个列可以是唯一键的成员。此外,Insert、Update、Delete和Clear操作可能符合多线程应用程序环境中其他线程的利益。因此,该类正在为此类操作引发事件。感兴趣的对象可以只订阅感兴趣的事件,它们将收到通知。
这篇文章更新是关于什么的?
此更新版本为 Select、Insert、Update和Delete方法提供了一些新功能。此外,它还提供对CSV 件的处理,其中的值将在使用或不带有引号字符嵌入的情况下进行处理。引号字符处理在新类CsvValue中实现。
类的详细信息
CsvFileProcessor实现不是火箭科学。它只是一组集合,用于处理列名到列索引、行索引到行内容、行索引到唯一键的映射,反之亦然。实现Select和Update方法是为了处理泛型类型。实现很简单,应该易于理解。
/*###############################################################################*/
/*# #*/
/*# (c) Jochen Haar #*/
/*# =============== #*/
/*# #*/
/*# Class-Name : CsvFileProcessor #*/
/*# #*/
/*# Derived from : none #*/
/*# #*/
/*# File-Name : CsvFileProcessor.cs #*/
/*# #*/
/*# Author : Dipl.-Ing. Jochen Haar #*/
/*# #*/
/*# Date : 01.05.2022 #*/
/*# #*/
/*# Tool/Compiler: Visual Studio 2022 #*/
/*# #*/
/*# Events : Will be raised on Insert, Update, Delete and Clear actions. #*/
/*# #*/
/*# Exceptions : Will be thrown on logical errors. Therefore, please use #*/
/*# try/catch blocks. #*/
/*# #*/
/*#=============================================================================#*/
/*# Change report #*/
/*#----------+-------------------+----------------------------------------------#*/
/*# Date | Name | description of the reason for changes #*/
/*#----------+-------------------+----------------------------------------------#*/
/*# | | #*/
/*#----------+-------------------+----------------------------------------------#*/
/*###############################################################################*/
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
//---------------------------------------------------------------------------------
namespace CommonLibrary
//---------------------------------------------------------------------------------
{
//-----------------------------------------------------------------------------
public class CsvFileProcessor
//-----------------------------------------------------------------------------
{
/// The Insert event handler
public event EventHandler<CsvFileProcessorEventArgs> InsertEvent;
/// The Update event handler
public event EventHandler<CsvFileProcessorEventArgs> UpdateEvent;
/// The Delete event handler
public event EventHandler<CsvFileProcessorEventArgs> DeleteEvent;
/// The Clear event handler
public event EventHandler<CsvFileProcessorEventArgs> ClearEvent;
/// The column and field separator
public char Separator { get; private set; } = ',';
/// The name of the Comma Separated Value file
public string FileName { get; private set; } = null;
/// The header column names as a string array
private string[] ColumnNames = null;
/// The counter for the RowIndex
private uint RowCounter = 0;
/// The CsvValue object to handle the values on reading from and writing
/// to files with or without embedded quote characters
private CsvValue CsvValue = null;
/// The collection for fast hash access to the ColumnIndex in the header
/// with the ColumnName as key
private readonly Dictionary<string,
int> ColumnNameToIndexCollection = new Dictionary<string, int>();
/// The collection for fast hash access to the value rows with RowIndex
/// as key. The RowIndex in the collection might not be consecutive
/// due to the fact that methods to delete and insert rows are provided
private readonly Dictionary<uint,
string[]> RowIndexToValueCollection = new Dictionary<uint, string[]>();
/// The collection for fast hash access to the content row indexes with
/// the UniqueKey as key. The RowIndex in the collection might not be
/// consecutive due to the fact that methods to delete and insert rows
/// are provided
private readonly Dictionary<string, uint>
UniqueKeyToRowIndexCollection = new Dictionary<string, uint>();
/// The collection for fast hash access to the UniqueKey of the row
/// indexes with the RowIndex as key. The RowIndex in the collection
/// might not be consecutive due to the fact that methods to delete
/// and insert rows are provided
private readonly Dictionary<uint,
string> RowIndexToUniqueKeyCollection = new Dictionary<uint, string>();
/// The list of the columns which describe the unique key of the
/// UniqueKeyToRowIndexCollection
private readonly List<string> UniqueKeyColumnList = new List<string>();
/// Synchronization object for multi-threading
private readonly object Locker = new object();
/// Constructor
//-------------------------------------------------------------------------
public CsvFileProcessor(char separator = ',', bool isQuoted = true,
char quoteCharacter = '"') => this.Initialize(null, separator,
isQuoted, quoteCharacter);
//-------------------------------------------------------------------------
/// Constructor for existing files. The column definition will be read from
/// the file.
//-------------------------------------------------------------------------
public CsvFileProcessor(string fileName, char separator = ',',
bool isQuoted = true, char quoteCharacter = '"')
//-------------------------------------------------------------------------
{
this.Initialize(fileName, separator, isQuoted, quoteCharacter);
this.ReadColumnsFromFile();
}
/// Constructor for non existing files. The FileName will be used for
/// saving the content into a file with that name. The columns are
/// separated by the Separator character.
//-------------------------------------------------------------------------
public CsvFileProcessor(string fileName, string columns, char separator = ',',
bool isQuoted = true, char quoteCharacter = '"')
//-------------------------------------------------------------------------
{
this.Initialize(fileName, separator, isQuoted, quoteCharacter);
this.AddColumns(columns);
}
/// Constructor for non existing files. The FileName will be used for
/// saving the content into a file with that name. The columns array
/// defines the ColumnNames.
//-------------------------------------------------------------------------
public CsvFileProcessor(string fileName, string[] columns, char separator = ',',
bool isQuoted = true, char quoteCharacter = '"')
//-------------------------------------------------------------------------
{
this.Initialize(fileName, separator, isQuoted, quoteCharacter);
this.AddColumns(columns);
}
/// Resets the instance according the parameter settings. This method is
/// meant for situations where the same instance will be used for different
/// Comma Separated Files or different Structured Tables. Please bear in
/// mind, that the previous content of the instance will be cleared
/// completely.
//-------------------------------------------------------------------------
public void Reset(string fileName, char separator = ',',
bool isQuoted = true, char quoteCharacter = '"')
//-------------------------------------------------------------------------
{
lock (this.Locker)
{
this.Initialize(fileName, separator, isQuoted, quoteCharacter);
this.ReadColumnsFromFile();
}
}
/// Resets the instance according the parameter settings and defines the
/// ColumnNames according the columns array. This method is meant for
/// situations where the same instance will be used for different Comma
/// Separated Files or different Structured Tables. Please bear in mind,
/// that the previous content of the instance will be cleared completely.
//-------------------------------------------------------------------------
public void Reset(string fileName, string[] columns, char separator = ',',
bool isQuoted = true, char quoteCharacter = '"')
//-------------------------------------------------------------------------
{
lock (this.Locker)
{
this.Initialize(fileName, separator, isQuoted, quoteCharacter);
this.AddColumns(columns);
}
}
/// Initializes the instance according the parameter settings
//-------------------------------------------------------------------------
private void Initialize(string fileName, char separator,
bool isQuoted, char quoteCharacter)
//-------------------------------------------------------------------------
{
string newFileName = fileName ?? this.FileName;
this.Clear();
this.FileName = newFileName;
this.Separator = separator;
this.CsvValue = new CsvValue(this.Separator, isQuoted, quoteCharacter);
}
/// Reads the column definition as content from the Comma Separated file
//-------------------------------------------------------------------------
private void ReadColumnsFromFile()
//-------------------------------------------------------------------------
{
string columns = null;
using (StreamReader streamReader = new StreamReader(this.FileName))
{ columns = streamReader.ReadLine(); }
this.AddColumns(this.CsvValue.Split(this.EraseLastSeparator(columns)));
}
/// Adds a column name to the list of the unique key members
//-------------------------------------------------------------------------
public void AddColumnNameToUniqueKey(string columnName)
//-------------------------------------------------------------------------
{
if (this.ColumnNameToIndexCollection.ContainsKey(columnName))
this.UniqueKeyColumnList.Add(columnName);
else throw new Exception($@"'{columnName}' is an unknown column
and can't be added as a member of the unique key!");
}
/// Reads the content rows of the file. Please bear in mind, the header
/// will be skipped.
//-------------------------------------------------------------------------
public void ReadFile()
//-------------------------------------------------------------------------
{
lock (this.Locker)
{
string row;
uint rowIndex = 0;
using (StreamReader streamReader = new StreamReader(this.FileName))
{
while (streamReader.Peek() >= 0)
{
row = streamReader.ReadLine();
if (rowIndex > 0 && !string.IsNullOrEmpty(row))
this.Insert(row);
rowIndex++;
}
}
}
}
/// Returns the UniqueKey of a specific RowIndex or null
//-------------------------------------------------------------------------
private string GetUniqueKeyOfRowIndex(uint rowIndex)
//-------------------------------------------------------------------------
{
string key = null;
if (this.RowIndexToUniqueKeyCollection.ContainsKey(rowIndex)) key =
this.RowIndexToUniqueKeyCollection[rowIndex];
return key;
}
/// Clears the current content of the instance completely
//-------------------------------------------------------------------------
public void Clear()
//-------------------------------------------------------------------------
{
lock (this.Locker)
{
this.FileName = null;
this.ColumnNames = null;
this.ColumnNameToIndexCollection.Clear();
this.RowIndexToValueCollection.Clear();
this.UniqueKeyToRowIndexCollection.Clear();
this.RowIndexToUniqueKeyCollection.Clear();
this.UniqueKeyColumnList.Clear();
this.RowCounter = 0;
}
this.ClearEvent?.Invoke(this, new CsvFileProcessorEventArgs());
}
/// Erases the last and unneeded Separator in the value string if
/// there is one
//-------------------------------------------------------------------------
private string EraseLastSeparator(string value) => value[value.Length - 1] ==
this.Separator ? value.Substring(0, value.Length - 1) : value;
//-------------------------------------------------------------------------
/// Determines wether the instance contains the RowIndex
//-------------------------------------------------------------------------
public bool ContainsKey(uint rowIndex) =>
this.RowIndexToValueCollection.ContainsKey(rowIndex);
//-------------------------------------------------------------------------
/// Determines wether the instance contains the UniqueKey
//-------------------------------------------------------------------------
public bool ContainsKey(string key) =>
this.UniqueKeyToRowIndexCollection.ContainsKey(key);
//-------------------------------------------------------------------------
/// Returns the amount of columns of the header and of each row
//-------------------------------------------------------------------------
public int GetColumnCount() => this.ColumnNames != null ?
this.ColumnNames.Length : 0;
//-------------------------------------------------------------------------
/// Returns the amount of rows. The header is not included!
//-------------------------------------------------------------------------
public int GetRowCount() => this.RowIndexToValueCollection.Count;
//-------------------------------------------------------------------------
/// Returns the column name of a specific column index or an empty string
/// if the column is out of the range
//-------------------------------------------------------------------------
public string GetColumnName(uint columnIndex) => this.ColumnNames != null &&
columnIndex < this.ColumnNames.Length ? this.ColumnNames[columnIndex] : @"";
//-------------------------------------------------------------------------
/// Returns the index of a specific column name or -1 if the column is
/// not valid
//-------------------------------------------------------------------------
public int GetColumnIndex(string columnName) => columnName != null &&
this.ColumnNameToIndexCollection.ContainsKey(columnName) ?
this.ColumnNameToIndexCollection[columnName] : -1;
//-------------------------------------------------------------------------
/// Returns true if the column name is part of the unique key
//-------------------------------------------------------------------------
public bool IsColumnNameUniqueKeyMember(string columnName) =>
this.UniqueKeyColumnList.Contains(columnName);
//-------------------------------------------------------------------------
/// Exports the content of the instance into a Comma Separated Value File
/// (CSV). The values of the columns will be embedded in quote characters
/// and single quote characters which are part of the value content will
/// be replaced by double quote characters
//-------------------------------------------------------------------------
public long ToQuotedCSV() => this.ToCSV(true);
//-------------------------------------------------------------------------
/// Exports the content of the instance into a plain Comma Separated Value
/// File (CSV). The values of the columns will be exported as plain
/// values - they are not embedded in quote characters
//-------------------------------------------------------------------------
public long ToPlainCSV() => this.ToCSV(false);
//-------------------------------------------------------------------------
/// Adds the columns definition to the instance by splitting the string
/// into the single column names.
//-------------------------------------------------------------------------
private void AddColumns(string columns)
//-------------------------------------------------------------------------
{
columns = columns.Trim();
if (string.IsNullOrEmpty(columns)) throw new Exception
($@"Missing header definition in/for file: '{this.FileName}'!");
columns = this.EraseLastSeparator(columns);
this.AddColumns(this.CsvValue.Split(columns));
}
/// Adds the columns definition to the instance. The ColumnNames are defined
/// in the string array.
//-------------------------------------------------------------------------
private void AddColumns(string[] columns)
//-------------------------------------------------------------------------
{
this.ColumnNames = new string[columns.Length];
for (int i = 0; i < columns.Length; i++)
{
this.ColumnNames[i] = columns[i];
this.ColumnNameToIndexCollection.Add(columns[i], i);
}
}
/// Returns an ascending sorted list of all existing RowIndexes.
//-------------------------------------------------------------------------
public List<uint> GetRowIndexList()
//-------------------------------------------------------------------------
{
List<uint> list = new List<uint>();
foreach (var pair in this.RowIndexToValueCollection) list.Add(pair.Key);
list.Sort();
return list;
}
/// Returns an unsorted list of all existing UniqueKeys. The order depends
/// on the manipulation of the content.
//-------------------------------------------------------------------------
public List<string> GetUniqueKeyList()
//-------------------------------------------------------------------------
{
List<string> list = new List<string>();
foreach (var pair in this.UniqueKeyToRowIndexCollection) list.Add(pair.Key);
return list;
}
/// Returns the definition of the unique key as a string with the name of
/// each unique key column member separated by a '+' character
//-------------------------------------------------------------------------
private string GetUniqueKeyColumns()
//-------------------------------------------------------------------------
{
string key = @"";
foreach (var column in this.UniqueKeyColumnList) key +=
column != this.UniqueKeyColumnList[this.UniqueKeyColumnList.Count - 1] ?
$@"{column}+" : column;
return key;
}
/// Validates the parameter "rowIndex" and "columnName" and throws an
/// Exception on any errors
//-------------------------------------------------------------------------
private void Validate(uint rowIndex, string columnName = null)
//-------------------------------------------------------------------------
{
if (rowIndex == 0) throw new Exception
($@"Invalid row index: '{rowIndex}'. Must be greater than zero!");
if (columnName != null && this.GetColumnIndex(columnName) == -1)
throw new Exception($@"'{columnName}' is not available
as a column in/for file: '{this.FileName}'!");
if (!this.RowIndexToValueCollection.ContainsKey(rowIndex))
throw new Exception($@"No content for row index: '{rowIndex}'
in/for file: '{this.FileName}' available!");
}
/// Updates the UniqueKey in the collections. If the column value
/// modification leads to a Duplicate Key the new value will be denied
/// and an Exception will be thrown - rollback scenario to the previous
/// value.
//-------------------------------------------------------------------------
private void UpdateUniqueKey(uint rowIndex, string columnName,
string newValue, string oldValue)
//-------------------------------------------------------------------------
{
string newKey = @"";
foreach (var keyColumn in this.UniqueKeyColumnList) newKey +=
this.RowIndexToValueCollection[rowIndex][this.GetColumnIndex(keyColumn)];
if (!this.UniqueKeyToRowIndexCollection.ContainsKey(newKey))
{
string oldKey = this.RowIndexToUniqueKeyCollection[rowIndex];
this.UniqueKeyToRowIndexCollection.Remove(oldKey);
this.UniqueKeyToRowIndexCollection.Add(newKey, rowIndex);
this.RowIndexToUniqueKeyCollection[rowIndex] = newKey;
}
else // modification leads to a Duplicate Key - rollback scenario
{
this.RowIndexToValueCollection[rowIndex]
[this.GetColumnIndex(columnName)] = oldValue;
throw new Exception($@"Duplicate unique key:
'{this.GetUniqueKeyColumns()}' with new value: '{newValue}'
for column: '{columnName}' and row: '{rowIndex}'
in/for file: '{this.FileName}'!. New value denied.");
}
}
/// Inserts a row content to the instance by splitting the content into the
/// single column values.
//-------------------------------------------------------------------------
private uint Insert(string values)
//-------------------------------------------------------------------------
{
uint rowIndex;
string key = @"";
lock (this.Locker)
{
values = values.Trim();
values = this.EraseLastSeparator(values);
string[] valueArray = this.CsvValue.Split(values);
if (this.GetColumnCount() == 0) throw new Exception
($@"Missing header definition in/for file: '{this.FileName}'!");
if (valueArray.Length != this.ColumnNames.Length)
throw new Exception($@"Column count mismatch.
Row '{values}' has '{valueArray.Length}' columns.
The header has: '{this.ColumnNames.Length}'
columns in/for file: '{this.FileName}'!");
foreach (var keyColumn in this.UniqueKeyColumnList)
key += valueArray[this.GetColumnIndex(keyColumn)];
if (this.UniqueKeyColumnList.Count > 0)
{
if (this.UniqueKeyToRowIndexCollection.ContainsKey(key))
throw new Exception($@"Duplicate unique key:
'{this.GetUniqueKeyColumns()}'
with row: '{values}' in/for file: '{this.FileName}'!");
this.UniqueKeyToRowIndexCollection.Add(key, this.RowCounter + 1);
this.RowIndexToUniqueKeyCollection.Add(this.RowCounter + 1, key);
}
this.RowIndexToValueCollection.Add(this.RowCounter + 1, valueArray);
this.RowCounter++; // increment the RowCounter here to
// avoid gaps in case of duplicate keys
rowIndex = this.RowCounter; // RowCounter can increase during Invoke
// in multi-threading applications - be safe and continue
// with a stack variable
key = this.GetUniqueKeyOfRowIndex(rowIndex);
}
this.InsertEvent?.Invoke
(this, new CsvFileProcessorEventArgs(rowIndex, key));
return rowIndex;
}
/// Inserts a row content to the instance. The values array contain the
/// content of the columns values.
//-------------------------------------------------------------------------
public uint Insert(string[] values) => this.Insert(this.CsvValue.ToCSV(values));
//-------------------------------------------------------------------------
/// Inserts an empty row content to the instance. Please use the returned
/// RowIndex and Update all columns which belong to the UniqueKey if there
/// are any right after the Insert() action.
//-------------------------------------------------------------------------
public uint Insert()
//-------------------------------------------------------------------------
{
uint rowIndex;
lock (this.Locker)
{
string[] values = new string[this.GetColumnCount()];
for (int i = 0; i < this.GetColumnCount(); i++)
values[i] = string.Empty;
rowIndex = this.Insert(this.CsvValue.ToCSV(values));
}
return rowIndex;
}
/// Deletes a dedicated row and throws an Exception if the RowIndex does
/// not exist.
//-------------------------------------------------------------------------
public bool Delete(uint rowIndex)
//-------------------------------------------------------------------------
{
string key;
lock (this.Locker)
{
if (rowIndex == 0) throw new Exception
($@"Invalid row index: '{rowIndex}'. Must be greater than zero!");
if (!this.RowIndexToValueCollection.ContainsKey(rowIndex))
throw new Exception($@"No content for row index:
'{rowIndex}' in/for file: '{this.FileName}' available!");
key = this.GetUniqueKeyOfRowIndex(rowIndex);
this.UniqueKeyToRowIndexCollection.Remove
(this.RowIndexToUniqueKeyCollection[rowIndex]);
this.RowIndexToUniqueKeyCollection.Remove(rowIndex);
this.RowIndexToValueCollection.Remove(rowIndex);
}
this.DeleteEvent?.Invoke
(this, new CsvFileProcessorEventArgs(rowIndex, key));
return true;
}
/// Deletes a dedicated row addressed via the UniqueKey and throws an
/// Exception if the Key/RowIndex does not exist.
//-------------------------------------------------------------------------
public bool Delete(string key)
//-------------------------------------------------------------------------
{
bool success;
lock (this.Locker)
{
uint rowIndex = 0;
success = this.UniqueKeyToRowIndexCollection.ContainsKey(key);
if (success) rowIndex = this.UniqueKeyToRowIndexCollection[key];
if (rowIndex > 0) success = this.Delete(rowIndex);
}
return success;
}
/// Delete all rows where the column value is equal to the generic Value
//-------------------------------------------------------------------------
public uint Delete<T>(string columnName, T value)
//-------------------------------------------------------------------------
{
uint counter = 0;
lock (this.Locker)
{
int columnIndex = this.GetColumnIndex(columnName);
if (columnIndex == -1) throw new Exception($@"'{columnName}'
is not available as a column in/for file: '{this.FileName}'!");
string strValue = TypeConverter.ConvertTo<string>(value);
List<uint> list = new List<uint>();
foreach (var pair in this.RowIndexToValueCollection.Where
(pair => pair.Value[columnIndex] == strValue)) list.Add(pair.Key);
foreach (uint rowIndex in list)
{
if (this.Delete(rowIndex)) counter++;
}
}
return counter;
}
/// Returns the column value as a generic type of a dedicated row index
//-------------------------------------------------------------------------
public bool Select<T>(uint rowIndex, string columnName, out T value)
//-------------------------------------------------------------------------
{
this.Validate(rowIndex, columnName);
value = TypeConverter.ConvertTo<T>
(this.RowIndexToValueCollection[rowIndex][this.GetColumnIndex(columnName)]);
return true;
}
/// Returns the column value as a generic type of the row which belongs
/// to the unique key
//-------------------------------------------------------------------------
public bool Select<T>(string key, string columnName, out T value)
//-------------------------------------------------------------------------
{
if (!this.UniqueKeyToRowIndexCollection.ContainsKey(key))
throw new Exception($@"Unknown key: '{key}'
in/for file: '{this.FileName}'!");
return this.Select(this.UniqueKeyToRowIndexCollection[key],
columnName, out value);
}
/// Returns all column values in a string array or throws an Exception
/// if RowIndex is not valid
//-------------------------------------------------------------------------
public string[] Select(uint rowIndex)
//-------------------------------------------------------------------------
{
this.Validate(rowIndex);
return (string[])this.RowIndexToValueCollection[rowIndex].Clone();
}
/// Returns all column values in a string array or throws an Exception
/// if the UniqueKey is not valid
//-------------------------------------------------------------------------
public string[] Select(string key)
//-------------------------------------------------------------------------
{
if (!this.UniqueKeyToRowIndexCollection.ContainsKey(key))
throw new Exception($@"Unknown key:
'{key}' in/for file: '{this.FileName}'!");
return this.Select(this.UniqueKeyToRowIndexCollection[key]);
}
/// Updates the column value as a generic type of a dedicated row index.
/// The UniqueKey of the row will be updated with the column value to be
/// updated if the column is member of the UniqueKey.
//-------------------------------------------------------------------------
public bool Update<T>(uint rowIndex, string columnName, T value)
//-------------------------------------------------------------------------
{
string key, newValue, oldValue;
lock (this.Locker)
{
this.Validate(rowIndex, columnName);
newValue = TypeConverter.ConvertTo<string>(value);
oldValue = this.RowIndexToValueCollection[rowIndex]
[this.GetColumnIndex(columnName)];
key = this.GetUniqueKeyOfRowIndex(rowIndex);
this.RowIndexToValueCollection[rowIndex]
[this.GetColumnIndex(columnName)] = newValue;
if (this.IsColumnNameUniqueKeyMember(columnName) &&
newValue != oldValue) this.UpdateUniqueKey
(rowIndex, columnName, newValue, oldValue);
}
if (newValue != oldValue) this.UpdateEvent?.Invoke
(this, new CsvFileProcessorEventArgs
(rowIndex, key, columnName, newValue, oldValue));
return true;
}
/// Updates the column value as a generic type of the row which belongs to
/// the unique key. The UniqueKey of the row will be updated with the
/// column value to be updated if the column is member of the UniqueKey.
//-------------------------------------------------------------------------
public bool Update<T>(string key, string columnName, T value)
//-------------------------------------------------------------------------
{
if (!this.UniqueKeyToRowIndexCollection.ContainsKey(key))
throw new Exception
($@"Unknown key: '{key}' in/for file: '{this.FileName}'!");
return this.Update
(this.UniqueKeyToRowIndexCollection[key], columnName, value);
}
/// Updates all column values of a specific column which contain the
/// OldValue with the NewValue as a generic type. The UniqueKey of the
/// rows will be updated with the column value to be updated if the
/// column is member of the UniqueKey. If the modification leads to a
/// Duplicate Key, an Exception will be thrown. This could lead to the
/// situation that the modification will end up partially.
//-------------------------------------------------------------------------
public uint Update<T>(string columnName, T oldValue, T newValue)
//-------------------------------------------------------------------------
{
uint counter = 0;
lock (this.Locker)
{
int columnIndex = this.GetColumnIndex(columnName);
if (columnIndex == -1) throw new Exception($@"'{columnName}'
is not available as a column in/for file: '{this.FileName}'!");
string strOldValue = TypeConverter.ConvertTo<string>(oldValue);
foreach (var pair in this.RowIndexToValueCollection.Where
(pair => pair.Value[columnIndex] == strOldValue))
{
if (this.Update(pair.Key, columnName, newValue)) counter++;
}
}
return counter;
}
/// Exports the content of the instance into a Comma Separated Value File.
//-------------------------------------------------------------------------
private long ToCSV(bool isQuoted)
//-------------------------------------------------------------------------
{
long length;
lock (this.Locker)
{
File.Delete(this.FileName);
CsvValue csv = new CsvValue(this.Separator, isQuoted,
this.CsvValue.QuoteCharacter);
using (StreamWriter streamWriter = File.AppendText(this.FileName))
{
streamWriter.WriteLine(csv.ToCSV(this.ColumnNames));
// iterate through the sorted RowIndexList
// to keep the original order in case of intensive manipulation
List<uint> list = this.GetRowIndexList();
foreach (uint rowIndex in list)
streamWriter.WriteLine(csv.ToCSV
(this.RowIndexToValueCollection[rowIndex]));
}
length = new FileInfo(this.FileName).Length;
}
return length;
}
}
}
使用CsvFileProcessor类
请在下面找到如何从文件中读取现有数据的实现和如何在内存中创建结构化表的实现,做一些操作的东西,并将实例的内容保存到文件中。
===================================================================================
Example of an existing file:
===================================================================================
Row 0 = "Column000","Column001","Column002","Column003","Val","Column005","Column006"
Row 1 = "R1C0Value","R1C1Value","R1C2Value","R1C3Value","123","R1C5Value","R1C6Value"
Row 2 = "R2C0Value","R2C1Value","R2C2Value","R2C3Value","555","R2C5Value","R2C6Value"
Row 3 = "R3C0Value","R3C1Value","R3C2Value","R3C3Value","999","R3C5Value","R3C6Value"
===================================================================================
Usage for an existing file:
===================================================================================
//---------------------------------------------------------------------------------
private void DoSomethingWithExistingFile()
//---------------------------------------------------------------------------------
{
try
{
CsvFileProcessor csv = new CsvFileProcessor("YourFile.csv");// comma as default
// column/field separator
csv.AddColumnNameToUniqueKey("Column000");
csv.AddColumnNameToUniqueKey("Column001");
csv.ReadFile();
string key = "R2C0Value" + "R2C1Value";
bool success = csv.Select(2, "Val", out int value);
// The statement above leads to the same result as the statement below:
success = csv.Select(key, "Val", out value);
// The out int value would be: 555 in this example.
success = csv.Update(key, "Val", value + 222);
// The content of the "Val" column in row 2 was set to "777"
// in the statement above.
long length = csv.ToPlainCSV(); // save without quote character embedding
csv.Reset(null, csv.Separator, false); // reset with same file name,
// but without quote character handling
csv.ReadFile(); // read the plain CSV file back
long length = csv.ToQuotedCSV(); // save with quote character embedding
}
catch (Exception e) { Console.WriteLine($@"ERROR: {e.Message}"); }
}
===================================================================================
Usage for non existing files:
===================================================================================
//---------------------------------------------------------------------------------
private void DoSomethingWithNonExistingFile()
//---------------------------------------------------------------------------------
{
try
{
double pi = 3.1415;
string[] columns = new string[] { "Column000", "Column001",
"Column002", "Column003", "Val", "Column005", "Column006, the last one" };
CsvFileProcessor csv =
new CsvFileProcessor("YourFile.csv", columns, '|'); // pipe as
// column/field separator
csv.AddColumnNameToUniqueKey("Column000"); // UniqueKey definition
csv.InsertEvent += OnInsert; // subscribe the InsertEvent
csv.UpdateEvent += OnUpdate; // subscribe the UpdateEvent
csv.DeleteEvent += OnDelete; // subscribe the DeleteEvent
csv.ClearEvent += OnClear; // subscribe the ClearEvent
for (int i = 1; i <= 9; i++) csv.Insert(new string[]
{ $"R{i}C0Value", $"R{i}C1Value",
$"R{i}C2Value", $"R{i}C3Value", $"{i*100}", $"R{i}C5Value", $"R{i}C6Value" });
string[] valueArray = csv.Select("R1C0Value"); // read complete row
// into an array
bool success = csv.Select(1, "Val", out int value); // read as int with index
success = csv.Select("R1C0Value", "Val", out value); // read as int with key
success = csv.Update("R2C0Value", "Val", value + 222); // update as int
success = csv.Update("R3C0Value", "Val", value + 333); // update as int
success = csv.Update("R4C0Value", "Val", pi); // update as double
success = csv.Update("R5C0Value", "Val", pi * 2); // update as double
success = csv.Update("R6C0Value", "Val", true); // update as boolean
success = csv.Update("R7C0Value", "Val", true); // update as boolean
success = csv.Select("R7C0Value", "Val", out bool b); // read as boolean
uint rows = csv.Update("Val", b, !b); // toggle update of
// all values in column
// "Val" where value = b
rows = csv.Delete("Val", false); // delete all rows
// where the value of
// column "Val" is false
uint rowIndex = csv.Insert(); // insert an empty row
success = csv.Update(rowIndex, "Column000", "\"AnyValue\""); // update the key
// column of the empty row
rowIndex = csv.Insert(new string[] { "Dog", "Cat", "Mouse", "Worm", "500", "Fly",
"I saw the movie, but didn't understand!" });
long length = csv.ToQuotedCSV(); // export with quote
// characters and get the
// file length in bytes
}
catch (Exception e) { Console.WriteLine($@"ERROR: {e.Message}"); }
}
//---------------------------------------------------------------------------------
private void OnInsert(object sender, CsvFileProcessorEventArgs e) =>
Console.WriteLine($@"Insert row: <{e.RowIndex}>,
key: <{(e.Key == null ? @"null" : e.Key)}>");
//---------------------------------------------------------------------------------
//---------------------------------------------------------------------------------
private void OnUpdate(object sender, CsvFileProcessorEventArgs e) =>
Console.WriteLine($@"Update row: <{e.RowIndex}>,
key: <{(e.Key == null ? @"null" : e.Key)}>,
column: <{e.ColumnName}>, NewValue: <{e.NewValue}>,
OldValue: <{e.OldValue}>");
//---------------------------------------------------------------------------------
//---------------------------------------------------------------------------------
private void OnDelete(object sender, CsvFileProcessorEventArgs e) =>
Console.WriteLine($@"Delete row: <{e.RowIndex}>,
key: <{(e.Key == null ? @"null" : e.Key)}>");
//---------------------------------------------------------------------------------
//---------------------------------------------------------------------------------
private void OnClear(object sender, CsvFileProcessorEventArgs e) =>
Console.WriteLine($@"Clear action");
//---------------------------------------------------------------------------------
使用CsvFileProcessor可能不仅有助于处理逗号分隔值文件中的数据,还有助于处理一些可以由不同线程操作且线程必须根据数据内容做出反应的中心数据。
结论
我希望这个小类和示例对某人有用,或者实现可能会帮助任何在完全不同的领域寻找解决方案的人。
https://www.codeproject.com/Tips/5333916/CSV-File-Processor-Manage-Comma-Separated-Value-Fi