Решение на Трета задача от Георги Ангелов

Обратно към всички решения

Към профила на Георги Ангелов

Резултати

  • 6 точки от тестове
  • 3 бонус точки
  • 9 точки общо
  • 69 успешни тест(а)
  • 0 неуспешни тест(а)

Код

module Graphics
class Canvas
attr_reader :width, :height
def initialize(width, height)
@width = width
@height = height
@canvas = Hash.new false
end
def set_pixel(x, y)
@canvas[[y, x]] = true
end
def pixel_at?(x, y)
@canvas[[y, x]]
end
def draw(figure)
figure.draw_on(self)
end
def render_as(renderer)
renderer.render(self)
end
end
module Equality
module AttributeEquality
def hash
equality_objects.hash
end
def eql?(other_object)
equality_objects.eql? other_object.equality_objects
end
alias_method :==, :eql?
end
module AttributeComparable
include Comparable
def <=>(other_object)
equality_objects <=> other_object.equality_objects
end
end
def attribute_equality(*args, comparable: false)
define_method :equality_objects do
args.map { |attribute| public_send attribute }
end
include AttributeEquality
include AttributeComparable if comparable
end
end
module Renderers
module BasicRenderer
def render(canvas, pixels, line_break, template='%{canvas}')
rows = 0.upto(canvas.height - 1).map do |y|
row = 0.upto(canvas.width - 1).map do |x|
pixels[canvas.pixel_at?(x, y)]
end
row.join('')
end
template % {canvas: rows.join(line_break)}
end
end
class Ascii
extend BasicRenderer
PIXELS = {
true => '@'.freeze,
false => '-'.freeze,
}.freeze
LINE_BREAK = "\n".freeze
def self.render(canvas)
super canvas, PIXELS, LINE_BREAK
end
end
class Html
extend BasicRenderer
TEMPLATE = <<-TEMPLATE.freeze
<!DOCTYPE html>
<html>
<head>
<title>Rendered Canvas</title>
<style type="text/css">
.canvas {
font-size: 1px;
line-height: 1px;
}
.canvas * {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 5px;
}
.canvas i {
background-color: #eee;
}
.canvas b {
background-color: #333;
}
</style>
</head>
<body>
<div class="canvas">
%{canvas}
</div>
</body>
</html>
TEMPLATE
PIXELS = {
true => '<b></b>'.freeze,
false => '<i></i>'.freeze,
}.freeze
LINE_BREAK = '<br>'.freeze
def self.render(canvas)
super canvas, PIXELS, LINE_BREAK, TEMPLATE
end
end
end
class Point
extend Equality
attr_reader :x, :y
attribute_equality :x, :y, comparable: true
def initialize(x, y)
@x = x
@y = y
end
def draw_on(canvas)
canvas.set_pixel(x, y)
end
def +(other_point)
Point.new x + other_point.x, y + other_point.y
end
def -(other_point)
Point.new x - other_point.x, y - other_point.y
end
def /(divisor)
Point.new x / divisor, y / divisor
end
end
class Line
extend Equality
attr_reader :from, :to
attribute_equality :from, :to
def initialize(from, to)
from, to = to, from if from > to
@from = from
@to = to
end
def draw_on(canvas)
if from == to
canvas.set_pixel from.x, from.y
else
rasterize_on canvas
end
end
private
def rasterize_on(canvas)
step_count = [(to.x - from.x).abs, (to.y - from.y).abs].max
delta = (to - from) / step_count.to_r
current_point = from
step_count.succ.times do
canvas.set_pixel(current_point.x.round, current_point.y.round)
current_point = current_point + delta
end
end
end
class Rectangle
extend Equality
attr_reader :left, :right
attr_reader :top_left, :top_right, :bottom_left, :bottom_right
attribute_equality :top_left, :bottom_right
def initialize(from, to)
from, to = to, from if from > to
@left = from
@right = to
determine_corners
end
def draw_on(canvas)
sides.each { |line| canvas.draw(line) }
end
private
def determine_corners
y_coordinates = [left.y, right.y]
@top_left = Point.new left.x, y_coordinates.min
@top_right = Point.new right.x, y_coordinates.min
@bottom_left = Point.new left.x, y_coordinates.max
@bottom_right = Point.new right.x, y_coordinates.max
end
def sides
[
Line.new(top_left, top_right ),
Line.new(top_right, bottom_right),
Line.new(bottom_left, bottom_right),
Line.new(top_left, bottom_left ),
]
end
end
end

