diff --git a/Directory.Build.props b/Directory.Build.props
index 6dfe6eff..346a4bd4 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -4,7 +4,7 @@
enable
true
net10.0
- 0.9.1
- 0.9.1
+ 0.10.0-fixedWidth.11
+ 0.10.0-fixedWidth.11
diff --git a/src/serde/FixedWidth/FieldOverflowHandling.cs b/src/serde/FixedWidth/FieldOverflowHandling.cs
new file mode 100644
index 00000000..ad1f5653
--- /dev/null
+++ b/src/serde/FixedWidth/FieldOverflowHandling.cs
@@ -0,0 +1,21 @@
+using StaticCs;
+
+namespace Serde.FixedWidth
+{
+ ///
+ /// Enumerates the supported options for field values that are too long.
+ ///
+ [Closed]
+ public enum FieldOverflowHandling
+ {
+ ///
+ /// Indicates that an exception should be thrown if the field value is longer than the field length.
+ ///
+ Throw = 0,
+
+ ///
+ /// Indicates that the field value should be truncated if the value is longer than the field length.
+ ///
+ Truncate = 1,
+ }
+}
diff --git a/src/serde/FixedWidth/FixedFieldInfoAttribute.cs b/src/serde/FixedWidth/FixedFieldInfoAttribute.cs
new file mode 100644
index 00000000..c1554dc8
--- /dev/null
+++ b/src/serde/FixedWidth/FixedFieldInfoAttribute.cs
@@ -0,0 +1,102 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Reflection;
+
+namespace Serde.FixedWidth
+{
+ ///
+ /// Decorates a field in a fixed-width file with meta information about that field.
+ ///
+ [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
+ public sealed class FixedFieldInfoAttribute : Attribute
+ {
+ ///
+ /// Gets the offset that indicates where the field begins.
+ ///
+ public int Offset { get; }
+
+ ///
+ /// Gets the length of the field.
+ ///
+ public int Length { get; }
+
+ ///
+ /// Gets the format string for providing to TryParseExact.
+ ///
+ ///
+ /// Defaults to .
+ ///
+ public string Format { get; }
+
+ ///
+ /// Gets a value indicating how to handle field overflows.
+ ///
+ ///
+ /// Defaults to .
+ ///
+ public FieldOverflowHandling OverflowHandling { get; }
+
+ public FixedFieldInfoAttribute(int offset, int length, string format = "", FieldOverflowHandling overflowHandling = FieldOverflowHandling.Throw)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(offset);
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(length);
+ Offset = offset;
+ Length = length;
+ Format = string.IsNullOrWhiteSpace(format) ? string.Empty : format;
+ OverflowHandling = overflowHandling;
+ }
+
+ public static FixedFieldInfoAttribute FromCustomAttributeData(CustomAttributeData? customAttribute)
+ {
+ if (!TryGetFixedFieldInfoAttribute(customAttribute, out var attribute))
+ {
+ throw new InvalidOperationException($"Cannot write fixed field value without required '{nameof(FixedFieldInfoAttribute)}' annotation.");
+ }
+
+ return attribute;
+ }
+
+ public static bool TryGetFixedFieldInfoAttribute(CustomAttributeData? customAttribute, [NotNullWhen(true)] out FixedFieldInfoAttribute? fixedFieldInfoAttribute)
+ {
+ fixedFieldInfoAttribute = null;
+
+ if (customAttribute is null)
+ {
+ return false;
+ }
+
+ string format;
+
+ if (!TryGetNamedArgumentValue(customAttribute, 0, out int offset))
+ {
+ return false;
+ }
+
+ if (!TryGetNamedArgumentValue(customAttribute, 1, out int length))
+ {
+ return false;
+ }
+
+ if (!TryGetNamedArgumentValue(customAttribute, 2, out string? formatValue))
+ {
+ format = string.Empty;
+ }
+ format = formatValue ?? string.Empty;
+
+ if (!TryGetNamedArgumentValue(customAttribute, 3, out FieldOverflowHandling fieldOverflowHandling))
+ {
+ fieldOverflowHandling = FieldOverflowHandling.Throw;
+ }
+
+ fixedFieldInfoAttribute = new(offset, length, format, fieldOverflowHandling);
+ return true;
+
+ static bool TryGetNamedArgumentValue(CustomAttributeData customAttribute, int argumentIndex, [NotNullWhen(true)] out T? value)
+ {
+ value = (T?)customAttribute.ConstructorArguments[argumentIndex].Value;
+ return value is { };
+ }
+ }
+ }
+}
diff --git a/src/serde/FixedWidth/FixedWidthDeserializer.Deserialize.cs b/src/serde/FixedWidth/FixedWidthDeserializer.Deserialize.cs
new file mode 100644
index 00000000..d1738d78
--- /dev/null
+++ b/src/serde/FixedWidth/FixedWidthDeserializer.Deserialize.cs
@@ -0,0 +1,38 @@
+using Serde.FixedWidth.Reader;
+using System;
+using System.Buffers;
+
+namespace Serde.FixedWidth
+{
+ internal sealed partial class FixedWidthDeserializer : IDeserializer
+ {
+ ITypeDeserializer IDeserializer.ReadType(ISerdeInfo typeInfo)
+ {
+ if (typeInfo.Kind is not InfoKind.CustomType)
+ {
+ throw new ArgumentException("Invalid type for ReadType: " + typeInfo.Kind);
+ }
+
+ return this;
+ }
+
+ string IDeserializer.ReadString() => throw new NotImplementedException();
+ T? IDeserializer.ReadNullableRef(IDeserialize deserialize) where T : class => throw new NotImplementedException();
+ bool IDeserializer.ReadBool() => throw new NotImplementedException();
+ char IDeserializer.ReadChar() => throw new NotImplementedException();
+ byte IDeserializer.ReadU8() => throw new NotImplementedException();
+ ushort IDeserializer.ReadU16() => throw new NotImplementedException();
+ uint IDeserializer.ReadU32() => throw new NotImplementedException();
+ ulong IDeserializer.ReadU64() => throw new NotImplementedException();
+ sbyte IDeserializer.ReadI8() => throw new NotImplementedException();
+ short IDeserializer.ReadI16() => throw new NotImplementedException();
+ int IDeserializer.ReadI32() => throw new NotImplementedException();
+ long IDeserializer.ReadI64() => throw new NotImplementedException();
+ float IDeserializer.ReadF32() => throw new NotImplementedException();
+ double IDeserializer.ReadF64() => throw new NotImplementedException();
+ decimal IDeserializer.ReadDecimal() => throw new NotImplementedException();
+ DateTime IDeserializer.ReadDateTime() => throw new NotImplementedException();
+ void IDeserializer.ReadBytes(IBufferWriter writer) => throw new NotImplementedException();
+ void IDisposable.Dispose() => throw new NotImplementedException();
+ }
+}
diff --git a/src/serde/FixedWidth/FixedWidthDeserializer.Type.cs b/src/serde/FixedWidth/FixedWidthDeserializer.Type.cs
new file mode 100644
index 00000000..5cdd72a5
--- /dev/null
+++ b/src/serde/FixedWidth/FixedWidthDeserializer.Type.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Buffers;
+using System.Globalization;
+
+namespace Serde.FixedWidth
+{
+ internal sealed partial class FixedWidthDeserializer : ITypeDeserializer
+ {
+ private const NumberStyles Numeric = NumberStyles.Integer | NumberStyles.AllowThousands;
+
+ int? ITypeDeserializer.SizeOpt => null;
+
+ private int _fieldIndex = -1;
+
+ T ITypeDeserializer.ReadValue(ISerdeInfo info, int index, IDeserialize deserialize)
+ => deserialize.Deserialize(this);
+
+ string ITypeDeserializer.ReadString(ISerdeInfo info, int index) => _reader.ReadString(info, index);
+ bool ITypeDeserializer.ReadBool(ISerdeInfo info, int index) => _reader.ReadBool(info, index);
+ char ITypeDeserializer.ReadChar(ISerdeInfo info, int index) => _reader.ReadChar(info, index);
+ DateTime ITypeDeserializer.ReadDateTime(ISerdeInfo info, int index) => _reader.ReadDateTime(info, index);
+ decimal ITypeDeserializer.ReadDecimal(ISerdeInfo info, int index) => _reader.ReadNumber(info, index, NumberStyles.Currency | NumberStyles.AllowLeadingSign);
+ float ITypeDeserializer.ReadF32(ISerdeInfo info, int index) => _reader.ReadNumber(info, index, NumberStyles.Float);
+ double ITypeDeserializer.ReadF64(ISerdeInfo info, int index) => _reader.ReadNumber(info, index, NumberStyles.Float);
+ short ITypeDeserializer.ReadI16(ISerdeInfo info, int index) => _reader.ReadNumber(info, index, Numeric);
+ int ITypeDeserializer.ReadI32(ISerdeInfo info, int index) => _reader.ReadNumber(info, index, Numeric);
+ long ITypeDeserializer.ReadI64(ISerdeInfo info, int index) => _reader.ReadNumber(info, index, Numeric);
+ sbyte ITypeDeserializer.ReadI8(ISerdeInfo info, int index) => _reader.ReadNumber(info, index, Numeric);
+ ushort ITypeDeserializer.ReadU16(ISerdeInfo info, int index) => _reader.ReadNumber(info, index, Numeric);
+ uint ITypeDeserializer.ReadU32(ISerdeInfo info, int index) => _reader.ReadNumber(info, index, Numeric);
+ ulong ITypeDeserializer.ReadU64(ISerdeInfo info, int index) => _reader.ReadNumber(info, index, Numeric);
+ byte ITypeDeserializer.ReadU8(ISerdeInfo info, int index) => _reader.ReadNumber(info, index, Numeric);
+
+ void ITypeDeserializer.SkipValue(ISerdeInfo info, int index) { }
+
+ int ITypeDeserializer.TryReadIndex(ISerdeInfo info) => TryReadIndexWithName(info).Item1;
+
+ (int, string? errorName) ITypeDeserializer.TryReadIndexWithName(ISerdeInfo info) => TryReadIndexWithName(info);
+
+ private (int, string? errorName) TryReadIndexWithName(ISerdeInfo serdeInfo)
+ {
+ _fieldIndex++;
+ if (_fieldIndex == serdeInfo.FieldCount)
+ {
+ return (ITypeDeserializer.EndOfType, null);
+ }
+
+ return (_fieldIndex, null);
+ }
+
+ void ITypeDeserializer.ReadBytes(ISerdeInfo info, int index, IBufferWriter writer)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/serde/FixedWidth/FixedWidthDeserializer.cs b/src/serde/FixedWidth/FixedWidthDeserializer.cs
new file mode 100644
index 00000000..0452525d
--- /dev/null
+++ b/src/serde/FixedWidth/FixedWidthDeserializer.cs
@@ -0,0 +1,16 @@
+// Contains implementations of data interfaces for core types
+
+using Serde.FixedWidth.Reader;
+using Serde.IO;
+using System.Text;
+
+namespace Serde.FixedWidth
+{
+ ///
+ /// Defines a type which handles deserializing a fixed-width file.
+ ///
+ internal sealed partial class FixedWidthDeserializer(string document)
+ {
+ private readonly FixedWidthReader _reader = new(document);
+ }
+}
diff --git a/src/serde/FixedWidth/FixedWidthSerializer.Serialize.cs b/src/serde/FixedWidth/FixedWidthSerializer.Serialize.cs
new file mode 100644
index 00000000..33627a3a
--- /dev/null
+++ b/src/serde/FixedWidth/FixedWidthSerializer.Serialize.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Serde.FixedWidth
+{
+ public sealed partial class FixedWidthSerializer : ISerializer
+ {
+ ///
+ ITypeSerializer ISerializer.WriteType(ISerdeInfo info)
+ {
+ if (info.Kind is not InfoKind.CustomType)
+ {
+ throw new ArgumentException("Invalid type for WriteType: " + info.Kind);
+ }
+
+ return this;
+ }
+
+ void ISerializer.WriteString(string s) => writer.WriteRaw(s);
+ void ISerializer.WriteBool(bool b) => writer.WriteRaw(b);
+ void ISerializer.WriteBytes(ReadOnlyMemory bytes) => throw new NotImplementedException();
+ void ISerializer.WriteChar(char c) => writer.WriteRaw(c);
+ ITypeSerializer ISerializer.WriteCollection(ISerdeInfo info, int? count) => throw new NotImplementedException();
+ void ISerializer.WriteDateTime(DateTime dt) => writer.WriteRaw(dt);
+ void ISerializer.WriteDateTimeOffset(DateTimeOffset dto) => writer.WriteRaw(dto);
+ void ISerializer.WriteDecimal(decimal d) => writer.WriteRaw(d);
+ void ISerializer.WriteF32(float f) => writer.WriteRaw(f);
+ void ISerializer.WriteF64(double d) => writer.WriteRaw(d);
+ void ISerializer.WriteI16(short i16) => writer.WriteRaw(i16);
+ void ISerializer.WriteI32(int i32) => writer.WriteRaw(i32);
+ void ISerializer.WriteI64(long i64) => writer.WriteRaw(i64);
+ void ISerializer.WriteI8(sbyte b) => writer.WriteRaw(b);
+ void ISerializer.WriteNull() { }
+ void ISerializer.WriteU16(ushort u16) => writer.WriteRaw(u16);
+ void ISerializer.WriteU32(uint u32) => writer.WriteRaw(u32);
+ void ISerializer.WriteU64(ulong u64) => writer.WriteRaw(u64);
+ void ISerializer.WriteU8(byte b) => writer.WriteRaw(b);
+ }
+}
diff --git a/src/serde/FixedWidth/FixedWidthSerializer.Type.cs b/src/serde/FixedWidth/FixedWidthSerializer.Type.cs
new file mode 100644
index 00000000..e7d3d6e4
--- /dev/null
+++ b/src/serde/FixedWidth/FixedWidthSerializer.Type.cs
@@ -0,0 +1,33 @@
+using System;
+
+namespace Serde.FixedWidth
+{
+ public sealed partial class FixedWidthSerializer : ITypeSerializer
+ {
+ void ITypeSerializer.WriteValue(ISerdeInfo typeInfo, int index, T value, ISerialize serialize)
+ => serialize.Serialize(value, this);
+
+ void ITypeSerializer.End(ISerdeInfo info) => writer.WriteRaw();
+ void ITypeSerializer.WriteBool(ISerdeInfo typeInfo, int index, bool b)
+ {
+ writer.WriteObject(typeInfo, index, b);
+ }
+ void ITypeSerializer.WriteChar(ISerdeInfo typeInfo, int index, char c) => writer.WriteObject(typeInfo, index, c);
+ void ITypeSerializer.WriteU8(ISerdeInfo typeInfo, int index, byte b) => writer.WriteObject(typeInfo, index, b);
+ void ITypeSerializer.WriteU16(ISerdeInfo typeInfo, int index, ushort u16) => writer.WriteObject(typeInfo, index, u16);
+ void ITypeSerializer.WriteU32(ISerdeInfo typeInfo, int index, uint u32) => writer.WriteObject(typeInfo, index, u32);
+ void ITypeSerializer.WriteU64(ISerdeInfo typeInfo, int index, ulong u64) => writer.WriteObject(typeInfo, index, u64);
+ void ITypeSerializer.WriteI8(ISerdeInfo typeInfo, int index, sbyte b) => writer.WriteObject(typeInfo, index, b);
+ void ITypeSerializer.WriteI16(ISerdeInfo typeInfo, int index, short i16) => writer.WriteObject(typeInfo, index, i16);
+ void ITypeSerializer.WriteI32(ISerdeInfo typeInfo, int index, int i32) => writer.WriteObject(typeInfo, index, i32);
+ void ITypeSerializer.WriteI64(ISerdeInfo typeInfo, int index, long i64) => writer.WriteObject(typeInfo, index, i64);
+ void ITypeSerializer.WriteF32(ISerdeInfo typeInfo, int index, float f) => writer.WriteObject(typeInfo, index, f);
+ void ITypeSerializer.WriteF64(ISerdeInfo typeInfo, int index, double d) => writer.WriteObject(typeInfo, index, d);
+ void ITypeSerializer.WriteDecimal(ISerdeInfo typeInfo, int index, decimal d) => writer.WriteObject(typeInfo, index, d);
+ void ITypeSerializer.WriteString(ISerdeInfo typeInfo, int index, string s) => writer.WriteObject(typeInfo, index, s);
+ void ITypeSerializer.WriteNull(ISerdeInfo typeInfo, int index) => writer.WriteObject(typeInfo, index, string.Empty);
+ void ITypeSerializer.WriteDateTime(ISerdeInfo typeInfo, int index, DateTime dt) => writer.WriteObject(typeInfo, index, dt);
+ void ITypeSerializer.WriteDateTimeOffset(ISerdeInfo typeInfo, int index, DateTimeOffset dt) => writer.WriteObject(typeInfo, index, dt);
+ void ITypeSerializer.WriteBytes(ISerdeInfo typeInfo, int index, ReadOnlyMemory bytes) => writer.WriteObject(typeInfo, index, bytes);
+ }
+}
diff --git a/src/serde/FixedWidth/FixedWidthSerializer.cs b/src/serde/FixedWidth/FixedWidthSerializer.cs
new file mode 100644
index 00000000..c09fe93d
--- /dev/null
+++ b/src/serde/FixedWidth/FixedWidthSerializer.cs
@@ -0,0 +1,115 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Serde.FixedWidth.Writer;
+
+namespace Serde.FixedWidth
+{
+ ///
+ /// Defines a serializer for Fixed Width files.
+ ///
+ public sealed partial class FixedWidthSerializer(FixedWidthWriter writer)
+ {
+ ///
+ /// Serializes a single line of a fixed-width text file.
+ ///
+ /// The type of the .
+ /// The object instance to serialize.
+ /// A type which can serialize.
+ /// A fixed-width string.
+ public static string Serialize(T value, ISerialize serde)
+ {
+ using var writer = new FixedWidthWriter();
+ var serializer = new FixedWidthSerializer(writer);
+ serde.Serialize(value, serializer);
+ writer.Flush();
+ return writer.GetStringBuilder().ToString();
+ }
+
+ ///
+ ///
+ /// The serialize provider..
+ public static string Serialize(T value)
+ where TProvider : ISerializeProvider
+ => Serialize(value, TProvider.Instance);
+
+ ///
+ public static string Serialize(T value)
+ where T : ISerializeProvider
+ => Serialize(value, T.Instance);
+
+ ///
+ public static IEnumerable SerializeDocument(IEnumerable values)
+ where T : ISerializeProvider
+ => SerializeDocument(values, T.Instance);
+
+ ///
+ /// Serializes a collection of , returning an enumerable for writing to a stream.
+ ///
+ /// The type of the object to serialize.
+ /// A collection of the items to serialize.
+ /// The serialize provider.
+ /// An enumerable of the rows of the document.
+ public static IEnumerable SerializeDocument(IEnumerable values, ISerialize serde)
+ where T : ISerializeProvider
+ {
+ foreach (var provider in values)
+ {
+ yield return Serialize(provider, serde);
+ }
+ }
+
+ ///
+ /// Deserializes a single line of a fixed-width text file.
+ ///
+ /// The type of the value to return.
+ /// The line to deserialize.
+ /// A type which can deerialize.
+ /// A fixed-width string.
+ public static T Deserialize(string source, IDeserialize d)
+ {
+ T result;
+ var deserializer = new FixedWidthDeserializer(source);
+ result = d.Deserialize(deserializer);
+ return result;
+ }
+
+ ///
+ public static T Deserialize(string source)
+ where T : IDeserializeProvider
+ => Deserialize(source);
+
+ ///
+ ///
+ /// The deserialize provider to use.
+ public static T Deserialize(string source)
+ where TProvider : IDeserializeProvider
+ => Deserialize(source, TProvider.Instance);
+
+ ///
+ public static IEnumerable DeserializeDocument(string document, int headerLines = 0)
+ where T : IDeserializeProvider
+ => DeserializeDocument(document, T.Instance, headerLines);
+
+ ///
+ public static IEnumerable DeserializeDocument(string document, int headerLines = 0)
+ where TProvider : IDeserializeProvider
+ => DeserializeDocument(document, TProvider.Instance, headerLines);
+
+ ///
+ /// Deserializes the provided document.
+ ///
+ /// The type of the value to return.
+ /// The document to deserialize.
+ /// The deserializer.
+ /// The number of lines to skip.
+ /// An enumerable of deserialized rows.
+ public static IEnumerable DeserializeDocument(string document, IDeserialize d, int headerLines = 0)
+ {
+ foreach (var line in document.Split(Environment.NewLine).Skip(headerLines))
+ {
+ yield return Deserialize(line, d);
+ }
+ }
+ }
+}
diff --git a/src/serde/FixedWidth/Reader/FixedWidthReader.cs b/src/serde/FixedWidth/Reader/FixedWidthReader.cs
new file mode 100644
index 00000000..b6978c39
--- /dev/null
+++ b/src/serde/FixedWidth/Reader/FixedWidthReader.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Numerics;
+
+namespace Serde.FixedWidth.Reader
+{
+ internal readonly struct FixedWidthReader(string line)
+ {
+ private readonly string _line = line;
+
+ public string ReadString(ISerdeInfo typeInfo, int index)
+ => GetText(typeInfo, index, out _).Trim().ToString();
+
+ public bool ReadBool(ISerdeInfo typeInfo, int index)
+ {
+ var span = GetText(typeInfo, index, out var attribute);
+
+ if (string.IsNullOrEmpty(attribute.Format))
+ {
+ return bool.Parse(span);
+ }
+
+ string[] splitFormat = attribute.Format.Split('/', StringSplitOptions.TrimEntries);
+ if (splitFormat.Length != 2)
+ {
+ throw new InvalidOperationException("Split format must be an empty string or have true and false text separated by a forward slash ('/')");
+ }
+
+ if (span.Equals(splitFormat[0], StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ else if (span.Equals(splitFormat[1], StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ else
+ {
+ throw new InvalidOperationException($"Value '{span}' was neither '{splitFormat[0]}' nor '{splitFormat[1]}'.");
+ }
+ }
+
+ public char ReadChar(ISerdeInfo typeInfo, int index)
+ {
+ var span = GetText(typeInfo, index, out _);
+
+ return span.Length == 1 ? span[0] : throw new InvalidOperationException("Char field comprised of multiple non-space characters.");
+ }
+
+ public DateTime ReadDateTime(ISerdeInfo typeInfo, int index)
+ {
+ var span = GetText(typeInfo, index, out var attribute);
+
+ return string.IsNullOrEmpty(attribute.Format)
+ ? DateTime.Parse(span)
+ : DateTime.ParseExact(span, attribute.Format, CultureInfo.CurrentCulture);
+ }
+
+ public TNumber ReadNumber(ISerdeInfo typeInfo, int index, NumberStyles numberStyles)
+ where TNumber : struct, INumber
+ {
+ var span = GetText(typeInfo, index, out _);
+
+ var trimmedValue = span.Trim();
+
+ if (trimmedValue.IsEmpty)
+ {
+ return TNumber.Zero;
+ }
+
+ return TNumber.Parse(trimmedValue, numberStyles, CultureInfo.InvariantCulture);
+ }
+
+ private ReadOnlySpan GetText(ISerdeInfo typeInfo, int index, out FixedFieldInfoAttribute attribute)
+ {
+ var customAttribute = typeInfo.GetFieldAttributes(index).FirstOrDefault(it => it.AttributeType == typeof(FixedFieldInfoAttribute));
+ attribute = FixedFieldInfoAttribute.FromCustomAttributeData(customAttribute);
+
+ return _line.AsSpan(attribute.Offset, attribute.Length).Trim();
+ }
+ }
+}
diff --git a/src/serde/FixedWidth/Writer/FixedWidthWriter.cs b/src/serde/FixedWidth/Writer/FixedWidthWriter.cs
new file mode 100644
index 00000000..4243f82a
--- /dev/null
+++ b/src/serde/FixedWidth/Writer/FixedWidthWriter.cs
@@ -0,0 +1,123 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace Serde.FixedWidth.Writer
+{
+ public sealed class FixedWidthWriter : IDisposable
+ {
+ private const char Padding = ' ';
+ private readonly StringWriter _writer = new();
+ private int _pos = 0;
+
+ public void Flush()
+ {
+ _writer.Flush();
+ }
+
+ public void Dispose()
+ {
+ Flush();
+ }
+
+ ///
+ public StringBuilder GetStringBuilder() => _writer.GetStringBuilder();
+
+ public void WriteObject(ISerdeInfo typeInfo, int index, T value)
+ {
+ var customAttribute = typeInfo.GetFieldAttributes(index).FirstOrDefault(it => it.AttributeType == typeof(FixedFieldInfoAttribute));
+ var attribute = FixedFieldInfoAttribute.FromCustomAttributeData(customAttribute);
+
+ WriteObject(value, attribute.Offset, attribute.Length, attribute.Format, attribute.OverflowHandling);
+ }
+
+ private void WriteObject(T value, int fieldOffset, int fieldLength, string format, FieldOverflowHandling overflowHandling)
+ {
+ // attribute ctor sets white space strings to empty.
+ if (!string.IsNullOrEmpty(format))
+ {
+ if (value is bool b)
+ {
+ WriteBool(b, fieldOffset, fieldLength, format, overflowHandling);
+ return;
+ }
+
+ if (value is DateTime dt)
+ {
+ WriteText(dt.ToString(format, CultureInfo.CurrentCulture), fieldOffset, fieldLength, overflowHandling);
+ return;
+ }
+
+ if (value is IFormattable formattable)
+ {
+ WriteText(formattable.ToString(format, CultureInfo.CurrentCulture), fieldOffset, fieldLength, overflowHandling);
+ return;
+ }
+ }
+
+ if (value is ReadOnlyMemory byteMemory)
+ {
+ WriteUtf8Span(byteMemory.Span, fieldOffset, fieldLength, overflowHandling);
+ return;
+ }
+
+ // Format string is either not used, null, or it's not a special case.
+ WriteText(value?.ToString() ?? string.Empty, fieldOffset, fieldLength, overflowHandling);
+ }
+
+ private void WriteUtf8Span(Utf8Span span, int fieldOffset, int fieldLength, FieldOverflowHandling overflowHandling)
+ {
+ string text = Encoding.UTF8.GetString(span);
+ WriteText(text, fieldOffset, fieldLength, overflowHandling);
+ }
+
+ private void WriteBool(bool value, int fieldOffset, int fieldLength, string format, FieldOverflowHandling overflowHandling)
+ {
+ string[] splitFormat = format.Split('/', StringSplitOptions.TrimEntries);
+ if (splitFormat.Length != 2)
+ {
+ throw new InvalidOperationException("Split format must be an empty string or have true and false text separated by a forward slash ('/')");
+ }
+
+ WriteText(value ? splitFormat[0] : splitFormat[1], fieldOffset, fieldLength, overflowHandling);
+ }
+
+ private void WriteText(string value, int fieldOffset, int fieldLength, FieldOverflowHandling overflowHandling)
+ {
+ if (_pos < fieldOffset)
+ {
+ // Fill in any missing space with padding.
+ _writer.Write(new string(Padding, fieldOffset - _pos));
+ _pos = fieldOffset;
+ }
+
+ if (value.Length > fieldLength)
+ {
+ value = overflowHandling switch
+ {
+ FieldOverflowHandling.Throw => throw new InvalidOperationException($"Cannot write {value} (length {value.Length}) to a field that is only {fieldLength} long."),
+ FieldOverflowHandling.Truncate => value[..fieldLength],
+ _ => throw new ArgumentOutOfRangeException(nameof(overflowHandling), $"{overflowHandling} is not a valid value for {nameof(FieldOverflowHandling)}.")
+ };
+ }
+
+ _writer.Write(value.PadRight(fieldLength));
+ _pos += fieldLength;
+ }
+
+ internal void WriteRaw()
+ => _writer.WriteLine();
+
+ internal void WriteRaw(string value)
+ => _writer.WriteLine(value);
+
+ internal void WriteRaw(T value)
+ where T : notnull
+ => _writer.WriteLine(value.ToString());
+
+ public override string ToString()
+ => GetStringBuilder().ToString();
+ }
+}