3
\$\begingroup\$

I'm working on a communications layer for a system that reads data from a TCPIP client that is formatted as fixed-width ASCII (yeah, old school). I was quite surprised that there seemed to be no built in way to do this, and ended up using the following simple StreamReader subclass:

/// <summary>
/// A Stream reader that reads values as fixed width fields from a stream
/// </summary>
class FixedWidthFieldStreamReader : StreamReader
{
    #region Private/Protected fields
    private char[] buffer; // Local buffer used to copy data before conversion
    #endregion

    #region Methods
    /// <summary>
    /// Instantiates a new FixedWidthFieldStreamReader for a stream
    /// </summary>
    /// <param name="stream">Stream to read from</param>
    /// <param name="initialBufferSize">Initial size of the buffer used to copy data before formatting</param>
    /// <param name="encoding">Encoding to use when reading from the stream</param>
    public FixedWidthFieldStreamReader(Stream stream, int initialBufferSize, Encoding encoding)
        : base(stream, encoding)
    {
        buffer = new char[initialBufferSize];
    }

    /// <summary>
    /// Checks if the buffer exists and is large enough,
    /// and allocates or grows it if necessary.
    /// </summary>
    /// <param name="length">The required buffer length</param>
    private void EnsureBufferLength(int length)
    {
        if (null == buffer ||
            buffer.Length < length)
        {
            buffer = new char[length];
        }
    }

    /// <summary>
    /// Reads a number of bytes into the buffer
    /// </summary>
    /// <param name="length">The number of bytes to read</param>
    /// <returns>True if the required number of bytes was read, false otherwise</returns>
    private bool ReadToBuffer(int length)
    {
        EnsureBufferLength(length);

        // Read from the stream
        int read = Read(buffer, 0, length);
        return read == length;
    }

    /// <summary>
    /// Reads a specified number of bytes from the stream and 
    /// converts and returns the read value.
    /// </summary>
    /// <typeparam name="T">Type of the object to read and return</typeparam>
    /// <param name="length">Number of bytes in the field to read from the stream.</param>
    /// <returns>The read object if successful, or the default value for the type otherwise.</returns>
    public T Read<T>(int length) where T : IConvertible
    {
        if (ReadToBuffer(length))
        {
            return (T)Convert.ChangeType(new string(buffer, 0, length), typeof(T));
        }

        return default(T);
    }

    /// <summary>
    /// Skips a specified number of bytes in the stream
    /// </summary>
    /// <param name="length">The number of bytes to skip</param>
    public void Skip(int length)
    {
        // Ideally we should be able to just seek on the current stream, 
        //  but that seems to seek to an incorrect location?
        //this.BaseStream.Seek(length, SeekOrigin.Current);
        ReadToBuffer(length);
    }
    #endregion
}

This would be used something like this:

using (MemoryStream stream = new MemoryStream(buffer))
{
    stream.Seek((int)FieldOffsets.DATA, SeekOrigin.Begin);
    using (FixedWidthFieldStreamReader reader = new FixedWidthFieldStreamReader(stream, 15, Encoding.ASCII))
    {
        intVal = reader.Read<int>(3);
        stringVal = reader.Read<string>(15);
        floatVal= reader.Read<float>(5);
    }
}

I have two questions based on this:

  1. Am I just missing some completely obvious existing utility to do this? It really does seem like a common problem that would have been solved by the framework team ages ago.
  2. Aside from the obvious optimization of having some type specific versions of Read that don't do the conversion, are there any suggestions to improve this approach?
\$\endgroup\$
3

1 Answer 1

4
\$\begingroup\$

I don't know if something does that already, but you could mimic StructLayout if you need to read multiple types of 'objects' or need reusability.

Basically just set starting point/offset and length for each property and read them.

public class Item
{
     // 3 chars starting from 0
    [Layout(0, 3)]
    public int Number { get; set; }

     // 15 chars starting from 3
    [Layout(3, 15)]
    public string Text { get; set; } 

    // 5 chars starting from 18
    [Layout(18, 5)]
    public float Number2 { get; set; } 
}

reader.Read<Item>()
\$\endgroup\$
6
  • \$\begingroup\$ +1 this is the way to go. In addition to explicit struct layout you could also look at fixed size buffers. \$\endgroup\$
    – MattDavey
    Jun 26, 2013 at 8:22
  • \$\begingroup\$ That is much more readable, good suggestion, thanks. \$\endgroup\$
    – FlintZA
    Jun 27, 2013 at 7:59
  • \$\begingroup\$ @FlintZA, just curious, feel like updating your post to you final version of the class using attributes? \$\endgroup\$
    – Terry
    Sep 19, 2014 at 20:26
  • \$\begingroup\$ @Terry unfortunately I haven't been able to justify refactoring with the attribute version. The original is working really well. It's on the todo list, but really, really low down below higher priority stuff :) \$\endgroup\$
    – FlintZA
    Sep 24, 2014 at 12:03
  • 1
    \$\begingroup\$ @FlintZA - I'm not sure what protocol here is, but given I just wrote up a slightly more generic version of the answer above (and posted by Giuseppe at SO) with Generics, LayoutAttribute, Compiled Expression Trees and Convert.ChangeType, I didn't post a new answer or code here. You can, however, read my blog post on this and see the sample code at terryaney.wordpress.com/2014/10/01/…. Should I have posted new answer/code here or? Just following hanselman.com/blog/YourWordsAreWasted.aspx :). \$\endgroup\$
    – Terry
    Oct 1, 2014 at 19:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.