Лог от изпълнението

.....................................................................

Finished in 0.08913 seconds
69 examples, 0 failures

История (3 версии и 10 коментара)

Георги обнови решението на 15.12.2013 23:57 (преди почти 11 години)

+module AttributeEquality
+ def hash
+ equality_objects.hash
+ end
+
+ def eql?(other_object)
+ equality_objects.eql? other_object.equality_objects
+ end
+
+ def ==(other_object)
+ eql? other_object
+ end
+end
+
+module AttributeComparable
+ include Comparable
+
+ def <=>(other_object)
+ equality_objects <=> other_object.equality_objects
+ end
+end
+
+class Object
+ def self.attribute_equality(*args, comparable: false)
+ define_method :equality_objects do
+ args.map { |attribute| public_send attribute }
+ end
+
+ include AttributeEquality
+ include AttributeComparable if comparable
+ end
+end
+
+module Graphics
+
+ class Canvas
+ attr_reader :width, :height
+
+ def initialize(width, height)
+ @width = width
+ @height = height
+
+ @canvas = Array.new height, []
+ @canvas.map! { Array.new width, false }
+ end
+
+ def set_pixel(x, y)
+ @canvas[y][x] = true
+ end
+
+ def pixel_at?(x, y)
+ @canvas[y][x]
+ end
+
+ def draw(figure)
+ figure.draw_on(self)
+ end
+
+ def render_as(renderer)
+ renderer.render(self)
+ end
+
+ def coordinates(yield_new_line = false)
+ enum_for :each_coordinate, yield_new_line
+ end
+
+ private
+ def each_coordinate(yield_new_line = false)
+ 0.upto(height - 1).each do |y|
+ 0.upto(width - 1).each do |x|
+ yield x, y, pixel_at?(x, y)
+ end
+
+ yield if yield_new_line and y < height - 1
+ end
+ end
+ end
+
+ module Renderers
+
+ class Ascii
+ PIXELS = {
+ true => '@'.freeze,
+ false => '-'.freeze
+ }.freeze
+
+ LINE_BREAK = "\n"
+
+ def self.render(canvas)
+ ascii_text = canvas.coordinates(true).map do |x, y, pixel|
+ if x and y
+ PIXELS[pixel]
+ else
+ LINE_BREAK
+ end
+ end
+
+ ascii_text.join
+ end
+ end
+
+ class Html
+ HEADER = <<-HEADER
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>Rendered Canvas</title>
+ <style type="text/css">
+ .canvas {
+ font-size: 1px;
+ line-height: 1px;
+ }
+ .canvas * {
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ border-radius: 5px;
+ }
+ .canvas i {
+ background-color: #eee;
+ }
+ .canvas b {
+ background-color: #333;
+ }
+ </style>
+ </head>
+ <body>
+ <div class="canvas">
+ HEADER
+
+ PIXELS = {
+ true => '<b></b>',
+ false => '<i></i>'
+ }.freeze
+
+ LINE_BREAK = '<br>'
+
+ FOOTER = <<-FOOTER
+ </div>
+ </body>
+ </html>
+ FOOTER
+
+ def self.render(canvas)
+ html_body = canvas.coordinates(true).map do |x, y, pixel|
+ if x and y
+ PIXELS[pixel]
+ else
+ LINE_BREAK
+ end
+ end
+
+ HEADER + html_body.join + FOOTER
+ end
+
+ end
+
+ end
+
+ class Point
+ attr_reader :x, :y
+ attribute_equality :x, :y, comparable: true
+
+ def initialize(x, y)
+ @x = x
+ @y = y
+ end
+
+ def draw_on(canvas)
+ canvas.set_pixel(x, y)
+ end
+
+ def +(other_point)
+ Point.new x + other_point.x, y + other_point.y
+ end
+
+ def -(other_point)
+ Point.new x - other_point.x, y - other_point.y
+ end
+
+ def /(divisor)
+ Point.new x / divisor, y / divisor
+ end
+ end
+
+ class Line
+ attr_reader :from, :to
+ attribute_equality :from, :to
+
+ def initialize(from, to)
+ from, to = to, from if from > to
+
+ @from = from
+ @to = to
+ end
+
+ def draw_on(canvas)
+ if from == to
+ canvas.set_pixel from.x, from.y
+ else
+ rasterize_on canvas
+ end
+ end
+
+ private
+ def rasterize_on(canvas)
+ step_count = [(to.x - from.x).abs, (to.y - from.y).abs].max
+ delta = (to - from) / step_count.to_r
+ current_point = from
+
+ (step_count + 1).times do
+ canvas.set_pixel(current_point.x.round, current_point.y.round)
+ current_point = current_point + delta
+ end
+ end
+ end
+
+ class Rectangle
+ attr_reader :left, :right
+ attribute_equality :top_left, :bottom_right
+
+ def initialize(from, to)
+ from, to = to, from if from > to
+
+ @left = from
+ @right = to
+ end
+
+ def top_left
+ if left.y > right.y
+ Point.new left.x, right.y
+ else
+ left
+ end
+ end
+
+ def top_right
+ if left.y < right.y
+ Point.new right.x, left.y
+ else
+ right
+ end
+ end
+
+ def bottom_left
+ if left.y < right.y
+ Point.new left.x, right.y
+ else
+ left
+ end
+ end
+
+ def bottom_right
+ if left.y > right.y
+ Point.new right.x, left.y
+ else
+ right
+ end
+ end
+
+ def draw_on(canvas)
+ sides.each do |line|
+ canvas.draw(line)
+ end
+ end
+
+ private
+ def sides
+ [
+ Line.new(top_left, top_right ),
+ Line.new(top_right, bottom_right),
+ Line.new(bottom_left, bottom_right),
+ Line.new(top_left, bottom_left )
+ ]
+ end
+ end
+
+end

