Skip to main content
cancel
Showing results for 
Search instead for 
Did you mean: 

Register now to learn Fabric in free live sessions led by the best Microsoft experts. From Apr 16 to May 9, in English and Spanish.

Reply
Anonymous
Not applicable

How to merge and expand a table multiple times until a condition is met

Hello everyone,

 

This is a rare situation that I want to solve using Power Query. I have the solution in an Excel VBA macro but it takes so much time to process that it is not pratical.

 

I have a table with two columns with the list of components and raw materials required to produce said components and finished products. Example:

 

Finished Product AComponent A1
Finished Product AComponent A2
Finished Product BComponent B1
Finished Product BComponent B2
Component A1Component B1
Component A1Component B2
Component A2Raw Material 1
Component B1Raw Material 2
Component B2Raw Material 3
Component B2Raw Material 4

 

I want a new table, have the first column with only finished products and the, sequentialy merge the column above, adding the second column multiple times until I finished with only raw materials:

 

Finished Product AComponent A1Component B1Raw Material 2
Finished Product AComponent A1Component B2

Raw Material 3

Finished Product AComponent A1Component B2Raw Material 4
Finished Product AComponent A2

Raw Material 1

 
Finished Product BComponent B1Raw Material 2 
Finished Product BComponent B2Raw Material 3 
Finished Product BComponent B2Raw Material 4 

 

This is a very simple example, there are products that have so many components and raw materials that can generate 30 columns or more.

I want to merge the blueprint table (first table) with the new blueprint matrix table until the last column doesn't find any more matches (there are no more components to match, only raw materials)

 

So basically:

While number of matchs <> 0 Do merge tables and expand component and raw materials column

1 ACCEPTED SOLUTION
Smauro
Solution Sage
Solution Sage

I'll have a go at it, but you should know it's still not an easy calculation if you have a lot of products.

 

@AnonymousI've written a function, which takes 4 inputs:

 

1) The table name -- which should be your last query step,

2) The name of the 1st column,

3) The name of the 2nd column, and

4) The start of the finished products' name; e.g. "Finished Product" if all finished products are of the kind "finished product XXXX"

 

    fnFindProductLine =
        (Tbl as table, FirstColumn as text, CheckColumn as text, FinishedProduct as text) as table =>
            let
                TbFinal =Table.SelectRows(Tbl,each Text.Contains(Record.Field(_, FirstColumn),FinishedProduct,Comparer.OrdinalIgnoreCase)),
                TbCheck =Table.SelectRows(Tbl,each not Text.Contains(Record.Field(_, FirstColumn),FinishedProduct,Comparer.OrdinalIgnoreCase)),
                MergeToTheEnd =
                    (tb as table, c1 as text, c2 as text, k as number) =>
                        let
                            NewColumnName = "Product Line " & Text.From(k),
                            NewTable =
                                Table.ExpandTableColumn(
                                    Table.NestedJoin(
                                        tb,
                                        {c2},
                                        TbCheck,
                                        {c1},
                                        "mrg",
                                        JoinKind.LeftOuter
                                    ),
                                    "mrg",
                                    {CheckColumn},
                                    {NewColumnName}
                                ),
                            IsRaw =
                                if List.IsEmpty(List.RemoveNulls(Table.Column(NewTable, NewColumnName))) then
                                    Table.RemoveColumns(
                                        NewTable,
                                        NewColumnName
                                    )
                                else
                                    @MergeToTheEnd(
                                        NewTable,
                                        c1,
                                        NewColumnName,
                                        k + 1
                                    )
                        in
                            IsRaw,
                CallTheRecursiveFn =MergeToTheEnd(TbFinal,FirstColumn,CheckColumn,1)
            in
                CallTheRecursiveFn,

 

I've used this table as reference:

   PreviousStep = Table.FromRows(Json.Document(Binary.Decompress(Binary.FromText("hZDBCoQgFEV/RVy3yaYPGC13A9E2WsgkjDCjQxn9fkGrdzVayjnee3nDwLXzbvnYiXVzmNZ3ZE9ecBV+/+CtP14lH4t7S+QtSSx5kQXWmUUmZFKu+d3/Crk4eG829jLRzs58GTbIEg3skEkGtqTGA42K7FTJCuC4QSUrazQESWiwoaGXapG3lGvkOrdg3AE=", BinaryEncoding.Base64), Compression.Deflate)), let _t = ((type nullable text) meta [Serialized.Text = true]) in type table [c1 = _t, c2 = _t]),

