Phillip Trelford's Array

POKE 36879,255

Fixed Width Data Files

Scanning over Alvin Ashcraft’s excellent Morning Dew (a must read for .Net devs as is Chris Alcock’s Morning Brew), I came across a short article from Richard Carr on writing Fixed Width Data Files using C#.

Richard creates a class that inherits from StreamWriter, and all-in it’s just over 50 lines of idiomatic C# code:

using System;
using System.IO;
using System.Text;

public class FixedWidthWriter : StreamWriter
    public int[] Widths { get; set; }

    public char NonDataCharacter { get; set; }

    public FixedWidthWriter(
          int[] widths, string path, bool append, Encoding encoding)
        : base(path, append, encoding)
        NonDataCharacter = ' ';
        Widths = widths;

    public FixedWidthWriter(int[] widths, string file)
        : this(widths, file, false, Encoding.UTF8) { }

    public FixedWidthWriter(int[] widths, string file, bool append)
        : this(widths, file, append, Encoding.UTF8) { }

    public void WriteLine(string[] data)
        if (data.Length > Widths.Length)
            throw new 
                InvalidOperationException("The data has too many elements.");

        for (int i = 0; i < data.Length; i++)
            WriteField(data[i], Widths[i]);

    private void WriteField(string datum, int width)
        char[] characters = datum.ToCharArray();
        if (characters.Length > width)
            Write(characters, 0, width);
            Write(new string(NonDataCharacter, width - characters.Length));

As you’d probably guess the same functionality takes half the lines to express in F#:

open System
open System.IO
open System.Text

type FixedWidthWriter(widths:int[], path:string, append, encoding) =
    inherit StreamWriter(path, append, encoding)
    member val Widths = widths with get, set
    member val NonDataCharacter = ' '  with get, set
    new(widths, file) = 
        new FixedWidthWriter(widths, file, false, Encoding.UTF8)
    new(widths, file, append) =
        new FixedWidthWriter(widths, file, append, Encoding.UTF8)
    member this.WriteLine(data:string[]) =
        if data.Length > this.Widths.Length
        then invalidOp "The data has too many elements."
        for i = 0 to data.Length-1 do      
            this.WriteField(data.[i], this.Widths.[i])
    member private this.WriteField(datum:string, width) =
        let xs = datum.ToCharArray()
        if xs.Length > width
        then base.Write(xs, 0, width)
            base.Write(String(this.NonDataCharacter, width - xs.Length))

The C# implementation uses classes and inheritance. In F# we’d typically favour composition over inheritance and start with the simplest possible thing that works, which in this case is a function that takes a file path, widths and the lines to write:

let WriteFixedWidth (path:string) (widths:int[]) (lines:string[] seq) =
    let pad = ' '
    use stream = new StreamWriter(path)
    let WriteField (datum:string) width =
        let xs = datum.ToCharArray()
        if xs.Length > width
        then stream.Write(xs, 0, width)
            stream.Write(String(pad, width - xs.Length))
    for line in lines do
        for i = 0 to widths.Length-1 do 
            WriteField line.[i] widths.[i]

The function is about half the size again and more closely follows YAGNI. To test it we can simply write:

let widths = [|20; 7; 6|]
[|"Company"; "Spend"; "Rating"|]
[|"Quality Foods Limited"; "1000000"; "Gold"|]
[|"The Pieman"; "50000"; "Silver"|]
[|"Bill's Fruit and Veg"; "10000"; "Bronze"|]
|> WriteFixedWidth "c:\\test.txt" widths

Does the language you chose make a difference?

Comments (2) -

  • Ehsan Irani

    5/14/2013 12:11:25 PM |

    Apparently F# is a cross between VB and C#.

Comments are closed