Macro to replace values within a table - excel

I have a large amount of data (approx 60 columns and 60,000 rows) formatted as a table in Excel. I'm looking to use a macro to replace all the values greater than 1 which reside in a column titled 'Salary' in the table with a value of '2'. the table is dynamic so I need to reference the replace to the Tables column name rather than a column range like D:D.
Update:
I have put together the following code but cannot get it to work when i use What:=">0" however it will work if what="5". What am I doing wrong?
Sub FindReplace3()
ActiveSheet.ListObjects("Table1").ListColumns(61).DataBodyRange.Replace _
What:=">0", replacement:="7", _
SearchOrder:=xlByColumns, MatchCase:=True
End Sub

Evaluate can be used to replace all at once :
[Table1[Salary]] = [if(Table1[Salary] > 1, 2, Table1[Salary])]

I see this is your first post on Stackoverflow, so, welcome.
I also see you have been marked down for your question which can be disheartening as a first introduction the the site.
On SO there is an expectation that you will have researched and tried a number of things first and posted that information with the question.
You are very close, but your code is failing because you are searching for a literal string ">0" (What:=">0"). >0 obviously does not exist as a literal string.
The built in replace function limits the find to a literal string. Therefore I would use this approach:
Sub replaceTest()
Dim dblCnt As Double
dblCnt = 0
With ThisWorkbook.Worksheets("Sheet1")
For i = 1 To Range("Table1").Rows.Count
If Range("Table1[Salary]")(i) > 1 Then
Range("Table1[Salary]")(i) = "2"
dblCnt = dblCnt + 1
End If
Next i
End With
MsgBox "Finished replacing " & CStr(dblCnt) & " items", vbOKOnly, "Complete"
End Sub
FYI, your code sample was referencing Column 61, but you said the column was called 'Salary'. You can reference the column name by changing your sample from:
ActiveSheet.ListObjects("Table1").ListColumns(61).DataBodyRange.Replace _
to
ActiveSheet.ListObjects("Table1").ListColumns("Salary").DataBodyRange.Replace _
I have added another code section below and credit must go to #Slai His approach using the 'Evaluate' function is instantaneous compared to my original answer:
Sub replaceTest001()
Dim StartTime As Date
StartTime = Now()
Dim dblCnt As Double
dblCnt = 0
Application.ScreenUpdating = False
With ThisWorkbook.Worksheets("Sheet1")
[Table1[Salary]] = [if(Table1[Salary] > 1, 2, Table1[Salary])]
End With
Application.ScreenUpdating = True
MsgBox "Finished updating " & CStr(dblCnt) & " items" & vbCrLf & _
"Time taken: " & Format((Now() - StartTime), "hh:mm:ss"), vbOKOnly, "Complete"
End Sub

Related

How to check if the date in a cell is an actual date or a string date?

