1

I have found this piece of code from somewhere else, and I modified it a bit to meet my needs, but I'm not entirely sure how it works. The reason I would like to know how it works is because I would like to modify it further, but I am newer to VBA and do not understand how the Resize, Index, and Evaluate functions work together in this macro.

Sub RearrangeColumns()
    Dim newColumnOrder As Variant
    newColumnOrder = Array(1, 2, 3, 4, 41, 42, 43, 44, 5, 6, 49, _
           7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, _
           21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, _
           35, 36, 37, 38, 39, 40, 45, 46, 47, 48, 50, 51, 52)
    Range("A1").Resize(Cells.Find("*", , xlFormulas, , xlRows, xlPrevious).Row, UBound(newColumnOrder) + 1) = _
       Application.Index(Cells, Evaluate("ROW(1:" & Cells.Find("*", , xlFormulas, _
       , xlRows, xlPrevious).Row & ")"), newColumnOrder)
End Sub

In addition, is it possible to modify this code to work with a table that has already been created using VBA? I've been trying to figure out a way to rearrange ListColumns, but it seems like the best choice is to just rearrange the actual letter/number columns.

Thanks!

Tim Williams
  • 154,628
  • 8
  • 97
  • 125

1 Answers1

5

Whoever wrote that line must have laughed like a mad scientist when they were finished. It combines numerous functions and methods to perform an entire sub worth of code in a single line. I will attempt to explain each step.

Range("A1") : Starting with Cell A1 so the range can extend from this top corner to grab the rest of the sheet.