Бързи бележки:

  • Интересно DRY-ване на проблема със сравнението; аз бих пробвал да капсулирам тези помощни модули в общия namespace
  • За == може да ползваш синоним на метод, няма да има проблем
  • Няма експлицитно изискване да може да сравняваш геометричните обекти за по-малки и по-големи, изисква се само равенство; не казвам, че трябва да го махнеш, просто че не го изискваме
  • Като правиш библиотека, която други хора ще ползват, трябва да си тройно по-въздържан да ползваш метапрограмиране; пробвай да запазиш решението си за сравнение, но без метапрограмиране – например с plain-old-Ruby-include
  • Като алтернатива на горното, ако много държиш на метапрограмирането, поне дефинирай клас-макрото в Class, а не в Object
  • Махни празните редове 35, 80, 277; на тези места не се оставят такива според нашите конвенции
  • Смяташ ли, че тази имплементация на вътрешно представяне на пано е най-оптимална? Струва ми се, че има и по-добри варианти
  • Трябва да има празен ред м/у 67 и 68, както и м/у 267 и 268 (интересно, точно 200 реда разлика)
  • Бих сложил запетая в края на 84-ти ред
  • Ред 211, step_count.succ.times do ...
  • Замразяваш пискелите в Ascii, но не и символа за нов ред на 87?
  • Нещо не ме кефи това canvas.coordinates(true); не ми е достатъчно ясно какво прави това true, трябва да ходя и да гледам дефиницията на метода; ако държиш да е такъв интерфейса, поне пробвай нещо друго, например keyword arguments: canvas.coordinates(yield_new_line: true) – далеч по-ясно е; този аргумент yield_new_line ми изглежда като някаква хакерия, подсказваща за нужда от по-различен дизайн
  • Като цяло, не разбирам защо рендерерите не ползват съществуващия интерфейс на паното, ами си дефинираш нов интерфейс за целта, който "вади" навън нещо много близко до вътрешното представяне на паното; при съществуващия ти интерфейс какво правиш, ако рендерерът е такъв, че се налага да чертае линиите отдолу нагоре, или пък отдясно наляво, или пък първо нечетните, после четните редове? Не трябва ли рендерерът да реши как да ползва паното? Не те задължавам да си променяш дизайна, просто разсъждавам на глас
  • Ако фрийзвам низове, бих го направил консистентно навсякъде, в т.ч. и в константите на Html; също така, там бих сложил HEADER и FOOTER един до друг; тук ще ти хинтна, че при мен има само една константа, която е TEMPLATE :)
  • Когато ползваш join, добра практика е да подаваш винаги, експлицитно с какво join-ваш; не разчитай на дифолта, той се контролира от шантава глобална променлива
  • Интересно, дефинирал си аритметика на точки, за да я ползваш в растеризацията на линия; хм, дали е достатъчно основание това? Просто въпрос.
  • Интересно също така, че имаш методи draw_on и rasterize_on; на мен втория ми харесва повече като име
  • Нещо ме дразни в методите за ъгли на правоъгълника; не съм категоричен, но все пак виж дали няма да успееш да ги рефакторираш по-нататък някак

