What I Learned from Pikabu JavaScript Challenge
Pikabu.ru is the Russian equivalent of Reddit. The other day their targeted ad post invited me to take a “JobSeeker” JavaScript coding challenge to join their team as a Front-end developer.
I hacked my way to the final “Congratulations, you made it!” stage with one test not consistently passing (more on that at the end). Spending hours on the challenge was completely worth it, because I got deeper knowledge on certain subjects and learned new things:
How it works
The challenge consists of a JobSeeker
class with code in it, but some pieces are missing. More like a puzzle :) We’re allowed to fill in the blanks with ONLY ONE VALUE which is a variable, class or function name, string literal, integer, unary or binary operator. NO function calls e.g. foo()
, NO dot notation e.g. arr.length
. NO ternary statements.
In the times of Jest and Mocha testing frameworks it was refreshing to learn another simple way to test your functions. While console.log()
is essential for debugging, it appears that console.assert()
can also be used to test code.
The challenge uses a sequence of console.assert()
statements to test the code. Asserts are independent from each other. If the first assert is not passing, the second assert will not be evaluated. We are going to walk through the tests one at a time.
Assert 1
The first call of buildArray()
initiates a stack
and returns a function. On all following calls in the test out()
function is called. The reference to stack
lives through all out()
calls, closure at its best.
We notice two types of input, a number and a function. value1
on line 7 has to be number
, so that all number args are saved in the stack
. When the arg is a function, it is applied to stack
. Thus someName
on line 12 is apply
.
Assert 2
Let’s determine the value
first. On line 15, value — 1 === value / 15
. Which entity of type Number can do that? If we assume NaN
, then both sides of the equation will become NaN
. But as we know NaN !== NaN
. The value
that works is Infinity
.
Look at variable f
on line 12. In the ASCII table character number 0b100
(4 in decimal) is EOT which stands for End Of Transmission. This character is not printable. So what do we put into someName
then? The character’s Unicode representation '\u0004'
.
Assert 3
Let us translate what the test wants from us.
- regex needs Cyrillic characters.
yourName
in format first name and the first letter of the last name likeНикки Д.
yourCity
expects one word for city preceded byг.
, which is short for “city” in Russian →г. НьюЙорк
- An empty
<span>
followed by<my-name>
element. - Two elements with
my-city
class, we will test the second one (with index 1).dataset
is a way to add custom attributes to an element.dataset.cityName
means that this element should contain an attributedata-city-name
. :out-of-range
pseudo class requires an<input>
element withmin
andmax
attributes. If we makemax = "18"
then my age will indeed be out of range.[data-my-age]
means that the input element needs to have this attribute. We also need to hardcode the agevalue
into this element.- On line 14 the placeholders are replaced with the props from “data” object. Regex tells us that we should prepend the placeholders with
::
With information above, the value
blank on line 13 should be filled with the following:
<span class="my-city"></span>
<my-name>::name</my-name>
<span class="my-city" data-city-name="::city"></span>
<input data-my-age type="number" min="17" max="18" value="::age" />
Assert 4
If strings are equal, then we have one unique string. Set
object is often used to discern unique things. someName1
blank will be filled with Set
. And indeed, the size of a Set built from array of two strings should be 1 if strings are duplicates, i.e. equal.
someName2
has to be map
, because we are applying some function to each string in the array. Note that arr.map()
is the same as arr['map']()
.
NFKC stands Normalization Form Compatibility Composition. It’s one the forms of Unicode Normalization, or putting strings in a certain Unicode format. someName3
becomes normalize
. And the test passes: regular equality check confirms string are not equal, but the “normalized” comparison says they are.
Assert 5
Did you know that since May 2019 1_000_000_000
is a valid number literal in JS V8 engine? (Thus the warning on line 6 about Chrome, which runs on V8). Underscore is a numeric separator, just like comma or dot. Here’s a twist. They cannot be parsed from a string with neither Number()
nor parseInt()
. According to MDN, eval()
invokes the JS interpreter. Sounds like just what we need to parse nums.join('_')
to a number. This way someName1
becomes eval
.
Since the formatNumbers
function returns a string, someName2
is toString
. By trial and error we can get that the radix of toString
is 8. How to get 8 from two and two? With bitwise operator “shift left”. operator
becomes <<
.
Assert 6
Whoa! new new new new new
? We can sure make it work. The first reaction to the line 21 is that we need a constructor that processes an argument. Let’s make function name someName2
on line 11 into constructor
, and someName3
will become arguments
.
When there is an argument passed to constructor on line 12, it needs to return a constructor, because new
keyword is encountered again and again in the test. JobSeeker
as a function is a constructor. And that is what we put instead of value2
on line 12.
The cases when JobSeeker object is the instance of JobSeeker
are 1) when no arguments is passed to the constructor, 2) when the argument is falsy. The constructor itself (when we return it) is not the instance of JobSeeker
.
Variable i
in the first test is the instance of JobSeeker. But on line 21 it is used with a decrement i--
, looks like it expects a number, which is a primitive and not an object. Conveniently every object has a way of returning a primitive when it’s referred, with a function valueOf()
, which we need to override for JobSeeker. This way someName1
on line 6 becomes valueOf
.
What will our custom valueOf
return? On line 21 we need the last call of new Jobseeker(i--)
to return the instance of JobSeeker
. For that we need the value of i
on the last call to be falsy, or in numeric terms, a zero. This happens when we put 4
into value1
on line 7. Our custom valueOf
ends up being called only once, when we refer to it for the first time. Then it’s treated as a number and keeps being decremented.
Assert 8 — my personal favorite
The most important variable here is rgb
. It’s a Uint8ClampedArray
of pixels that we get from the canvas context. Each pixel occupies four array slots with values of RGBA each in its own slot. When we iterate through pixels we have to increase the counter by 4, like in line 29.
We have a path for the image in the test, let’s download it from the server and have a look.
Thanks for the hint, Pikabu. Line 30 confirms that keys
array has three elements. We shall put 342 34 11
into value2
on line 9. After applying these filters for each pixel’s RGB values (lines 29–34) we get the following drawing on canvas:
Here is the maze/labyrinth! This image contains hints about where the start and where the exit is, and that the side of each step is 12 pixels. When we look at lines 40 and 65, we see that one of the options for step
is size
. We make value1
on line 8 into size 12
.
In order to get to the yellow square, the other squares can help us with directions. Red — go down, fuchsia — go right, green — go up, blue — go left, white — continue going in the same direction. Black squares are the walls and must be avoided.
Our rgb
array fills the canvas image with pixels from top left to bottom right. The directions
array on line 40 is interpreted as[up, right, down, left]
. Let us note that “up” has index 0, “right” has index 1, “down”— index 2, and “left” — index 3.
There is also a key
associated with each color (line 43–45). We have RGB values for colors from the picture. Let’s see how a key is calculated:
It appears that ~~
is another way to say Math.floor
. Neat. After calculating the key for each color let’s summarize what we have:
╔═════════╦═══════════╦═══════╦══════════╦═════════════════════════╗
║ color ║ key ║ index ║ meaning ║ in code (lines 42-68) ║
╠═════════╬═══════════╬═══════╬══════════╬═════════════════════════╣
║ black ║0 or 0b0 ║ - ║ wall ║ returns false ║
║ white ║7 or 0b111 ║ - ║ continue ║ keep looping do..while ║
║ yellow ║6 or 0b110 ║ - ║ finish ║ returns true ║
║ green ║2 or 0b010 ║ 0 ║ go up ║ changes step/direction ║
║ fuchsia ║5 or 0b101 ║ 1 ║ go right ║ changes step/direction ║
║ red ║4 or 0b100 ║ 2 ║ go down ║ changes step/direction ║
║ blue ║3 or 0b011 ║ 3 ║ go left ║ changes step/direction ║
╚═════════╩═══════════╩═══════╩══════════╩═════════════════════════╝
Inside the do…while loop the cases for black and yellow are covered. What we need to add to value3
on line 55 is 0b111
, which is the key for white. Because we only have to change the direction(step) when the pixel is NOT white.
And finally, let’s figure out what the map
variable does. It is used on line 57, where parts of it are compared to the pixel color key. 7 in binary is 111
. In 7 & something
it acts like a mask where it copies the last three bits of something
. map
is shifted right by dir
. dir
can be 9, 6, 3 or 0. Looks like we need to combine the keys for direction-changing colors in a certain order, so that when while loop on line 57 stops, index i
would end up representing the direction we need. Now we know thatmap
is mapping the color keys to correct indexes in directions
array. From the table above we see that the order of colors mapped onto directions
array is [green, fuchsia, red, blue]
. So the binary number that we put into map
blank on line 7 is a sequence of binary color keys in the correct order : 10101100011
.
10 101 100 011
green fuchsia red blue
Assert 7 — oopsie
The only HTML element that has a content
property is HTMLTemplateElement
. It means that we need to create a <template>
element, and on line 7 instead of name
we will put template
.
el.content
returns a DocumentFragment
, which is a Document Object that has no parent. Thus, we can query it with querySelector()
and querySelectorAll()
.
On lines 11–13 we are swapping two <p>
elements. To pass the test we need to swap 3 and 4. We need to select the <p>
element that contains 4. We notice that the <div>
with 4 is the only one that has three children, and the <p>
we need is preceded by other two <p>
elements. So on line 11value1
selector string will be p + p + p
.
On line 18 we are selecting certain type of <p>
elements and for each remove their parent. That’s exactly what we need to do with all the <div>
s that contain X and Y. We observe that those <div>
s have exactly two two children. To select a <p>
that has exactly one sibling, this <p>
is at the same the first child and the second child from the end. So the value3
selector string will combine two pseudo classes: p:first-child:nth-last-child(2)
.
Now that’s where the trouble comes. On lines 15–16 we need to select an element that contains 5 and put value 6 into it to pass the test. What can we observe about the <p>
that contains 5? It’s the only child. But the problem is that the <p>
with 1 is also the only child. Since querySelector()
is used, it will choose the first “only child” that it sees. And that would be 1, not 5. If there is a way to select the second occurrence of :only-child
with querySelector()
, please share.
Here’s a hacky solution to pass this test. Since the mix
variable is created randomly, then there’s a chance it would be repeated zero times i.e. could be empty. If mix
is empty, then we just select the last <p>
which is 5! Let’s make value2
selector div:last-of-type p
and keep running the test until Math.random
returns zero. ¯\_(ツ)_/¯
Thanks Pikabu. It’s been real. 🤓