Macro to Limit % - vba

I have an excel spreadsheet which has multiple tabs and cells filled with % values. There are some cells which read 20%, 30%, 40% etc and the format of these cells are 'percentage'. There is a sum cell which sums up all these percentages to equal 100%. I am trying to write a simple macro/rule which would constantly read the sum cell and throw out an alert if it does not equal 100%. I am not sure how to proceed.
I am trying to use Data Validation to do this instead of any coding as what I really want is for the users to be unable to change the values of cells if the total changes from 100. Data Validation still is not working. It always seems to throw errors. Anyone have any thoughts on this ? I have selected the range of numbers and in data validation I am choosing Custom where I am saying = Sum(C5:C10)=100. This is always throwing an error even when the total is 100. Any thoughts ?

If I've understood your question correctly you'll be able to do this without any code.
For example, your percentage cells are on Sheet3, Sheet4 and Sheet5 in the ranges B2:B6.
On Sheet6 you can use the formula =SUM(Sheet3:Sheet5!$B$2:$B$6)
NB: This is a 3D formula and works providing Sheet4 is between Sheet3 & Sheet5 in tab order.
You can then use conditional formatting on the total - Format only cells that contain - Cell Value is greater than 1.
Then format it as something obvious (I generally use bold yellow text with a red background).
Ok, as reply to your comment, add this code to the worksheet code module which will run every time the sheet calculates:
Private Sub Worksheet_Calculate()
If Cells(6, 3) > 1 Then
MsgBox "Total is too high"
End If
End Sub

I would not use the Worksheet_Calculate event procedure because doing so would cause much too frequent evaluation of the MsgBox condition.
Try to keep that to an absolute minimum like so:
Private Sub Worksheet_Change(ByVal Target As Range)
If Target.Count = 1 Then
If Target.Column = 3 Then
If Target.Row < 6 Then
If Val(Me.[c6]) > 1 Then
MsgBox "Greater than 100%"
End If
End If
End If
End If
End Sub

You can use a UDF to replace the SUM function:
Function SumAndAlert(rng As Range) As Variant
SumAndAlert = Application.Sum(rng)
If Application.CountBlank(rng) = 0 And SumAndAlert <> 1 Then MsgBox "not 100%"
End Function
but I think it might be a little annoying to have message boxes poping out while entering data.

Related

Embedded "IF" formula breaks occasionally, VBA alternative?

