1

I have a macro that rearranges the columns into a particular order.

Sub ArrangeColumns()

' ArrangeColumns Macro

    Columns("C:C").Select
    Application.CutCopyMode = False
    Selection.Cut
    Columns("A:A").Select
    Selection.Insert Shift:=xlToRight
    Columns("K:K").Select
    Selection.Cut
    Columns("B:B").Select
    Selection.Insert Shift:=xlToRight
    Columns("H:H").Select
    Selection.Cut
    Columns("G:G").Select
    Selection.Insert Shift:=xlToRight
    Columns("J:J").Select
    Selection.Cut
    Columns("H:H").Select
    Selection.Insert Shift:=xlToRight
    Columns("J:J").Select
    Selection.Cut
    Columns("I:I").Select
    Selection.Insert Shift:=xlToRight
    Columns("K:K").Select
    Selection.Cut
    Columns("J:J").Select
    Selection.Insert Shift:=xlToRight
    Range("P11").Select
End Sub

This no longer works because the columns of the raw data can no longer be guaranteed to be in a specific order.

Is there a way that I can rewrite the above code (Yes it was created by "Record Macro") to replace the lines "Columns("C:C")", Columns("A:A")", etc. with their column header names?

Even better is there a better approach to this problem?

Cindy Meister
  • 25,071
  • 21
  • 34
  • 43