Добро решение! :)

Моето е 297 реда. Мисля, че бих могъл да разкарам някои неща там. Основното тегаво нещо при мен е растеризацията на линия. При теб изглежда доста по-добре :)

Бързи-бързи... Колко да са бързи.

Благодаря за коментарите!

  • За метапрограмирането се учудих, че няма нещо такова в стандартната библиотека предвид колко много неща има там, стори ми се като нещо, което би се използвало често.
  • Сравнението го използвам при точките, за да мога да кажа това: from, to = to, from if from > to
  • За фрийзването общо взето се чудих доста дали има смисъл да го правя над константи, които са стрингове. Те ще се преизползват така или иначе. За промяна - ако някой ги промени си е на негова отговорност. Как е най-добре и какви са доводите за конкретен избор? Специално за хеша го фрийзвам, за да не се добавят други ключове, които така или иначе няма да се използват. Пикселите в Ascii съм забравил да ги "размразя".
  • Да, canvas.coordinates(true) и мен не ме кефи особено, но ударих в шестте реда на метод и това е първото, за което се сетих. Иначе рендерерите пак могат да си ползват pixel_at?. Ще го направя по друг начин.
  • За TEMPLATE не знам как не се сетих :)
  • Аритметиката на точки - не е достатъчно основание, обаче не ми се стори по-прегледно да създавам нова точка ръчно в растеризирането, или да пазя отделни променливи за x и y (again, 6 реда на метод). :) Знам, че мога да разбия растеризирането на още методи, но не ми се струва уместно. А може и да не съм се сетил за по-добър вариант още, има време до неделя :)
  • draw_on и rasterize_on <= 6 реда на метод :)
  • Какво точно имаш предвид за ъглите на правоъгълника? Можех да ги изчисля в началото и да ги запазя... Всъщност в такива случай, когато няма публични мутиращи функции, да разчитам ли на това, че някой няма да ми промени инстанционните променливи и да си сметна нещата, които зависят само от тях предварително?
  • Относно замразяването, аз предпочитам да замразявам неща, присвоени на константи, за да може ако някой неволно ги промени (дори не директно, а променяйки променлива, на която е била присвоена стойността от константата), да получи изключение рано (fail early)
  • Растеризирането ти на линия е интересно, може и така да остане. Аз просто си разсъждавах на глас. Но пък ако ти дойде нова идея преди крайния срок, ще е интересно да я видим :)
  • Относно ъглите на правоъгълника (и не само), разбира се, че трябва да разчиташ на това, че няма някой да ползва instance_variable_set; това вече е ясно прекрачване на бариерата и определено се прави на отговорността на прекрачващия; за константите не е точно така, защото не е далеч от ума следният хипотетичен сценарий:

      HOSTNAME = 'foo.bar.com'
    
      if valid_tld? HOSTNAME
        connect_to HOSTNAME
        ...
      end
    
      def valid_tld?(host)
        tld = host.slice!(-3..-1)
        %w( com net org ).include? tld
      end
    

Допълнение: За ъглите на правоъгълника, пробвай да го направиш без if-ове в методите. Виж дали ще стане и дали ще ти хареса, ако стане.

Георги обнови решението на 16.12.2013 17:29 (преди почти 11 години)