At work we are using Office 365 Excel's file as a booking system. It has multiple tabs for each site where each record has a booking date. There is problem of date formatting - basically the default locale is set to "mm/dd/yyyy" however the date is displayed in "dd/mm/yyyy". When people add rows manually (normally booking slots are generated automatically for each day) and just type in date in a wrong format instead of copying date value from the adjacent cell it displays right, but the cell value in the top bar is different, but when opening this file in the Desktop App it does not see this as different values at all. Only when applying filter, there are dates, and string date values you can filter by. This causes some of the dates not being picked up by the macros while creating reports, or importing data based on the date.
I've been thinking of writing an utility macro that would sanitize all dates based on the dates up and down to the current date, however I am not sure if this is the best way to go. I don't think I can just change the locale settings for all users as for what I read in docs this will make changes only to the single user settings and I am not really sure how this will affect overall functionality of whole system. Is there any way it can be done rather more easily than parsing this massive file or manually finding this dates?
It is a real pain as this file was designed long time before I came to the team and now I am trying to make this less error prone.
Thanks for any clues in advance!
Real dates are numeric. So you can check with IsNumeric
If IsNumeric(Range("A1").Value2) Then
Debug.Print "date"
Else
Debug.Print "string"
End If
Note that you need to check .Value2 not .Value
Peh's answer guided me to the right solution. Here is the whole code if anyone would came across similar problem:
Sub SanitizeDates()
' ---
' Utility macro that goes over all live sheets and checks all rows
' for the string dates that have been input manually
' and converts it to an actual Date values.
' ---
Debug.Print "--- Log Start"
Dim Prompt As String
Dim Errors As Integer
Dim TotalErrors As Integer
TotalErrors = 0
Errors = 0
Dim Tracker As Workbook
Dim WS As Worksheet
Dim CurrentDateValue As Variant
Dim NewDate As Date
Dim RowCount As Long
Dim nRow As Long
Set Tracker = ThisWorkbook
Application.ScreenUpdating = False
For Each WS In Tracker.Worksheets
If WS.Visible And Not WS.Name = "Matrix" Then ' if worksheet is not visible and not a Matrix
If InStr(1, WS.Name, "Template", vbTextCompare) = 0 Then ' if worksheet is not a template
Errors = 0
RowCount = WS.ListObjects(1).DataBodyRange.Rows.Count
'loop over all rows in table
For nRow = 1 To RowCount
With WS.ListObjects(1).DataBodyRange
' check if the cell is a black bar / divider
If Not .Cells(nRow, 3).Interior.Color = RGB(0, 0, 0) Then
If Not IsNumeric(.Cells(nRow, 3).Value2) Then
On Error GoTo SkipInvalid
NewDate = DateValue(.Cells(nRow, 3).Value2)
.Cells(nRow, 3).Value2 = NewDate
Errors = Errors + 1
'Error logging
'Call LogError(.Cells(nRow, 5), .Cells(nRow, 15), "Date Format - dev")
End If
End If
End With
SkipInvalid:
Next nRow
TotalErrors = TotalErrors + Errors
If Errors Then
Prompt = Prompt & "Found " & Errors & " errors in " & WS.Name & vbCrLf
Debug.Print "Found " & Errors & " errors in " & WS.Name
End If
End If
End If
Next WS
Application.ScreenUpdating = True
Debug.Print "--- Log End"
If TotalErrors Then
MsgBox (Prompt & vbCrLf & vbCrLf & _
"Total of " & TotalErrors & " errors found. All data sanitized successfully.")
Else
MsgBox ("No errors found")
End If
End Sub
Looks like your problem derives from the fact people are entering invalid dates.
You may try to apply a data validation on the cells to sanitize during data entry.
Data validation allows to set a cell as Date and then you can specify a date range. So only valid dates within that range will be allowed.

Calculate time difference between 2 dates and give outputs in total hours is generating output as #### which is wrong

