Excel VBA - Expand range from using just one column to using multiple - vba

I have a working piece of code that looks at a columns value, copies those values, and strips off the 'speed' component string of that value - Turning '200 Mbps' into just '200', etc.
They updated the source data on me and the values are now in three columns - AC, AD, AE instead of just AC now. So values can exist in either column and any row, can be Gbps and Mbps, etc.
At the end of the day, I need the total of the three columns and X number of rows. I have sample data below.
How can I (or can I even) modify this existing code to account for the extra two columns. I am wondering if the Dictionary approach is correct at this point. It was originally added at someone else's suggestion.
Dim Cla As Range
With CreateObject("scripting.dictionary")
For Each Cla In wbFrom.Sheets("Sheet0").Range("AC9", Range("AC" & Rows.Count).End(xlUp))
Cla.Value = Replace(Cla.Value, " Mbps", "")
Cla.Value = Replace(Cla.Value, " Gbps", "")
If Not .exists(Cla.Value) Then
.Add Cla.Value, Nothing
End If
Next Cla
wbTo.Sheets("Sheet1").Range("D13").Resize(.Count).Value = Application.Transpose(.keys)
End With
I don't really understand the If and With loops and how they combine with keys and Transpose like this. ((Thanks TinMan for the info))
I've tried moving this out, but having this outside the loop breaks the code. Is there something in this section that I need to update?
If Not .exists(Cla.Value) Then
.Add Cla.Value, Nothing
End If
Next Cla
Some sample data looks like this: Notice each element is on its own row.
AC AD AE
300
123
72
200
101
The 300 gets paste where it belongs but nothing else adds up or get grabbed I think. Also, when the data looks like THIS, it pastes two values instead of just one:
Notice the 300 and 123 are now on the same line, 300 gets paste into the destination cell and 123 gets paste into two cells below that.
AC AD AE
300 123
72
200
101

Example 1
Use Range.Resize() to extend the number of columns targeted.
For Each Cla In wbFrom.Sheets("Sheet0").Range("AC9", wbFrom.Sheets("Sheet0").Range("AC" & Rows.Count).End(xlUp)).Resize(, 3)
Next
Example 2
Set a helper variable to the Range and use Range.Resize() to extend the number of columns targeted.
Dim Target As Range
With wbFrom.Sheets("Sheet0")
Set Target = .Sheets("Sheet0").Range("AC9", .Range("AC" & Rows.Count).End(xlUp)).Resize(, 3)
Debug.Print Target.Address
End With
Addendum
This will strip away the " Mbps" and " Gbps" as well as insert the sum the a;; the numbers into Range("D13").
With wbFrom.Sheets("Sheet0")
Set Target = .Range("AC9", .Range("AC" & .Rows.Count).End(xlUp)).Resize(, 3)
For Each Cla In Target
Cla.Value = Replace(Cla.Value, " Mbps", "")
Cla.Value = Replace(Cla.Value, " Gbps", "")
Next Cla
.Range("D13").Value = WorksheetFunction.Sum(Target)
End With

I'm not sure that you are describing the problem correctly. Clearly, using the dictionary, get you a list of unique distinct values. But you stated that:
At the end of the day, I need the total of the three columns and X
number of rows.
The existing code leads me to believe that you are going to be doing a count of different speeds...how many 200 Mbps, how many 72 Mpbs, etc.
Either that, or the previous code didn't work as intended.
Assuming that you described the problem correctly and all you want is the total bandwidth then this should do the trick...
Dim LastRow As Long, Value As Long, Sum As Long, Count As Long
' Get the last row, looking at all 3 columns "AC:AE"
LastRow = FindLastRow(wbFrom.Sheets("Sheet0").Range("AC:AE"))
' Iterate through all 3 columns
For Each Cla In wbFrom.Sheets("Sheet0").Range("AC9:AE" & LastRow)
' Use Val() to get just the numeric value and the inline IIF() statment to automatically adjust the speed
Value = Val(Cla.Value) * IIf(InStr(Cla.Value, "Gbps") > 0, 1000, 1)
' Check if there is a Value, if so, Add it to the Sum (and increment the count)
If Value > 0 Then
Sum = Sum + Value
Count = Count + 1
End If
Next Cla
' Write the Sum to the other Workbook (not sure if you need the Count)
wbTo.Sheets("Sheet1").Range("D13") = Sum
And a function to find the last cell in a range (even if the list is filtered)
Public Function FindLastRow(r As Range) As Long
' Works on Filtered Lists/Hidden rows
Const NotFoundResult As Long = 1 ' If all cells are empty (no value, no formula), this value is returned
FindLastRow = r.Worksheet.Evaluate("IFERROR(LARGE(ROW('" & r.Worksheet.Name & "'!" & r.Address & ")*--(NOT(ISBLANK('" & r.Worksheet.Name & "'!" & r.Address & "))),1)," & NotFoundResult & ")")
End Function