.Resize( : This is the property that will stretch Range("A1") from a single cell to encompass the targetted area. Resize has two arguments, Rows and Columns, that decide the size of the final range.

Cells.Find("*", , xlFormulas, , xlRows, xlPrevious).Row : This .Find method searches in Cells which is all cells in the sheet. It is searching for * which means anything. It specifies xlPrevious which means it is searching backwards. But since it is searching all cells, it is searching from the bottom right to top left. It is a quick way to find the bottom row of the sheet.

.Row : This grabs the row # from the result of the .Find which will be the bottom row of the sheet.

UBound(newColumnOrder) + 1 : This counts the number of items inside the array newColumnOrder. Since arrays defined using Array() start at 0, you need to add one to the upper bound to get the count.

Resize( : Going back to this property now that we have understood the input arguements, we are resizing Range("A1") to extend from "A1" to a number of columns equal to the size of the array newColumnOrder and enough rows to encompass all non-blank cells of the sheet.

= Application.Index( : The index function returns a single value, but because we are assigning this value to a multi-cell range, VBA automatically interprets this as "Do this for every cell in the range". VBA is also smart when applying formula values into ranges, automatically adjusting the references to follow the current cell's address. This is how the function "iterates" through the array newColumnOrder without actually looping.

Cells : The first argument of Index. This is where Excel will grab the values from. Cells is all cells on the sheet. Notably, since Cells also starts from "A1" there is no offset between this and our target range. If you replace this with a different range, make sure to account for the offset with the other two arguments of Index.

Evaluate( : This second argument of the Index function is the row# of the value to be returned from the range defined in the first argument. The Evaluate method asks VBA to interpret the input expression similarly to how an Excel sheet evaluates input formulas/references. This is used to turn the input string which is an Excel formula into an array of values.

"ROW(1:" __ ")" : This is the ROW function from Excel which returns the row # from the input reference. Since the input is a range instead of a single reference, we get an array of values corresponding to the ROW # of each cell in the range. As VBA fills our target range with results from the Index function, it will automatically "iterate" through this array. This will input into Index a row# corresponding to each cells current row# since there is no offset between the filled target range and the Indexed range.

Cells.Find("*", , xlFormulas, , xlRows, xlPrevious).Row : Same as above, this is the row # of the bottommost cell on the sheet.

newColumnOrder : The third argument of Index is asking for the Column #. Instead of inputting a single value, we are inputting an entire array. Similarly to how cell references automatically shift when you Autofill a formula across a range, VBA iterates through the array as it fills in our range, and in doing so grabs the column numbers directly from it. So the first column of our target range will fill from the column # in the first element of the array, the second column will fill from the column # in the second element of the array.

Conclusion:

This line says "Into a range starting from A1 stretching to the size of the array newColumnOrder and to the last filled row of the sheet. Re-fill each cell with the value of a cell in the same row but whose column # can be found in the array newColumnOrder with index of the array being your current column number. "

To answer your second question about how to write this for a different table, I suggest that you don't write code like this. While it might be a code golf winner as fewest lines to accomplish the task, it is cryptic and hard to read or edit. Instead I would suggest code similar to the following:

Sub ReOrderingColumnsExample()
   'Define the desired column order
   Dim newColumnOrder() As Variant
   newColumnOrder = Array(1, 3, 5, 2, 4, 6)
   
   'Define the output range, manually or dynamically
   Dim OutputRange As Range
   Set OutputRange = Sheet1.Range("A1:F20")
   
   'Define the input range, manually or dynamically
   Dim InputRange As Range
   Set InputRange = Sheet1.Range("R34:W53")
   
   'Create an array to hold the columns
   Dim ColArrs() As Variant
   ReDim ColArrs(1 To InputRange.Columns.Count)
   
   'Fill the array with arrays of each column's values
   Dim i As Long
   For i = 1 To InputRange.Columns.Count
       ColArrs(i) = InputRange.Columns(i).Value
   Next i
   
   'Output the arrays into the output range with the column based on newColumnOrder
   For i = 1 To OutputRange.Columns.Count
       OutputRange.Columns(i) = ColArrs(newColumnOrder(i - 1))
   Next i
End Sub

Significantly less confusing and all of the pieces are easily editable to fit your specific parameters.

Toddleson
  • 4,321
  • 1
  • 6
  • 26
  • 1
    I apologize for the rapid-fire edits. I just want to make sure that what I have said is 100% accurate and clearly stated. Hopefully I have done that. I haven't done any academic courses in programming so my terminology or knowledge might be lacking. I can only describe things the way that I understand them. – Toddleson Jun 29 '21 at 15:14
  • A really extensive answer explaining the long, long code above. +:) Fyi A possible (at least partial) **source** of OP's *"code from somewhere else"* might be my elder post [Reordering columns based on numerical column header](https://stackoverflow.com/questions/58977976/reordering-columns-based-on-numerical-column-header/60327247#60327247) in connection with [Some pecularities of Application.Index](https://stackoverflow.com/questions/51688593/excel-vba-insert-new-first-column-in-datafield-array-without-loops-or-api-call/51714153#51714153) – T.M. Jun 29 '21 at 16:12
  • @T.M. It's definitely not from your source at all considering the code I'm using is over ten years old. – Bob the Builder Jun 29 '21 at 16:20
  • @Toddleson Thank you so much for your answer! I skimmed through it as I'm a bit short on time at the moment, but I will definitely take time to appreciate it some more later. I do have a question. Do you know of any sources or do you know how to do a similar feat using a table? I'm trying to do something similar but by using an array with table headers and reordering them based on a predetermined order of the table headers. – Bob the Builder Jun 29 '21 at 16:23
  • 1
    @BobtheBuilder Thanks for the response; as for my more recent posts: they cover the astonishing restructuring possibilities of `Application.Index` which I (re-)discovered just by trying. So you can build any wanted order, omit or even repeat other rows or columns. Read some interesting further hints in my above mentioned links; another example with predetermined listobject headers [here](https://stackoverflow.com/questions/67037406/transposing-data-from-one-workbook-to-another-depending-on-column-heading/67064288#67064288) – T.M. Jun 29 '21 at 16:35
  • 1
    @BobtheBuilder I would avoid writing code like this because of the cryptic structure of it. If you just need to re-arrange a table, you can take each column into an array and then output the column arrays back into the range in their new position. Its much easier to see what is happening and much more flexible this way. – Toddleson Jun 29 '21 at 16:41
  • @Toddleson Are you referring to the original code above? Also, do you have an example of how to just output it? I know how to declare and initialize the arrays, but would I just load the table columns (based on headers) or Excel columns (letters or numbers) into an array and then set that array equal to the new order? I can ask a new question if you'd like me to. – Bob the Builder Jun 29 '21 at 16:46
  • myArray = Array("Date","Amount","Type") tbl.ListColumns(myArray) Set myArray = (newOrder) – Bob the Builder Jun 29 '21 at 16:47
  • 1
    @BobtheBuilder I have added an example to my answer. – Toddleson Jun 29 '21 at 17:01
  • @Toddleson Thank you so much! Without you having to modify the code (I can test it), would it also work with ListObjects? I wonder if I'll need to use an Application.Index solution for ListObjects. – Bob the Builder Jun 29 '21 at 17:05
  • @BobtheBuilder It works with list objects but if the input and output range is in the same table, you will need to blank out the headers inbetween the input and output loops to avoid duplicate header issues. – Toddleson Jun 29 '21 at 20:08
  • 1
    Friendly hint in addition to your upvoted answer: imo the column replacements should rather be `OutputRange.Columns(i) = ColArrs(newColumnOrder(i - 1))` (instead of `OutputRange.Columns(newColumnOrder(i - 1)) = ColArrs(i)`) to rearrange the columns as cited in `newColumnOrder`. @Toddleson - *btw A speed comparison over 30k rows with 26 columns resulted in very similar times* (OP 2.8 secs, your post ~3.1 secs, a better readable approach [Reordering columns](https://stackoverflow.com/questions/58977976/reordering-columns-based-on-numerical-column-header/60327247#60327247) needed ~3.2 secs). – T.M. Jul 01 '21 at 19:39
  • 1
    @T.M. Haha I don't know how I missed that. I did indeed write that column relationship backwards, it has been corrected. Thank you! – Toddleson Jul 05 '21 at 13:41