feat: Initialize Strata.Excel.Core library with export, import, and test utilities
This commit introduces the `Strata.Excel.Core` project, a .NET library built on ClosedXML for robust Excel document generation and data import. Key features include: - **Export:** Flexible data export to Excel, supporting custom formatting, titles, subtitles, humanized headings, and batched processing for large datasets. Includes `ExcelContentResult` and `ZipFileContentResult` for ASP.NET Core integration. - **Import:** Utilities to easily import data from Excel worksheets into C# objects. - **Test Utilities:** Comprehensive helpers for comparing Excel workbooks in tests, handling resource extraction, and performing load tests. - **Build Infrastructure:** Sets up a Dockerfile for building the library, including SonarQube and Dependency-Check scanning. - **Project Structure:** Establishes `.gitignore`, `.dockerignore`, `nuget.config`, and a `README.md` with usage instructions and versioning guidelines. This foundational commit provides a reusable and well-tested framework for Excel operations within Strata applications.
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Strata.Excel.TestUtilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Help methods for work with streams
|
||||
/// </summary>
|
||||
[ExcludeFromCodeCoverage]
|
||||
public static class StreamHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Convert stream to byte array
|
||||
/// </summary>
|
||||
/// <param name="pStream">Stream</param>
|
||||
/// <returns>Byte array</returns>
|
||||
public static byte[] StreamToArray(Stream pStream)
|
||||
{
|
||||
long iLength = pStream.Length;
|
||||
var bytes = new byte[iLength];
|
||||
for (int i = 0; i < iLength; i++)
|
||||
{
|
||||
bytes[i] = (byte)pStream.ReadByte();
|
||||
}
|
||||
pStream.Close();
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert byte array to stream
|
||||
/// </summary>
|
||||
/// <param name="pBynaryArray">Byte array</param>
|
||||
/// <param name="pStream">Open stream</param>
|
||||
/// <returns></returns>
|
||||
public static Stream ArrayToStreamAppend(byte[] pBynaryArray, Stream pStream)
|
||||
{
|
||||
#region Check params
|
||||
|
||||
if (ReferenceEquals(pBynaryArray, null))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(pBynaryArray));
|
||||
}
|
||||
if (ReferenceEquals(pStream, null))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(pStream));
|
||||
}
|
||||
if (!pStream.CanWrite)
|
||||
{
|
||||
throw new ArgumentException("Can't write to stream", nameof(pStream));
|
||||
}
|
||||
|
||||
#endregion Check params
|
||||
|
||||
foreach (byte b in pBynaryArray)
|
||||
{
|
||||
pStream.WriteByte(b);
|
||||
}
|
||||
return pStream;
|
||||
}
|
||||
|
||||
public static void StreamToStreamAppend(Stream streamIn, Stream streamToWrite)
|
||||
{
|
||||
StreamToStreamAppend(streamIn, streamToWrite, 0);
|
||||
}
|
||||
|
||||
public static void StreamToStreamAppend(Stream streamIn, Stream streamToWrite, long dataLength)
|
||||
{
|
||||
#region Check params
|
||||
|
||||
if (ReferenceEquals(streamIn, null))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(streamIn));
|
||||
}
|
||||
if (ReferenceEquals(streamToWrite, null))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(streamToWrite));
|
||||
}
|
||||
if (!streamIn.CanRead)
|
||||
{
|
||||
throw new ArgumentException("Can't read from stream", nameof(streamIn));
|
||||
}
|
||||
if (!streamToWrite.CanWrite)
|
||||
{
|
||||
throw new ArgumentException("Can't write to stream", nameof(streamToWrite));
|
||||
}
|
||||
|
||||
#endregion Check params
|
||||
|
||||
var buf = new byte[512];
|
||||
long length;
|
||||
if (dataLength == 0)
|
||||
{
|
||||
length = streamIn.Length - streamIn.Position;
|
||||
}
|
||||
else
|
||||
{
|
||||
length = dataLength;
|
||||
}
|
||||
long rest = length;
|
||||
while (rest > 0)
|
||||
{
|
||||
int len1 = streamIn.Read(buf, 0, rest >= 512 ? 512 : (int)rest);
|
||||
streamToWrite.Write(buf, 0, len1);
|
||||
rest -= len1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compare two streams by converting them to strings and comparing the strings
|
||||
/// </summary>
|
||||
/// <param name="one"></param>
|
||||
/// <param name="other"></param>
|
||||
/// /// <param name="stripColumnWidths"></param>
|
||||
/// <returns></returns>
|
||||
public static bool Compare(Tuple<Uri, Stream> tuple1, Tuple<Uri, Stream> tuple2, bool stripColumnWidths)
|
||||
{
|
||||
#region Check
|
||||
|
||||
if (tuple1 == null || tuple1.Item1 == null || tuple1.Item2 == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tuple1));
|
||||
}
|
||||
if (tuple2 == null || tuple2.Item1 == null || tuple2.Item2 == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tuple2));
|
||||
}
|
||||
if (tuple1.Item2.Position != 0)
|
||||
{
|
||||
throw new ArgumentException("Must be in position 0", nameof(tuple1));
|
||||
}
|
||||
if (tuple2.Item2.Position != 0)
|
||||
{
|
||||
throw new ArgumentException("Must be in position 0", nameof(tuple2));
|
||||
}
|
||||
|
||||
#endregion Check
|
||||
|
||||
var stringOne = new StreamReader(tuple1.Item2).ReadToEnd().RemoveIgnoredParts(tuple1.Item1, stripColumnWidths, ignoreGuids: true);
|
||||
var stringOther = new StreamReader(tuple2.Item2).ReadToEnd().RemoveIgnoredParts(tuple2.Item1, stripColumnWidths, ignoreGuids: true);
|
||||
return stringOne == stringOther;
|
||||
}
|
||||
|
||||
private static string RemoveIgnoredParts(this string s, Uri uri, bool ignoreColumnWidths, bool ignoreGuids)
|
||||
{
|
||||
s = uriSpecificIgnores.Where(p => p.Key.Equals(uri.OriginalString)).Aggregate(s, (current, pair) => pair.Value.Replace(current, ""));
|
||||
|
||||
// Collapse empty xml elements
|
||||
s = emptyXmlElementRegex.Replace(s, "<$1 />");
|
||||
|
||||
if (ignoreColumnWidths)
|
||||
s = RemoveColumnWidths(s);
|
||||
|
||||
if (ignoreGuids)
|
||||
s = RemoveGuids(s);
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
private static IEnumerable<KeyValuePair<string, Regex>> uriSpecificIgnores = new List<KeyValuePair<string, Regex>>()
|
||||
{
|
||||
// Remove dcterms elements
|
||||
new KeyValuePair<string, Regex>("/docProps/core.xml", new Regex(@"<dcterms:(\w+).*?<\/dcterms:\1>", RegexOptions.Compiled))
|
||||
};
|
||||
|
||||
private static Regex emptyXmlElementRegex = new Regex(@"<([\w:]+)><\/\1>", RegexOptions.Compiled);
|
||||
private static Regex columnRegex = new Regex("<x:col.*?width=\"\\d+(\\.\\d+)?\".*?\\/>", RegexOptions.Compiled);
|
||||
private static Regex widthRegex = new Regex("width=\"\\d+(\\.\\d+)?\"\\s+", RegexOptions.Compiled);
|
||||
|
||||
private static string RemoveColumnWidths(string s)
|
||||
{
|
||||
var replacements = new Dictionary<string, string>();
|
||||
|
||||
foreach (var m in columnRegex.Matches(s).OfType<Match>())
|
||||
{
|
||||
var original = m.Groups[0].Value;
|
||||
var replacement = widthRegex.Replace(original, "");
|
||||
replacements.Add(original, replacement);
|
||||
}
|
||||
|
||||
return replacements.Aggregate(s, (current, r) => current.Replace(r.Key, r.Value));
|
||||
}
|
||||
|
||||
private static Regex guidRegex = new Regex(@"{[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}}", RegexOptions.Compiled | RegexOptions.Multiline);
|
||||
|
||||
private static string RemoveGuids(string s) => guidRegex.Replace(s, m => string.Empty);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user