From the comments on your question I gather that you want the sum of the original inputs that are contained in columns AC, AD and AE. Such sum you want to store it at cell d13. Since I have limited input, this is the least ugly code I can provide:
nRow1 = ActiveSheet.Cells(ActiveSheet.Rows.Count, "AC").End(xlUp).Row
nRow2 = ActiveSheet.Cells(ActiveSheet.Rows.Count, "AD").End(xlUp).Row
nRow3 = ActiveSheet.Cells(ActiveSheet.Rows.Count, "AE").End(xlUp).Row
nRow = Application.Max(nRow1, nRow2, nRow3)
input_range = Range("AC9:AE" & nRow)
acum = 0
For Each cell In input_range
If Not IsEmpty(cell) Then
temp = Split(cell, " ")
acum = acum + CInt(temp(0))
End If
Next
Range("D13").Value = acum

Related

Trying to Add a Footnote on the last Page of an Excel File using a loop

I'm currently working on an invoice which could span multiple pages and I want the Signature to appear near the bottom of the last page of the invoice.
My idea:
If the invoice is only one page long I would like to place the signature on Row 39.
If there is data in Row 39,then Place the Footnote at the end of the next page which is Row 86 (add 47 rows).
Continue doing that until an empty row is found. So if Row 86 has Data add another 47 Rows and place the footer in Row 133.
I'm having some trouble figuring out how to get the loop to work, I know how to get a loop to work when you're using a count Do while i > (insert amount here) but I don't know how to do it until it finds an empty row.
Don't do it exactly that way. Start with the last used row (plus one), then use a loop to find the next appropriate row after that.
lastRow = activesheet.usedrange.rows.count + 1
sigRow = 39
while sigRow < lastRow
sigRow = sigRow + 47
wend
activesheet.cells(sigRow, c) = signature
Now, this is pseudocode, so you will need to adapt it to your specific use. For instance, you have to define the signature, and tell it what column to put it in (c is the placeholder). You may want to explicitly name the worksheet instead of just using activesheet.
Ended up Solving it
Public Sub Signature()
Dim Signature_Line As String
Dim Signature_Labels As String
Dim Signed As Integer
Dim Signature_Row As Integer
Signature_Line = " _________________________ _________________________ ______________________________"
Signature_Labels = " Name Date Signature"
Signed = 0
Signature_Row = 39
Do Until Signed = 1
If IsEmpty(ActiveSheet.Range("A" & Signature_Row - 1)) = True Then
ActiveSheet.Range("A" & Signature_Row).Value = Signature_Line
ActiveSheet.Range("A" & Signature_Row + 1).Value = Signature_Labels
Signed = Signed + 1
Else
Signature_Row = Signature_Row + 47
End If
Loop
End Sub

I have 3 excel formulas that I would like to fill ranges in a spreadsheet with, how can I make sure the cells change with the rows?