I have a very large embedded IF formula that appears to occasionally break for no reason. Opening and closing the page a few times eventually gets it working again. I am wondering if there is a VBA alternative for it. Here is the IF formula I am running.
=IF(ISNUMBER(SEARCH("76210",E125)),"_012_00762_10",IF(ISNUMBER(SEARCH("76220",E125)),"_012_00762_20",IF(ISNUMBER(SEARCH("76900",E125)),"_012_00769_00",IF(ISNUMBER(SEARCH("76901",E125)),"_012_00769_01",IF(ISNUMBER(SEARCH("85702",E125)),"_012_00857_02",IF(ISNUMBER(SEARCH("85710",E125)),"_012_00857_10",IF(ISNUMBER(SEARCH("100800",E125)),"_012_01008_00",IF(ISNUMBER(SEARCH("100900",E125)),"_012_01009_00",IF(ISNUMBER(SEARCH("123100",E125)),"_012_01231_00",IF(ISNUMBER(SEARCH("124600",E125)),"_012_01246_00",IF(ISNUMBER(SEARCH("124601",E125)),"_012_01246_01",IF(ISNUMBER(SEARCH("124640",E125)),"_012_01246_40",IF(ISNUMBER(SEARCH("124641",E125)),"_012_01246_41",IF(ISNUMBER(SEARCH("142301",E125)),"_012_01423_01",IF(ISNUMBER(SEARCH("158801",E125)),"_012_01588_01",IF(ISNUMBER(SEARCH("158900",E125)),"_012_01589_00",IF(ISNUMBER(SEARCH("159203",E125)),"_012_01592_03",IF(ISNUMBER(SEARCH("159303",E125)),"_012_01593_03",IF(ISNUMBER(SEARCH("159401",E125)),"_012_01594_01",IF(ISNUMBER(SEARCH("159410",E125)),"_012_01594_10",IF(ISNUMBER(SEARCH("159420",E125)),"_012_01594_20",IF(ISNUMBER(SEARCH("159501",E125)),"_012_01595_01",IF(ISNUMBER(SEARCH("169000",E125)),"_012_01690_00",IF(ISNUMBER(SEARCH("186900",E125)),"_012_01869_00",IF(ISNUMBER(SEARCH("213200",E125)),"_012_02132_00",IF(ISNUMBER(SEARCH("213300",E125)),"_012_02133_00",IF(ISNUMBER(SEARCH("215400",E125)),"_012_02154_00",IF(ISNUMBER(SEARCH("220100",E125)),"_012_02201_00",IF(ISNUMBER(SEARCH("223800",E125)),"_012_02238_00",IF(ISNUMBER(SEARCH("225600",E125)),"_012_02256_00",IF(ISNUMBER(SEARCH("230700",E125)),"_012_02307_00",IF(ISNUMBER(SEARCH("230701",E125)),"_012_02307_01",IF(ISNUMBER(SEARCH("231800",E125)),"_012_02318_00",IF(ISNUMBER(SEARCH("235000",E125)),"_012_02350_00",IF(ISNUMBER(SEARCH("235020",E125)),"_012_02350_20",IF(ISNUMBER(SEARCH("242000",E125)),"_012_02420_00",IF(ISNUMBER(SEARCH("246400",E125)),"_012_02464_00",IF(ISNUMBER(SEARCH("292900",E125)),"_012_02929_00",""))))))))))))))))))))))))))))))))))))))
Basically it is built so a serial number is scanned and it populates a cell for the users who use this sheet with its results from the search. I am already running one macro in this sheet as well. Here is that...
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
Dim rng As Range
Set rng = Intersect(Range("A2:A500, J2:J500"), Target) ' define range of interest
If Not rng Is Nothing Then ' check it's not "nothing"
If WorksheetFunction.CountA(rng) = rng.Count Then 'check for all of its cells being not empty
On Error GoTo safe_exit 'add error control
Application.EnableEvents = False 'don't do anything until you know something has to be done
rng.Offset(, 1).Value = Date 'write Date next to all relevant changed cells
End If
End If
safe_exit:
Application.EnableEvents = True
End Sub
Maybe there is a better way to build this search using a formula that isn't using embedded IF statements, but i couldn't think of another way to do it. Thanks in advance.
This may be what you're looking for:
=IF(ISNA(MATCH(1,IF(ISERR(SEARCH($A$5:$A$42,$E$125)),0,1),0)),"",INDEX($B$5:$B$42,MATCH(1,IF(ISERR(SEARCH($A$5:$A$42,$E$125)),0,1),0)))
entered as an array formula (CTRL-SHIFT-ENTER).
Here $A$5:$A$42 contains 76210, 76220, ... , 292900 (entered as text, not numbers); and $B$5:$B$42 contains _012_00762_10, _012_00762_20, ... , _012_02929_00.
Hope that helps.
Any time you have to go more than 2 deep on an IF you may want to rethink the usage.
What you can do is build a table from your values. Then reference that table as part of your lookup. Assuming your list of value is in range D8:E45 you could use the formula =VLOOKUP(E125,$D$8:$E$45,2).
The beginning of your table would look like what's seen below. The input result cell is referencing your input value and pulling the match of the second column.
To get your table you can take your source formula and replace (Find and Replace - Ctrl+H) some characters with unique delimiting characters. Then use Text To Columns Alt+D+E and delimit and Copy>Paste special>Transpose to quickly have it close to the format you need.

Incorrect target range in Worksheet_Change event