-module AttributeEquality
- def hash
- equality_objects.hash
- end
-
- def eql?(other_object)
- equality_objects.eql? other_object.equality_objects
- end
-
- def ==(other_object)
- eql? other_object
- end
-end
-
-module AttributeComparable
- include Comparable
-
- def <=>(other_object)
- equality_objects <=> other_object.equality_objects
- end
-end
-
-class Object
- def self.attribute_equality(*args, comparable: false)
- define_method :equality_objects do
- args.map { |attribute| public_send attribute }
- end
-
- include AttributeEquality
- include AttributeComparable if comparable
- end
-end
-
module Graphics
-
class Canvas
attr_reader :width, :height
def initialize(width, height)
@width = width
@height = height
- @canvas = Array.new height, []
- @canvas.map! { Array.new width, false }
+ @canvas = Hash.new false
end
def set_pixel(x, y)
- @canvas[y][x] = true
+ @canvas[[y, x]] = true
end
def pixel_at?(x, y)
- @canvas[y][x]
+ @canvas[[y, x]]
end
def draw(figure)
figure.draw_on(self)
end
def render_as(renderer)
renderer.render(self)
end
+ end
- def coordinates(yield_new_line = false)
- enum_for :each_coordinate, yield_new_line
+ module Equality
+ module AttributeEquality
+ def hash
+ equality_objects.hash
+ end
+
+ def eql?(other_object)
+ equality_objects.eql? other_object.equality_objects
+ end
+
+ alias_method :==, :eql?
end
- private
- def each_coordinate(yield_new_line = false)
- 0.upto(height - 1).each do |y|
- 0.upto(width - 1).each do |x|
- yield x, y, pixel_at?(x, y)
- end
+ module AttributeComparable
+ include Comparable
- yield if yield_new_line and y < height - 1
+ def <=>(other_object)
+ equality_objects <=> other_object.equality_objects
end
end
+
+ def attribute_equality(*args, comparable: false)
+ define_method :equality_objects do
+ args.map { |attribute| public_send attribute }
+ end
+
+ include AttributeEquality
+ include AttributeComparable if comparable
+ end
end
module Renderers
-
class Ascii
PIXELS = {
true => '@'.freeze,
- false => '-'.freeze
+ false => '-'.freeze,
}.freeze
- LINE_BREAK = "\n"
+ LINE_BREAK = "\n".freeze
def self.render(canvas)
- ascii_text = canvas.coordinates(true).map do |x, y, pixel|
- if x and y
- PIXELS[pixel]
- else
- LINE_BREAK
+ rows = 0.upto(canvas.height - 1).each_with_object([]) do |y, rows|
+ row = 0.upto(canvas.width - 1).each_with_object([]) do |x, row|
+ row << PIXELS[canvas.pixel_at?(x, y)]
end
+
+ rows << row.join('')
end
- ascii_text.join
+ rows.join(LINE_BREAK)
end
end
class Html
- HEADER = <<-HEADER
+ TEMPLATE = <<-HEADER.freeze
<!DOCTYPE html>
<html>
<head>
<title>Rendered Canvas</title>
<style type="text/css">
.canvas {
font-size: 1px;
line-height: 1px;
}
.canvas * {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 5px;
}
.canvas i {
background-color: #eee;
}
.canvas b {
background-color: #333;
}
</style>
</head>
<body>
<div class="canvas">
+ %s
+ </div>
+ </body>
+ </html>
HEADER
PIXELS = {
- true => '<b></b>',
- false => '<i></i>'
+ true => '<b></b>'.freeze,
+ false => '<i></i>'.freeze,
}.freeze
- LINE_BREAK = '<br>'
+ LINE_BREAK = '<br>'.freeze
- FOOTER = <<-FOOTER
- </div>
- </body>
- </html>
- FOOTER
-
def self.render(canvas)
- html_body = canvas.coordinates(true).map do |x, y, pixel|
- if x and y
- PIXELS[pixel]
- else
- LINE_BREAK
+ rows = 0.upto(canvas.height - 1).each_with_object([]) do |y, rows|
+ row = 0.upto(canvas.width - 1).each_with_object([]) do |x, row|
+ row << PIXELS[canvas.pixel_at?(x, y)]
end
+
+ rows << row.join('')
end
- HEADER + html_body.join + FOOTER
+ TEMPLATE % rows.join(LINE_BREAK)
end
-
end
-
end
class Point
+ extend Equality
+
attr_reader :x, :y
attribute_equality :x, :y, comparable: true
def initialize(x, y)
@x = x
@y = y
end
def draw_on(canvas)
canvas.set_pixel(x, y)
end
def +(other_point)
Point.new x + other_point.x, y + other_point.y
end
def -(other_point)
Point.new x - other_point.x, y - other_point.y
end
def /(divisor)
Point.new x / divisor, y / divisor
end
end
class Line
+ extend Equality
+
attr_reader :from, :to
attribute_equality :from, :to
def initialize(from, to)
from, to = to, from if from > to
@from = from
@to = to
end
def draw_on(canvas)
if from == to
canvas.set_pixel from.x, from.y
else
rasterize_on canvas
end
end
private
+
def rasterize_on(canvas)
step_count = [(to.x - from.x).abs, (to.y - from.y).abs].max
delta = (to - from) / step_count.to_r
current_point = from
- (step_count + 1).times do
+ step_count.succ.times do
canvas.set_pixel(current_point.x.round, current_point.y.round)
current_point = current_point + delta
end
end
end
class Rectangle
+ extend Equality
+
attr_reader :left, :right
+ attr_reader :top_left, :top_right, :bottom_left, :bottom_right
attribute_equality :top_left, :bottom_right
def initialize(from, to)
from, to = to, from if from > to
@left = from
@right = to
- end
- def top_left
- if left.y > right.y
- Point.new left.x, right.y
- else
- left
- end
+ calculate_points
end
- def top_right
- if left.y < right.y
- Point.new right.x, left.y
- else
- right
- end
+ def draw_on(canvas)
+ sides.each { |line| canvas.draw(line) }
end
- def bottom_left
- if left.y < right.y
- Point.new left.x, right.y
- else
- left
- end
- end
+ private
- def bottom_right
- if left.y > right.y
- Point.new right.x, left.y
- else
- right
- end
- end
+ def calculate_points
+ y_coordinates = [left.y, right.y]
- def draw_on(canvas)
- sides.each do |line|
- canvas.draw(line)
- end
+ @top_left = Point.new left.x, y_coordinates.min
+ @top_right = Point.new right.x, y_coordinates.min
+ @bottom_left = Point.new left.x, y_coordinates.max
+ @bottom_right = Point.new right.x, y_coordinates.max
end
- private
def sides
[
Line.new(top_left, top_right ),
Line.new(top_right, bottom_right),
Line.new(bottom_left, bottom_right),
- Line.new(top_left, bottom_left )
+ Line.new(top_left, bottom_left ),
]
end
end
-
end

