I'm surprised I can't find this anywhere which makes me think I must be going about this incorrectly. I want to be able to include a series of values within sumifs so it performs like a loop for each value in the conditional (without having to write a " +sumifs(....) for each value. Here's an example of what I have so far that is not working
`=SUMIFS(Sum,Range1,Criteria1, '[Stores.xlsx]Sheet1'!$H:$H, "Store #"&Regions!$T:$T&"*")`
So I'm trying to pass every value in Regions!T:T as a criteria.
For example "Store #150 Los Angeles" and "Store #155 San Diego" would both need to be passed through the argument. Currently the formula just returns the first item it matches and doesn't continue to the next item.
I hope that makes sense. Please ask if you need more clarity.
I think the easiest is to start an "intermediate" column next to the T column, do a sumifs for each of the rows of that column, and then sum that column into another cell. Tables or even just array sums may also be helpful here.
I came up with the following in VBA, but I cannot test it completely:
Option Explicit
Function SumSumIfs(ByVal inp As Range) As Integer
Dim i As Integer
Dim QBData As Worksheet
Dim Stores As Worksheet
Set QBData = Workbooks.Open("QBData.xlsx").Sheets("Sheet1")
Set Stores = Workbooks.Open("Stores.xlsx").Sheets("Sheet1")
Dim QBRange1, QBRange2, SalesRange As Range
Set QBRange1 = QBData.Range("H1:H" & Range("H1").End(xlDown).Row)
Set QBRange2 = QBData.Range("I1:I" & Range("I1").End(xlDown).Row)
Set SalesRange = QBData.Range("H1:H" & QBData.Range("H1").End(xlDown).Row)
For i = 1 To inp.End(xlDown).Row
SumSumIfs = SumSumIfs + Application.WorksheetFunction.SumIfs(QBRange1, QBRange2, _
"=" & Stores.Cells(16, 5), StoreRange3, "=" & inp.Cells(i, 19))
Next i
End Function
Again, I'm certain there's a way to do this looping with the formula, but searching around, it was not evident to me.
Related
My file has two identical Worksheets for users to input two different sets of assumption variables, called "InputA" and "InputB". I want to quickly switch which Input sheet is feeding into the other sheets of the model.
Using Find and Replace took over 5 minutes, and there were over 350,000 references to "InputA".
I tried the following macro, which takes an instant to run, but unfortunately also changes all references in the workbook, effectively keeping everything referenced to the original input sheet.
Sheets("InputA").Name = "temp"
Sheets("InputB").Name = "InputA"
Sheets("temp").Name = "InputB"
Is there a way to execute the macro but prevent any references to worksheets from changing to the new sheet name, basically freezing everything except the sheet name change? Or perhaps any other solution that will work quickly? I don't want to go through 350,000 instances and rewrite using INDIRECT(), as that is the only other solution I've seen, because my references are complex and nested and that will take an age.
Thanks.
Assuming that your 2 Input-Sheets have the same structure, I would suggest the following:
Create a named range on Workbook-level, name it for example InputData. This range should contain all data from InputA.
Create a helper-sheet and name it Input - you can later set it to hidden.
Mark the range in the new sheet that is exactly the size of the Input-Data-Range and enter the formula =InputData as Array-formula. You can do so by entering Ctrl+Shift+Enter. The formula should have curly brackets and the sheet should now display the data of InputA.
Change all you formulas to refer to the helper Sheet Input instead of InputA.
Create a macro:
Sub switchInput()
Const sheetAName = "InputA"
Const sheetBName = "InputB"
With ThisWorkbook.Names("inputData")
If InStr(.RefersTo, sheetAName) > 0 Then
.RefersTo = Replace(.RefersTo, sheetAName, sheetBName)
Else
.RefersTo = Replace(.RefersTo, sheetBName, sheetAName)
End If
End With
End Sub
This routine will just change the definition of the named range to point either to the first or second input sheet. As a consequence, the helper sheet will show the data of the selected Input-Sheet. All your formulas itself stays unchanged.
As stated in the comments, you could take the approach recommended by Damian and use a conditional in all relevant cells. The generic conditional would be
=IF(A1="InputA",FORMULA INPUTA,FORMULA INPUTB)
This formula makes A1 the cell that decides which input to pull. This will make changing the around 350.000 output formulas in your workbook the bottleneck, the following macro takes care of changing all the formulas to conatin the conditional:
Sub changeFormulas()
Dim rng As Range, cll As Range
Set rng = shOutput.Range("A2:C10") 'Your relevant worksheet and range here
Dim aStr As String, bStr As String, formulaStr As String
aStr = "InputA"
bStr = "InputB"
For Each cll In rng
If cll.HasFormula And InStr(1, cll.Formula, aStr, 1) Then
formulaStr = Right(cll.Formula, Len(cll.Formula) - 1)
cll.Formula = "=IF(A1=" & Chr(34) & aStr & Chr(34) & "," & formulaStr & "," & Replace(formulaStr, aStr, bStr) & ")" 'Change A1 to the reference most suited for your case
End If
Next cll
End Sub
This might take a bit of time, since it has to access all the relevant cells one by one, but it will only have to run once.
To explain: This macro will go through all the cells in your range rng specified at the top. If a cell has a formula in it and the formula contains "InputA" it will change that formula to fit the generic conditional (with the cells own formula of course). Chr(34) is the quotation mark ", I find using Chr(34) more readable than multiple quotation marks """, but that is just preference.
I have tried searching for the answer for the past couple hours but cannot seem to find what I am looking for - mind you, I am really new to VBA and macros.
What I am trying to do is to add one to what my formula is referencing to every time the macro is run. For example:
Currently the formula is =100*((Q$179/Q$167)-1)
When run, I would like it to be =100*((Q$180/Q$168)-1)
I have to do this for multiple formulas. Is there an easy way to do this with a macro?
Thank you so much in advance
Personally I'd use RegEx to achieve this. The following will increment every cells row by 1 in your formula. This should also work on columns other then Q as well
Option Explicit
Sub IncrementFormula()
Dim tmpFormula As String
Dim RegEx As Object
Dim Match, SubMatch
tmpFormula = "=100*((Q$179/Q$167)-1)"
Set RegEx = CreateObject("vbscript.regexp")
With RegEx
.Global = True
.Pattern = "(?:[A-Z]{1,})(?:\$|)([0-9]{1,})"
If .test(tmpFormula) Then
For Each Match In .Execute(tmpFormula)
For Each SubMatch In Match.submatches
tmpFormula = Replace(tmpFormula, Match, Replace(Match, SubMatch, vbNullString) & CLng(SubMatch) + 1)
Next SubMatch
Next Match
End If
End With
MsgBox tmpFormula
End Sub
Using your formula above it will output =100*((Q$180/Q$168)-1)
If you want it to persist when the workbook is closed, you'll need to store your number somewhere in a worksheet. Say it's in Cell(1,1) of Sheets(2), you could use
dim incVal as integer
incVal = cint(Sheets(2).Cells(1,1))+1
Sheets(2).Cells(1,1) = incVal
dim formula as string
formula = "=100*((Q$" & incVal & "/Q$" & (incVal-12) & ")-1)"
Then just set the desired Cell's formula to that formula. Hope this helps.
A different approach is to use OFFSET in your formula
Assuming the formula is in Q185
then:
=100*((Q$180/Q$168)-1)
Becomes:
=100*((OFFSET(Q185,-5,0)/OFFSET(Q185,-17,0)-1))
As you insert rows at the bottom of the table (and above the formulas), the formula and the cell it refers to will also move down
I have a column number , say columnNumber = 4 . I need the used range of this column. I know how to find the last used row, and I could convert the column number to a column number like so
ColumnLetter = Split(Cells(1, ColumnNumber).Address, "$")(1)
LastRow = sht.Cells(sht.Rows.Count, ColumnLetter).End(xlUp).Row
and then build an address like so
rngaddy = ColumnLetter & "1:" & ColumnLetter & LastRow
and finally do
Range(rngaddy)
But is there an easier way to find the complete used range of a column given it's number ?
Dim rngaddy As Range
With Sheet1
Set rngaddy = .Range(.Cells(1, 4), .Cells(.Rows.Count, 4).End(xlUp))
End With
and if, for some reason, you want to see the address in A1 notation, merely:
debug.print rngaddy.address
Note that in doing it this way, rngaddy is, itself, the range object and not a string. So no need to do Range(rngaddy)
You could return the last populated cell is in columns # col with this:
MsgBox Cells(sht.Rows.Count,col).End(xlUp).Address
If you want to return the first populated cell as well, you could use:
MsgBox IIf(IsEmpty(Cells(1,col)),Cells(1,col).End(xlDown),Cells(1,col)).Address
Therefore this would return only the "used" range of Column #4 (D):
Sub Example_GetUsedRangeOfColumn()
Const col = 4
Dim sht As Worksheet
Set sht = Sheets("Sheet1")
MsgBox Range(IIf(IsEmpty(Cells(1, col)), Cells(1, col).End(xlDown), _
Cells(1, col)), Cells(sht.Rows.Count, col).End(xlUp)).Address
End Sub
So with this example:
...the above procedure would return: .
My preferred method is to use ListObjects aka Excel Tables to hold any input data whenever I possibly can. ListObjects are named ranges that Excel automatically maintains on your behalf, and because they grow automatically when new data is added, they give you a very robust way of referencing ranges in Excel from VBA, that is more immune to users doing things that might otherwise break code reliant on the .End(xlUp) approach.
? Range("MyTable").ListObject.ListColumns("Column 1").DataBodyRange.Address
$A$3:$A$7
Often I'll give the column concerned a named range of its own, in case the user (or a developer) later wants to change the Table column name, and use that name in my code instead.
? Range("FirstColumn").Address
$A$3:$A$7
If somebody (perhaps me) adds rows/columns above/left of the range of interest or shuffles the order of Table columns around, or changes the name of a column, the code still references the intended range and doesn't need to be changed.
? Range("FirstColumn").Address
$C$4:$C$8
? Range(Range("FirstColumn").Address & ":" & Range("FirstColumn").EntireColumn.cells(1).address).Address
$C$1:$C$8
Granted, that method of getting the range from the top cell (which may be above the ListObject) to the bottom of the column concerned is kinda long, but once you start using ListObjects more in your code you normally don't care what is above or below them...you just want the goods held inside.
I haven't used .End(xlUp) in years, other than to find where my data ends should I be in the process of turning it into a ListObject. But I'm a ListObject evangelist...your mileage may vary :-)
to get the real UsedRange of a columns you could use:
With Columns(columnNumber).SpecialCells(xlCellTypeConstants)
Set rngaddy = .Parent.Range(.Areas(1), .Areas(.Areas.Count))
End With
where rngaddy is a Range object
of course what above would fail if the column has no "constant" cells, then you may want to add some error trapping or entry check (e.g. If WorksheetFunction.CountA(Columns(columnNumber)) = 0 Then Exit Sub
Or
Option Explicit
Public Sub test()
Const columnNumber As Long = 4
Dim rngaddy As Range
Set rngaddy = Intersect(Columns(2), ActiveSheet.UsedRange): Debug.Print rngaddy.Address
End Sub
I want to change the value in a column of a table based on the value of another column in the same table using structured references. So far I have something like:
Sub Test
For Each cell In Worksheets("FNM_DB").Range("DB_SS[Resource_Loc_Type]")
If cell.Value = "TH" Then
Worksheets("FNM_DB").Range("DB_SS[Resource]") = Worksheets("FNM_DB").Range("DB_SS[SOURCE_AND_SINK_NAMES]")
End If
Next cell
End Sub
I know I could do this without the structured reference, but I want the code to be more readable. Is there a way to loop through all the values? I know the # refers to a specific row, but I don't know how to use it in this instance.
The above statement
The # sign refers to the current row, but that syntax only works inside a worksheet cell, not within a VBA.
is false.
This works:
[tblActualsWithFacts[#1c]].Select
these work too:
Range("tblActualsWithFacts[#1c]").Select
For i = 1 to 12
Range("tblActualsWithFacts[" & i & " c]").Select
Next i
however, the combination of the above techniques i.e. concatenating a structured reference which makes use of "#" does not work, therefore this does NOT work
For i = 1 to 12
Range("tblActualsWithFacts[#" & i & " c]").Select
Next i
While it is possible to use structured references in VBA, addressing the current row is different. The # sign refers to the current row, but that syntax only works inside a worksheet cell, not within a VBA.
If you are concerned about readability, I'm not sure the long syntax is helpful at all. My suggestion is to create variables that represent the offset of the table column name from the current column. Something like this:
Sub test()
Dim Height As Integer, Width As Integer, Result As Integer
Result = 3
Height = 1
Width = 2
For Each cel In Worksheets("Sheet1").Range("SizingTable[Colour]")
If cel.Value = "red" Then
cel.Offset(0, Result) = cel.Offset(0, Height) * cel.Offset(0, Width)
End If
Next cel
End Sub
I would like to do a vertical lookup for a list of lookup values and then have multiple values returned into columns for each lookup value. I actually managed to do this after a long Google search, this is the code:
=INDEX(Data!$H$3:$H$70000, SMALL(IF($B3=Data!$J$3:$J$70000, ROW(Data!$J$3:$J$70000)-MIN(ROW(Data!$J$3:$J$70000))+1, ""), COLUMN(A$2)))
Now, my problem is, as you can see in the formula, my lookup range contains 70,000 rows, which means a lot of return values. But most of these return values are double. This means I have to drag above formula over many columns until all lookup values (roughly 200) return #NUM!.
Is there any possible way, I guess VBA is necessary, to return the values after duplicates have been removed? I'm new at VBA and I am not sure how to go about this. Also it takes forever to calculate having so many cells.
[Edited]
You can do what you want with a revised formula, not sure how efficient it will be with 70,000 rows, though.
Use this formula for the first match
=IFERROR(INDEX(Data!$H3:$H70000,MATCH($B3,Data!$J3:$J70000,0)),"")
Now assuming that formula in in F5 use this formula in G5 confirmed with CTRL+SHIFT+ENTER and copied across
=IFERROR(INDEX(Data!$H3:$H70000,MATCH(1,($B3=Data!$J3:$J70000)*ISNA(MATCH(Data!$H3:$H70000,$F5:F5,0)),0)),"")
changed the bolded part depending on location of formula 1
This will give you a list without repeats.....and when you run out of values you get blanks rather than an error
Not sure if you're still after a VBA answer but this should do the job - takes about 25 seconds to run on my machine - it could probably be accelerated by the guys on this forum:
Sub ReturnValues()
Dim rnSearch As Range, rnLookup As Range, rnTemp As Range Dim varArray
As Variant Dim lnIndex As Long Dim strTemp As String
Set rnSearch = Sheet1.Range("A1:A200") 'Set this to your 200 row value range
Set rnLookup = Sheet2.Range("A1:B70000") 'Set this to your lookup range (assume 2
columns)
varArray = rnLookup
For Each rnTemp In rnSearch
For lnIndex = LBound(varArray, 1) To UBound(varArray, 1)
strTemp = rnTemp.Value
If varArray(lnIndex, 1) = strTemp Then
If WorksheetFunction.CountIf(rnTemp.EntireRow, varArray(lnIndex, 2)) = 0 Then 'Check if value exists already
Sheet1.Cells(rnTemp.Row, rnTemp.EntireRow.Columns.Count).End(xlToLeft).Offset(0, 1).Value =
varArray(lnIndex, 2)
End If
End If
Next Next
End Sub