image.png

So, in my case 1) is PreviousStep , 2) is "c1", 3) is "c2", 4) is "Finished Product"

 

And I called it:

    FindProductLine = fnFindProductLine( PreviousStep, "c1", "c2", "Finished Product" )

image.png

 

If you somehow have a "Product line {n}" column on your start table, the code will not work, because it names the new columns this way, as you see.

 

Full dummy code:

Spoiler
let
   PreviousStep = Table.FromRows(Json.Document(Binary.Decompress(Binary.FromText("hZDBCoQgFEV/RVy3yaYPGC13A9E2WsgkjDCjQxn9fkGrdzVayjnee3nDwLXzbvnYiXVzmNZ3ZE9ecBV+/+CtP14lH4t7S+QtSSx5kQXWmUUmZFKu+d3/Crk4eG829jLRzs58GTbIEg3skEkGtqTGA42K7FTJCuC4QSUrazQESWiwoaGXapG3lGvkOrdg3AE=", BinaryEncoding.Base64), Compression.Deflate)), let _t = ((type nullable text) meta [Serialized.Text = true]) in type table [c1 = _t, c2 = _t]),


    fnFindProductLine =
        (Tbl as table, FirstColumn as text, CheckColumn as text, FinishedProduct as text) as table =>
            let
                TbFinal =Table.SelectRows(Tbl,each Text.Contains(Record.Field(_, FirstColumn),FinishedProduct,Comparer.OrdinalIgnoreCase)),
                TbCheck =Table.SelectRows(Tbl,each not Text.Contains(Record.Field(_, FirstColumn),FinishedProduct,Comparer.OrdinalIgnoreCase)),
                MergeToTheEnd =
                    (tb as table, c1 as text, c2 as text, k as number) =>
                        let
                            NewColumnName = "Product Line " & Text.From(k),
                            NewTable =
                                Table.ExpandTableColumn(
                                    Table.NestedJoin(
                                        tb,
                                        {c2},
                                        TbCheck,
                                        {c1},
                                        "mrg",
                                        JoinKind.LeftOuter
                                    ),
                                    "mrg",
                                    {CheckColumn},
                                    {NewColumnName}
                                ),
                            IsRaw =
                                if List.IsEmpty(List.RemoveNulls(Table.Column(NewTable, NewColumnName))) then
                                    Table.RemoveColumns(
                                        NewTable,
                                        NewColumnName
                                    )
                                else
                                    @MergeToTheEnd(
                                        NewTable,
                                        c1,
                                        NewColumnName,
                                        k + 1
                                    )
                        in
                            IsRaw,
                CallTheRecursiveFn =MergeToTheEnd(TbFinal,FirstColumn,CheckColumn,1)
            in
                CallTheRecursiveFn,

    FindProductLine = fnFindProductLine( PreviousStep, "c1", "c2", "Finished Product" )
in
    FindProductLine

Hope this works for your case.

 

Best,

Spyros




Feel free to connect with me:
LinkedIn

View solution in original post

5 REPLIES 5
mahoneypat
Employee
Employee

Here is an example of how to transform your example data to your desired result.  To see how it works, just create a blank query, go to Advanced Editor, and replace the text there with the M code below.

 

