Table of Contents

Addax - Extensibility

Fields

A complete example of a custom value converter that handles System.DateTime values represented as Unix timestamps:

internal class UnixEpochDateTimeConverter : TabularConverter<DateTime>
{
    public override bool TryFormat(DateTime value, Span<char> destination, IFormatProvider? provider, out int charsWritten)
    {
        var seconds = (long)(value.ToUniversalTime() - DateTime.UnixEpoch).TotalSeconds;

        return seconds.TryFormat(destination, out charsWritten, "g", provider);
    }

    public override bool TryParse(ReadOnlySpan<char> source, IFormatProvider? provider, out DateTime value)
    {
        if (long.TryParse(source, NumberStyles.Integer, provider, out var seconds))
        {
            value = DateTime.UnixEpoch.AddSeconds(seconds);

            return true;
        }
        else
        {
            value = default;

            return false;
        }
    }
}

var dialect = new TabularDialect("\r\n", ',', '\"');

using (var writer = new TabularWriter<Book>(File.Create("books.csv"), dialect))
{
    var book1 = new Book
    {
        Author = "Lewis Carroll",
        Title = "Alice's Adventures in Wonderland",
        Published = new(1865, 11, 09, 0, 0, 0, DateTimeKind.Utc)
    };

    writer.WriteRecord(book1);

    var book2 = new Book
    {
        Author = "H. G. Wells",
        Title = "The Time Machine",
        Published = new(1894, 03, 17, 0, 0, 0, DateTimeKind.Utc)
    };

    writer.WriteRecord(book2);
}

using (var reader = new TabularReader<Book>(File.OpenRead("books.csv"), dialect))
{
    while (reader.TryReadRecord())
    {
        var book = reader.CurrentRecord;

        Console.WriteLine($"{book.Author} '{book.Title}' ({book.Published})");
    }
}

[TabularRecord]
internal struct Book
{
    [TabularFieldOrder(0)]
    public string? Author;

    [TabularFieldOrder(1)]
    public string? Title;

    [TabularConverter<UnixEpochDateTimeConverter>]
    [TabularFieldOrder(2)]
    public DateTime? Published;
}

Records

A simple example of a custom record handler that interprets a record as a tuple with point coordinates:

internal class PointHandler : TabularHandler<(double, double)>
{
    public override TabularRecord<(double, double)> Read(TabularReader reader)
    {
        reader.TryReadField();
        reader.TryGetDouble(out var item0);
        reader.TryReadField();
        reader.TryGetDouble(out var item1);

        return new((item0, item1));
    }

    public override void Write(TabularWriter writer, (double, double) record)
    {
        writer.WriteDouble(record.Item1);
        writer.WriteDouble(record.Item2);
    }
}

The primary approach is to specify the record handler for reader or writer explicitly:

var handler = new PointHandler();
var dialect = new TabularDialect("\r\n", ',', '\"');

using (var writer = new TabularWriter<(double, double)>(File.Create("points.csv"), dialect, handler: handler))
{
    writer.WriteRecord((50.4501, 30.5234));
    writer.WriteRecord((45.4215, 75.6972));
}

using (var reader = new TabularReader<(double, double)>(File.OpenRead("points.csv"), dialect, handler: handler))
{
    while (reader.TryReadRecord())
    {
        var (lat, lon) = reader.CurrentRecord;

        Console.WriteLine($"{lat} N, {lon} W");
    }
}

Additionally, it can be added to the TabularRegistry.Handlers collection with generated record handlers:

TabularRegistry.Handlers[typeof((double, double))] = new PointHandler();

var dialect = new TabularDialect("\r\n", ',', '\"');

using (var writer = new TabularWriter<(double, double)>(File.Create("points.csv"), dialect))
{
    writer.WriteRecord((50.4501, 30.5234));
    writer.WriteRecord((45.4215, 75.6972));
}

using (var reader = new TabularReader<(double, double)>(File.OpenRead("points.csv"), dialect))
{
    while (reader.TryReadRecord())
    {
        var (lat, lon) = reader.CurrentRecord;

        Console.WriteLine($"{lat} N, {lon} W");
    }
}