Forum: Ruby Bracket Packing (#78)

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
James G. (Guest)
on 2006-05-11 18:03
(Received via mailing list)
As I'm sure you have noticed by now, this was a very popular quiz.  The
problem
gave two options for solutions and I'm glad to see that we had a fair
number of
both.  Let's examine a couple of checkers and one corrector.

First, we will have a look at a solution that just checks the packer
instructions.  This is an improved version of my own code, based on
changes I
learned from Ross B.'s solution:

	#!/usr/local/bin/ruby -w

	require "strscan"

	stack = Array.new
	input = StringScanner.new(ARGF.read)

	until input.eos?
	  if input.scan(/[\[({]/)
	    stack.push(input.matched)
	  elsif input.scan(/[\])}]/)
	    exit(1) unless "#{stack.pop}#{input.matched}" =~
/\A(?:\[\]|\(\)|\{\})\Z/
	  else
	    input.scan(/[^\[\](){}]+/)
	  end
	end
	exit(1) unless stack.empty?

	exit(1) if input.string =~ /\(\)|\[\]|\{\}/

	puts input.string

You can see that I start by pulling in the StringScanner library and
wrapping
the input in an instance of that class.  I also create a stack to help
me with
the checks we will talk about next.

The until loop may be the longest section of code, but it is a simple
process.
Here we just work through the input in chunks, which can be one of three
things.
If a chunk is an opening parenthesis, bracket, or brace, we push it onto
the
stack (in the if branch).  If we find a run of
non-parenthesis/bracket/brace
characters (the Bs in the quiz examples), we skip right over them (the
else
branch).  Finally, each time we meet a closing parenthesis, bracket, or
brace,
we pop the latest opener off of the stack and make sure we have a
matched pair
(the elsif branch).  When we have run through all of the characters, the
stack
should be empty?() if we properly matched all of the packaging material.
(Otherwise, we have an opener that was never closed.)

There's still one other condition our stack check didn't catch.  With an
input
like [{(B)()}], we have an empty set of packaging that doesn't make
sense.  I
use one last Regexp to spot these cases.

If my expectations are not satisfied at any point, there is no need to
finish
the checks and the program just exit()s with a code of one indicating
the error.
If we make it all the way to the end, it means I am satisfied and the
program
will naturally exit() with the zero all-clear code, after printing the
packing
sequence.

The minus of this stack trick is that it didn't cover the edge cases
well, so I
had to add more checks to handle them.  Some found a better process to
get
around this, which was to "unwrap" each layer of packing.  Here's a nice
example
from Christian N.:

	def unwrap(desc)
	  [desc.gsub!('BB',  'B'), desc.gsub!('(B)', 'B'),
	   desc.gsub!('[B]', 'B'), desc.gsub!('{B}', 'B')].nitems > 0
	end

	def valid?(desc)
	  desc = desc.dup
	  true  while unwrap desc
	  desc == "B"
	end

	packet = ARGV.first.to_s
	if valid? packet
	  puts packet
	  exit 0
	else
	  exit 1
	end

Start with the bottom third here.  You can see the input packet is read
and then
checked by valid?().  Depending on the result, the if statement handles
the
printing and exit codes.

Now we can move up to valid?().  It does three things:  copy the input,
run
unwrap() on the input until it returns false, and check to see if we are
left
with a single B at the end.  That of course leads us to the magic
unwrap()er.

The unwrap() method tries to perform four global substitutions.  The
first just
combines and side by side Bs.  The other three remove any packing around
a (B),
[B], or {B} respectively (leaving just the B).  The results of all four
substitutions are gathered into an Array and the non-nil items are
counted.
(gsub!() returns nil if it didn't change anything.)  If that count is
greater
than zero, we made a change and should loop again to check for more, so
true is
returned.  When all four substitutions are nil, we have unwrap()ed the
package
as far as possible and can stop.

I thought that solution came out cleaner, with less special cases.

Now what if you wanted to go the distance and do the corrections too?
Here's
the beginning of one possible solution by Kerry B.:

	Brackets = {'(' => ')', '[' => ']', '{' => '}'}

	# Adds missing close brackets (aborts on error unless @fix).
	def fix_closings(str)
	  closers = []
	  fixed = ""
	  str.split(//).each do |c|
	    if Brackets.has_key?(c)
	      # Add expected corresponding bracket to a stack
	      closers.push(Brackets[c])
	    elsif Brackets.has_value?(c)
	      closer = closers.pop
	      if closer
	        # Append any missing closing brackets
	        while closer != c
	          abort unless @fix
	          fixed << closer
	          closer = closers.pop
	        end
	      else
	        abort unless @fix
	      end
	    end
	    fixed << c
	  end
	  # If we've hit the end of the description, make sure any leftover
	  # closing brackets are added
	  closers.reverse.each {|a| fixed << a}
	  fixed
	end

	# ...

This method works similar to the stack solution we examined earlier.
Here the
closers variable holds the stack, which gets added to anytime the code
runs into
an opener.  Then, when it is time to close a pair (because we have
reached a
closer or the end of the input), the stack is popped until we find the
correct
closer or empty the stack.

Now this will complete any opened sets, whether the input had them right
or not.
See if a closer is reached in the input that doesn't match the latest
opening,
popping the stack until we get to it closes all outstanding pairs up to
that
point.

What this doesn't yet handle is pairs that were never opened.  For that,
we need
another trick:

	# ...

	# Reverses the description, mirroring brackets (eg "{foo]" => "[oof}").
	def reverse_desc(str)
	  new_str = ""
	  str.split(//).each do |c|
	    if Brackets.has_key?(c)
	      new_str << Brackets[c]
	    elsif Brackets.has_value?(c)
	      new_str << Brackets.invert[c]
	    else
	      new_str << c
	    end
	  end
	  new_str.reverse
	end

	# ...

This just reverses a string, with a catch:  all parenthesis, brackets,
and
braces are flopped.  If it was an opener it becomes a closer, and visa
versa.

Now if you fix the input once, reverse and fix again, then reverse back
to
normal, you end up with a totally corrected input.  Here's the
application code
that triggers that process:

	# ...

	@fix = ARGV.shift == "-f"
	desc = gets.chomp
	# Add missing closing brackets, flip the description and repeat, then
flip
	# it back the right way round again
	fixed = reverse_desc(fix_closings(reverse_desc(fix_closings(desc))))
	puts fixed

Very clever Kerry.  Thanks for sharing.

A big thank you to all who played with this quiz.  You all taught me
clever
approaches this week.

Next week, Ross is in charge and he's got a great problem for you.
After that,
there will be one week off, then I'll return with more material.  See
you then!
This topic is locked and can not be replied to.