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]);
}
WriteLine();
}
private void WriteField(string datum, int width)
{
char[] characters = datum.ToCharArray();
if (characters.Length > width)
{
Write(characters, 0, width);
}
else
{
Write(characters);
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])
base.WriteLine()
member private this.WriteField(datum:string, width) =
let xs = datum.ToCharArray()
if xs.Length > width
then base.Write(xs, 0, width)
else
base.Write(xs)
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)
else
stream.Write(xs)
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]
stream.WriteLine()
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?