=IF(AND(G2<>100,TODAY()>=H2, TODAY()<=I2), E2, " ")
=IF(N2=" ", " ",NETWORKDAYS(H2,TODAY()))
=IF(OR(O2 = " ", O2 <= 0), " ", (O2/N2)*100)
These are the three formulas, I want to make sure that as they are inserted into the worksheet the cell references will still change to match the rows they are on, as they would in a normal spreadsheet. Any advice would be much appreciated! (To clarify, I need to fill the ranges using VBA as the code I'm using clears the worksheet every time it is run.)
you could use FormulaR1C1 property of range object, which uses the "R1C1" notation for range addresses
for instance inserting your first formula in "A1" would be:
Range("A1").FormulaR1C1 = "=IF(AND(RC7<>100,TODAY()>=RC8, TODAY()<=RC9), RC5, "" "")"
where the pure R would assume the current cell row index, while C7 stands for a fixed (not varying with host cell position) 7th column index reference, and so on
If i have interpreted your question correctly, you need something like the below:
Option Explicit
Sub InsertFormula()
Dim i As Long
Dim n As Long
n = Cells(Rows.Count, 1).End(xlUp).Row
For i = 1 To n
Cells(i, 1).Formula = "=IF(AND(G2<>100,TODAY()>=H2, TODAY()<=I2), E2, "" "")"
Next i
End Sub
replace the 1 in n=... with whichever column has the most rows of data
replace for i = 1 to whichever row it must begin form
You will notice i have added extra quotations to the end of the formula, this is needed as quotes in a formula in VBA must be enclosed... in more quotes lol
Apply this concept for the other formulas :)
Instead of absolute references like G2 you can use something along
.FormulaR1C1 = "=SUM(RC[-2]:R[5]C[-2])"
where R and C reference the offset from the current cell (positive: right or down, negative: up or left).
Use it in a way similar to this:
Dim c
For Each c In Selection
c.FormulaR1C1 = "=SUM(RC[-2]:R[5]C[-2])"
Next c
Relative References are adjusted when you set the formula to range of cells:
[A1:B2].Formula = "=C$1" ' now the formula in B2 will become "=D$1"
You can also set multiple formulas at once:
Range("K2:M9").Formula = Array("=IF(AND(G2<>100,TODAY()>=H2, TODAY()<=I2), E2, "" "")", _
"=IF(N2="" "", "" "",NETWORKDAYS(H2,TODAY()))", _
"=IF(OR(O2 = "" "", O2 <= 0), "" "", (O2/N2)*100)" )
or if each row has different formula:
[A1:Z3] = [{"=1";"=2";"=3"}]

Merge or concatenate column by adjacent cell value

