diff --git a/FileHelpers.FSharp.Tests/FileHelpers.FSharp.Tests.csproj b/FileHelpers.FSharp.Tests/FileHelpers.FSharp.Tests.csproj index 1f8bc4cbb..643434aa8 100644 --- a/FileHelpers.FSharp.Tests/FileHelpers.FSharp.Tests.csproj +++ b/FileHelpers.FSharp.Tests/FileHelpers.FSharp.Tests.csproj @@ -65,6 +65,9 @@ 3.12.0 + + 3.17.0 + 1.5.0 diff --git a/FileHelpers.Tests/FileHelpers.Tests.csproj b/FileHelpers.Tests/FileHelpers.Tests.csproj index 0d8c63b41..96e37d9e0 100644 --- a/FileHelpers.Tests/FileHelpers.Tests.csproj +++ b/FileHelpers.Tests/FileHelpers.Tests.csproj @@ -147,6 +147,7 @@ Code + @@ -658,6 +659,9 @@ 3.12.0 + + 3.17.0 + diff --git a/FileHelpers.Tests/Tests/Common/DelimitedFieldPadding.cs b/FileHelpers.Tests/Tests/Common/DelimitedFieldPadding.cs new file mode 100644 index 000000000..fbaf2ed5c --- /dev/null +++ b/FileHelpers.Tests/Tests/Common/DelimitedFieldPadding.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; + +namespace FileHelpers.Tests.CommonTests +{ + [TestFixture] + public class DelimitedFieldPadding + { + + [DelimitedRecord(",")] + private class AlignCenterSample + { + public AlignCenterSample() + { + + } + + public AlignCenterSample(string name, int amount, DateTime date ) + { + this.Name = name; + this.Amount = amount; + this.Date = date; + } + + + [DelimitedFieldPadding(6, AlignMode.Center, '+')] + public string Name { get; set; } + + [DelimitedFieldPadding(5, AlignMode.Center, '+')] + [FieldConverter(ConverterKind.Int32)] + public int Amount { get; set; } + + [DelimitedFieldPadding(12, AlignMode.Center, '+')] + [FieldConverter(ConverterKind.Date, "yyyy-MM-dd")] + public DateTime Date { get; set; } + } + + + [Test] + public void TestPaddingCenterAlign() + { + + List records = new List(); + records.Add(new AlignCenterSample("AA", 100, new DateTime(2020, 1, 1))); + + + var engine = new FileHelperEngine(); + string output = engine.WriteString(records).TrimEnd(); + + string expected = "++AA++,+100+,+2020-01-01+"; + + Assert.AreEqual(expected, output); + } + + + [DelimitedRecord(",")] + private class AlignLeftSample + { + public AlignLeftSample() + { + + } + + public AlignLeftSample(string name, int amount, DateTime date) + { + this.Name = name; + this.Amount = amount; + this.Date = date; + } + + + [DelimitedFieldPadding(6, AlignMode.Left, ' ')] + public string Name { get; set; } + + [DelimitedFieldPadding(5, AlignMode.Left, ' ')] + [FieldConverter(ConverterKind.Int32)] + public int Amount { get; set; } + + [DelimitedFieldPadding(12, AlignMode.Left, '*')] + [FieldConverter(ConverterKind.Date, "yyyy-MM-dd")] + public DateTime Date { get; set; } + } + + + [Test] + public void TestPaddingLeftAlign() + { + + var records = new List(); + records.Add(new AlignLeftSample("AA", 100, new DateTime(2020, 1, 1))); + + + var engine = new FileHelperEngine(); + string output = engine.WriteString(records).TrimEnd(); + + string expected = "AA ,100 ,2020-01-01**"; + + Assert.AreEqual(expected, output); + } + + + [DelimitedRecord(",")] + private class AlignRightSample + { + public AlignRightSample() + { + + } + + public AlignRightSample(string name, int amount, DateTime date) + { + this.Name = name; + this.Amount = amount; + this.Date = date; + } + + + [DelimitedFieldPadding(6, AlignMode.Right, ' ')] + public string Name { get; set; } + + [DelimitedFieldPadding(5, AlignMode.Right, '*')] + [FieldConverter(ConverterKind.Int32)] + public int Amount { get; set; } + + [DelimitedFieldPadding(12, AlignMode.Right, '+')] + [FieldConverter(ConverterKind.Date, "yyyy-MM-dd")] + public DateTime Date { get; set; } + } + + + + + [Test] + public void TestPaddingRightAlign() + { + + var records = new List(); + records.Add(new AlignRightSample("AA", 100, new DateTime(2020, 1, 1))); + + + var engine = new FileHelperEngine(); + string output = engine.WriteString(records).TrimEnd(); + + string expected = " AA,**100,++2020-01-01"; + + Assert.AreEqual(expected, output); + } + + + [FixedLengthRecord] + private class FixedWithWithDelimitedFiledPaddingSample + { + public FixedWithWithDelimitedFiledPaddingSample() + { + + } + + public FixedWithWithDelimitedFiledPaddingSample(string name, string description) + { + this.Name = name; + this.Description = description; + } + + [DelimitedFieldPadding(10, AlignMode.Left, ' ')] + [FieldFixedLength(10)] + public string Name { get; set; } + + [DelimitedFieldPadding(50, AlignMode.Left, ' ')] + [FieldFixedLength(50)] + public string Description { get; set; } + + + } + + + + + + + + [Test] + public void DelimitedFieldPaddingInvalidOnFixedWidthFile() + { + + Assert.Throws(() => + { + var engine = new FileHelperEngine(); + }); + + } + } +} diff --git a/FileHelpers/Attributes/DelimitedFieldPaddingAttribute.cs b/FileHelpers/Attributes/DelimitedFieldPaddingAttribute.cs new file mode 100644 index 000000000..85902c2bd --- /dev/null +++ b/FileHelpers/Attributes/DelimitedFieldPaddingAttribute.cs @@ -0,0 +1,45 @@ +using System; + + +namespace FileHelpers +{ + /// + /// Supports padding when applied to a Delimited record field or property + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public sealed class DelimitedFieldPaddingAttribute : Attribute + { + /// + /// Total length of the fields in the output file. + /// + public int TotalLength { get; private set; } + + /// + /// The position of the alignment. + /// + public AlignMode AlignMode { get; private set; } + + /// + /// The character for padding. + /// + public char PaddingChar { get; private set; } + + + /// + /// Adds padding for a delimited file record field + /// + /// Total length + /// Alignment position + /// Character used for padding + public DelimitedFieldPaddingAttribute(int totalLength, AlignMode alignMode, char paddingChar) + { + + this.PaddingChar = paddingChar; + + this.TotalLength = totalLength; + + this.AlignMode = alignMode; + + } + } +} diff --git a/FileHelpers/Fields/DelimitedField.cs b/FileHelpers/Fields/DelimitedField.cs index d8b40fb39..ac148aa24 100644 --- a/FileHelpers/Fields/DelimitedField.cs +++ b/FileHelpers/Fields/DelimitedField.cs @@ -1,3 +1,4 @@ +using System; using System.Globalization; using System.Reflection; using System.Text; @@ -186,6 +187,10 @@ internal override void CreateFieldString(StringBuilder sb, object fieldValue, bo { string field = base.CreateFieldString(fieldValue); + + if (this.DelimitedFieldPaddingChar.HasValue && this.DelimitedTotalLength > 0 && field != null && field.Length < this.DelimitedTotalLength) + field = this.applyDelimitedFieldPadding(field); + bool hasNewLine = mCompare.IndexOf(field, StringHelper.NewLine, CompareOptions.Ordinal) >= 0; // If have a new line and this is not allowed. We throw an exception @@ -214,6 +219,54 @@ internal override void CreateFieldString(StringBuilder sb, object fieldValue, bo sb.Append(Separator); } + private string applyDelimitedFieldPadding(string baseFieldValue) + { + + string safeFieldValue = baseFieldValue ?? String.Empty; + + string formattedOutput = null; + + + + if (baseFieldValue != null && this.DelimitedTotalLength > 0 && baseFieldValue.Length < this.DelimitedTotalLength && this.DelimitedFieldPaddingChar.HasValue) + { + switch (this.DelimitedFieldAlignMode) + { + case AlignMode.Left: + formattedOutput = safeFieldValue.PadRight(this.DelimitedTotalLength, this.DelimitedFieldPaddingChar.Value); + break; + + case AlignMode.Right: + formattedOutput = safeFieldValue.PadLeft(this.DelimitedTotalLength, this.DelimitedFieldPaddingChar.Value); + break; + + case AlignMode.Center: + formattedOutput = this.padCenter(safeFieldValue); + break; + + + } + + } + else + formattedOutput = baseFieldValue; + + + return formattedOutput; + } + + private string padCenter(string baseFieldValue) + { + if (baseFieldValue == null) + throw new NullReferenceException("baseFieldValue"); + + int totalPaddingCount = this.DelimitedTotalLength - baseFieldValue.Length; + + int padLeftCount = totalPaddingCount / 2 + baseFieldValue.Length; + + return baseFieldValue.PadLeft(padLeftCount, this.DelimitedFieldPaddingChar.GetValueOrDefault()).PadRight(this.DelimitedTotalLength, this.DelimitedFieldPaddingChar.GetValueOrDefault()); + } + /// /// create a field base class and populate the delimited values /// base class will add its own values diff --git a/FileHelpers/Fields/FieldBase.cs b/FileHelpers/Fields/FieldBase.cs index bcbafc8d6..d03ba76ab 100644 --- a/FileHelpers/Fields/FieldBase.cs +++ b/FileHelpers/Fields/FieldBase.cs @@ -158,6 +158,12 @@ internal bool NextIsOptional /// internal string FieldCaption { get; private set; } + internal char? DelimitedFieldPaddingChar { get; private set; } + + internal AlignMode DelimitedFieldAlignMode { get; private set; } + + internal int DelimitedTotalLength { get; private set; } + // -------------------------------------------------------------- // WARNING !!! // Remember to add each of these fields to the clone method !! @@ -207,6 +213,8 @@ public static FieldBase CreateField(FieldInfo fi, TypedRecordAttribute recordAtt #pragma warning restore 612,618 return null; + + var attributes = (FieldAttribute[])mi.GetCustomAttributes(typeof(FieldAttribute), true); // CHECK USAGE ERRORS !!! @@ -239,6 +247,14 @@ public static FieldBase CreateField(FieldInfo fi, TypedRecordAttribute recordAtt "' can't be marked with the FieldArrayLength attribute - it is only valid for array fields."); } + if(!(recordAttribute is DelimitedRecordAttribute) + && mi.IsDefined(typeof(DelimitedFieldPaddingAttribute), false)) + { + throw new BadUsageException(memberName + + "' can't be marked with the PaddingConverterAttribute attribute - it is only valid for delimited files."); + + } + // PROCESS IN NORMAL CONDITIONS if (attributes.Length > 0) { @@ -299,6 +315,8 @@ public static FieldBase CreateField(FieldInfo fi, TypedRecordAttribute recordAtt // FieldDiscarded res.Discarded = mi.IsDefined(typeof(FieldValueDiscardedAttribute), false); + + // FieldTrim Attributes.WorkWithFirst(mi, (x) => @@ -371,6 +389,16 @@ public static FieldBase CreateField(FieldInfo fi, TypedRecordAttribute recordAtt if (string.IsNullOrEmpty(res.FieldFriendlyName)) res.FieldFriendlyName = res.FieldName; + //PaddingConverter + Attributes.WorkWithFirst(mi, + (x) => + { + res.DelimitedFieldAlignMode = x.AlignMode; + res.DelimitedFieldPaddingChar = x.PaddingChar; + res.DelimitedTotalLength = x.TotalLength; + }); + + return res; } @@ -420,6 +448,9 @@ internal FieldBase() NullValue = null; IsArray = false; IsNotEmpty = false; + DelimitedFieldPaddingChar = null; + DelimitedFieldAlignMode = AlignMode.Left; + DelimitedTotalLength = 0; } /// @@ -889,6 +920,8 @@ public object CreateValueForField(object fieldValue) } } + + return val; } @@ -978,6 +1011,9 @@ internal FieldBase Clone() res.FieldCaption = FieldCaption; res.Parent = Parent; res.ParentIndex = ParentIndex; + res.DelimitedFieldAlignMode = DelimitedFieldAlignMode; + res.DelimitedFieldPaddingChar = DelimitedFieldPaddingChar; + res.DelimitedTotalLength = DelimitedTotalLength; return res; } diff --git a/FileHelpers/FileHelpers.csproj b/FileHelpers/FileHelpers.csproj index 2da802573..7515f899a 100644 --- a/FileHelpers/FileHelpers.csproj +++ b/FileHelpers/FileHelpers.csproj @@ -41,7 +41,7 @@ 3.5 - false + false ..\Debug\Bin\ @@ -55,7 +55,8 @@ true 4096 false - + + false false false @@ -104,6 +105,7 @@ Code + Code