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.