2019-12-04 17:10:06 +01:00
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
2019-02-17 14:08:57 +01:00
/// <summary>
2019-12-04 17:10:06 +01:00
/// Initializes a new instance of the <see cref="CsvWriter" /> class.
2019-02-17 14:08:57 +01:00
/// </summary>
2019-12-04 17:10:06 +01:00
/// <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 ) ) ;
}
2019-02-17 14:08:57 +01:00
#if NET452
2019-12-04 17:10:06 +01:00
/// <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 ) ;
}
2019-02-17 14:08:57 +01:00
#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
2019-12-04 17:10:06 +01:00
#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
}
2019-02-17 14:08:57 +01:00
}