5.4. Handling Other Lines
Alice: So now we enter a loop, I presume. What does loop mean?
Bob: It stands for an endless loop. In C you would write while(1)
or for{;;} depending on your taste, but Ruby has a more clean and
direct construct. The only way to leave the loop loop is to break
out explicitly: you break with break.
Alice: Ruby seems to do exactly what you tell it to do! The case
statement reminds me of the switch statement in C and C++.
Bob: Yes. In this case, case is used to compare the first word of
each new line with the three legal choices we have so far for the names
of input and output data: mass, position and velocity. When one of these
choices is encountered, the appropriate helper method kicks in, a simple
translator to floating point for the mass, and a translator to a
floating point vector for position and velocity. Here is the first one:
def s_to_f(s) # string to floating-point number
s.split("=")[1].to_f
end
This time we give split an explicit argument. The default has been
to consider white space as the separator between words, but now we use
an equal sign = as a separator. The name of the variable, in
our case the string "mass" is returned as the value of
s.split("=")[0]. Everything to the right of the equal sign,
in our case the value of the mass, is returned as s.split("=")[0],
and promptly converted to floating point format with .to_f.
Alice: Everything, unless there are more equal signs, in which case the
following pieces would wind up in s.split("=")[2], etc.
Bob: True. Normally there should be one and exactly one equal sign,
and I could have checked that by checking and raising an error condition
in case s.split("=").size != 2. However, at this stage I'm happy
to live a bit dangerously. And besides, the extra stuff will be ignored,
since it is not used.
Alice: Even so, I wouldn't want to continue if there is was an second
equal sing in the input line, since that would mean that there would be
something very seriously wrong. Let's get back to error handling soon;
we really should teach the students some defensive programming, instead
of just assuming that the world is a paradise, and that nobody will hand
you wrong data.
Bob: I agree. But first things first: let's get things to work. Here
is my vector version:
def s_to_f_v(s) # string to floating-point vector
s.split("=")[1].split.map{|x| x.to_f}.to_v
end
As before, it uses the map method to convert each component of the vector
from a string to a floating point number.
Alice: And when you encounter a new begin before you have reached the
end of the current particle data input, you interpret that as a new
particle that is contained in the current particle, in the same way that
a star is contained in a star cluster.
Bob: Indeed. Such a new particle should normally be indented one level
further, since it lies one level deeper in the hierarchy of particles, but
I don't check for that, since I don't want to insist on a particular style
of indenting. Style is nice for human readers, but computers should
be able to handle anything logically reasonable.
Alice: And subread is the method that handles those particles deeper in
the hierarchy, under the current particle. What does it do?
Bob: I don't know yet. I just put it there as a stub, a place holder to
remind us that we still had some work to do there. Hmm. Now that you ask
me, I guess it should just be read again, a recursive invocation of the
same method that we are currently executing.
Alice: Yes, but you first have to know where to put the date you read
for the daughter particle. I think you will have to create such a
particle, to start with, and then hand execution to that particle.
Bob: That sounds right. Let's do that in a moment.
Alice: Well done! This is clean and elegant, but I'm still worrying about
handling unusual lines. If somebody hands you a line with
acceleration = 0.1 -0.3
your program will raise an error. But there is nothing wrong with
providing extra information. How about creating a little scratch pad
where you store all the lines for which you did not know what to do
with them. You can just keep this scratch pad lying around, and by
the time you do an output, you echo its contents so that the information
is passed on correctly to the next function, which may know what to do
with it.
Bob: That is a good idea too. Let's do that in a moment too, in another
moment.
5.5. Testing
Alice: Did you test your code?
Bob: Not yet, but it's high time. Let's try a write-read cycle.
Alice: You mean, you will output the content of one Body instance
with its write method and then read it in again into another Body
instance using that instances read function?
Bob: Conceptually, yes, but in practice it is more complicated.
Remember that we haven't yet written the higher level function that
handles the top level lines begin ACS and all that. That
function gobbles up not only those higher levels lines, but also the
header of the particle data. Let's see.
Alice: Ah, of course, you're right. This means that we will have
to pass the header by hand, for now, in the read command, after which
we can feed in the remaining lines from the standard input, or from a
file, if we wish.
Bob: Exactly. Let's check it, by writing another test
file test.rb:
require "iobody3.rb"
b = Body.new
b.read("begin particle star giant AGB")
b.write
And here is the result:
|gravity> ruby test.rb
mass = 1
position = 2 3
velocity = 4.5 6.7
end
begin particle star giant AGB
mass = 1.0000000000000000e+00
position = 2.0000000000000000e+00 3.0000000000000000e+00
velocity = 4.5000000000000000e+00 6.7000000000000002e+00
end
Alice: Just what it should be. After typing in those four lines,
with whatever indentation, b.write generates the right
output. Congratulations!
Bob: Let's see what happens if we give a wrong input line.
Alice: Okay. How about giving it a supersymmetric particle,
for a change?
require "iobody3.rb"
b = Body.new
b.read("begin sparticle")
b.write
And here is the result:
|gravity> ruby test.rb
./iobody3.rb:40:in `read': unhandled exception
from test.rb:4
Bob: It works, but I admit, this type of error message is less
than helpful.
Alice: A third thing to do in yet another moment. So here is our
to-do list:
-
implement a scratch pad
-
write the subread method
-
improve the error handling
Bob: And then of course we have to write the higher level read functions
to. Okay, one thing at a time!