I am an Excel-VBA newcomer and I am trying to write a short piece of code which is triggered by somebody changing the value of cells in a worksheet. It should set the value of the changed cell to zero if it's less than zero. The code looks like this:
Private Sub Worksheet_Change(ByVal Target As Range)
'Debug.Print Target.Address
If Target.Column = 6 Then
For Each Cell In Target.SpecialCells(xlCellTypeConstants, 3)
If Cell.Value < 0 Then
Cell.Value = 0
End If
Next
End If
End Sub
Now what happens is that when I change the value of any cell in column 6, every cell in the sheet containing numbers less than zero is also changed to zero.
I was thinking that the "Target" Object that is created for the Worksheet_Change event handler would only contain the cell/cells changed, and thus my code would only change the value of cells that were modified and triggered the event in the first place.
I tried to help myself by using Debug.Print to output the address of the object. It printed out the address of every cell in the sheet with a value less than zero, so I assume the handler ran several times.
I actually found a workaround for the problem itself but my questions are these: how did I fail in using the Worksheet_Change event and what can I do in the future to not have such problems?
EDIT 1: Updated code with bug fix suggested in comments
EDIT 2: Updated code with error handling to deal with Array Formulae
In answer to your question
[1] how did I fail in using the Worksheet_Change event and [2] what can I do in the future to not have such problems?
Technically you didn't
Nothing for this type of problem, although the following rule helps in general
You are correct in your understanding of the Targetobject. Your code failed because SpecialCells doesn't like a single cell to operate on. Give it one and it expands it to the entire sheet! Give it anything else and it works just fine.
The reason your Debug.Print displays all the cells is that every time your code changes a cell, another change event is triggered. Luckily for you the second one finds a zero so it doesn't trigger another one. The following general rule should help avoid a lot of problems, just not this particular one:
Always surround any code in an event handler that changes any part of the workbook with Application.EnableEvents.
To fix you code so it works, simply remove the SpecialCells method call. Since you are using Cell.Value instead of the highly recommended Cell.Value2 (see here), VBA implicit type converts numbers formatted as text to actual numbers for you. Thus the code works both on numeric and text values.
Code:
Private Sub Worksheet_Change(ByVal Target As Range)
'Debug.Print Target.Address;
Application.EnableEvents = False
For Each Cell In Target '.SpecialCells(xlCellTypeConstants, 3)
If Cell.Column = 6 And Cell.Value < 0 Then
On Error GoTo Error:
Cell.Value = 0
On Error GoTo 0
End If
Next
GoTo ExitSub:
Error:
If Err.Number = 1004 Then ' 1004 -> "You cannot change part of an array."
'Application.Undo ' Uncomment to disallow array entering a negative value formula into/across column 6
MsgBox "A cell could not be zeroed as" & vbCr & "it is part of an array formula.", vbExclamation, "Microsoft Office Excel"
On Error GoTo 0
Else
On Error GoTo 0
Resume
End If
ExitSub:
Application.EnableEvents = True
End Sub
Notes:
- Update 1: Code now correctly deals with multi-cell changes.
- Update 2: Code now traps error 1004 to deal with entering Array Formulae. It can either allow the array formula to be entered resulting in negative values in column 6, or stop the entry altogether.
This works (Updated)
Private Sub Worksheet_Change(ByVal Target As Range)
Dim c As Range
For Each c In Target
If c.Column = 6 Then If IsNumeric(c) Then If c < 0 Then c = 0
Next c
End Sub
And please, learn and use OPTION EXPLICIT !
I came here looking for a way to stop my worksheet from "flashing" as it ran code to format columns and rows, due to users changing data anywhere in the sheet, including Filters and Sorting.
Aside, from the official answer to the OP's problem, If you're trying to control the Worksheet_Change() event from firing at all (it will still fire but you can bypass it), in circumstances where you want users to change data for example, but not areas around the data, such as fields or labels as part of your VBA system, I use the Target.Row element to determine my limits. Combined with Target.Column, and you can hone-in on specific cells that are free for the user to control.
i.e.
Dim myTopLimit AS Integer
myTopLimit = 6
etc..etc
if Target.Row < myTopLimit and Target.Row > myBottomLimit and Target.Column < myLeftLimit and Target.Column > myRightLimit then
.... code to allow WorkSheet_Change() to do somthing
else
.... code to force WorkSheet_Change() to drop through or no code
End If

Keeping a conditional format on other cells even if value changes?

There is probably a better way to do what this however for science, I have a cell which goes up and down and at certain break points I would like to change the conditional formatting on other cells if a certain number is ever hit I want to change the color of the cell and when it changes I still want that cell to be conditionally formatted. Is this possible using a formula to conditional format?
Example
Let's say 100 is one of my breakpoints.
When I hit 100 on cell A1, I want to change B2 to a green fill
Now let's say A1 Changes from 100 to 125 for whatever reason. I still want to keep the fill on B2
When trying the below code:
Private Sub Worksheet_Change(ByVal Target As Excel.Range)
If Range("A1").value = 100 Then Range("B1").ColorIndex = 4
End Sub
I get this error
Run-Time error '438':
Object doesn't support this property or method
You could start by placing this code in the relevant worksheet code pane (not in a standard module one):
Private Sub Worksheet_Change(ByVal Target As Excel.Range)
If Target.Address = "$A$1" Then
If Target.Value = 100 Then Range("B1").Interior.ColorIndex = 4
End If
End Sub
You just have to change:
colorindex to the wanted color one
100 to the needed breakpoint