Така, попромених малко неща. Ето changelog :)

  • Метапрограмирането го вкарах в "помощен" модул Graphics::Equality, който не добавя неща към Object/Class или други. Самата идея за клас макро доста ми харесва в този случай, иначе можеше и директно да се прави include и equality_objects да се дефинира ръчно.
  • Замразих всички константи
  • Махнах canvas.coordinates за сметка на малко работа с each_with_object
  • TEMPLATE. Между другото има ли добър начин да се ползват именувани placeholder-и в такъв стринг?
  • canvas вътрешно ползва Hash
  • top_left, top_right и сие се изчисляват при създаване на обект и няма if-ове
  • Оправих дребните стилови неща, за които спомена

Именувани placeholder-и при интерполация могат да се ползват така:

"The answer is: %{answer}" % {answer: 42}

Неименувани placeholder-и в стил sprintf:

"The answer is: %d" % 42

Документацията за това е в Kernel.sprintf.

И аз ползвам Hash в паното. Не е необходимо pixel_at? да връща false за празни пиксели. И nil е напълно окей. Ако смениш това, ще трябва да си редактираш Ascii::PIXELS.

Относно Ascii.render, защо не пробваш map + join, вместо each_with_object?

Между другото, няма експлицитна нужда да ползваш HEREDOC за TEMPLATE. Може да е и обикновен single-quoted низ.

