Structured references in Excel

While typing some formula, we can click on spreadsheet cell to get its reference. If the cell is inside declared table, our reference will be structured reference.

Structured reference works by cutting horizontal and vertical slices of a table, and then it returns intersection of those slices.

There is limited number of ways how we can slice table horizontally. Let's ignore vertical slicing for now, and we will cut the whole table only horizontally. We can use 5 specifiers to get what we want. We type the reference as name of table + specifier (like in cell H1 bellow).

-[#Headers] returns table header (A).
-[@] returns current row (B).
-[#Data] returns table body (C).
-[#Totals] returns Total row (D).
-[#All] returns the whole table (E).
So, when we combine name of a Table with some of specifiers, we can get horizontal slices of a table.
One special specifier is "[@]". This specifier means that we'll get data from a row where our formula is placed. If our formula is in cell H5, then we are going to get data from cells C5:F5 in the table.

Vertical slicing can be done in two ways. We can slice only one column, and we can slice several consecutive columns.

(1) is how to reference only one column.
(2) is how to reference several consecutive columns.
We can notice (3) that we don't get whole columns. We only get [#Data] part of columns. This is because [#Data] horizontal slicing is the default. If we want some other horizontal part of table, then we have to combine horizontal and vertical specifiers.

Combining specifiers

Now we have to use two specifiers. First we write horizontal specifier, and then the vertical one. Yellow example bellow, returns whole "Units" column because horizontal specifier is [#All]. Blue example returns headers for two consecutive columns because horizontal specifier is [#Headers]. Red example is different. It uses two horizontal specifiers. This way we can only use combinations ( [#Data],[#Headers] ) and ( [#Data],[#Totals] ). Red example returns "Total" column without its header. Green example assumes that formula is in the sixth row. That is why it returns values for columns "Region" and "Rep" in sixth row.

=SampleTable[[#All],[Units]]
=SampleTable[[#Headers],[Region]:[Rep]]
=SampleTable[[#Data],[#Totals],[Total]]
=SampleTable[@[Region]:[Rep]]

It is not possible to combine [#All] specifier, or [@] specifier with some other. They only goes alone.
Specifier for "current row" is not separated with coma, there is no coma between "@" and name of column. If we want current value for only one column we type "=SampleTable[@ColumnName]".

Choose function

Choose function can help us to solve two problems. First is, that we want to reference several not consecutive columns from the table. Let's say that we want to reference columns "Region" and "Total", but we don't want their [#Totals]. We can use this formula:

=CHOOSE({1,2},SampleTable[[#Headers],[#Data],[Region]], SampleTable[[#Headers],[#Data],[Total]] )

If we want to wrap two columns into one aggregate function, we don't need Choose function, we can directly type columns references as arguments into function. Function bellow will return 2339,36 ( 264 + 2075,36 ).

=SUM(SampleTable[Units],SampleTable[Total])

Other problem is that we can not combine [#Headers] and [#Totals]. Solution is again to use Choose function. Notice that this time "{1;2}" argument is using semicolon, and not coma.

=CHOOSE({1;2},SampleTable[#Headers],SampleTable[#Totals])

Relativity of structured references

If we copy formula with structured reference to some other cell (1), it will not adapt. We can see in cells A10:D10, in image bellow, that all cells have the same value. Sometimes this behavior is desired, but sometimes is not.

Solution is to drag this formula, instead of copying it. We can see in cells A11:D11 that now every cell has value which corresponds to its position.

This is valid only for individual columns. Any structured reference that has range of columns in it, will not adapt, it will always be absolute. If we type our formula like a range of one column, that formula will not change its result even if we drag it:

=SampleTable[[#Headers],[Region]:[Region]]

Things to consider about structured references

Structured references are easy to read, but not to type. Usually we just click on some table cell and Excel creates structured reference for us. If this is not what we want, it is possible to disable automatic creation of structured references. This is done by unchecking option in Excel Options > Formulas > Working with formulas > Use table names in formulas. VBA version of this would be:

Application.GenerateTableRefs = xlGenerateTableRefA1   'to turn automatic creation OFF
Application.GenerateTableRefs = xlGenerateTableRefStruct  'to turn automatic creation ON

Structured references can be used inside VBA. Here is one example:

Worksheets(1).Range("Table1[#All]")

Declared table doesn't need to have headers and total row. They can be disabled in Table Design > Table Style Options, in Excel ribbon. After this, formulas which refer to [#Headers] or [#Totals] will not work.

Column names can be qualified and unqualified. Unqualified names can only be used inside tables. This means that instead of writing SampleTable[Region] we can just type [Region]. This will not work outside of table which has "Region" column.

In ribbon, there is option to convert declared table into ordinary range. That option is in Table Design > Tools > Convert to range. After this, all structured references for that table will be transformed into regular A1 references.

For special signs [ ] # ' we have to include escape sign – single quotation mark (').

=DeptSalesFYSummary['#OfItems]

File with examples can be downloaded here:

Splitter functions in Power Query

Splitter functions are used as arguments in Table.SplitColumn function. Table.SplitColumn function is used to split column vertically into more columns (1 => 2). Here is the syntax and example of this function.

Table.SplitColumn( 
  table
, sourceColumn 
, splitter function 
, optional Names|Number
, optional default
, optional extraValues
) 
Table.SplitColumn( 
  TableWithColumnToSplit
, "ColumnToSplit"
, Splitter.SplitTextByDelimiter(",")
, {"A","B","C","D"}
, null
, ExtraValues.Ignore
)
  • table – table where is the column which we are going to split.
  • sourceColumn – column that will be splitted.
  • splitter function – function that explains how to split the column.
  • Names | Number – names of new columns.
  • default – value to place in cells which would be empty because we have more columns then values.
  • extraColumns – if we have the opposite situation, number of values is bigger than the number of columns, then this argument decide what to do with extra values.

This is how we give names to new columns. If we have too many column names, extra columns will be filled with default value.

We can name columns by setting some number as fourth argument. In that case, names of columns will be generated as name of initial column + index number ( ColumnToSplit + 1,2,3 ). In the example bellow we are using number three, so there are going to be only 3 columns as a result.

If we don't have enough column names for all the columns, those columns will be missing, but only if we use "ExtraValues.Ignore" (1) as the last argument. There are two more possibilities. We can use "ExtraValues.List" (2), so last column will held all surplus values as a list. Last possibility is to raise an error. This will happen when we use "ExtraValues.Error" (3). "ExtraValues.Ignore" is default value.

Third argument is what interests us the most. It contains function that explains how to vertically slice origin column.

  • Splitter.SplitByNothing
  • Splitter.SplitTextByAnyDelimiter
  • Splitter.SplitTextByDelimiter
  • Splitter.SplitTextByEachDelimiter
  • Splitter.SplitTextByLengths
  • Splitter.SplitTextByPositions
  • Splitter.SplitTextByRanges
  • Splitter.SplitTextByRepeatedLengths
  • Splitter.SplitTextByWhiteSpace
  • Splitter.SplitTextByCharacterTransition

Splitter.SplitByNothing

"SplitByNothing" will not split origin column. It will only add new columns filled with default values.

Table.SplitColumn( TableWithColumnToSplit, "ColumnToSplit", Splitter.SplitByNothing(),{"A","B","C","D"},"default",ExtraValues.Error )
ColumnToSplitABCD
Row1A,B,C,D=>A,B,C,Ddefultdefultdefult
Row2A,B=>A,Bdefultdefultdefult

If names of new columns and default value are not defined (because they are optional), then two things would still change. Name of column would change, and type of column would become "ABC 123".

Splitter.SplitTextByDelimiter

"Splitter.SplitTextByDelimiter" takes one argument. That argument is delimiter by which text will be splitted.

Table.SplitColumn( TableWithColumnToSplit, "ColumnToSplit", Splitter.SplitTextByDelimiter(","), { "A", "B", "C" }, "value" )

Delimiter can have more than one character. Also, we can see what will happen when delimiter is at start of the text.

Table.SplitColumn( TableWithColumnToSplit, "ColumnToSplit", Splitter.SplitTextByDelimiter("A,"), { "A", "B", "C" }, "value" )

"SplitTextByDelimiter" accepts second optional argument. Default value for this argument is QuoteStyle.None. Difference is made when we use QuoteStyle.Csv. In that case all individual quotes will be removed ("), and double quotes will be transformed into individual quotes ("" => "). So, instead "O""A","B" we will have O"A.

Table.SplitColumn( TableWithColumnToSplit, "ColumnToSplit", Splitter.SplitTextByDelimiter( ",", QuoteStyle.Csv), { "A", "B", "C" }, "value" )

Splitter.SplitTextByAnyDelimiter

Anytime we find any of the delimiters from the list in the text, we will split text. In example bellow, anytime we meet coma or semicolon, we will split the text. In table we can, also, see what will happen if there are two delimiters side by side. Two consecutive delimiters will define one empty cell. "SplitTextByAnyDelimiter" also accepts "QuoteStyle" argument.

Table.SplitColumn( TableWithColumnToSplit, "ColumnToSplit", Splitter.SplitTextByAnyDelimiter( { ",", ";"},QuoteStyle.None), { "A", "B", "C", "D" }, "value" )
ColumnToSplitABCD
Row1A;B:C;D,E=>AB:CDE
Row2"O""A",,"B"=> "O""A" "B"value

Splitter.SplitTextByEachDelimiter

"SplitTextByEachDelimiter" is different from "SplitTextByAnyDelimiter" because now, we will split text consecutively in the same order delimiters have in their list. In our example, first we look for coma, then for colon, and then for coma again. So, if we have three delimiters in our list, we can make maximally three splits, and only if all three delimiters exist in text in proper order.

Table.SplitColumn( TableWithColumnToSplit, "ColumnToSplit", Splitter.SplitTextByEachDelimiter( { ",",":", ","},QuoteStyle.None), { "A", "B", "C", "D" }, "value" )
ColumnToSplitABCD
Row1A,B,C:D,E;F=>AB,CDE;F
Row2A:B;C:D,E,F=>A:B;C:DE,Fvaluevalue

In second row, in table above, we can see that we only made one split. We split on the first coma, but we couldn't split on semicolon because there were no semicolons after first coma.

Splitter.SplitTextByLengths

This time, splitting is not done by delimiters but with consecutive lengths. The last number in the list is three, that is why our "D" column has value "DDD", and not "DDDD" with 4 D's. If this number was 4 (or 5 or 6…) then we would see all four D's.

Table.SplitColumn( TableWithColumnToSplit, "ColumnToSplit", Splitter.SplitTextByLengths( { 2, 3, 2, 3 } ), { "A", "B", "C", "D" }, "default" )
ColumnToSplitABCD
Row1AABBBCCDDDD=>AABBBCCDDD

If we had longer list, for example { 2, 3, 2, 3, 8, 4, 2 }, nothing would change. But, if we had smaller list, for example { 2, 3 }, then last two columns would be filled with default values.

ColumnToSplitABCD
Row1AABBBCCDDDD=>AABBBdefaultdefault

Splitter.SplitTextByPositions

Positions are positive numbers and every position can not be smaller than previous position. This positions define places where text would be splitted. In our examples we can see positions 011 as "0A1A2B3B4B5C6C7D8D9D10D11".

Table.SplitColumn( TableWithColumnToSplit, "ColumnToSplit", Splitter.SplitTextByPositions( { 0, 2, 5, 7, 9 } ), { "A", "B", "C", "D" }, "default" )
PositionsColumnToSplitABCDWhere we split:
{0,2,5,7,9}AABBBCCDDDD=>AABBBCCDD0A1A2B3B4B5C6C7D8D9D10D11
{0,2,5}AABBBCCDDDD=>AABBBCCDDDDdefault0A1A2B3B4B5C6C7D8D9D10D11
{0,2,2,5}AABBBCCDDDD=>AABBBCCDDDD0A1A22B3B4B5C6C7D8D9D10D11

In first row we have position "9" which limits value in column "D" to only two D's.
In second row, we only have three positions, so the text is splitted to only three columns.
In third row, we have two same consecutive positions. This will create empty cell in column "B".

Splitter.SplitTextByRanges

In Excel, when we use function "=MID( A1; 5; 3)", we first jump to fifth character and then we take that and next two characters. This is how "SplitTextByRanges" work. Function takes argument that looks like { { 0, 2 } ,{ 3, 1 } } . Each pair of values defines position and length, the same way as MID function. In table bellow, we will see how to split text AABBBCCDDDD.

Table.SplitColumn( TableWithColumnToSplit, "ColumnToSplit", Splitter.SplitTextByRanges( { { 0, 2 }, { 2, 3 }, { 4, 4 } } ), { "A", "B", "C", "D" }, "default" )
RangesColumnToSplitABCD
{ { 0, 2 }, { 2, 3 }, { 4, 4 } }AABBBCCDDDD =>AABBBBCCDdefault

Each pair defines one substring from initial column. Substrings can overlap. We can see that black "B" in ColumnToSplit belongs both to "B" and "C" column.

Splitter.SplitTextByRepeatedLengths

This function is similar to "SplitTextByLengths", except all lenths are of the same lenght. Here, we want to split text into groups of three characters. We can notice that last column has only two characters.

Table.SplitColumn( TableWithColumnToSplit, "ColumnToSplit", Splitter.SplitTextByRepeatedLengths( 3 ), { "A", "B", "C", "D" }, "default" )

Splitter.SplitTextByWhiteSpace

SplitTextByWhiteSpace is the same as SplitTextByDelimiter when delimiter is space.

Table.SplitColumn( TableWithColumnToSplit, "ColumnToSplit", Splitter.SplitTextByWhitespace(), { "A", "B", "C", "D" }, "default" )
Table.SplitColumn( TableWithColumnToSplit, "ColumnToSplit", Splitter.SplitTextByDelimiter(" "), { "A", "B", "C" }, "value" )

Splitter.SplitTextByCharacterTransition

This function accepts two arguments. Both arguments can be lists of characters. If character from first list stands before some character from the second list, then text will split between those two characters.

Table.SplitColumn( TableWithColumnToSplit, "ColumnToSplit", Splitter.SplitTextByCharacterTransition( { "A", "B" }, { "B", "C" } ), { "A", "B", "C", "D", "E" }, "default" )
ColumnToSplitWhere to splitABCDE
AABBBCCDDDD AABBBCCDDDDAABBBCCDDDD

In the table above, we can see that we made a split anywhere between A and B, B and B and B and C.

We can also use functions which decide, are two consecutive characters adequate to be split between. In example bellow we have two functions. First function returns true if character is before or on "B", second function decides whether character is after "B". So this logic says that if first character is A or B, and if second character is any other letter, then split text between them (we will observe only upper letters).

Table.SplitColumn( TableWithColumnToSplit, "ColumnToSplit", Splitter.SplitTextByCharacterTransition( each if _ <= "B" then true else false, each if _ > "B" then true else false ), { "A", "B", "C" }, "default" )
ColumnToSplitWhere to splitABC
AABBBCCDDAABBBCCDD=>AABBBCCDDdefault
BBAFFFBBAFFF=>BBAFFFdefault
AAAGGGBBFFAAAGGGBBFF=>AAAGGGBBFF

In table above, we can see that we made a split anywhere between B and C, A and F, A and G and B and F.

Sample file can be downloaded here:

Export all formulas from Power BI desktop

There are three kinds of formulas in Power BI desktop. We can create measures, calculated columns or calculated tables. Let's say we want to find all places where one of our columns is mentioned in formulas. For that to happen, we can export all formulas and than we can use Search to find all formulas with our column mentioned.

We'll use Tabular Editor program to extract all formulas from Power BI file. Tabular Editor can be downloaded from here:
https://github.com/TabularEditor/TabularEditor/releases/tag/2.16.1
After installing Tabular Editor, we should open it.

In order to be able to get all formulas we need to check option (1). This option is located in File > Preferences > Features in Tabular Editor. Next step is to attach to the PBID from the Tabular Editor. Go to File> Open> From DB (2). In the "local instance" we select our PBID file (3).  PBID file has to be already opened.

Tabular Editor will now look like this. In the left pane we will see the tables and columns from our Power BI file (1). Advanced Scripting (2) is the area where we will enter the code. After entering the code in (3), we will start the code by clicking on the green arrow (4).   

These are the building blocks of code we are going to use. First we need to declare file where our formulas will be saved. Part "new System.Text.UTF8Encoding (true))" is important. It adds BOM mark to our file. This mark helps programs understand what encoding the file is using. We will afterwards open this file in Excel. If our file contains unusual characters, like Č, Ž, Á, Ж; Excel will properly read and present such characters.
using System.IO;
var file = @"C:\Users\Sima\Desktop\A.csv";      //file where to export column names

using(var fileWriter = new StreamWriter(file,false, new System.Text.UTF8Encoding (true))  )

We will loop through all of Columns and if column is calculated we will print in our file its type, name of a column and formula that is used to create column. We will remove all line breaks from formula. Semicolon sign ";" is used as delimiter in final CSV file.

foreach(var Col in Model.AllColumns)
    if ( Convert.ToString( Col.Type ) == "Calculated" )
        {
            fileWriter.Write( "CalculatedColumn" + ";" + Col.Name + ";" + (Col as CalculatedColumn).Replace("\n", "") +"\n" );
        }

Next, we will loop through all tables, and through all of their Partitions. In this context, Partition is part of calculated tables that is created by table creating formula. Again, we will write to our file type of table, its name and its expression.

foreach(var Tab in Model.Tables)
foreach(var Part in Tab.Partitions )
        if ( Convert.ToString( Part.SourceType ) == "Calculated" )
             {
                     fileWriter.Write( "CalculatedTable" + ";" + Tab.Name + ";" + Part.Expression.Replace("\n", "") +"\n" );
            }

Last part of puzzle is similar. It is used to extract formulas for measures. When we combine all building blocks we'll get our final and complete code:

using System.IO;
var file = @"C:\Users\Sima\Desktop\A.csv";      //file where to export column names

using(var fileWriter = new StreamWriter(file,false,new System.Text.UTF8Encoding(true)) )
{
    foreach(var Col in Model.AllColumns)
        if ( Convert.ToString( Col.Type ) == "Calculated" )
            {
                fileWriter.Write( "CalculatedColumn" + ";" + Col.Name + ";" + (Col as CalculatedColumn).Expression.Replace("\n", "")  +"\n" );
            }

    foreach(var Tab in Model.Tables)
    foreach(var Part in Tab.Partitions )
        if ( Convert.ToString( Part.SourceType ) == "Calculated" )
            {
                fileWriter.Write( "CalculatedTable" + ";" + Tab.Name + ";" + Part.Expression.Replace("\n", "")  +"\n" );
            }


    foreach(var M in Model.AllMeasures)
            {
                fileWriter.Write( "Measure" + ";" + M.Name + ";" + M.Expression.Replace("\n", "")  +"\n" );
            }
}

After we run our code, we can open our CSV file in Excel. This is how it will looks like:

Here you can download sample PBIX file to test code.

SPSS data entry

We will see here how to manually enter data into SPSS, or automatically from Excel or from SQL Server. When we open SPSS, we can see Data View (1) and Variable View (2). Data View shows data and is like Excel spreadsheet table. Variable view is used to declare that "Data View" table. There we can declare columns, their content, formatting and possible values.

Manual entry

To enter data manually, it is enough to start typing in the cells in Data View. As we see, names of columns will be created automatically and we have to change them, together with other columns attributes.

Before explaining columns attributes let's recall of different measurement scales that are used in SPSS:
– Nominal scale is used for categorical data ( "man/woman/child" or "India/Japan/China" ).
– Ordinal scale is used for ordered data ( "good/neutral/bad" or "before/during/after" ).
– "Scale" is used in SPSS to label data that can be measured with some measuring unit ( height, weight, temperature ).

Data that is measured in Nominal and Ordinal scale has to be enter in SPSS as codes. This is is necessary so we can use all available statistical tools in SPSS. Coded means that each category has to be presented by number. For example "small, medium, big" can be presented with codes "1,2,3". Those codes are values that we enter into the program (1). Then, in the program itself, we assign one of the categories labels to each code. If we want, we can show those categories labels to user instead of codes (2).

By clicking on this button in the main toolbar, user can switch between the two views from the image above.

Declaration of code is done in "Variable View". Let's see what options are available in Variable View.

Variable View

"Variable View" is place where we enter columns attributes.
– In "Name" (1) we type correct name of a column. Name can have characters, numbers and underscores.
– In "Type" (2) we open new dialog (5) to choose between different data types. As we saw, because all categorical data should be coded, almost all of our columns should be declared as "Numeric".
– Width (3) is to limit how many characters can textual data has. Textual data longer than this will be truncated.
– Decimals (4) will limit number of decimal places presented in "Data View". This is just for visual representation. Real calculations will be conducted with all available decimal figures.

– In "Label" (1) we place short descriptions of our columns.
– In "Values" (2) we can set labels for data that is categorical in nature. This will open new dialog (5). So, if possible codes in column are "1, 2, 3" then we have to attribute label to each code. Our codes "1,2,3" can represent "Man, Woman ,Child". By clicking on button in toolbar, as explained earlier, user will be able to see those labels instead of incomprehensible codes.
– In "Missing" (3) we can determine values that are impossible or unacceptable. After we enter data, every value that is the same as those registered here, will be excluded from calculations as incorrect value. Such values will not be part of statistical calculations, SPSS will just ignore them. We can give three such discrete values (6). Other option is to give one interval and one discrete value (7).
– "Columns" (4) is visual width of column, measured in numbers of characters. We can also change width of columns with mouse on the same way as in Excel (8).

Last 3 column attributes are Align, Measure and Role. In align we can choose between Left, Right and Center alignment (1). Measure (2) is used to declare scale for data. This will not influence SPSS calculations but it is important to declare scale of data for other users of that data. In Role (3) we can just leave the default value ( "Input" ).

This process of declaring our columns should be done for data loaded from Excel or database, too.

Loading from Excel

File > Open > Data (1) in the main menu is option to open dialog (2). Dialog (2) needs from us to choose Excel type of files, folder where Excel file is, and concrete Excel file. After clicking "Open" in dialog (2) we will got dialog (3). There we choose one of the Sheets in the workbook and range of our data. If we don't supply range, automatically determined range will be used ( "A1:G44" on image ). After this our data will be loaded and we can see it in "Data View" (4).

Loading from Database

For getting data out of database, "IBM Data Access Pack" can be installed. This is IBM collection of drivers for different databases we can use. We don't have to use IBM drivers, but they will probably work the best, if we want to transfer data to SPSS. We start loading process by clicking on File > Open Database > New Query (1). Then we click button "Add ODBC Data Source" (2). In new dialog, in "User DSN" tab we should click on "Add" button (3).

"IBM Data Access Pack" will add many ODBC drivers whose names start with "IBM SPSS OEM" (1). We will choose "SQL Server Native Wire Protocol". In next screen we'll add credentials for our database (2). Now, we will close everything until we get back to our start screen (3), so we can click "Next" button.

Now we can select some table and its columns (1 => 2) and click on Finish. Those columns will be now presented into Data View (3).

Instead of Finish we can also use Next buttons to follow whole graphical wizard. This wizard will provide us with opportunity to define relations between tables, to filter data and to rename columns. This is all great, but the last screen is where we will be able to see and directly change SQL statement. I find it easiest to make changes here. After this step we have to click on Finish button, wizard will exit, and we will see our data loaded into SPSS program.

Recursive functions in Power Query

In Power Query there are no loops. There are no "For each", "For next", "Do Until" blocks. In Power Query we can accomplish the same thing with recursive functions. Let's say that we want to sum elements of a list, from the start, until our sum reach 10 or more. Question is, after how many elements, our goal will be reached. We can see on the chart that our goal will be reached after 4-th element 2 + 0 + 3 + 5 = 10.

List = { 2,0,3,5,0 }

First we have to establish our initial state. We know that our list is { 2,0,3,5,0 } and that initial sum is zero. We also know that first element in the list has index zero. We can write that into query. This query will call recursive function which will supply the final result.

let 
	List = { 2,0,3,5,0 }
	, Sum = 0
	, ElementIndex = 0
	, NoOfElements = RecursiveFunction ( List, Sum, ElementIndex )
in
	NoOfElements

Second, what is our logic? Our logic will go like this:
0) Does our initial state (Sum=0) meet the condition? It doesn't, so we'll go to next step.
1) We'll add one element to sum. We got 2, this doesn't meet the condition so we go to next step.
2) We'll add another element to sum. We got 2 (2+0), this doesn't meet the condition so we go to next step.
3) We'll add another element to sum. We got 5 (2+3), this doesn't meet the condition so we go to next step.
4) We'll add another element to sum. We got 10 (5+5), this does meet the condition so our answer is 4 elements.

Step 0) is already described in our query. Steps 1-4 are pretty similar. They have the same logic, but initial state for each step is different. All we have to do is to wrap this logic into function and then to call that function with different arguments each time.  Our recursive function is bellow. If condition is satisfied, we will return "Sum", otherwise we will call function again, this time with different arguments. That will repeat until condition is met.

( List, Sum, ElementIndex ) => 
let
  SubTotal = if Sum >= 10 then Sum else
    RecursiveFunction( List, Sum + List{ ElementIndex }, ElementIndex + 1)
in
    SubTotal

Every recursive function has the same logic. First we establish initial state and then we repeat this two steps until condition is met:
– Does current state satisfied the condition? If does, then we have final result.
– If it doesn't, we'll change the state and new state will give as arguments for another call of function itself.

Let's do one more complex example. Now we have a bunch of nested lists. Our goal is to sum all scalar values in those lists. Our condition is that we use all scalar values, so we are looking for the sum of 2 + 7 + 8 + 9 + 3 + 5 + 5 + 4 + 4 + 11 = 58.


NestedLists = {   { { 2, 7 }, { 8, 9 } }
                , { 3 }
                , { { 5, 5 }, { 4, 4 } } 
                , 11     
}

First, we will establish initial state. We know that initial sum is zero, and we have our list. This query will call recursive function which will supply final result.

let 
          NestedLists = {  { { 2, 7 }, { 8, 9 }  }
                         , { 3 }
                         , { { 5, 5 }, { 4, 4 } }   	
                         , 11
          }
        , Sum = 0
        , ElementIndex = 0
	, Total = NestedListsRecursiveFunction ( NestedLists, Sum, ElementIndex )
in
	Total

All we need more is a recursive function. This function is complex. Let's try to make it easier to understand by thinking about simplier NestedLists = { 1, { 2 }, 2, { 3 } }. We can present this list by picture:

First we will count how many toothbrushes are in each package. After that is easy to sum the whole list.

Here is the trick. Addition of all toothbrushes in one package is similar to addition of all toothbrushes in the whole list. This mean that we can use same logic to individual package and to whole list. That means we can use recursion. Here is recursion function.

(NestedLists, Sum, ElementIndex) =>
  let
    NewSum = 
      if ElementIndex < List.Count(NestedLists) then
        if Value.Is(NestedLists{ElementIndex}, List.Type) then
          NestedListsRecursiveFunction(
              NestedLists 
            , Sum + NestedListsRecursiveFunction(NestedLists{ElementIndex}, 0, 0) 
            , ElementIndex + 1
          )
        else
          NestedListsRecursiveFunction(
              NestedLists 
            , Sum + NestedLists{ElementIndex} 
            , ElementIndex + 1
          )
      else
        Sum
  in
    NewSum

With purple code we are passing through all elements of a List. If we reach the final element, function will return sum of that List as final result. For each element we are using red code to determin if element is List or not.  If element is scalar, we will use green line above to add such element. Problem is that beside scalar elements, we can also have subList elements.

Orange code is used to add such elements. It is similar to green code. Green code is adding scalar values, and orange code is adding sums of subLists. Blue code above is used to sum every subList. Blue code is using recursion.

So this is our goal, we want to replace every subList with its sum. When we reach subList element, we will dive in it with recursive call to the function. If that subList element has only scalars, green line of code will give us sum of that subList. If that is not true, we will dive deeper until we find subList that contains only scalars. When we get sums of lowest subLists, we can use those to calculate sums for subLists that are higher in hierarchy.

Excel file with sample in Power Query: