The other day I stumbled on a piece of code that really threw me for a loop. It had to do with constructing a data structure that included positioning data for use in a grid layout.
function buildCells() { return [ { key: 'foo', label: 'Foo', value: 'This was a foo value', position: [0, 0], }, { key: 'bar', label: 'Bar', value: 'This was a bar value', position: [0, 1], }, { key: 'baz', label: 'Baz', value: 'This was a baz value', position: [1, 1], }, ]; }
Which would cause the rendering engine to position boxes for each entry at the coordinates outlined by the position values.
The trouble came in when that structure was dynamic. At first the idea would be to save the basic array to a variable then mutate it as conditions came to light.
function buildCells() { let result = [ { key: 'foo', label: 'Foo', value: 'This was a foo value', position: [0, 0] }, { key: 'bar', label: 'Bar', value: 'This was a bar value', position: [0, 1] }, { key: 'baz', label: 'Baz', value: 'This was a baz value', position: [1, 1] } ]; if (condition1) { result.splice(0, 1); } if (condition2) { result.splice(1, 1); result[0].position = [0, 0]; } }
And if I am honest I got very confused very quickly. I couldn’t tell which slot each cell was going to be in or why.
I did what I usually do and broke it down into parts. In these kinds of builders sometimes I find using generator functions helps because the control flow is explicit.
function *buildCells() { if (condition1) { yield { key: 'foo', label: 'Foo', value: 'This was a foo value', position: [0, 0] }; } yield { key: 'bar', label: 'Bar', value: 'This was a bar value', position: condition2 ? [0, 0] : [0, 1], }; yield { key: 'baz', label: 'Baz', value: 'This was a baz value', position: [1, 1] }; }
But to be frank that just felt like I was shuffling dirt around and not really cleaning it up. What I really wanted was a way to express the actual expected cell positions in a way that was easy to see in the code.
Then it hit me, what if I had a syntax that could visually demonstrate what I really needed.
function buildCells() { if (condition1) { return Array.from(buildLayout(cellsByKeyword, ` ... bar ... baz `)); } else if (condition2) { return Array.from(buildLayout(cellsByKeyword, ` bar ... ... baz `)); } else { return Array.from(buildLayout(cellsByKeyword, ` foo bar ... baz `)); } }
Something like that would be far easier to reason about. I was afraid that something like that would be to complex but it turns out the algorithm isn’t all that bad.
The setup is to move the data into a lookup table.
const cellsByKeyword = { foo: { label: 'Foo', value: 'This was a foo value', }, bar: { label: 'Bar', value: 'This was a bar value', }, baz: { label: 'Baz', value: 'This was a baz value', } };
Then the algorithm:
function *buildLayout(cellsMap, layout) { let rows = layout.trim().split('\n'); for (let row = 0; row < rows.length; row++) { let columns = rows[row].trim().split(/\s+/); for (let column = 0; column < columns.length; column++) { let key = columns[column]; let cell = cellsMap[key]; if (cell) yield { ...cell, position: [row, column] }; } } }