Не се ли дублира обхождането в Ascii и Html (до голяма степен)? Може би не е фатално, но може би не искаш да го оставиш така.

Така ми харесва повече "метапрограмирането". Доста по-чистичко е. Щом ти харесва клас-макрото, това е добър начин да го вкараш – не замърсява нищо в обектите на потребителя.

Пак да зачекна темата за draw_on и rasterize_on – мисля си, че второто е по-добро име за този вътрешен интерфейс. Ако те спъва ограничението от 6 символа в метод, може би трябва да разпаднеш самия rasterize_on някак. Просто draw_onrasterize_on и разпадаш Line#rasterize_on някак, за да се вместиш в ограничението.

Може би calculate_pointsdetermine_corners или нещо по-специфично?

Трябва ли ти отделен метод Rectangle#sides? Не държа да го махаш, private е, така че е окей.

Решението е станало доста добро, много ми харесва :)

Георги обнови решението на 16.12.2013 20:46 (преди почти 11 години)

module Graphics
class Canvas
attr_reader :width, :height
def initialize(width, height)
@width = width
@height = height
@canvas = Hash.new false
end
def set_pixel(x, y)
@canvas[[y, x]] = true
end
def pixel_at?(x, y)
@canvas[[y, x]]
end
def draw(figure)
figure.draw_on(self)
end
def render_as(renderer)
renderer.render(self)
end
end
module Equality
module AttributeEquality
def hash
equality_objects.hash
end
def eql?(other_object)
equality_objects.eql? other_object.equality_objects
end
alias_method :==, :eql?
end
module AttributeComparable
include Comparable
def <=>(other_object)
equality_objects <=> other_object.equality_objects
end
end
def attribute_equality(*args, comparable: false)
define_method :equality_objects do
args.map { |attribute| public_send attribute }
end
include AttributeEquality
include AttributeComparable if comparable
end
end
module Renderers
+ module BasicRenderer
+ def render(canvas, pixels, line_break, template='%{canvas}')
+ rows = 0.upto(canvas.height - 1).map do |y|
+ row = 0.upto(canvas.width - 1).map do |x|
+ pixels[canvas.pixel_at?(x, y)]
+ end
+
+ row.join('')
+ end
+
+ template % {canvas: rows.join(line_break)}
+ end
+ end
+
class Ascii
+ extend BasicRenderer
+
PIXELS = {
true => '@'.freeze,
false => '-'.freeze,
}.freeze
LINE_BREAK = "\n".freeze
def self.render(canvas)
- rows = 0.upto(canvas.height - 1).each_with_object([]) do |y, rows|
- row = 0.upto(canvas.width - 1).each_with_object([]) do |x, row|
- row << PIXELS[canvas.pixel_at?(x, y)]
- end
-
- rows << row.join('')
- end
-
- rows.join(LINE_BREAK)
+ super canvas, PIXELS, LINE_BREAK
end
end
class Html
- TEMPLATE = <<-HEADER.freeze
+ extend BasicRenderer
+
+ TEMPLATE = <<-TEMPLATE.freeze
<!DOCTYPE html>
<html>
<head>
<title>Rendered Canvas</title>
<style type="text/css">
.canvas {
font-size: 1px;
line-height: 1px;
}
.canvas * {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 5px;
}
.canvas i {
background-color: #eee;
}
.canvas b {
background-color: #333;
}
</style>
</head>
<body>
<div class="canvas">
- %s
+ %{canvas}
</div>
</body>
</html>
- HEADER
+ TEMPLATE
PIXELS = {
true => '<b></b>'.freeze,
false => '<i></i>'.freeze,
}.freeze
LINE_BREAK = '<br>'.freeze
def self.render(canvas)
- rows = 0.upto(canvas.height - 1).each_with_object([]) do |y, rows|
- row = 0.upto(canvas.width - 1).each_with_object([]) do |x, row|
- row << PIXELS[canvas.pixel_at?(x, y)]
- end
-
- rows << row.join('')
- end
-
- TEMPLATE % rows.join(LINE_BREAK)
+ super canvas, PIXELS, LINE_BREAK, TEMPLATE
end
end
end
class Point
extend Equality
attr_reader :x, :y
attribute_equality :x, :y, comparable: true
def initialize(x, y)
@x = x
@y = y
end
def draw_on(canvas)
canvas.set_pixel(x, y)
end
def +(other_point)
Point.new x + other_point.x, y + other_point.y
end
def -(other_point)
Point.new x - other_point.x, y - other_point.y
end
def /(divisor)
Point.new x / divisor, y / divisor
end
end
class Line
extend Equality
attr_reader :from, :to
attribute_equality :from, :to
def initialize(from, to)
from, to = to, from if from > to
@from = from
@to = to
end
def draw_on(canvas)
if from == to
canvas.set_pixel from.x, from.y
else
rasterize_on canvas
end
end
private
def rasterize_on(canvas)
step_count = [(to.x - from.x).abs, (to.y - from.y).abs].max
delta = (to - from) / step_count.to_r
current_point = from
step_count.succ.times do
canvas.set_pixel(current_point.x.round, current_point.y.round)
current_point = current_point + delta
end
end
end
class Rectangle
extend Equality
attr_reader :left, :right
attr_reader :top_left, :top_right, :bottom_left, :bottom_right
attribute_equality :top_left, :bottom_right
def initialize(from, to)
from, to = to, from if from > to
@left = from
@right = to
- calculate_points
+ determine_corners
end
def draw_on(canvas)
sides.each { |line| canvas.draw(line) }
end
private
- def calculate_points
+ def determine_corners
y_coordinates = [left.y, right.y]
@top_left = Point.new left.x, y_coordinates.min
@top_right = Point.new right.x, y_coordinates.min
@bottom_left = Point.new left.x, y_coordinates.max
@bottom_right = Point.new right.x, y_coordinates.max
end
def sides
[
Line.new(top_left, top_right ),
Line.new(top_right, bottom_right),
Line.new(bottom_left, bottom_right),
Line.new(top_left, bottom_left ),
]
end
end
end