Is it possible to concatenate cells in a column dependent on values (ID) in another column, and then output as a string (possibly in another sheet as it would be cleaner)?
E.g.
ID Text
1234 a
1234 b
1234 c
4321 a
4321 b
4321 c
4321 d
Output:
1234 a b c
4321 a b c d
Issues:
Column IDs aren't in order (but can be sorted).
Different amounts of each ID
This seemed like a possible VBA solution
from How to merge rows in a column into one cell in excel?
Function JoinXL(arr As Variant, Optional delimiter As String = " ")
'arr must be a one-dimensional array.
JoinXL = Join(arr, delimiter)
End Function
Example usage:
=JoinXL(TRANSPOSE(A1:A4)," ")
So I thought maybe if INDEX and MATCH etc could be used in conjuction with TRANSPOSE it could work. Just not sure how to go about it.
I can have a column of the unique IDs in another sheet.
This solution is nice because it works even in cases where:
The text you're concatenating contains spaces.
You want to use a delimiter other than a space (ie a comma).
First, add "Transpose" to your custom function
Function JoinXL(arr As Variant, Optional delimiter As String = " ")
'arr must be a one-dimensional array.
arr = Application.Transpose(arr)
JoinXL = Join(arr, delimiter)
End Function
For vertical arrays, use this formula (replace comma+space with your delimiter of choice, and "{}" with your garbage characters of choice):
{=SUBSTITUTE(SUBSTITUTE(JoinXL(IF($A$2:$A$31=D3,$B$2:$B$31,"{}"),", "),"{}, ",""),", {}", "")}
For horizontal arrays, use this formula:
{=SUBSTITUTE(SUBSTITUTE(JoinXL(IF(TRANSPOSE($E$19:$AH$19)=D12,TRANSPOSE($E$20:$AH$20),"{}"),", "),"{}, ",""),", {}", "")}
Make sure you enter the formulas as array formulas (Ctrl+Shift+Enter after typing formula in cell).
While no convenient function like your cited example, consider using a dictionary of collections with the ID column as the key. Below macro assumes data begins at A2 (column headers in first row) with result outputting in D and E columns:
Sub TransposeValuesByID()
Dim i As Integer, lastrow As Integer
Dim valDict As Object
Dim innerColl As New Collection
Dim k As Variant, v As Variant
Set valDict = CreateObject("Scripting.Dictionary")
lastrow = Cells(Rows.Count, "A").End(xlUp).Row
For i = 2 To lastrow
If Range("A" & i) = Range("A" & i + 1) Then
innerColl.Add Range("B" & i)
Else
innerColl.Add Range("B" & i)
valDict.Add CStr(Range("A" & i).Value), innerColl
Set innerColl = Nothing
End If
Next i
i = 2
For Each k In valDict.keys
Range("D" & i) = k
For Each v In valDict(k)
Range("E" & i) = Trim(Range("E" & i) & " " & v)
Next v
i = i + 1
Next k
End Sub
Since all you want is space between you can use your code with a couple changes.
If your data is Vertical you need to transpose the array to make it a one dimensional array:
Function JoinXL(arr As Variant, Optional delimiter As String = " ")
'arr must be a one-dimensional array.
arr = Application.Transpose(arr)
JoinXL = Join(arr, delimiter)
End Function
If it is horizontal then use what you have.
The main change is how you call it.
Use the following array formula:
=TRIM(JoinXL(IF($A$2:$A$8=C2,$B$2:$B$8,"")," "))
being an array it needs to be confirmed with Ctrl-Shift-Enter instead of Enter when exiting edit mode. If done correctly then Excel will put {} around the formula.
The If passes an array of values or blanks depending on if the cell is equal to the criteria.
Put this in the first cell Hit Ctrl-Shift-Enter. Then drag/copy down
I've come up with a work around, that although a little cumbersome works quite nicely.
Table has to be sorted by ID.
In another sheet.
ID: (Column A)
1234
MIN REF: (Column B)
=ADDRESS(MATCH(A2,OCCRANGE,0)+1,6)
MAX REF: (Column C)
=ADDRESS(MATCH(A2,OCCRANGE)+1,6)
RANGE: (Column D)
=CONCATENATE("'OCCS COMBINED'!",B2,":",C2)
STRING: (Column E)
{=IF([#[MIN REF]]=[#[MAX REF]],INDIRECT(CONCATENATE("'OCCS COMBINED'!",[#[MIN REF]])),JoinXL(TRANSPOSE(INDIRECT(D2)), " "))}

MS Excel 2010 - VBA to lookup in one column a customer number and Tag the corresponding column with Yes or No

I have an extremely large dataset with customer numbers and we cannot just use a =IF(E3=160248, "YES", "NO") to tag a particular customer number of 160248 with YES or NO. Instead, I would like to use VBA code to lookup Customer_Number in column E and return a YES or NO in the corresponding row in Column AG, called Incorporated_160248. I have not done an If then clause in VBA, so I have no idea where to start. Please note, each month the data set can change. One month it could be 4,000 entries and the next 3,500, so that has to be dynamic. Any thoughts?
Sub TagTryco()
Dim CN As Integer, result As String
CN = Range("E:E").Value
If CN = 160248 Then
result = "YES"
Else
result = "NO"
End If
Range("AG:AG").Value = result
End Sub
I get a Compile error: Wrong number of arguments or invalid property assignment.
This CODE Works now:
Sub TagTryco()
Dim listLength
listLength = Worksheets("ILS_Import").Cells(Rows.Count, "E").End(xlUp).Row - 1
Dim i As Integer
For i = 2 To listLength + 2
If Worksheets("ILS_Import").Range("E" & i) = 160248 Then
Worksheets("ILS_Import").Range("AG" & i) = "Yes"
Else
Worksheets("ILS_Import").Range("AG" & i) = "No"
End If
Next
End Sub
To know how many entries you have:
dim listLength
listlength = Sheet1.Cells(Rows.Count, "E").End(xlUp).Row - 1 'I assumed column E, starting at row 2
You need to loop from row 2 to the row 2 + listLength, check the cell in column E, and check if it is equal to your number:
dim i as integer
for i = 2 to listLength + 2
If Range("E" & i) = 160248 Then
Range("AG" & i) = "Yes"
Else
Range("AG" & i) = "No"
End If
Next
If you wish to scan for different numbers you can adapt the code to use a value from a cell in which you enter that number, OR use an inputbox to enter the number you want to look for, or something else. This code was not tested.
If you want to use the column name you assigned instead of AG (which is safer) you can use something along the lines of:
= Range("Incorporated_160248")(i+1)
Instead, which gives the column with an offset of i. Should bring you to the right cell.

VBA excel finding the statistical mode of a collection

So I'm trying to analysis some data in Excel and having some trouble finding the most frequent numbers. I have an unknown number of locations which can have an unknown number of donations. For example
Brantford $50.00
Brantford $25.00
Brantford $50.00
Windsor $200.00
Quebec $25.00
Quebec $100.00
Quebec $50.00
Quebec $50.00
Quebec $25.00
Quebec $50.00
Quebec $50.00
Quebec $25.00
Quebec $100.00
Quebec $40.00
Windsor $140.00
Windsor $20.00
Windsor $20.00
So I need to use VBA to find for each location the count, sum, mean and mode (has to be done through VBA, can't just write instructions on how to do this using advanced filters/pivot tables :().
So right now using VBA I have a dictionary object that stores the location name as a key and each donation in a collection. Using the count of the collection I have the count, can easily loop through the collection for the sum, using those I have the mean; but, I am not sure the most efficient way to get the mode.
I know I can find it if my data was in an array using Application.mode, but that doesn't appear to work for collections :(. Converting a collection to an array though to find the mode though really doesn't strike me as the most efficient solution. Only other option i can find of though is to sort the collections then loop through them to find the mode.
So wondering if anyone knows of a good way to find the statistical mode of a collection?
Dim locdata As Object
Set locdata = CreateObject("scripting.dictionary")
For counter = 2 To max
mykey = Cells(counter, loccol).value
If Not (locdata.exists(mykey)) Then
locdata.Add (mykey), New Collection
End If
locdata(mykey).Add (Cells(counter, donamountcol).value)
Next counter
For Each k In locdata.keys
locname = k
Cells(counter, 1) = k
Cells(counter, 2) = locdata(k).Count
donationtotal = 0
For Each donvalue In locdata(k)
donationtotal = donationtotal + donvalue
Next donvalue
Cells(counter, 3) = donationtotal
Cells(counter, 4) = donationtotal / CDbl(locdata(k).Count)
'Cells(counter, 5) = Application.mode(locdata(k)) doesn't work :(
counter = counter + 1
Next k
edit: Ideally the output should be (using Quebec as an example)
Quebec: Count: 10 Sum: 515 Average: 51.5 Mode: 50
Here is how you can have values in a range into aarray dynamically. And I would use CountIF in the VBA to find the most frequent objects by their names.. Since you don't know the location names or the donations. Then array is the way to go.
Dim ar as Variant
Dim endRow as Long
'get last row in the range
endRow = Sheets(1).Cells(Sheets(1).Rows.Count, "A").End(xlUp).Row
'ar = WorksheetFunction.Transpose(Shets(1).Range("A1:A12")
'using endrow
ar = WorksheetFunction.Transpose(Shets(1).Range("A1").resize(endRow).value)
UPDATE: The subroutine below uses one iteration (for loop) to find the Mode..
Sub FrequencyByLocDonations()
Dim ar As Variant, dc As Object
Dim rngInput As Range, mxRng As Range
Dim endRow As Long, i As Integer
Dim counts As Double, maxLoc As Double
Dim maxLocation As String
Set dc = CreateObject("Scripting.Dictionary")
'-- When you know the range
' ar = WorksheetFunction.Transpose(Shets(1).Range("A1:A12").Value
'get last row in the range when you don't know but the starting cell
endRow = Sheets(3).Cells(Sheets(3).Rows.Count, "C").End(xlUp).Row
Set rngInput = Sheets(3).Range("C2").Resize(endRow - 1, 1)
'--you may also use that set rngInput as well
' WorksheetFunction.Transpose(rngInput).Value
'-- using endrow-1 to not to take an extra blank row at the end
ar = WorksheetFunction.Transpose(Sheets(3).Range("C2").Resize(endRow - 1, 2).Value)
For i = LBound(ar, 2) To UBound(ar, 2)
If Not (dc.exists(ar(1, i))) Then
counts = Application.WorksheetFunction.CountIf(rngInput, ar(1, i))
If counts >= maxLoc Then
maxLocation = ar(1, i)
maxLoc = counts
End If
dc.Add ar(1, i), counts
End If
Next i
'-- output to the Sheet
Sheets(3).Range("C2").Offset(0, 2).Resize(UBound(dc.keys) + 1, 1) = _
Application.Transpose(dc.keys)
Sheets(3).Range("C2").Offset(0, 3).Resize(UBound(dc.items) + 1, 1) = _
Application.Transpose(dc.items)
Sheets(3).Range("C2").Offset(0, 4) = "Most Frequent Location :" _
& maxLocation & "; " & maxLoc
Set dc = Nothing
End Sub
output:
I have run into a similar situation in the past. It seemed to me there was a very powerful VBA function missing from excel - equivalent to the "where" statement in MySQL.
So I wrote a very simple one myself... This lacks a lot of functionality, but it will allow you to do what you are asking for, while minimizing the amount of code you write.
Fundamental concept: you can return an array from a function call, and Excel built-in functions can operate on such an array as they would on a function. Thus, if you have a function that returns "all the numbers of which I want the mode", then =MODE(myfunction()) will give you the answer you want.
I chose to call my function subset(criteria, range1, range2).
In its most simple form it returns the elements in range2 that correspond to elements in range1 that meet criteria.
This is NOT extensively tested, but I hope you get the idea.
By the way, you can enter this as an array formula (shift-ctrl-enter) in multiple cells; in that case you get the first returned element in the first cell, etc. Sometimes that's a useful trick when you have a function that needs to return more than one value (e.g. a range) - but for this case you only need the result to feed to another function.
Option Explicit
' Function subset(criteria, range1, range2)
' Return an array with the elements in range2 that correspond to
' elements in range1 that match "criteria"
' where "criteria" can be a string, or a value with a < = > sign in front of it
' example: =subset("bravo", A1:A10, B1:B10)
' returns all cells from B that corresponds to cells in A with "bravo"
' =subset("<10", A1:A10, B1:B10) returns all cells in B corresponding to
' cells in A with a value < 10
' This is analogous to the "where" function in SQL, but much more primitive
Function subset(criteria As String, range1 As Range, range2 As Range)
Dim c
Dim result
Dim ii, jj As Integer
On Error GoTo etrap
If range1.Cells.Count <> range2.Cells.Count Then Exit Function
ReDim result(1 To range1.Cells.Count)
ii = 1
jj = 1
For Each c In range1.Cells
If compare(c.Value, criteria) = 0 Then
result(ii) = range2.Cells(jj).Value
ii = ii + 1
End If
jj = jj + 1
Next c
If ii > 1 Then
ReDim Preserve result(1 To ii - 1)
subset = result
Else
subset = Nothing
End If
Exit Function
etrap:
MsgBox "Error " & Err.Description
End Function
Private Function compare(a, b)
' type of a decides what kind of comparison we do
If TypeName(a) <> TypeName("hello") Then
' use numerical comparison
compare = Not (Evaluate(a & b))
Else
' use string comparison
compare = StrComp(a, b, vbTextCompare)
End If
End Function
I actually just decided to make a dictionary of dictionaries. So I have the locations and each location than has a dictionary of the count of each donation amount. Was easy enough to compare counts that way to find the mode.