VBA function to shift columns by variable value starting with variable column number

kt_mr_excel

New Member
Joined
Sep 24, 2021
Messages
15
Office Version
  1. 365
  2. 2016
Platform
  1. Windows
Help! I have a very large volume of spreadsheets that were converted from PDF files and need to correct some column shifts that resulted from the conversion. The worksheets have thousands of rows that would need all cells to be shifted to the right from a variable starting column and variable amount by row. I have two columns, A & B, where A displays the letter value of the starting column to shift (like "L") and B contains a numeric value indicating the amount to shift. So ideally, I need to start on row 2 (to avoid the header row) and loop through all rows that contain data (rather than a fixed number of rows) and perform the appropriate shift for each row based on the values in columns A & B. I've found some similar logic in the forum but nothing that handles all of the variables. Any help will be be GREATLY appreciated!
 

Excel Facts

Copy PDF to Excel
Select data in PDF. Paste to Microsoft Word. Copy from Word and paste to Excel.
Let me if this is what you want.

This code (which can be condensed, but I kept it like this to be comprehensible!) will:
  • Only shift rows (left OR right: left if the number in column B is negative, right if the number in column B is positive) which have a valid integer in column B and a validcolumn letter (not case-sensitive, so Aa=AA=aa=aA) in column A.
    • An column letter in column A is considered valid if:
      • It's only English alphabet letters, does not go past XFD (the last column letter available in a spreadsheet).
      • It's not blank.
    • An integer in column B is considered valid if:
      • It's not blank or 0. (If it's zero, it simply means not to shift, so it will be skipped.)
      • It doesn't move the start column letters contents (specified in column B for that particular row) in any column to the left of column C. (We don't want to shift the data into columns A and B = "the shift instructions" . . . or "off the page to the left" for that matter!)
      • It doesn't move the last used (non-blank) cell in that particular row off of the page/sheet to the right. (A row cannot be shifted so that the right-most cell contents were to be shifted to the right of column XFD = the last column letter in a spreadsheet.
      • Can be negative, or positive.
  • When if shifts, I assume that you only have values(no formatting). So it will:
    • Copy ONLY from the start column letter (specified in column A) to the last non-empty cell in the row.
    • Paste (well, it's not exactly paste. It's assignment which is much faster than copy/paste) in the destination.
    • Delete the content in the cells which either a part or the whole block or the whole block + more "used to be". (To fully mimic Cut/Paste.)
    • Again, by values only. (No formulas, formatting, etc.)
    • But there is a Range.cut line of code (Line #3). So if you uncomment (Line #3) and comment (Line #1) and (Line #2), it will cut and paste (without the clipboard!) the formatting, formulas, and values, but it will probably be slower. The way it sounded, it seems to be values only and can be potentially very large sheets. But regardless, you have an option for both!
  • In the top (test) sub's code, I have ActiveWorkbookas the argument passed, so that you can use the Workbook you put this code in as like an actual program. You have the code window (VBA project) activated, but when you run the code from that Workbook's code module, you can then select another Workbook (the normal window with the sheets) so that you don't have to load this code into every Workbook that you want to process.
    • It will only process one sheet at a time (the sheet that you have to actually click on a cell in it to select it), but this can easily be changed to process all sheets in a Workbook. And it can also be easily changed to process all sheets in all Workbooks that are in a specified folder! Just let me know!
  • I also allow you to specify the start row number in that top sub (which I set to 2 at default).

  • As far as I can tell, based on the (several) filters I set, it should be able to safely process a Worksheet without any error messages.

  • I have 3 Debug.Print lines commented. You can press Ctrl G to open the Immediate Window in the code window to view the message that the program creates. (It tells you the copy and paste range and the range which its contents were erased. It also tells you what rows it skipped. (It doesn't tell you why it skipped the rows, but I can make it do that if you wish.)

  • Lastly, I just recorded a video explaining this visually (with examples). I will be editing it and will upload it soon.

Put this code in a standard code module ( Insert and run VBA macros in Excel - step-by-step guide - Ablebits.com ) in a new Workbook and save as .xlsb extension type. (.xlsm is okay also, but .xlsb is better.) Click inside of the top (3 line) sub and press the F5 key (or the play button) to run the code. But again, it will affect the Active Workbook, not necessarily the Workbook for which the code is run from. So if you have several Workbooks opened, be sure to click on the specific sheet in the specific Workbook that you want the program to affect.
VBA Code:
Option Explicit


Sub Test__Shift_Rows_In_The_Workbook_In_The_Sheet()
Call Shift_Rows_In_The_Workbook_In_The_Sheet(ActiveWorkbook, ActiveSheet.Name, 2)
End Sub
Sub Shift_Rows_In_The_Workbook_In_The_Sheet(book As Workbook, sheetName As String, startRow As Long)

'To speed up the program.  Tell Excel to not update formulas until finished
Application.Calculation = xlCalculationManual

'Turn off "window jumping"
'Application.ScreenUpdating = False

With book.Sheets(sheetName)

    'This not the last used row in the entire sheets used range.
    'Just the last used row number in Column A,
    'since that's where we are going to be reading the shifting "instructions" from.
    Dim lastUsedRowNumber As Long
    lastUsedRowNumber = .Range("A" & .Rows.Count).End(xlUp).Row

    Dim currentEndColumnNumber As Integer

    Dim currentStartColumnLetter_Input As String
    Dim currentStartColumnLetter As String
    Dim currentStartColumnNumber As Variant
   
    Dim amountToShift_Input As String
    Dim amountToShift As Integer

    Dim currentBlockLengthToShift As Integer

    Dim currentBlockStartingLocation As Range
    Dim currentBlockDestination As Range
    Dim currentBlockToDeleteValuesAfterTheMove As Range

    Dim cell As Range
    For Each cell In .Range("A" & startRow & ":" & "A" & lastUsedRowNumber)
        currentStartColumnLetter_Input = Trim(cell.Value) 'We are looping through column A.  So this is supposedly a potential column letter(s)
        amountToShift_Input = Trim(cell.Offset(0, 1).Value) 'This is the potential number in Column B.

        'If either column A, column B, or both don't have row offset "instructions", or Column A's value is more than three letters, skip row.
        If (Len(currentStartColumnLetter_Input) = 0) Or (Len(amountToShift_Input) = 0) Or (amountToShift_Input = "0") Or (Len(currentStartColumnLetter_Input) > 3) Then GoTo Next_Row
 
        'If the content in Column A is not entirely English letters (not case-sensitive: "Ab","ab","AB" are all acceptable),
        'OR the content in Column B is not entirely numerical digits, skip row. (This doesn't handle the potential input "00".)
        If (Is_Just_One_Or_More_English_Letters(currentStartColumnLetter_Input) = False) Or (IsNumeric(amountToShift_Input) = False) Then GoTo Next_Row

        'Convert the inputs to usable values.
        amountToShift = CInt(amountToShift_Input)
        currentStartColumnLetter = currentStartColumnLetter_Input
 
        'Calculate the left and right bounds for the current row.
        currentStartColumnNumber = Columns(currentStartColumnLetter).Column
        currentEndColumnNumber = .Cells(cell.Row, 16384).End(xlToLeft).Column ' (*)

        'Block length
        currentBlockLengthToShift = currentEndColumnNumber - currentStartColumnNumber + 1

        'If any part of the current row is going to be moved "off of the sheet" on the right sides, skip the row.
        If currentStartColumnNumber + currentBlockLengthToShift - 1 + amountToShift > 16384 Then GoTo Next_Row
               
        'For shifting to the left (negative integers in column B ... that is currentBlockLengthToShift
        'can be negative), if the the first column to move is being instructed to move to any column to
        'the left of Column C, go to the next row.
        If currentStartColumnNumber + amountToShift < 3 Then GoTo Next_Row

        'Starting location, ending location.
        Set currentBlockStartingLocation = .Range(.Cells(cell.Row, currentStartColumnNumber), .Cells(cell.Row, currentStartColumnNumber + currentBlockLengthToShift - 1))
        Set currentBlockDestination = .Range(.Cells(cell.Row, currentStartColumnNumber + amountToShift), .Cells(cell.Row, currentStartColumnNumber + currentBlockLengthToShift - 1 + amountToShift))

        'Block portion to erase after move.  (We are copying.  We have to delete after to mimic CUTTING.)
        If amountToShift > 0 Then
            Set currentBlockToDeleteValuesAfterTheMove = .Range(.Cells(cell.Row, currentStartColumnNumber), .Cells(cell.Row, currentStartColumnNumber + amountToShift - 1))
        Else 'amountToShift < 0 because we exited already if amountToShift = 0
            Set currentBlockToDeleteValuesAfterTheMove = .Range(.Cells(cell.Row, currentStartColumnNumber + currentBlockLengthToShift - 1 + amountToShift + 1), .Cells(cell.Row, currentStartColumnNumber + currentBlockLengthToShift - 1))
        End If

        'Make the move.
            'Copy
            currentBlockDestination.Value = currentBlockStartingLocation.Value '(Line #1)
            'Debug.Print currentBlockStartingLocation.Address & " moved to " & currentBlockDestination.Address
           
            'Erase
            currentBlockToDeleteValuesAfterTheMove.Value = "" '(Line #2)
            'Debug.Print currentBlockToDeleteValuesAfterTheMove.Address & " erased "


            'If you want to use cut and paste (to move all formatting, formulas, and values),
            'comment the above to lines (Line #1 and Line #2) and uncomment the line below.
            'currentBlockStartingLocation.Cut (.Cells(cell.Row, currentStartColumnNumber + amountToShift))
   


                'For Debugging/seeing what is moved, where it is moved, and what is deleted.

                    'Coloring blocks that are moved (see the video)
                        'currentBlockStartingLocation.Interior.Color = RGB(255, 0, 0)
                        'currentBlockDestination.Interior.Color = RGB(0, 0, 255)
                        'currentBlockToDeleteValuesAfterTheMove.Interior.Color = RGB(255, 255, 0)
               
                    'Alternatively (instead of coloring the blocks), see this for the first row it shifts).
                        'currentBlockStartingLocation.Select
                        'MsgBox ""
                        'currentBlockDestination.Select
                        'MsgBox ""
                        'currentBlockToDeleteValuesAfterTheMove.Select
                        'MsgBox ""
                        'End


            'GoTo Next_Row2 '(Uncomment this line if you want to uncomment the Debug.print below!)
Next_Row:
            'Debug.Print "Row " & cell.Row & " skipped."
Next_Row2:

    Next cell

End With

'Turn the two things we turned off back on.
Application.Calculation = xlCalculationAutomatic
'Application.ScreenUpdating = True

End Sub


Sub Test__Is_Just_One_Or_More_English_Letters()
MsgBox Is_Just_One_Or_More_English_Letters("AC")
End Sub
'A regular expression, https://stackoverflow.com/questions/29633517/how-can-i-check-if-a-string-only-contains-letters
Function Is_Just_One_Or_More_English_Letters(strValue As String) As Boolean
Is_Just_One_Or_More_English_Letters = strValue Like WorksheetFunction.Rept("[a-zA-Z]", Len(strValue))
End Function
 
Upvote 0
Solution
Here is the video!

And I didn't show it in the video, but this is a cool example (input/output)! (This is of course with the Range.Cut(Destination) route, not the by value route which, again, you can easily switch to by commented the Rang.cut line of code (line #3) and uncommenting lines #1 and #2.)

Start with:
Shift Rows Program.xlsb
ABCDEFGHIJKLMNOP
1
2
3C112345678910111213
4D212345678910111213
5E312345678910111213
6F412345678910111213
7G512345678910111213
8H612345678910111213
9I712345678910111213
10J812345678910111213
11i712345678910111213
12h612345678910111213
13g512345678910111213
14f412345678910111213
15e312345678910111213
16d212345678910111213
17c112345678910111213
18
19
Sheet2 (2)


End with:
Shift Rows Program.xlsb
ABCDEFGHIJKLMNOPQRSTUVW
1
2
3C112345678910111213
4D212345678910111213
5E312345678910111213
6F412345678910111213
7G512345678910111213
8H612345678910111213
9I712345678910111213
10J812345678910111213
11i712345678910111213
12h612345678910111213
13g512345678910111213
14f412345678910111213
15e312345678910111213
16d212345678910111213
17c112345678910111213
18
19
Sheet2 (3)
 
Upvote 0
Hi & welcome to MrExcel.
Another option
VBA Code:
Sub ktmrexcel()
   Dim Cl As Range
   For Each Cl In Range("A2", Range("A" & Rows.Count).End(xlUp))
      Range(Cl.Value & Cl.Row).Resize(, Cl.Offset(, 1).Value).Insert xlShiftToRight
   Next Cl
End Sub
 
Upvote 0
Hi & welcome to MrExcel.
Another option
VBA Code:
Sub ktmrexcel()
   Dim Cl As Range
   For Each Cl In Range("A2", Range("A" & Rows.Count).End(xlUp))
      Range(Cl.Value & Cl.Row).Resize(, Cl.Offset(, 1).Value).Insert xlShiftToRight
   Next Cl
End Sub
It actually could be that simple if the data is:
  • Structured perfectly (all entries in Columns A are valid column letters (including without any white characters), numbers which are "legal" shifts.
  • Continuous (no breaks of "instructions from columns A or B")
  • Has no "negative" shifts
But clearly we have no idea what the data looks like.

I guess what we can compare (assuming that the data is formatting perfectly as stated above) is timing. I just did a time test of yours and mine (on the Start with: example in my previous post, but with the entire used ranged shifted up one row so that you code would not break),

VBA Code:
Sub ProgramTimer()

'Put a timer to output to the user how long it took the program to run.
Dim startTime As Single
startTime = Timer()

'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
'Our sub calls here
Call Shift_Rows_In_The_Workbook_In_The_Sheet(ActiveWorkbook, ActiveSheet.Name, 2)
'Call ktmrexcel
'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Dim finishingTime As Single
finishingTime = Timer()

Dim runTime As Single
runTime = finishingTime - startTime
Debug.Print "Total RunTime: " & CStr(Round(runTime, 2)) & " seconds.", vbInformation, "Program RunTime"

End Sub

And I got these times:
Total RunTime: 0.06 seconds. (Yours) (Enclosed with Application.ScreenUpdating, Application.Calculation)
Total RunTime: 0.08 seconds. (My by value only shift version)
Total RunTime: 0.16 seconds. (My full shift (formatting and all) version . . . with the If amountToShift > 0 Then IF statement commented out, as that's not needed in the formatting and all version.)
 
Upvote 0
MANY thanks for the valuable replies! I will try these as soon as I'm able and respond back with results. If I can get this to work correctly in a single active workbook, I will in fact be looking to execute the code against all workbooks in a given folder, with just a single sheet per workbook, but will need to either insert several columns of formula values in each sheet to create the shift criteria or enhance the code to calculate the shift value for each row.
 
Upvote 0
For some reason the short code version is producing a runtime error '1004' and the debugger highlights this line without updating any rows:
Range(Cl.Value & Cl.Row).Resize(, Cl.Offset(, 1).Value).Insert xlShiftToRight
All values in column A are correct column letters and all values in column B are positive integer values so I'm not sure what might be causing the problem.

However, the longer function provided by *cmowla produced the exact results I was expecting. Amazing! Thank you SO much!
 
Upvote 0
All values in column A are correct column letters and all values in column B are positive integer values so I'm not sure what might be causing the problem.
The range also has to be continuous and start at row 2. Maybe that's it.

However, the longer function provided by *cmowla produced the exact results I was expecting. Amazing! Thank you SO much!
You're very welcome! I guess you used the "by value" (lines #1 and #2) instead of the Range.Cut (line #3)? (Because that's what you asked for.) Just curious.

But about my code: it can be shortened (I can remove a lot of filters that are slowing it down), but I wasn't sure if the values in Columns A and B were manually inputted or inputted by code. So it's better to be on the safe side.
 
Upvote 0

Forum statistics

Threads
1,215,167
Messages
6,123,401
Members
449,098
Latest member
ArturS75

We've detected that you are using an adblocker.

We have a great community of people providing Excel help here, but the hosting costs are enormous. You can help keep this site running by allowing ads on MrExcel.com.
Allow Ads at MrExcel

Which adblocker are you using?

Disable AdBlock

Follow these easy steps to disable AdBlock

1)Click on the icon in the browser’s toolbar.
2)Click on the icon in the browser’s toolbar.
2)Click on the "Pause on this site" option.
Go back

Disable AdBlock Plus

Follow these easy steps to disable AdBlock Plus

1)Click on the icon in the browser’s toolbar.
2)Click on the toggle to disable it for "mrexcel.com".
Go back

Disable uBlock Origin

Follow these easy steps to disable uBlock Origin

1)Click on the icon in the browser’s toolbar.
2)Click on the "Power" button.
3)Click on the "Refresh" button.
Go back

Disable uBlock

Follow these easy steps to disable uBlock

1)Click on the icon in the browser’s toolbar.
2)Click on the "Power" button.
3)Click on the "Refresh" button.
Go back
Back
Top