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:
- 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.
- 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?
-
\$\begingroup\$ I considered posting there as well, but thought since the primary question was really "is there a better way to do this?" here would be more appropriate. \$\endgroup\$FlintZA– FlintZA2013年06月19日 08:03:46 +00:00Commented Jun 19, 2013 at 8:03
-
\$\begingroup\$ There is an example of implementation of Mika Kolari's suggestion here: stackoverflow.com/questions/26060441/… \$\endgroup\$Giuseppe– Giuseppe2014年09月29日 11:52:43 +00:00Commented Sep 29, 2014 at 11:52
-
\$\begingroup\$ An alternative approach is to use C# CSV Reader. More info: http://csvreader.readthedocs.io/en/latest/FixedWidthFileReader/ \$\endgroup\$Tohid– Tohid2016年10月31日 20:38:34 +00:00Commented Oct 31, 2016 at 20:38
1 Answer 1
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>()
-
\$\begingroup\$ +1 this is the way to go. In addition to explicit struct layout you could also look at fixed size buffers. \$\endgroup\$MattDavey– MattDavey2013年06月26日 08:22:44 +00:00Commented Jun 26, 2013 at 8:22
-
\$\begingroup\$ That is much more readable, good suggestion, thanks. \$\endgroup\$FlintZA– FlintZA2013年06月27日 07:59:17 +00:00Commented 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– Terry2014年09月19日 20:26:25 +00:00Commented 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– FlintZA2014年09月24日 12:03:50 +00:00Commented 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– Terry2014年10月01日 19:05:45 +00:00Commented Oct 1, 2014 at 19:05