I am very new to Excel, VBA, Macro. My macro was working fine because I gave a simple formula, for example, D2(column name)-C2(column name) = Total time in HH:MM format new column. But I notice for some output is just #### not sure what is wrong. 1).Column)).Formula = _
"=" & cl.Offset(1, 0).Address(0, 0) & "-" & .Cells(2, col1).Address(0, 0)
cl.Offset(, 1).EntireColumn.NumberFormat = "[hh]:mm"
The issue occurs because your date in J is earier than in I and therefore the result is negative. You can use the ABS() function to get the absolute difference as positive value.
Therefore adjust your formula as below:
.Formula = "=ABS(" & cl.Offset(1, 0).Address(0, 0) & "-" & .Cells(2, col1).Address(0, 0) & ")"
You have an incorrect formula in this line:
.Range(cl.Offset(1, 1), .Cells(lastR, cl.Offset(1, 1).Column)).Formula = _
"=" & cl.Offset(1, 0).Address(0, 0) & "-" & .Cells(**2**, col1).Address(0, 0)
Why .Cells(2, col1)? This is always giving you row2 of column 1.
Also, after this line:
If cl.Value = "Full Out Gate at Inland or Interim Point (Destination)_recvd"
Then
Add:
If cl.Offset(0,1).Value = "Response Time" Then Exit For
This will keep you from inserting a new column every time you run the macro.
Try using clear variable names and consistent method for referring to rows and columns.
actCol = col1
recvdCol = cl.Column
responseCol = cl.offset(0,1).Column
.Range(lastR, responseCol).Formula = _
"= Abs(" & .Cells(lastR, recvdCol) & "-" & .Cells(lastR, actCol).Address(0, 0) & ")"
I would use a simpler approach. Highlight the entire table, and click "Format as Table", and be sure to check off "My table has headers." This will give you a named range (default name is Table1, but you can change it). Then, in the Response Time column, simply enter your formula on the first row of the table, but use your mouse to select the cells instead of typing in a cell name like "I2". You will find that the resulting formula includes something like =[#actl]-[#recvd], except that the actl and recvd will be replaced by your actual column names. And, the formula will apply to every row of the table. If you add a new row, the formula will automatically appear in that row. No code needed.
If you have a reason to use code instead of a Table (named ranges), then I would recommend (1) this code be placed directly in the "Main" worksheet module and (2) use use the "Worksheet_Changed" procedure. Microsoft Excel VBA Reference. In this case, any time the
Private Sub Worksheet_Change(ByVal Target As Range)
'Note, Target is the Range of the cell(s) that just changed.
If Intersect(Target, Range("A1:A10")) Is Nothing Or Target.Cells.Count > 1 Then Exit Sub
If ActiveSheet.Cells(1, Target.Column) = "Full Out Gate at Inland or Interim Point (Destination)_actual" Then
' Cell in actual column was modified. Let's set the formula in the Response Time column:
On Error Goto EH
Application.EnableEvents = False
' Add your code here. You'll need to modify it somewhat to accommodate this methodology.
Application.EnableEvents = True
End If
EH:
Application.EnableEvents = True
Err.Raise ' expand this to whatever error you wish to raise
End Sub
Err.Raise help

Offset Height Parameter in VBA

I'm trying to convert an Excel formula into VBA and I'm having some difficulties with the Offset conversion. The formula is being used to perform a VLookup on filtered data.
The Excel formula is:
=VLOOKUP(G4 & "",IF(SUBTOTAL(3,OFFSET(D2:D36419,ROW(D2:D36419)-ROW(D2),0,1))>0,D2:E36419),2,0)
My current VBA code is:
count = Application.WorksheetFunction.VLookup(key & "", _
IIf(Application.WorksheetFunction.Subtotal(3, _
ws.Range("D2:D36419").Offset(ws.Range("D2:D36419").Row - ws.Range("D2").Row, 0)) > 0, ws.Range("D2:E36419"), 0), 2, 0)
I need some way to include the Offset height parameter (1). Any ideas?
Note: I've tried
count = Application.WorksheetFunction.VLookup(key & "", _
IIf(Application.WorksheetFunction.Subtotal(3, _
ws.Range("D2:D36419").Offset(ws.Range("D2:D36419").Row - ws.Range("D2").Row, 0).Resize(1)) > 0, ws.Range("D2:E36419"), 0), 2, 0)
without success.
Try something like this:
Public Sub Test()
Dim SearchRange As Range
Dim FindValue As Range
Dim ReturnValue As Range
With ThisWorkbook.Worksheets("Sheet1")
'Note: I've set this one row above your search range so D2 is first cell that's looked at.
Set SearchRange = .Range("D1:D36419").SpecialCells(xlCellTypeVisible)
Set FindValue = SearchRange.Find(What:=.Range("G4"), After:=SearchRange.Cells(1), _
LookIn:=xlValues, LookAt:=xlWhole, SearchDirection:=xlNext)
If Not FindValue Is Nothing Then
Set ReturnValue = FindValue.Offset(, 1)
MsgBox ReturnValue & " found in " & ReturnValue.Address, vbOKOnly + vbInformation
Else
MsgBox "Nothing found.", vbOKOnly + vbInformation
End If
End With
End Sub
I sometimes find it hard finding the 'correct' VBA alternative to some of the Excel functions. And resort to this technique range("C1").value="=A1+B1" where you put the Excel formula in speech marks.
Sometimes I would need to know the value of A1+B1 for an if statement, but wouldn't need the total of A1+B1 being displayed. So I may do Range("AAA1481").value = "=A1+B1". It is unlikely AAA1481 will have any actual data in it. It should be out of sight and mind.
And then run a if statement such as -
If Range("AAA1481").value > 1000 then
msgbox ("above 1000")
end if
Range("AAA1481").clearcontents
Finding the correct VBA way of writing A1+B1 probably isn't too hard but I find my technique is a godsend when dealing with messy formulas like -
=SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(MAX(NUMBERVALUE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(A:A,"L",".4"),"M",".3"),"S",".2"),"R",".1"))),".4","L"),".3","M"),".2","S"),".1","R")

