---
title: 'RDV with Cassidoo #201 : Cubes!'
url: 'https://www.trancart.eu/en/blog/20210621-rendezvous-cassidoo-201'
markdown: 'https://www.trancart.eu/en/blog/20210621-rendezvous-cassidoo-201.md'
lang: en
date: '2021-06-23'
description: "I feel like I'm gonna like this serie a lot :-) It's so much fun! Let's dive right in! The interview question Write a function that draws an ASCII art cube of given height x. Example: $ drawCube(2) +----+ / /| +----+ | | | + | |/ +----+ $ drawCube(4) +--------+ / /| / …"
taxonomy:
  tag:
    - sql
    - puzzle
    - cassidoo
---

# RDV with Cassidoo #201 : Cubes!

I feel like I'm gonna like this serie a lot :-) It's so much fun!

Let's dive right in!

### The interview question

> 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.

### My solution

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:

```sql
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$
```

### Generate the lines

I chose to again use [`generate_series`](https://www.postgresql.org/docs/13/functions-srf.html) 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:

```sql
\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.

### About `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:

### Space\_right\_face

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:

- I started by inverting it: `-abs(line_number)`
- then by shifting the inflexion point: 
    - `-2` because line 1 and 2 should give back 0
    - then the right face has at least one space for 3 half-height. So I shift again by half this value: `3 * length / 4`
    - but we need to make sure that f(2) = 0, whch gives us the `3*4 / height` at the beginning
    - we then use `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!

## Conclusion

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](https://twitter.com/autra42).

Some ideas I have on top of my head:

- do overlays: draw the first square (the rear face), then the edges, then the front face and keep only the front char. The idea is that it's easy to draw squares. Not sure how to deal with edges.
- go full 3D: do a list of vertices, use a projection matrix and then "serialize" all that to ascii! Maybe overkill? And how to you translate edges that are not aligned to axis in a consistent way? This way would open up a whole field of possibilites (rotating ascii cube for instance).

Yes, in the fun-o-meter, a rotating ascii cube might score well!

 [ Previous Post](https://www.trancart.eu/en/blog/20210618-rendezvous-cassidoo-200) [Next Post ](https://www.trancart.eu/en/blog/rendez-vous-with-cassidoo-211-longuest-prefix-in-sql)

#### Syndicate

 [ Atom 1.0](https://www.trancart.eu/en/blog.atom) [ RSS](https://www.trancart.eu/en/blog.rss)

---

## Navigation

- Parent: [Blog](https://www.trancart.eu/en/blog.md)
- Previous: [Rendez-vous With Cassidoo #211: longuest prefix, in sql](https://www.trancart.eu/en/blog/rendez-vous-with-cassidoo-211-longuest-prefix-in-sql.md)
- Next: [Rendez-vous with cassidoo #200, interview question of the week](https://www.trancart.eu/en/blog/20210618-rendezvous-cassidoo-200.md)
