Home =>  Articles =>  Poetry Magnets: Second Verse

Poetry Magnets: Second Verse
Charlie Poole
November, 2005

Where Were We?

I've been on vacation, so I didn't follow up on the first part of this series, as quickly as I intended. So let's recap...

We're trying to solve this problem...

  • Magnets with words on them are placed on a surface. The magnets are rectangular in shape and are all the same height. Their length varies depending on their content. They may be placed in any location and oriented horizontally, vertically or at some other angle. Due to physical limitations, they may not overlap.
  • The program should take a list of magnets, with the location, orientation and content of each one, and produce a text string that concatenates them in the order a human would read them. The text should be broken into lines if the layout "looks like" a set of lines.

I deliberately chose a rather naive approach: the magnets were initially represented as strings, without any additional info as to their position. We were able to get started but ran into a problem, as you might expect.

We want both of the following tests to pass...

[TestFixture]
public class MagnetTests
{
    ...

	[Test]
    public void TwoMagnets()
    {
        magnets.Add( "Hello" );
        magnets.Add( "World" );
        Assert.AreEqual( "Hello World", magnets.Text );
    }

    [Test]
    public void TwoMagnetsInWrongOrder()
    {
        magnets.Add( "World" );
        magnets.Add( "Hello" );
        Assert.AreEqual( "Hello World", magnets.Text );
    }
}

...but one of them must obviously fail. We're only giving the MagneticSurface two pieces of info about each magnet: the text and the order in which we place them on the surface. The problem statement implies that each magnet should have a position, so we need to add more info.

We like to refactor on a green bar, so we first comment out (or Ignore) the failing test, TwoMagnetsInWrongOrder(). All tests pass. Now, we need to add a Magnet object, while keeping the tests passing.

Adding the Magnet Object

To keep things simple - and because this is after all a demonstration of programming in small steps - we'll first create a magnet object that has nothing more than a text field. We rewrite all our tests to have code similar to this...

[TestFixture]
public class MagnetTests
{
    ...
    [Test]
    public void TwoMagnets()
    {
        magnets.Add( new Magnet("Hello") );
        magnets.Add( new Magnet("World") );
        Assert.AreEqual( "Hello World", magnets.Text );
    }
    ...
}

To make it compile, we need a magnet class...

public class Magnet
{
    public string Text;
	
    public Magnet( string text )
    {
        this.Text = text;
    }
}

...and a small change to the MagneticSurface class...

public class MagneticSurface
{
    private string text = "";

    public string Text
    {
        return text;
    }

    public void Add( Magnet magnet )
    {
        if ( this.text != string.Empty )
            this.text += " ";
        this.text += magnet.Text;
    }
}

All tests continue to pass. Now, continuing to refactor, we'll add a horizontal position to the magnet. We could also add a vertical coordinate and it wouldn't do any harm. But we don't need it now and I'm making a point here: it's possible to code only what you need for each test without doing an excessive amount of rework. Here's the new Magnet class...

public class Magnet
{
    public int X;
    public string Text;
	
    public Magnet( int x, string text )
    {
        this.X = x;
        this.Text = text;
    }
}

...and here's how the tests generally look, using the new constructor...

[TestFixture]
public class MagnetTests
{
    ...
    [Test]
    public void TwoMagnets()
    {
        magnets.Add( new Magnet(10, "Hello") );
        magnets.Add( new Magnet(20, "World") );
        Assert.AreEqual( "Hello World", magnets.Text );
    }
    ...
}

You'll notice that we aren't actually using the positions provided in the constructor yet, but we've assigned them ascending numbers so that the tests still pass - and they all do.

Ordering the Magnets

It's now time to look again at the failing test that led us to refactor. As modified to use our new magnet object, it looks like this...

[TestFixture]
public class MagnetTests
{
    ...

    [Test]
    public void TwoMagnetsInWrongOrder()
    {
        magnets.Add( new Magnet(20, "World") );
        magnets.Add( new Magnet(10, "Hello") );
        Assert.AreEqual( "Hello World", magnets.Text );
    }
}

It still fails, but now we have enough info to make it pass. We'll make one final change to our code that allows it to pass more easily.

Up to now, we've been building the text string as magnets are added. We could continue to do that, but it seems simpler to just save the magnets somewhere and sort them appropriately when we're asked for the text. We modify MagneticSurface as follows...

public class MagnetSpace
{
    private List<Magnet> magnets = new List<Magnet>();