Creating exact dates in Excel VBA by inputing only the day

In the following picture of an Excel sheet, the heading of the first column, and then of every 7th column after that, contains a month and a year.
I am trying to think of some code which would make entering complete dates under these headings faster. Since the month and the year are already present, I'm thinking there must be a way to enter just the day, and get the whole thing. For example, if "21" were entered in cell A26, "2/21/2015" would result.
Anyone have an idea for how I might get this output?
Edit: Thanks to the helpful replies on this forum, I figured out exactly how to do this. Here is the code for my finished product, in case anyone wants to do something similar:
Private Sub Worksheet_change(ByVal Selection As Range)
Set Sel = Selection
If Sel.Count > 1 Then
Exit Sub
End If
If (Sel.Column - 1) Mod 7 = 0 Or Sel.Column = 1 Then
'In my case, date columns always follow the pattern of 1, 8, 15...
If Sel.Value > 31 Or Sel.Value = "" Then
Exit Sub
Else
Sel.NumberFormat = "General"
Sel.Value = Left(Cells(1, Sel.Column), InStr(Cells(1, Sel.Column), ",") - 1) & " " & _
Sel.Value & Right(Cells(1, Sel.Column), 6)
Selection.NumberFormat = "m/d/yyyy"
End If
End If
End Sub
How about entering the day numbers, selecting the range where these day numbers are entered, and running the below:
Sub Add_month_year()
Dim c As Range
For Each c In Selection
c = Left(Cells(1, c.Column), InStr(Cells(1, c.Column), ",") - 1) & " " & _
c.Value & Right(Cells(1, c.Column), 6)
Next
End Sub
This should return the full dates in date code, which you can then format as you see fit.

Excel VBA correct date type for numbers?

