463 lines
16 KiB
C#
463 lines
16 KiB
C#
using Unosquare.Swan.Reflection;
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
|
|
namespace Unosquare.Swan.Formatters {
|
|
/// <summary>
|
|
/// A CSV writer useful for exporting a set of objects.
|
|
/// </summary>
|
|
/// <example>
|
|
/// The following code describes how to save a list of objects into a CSV file.
|
|
/// <code>
|
|
/// using System.Collections.Generic;
|
|
/// using Unosquare.Swan.Formatters;
|
|
///
|
|
/// class Example
|
|
/// {
|
|
/// class Person
|
|
/// {
|
|
/// public string Name { get; set; }
|
|
/// public int Age { get; set; }
|
|
/// }
|
|
///
|
|
/// static void Main()
|
|
/// {
|
|
/// // create a list of people
|
|
/// var people = new List<Person>
|
|
/// {
|
|
/// new Person { Name = "Artyom", Age = 20 },
|
|
/// new Person { Name = "Aloy", Age = 18 }
|
|
/// }
|
|
///
|
|
/// // write items inside file.csv
|
|
/// CsvWriter.SaveRecords(people, "C:\\Users\\user\\Documents\\file.csv");
|
|
///
|
|
/// // output
|
|
/// // | Name | Age |
|
|
/// // | Artyom | 20 |
|
|
/// // | Aloy | 18 |
|
|
/// }
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public class CsvWriter : IDisposable {
|
|
private static readonly PropertyTypeCache TypeCache = new PropertyTypeCache();
|
|
|
|
private readonly Object _syncLock = new Object();
|
|
private readonly Stream _outputStream;
|
|
private readonly Encoding _encoding;
|
|
private readonly Boolean _leaveStreamOpen;
|
|
private Boolean _isDisposing;
|
|
private UInt64 _mCount;
|
|
|
|
#region Constructors
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="CsvWriter" /> class.
|
|
/// </summary>
|
|
/// <param name="outputStream">The output stream.</param>
|
|
/// <param name="leaveOpen">if set to <c>true</c> [leave open].</param>
|
|
/// <param name="encoding">The encoding.</param>
|
|
public CsvWriter(Stream outputStream, Boolean leaveOpen, Encoding encoding) {
|
|
this._outputStream = outputStream;
|
|
this._encoding = encoding;
|
|
this._leaveStreamOpen = leaveOpen;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="CsvWriter"/> class.
|
|
/// It automatically closes the stream when disposing this writer.
|
|
/// </summary>
|
|
/// <param name="outputStream">The output stream.</param>
|
|
/// <param name="encoding">The encoding.</param>
|
|
public CsvWriter(Stream outputStream, Encoding encoding)
|
|
: this(outputStream, false, encoding) {
|
|
// placeholder
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="CsvWriter"/> class.
|
|
/// It uses the Windows 1252 encoding and automatically closes
|
|
/// the stream upon disposing this writer.
|
|
/// </summary>
|
|
/// <param name="outputStream">The output stream.</param>
|
|
public CsvWriter(Stream outputStream)
|
|
: this(outputStream, false, Definitions.Windows1252Encoding) {
|
|
// placeholder
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="CsvWriter"/> class.
|
|
/// It opens the file given file, automatically closes the stream upon
|
|
/// disposing of this writer, and uses the Windows 1252 encoding.
|
|
/// </summary>
|
|
/// <param name="filename">The filename.</param>
|
|
public CsvWriter(String filename)
|
|
: this(File.OpenWrite(filename), false, Definitions.Windows1252Encoding) {
|
|
// placeholder
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="CsvWriter"/> class.
|
|
/// It opens the file given file, automatically closes the stream upon
|
|
/// disposing of this writer, and uses the given text encoding for output.
|
|
/// </summary>
|
|
/// <param name="filename">The filename.</param>
|
|
/// <param name="encoding">The encoding.</param>
|
|
public CsvWriter(String filename, Encoding encoding)
|
|
: this(File.OpenWrite(filename), false, encoding) {
|
|
// placeholder
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Properties
|
|
|
|
/// <summary>
|
|
/// Gets or sets the field separator character.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The separator character.
|
|
/// </value>
|
|
public Char SeparatorCharacter { get; set; } = ',';
|
|
|
|
/// <summary>
|
|
/// Gets or sets the escape character to use to escape field values.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The escape character.
|
|
/// </value>
|
|
public Char EscapeCharacter { get; set; } = '"';
|
|
|
|
/// <summary>
|
|
/// Gets or sets the new line character sequence to use when writing a line.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The new line sequence.
|
|
/// </value>
|
|
public String NewLineSequence { get; set; } = Environment.NewLine;
|
|
|
|
/// <summary>
|
|
/// Defines a list of properties to ignore when outputting CSV lines.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The ignore property names.
|
|
/// </value>
|
|
public List<String> IgnorePropertyNames { get; } = new List<String>();
|
|
|
|
/// <summary>
|
|
/// Gets number of lines that have been written, including the headings line.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The count.
|
|
/// </value>
|
|
public UInt64 Count {
|
|
get {
|
|
lock(this._syncLock) {
|
|
return this._mCount;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helpers
|
|
|
|
/// <summary>
|
|
/// Saves the items to a stream.
|
|
/// It uses the Windows 1252 text encoding for output.
|
|
/// </summary>
|
|
/// <typeparam name="T">The type of enumeration.</typeparam>
|
|
/// <param name="items">The items.</param>
|
|
/// <param name="stream">The stream.</param>
|
|
/// <param name="truncateData"><c>true</c> if stream is truncated, default <c>false</c>.</param>
|
|
/// <returns>Number of item saved.</returns>
|
|
public static Int32 SaveRecords<T>(IEnumerable<T> items, Stream stream, Boolean truncateData = false) {
|
|
// truncate the file if it had data
|
|
if(truncateData && stream.Length > 0) {
|
|
stream.SetLength(0);
|
|
}
|
|
|
|
using(CsvWriter writer = new CsvWriter(stream)) {
|
|
writer.WriteHeadings<T>();
|
|
writer.WriteObjects(items);
|
|
return (Int32)writer.Count;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves the items to a CSV file.
|
|
/// If the file exits, it overwrites it. If it does not, it creates it.
|
|
/// It uses the Windows 1252 text encoding for output.
|
|
/// </summary>
|
|
/// <typeparam name="T">The type of enumeration.</typeparam>
|
|
/// <param name="items">The items.</param>
|
|
/// <param name="filePath">The file path.</param>
|
|
/// <returns>Number of item saved.</returns>
|
|
public static Int32 SaveRecords<T>(IEnumerable<T> items, String filePath) => SaveRecords(items, File.OpenWrite(filePath), true);
|
|
|
|
#endregion
|
|
|
|
#region Generic, main Write Line Method
|
|
|
|
/// <summary>
|
|
/// Writes a line of CSV text. Items are converted to strings.
|
|
/// If items are found to be null, empty strings are written out.
|
|
/// If items are not string, the ToStringInvariant() method is called on them.
|
|
/// </summary>
|
|
/// <param name="items">The items.</param>
|
|
public void WriteLine(params Object[] items)
|
|
=> this.WriteLine(items.Select(x => x == null ? String.Empty : x.ToStringInvariant()));
|
|
|
|
/// <summary>
|
|
/// Writes a line of CSV text. Items are converted to strings.
|
|
/// If items are found to be null, empty strings are written out.
|
|
/// If items are not string, the ToStringInvariant() method is called on them.
|
|
/// </summary>
|
|
/// <param name="items">The items.</param>
|
|
public void WriteLine(IEnumerable<Object> items)
|
|
=> this.WriteLine(items.Select(x => x == null ? String.Empty : x.ToStringInvariant()));
|
|
|
|
/// <summary>
|
|
/// Writes a line of CSV text.
|
|
/// If items are found to be null, empty strings are written out.
|
|
/// </summary>
|
|
/// <param name="items">The items.</param>
|
|
public void WriteLine(params String[] items) => this.WriteLine((IEnumerable<String>)items);
|
|
|
|
/// <summary>
|
|
/// Writes a line of CSV text.
|
|
/// If items are found to be null, empty strings are written out.
|
|
/// </summary>
|
|
/// <param name="items">The items.</param>
|
|
public void WriteLine(IEnumerable<String> items) {
|
|
lock(this._syncLock) {
|
|
Int32 length = items.Count();
|
|
Byte[] separatorBytes = this._encoding.GetBytes(new[] { this.SeparatorCharacter });
|
|
Byte[] endOfLineBytes = this._encoding.GetBytes(this.NewLineSequence);
|
|
|
|
// Declare state variables here to avoid recreation, allocation and
|
|
// reassignment in every loop
|
|
Boolean needsEnclosing;
|
|
String textValue;
|
|
Byte[] output;
|
|
|
|
for(Int32 i = 0; i < length; i++) {
|
|
textValue = items.ElementAt(i);
|
|
|
|
// Determine if we need the string to be enclosed
|
|
// (it either contains an escape, new line, or separator char)
|
|
needsEnclosing = textValue.IndexOf(this.SeparatorCharacter) >= 0
|
|
|| textValue.IndexOf(this.EscapeCharacter) >= 0
|
|
|| textValue.IndexOf('\r') >= 0
|
|
|| textValue.IndexOf('\n') >= 0;
|
|
|
|
// Escape the escape characters by repeating them twice for every instance
|
|
textValue = textValue.Replace($"{this.EscapeCharacter}",
|
|
$"{this.EscapeCharacter}{this.EscapeCharacter}");
|
|
|
|
// Enclose the text value if we need to
|
|
if(needsEnclosing) {
|
|
textValue = String.Format($"{this.EscapeCharacter}{textValue}{this.EscapeCharacter}", textValue);
|
|
}
|
|
|
|
// Get the bytes to write to the stream and write them
|
|
output = this._encoding.GetBytes(textValue);
|
|
this._outputStream.Write(output, 0, output.Length);
|
|
|
|
// only write a separator if we are moving in between values.
|
|
// the last value should not be written.
|
|
if(i < length - 1) {
|
|
this._outputStream.Write(separatorBytes, 0, separatorBytes.Length);
|
|
}
|
|
}
|
|
|
|
// output the newline sequence
|
|
this._outputStream.Write(endOfLineBytes, 0, endOfLineBytes.Length);
|
|
this._mCount += 1;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Write Object Method
|
|
|
|
/// <summary>
|
|
/// Writes a row of CSV text. It handles the special cases where the object is
|
|
/// a dynamic object or and array. It also handles non-collection objects fine.
|
|
/// If you do not like the way the output is handled, you can simply write an extension
|
|
/// method of this class and use the WriteLine method instead.
|
|
/// </summary>
|
|
/// <param name="item">The item.</param>
|
|
/// <exception cref="System.ArgumentNullException">item.</exception>
|
|
public void WriteObject(Object item) {
|
|
if(item == null) {
|
|
throw new ArgumentNullException(nameof(item));
|
|
}
|
|
|
|
lock(this._syncLock) {
|
|
switch(item) {
|
|
case IDictionary typedItem:
|
|
this.WriteLine(this.GetFilteredDictionary(typedItem));
|
|
return;
|
|
case ICollection typedItem:
|
|
this.WriteLine(typedItem.Cast<Object>());
|
|
return;
|
|
default:
|
|
this.WriteLine(this.GetFilteredTypeProperties(item.GetType())
|
|
.Select(x => x.ToFormattedString(item)));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a row of CSV text. It handles the special cases where the object is
|
|
/// a dynamic object or and array. It also handles non-collection objects fine.
|
|
/// If you do not like the way the output is handled, you can simply write an extension
|
|
/// method of this class and use the WriteLine method instead.
|
|
/// </summary>
|
|
/// <typeparam name="T">The type of object to write.</typeparam>
|
|
/// <param name="item">The item.</param>
|
|
public void WriteObject<T>(T item) => this.WriteObject(item as Object);
|
|
|
|
/// <summary>
|
|
/// Writes a set of items, one per line and atomically by repeatedly calling the
|
|
/// WriteObject method. For more info check out the description of the WriteObject
|
|
/// method.
|
|
/// </summary>
|
|
/// <typeparam name="T">The type of object to write.</typeparam>
|
|
/// <param name="items">The items.</param>
|
|
public void WriteObjects<T>(IEnumerable<T> items) {
|
|
lock(this._syncLock) {
|
|
foreach(T item in items) {
|
|
this.WriteObject(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Write Headings Methods
|
|
|
|
/// <summary>
|
|
/// Writes the headings.
|
|
/// </summary>
|
|
/// <param name="type">The type of object to extract headings.</param>
|
|
/// <exception cref="System.ArgumentNullException">type.</exception>
|
|
public void WriteHeadings(Type type) {
|
|
if(type == null) {
|
|
throw new ArgumentNullException(nameof(type));
|
|
}
|
|
|
|
IEnumerable<Object> properties = this.GetFilteredTypeProperties(type).Select(p => p.Name).Cast<Object>();
|
|
this.WriteLine(properties);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes the headings.
|
|
/// </summary>
|
|
/// <typeparam name="T">The type of object to extract headings.</typeparam>
|
|
public void WriteHeadings<T>() => this.WriteHeadings(typeof(T));
|
|
|
|
/// <summary>
|
|
/// Writes the headings.
|
|
/// </summary>
|
|
/// <param name="dictionary">The dictionary to extract headings.</param>
|
|
/// <exception cref="System.ArgumentNullException">dictionary.</exception>
|
|
public void WriteHeadings(IDictionary dictionary) {
|
|
if(dictionary == null) {
|
|
throw new ArgumentNullException(nameof(dictionary));
|
|
}
|
|
|
|
this.WriteLine(this.GetFilteredDictionary(dictionary, true));
|
|
}
|
|
|
|
#if NET452
|
|
/// <summary>
|
|
/// Writes the headings.
|
|
/// </summary>
|
|
/// <param name="item">The object to extract headings.</param>
|
|
/// <exception cref="ArgumentNullException">item</exception>
|
|
/// <exception cref="ArgumentException">Unable to cast dynamic object to a suitable dictionary - item</exception>
|
|
public void WriteHeadings(dynamic item) {
|
|
if(item == null) {
|
|
throw new ArgumentNullException(nameof(item));
|
|
}
|
|
|
|
if(!(item is IDictionary<global::System.String, global::System.Object> dictionary)) {
|
|
throw new ArgumentException("Unable to cast dynamic object to a suitable dictionary", nameof(item));
|
|
}
|
|
|
|
this.WriteHeadings(dictionary);
|
|
}
|
|
#else
|
|
/// <summary>
|
|
/// Writes the headings.
|
|
/// </summary>
|
|
/// <param name="obj">The object to extract headings.</param>
|
|
/// <exception cref="ArgumentNullException">obj.</exception>
|
|
public void WriteHeadings(object obj)
|
|
{
|
|
if (obj == null)
|
|
throw new ArgumentNullException(nameof(obj));
|
|
|
|
WriteHeadings(obj.GetType());
|
|
}
|
|
#endif
|
|
|
|
#endregion
|
|
|
|
#region IDisposable Support
|
|
|
|
/// <inheritdoc />
|
|
public void Dispose() => this.Dispose(true);
|
|
|
|
/// <summary>
|
|
/// Releases unmanaged and - optionally - managed resources.
|
|
/// </summary>
|
|
/// <param name="disposeAlsoManaged"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
|
protected virtual void Dispose(Boolean disposeAlsoManaged) {
|
|
if(this._isDisposing) {
|
|
return;
|
|
}
|
|
|
|
if(disposeAlsoManaged) {
|
|
if(this._leaveStreamOpen == false) {
|
|
this._outputStream.Dispose();
|
|
}
|
|
}
|
|
|
|
this._isDisposing = true;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Support Methods
|
|
|
|
private IEnumerable<String> GetFilteredDictionary(IDictionary dictionary, Boolean filterKeys = false)
|
|
=> dictionary
|
|
.Keys
|
|
.Cast<Object>()
|
|
.Select(key => key == null ? String.Empty : key.ToStringInvariant())
|
|
.Where(stringKey => !this.IgnorePropertyNames.Contains(stringKey))
|
|
.Select(stringKey =>
|
|
filterKeys
|
|
? stringKey
|
|
: dictionary[stringKey] == null ? String.Empty : dictionary[stringKey].ToStringInvariant());
|
|
|
|
private IEnumerable<PropertyInfo> GetFilteredTypeProperties(Type type)
|
|
=> TypeCache.Retrieve(type, t =>
|
|
t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
|
.Where(p => p.CanRead))
|
|
.Where(p => !this.IgnorePropertyNames.Contains(p.Name));
|
|
|
|
#endregion
|
|
|
|
}
|
|
} |