09-spaceship/SpaceShuttle/Tools/CsvImport.cs
github-classroom[bot] 8a812b7eac
Initial commit
2026-01-08 10:40:56 +00:00

328 lines
12 KiB
C#

/*
This file is part of https://github.com/aiten/Framework.
Copyright (c) Herbert Aitenbichler
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
namespace SpaceShuttle.Tools;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
public class CsvImport<T> : CsvImportBase where T : class
{
public CsvImport()
{
if (typeof(T).GetCustomAttributes(typeof(CsvImportFormatAttribute)).FirstOrDefault() is CsvImportFormatAttribute formatAttribute)
{
if (!string.IsNullOrEmpty(formatAttribute.Format)) throw new ArgumentException("cannot use format for class attribute");
if (!string.IsNullOrEmpty(formatAttribute.Culture))
{
DateTimeCultureInfo = CultureInfo.GetCultureInfo(formatAttribute.Culture);
NumberFormat = DateTimeCultureInfo.NumberFormat;
}
}
}
public class ColumnMapping
{
public required string ColumnName { get; set; }
public PropertyInfo? MapTo { get; set; }
public bool Ignore { get; set; }
public string? CsvFormat { get; set; }
public CultureInfo? DateTimeCultureInfo { get; set; }
public NumberFormatInfo? NumberFormat { get; set; }
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
public Func<string, object?>? GetValue { get; set; }
public Func<object?, object?>? AdjustValue { get; set; }
public Action<T, string>? SetValue { get; set; }
#pragma warning restore CS8632
public bool IsConfigured => Ignore || MapTo != null || SetValue != null;
public bool IsMapped => !Ignore && MapTo != null;
public bool IsSetValue => !Ignore && SetValue != null;
}
public ICollection<string>? IgnoreColumns { get; set; }
public IDictionary<string, string>? MapColumns { get; set; }
public IList<T> Read(string[] csvLines)
{
var lines = ReadStringMatrixFromCsv(csvLines, false);
return MapTo(lines);
}
public IList<T> Read(string fileName)
{
var lines = ReadStringMatrixFromCsv(fileName, false);
return MapTo(lines);
}
public async Task<IList<T>> ReadAsync(string fileName)
{
var lines = await ReadStringMatrixFromCsvAsync(fileName, false);
return MapTo(lines);
}
public IList<T> MapTo(IList<IList<string>> lines)
{
// first line is columnLineHeader!!!!
var mapping = GetPropertyMapping(lines[0]);
CheckPropertyMapping(mapping);
var list = new List<T>();
var first = true;
foreach (var line in lines)
{
if (first)
{
first = false;
}
else
{
list.Add(Map(line, mapping));
}
}
return list;
}
private void CheckPropertyMapping(ColumnMapping[] mapping)
{
var notConfigured = mapping.Where(m => !m.IsConfigured).ToList();
if (notConfigured.Any())
{
foreach (var col in notConfigured)
{
if (typeof(T).GetField(col.ColumnName, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) != null)
{
throw new ArgumentException($"Column ist mapped to field instead of a property: {col.ColumnName}");
}
if (typeof(T).GetProperty(col.ColumnName, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) != null)
{
throw new ArgumentException($"Column ist mapped to none public property: {col.ColumnName}");
}
}
var columnList = string.Join(", ", notConfigured.Select(m => m.ColumnName));
throw new ArgumentException($"Column cannot be mapped: {columnList}");
}
var notCanWrite = mapping.Where(x => x.IsMapped && !x.MapTo!.CanWrite).ToList();
if (notCanWrite.Any())
{
var columnList = string.Join(", ", notCanWrite.Select(m => m.ColumnName));
throw new ArgumentException($"Column is readonly: {columnList}");
}
}
protected virtual ColumnMapping[] GetPropertyMapping(IList<string> columnNames)
{
return columnNames
.Select(GetColumnMapping)
.ToArray();
}
public Action<ColumnMapping>? ConfigureColumnMapping { get; set; }
protected virtual ColumnMapping GetColumnMapping(string columnName)
{
var ignoreColumn = IgnoreColumns?.Contains(columnName, StringComparer.InvariantCultureIgnoreCase) ?? false;
var mapToColumn = (MapColumns?.ContainsKey(columnName) ?? false) ? MapColumns[columnName] : columnName;
var columnMapping = new ColumnMapping
{
ColumnName = columnName,
Ignore = ignoreColumn,
MapTo = ignoreColumn ? null : GetPropertyInfo(mapToColumn),
};
if (columnMapping.MapTo != null)
{
if (columnMapping.MapTo.GetCustomAttributes(typeof(CsvImportFormatAttribute)).FirstOrDefault() is CsvImportFormatAttribute formatAttribute)
{
columnMapping.CsvFormat = formatAttribute.Format;
if (!string.IsNullOrEmpty(formatAttribute.Culture))
{
columnMapping.DateTimeCultureInfo = CultureInfo.GetCultureInfo(formatAttribute.Culture);
columnMapping.NumberFormat = columnMapping.DateTimeCultureInfo.NumberFormat;
}
}
}
ConfigureColumnMapping?.Invoke(columnMapping);
return columnMapping;
}
public static PropertyInfo? GetPropertyInfo(string columnName)
{
return typeof(T).GetProperty(columnName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
}
private T Map(IList<string> line, ColumnMapping[] mapping)
{
// because the T may be with "required", we cant use new() any more
// var newT = new T();
// now create it with reflection
var newT = (T)Activator.CreateInstance(typeof(T))!;
if (mapping.Length < line.Count)
{
var message = $"Line '{string.Join(",", line)}' has to many columns";
throw new ArgumentException(message);
}
var idx = 0;
foreach (var column in line)
{
AssignProperty(newT, column, mapping[idx++]);
}
return newT;
}
#pragma warning disable 8632
private object? GetValue(string valueAsString, Type asType, ColumnMapping mapping)
#pragma warning restore 8632
{
if (asType.IsGenericType && asType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
if (string.IsNullOrEmpty(valueAsString))
{
return null;
}
asType = asType.GenericTypeArguments[0];
}
try
{
if (asType == typeof(string))
{
return ExcelString(valueAsString);
}
else if (asType == typeof(int))
{
return ExcelInt(valueAsString);
}
else if (asType == typeof(long))
{
return ExcelLong(valueAsString);
}
else if (asType == typeof(short))
{
return ExcelShort(valueAsString);
}
else if (asType == typeof(uint))
{
return ExcelUInt(valueAsString);
}
else if (asType == typeof(ulong))
{
return ExcelULong(valueAsString);
}
else if (asType == typeof(ushort))
{
return ExcelUShort(valueAsString);
}
else if (asType == typeof(decimal))
{
return ExcelDecimal(valueAsString, mapping.NumberFormat ?? NumberFormat);
}
else if (asType == typeof(byte))
{
return ExcelByte(valueAsString);
}
else if (asType == typeof(bool))
{
return ExcelBool(valueAsString);
}
else if (asType == typeof(DateTime))
{
return ExcelDateOrDateTime(valueAsString, mapping.CsvFormat, mapping.DateTimeCultureInfo ?? DateTimeCultureInfo);
}
else if (asType == typeof(DateOnly))
{
return ExcelDateOnly(valueAsString, mapping.CsvFormat ?? DateFormat, mapping.DateTimeCultureInfo ?? DateTimeCultureInfo);
}
else if (asType == typeof(TimeOnly))
{
return ExcelTimeOnly(valueAsString, mapping.CsvFormat ?? TimeFormat, mapping.DateTimeCultureInfo ?? DateTimeCultureInfo);
}
else if (asType == typeof(TimeSpan))
{
return ExcelTimeSpan(valueAsString);
}
else if (asType == typeof(float))
{
return ExcelFloat(valueAsString, mapping.NumberFormat ?? NumberFormat);
}
else if (asType == typeof(double))
{
return ExcelDouble(valueAsString, mapping.NumberFormat ?? NumberFormat);
}
else if (asType.IsEnum)
{
return ExcelEnum(asType, valueAsString);
}
else if (asType == typeof(byte[]))
{
return ExcelImage(valueAsString);
}
}
catch (FormatException e)
{
throw new ArgumentException($"Illegal value for column '{mapping.ColumnName}:{asType.Name}': {valueAsString}", e);
}
throw new ArgumentException($"Illegal type of column '{mapping.ColumnName}': {asType.Name}");
}
private void AssignProperty(object obj, string valueAsString, ColumnMapping mapping)
{
if (mapping.IsSetValue)
{
mapping.SetValue!((T)obj, valueAsString);
}
else if (mapping.IsMapped)
{
var mapTo = mapping.MapTo;
#pragma warning disable 8632
object? val = mapping.GetValue != null
? mapping.GetValue(valueAsString)
: GetValue(valueAsString, mapTo!.PropertyType, mapping);
#pragma warning restore 8632
if (mapping.AdjustValue != null)
{
val = mapping.AdjustValue(val);
}
mapTo!.SetValue(obj, val);
}
}
}