    public string Text
    {
        get 
        {
            magnets.Sort();
            StringBuilder sb = new StringBuilder();

            foreach(Magnet mag in magnets)
            {
                if ( sb.Length > 0 )
                    sb.Append( ' ' );
                sb.Append( mag.Text );
            }

            return sb.ToString();
        }
    }

    public void Add(Magnet magnet)
    {
        magnets.Add(magnet);
    }
}

Now both of the tests that use two magnets fail with an InvalidOperationException. In order to sort a List, the objects contained in it need to implement the IComparable interface. In the case of Magnet, that's easy enough...

public class Magnet : IComparable
{
    public int X;
    public string Text;

    public Magnet(int x, string text)
    {
        this.X = x;
        this.Text = text;
    }

    int IComparable.CompareTo(object other)
    {
        Magnet m = (Magnet)other;
        return this.X.CompareTo(m.X);
    }
}

We're taking advantage of some of the constraints we established in the first part of the series. Since magnets may not overlap, we can assume that a magnet with a greater X coordinate comes after one with a lesser coordinate. We're also assuming that the magnets all lie in one line, a restriction we'll remove next.

Adding a Dimension

Here's the next test we want to make pass...

[TestFixture]
public class MagnetTests
{
    ...
    [Test]
    public void TwoLinesOfMagnets()
    {
        magnets.Add( new Magnet( 10, 7, "Hello" ) );
        magnets.Add( new Magnet( 20, 10, "I" ) );
        magnets.Add( new Magnet( 25, 10, "Am" ) );
        magnets.Add( new Magnet( 15, 10, "Here" ) );
        magnets.Add( new Magnet( 20, 7, "World" ) );
        Assert.AreEqual( "Hello World\nHere I Am", magnets.Text );
    }
}

Notice that we've kept the Y coordinates for the magnets on each line the same. This is in keeping with our constraint that magnets in a line match up perfectly with one another - for now anyway. In order to make this compile, we again change the Magnet constructor...

public class Magnet : IComparable
{
    public int X;
    public int Y;
    public string Text;

    public Magnet(int x, int y, string text)
    {
        this.X = x;
        this.Y = y;
        this.Text = text;
    }
    ...
}

We modify all our other tests to pass in a constant Y coordinate to the constructor and are able to compile. The latest test fails, since we are not using the Y coordinate in our sort. On my system, the text becomes...

"Hello Here World I Am"

Let's try simply changing the code of the CompareTo() method...

public class Magnet : IComparable
{
    ...
    int IComparable.CompareTo(object other)
    {
        Magnet m = (Magnet)other;
		
		int result = this.Y.CompareTo(m.Y);
		if ( result != 0 )
		    return result;
			
        return this.X.CompareTo(m.X);
    }
}

This still fails, but we're closer. The text is now...

"Hello World Here I Am"

...without the new line character. This is easy to fix in the code that actually creates the text string...

public class MagnetSpace
{
    private List<Magnet> magnets = new List<Magnet>();

    public string Text
    {
        get 
        {
            magnets.Sort();
            StringBuilder sb = new StringBuilder();
            int lastY = 0;

            foreach(Magnet mag in magnets)
            {
                if ( sb.Length > 0 )
                {
                    if ( lastY != mag.Y )
                        sb.Append( '\n' );
                    else
                        sb.Append( ' ' );
                }
                sb.Append( mag.Text );
                lastY = mag.Y;
            }

            return sb.ToString();
        }
    }

    public void Add(Magnet magnet)
    {
        magnets.Add(magnet);
    }
}

Once again, all our tests pass.

So What Have We Accomplished?

Well, we've solved some of the basic aspects of the problem. We still haven't dealt with lines of text that don't quite line up. And we still assume that all magnets are perfectly horizontal.

In fact, the constraints we have not yet removed are exactly those that I set initially in part one. In the next two parts this series, we'll finally relax each of them.

So far, we have changed our "design" at least four times.

  1. We went from a string to a Magnet object.
  2. We added an X coordinate to Magnet.
  3. We went from building the text as magnets were added to keeping a list of magnets and building the text when it was requested.
  4. We added a Y coordinate to Magnet.

We certainly could have planned some of this up front, but we suffered very little slowdown by starting simply and refactoring as needed. More important, we avoided building in "features" that we don't need.

Of course, you might say that we have only solved the easy parts up to now. So let's see what happens when we deal with "jagged" lines of magnets in the next article of the series.