Converting a List to a Table
In my last post, I mentioned the creation of BulletsToTable.com which arose because I had a need for converting a list to a table, but did not find that functionality in any software I used (OneNote, Word, Outlook). Today, we'll look at the problem itself:
Given a bullet list where indents (or levels of the list) show hierarchy, how do you convert it to a table?
We won't go into the HTML or JavaScript behind the logic. If you're interesting in that, feel free to visit the project's GitHub page
Step 1: Define the output table
It's tempting to think about the solution starting from the problem you're given, but it's usually helpful to think backwards from the solution. What does the final output look like? It's a table, obviously, but what size is it? Say it's m rows x n columns, what are m and n?
Columns first: Since we want to put each level of the list in a new column of the table, this one is easy. It's just the number of different levels in the list. While it is theoretically possible for a list to skip a level (when someone has indented twice), we'll assume that if this happens it is on purpose, and include that skipped level as a column. So the number of columns:
# of columns n = level of max indent - level of min indent + 1
We add 1 at the end to include both levels at the end, so if the levels are (1, 2, 3), you get n = 3.
Rows: Not as straightforward, but let's take a moment to consider. If you have a list to put in a table, what exactly separates one row from another? Working our way down the list, we can see that every time we have an entry in the last level of the list (i.e. at the largest indent) we want to put that in a new row.
So, our rule for the number of rows m is "number of entries at the max indent level". That's a good start, but consider this list:
If we apply our rule, m is just 1, which clearly isn't the case – so how can we improve our rule?
One way is to work down the list, and every time the next entry is at the same or lower level than the current one, we count it as a new row. However, when the next entry is at a higher level, we keep it in the same row but in a new column.
# of rows m = # of elements where the next element is at the same or lower level + 1
We add 1, since we can't do this comparison when we arrive at the last line of the list (there are no more lines to compare to), but the last line obviously occupies its own row.
So now we have the size defined for the output table. Some of these cells will need to be merged down across rows in some cases, but that comes later.
Step 2: Prepare the input
We've actually done the thinking for this step already when we created the rule for separating rows of the table. By applying the same logic to the input list, we can split it into rows. No further processing is needed at this step.
Step 3: Defining the source for each table cell
Before we can map the list's entries to cells in the table, it's important to define how we're filling each cell. There are more cells in the table than entries in the list, so not every cell will get its own entry.
First, we can create a version of the table that tells you which cells have entries defined.
Then, we refine this table to say where each entry gets sourced from. If there was a FALSE in the previous table, then the cell is either empty, or would be filled from an entry above. If there's an entry above this FALSE cell, then it gets filled from above – if not, then it's an empty cell (which only happens in the last column).
This 'sourcing table' tells us all we need to complete our output table.
Step 4: Fill in the table
Finally! The last step involves going through the table cell by cell, and filling it based on the 'sourcing table' we generated in step 2.
We simply go through the cells and if the sourcing table says "TRUE", we pick out the appropriate row's appropriate entry. When filling an entry, we do a check in the cells below to see if they have "fill from above". If so, we merge them together by using a HTML rowspan attribute. An "empty cell" is left empty.
And that's it! Put that logic in JavaScript and you get an obvious feature that Microsoft forgot about...