I feel like I'm gonna like this serie a lot :-) It's so much fun!
Let's dive right in!
Write a function that draws an ASCII art cube of given height x.
Example:
$ drawCube(2)
+----+
/ /|
+----+ |
| | +
| |/
+----+
$ drawCube(4)
+--------+
/ /|
/ / |
+--------+ |
| | |
| | +
| | /
| |/
+--------+
What to do with odd numbers is not specified. What to do when input is 0 or 1 neither.
I'm still doing it in SQL, because it adds even more fun to me (fun is a relative notion you know).
Here it is, explanations below:
create or replace function public.draw_cube(height integer)
returns text
language sql
immutable
as $function$
-- generate one line for each line of output, and calculate some preliminary infos
with infos as (
select
line_number,
-- the number of spaces we need before (for the top face)
greatest(height / 2 - line_number + 2, 0)
as space_prefix,
-- the number of spaces we need to draw the right/east face
-- it's an inverted, shifted, translated abs(x) :-)
floor(least(height / 2, (3 * height)::float / 4 - abs(line_number - 2 - (3 * height)::float / 4)))::integer
as space_right_face,
-- are we drawing an horizontal line?
line_number = 1 or line_number = height / 2 + 2 or line_number = 3 * height / 2 + 3
as is_horizontal,
-- we start drawing bottom_line after one face + 2 horizontal lines
line_number > height + 2
as drawing_bottom_face
from
-- the number of lines: height + height / 2 for the top face + 3 horizontal edges
generate_series(1, 3 * height / 2 + 3 ) line_number
),
-- draw the cube
cube_lines as (
select
line_number,
repeat(' ', space_prefix) ||
case
-- horizontal line
when is_horizontal then
'+' || repeat('-', 2 * height) || '+'
else
-- front facing square edge, see lateral below
front_facing_side_char || repeat(' ', 2 * height) || front_facing_side_char
end
||
-- now drawing the right edge of the east face
case
when space_right_face >= 0 then
-- spaces
repeat(' ', space_right_face) ||
-- rightmost edge
case
when drawing_bottom_face then
'/'
-- a special case for the end of the hidden edge, there might be a more elegant way here
else case when line_number - 2 = height then '+' else '|' end
end
else '' -- we need this empty string, because 'foo' || null is null!
end as line
from
infos,
-- factorize top face char calculation
lateral (select case when space_prefix > 0 then '/' else '|' end) as _(front_facing_side_char)
)
-- aggregate in one big string separated by \n
select
string_agg(line, E'\n' order by line_number)
from cube_lines
;
$function$
I chose to again use generate_series
to get all the lines then aggregate them. It's certainly possible to do differently.
I've decided against using any union btw (I think they would have made the query slightly easier to write, but it feels less elegant to me).
Here there is 3 lines for the horizontal edges, height
lines for the front-facing face and height / 2
lines for the top face. I've already made a choice here: 2*n
and 2*n+1
cube will have the same apparent height for the top face (so rounding down height / 2
each time it's needed, which is actually what an integer division does, which is what postgres does with the /
operator between integer, so the choice is really the lazy one here ;-) ).
I first use a CTE (infos
) to precalculate some states per lines. Here is what it looks like if I execute it alone:
\set height 6
select
line_number,
-- the number of spaces we need before (for the top face)
greatest(:height / 2 - line_number + 2, 0)
as space_prefix,
-- the number of spaces we need to draw the right/east face
-- it's an inverted, shifted, translated abs(x) :-)
floor(least(:height / 2, (3 * :height)::float / 4 - abs(line_number - 2 - (3 * :height)::float / 4)))::integer
as space_right_face,
-- are we drawing an horizontal line?
line_number = 1 or line_number = :height / 2 + 2 or line_number = 3 * :height / 2 + 3
as is_horizontal,
-- we start drawing bottom_line after one face + 2 horizontal lines
line_number > :height + 2
as drawing_bottom_face
from
-- the number of lines: :height + :height / 2 for the top face + 3 horizontal edges
generate_series(1, 3 * :height / 2 + 3 ) line_number
;
line_number | space_prefix | space_right_face | is_horizontal | drawing_bottom_face
-------------+--------------+------------------+---------------+---------------------
1 | 4 | -1 | t | f
2 | 3 | 0 | f | f
3 | 2 | 1 | f | f
4 | 1 | 2 | f | f
5 | 0 | 3 | t | f
6 | 0 | 3 | f | f
7 | 0 | 3 | f | f
8 | 0 | 3 | f | f
9 | 0 | 2 | f | t
10 | 0 | 1 | f | t
11 | 0 | 0 | f | t
12 | 0 | -1 | t | t
(12 rows)
Each line represents a line of output. If I break down each column:
is_horizontal
tells me if I need to draw an horizontal edge on this line (so it's true for line 1, 5 and 12)drawing_bottom_face
helps me know whether to choose /
or |
as the last character of one line.space_prefix
is the number of space needed before the first non-empty ascii char.space_right_face
is similar, but for the empty space inside the right face.Then using these informations, I can express the drawing logic in a - hopefully - clear way in the cube_lines
CTE.
space_prefix
The slope of the function x -> nb space is obviously -1, and I want it to be 0 at the second horizontal lines, so after 1 + height / 2 + 1 lines (line numbers start from 1). Moreover, it does not go below 0, hence the use of greatest
.
Here is the plot for height = 6:
The idea is to calculate the number of spaces I need to put inside the right face for each lines. For height=6 I need:
line_number | space_right_face |
---|---|
1 | -1 |
2 | 0 |
3 | 1 |
4 | 2 |
5 | 3 |
6 | 3 |
7 | 3 |
8 | 3 |
9 | 2 |
10 | 1 |
11 | 0 |
12 | -1 |
It really looks like an abs
capped by a max
! The trick now is to find the exact abs
function.
I didn't make complicated calculus for that:
-abs(line_number)
-2
because line 1 and 2 should give back 03 * length / 4
3*4 / height
at the beginning least
to avoid going under 0. Note that this part is not really necessary because I test for the result to be >=0 later in the query.So why the floor? Well, here are the curve I want for heigth = 5, 6 and 7:
Notice how it gives non integer numbers? It's a problem when drawing ascii art: we can't draw half a column! Adding the floor calculation changes it to:
Much more like what we want!
Cassidy claims this is a follow-up of previous week question, but I felt this one was actually a lot harder. It was also a lot more fun (relative notion, remember?)!
The solution I chose feels a bit "naive" to me and I'm pretty sure there's more clever way to do this. If you have one, I'd love to see it! Please tell me on twitter.
Some ideas I have on top of my head:
Yes, in the fun-o-meter, a rotating ascii cube might score well!