While reading the 2nd edition of Eloquent JavaScript by Marijn Haverbeke I go through the chapter 1-4 without having any trouble but at chapter 5 I have struggled a little bit to understand what's going on but with the help of the Annotated version of Eloquent JavaScript by Gordon Zhu (the ex-Google developer) I understand it properly. At chapter 6 I understand most of the topic pretty much well except the "Laying Out A table" section. I put much attention and give as much time as I can to understand the problem but it makes me crazy. Even the Annotated version of Eloquent JavaScript didn't help me much.
I did some search on google and got a lot of people who have faced the same problem and I am not alone who was struggling with it.
while searching for the problem in google I found a blog where a guy named Tomio Mizoroki who has explained the problem much better. But he didn't explain the rest of it. he just explains the first checkerboard problem.
You can find the problem from the online version of eloquent javaScript. Tomio Mizoroki has explained the checkerboard problem very well so I will explain the main problem of this topic - Nicely laid out a table from a given data. But I recommend you to read the Tomio Mizoroki's explanation first.
Remember I am an advanced beginner and I have never tried to explain code before. English is also not my native language, So some of my explanation may look wired for the readers.
Before reading this article be sure that you have a clear understanding on javaScript array method's such as map and reduce. You also must have a strong understanding on javaScript prototypes and contractors.
The Problem:
The input will be this -
var MOUNTAINS = [
{name: "Kilimanjaro", height: 5895, country: "Tanzania"},
{name: "Everest", height: 8848, country: "Nepal"},
{name: "Mount Fuji", height: 3776, country: "Japan"},
{name: "Mont Blanc", height: 4808, country: "Italy/France"},
{name: "Vaalserberg", height: 323, country: "Netherlands"},
{name: "Denali", height: 6168, country: "United States"},
{name: "Popocatepetl", height: 5465, country: "Mexico"}
];
and the output will look like this in the browser console
name height country
------------ ------ -------------
Kilimanjaro 5895 Tanzania
Everest 8848 Nepal
Mount Fuji 3776 Japan
Mont Blanc 4808 Italy/France
Vaalserberg 323 Netherlands
Denali 6168 United States
Popocatepetl 5465 Mexico
Let's Get Into The Program:
Our input data is very massive. We will make it short so that we can track what is happening with them. so we will work with this input -
var MOUNTAINS = [
{name: "Everest", height: 8848},
{name: "vaalserberg", height: 323}
]
If our program works for this it will also work for large data set. So finally our output will look like this -
name height
----------- ------
Everest 8848
Vaalserberg 323
Build The Rows Array:
The drawTable is the final function that makes this magic happen. But it only accepts a grid of cells. So at first we have to make our data a grid of cells. which we will call the rows. Our dataTable function will do the job. Let's take a look at dataTable function.
function dataTable(data) {
var keys = Object.keys(data[0]);
var headers = keys.map(function(name){
return new UnderlinedCell(new TextCell(name));
});
var body = data.map(function(row){
return keys.map(function(name){
return new TextCell(String(row[name]));
});
});
return [headers].concat(body);
}
console.log(drawTable(dataTable(MOUNTAINS)));
Let's break it down. At the last line our data "MOUNTAINS" have passed through the dataTable function and it accepts it as data.
The dataTable function creates 3 variable - keys, headers, and body. Let's find out what those variable will contain.
The keys variable:
The first variable keys contain -
var keys = Object.keys(data[0]);
The standard Object.keys function returns an array of property names in an object. by data[0] it will look only in the first element in our data. which is {name: "Everest", height: 8848} and the Object.keys will pull out their properties. So our keys variable will contain -
var keys = ["name", "height"];
The headers variable:
Step 1:
The second variable is headers.
var headers = keys.map(function(name){
return new UnderlinedCell(new TextCell(name));
});
The map method has been applied on our keys. The callback function will transform the two inner array and return them in a new array. The callback function will return this -
return new UnderlinedCell(new TextCell(name));
The each element of keys has been sent to the TextCell contractor and the result of it has been sent to the UnderlinedCell constructor. At first, the map method will look at "name" and this will be passed to the TextCell contractor. Like this - new TextCell("name");
Let's take a look at new TextCell contractor.
function TextCell(text) {
this.text = text.split("\n");
}
The TextCell contractor will create an Object and the split("\n") method with turns it into an array. So finally our object will look like this -
{text: ["name"]}
Step 2:
This result from the Step 1 will be sent to the UnderlinedCell constructor. Like this -
new UnderlinedCall({text: ["name"]});
Let's take a look at the new UnderlinedCell constructor.
function UnderlinedCell(inner) {
this.inner = inner;
};
The UnderlinedCell will create an object and it will look like this -
{inner: {text: ["name"]}}
Step 3:
The callback function of map will return {inner: {text: ["name"]}} on the first call and {inner: {text: ["height"]}} on the second call. So finally our header variable will contain -
var headers = [{inner: {text: ["name"]}}, {inner: {text: ["height"]}}]
The Body variable:
the body is the third variable and the hardest part of the dataTable function.
var body = data.map(function(row){
return keys.map(function(name){
return new TextCell(String(row[name]));
});
});
The map method has been applied to our data and the callback function return another 'map' method that has been applied to keys. It's like a loop into another loop.
Remember what is our data and keys -
// data
var MOUNTAINS = [
{name: "Everest", height: 8848},
{name: "vaalserberg", height: 323}
]
// keys
var keys = ["name", "height"];
Step 1:
at first, the outer map method will look at {name: "Everest", height: 5895} and the inner map will look at "name". So the inner map method's callback function will return this -
return new TextCell(String({name: "Everest", height: 5895}["name"]))
// Or
return new TextCell("Everest")
// and we will get from The TextCell constructor -
{text: ["Everest"]}
The inner map method will go over the keys twice. Second time the inner map method's callback function will return this -
return new TextCell(String({name: "Everest", height: 5895}["height"]))
// or
return new TextCell("5895")
// And we will get from The TextCell constructor -
{text: ["5895"]}
Step 2:
On the first call, the outer map will receive -
[{text: ["Everest"]},{text: ["5895"]}]
similarly, on the second call, the outer map will receive -
[{text: ["Everest"]},{text: ["5895"]}]
Finally, the body variable will contain -
var body = [[{text: ["Everest"]},{text: ["5895"]}], [{text: ["Vaalserberg"]},{text: ["323"]}]
Step 3 - Concat Headers And Body:
The dataTable function returns the headers and body by concatenating it.
return [headers].concat(body);
// our headers is -
var headers = [{inner: {text: ["name"]}}, {inner: {text: ["height"]}}]
// and body is -
var body = [[{text: ["Everest"]},{text: ["5895"]}], [{text: ["Vaalserberg"]},{text: ["323"]}]]
So finally dataTable function will return -
[[{inner: {text: ["name"]}}, {inner: {text: ["height"]}}], [{text: ["Everest"]},{text: ["5895"]}], [{text: ["Vaalserberg"]},{text: ["323"]}]]
This is our rows. Finally, this rows has been passed to the drawTable function.
Get The Heights:
If we look at the drawTable function we can see that at first, it defines variable heights, our rows have been passed through the rowHeights function and the heights will contain the result. Let's take a look at rowHeights function -
Step 1:
function rowHeights(rows) {
return rows.map(function(row){
return row.reduce(function(max, cell){
return Math.max(max, cell.minHeight());
}, 0);
});
}
In the rowHeights function map method has been applied to our rows and in the callback function reduce method has been applied to the each of the arrays of rows. So at first the map method will take a look at this as row -
[{inner: {text: ["name"]}}, {inner: {text: ["height"]}}]
and the reduce method will take a look at this as cell -
{inner: {text: ["name"]}}
Make sense?
Step 2:
reduce method's callback function will return the maximum value of height. cell.minHeight() method will be applied to each of our cells.
Let's take a look at cell.minHeight(), you have to keep in mind that "inner" object is being created by the UnderlinedCell constructor so cell.minHeight() refers to the UnderlinedCell prototypes.
UnderlinedCell.prototype.minHeight = function(){
return this.inner.minHeight() + 1;
};
// This will return -
{text: ["name"]}.minHeight() + 1
Step 3:
Again the "text" object is being created by the TextCell constructor so minHeight() will refer to the TextCell prototypes.
TextCell.prototype.minHeight = function(){
return this.text.length;
};
So finally it will return 1. because ["name"].length will give you 1. Don't make mistakes by thinking that this will return the length of the "name". The colWidths function will do this.
Step 4:
Get back to the Step 2. minHeight() function of UnderlinedCell prototype will return 2.
UnderlinedCell.prototype.minHeight = function(){
return 1 + 1;
};
similarly {inner: {text: ["height"]}}] will also return 2.
Step 5:
Again our rows are -
[[{inner: {text: ["name"]}}, {inner: {text: ["height"]}}], [{text: ["Everest"]},{text: ["5895"]}], [{text: ["Vaalserberg"]},{text: ["323"]}]]
it's an array of 3 arrays, so we can write it like this -
[ Array[2], Array[2], Array[2] ]
For the first array, Our map method will get - 2 . Similarly, for 2nd and 3rd array, our map method will get 1. So finally it will return [2,1,1].
But why the 2nd and the 3rd array will get 1 ?
well, if we take a closer look at our rows we can see that into the first array it has "inner" object (constructed by UnderlineCell) But the next 2 array don't have the "inner" object. So the next two array will not get through the UnderlineCell prototypes. We can see that minHeight() of UnderlineCell prototype adding 1 with the result.
Finally, our heights will be -
var heights = [2,1,1]
Get The Widths:
Step 1:
'widths' is the second variable that drawTable function has defined. it has passed the rows into the colWidths function. Let's take a look at colWidths function.
function colWidths(rows) {
return rows[0].map(function(_, i){
return rows.reduce(function(max, row) {
return Math.max(max, row[i].minWidth());
}, 0);
});
}
The colWidths function applied map method to the first row of rows. which is -
[{inner: {text: ["name"]}}, {inner: {text: ["height"]}}]
and the callback function of the map has applied reduce method to rows. We should be careful that the reduce method use the entire rows, not rows[0]. And the reduce method's callback function will return the maximum width. Let's see how.
Step 2:
On the first call, the map will look at the first index of rows[0] which is 0 (zero). and the second index is 1.
we can see from the arguments of the callback function that the value of rows[0] is not needed. And the reduce method will loop over the entire rows.
So on the first call, the reduce method will look up for -
[{inner: {text: ["name"]}}, {inner: {text: ["height"]}}]
// and it will return -
Math.max(max, [{inner: {text: ["name"]}}, {inner: {text: ["height"]}}][0].minWidth())
// or
Math.max(max, {inner: {text: ["name"]}}.minWidth())
Let's take a look at minWidth() and find out what it returns.
Step 3:
Again first it will refer to the UnderlineCell prototypes and then TextCell prototypes.
UnderlinedCell.prototype.minWidth = function(){
return this.inner.minWidth();
};
// So it will look at this -
{text: ["name"]}}.minWidth()
Step 4:
And then the minWidth() of TextCell prototype will be applied -
TextCell.prototype.minWidth = function() {
return this.text.reduce(function(width, line) {
return Math.max(width, line.length);
}, 0);
};
minWidth() of TextCell will apply a reduce method to the each of the element in that array. Currently, we have only one element in our array ["name"]. But in some situation, it may have multiple elements. The reduce method will return widest characterized word from this array. In this case, it will return 4. Because ["name"].length = 4.
Step 5:
Get back to the Step 2. For the first call to rows the reduce method receive 4 similarly for the 2nd and 3rd call it will receive 7 and 11. And it will return the maximum number to the map which is 11.
Similarly, for the second call of the map, it will look at the second index of row[0], which is 1. And through the process, it will receive the maximum number which is 6. So finally our widths variable will contain [11,6].
var widths = [11,6]
Draw The Rows:
If we take a close look at what the drawTable function returns we can see this -
return rows.map(drawRow).join("\n");
The map method has been applied to the rows and the callback function is drawRow which is defined into the drawTable function. Let's take a look at drawRow.
function drawRow(row, rowNum) {
var blocks = row.map(function(cell, colNum){
return cell.draw(widths[colNum], heights[rowNum]);
});
return blocks[0].map(function(_,lineNo){
return drawLine(blocks, lineNo);
}).join("\n");
}
The drawRow function receives two arguments from rows. First is the value and second are the index number. At first, the drawRow has defined a variable blocks.
The blocks variable:
In the 'blocks' map method has been applied to the row. Here map method also receives two parameter - value and index number.
var blocks = row.map(function(cell, colNum){
return cell.draw(widths[colNum], heights[rowNum]);
});
Step 1:
So for the first call, the row will be -
[{inner: {text: ["name"]}}, {inner: {text: ["height"]}}] // and rowNum = 0
And for the first call, the cell will be -
{inner: {text: ["name"]}}
this will return from the first call -
return cell.draw(widths[colNum], heights[rowNum]);
// or
return cell.draw([11,6][0], [2,1,1][0]);
// or
return cell.draw(11,2);
// or
return {inner: {text: ["name"]}}.draw(11,2);
Step 2:
Let's take a look at draw function. Again the object "inner" has been constructed by the UnderlineCell constructor so the draw function will refer to the UnderlineCell's prototype.
UnderlinedCell.prototype.draw = function(width, height){
return this.inner.draw(width, height - 1)
.concat([repeat("-", width)]);
};
// or
UnderlinedCell.prototype.draw = function(width, height){
return {text: ["name"]}.draw(11, 1) // height - 1 = 2 -1 =1
.concat([repeat("-", width)]);
};
Again the draw function has been applied to the object but this time the draw function will refer to the TextCell's prototypes. The result that return from the draw function (by TextCell Prototype) it's concatenated ([repeat("-", width)]) with it. We will get to this point a little bit later. Let's take a look at TextCell's draw function.
Step 3:
TextCell.prototype.draw = function(width, height) {
var result = [];
for (var i = 0; i < height; i++) {
var line = this.text[i] || "";
result.push(line + repeat(" ", width - line.length));
}
return result;
}
We can see that there is a for loop into the function. As our height is 1 this for loop will execute only once. And there is a variable line which will contain either the text object content or "". In this case, the line variable will contain -
var line = this.text[0]
// or
var line = "name";
before pushing the line into result it will call the helper function repeat. Let's take a look at repeat function.
Step 4:
function repeat(string, times) {
var result = "";
for (var i = 0; i < times; i++)
result += string;
return result;
}
The repeat function will receive two parameter
repeat(" ", width - line.length)
// or
repeat(" ", 11 - 4) // width = 11, "name".length = 4
// or
repeat(" ", 7)
from the repeat function's result, we can see that it will return 7 empty space.
Step 5:
Back to the step 3. It will add 7 empty space to the line and then pushed it to the result. so on step 3 draw function will return ["name "].
On step 2 there is a 'concat' method has been applied to the result that comes from step 3 like this -
["name "].concat([repeat("-", width)]);
similarly, the repeat will create 11 "-" character and it will add to the ["name "] like this -
["name ", "-----------"]
This was the first call to row in map method. On the second call similarly it will receive this -
["height ", "------"]
So the blocks variable will contain -
var blocks = [["name ", "-----------"], ["height ", "------"]]
Similarly, on the 2nd and 3rd call of the rows in map, the blocks variable will contain something like this -
var blocks = [["Everest "],["8848"]]
var blocks = [["Vaalserberg"],["323 "]]
The empty spaces may not be accurate that I have written to explain the code
Draw The Line:
Step 1:
return blocks[0].map(function(_,lineNo){
return drawLine(blocks, lineNo);
}).join("\n");
We can see map method has been applied to the first element of blocks. On the first call, our blocks variable will contain -
var blocks = [["name ", "-----------"], ["height ", "------"]]
// and block[0] will be -
["name ", "-----------"]
The callback function will return drawLine function and pass two arguments - the entire blocks and the lineNo which is 0 for now. It's looks like this -
drawLine( [["name ", "-----------"], ["height ", "------"]], 0)
Let's take a look at drawLine function.
Step 2:
function drawLine(blocks, lineNo) {
return blocks.map(function(block){
return block[lineNo];
}).join(" ");
}
map method has been applied to the blocks. The blocks containing 2 arrays. so at first the map will look at -
["name ", "-----------"]
// and the callback function will return -
["name ", "-----------"][0]
// or
["name "]
similarly, for the second array, it will return ["height "]. So finally the drawLine function will return -
[["name "], ["height "]].join(" ")
// or
"name height "
Step 3:
get back to the step 1. On the 2nd call of the map, the line number will be 1. So from the drawLine function, we will receive -
"----------- ------" // we will recieve this pattern
// So finally Step 1 will return -
["name height ","----------- ------"]
// And finally, the drawTable function will return -
["name height ","----------- ------"].join("\n")
//or
name height
----------- ------
Amazing ! we get our first nicely laid out table header.
Previously we saw that our drawTable function return this -
return rows.map(drawRow).join("\n");
For the first row we will receive our first nicely laid out table header and for the 2nd and 3rd row similarly we will receive the rest of our nicely laid out table content.
Conclusion
This problem is very difficult for the beginners. I am also a beginner in programming and javaScript. I know that most of my explanation may look irrelevant to the readers. Your feedback will help me to improve it. So please don't hesitate to make comments and ask questions.
This comment has been removed by the author.
ReplyDeleteDear Parish,
ReplyDeleteThank you very much for your explanation. It has helped me a lot in understanding chapter 6 of eloquent javascript, as I am new to javascript as well.
Best Regards,
Ioannis
You're welcome Ioannis! Thanks for the comment.
DeleteHi Parish,
ReplyDeleteThank You for the guidance on this example! I'm relatively new to programming and without this to get through the #6Ch. in the Eloquent JS book, would have been really difficult and time-consuming. It was also good to hear I was not the only one who experienced problems with this task.
Norbert
Thanks for your comment. Eloquent JS is one of the best book to learn programming and javascript. But some chapter is so much complicated. I am still reading this book and i had to skip some project chapter because it was very difficult and time-consuming.
DeleteHi Parish,
ReplyDeleteI wondered if JS was indeed for me when all my attempts at trying to understand this example failed; you reinstated my confidence with your wonderful exposition. Please do more of your 'simplification' posts - you'll make learners like me really happy
Best
Srini
you are my hero .
ReplyDeleteyou saved my time
Thanks Man.
DeleteThanks a lot Parish , for such a wonderful and step by step explanation .
ReplyDeleteYou are welcome Manish
DeleteThis comment has been removed by the author.
ReplyDeleteHi Parish,
ReplyDeleteVery helpful information . Thanks . I think on the body variable section 2 there is a correction
"similarly, on the second call, the outer map will receive -
[{text: ["Everest"]},{text: ["5895"]}]
Actually,
//i think [{text: ["Vaalserberg"]},{text: ["323"]} object should be mentioned instead of {text: ["Everest"]},{text: ["5895"]} object .
Smm Panel
ReplyDeletesmm panel
iş ilanları
İnstagram Takipçi Satın Al
HIRDAVATÇI BURADA
beyazesyateknikservisi.com.tr
servis
tiktok jeton hilesi
özel ambulans
ReplyDeletelisans satın al
nft nasıl alınır
minecraft premium
en son çıkan perde modelleri
en son çıkan perde modelleri
yurtdışı kargo
uc satın al