I have a template that records hours worked by employees. Column 5 shows their contracted hours for the week and Column 14 shows additional hours they work. Part time staff (less than 37.5 hrs p/week) who work additional hours are paid a standard rate. However once they exceed 37.50 hours for the week they are paid at time and a half (this is recorded in a seperate column).
The code below picks up the total number of hours for the week (column 18) and if it exceeds 37.5 it will prompt the user to record some of the hours at time and a half. It's a failsafe way of ensuring people are paid correctly.
The code below works almost perfectly however if the contracted hours are less than 10, the message box pops up regardless. I think it is because I have a String data type for the hours in the code is as a String but I can't seem to get it to work with other data types. Any assistance would be much appreciated.
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
If Target.Column = 14 Then
Dim I As Integer, CheckHours As Boolean
Dim MonthX As Worksheet
I = 6
CheckHours = False
Set MonthX = ThisWorkbook.ActiveSheet
Dim FT As String
FT = 37.5
Application.ScreenUpdating = False
'Use the Employee Number column to perform the check
Do While MonthX.Cells(I, 3) <> ""
'Declare variables
Dim ContractHours As String
Dim HoursPaid As String
Dim TotalHours As String
ContractHours = MonthX.Cells(I, 5)
HoursPaid = MonthX.Cells(I, 14)
TotalHours= MonthX.Cells(I, 18)
'If the contract hours plus the additional hours are greater than 37.50 then display warning
If TotalHours > FT Then
MsgBox "WARNING: Check the additional hours entered for " & _
MonthX.Cells(I, 2).Value & " " & MonthX.Cells(I, 1).Value & _
" as they will need to be split between Additional Basic and Overtime." & _
vbNewLine & vbNewLine & _
"Please refer to the Additional Hours Guidelines tab for more information.", vbOKOnly, "Please Check"
CheckHours = True
End If
I = I + 1
Loop
'Cancel boolean
If CheckHours = True Then
Cancel = True
End If
Application.ScreenUpdating = True
End If
End Sub
I don't know if your logic is right, but here's a rewrite that does the same thing as your code. There's a lot of extra stuff in your code that doesn't seem to have a purpose, so I removed it.
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Dim i As Long
Dim dTotalHours As Double
Dim aMsg(1 To 5) As String
Const dFULLTIME As Double = 37.5
i = 6
If Target.Column = 14 Then
Do While Len(Me.Cells(i, 3).Value) > 0
dTotalHours = Me.Cells(i, 18).Value
If dTotalHours > dFULLTIME Then
aMsg(1) = "WARNING: Check the additional hours entered for"
aMsg(2) = Me.Cells(i, 2).Value
aMsg(3) = Me.Cells(i, 3).Value
aMsg(4) = "as they will need to be split between Additional Basic and Overtime." & vbNewLine & vbNewLine
aMsg(5) = "Please refer to the Additional Hours Guidelines tab for more information."
MsgBox Join(aMsg, Space(1)), vbOKOnly, "Please Check"
End If
i = i + 1
Loop
End If
End Sub
Some notes
Excel stores numeric cell values as Doubles. If you're reading a number from a cell, there's really no reason to use anything but a Double.
When you're in the sheet's class module (where the events are), you can use the Me keyword to refer to the sheet. You refer to Activesheet, but what you really want is the sheet where the selection change occurred. They happen to be the same in this case, but for other events they may not be.
It's faster to check the length of a string rather than to check if <>"".
Your FT variable never changes making it not variable at all. A constant may be a better choice.
I use an array to store all the elements of a long message, then use Join to make the final string. Easier to read and maintain.
I'm a keyboard guy, so this hits closer to home for me that most, but a message box every time the selection changes? That means that if I attempt to use the arrow keys to get to the cell where I will fix the error, I will get constant message boxes. Brutal. Maybe the _Change event or _BeforeSave event are worth consideration.
Try declaring as a 'single' instead of a 'String'
We were told to declare decimal numbers as singles when at uni. It may solve your issue.
Or another thing I have notice but don't know if it will affect it, you don't have an ELSE with your IF statement
The following code may need a bit of tweaking, but it should come close to what you need. It implements several of the suggestions in the comments to your question. The source of your difficulty was the use of string variables to deal with numeric values.
I've declared FT, ContractHours, HoursPaid, and SumHours as Single variables, and Cancel as a Boolean (although you don't use it in the subroutine).
You can set "Option Explicit" - which requires that variables be declared - as the default for your code by choosing Tools/Options from the menu bar of the VBA editor and then check-marking the "Require Variable Declaration" option on the Editor tab.
Option Explicit
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Dim i As Integer, CheckHours As Boolean, Cancel As Boolean
Dim MonthX As Worksheet
Dim FT As Single
Dim ContractHours As Single
Dim HoursPaid As Single
Dim SumHours As Single
Set MonthX = ThisWorkbook.ActiveSheet
i = 6
FT = 37.5
If Target.Column = 14 Then
Application.ScreenUpdating = False
'Use the Employee Number column to perform the check
Do While MonthX.Cells(i, 3).Value <> ""
'Assign variables
ContractHours = MonthX.Cells(i, 5).Value
HoursPaid = MonthX.Cells(i, 14).Value
SumHours = MonthX.Cells(i, 18).Value
'When the contract hours plus the additional hours are greater than 37.50
' display warning
If SumHours > FT Then
MsgBox "WARNING: Check the additional hours entered for " & _
MonthX.Cells(i, 2).Value & " " & MonthX.Cells(i, 1).Value & _
" as they will need to be split between Additional Basic and Overtime." & _
vbNewLine & vbNewLine & _
"Please refer to the Additional Hours Guidelines tab for more information.", vbOKOnly, "Please Check"
CheckHours = True
End If
i = i + 1
Loop
'Cancel boolean
If CheckHours = True Then
Cancel = True
End If
Application.ScreenUpdating = True
End If
End Sub

Resources