let
    Source = Table.FromRows(Json.Document(Binary.Decompress(Binary.FromText("i45WcsvMyyzOSE1RCCjKTylNLlFwVNJRcs7PLcjPS80D8gyVYnUIqzLCrsoJRZUTDrPQVEHMQnECFlNwy2PoNwLKByWWK/gmlqQWZSbmKKCb4GSIrgLdDCcMM4wJqjBRio0FAA==", BinaryEncoding.Base64), Compression.Deflate)), let _t = ((type nullable text) meta [Serialized.Text = true]) in type table [Column1 = _t, Column2 = _t]),
    Split = Table.TransformColumnTypes(Source,{{"Column1", type text}, {"Column2", type text}}),
    ForMerging = Table.Buffer(Split),
    #"Filtered Rows" = Table.SelectRows(Split, each Text.Contains([Column1], "Finished")),
    #"Merged Queries" = Table.NestedJoin(#"Filtered Rows", {"Column2"}, ForMerging, {"Column1"}, "Filtered Rows", JoinKind.LeftOuter),
    #"Expanded Filtered Rows" = Table.ExpandTableColumn(#"Merged Queries", "Filtered Rows", {"Column2"}, {"Column2.1"}),
    #"Merged Queries1" = Table.NestedJoin(#"Expanded Filtered Rows", {"Column2.1"}, ForMerging, {"Column1"}, "Expanded Filtered Rows", JoinKind.LeftOuter),
    #"Expanded Expanded Filtered Rows" = Table.ExpandTableColumn(#"Merged Queries1", "Expanded Filtered Rows", {"Column2"}, {"Column2.2"})
in
    #"Expanded Expanded Filtered Rows"

 

At first, I thought you could solve this with the PATH function in DAX (or with its M equivalent).  However, since each child can have multiple parents, it wouldn't work.

 

The approach I provided is the brute force approach.  You can continue the same pattern for the number of levels you have (30?).  It isn't as elegant as one that would automatically iterate until it hits the end, but hopefully you just have to do this one time and then it will be automatic upon refresh.  Note that I included Table.Buffer on the table to be merged many times, which should speed things up significantly.

 

If you do want to adapt the M equivalent of the PATH function to do this, here it is - https://community.powerbi.com/t5/Quick-Measures-Gallery/DAX-s-PATH-function-equivalent-Custom-Column...

 

If this works for you, please mark it as the solution.  Kudos are appreciated too.  Please let me know if not.

Regards,

Pat





Did I answer your question? Mark my post as a solution! Kudos are also appreciated!

To learn more about Power BI, follow me on Twitter or subscribe on YouTube.


@mahoneypa HoosierBI on YouTube


Smauro
Solution Sage
Solution Sage

I'll have a go at it, but you should know it's still not an easy calculation if you have a lot of products.

 

@AnonymousI've written a function, which takes 4 inputs:

 

1) The table name -- which should be your last query step,

2) The name of the 1st column,

3) The name of the 2nd column, and

4) The start of the finished products' name; e.g. "Finished Product" if all finished products are of the kind "finished product XXXX"

 

    fnFindProductLine =
        (Tbl as table, FirstColumn as text, CheckColumn as text, FinishedProduct as text) as table =>
            let
                TbFinal =Table.SelectRows(Tbl,each Text.Contains(Record.Field(_, FirstColumn),FinishedProduct,Comparer.OrdinalIgnoreCase)),
                TbCheck =Table.SelectRows(Tbl,each not Text.Contains(Record.Field(_, FirstColumn),FinishedProduct,Comparer.OrdinalIgnoreCase)),
                MergeToTheEnd =
                    (tb as table, c1 as text, c2 as text, k as number) =>
                        let
                            NewColumnName = "Product Line " & Text.From(k),
                            NewTable =
                                Table.ExpandTableColumn(
                                    Table.NestedJoin(
                                        tb,
                                        {c2},
                                        TbCheck,
                                        {c1},
                                        "mrg",
                                        JoinKind.LeftOuter
                                    ),
                                    "mrg",
                                    {CheckColumn},
                                    {NewColumnName}
                                ),
                            IsRaw =
                                if List.IsEmpty(List.RemoveNulls(Table.Column(NewTable, NewColumnName))) then
                                    Table.RemoveColumns(
                                        NewTable,
                                        NewColumnName
                                    )
                                else
                                    @MergeToTheEnd(
                                        NewTable,
                                        c1,
                                        NewColumnName,
                                        k + 1
                                    )
                        in
                            IsRaw,
                CallTheRecursiveFn =MergeToTheEnd(TbFinal,FirstColumn,CheckColumn,1)
            in
                CallTheRecursiveFn,

 