Intersect not working if target range gets bigger

I am relatively new to VBA and I need help with this please.
I have a private sub within a sheet and I want it to autofill formulas adjacent to a dynamic named range, if the size of the range changes.
(edit) I am pasting data from another worksheet into this one columns A-M. My dynamic range is defined as =OFFSET($A$1,1,0,COUNTA($A:$A)-1,13). The first If statement should exit the sub if there is no data in column M and I had the destination calculating the last row of column M because I want to fill the formulas in N:O so that they cover the same number of rows as column M.
This is my code and it works if the size of the range gets smaller (i.e. if I delete rows from the bottom), but not if it gets bigger and I can't work out why!
Private Sub Worksheet_Change(ByVal Target As Range)
If Me.Range("M2").Value = "" Then
MsgBox "No Data!"
Exit Sub
Else
If Intersect(Target, Me.Range("rngOracleInvoices")) Is Nothing Then
Application.EnableEvents = False
Dim Lrows As Long
Lrows = Me.Cells(Me.Rows.Count, "N").End(xlUp).Row
Me.Range(Me.Cells(3, 14), Me.Cells(Lrows, "O")).ClearContents
Me.Range("N2:O2").AutoFill Destination:=Me.Range("N2:O" & Me.Range("M" & Me.Rows.Count).End(xlUp).Row)
End If
End If
Application.EnableEvents = True
End Sub
I put the last bit into a separate macro to test if it works on its own and for some reason, when I run it, the autofill goes all the way up to row 1 and overwrites the formulas, which is weird because I use that code a lot and it's never done that before. What have I done??!!
Also, if there is a better way to do the autofill I'd appreciate if someone could let me know what it is because I just cobbled that together from bits I found on forums :)
Thanks,
Soph
In this line Me.Range("N2:O2").AutoFill Destination:=Me.Range("N2:O" & Me.Range("M" & Me.Rows.Count).End(xlUp).Row) you calculate your last row on the column M so if it is empty it'll give you 1 and autofill your formula on row 1.
So start by calculating it on the good column (my guess is O)
You can also simply define an Integer variable to test it and if it is inferior to 2, change it back to 2, 3, 4 or whatever you want.
For your dynamic range, we might need some precision.
And for the AutoFill, you could just select manually the range N2:02 and then double-click on the bottom right square (the one you drag to autofill) and it'll autofill as long as there data in adjacent cells! (give it a try ;) )

Automatically filling in 0 and 0% in Excel blank cells

I am trying to automatically enter 0 and 0% values in blank cells in Excel. I have a report in Excel that gets automatically filled up from SAS. After I save this report, I want the blank cells get automatically filled as 0 in numeric columns and 0% in percent columns.
What would the macro or VBA code be for that?
If you just want to add 0 to a blank cell, there are several ways to do that - here's one using range A1:D10 as an example. Note that if a cell is formatted as percentage, the "%" is automatically appended to the 0. :
Sub test()
Dim cell As Range
For Each cell In Range("A1:D10")
If Len(cell.Value) = 0 Then
cell.Value = 0
End If
Next
End Sub
Please note that if you are doing this on a large range of cells (actually it's good practice to do this all the time), you want to make sure you add Application.ScreenUpdating = False at the start of the code and Application.ScreenUpdating = True at the end. That will make it run much faster.
If the cells are truly blank (ie empty) then there are two ways to fill the cells immediately using SpecialCells, either manually, or with quick code avoiding loops.
David Mcritchie has written this up in detail here.
manual route
Make a selection
PressF5 ... Special .. Goto blanks
in the formula bar add 0
then press Ctrl + Enter
code route
Sub Quickfull()
'reset usedrange
ActiveSheet.UsedRange
On Error Resume Next
ActiveSheet.Cells.SpecialCells(xlBlanks).Value = 0
End Sub