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.
- We went from a string to a Magnet object.
- We added an X coordinate to Magnet.
- We went from building the text as magnets were added to keeping a list
of magnets and building the text when it was requested.
- 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.
|