corners е думата, за която не успях да се сетя, благодаря :)

  • pixel_at? връщащо nil не съм сигурен дали трябва да ме притеснява в случая. Хмм...
  • Предпочитам HEREDOC за HTML константи - няма да има нужда да escape-вам кавички ако се появят такива някъде, въпреки че е малко вероятно.
  • За дублиране - дублира се :) Изсуших го.
  • sides си го харесвам :) Прави draw_on доста ясен.
  • map много ясно, пфф...
  • Някак не ми се иска да разпадам сегашния rasterize_on. Така целия алгоритъм за растеризирането е на едно място и се вижда всичко, което се случва.

А всъщност се колебая дали draw_on или rasterize_on е по-добре за публичния интерфейс. С draw_on има някаква симетрия canvas.draw(figure) => figure.draw_on(canvas)

  • Не знам защо, но мен лично ме дразни синтаксиса за sprintf-интерполация с именувани placeholder-и, когато трябва да подам стойностите литерално и имам само една стойност... Някак си натежават нещата и аз ползвах само %s в случая.
  • За рендерерите - малко особен ми е дизайнът, как подаваш няколко аргумента на render и прочее. Струва ми се, че може да стане по малко по-обектно-ориентиран начин (с наследяване и предефиниране на методи, например). В момента ти ползваш тези класове само статично и като едни namespaces за константи и функции. А не е нужно да е така. Заслужава си да обсъдим това по време на лекции, когато разглеждаме решения на тази задача, което ще е вероятно на 6-ти януари догодина.
  • За rasterize_on и draw_on аз си казах мнението вече :)

За рендерерите съм съгласен, но в случая няма публичен интерфейс, на който се подават аргументи на рендерера, или поне не се изисква такъв в условието на задачата. Ако например се подаваше инстанция на рендерер на render_as тогава наистина би било по-интуитивно да се имплементира по друг начин.

А не съществува ли рендерера само заради тази негова функция, която да бъде използвана от canvas? Поне за сегашните изисквания.

За стринг интерполацията с имена ми е трудно да определя хубав аргумент за или против... Обичам да именувам разни неща, но това не се брои.

Наистина май само с %s би било по-добре.