I've used this table as reference:

   PreviousStep = Table.FromRows(Json.Document(Binary.Decompress(Binary.FromText("hZDBCoQgFEV/RVy3yaYPGC13A9E2WsgkjDCjQxn9fkGrdzVayjnee3nDwLXzbvnYiXVzmNZ3ZE9ecBV+/+CtP14lH4t7S+QtSSx5kQXWmUUmZFKu+d3/Crk4eG829jLRzs58GTbIEg3skEkGtqTGA42K7FTJCuC4QSUrazQESWiwoaGXapG3lGvkOrdg3AE=", BinaryEncoding.Base64), Compression.Deflate)), let _t = ((type nullable text) meta [Serialized.Text = true]) in type table [c1 = _t, c2 = _t]),

image.png

So, in my case 1) is PreviousStep , 2) is "c1", 3) is "c2", 4) is "Finished Product"

 

And I called it:

    FindProductLine = fnFindProductLine( PreviousStep, "c1", "c2", "Finished Product" )

image.png

 

If you somehow have a "Product line {n}" column on your start table, the code will not work, because it names the new columns this way, as you see.

 

Full dummy code:

Spoiler
let
   PreviousStep = Table.FromRows(Json.Document(Binary.Decompress(Binary.FromText("hZDBCoQgFEV/RVy3yaYPGC13A9E2WsgkjDCjQxn9fkGrdzVayjnee3nDwLXzbvnYiXVzmNZ3ZE9ecBV+/+CtP14lH4t7S+QtSSx5kQXWmUUmZFKu+d3/Crk4eG829jLRzs58GTbIEg3skEkGtqTGA42K7FTJCuC4QSUrazQESWiwoaGXapG3lGvkOrdg3AE=", BinaryEncoding.Base64), Compression.Deflate)), let _t = ((type nullable text) meta [Serialized.Text = true]) in type table [c1 = _t, c2 = _t]),


    fnFindProductLine =
        (Tbl as table, FirstColumn as text, CheckColumn as text, FinishedProduct as text) as table =>
            let
                TbFinal =Table.SelectRows(Tbl,each Text.Contains(Record.Field(_, FirstColumn),FinishedProduct,Comparer.OrdinalIgnoreCase)),
                TbCheck =Table.SelectRows(Tbl,each not Text.Contains(Record.Field(_, FirstColumn),FinishedProduct,Comparer.OrdinalIgnoreCase)),
                MergeToTheEnd =
                    (tb as table, c1 as text, c2 as text, k as number) =>
                        let
                            NewColumnName = "Product Line " & Text.From(k),
                            NewTable =
                                Table.ExpandTableColumn(
                                    Table.NestedJoin(
                                        tb,
                                        {c2},
                                        TbCheck,
                                        {c1},
                                        "mrg",
                                        JoinKind.LeftOuter
                                    ),
                                    "mrg",
                                    {CheckColumn},
                                    {NewColumnName}
                                ),
                            IsRaw =
                                if List.IsEmpty(List.RemoveNulls(Table.Column(NewTable, NewColumnName))) then
                                    Table.RemoveColumns(
                                        NewTable,
                                        NewColumnName
                                    )
                                else
                                    @MergeToTheEnd(
                                        NewTable,
                                        c1,
                                        NewColumnName,
                                        k + 1
                                    )
                        in
                            IsRaw,
                CallTheRecursiveFn =MergeToTheEnd(TbFinal,FirstColumn,CheckColumn,1)
            in
                CallTheRecursiveFn,

    FindProductLine = fnFindProductLine( PreviousStep, "c1", "c2", "Finished Product" )
in
    FindProductLine

Hope this works for your case.

 

Best,

Spyros




Feel free to connect with me:
LinkedIn

Anonymous
Not applicable

What I was really missing was a method for looping the merge function, like you did with "@". By adapting my code with it I managed to solve the problem! Thank you very much for the help! That solution is brilliant, although this is not very fast, it's still better than using VBA and I can use it in both Power BI and Excel Power Query.

Anonymous
Not applicable

give  a try to this:

 

#####edited####

 

 

