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
  • 3
    \$\begingroup\$ ++ for "As always, I like writing code that abuses every feature it can." \$\endgroup\$
    – Phrancis
    Commented Aug 6, 2015 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\$
    – anon
    Commented Aug 6, 2015 at 13:26
  • \$\begingroup\$ Wow, this has to be the coolest thing I've seen all week. +++ \$\endgroup\$ Commented Aug 6, 2015 at 14:56

1 Answer 1

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 and acknowledge you have read our privacy policy.