PhilNBlanks
  • 117
  • 1
  • 1
  • 8
  • 1
    [Power Query](https://support.office.com/en-us/article/introduction-to-microsoft-power-query-for-excel-6e92e2f4-2079-4e1f-bad5-89f6269cd605) if your version of Excel supports it. – BigBen Oct 15 '18 at 18:25
  • you can use `lResult=application.worksheetfunction.match()` to find your column number, then use that in your code `columns(lResult)…` – nutsch Oct 15 '18 at 18:54

2 Answers2

9

If you know all the header names, you can define an array of the header names and use the array's index to move the columns around.

Sub columnOrder()
Dim search As Range
Dim cnt As Integer
Dim colOrdr As Variant
Dim indx As Integer

colOrdr = Array("id", "last_name", "first_name", "gender", "email", "ip_address") 'define column order with header names here

cnt = 1


For indx = LBound(colOrdr) To UBound(colOrdr)
    Set search = Rows("1:1").Find(colOrdr(indx), LookIn:=xlValues, LookAt:=xlWhole, SearchOrder:=xlByColumns, SearchDirection:=xlNext, MatchCase:=False)
    If Not search Is Nothing Then
        If search.Column <> cnt Then
            search.EntireColumn.Cut
            Columns(cnt).Insert Shift:=xlToRight
            Application.CutCopyMode = False
        End If
    cnt = cnt + 1
    End If
Next indx
End Sub

Any column not named in the array will appear on the right of the ones named.

oxwilder
  • 756
  • 5
  • 14
  • 1
    Thank You oxwilder that worked perfectly! I don't understand how the code works, guess I have some research ahead of me. – PhilNBlanks Oct 15 '18 at 22:41
  • The `indx` is the number of the array entry, in this case "id" is 1st in the array, so its index is 1. That coincides with the counter `cnt`. The line columns(cnt).insert says basically "find the word that is the column header and move it to the position stated in the array." I'm glad this works for you, please take a moment to mark this question as answered. – oxwilder Oct 16 '18 at 02:57
  • Hi oxwilder I think that I just marked this question as answered. Let me know if I didn't. And thanx again – PhilNBlanks Oct 16 '18 at 19:14
4

Alternative using Application.Index in a one liner

For the sake of the art and just to demonstrate a working alternative using the advanced restructuring possibilities of the Application.Index function (c.f. section [2]):


Sub colOrder()
' Purpose: restructure range columns
  With Sheet1                                               ' worksheet referenced e.g. via CodeName
  
    ' [0] identify range
      Dim rng As Range, lastRow&, lastCol&
      lastRow = .Cells(.Rows.Count, 1).End(xlUp).Row        ' get last row and last column
      lastCol = .Cells(1, .Columns.Count).End(xlToLeft).Column
      Set rng = .Range(.Cells(1, 1), .Cells(lastRow, lastCol))
      
    ' ~~~~~~~~~~~~
    ' [1] get data
    ' ~~~~~~~~~~~~
      Dim v: v = rng                                        ' assign to 1-based 2-dim datafield array
      
    ' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    ' [2] restructure column order in array in a one liner
    ' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      v = Application.Index(v, Evaluate("row(1:" & lastRow & ")"), getColNums(v))
      
    ' [3] write data back to sheet
      rng = vbNullString                                    ' clear orginal data
      .Range("A1").Resize(UBound(v), UBound(v, 2)) = v      ' write new data
  
  End With
  
End Sub

Helper function called by above main procedure

The helper function simply returns an array with the correct column numbers found in the current titles; it uses Application.Match to find occurrencies:

Function getColNums(arr) As Variant()
' Purpose: return array of found column number order, e.g. Array(3,2,1,4,6,5)
Dim colOrdr(), titles                                           ' wanted order, current titles
colOrdr = Array("id", "last_name", "first_name", "gender", "email", "ip_address") 'define column order with header names here
titles = Application.Transpose(Application.Transpose(Application.Index(arr, 1, 0)))

Dim i&, ii&, pos                                                ' array counters, element position
ReDim tmp(0 To UBound(colOrdr))                                 ' temporary array to collect found positions
For i = 0 To UBound(colOrdr)                                    ' loop through titles in wanted order
    pos = Application.Match(colOrdr(i), titles, 0)              ' check positions
    If Not IsError(pos) Then tmp(ii) = pos: ii = ii + 1         ' remember found positions, increment counter
Next i
ReDim Preserve tmp(0 To ii - 1)                                 ' remove empty elements
getColNums = tmp                                                ' return array with current column numbers (1-based)
End Function

Related link

I listed some pecularities of the Application.Index function at Insert first column in datafield array without loops or API calls

//Edit due to comment(s) as of 09/25 2021

The modified help function getColNums() includes now an option via a 2nd argument DeleteOtherCols:={False|True} to

  • preserve non-listed columns ~~> getColNums(v, DeleteOtherCols:=False) or simply getColNums(v, False); without setting the 3rd argument to True these other columns will be shown right to the listed columns (see remarks to the 3rd argument)
  • rearrange only listed columns (i.e. other columns get deleted by default) ~~> getColNums(v) or getColNums(v, True)

as well as an option via a 3rd argument StartWithOtherCols:={False|True}

  • to start with not listed columns ~~> getColNums(v, False, StartWithOtherCols:=True) or simply getColNums(v, False, True)
  • to continue with not listed columns on the right of the ones named ~~> getColNums(v,False) or getColNums(v,False,False) or or getColNums(v,False,StartWithOtherCols:=False)``:

Function getColNums(arr, _
    Optional DeleteOtherCols As Boolean = True, _
    Optional StartWithOtherCols As Boolean = False) As Variant()
' Purpose: return array of found column number order, e.g. Array(3,2,1,4,6,5)
    Dim colOrdr(), titles                        ' wanted order, current titles
    colOrdr = Array("id", "last_name", "first_name", "gender", "email", "ip_address") 'define column order with header names here
    titles = Application.Transpose(Application.Transpose(Application.Index(arr, 1, 0)))

    Dim i&, ii&, pos                             ' array counters, element position
    ReDim tmp(0 To UBound(titles) - 1)
    If StartWithOtherCols Then
        DeleteOtherCols = False                  ' correct possible input error
        ii = UBound(titles) - UBound(colOrdr) - 1 ' << EDITED: get start counter
    End If
    For i = 0 To UBound(colOrdr)                 ' loop through titles in wanted order
        pos = Application.Match(colOrdr(i), titles, 0) ' check positions
        If Not IsError(pos) Then tmp(ii) = pos: ii = ii + 1 ' remember found positions, increment counter
    Next i
    'options
    If DeleteOtherCols Then                      ' delete non-listed columns
        ReDim Preserve tmp(0 To UBound(colOrdr)) ' remove empty elements
    Else                                         ' preserve non-listed columns
        Dim tmp2
        tmp2 = Application.Match(titles, colOrdr, 0)
        If StartWithOtherCols Then ii = 0        ' start with other columns
        For i = LBound(tmp2) To UBound(tmp2)     ' loop through titles
            If IsError(tmp2(i)) Then tmp(ii) = i: ii = ii + 1
        Next i
    End If
    getColNums = tmp                             ' return array with current column numbers (1-based)

End Function


T.M.
  • 9,436
  • 3
  • 33
  • 57
  • Thanks a lot. What if I need to move only the headers stored in `colOrdr` array? I mean to move only those columns within the headers of that array. – YasserKhalil Sep 25 '21 at 15:22
  • If I understand you correctly my recent edit should cover this :-) @YasserKhalil – T.M. Sep 25 '21 at 18:26
  • 1
    Amazing. That's great. One last point, the matching headers are at the first of the 1d array. How can I move the column numbers of the matching headers to the end of the 1d array... it will be great to let this optional to be at the beginning or at the end – YasserKhalil Sep 25 '21 at 21:12
  • See 2nd edit of help function as of 9/26 2021 :-) @YasserKhalil – T.M. Sep 26 '21 at 09:15
  • Thank you very much. I tried `v = Application.Index(v, Evaluate("row(1:" & lastRow & ")"), getColNums(v))` and this is ok and returned the matching columns with the correct data. I also tried this `v = Application.Index(v, Evaluate("row(1:" & lastRow & ")"), getColNums(v, DeleteOtherCols:=False))` and got all the columns and the matching columns at beginning and all is OK. But when trying this `v = Application.Index(v, Evaluate("row(1:" & lastRow & ")"), getColNums(v, DeleteOtherCols:=False, StartWithOtherCols:=True))` I got other columns at begin ok but matching columns not correct at all. – YasserKhalil Sep 26 '21 at 12:42
  • I get all listed columns rearranged in the wanted order using the 3rd assignment; did you actually reset the original data? @YasserKhalil – T.M. Sep 26 '21 at 16:58
  • Yes, I reviewed the results well. The problem only occurred when using the third parameter and set it to True to make the matching columns to the end. At this I got incorrect headers and also incorrect data. – YasserKhalil Sep 26 '21 at 19:19
  • Please review the edited help function code as well if you copied it correctly, I get no issues . – T.M. Sep 27 '21 at 06:39
  • The same problem I am sorry. The problem occurs only with that approach `GetColNums(v, False, True)` – YasserKhalil Sep 27 '21 at 06:46
  • Please have a look at this output https://ibb.co/WkQCqtD – YasserKhalil Sep 27 '21 at 06:51
  • 1
    Think to have solved the remaining issue based on your example data: the `ii` assignment within `If StartWithOtherCols Then` has to be: `ii = UBound(titles) - UBound(colOrdr) - 1` (whereas OP data had wanted output in the 3rd variant only by chance); though the requirements in commenting go far beyond the original question, I do appreciate any qualified feedback leading to new ideas - @YasserKhalil – T.M. Sep 28 '21 at 13:51
  • 1
    Amazing. Now it is perfect and more flexible. Thank you very much. – YasserKhalil Sep 28 '21 at 14:17