let
  Source = Table.FromRows(
    Json.Document(
      Binary.Decompress(
        Binary.FromText(
          "hY+xDsIgEEB/hTB3ATXqKFA3ksa1YSCWRBItptb4+yKHw9E2HV/u8bhrW3r2vX/dXEeaIXTv60hOtKIyPJ6hd30kRk21bvF5SyBLLLQKC1pohZnK8nzynsf5xX6ItqMbvL2TsiBYaZQNMWlsVo0tXNvI33qSIeKIckumQxRDlDeJTszr/yx9pvNMJbPGlJsKzB1QDbRHdMgEPxypMV8=", 
          BinaryEncoding.Base64
        ), 
        Compression.Deflate
      )
    ), 
    let
      _t = ((type text) meta [Serialized.Text = true])
    in
      type table[col1 = _t, col2 = _t]
  ),
  ct = Table.TransformColumnTypes(Source, {{"col1", type text}, {"col2", type text}}),
  add = (tab, n) => 
    let
      ac = Table.AddColumn(
        tab, 
        "col" & Text.From(n), 
        each List.Distinct(
          Table.SelectRows(tab, (r) => r[col1] = Record.Field(_, "col" & Text.From(n - 1)))[col2]
        )
      ),
      tabn = Table.ExpandListColumn(ac, "col" & Text.From(n))
    in
      tabn,
  steps = List.Generate(
    () => [y = ct, n = 3, x = true], 
    each [x], 
    each [y = add([y], [n]), n = [n] + 1, x = not List.IsEmpty(
      List.Intersect({Table.Column(y, "col" & Text.From([n] - 1)), y[col1]})
    )], 
    each [y]
  )
in
  Table.SelectRows(List.Last(steps), each not List.Contains(Source[col2], [col1]))

 

 

Anonymous
Not applicable

just to make the code more readable, using the  formatter that makes the view more pleasant 😀

 

 

 

let
  Source = Table.FromRows(
    Json.Document(
      Binary.Decompress(
        Binary.FromText(
          "hY+xDsIgEEB/hTB3ATXqKFA3ksa1YSCWRBItptb4+yKHw9E2HV/u8bhrW3r2vX/dXEeaIXTv60hOtKIyPJ6hd30kRk21bvF5SyBLLLQKC1pohZnK8nzynsf5xX6ItqMbvL2TsiBYaZQNMWlsVo0tXNvI33qSIeKIckumQxRDlDeJTszr/yx9pvNMJbPGlJsKzB1QDbRHdMgEPxypMV8=", 
          BinaryEncoding.Base64
        ), 
        Compression.Deflate
      )
    ), 
    let
      _t = ((type text) meta [Serialized.Text = true])
    in
      type table[col1 = _t, col2 = _t]
  ),
    add = (tab, n) => 
    let
      ac = Table.AddColumn(
        tab, 
        "col" & Text.From(n), 
        each List.Distinct(
          Table.SelectRows(tab, (r) => r[col1] = Record.Field(_, "col" & Text.From(n - 1)))[col2]
        )
      ),
      tabn = Table.ExpandListColumn(ac, "col" & Text.From(n))
    in
      tabn,
  last = (tab, coln) => not List.IsEmpty(
    List.Intersect({Table.Column(tab, "col" & Text.From(coln - 1)), tab[col1]})
  ),
  steps = List.Generate(
    () => [y = Source, n = 3, x = true], 
    each [x], 
    each [y = add([y], [n]), n = [n] + 1, x = last([y], [n])], 
    each [y]
  )
in
  Table.SelectRows(List.Last(steps), each not List.Contains(Source[col2], [col1]))

 

 

 

Helpful resources

Announcements
Microsoft Fabric Learn Together

Microsoft Fabric Learn Together

Covering the world! 9:00-10:30 AM Sydney, 4:00-5:30 PM CET (Paris/Berlin), 7:00-8:30 PM Mexico City

PBI_APRIL_CAROUSEL1

Power BI Monthly Update - April 2024

Check out the April 2024 Power BI update to learn about new features.

April Fabric Community Update

Fabric Community Update - April 2024

Find out what's new and trending in the Fabric Community.

Top Solution Authors
Top Kudoed Authors