12
\$\begingroup\$

Refer to the help message and title of this post for what the code does.

I'm looking for tips on:

  • Efficiency: While the find-and-replace approach I have works perfectly fine and does so in a relatively quick manner, it feels far slower than it could be.
  • Idiomaticness: As always, I like writing code that abuses every feature it can.
  • Code readability: It's pretty freaking complicated. Even I barely understand what the heck is going on here:

    self.chunk(rows).map { |row| row.shift.zip(*row) }.map { |col| col.chunk(cols) }.flatten(1)
    

    Note that I'm not asking for an explanation. I wrote the code; I understand how it works. I'm asking for tips to improve readability.

  • Help message: Everything about this. I've never been particularly good at writing documentation for other people to use.
require 'pathname'

if ARGV == ['-h'] || ARGV == ['/?']
  file = Pathname.new(__FILE__).relative_path_from(Pathname.new(Dir.getwd))
  puts <<-END.gsub(/^\s*\|/m, '')
    | A simple fractal generator, written in Ruby.
    | 
    | Takes as input the number of rounds to go through, the starting 'map' and a set
    | of 'rules' which determine how to modify the input each round.
    | 
    | Outputs 
    | 
    | SYNTAX:
    |   ruby #{file} -h
    |   ruby #{file} <round count> <file with initial> <file with rules>
    |   <data_source_command> | ruby #{file} <round count> -g
    | 
    | Command-line flags:
    | -------------------
    | 
    | -h OR /?: Display this help message.
    | -g:       Retrieve the data from standard input, rather than the command line.
  END
  exit
end

pattern = []
rules = {}
count = Integer(ARGV.shift)

if ARGV.length == 1 && ARGV[0] == '-g'
  pattern << $_.chars until gets == ''
  until gets == ''
    from, to = $_.chomp.split('>').map { |s| s.split(',').map(&:chars) }
    rules[from] = to
  end
else
  pattern = IO.readlines(ARGV[0]).map(&:chomp).map(&:chars)
  rules = Hash[
      IO.readlines(ARGV[1]).map(&:chomp).reject { |line| line.empty? }.map do |line|
        line.split('>').map { |half| half.split(',').map(&:chars) }
      end
  ]
end

class Array
  def dimensions
    width = self[0].size
    jagged = self.any? { |row| row.size != width }
    [self.size, jagged ? nil : width]
  end

  def chunk(chunk_size)
    self.each_with_index.with_object(Array.new(size / chunk_size) { [] }) do |(elt, ind), result|
      result[ind / chunk_size] << elt
    end
  end

  def to_cells(cell_dims)
    rows, cols = cell_dims
    self.chunk(rows).map { |row| row.shift.zip(*row) }.map { |col| col.chunk(cols) }.flatten(1)
  end

  def from_cells(cells_per_row)
    self.chunk(self.size / cells_per_row).each_with_object([]) do |row_of_cells, result|
      result << row_of_cells.each_with_object(Array.new(row_of_cells[0].size) { [] }) do |cell, array|
        cell.each_with_index { |cell_row, index| array[index].concat(cell_row) }
      end
    end.flatten(1)
  end
end

rules.each do |from, to|
  from_size = from.dimensions
  to_size = to.dimensions
  raise ArgumentError, "#{from} is jagged" if from_size.any? { |side| side.nil? }
  raise ArgumentError, "#{to} is jagged"   if to_size.any? { |side| side.nil? }
  raise ArgumentError, "#{from} doesn't fit into #{to}" unless to_size.zip(from_size).all? { |(a,b)| a % b == 0 }
end
from_size = rules.keys[0].dimensions
to_size = rules.values[0].dimensions
rules.each do |from, to|
  raise ArgumentError, "Rule sources aren't of equal size" unless from.dimensions == from_size
  raise ArgumentError, "Rule results aren't of equal size" unless to.dimensions == to_size
end
raise ArgumentError, "Rule sources don't fit the input" unless pattern.dimensions.zip(from_size).all? { |(a,b)| a%b==0 }

(1..count).each do |round_num|
  cells_per_row = pattern.dimensions[1] / rules.keys[0].dimensions[1]
  pattern = pattern.to_cells(from_size).map { |cell| rules.fetch(cell, cell) }.from_cells(cells_per_row)
end

pattern.each { |row| puts row.join '' }

And some test data:

ruby build_fractal.rb 3 start.txt rules.txt

start.txt:

#

rules.txt:

#> # ,###, # 
 >   ,   ,   

outputs:

             #             
            ###            
             #             
          #  #  #          
         #########         
          #  #  #          
             #             
            ###            
             #             
    #        #        #    
   ###      ###      ###   
    #        #        #    
 #  #  #  #  #  #  #  #  # 
###########################
 #  #  #  #  #  #  #  #  # 
    #        #        #    
   ###      ###      ###   
    #        #        #    
             #             
            ###            
             #             
          #  #  #          
         #########         
          #  #  #          
             #             
            ###            
             #

Note that there is trailing whitespace.

\$\endgroup\$
  • 3
    \$\begingroup\$ ++ for "As always, I like writing code that abuses every feature it can." \$\endgroup\$ – Phrancis Aug 6 '15 at 12:50
  • 1
    \$\begingroup\$ @Phrancis Dang, I was hoping it'd be because fractals are freaking cool. Eh, a ++ is a ++ either way. Assuming they're both either prefixes or suffixes, not one of each. \$\endgroup\$ – Nic Hartley Aug 6 '15 at 13:26
  • \$\begingroup\$ Wow, this has to be the coolest thing I've seen all week. +++ \$\endgroup\$ – Ethan Bierlein Aug 6 '15 at 14:56
2
\$\begingroup\$

I think I found a bug:

pattern << $_.chars until gets == ''

The problem here is that gets cannot return an empty string, so this probably does not behave as you think it should.

Look at this REPL session:

irb(main):003:0> gets

=> "\n"

I think that just adding .chomp after gets should fix this.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.