I have an Excel sheet with prices (of materials used in construction) from previous years.
I am trying to make a code that will show me all the data based on a certain year that I choose.
What my list/excel looks like
In cell E3 (in yellow), I input the year to "analyse".
I need a function that will search columns K to Q (and more since every year, prices change), for that year, and copy all the data of sales, reg. loc. and spec. loc. into the columns F, G and H.
If it's easier that, instead of putting the year in cells K3, L3 and M3 (for example), if I put it in cell N3, R3, etc. (in red) instead, the function will take the 3 previous columns and copy/paste them in columns F to H.
Option 2, if it makes the coding easier
Also the list goes until row 381, and there's a potential that more data will be input eventually so take into consideration as if the list had an infinite amount of rows. However, for the columns, it's always fixed to 3 columns.
FYI: it is not a school project. I'm trying to simplify my work instead of manually searching and copy/pasting the data every time.
Please, try the next code. It should do what (I understand) you need. It should be fast, not using clipboard for copying. As I suggested in my comment, it firstly searches/finds in the third row the year (long or string, as it is written in "E3"), starting searching after "E3", then copying the range built according to the found cell. If not a match is found, the code exits on the line If rngFirstCol Is Nothing Then Exit Sub. You may place a message there, to warn in such a case. It works on your first arrangement/picture, meaning that the year must be filled in the third row of the first column where from the necessary data should be collected/copied:
Sub ExtractPricesPerYear()
Dim sh As Worksheet, lastR As Long, rngFirstCol As Range, lngYear, necCol As Long
Set sh = ActiveSheet 'use here your necessary sheet
lngYear = sh.Range("E3").value 'the year to be searched
Set rngFirstCol = sh.rows(3).Find(What:=lngYear, After:=sh.Range("E3"), LookIn:=xlValues, Lookat:=xlWhole)
If rngFirstCol Is Nothing Then Exit Sub
necCol = rngFirstCol.Column
lastR = sh.cells(sh.rows.count, necCol).End(xlUp).row
With sh.Range(rngFirstCol.Offset(1), sh.cells(lastR, necCol + 2))
sh.Range("E4").Resize(.rows.count, .Columns.count).value = .value
End With
End Sub
Please, send some feedback after testing it.
And another issue: It is good to show us what you tried by your own. If not a piece of code, at least, something to prove that you investigated and had some ideas about the task to be solved, asking for hints, suggestions etc. proving that you know something about how it can be done...
Edited:
Following your requirement from last comment, please use the next solution. Please, copy the next code in the respective sheet code module (right click on the sheet name, then choose View Code):
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
Dim lastR As Long, lastRF As Long, rngFirstCol As Range, lngYear, necCol As Long
If Target.Address(0, 0) = "E3" Then 'the code exits for any other change on the sheet
lngYear = Target.value 'the year to be searched
Set rngFirstCol = Me.rows(3).Find(What:=lngYear, After:=Me.Range("E3"), LookIn:=xlValues, Lookat:=xlWhole)
If rngFirstCol Is Nothing Then MsgBox "No such year found on the third row...:": Exit Sub
necCol = rngFirstCol.Column 'column number of the found cell
lastR = Me.cells(Me.rows.count, necCol).End(xlUp).row 'last row on the found column
lastRF = Me.Range("F" & Me.rows.count).End(xlUp).row 'last row on F:F column (to delete its content, if any)
If lastRF > 4 Then Me.Range("F4:H" & lastRF).ClearContents 'clear the range to make place for the new data
With Me.Range(rngFirstCol.Offset(1), Me.cells(lastR, necCol + 2))
Me.Range("F4").Resize(.rows.count, .Columns.count).value = .value
End With
End If
End Sub
Related
I want to insert a timestamp (E3) when the status (B3) changes. This should happen for at least 30 more such examples in the worksheet. The code currently works only for one example (Country1). Do you have an idea how this can be implemented?
I already tried different types but it just worked for example "Country 1" not for "Country 1", "Country 2", "Country 3" etc.
When I adjust the code for the range "B3:I3" then I received an adjustment in every 3rd column, example: I add a comment in D3 then a timestamp will be created in H3. That is not what I want. :(
Is there a way to adjust the code so that as soon as a change is made in the Status column (B3;F3;J3etc.), the Timestamp column (E3;I3 etc.) will reflect the time stamp?
Code:
Private Sub Worksheet_Change(ByVal Target As Range)
If Intersect(Target, Range("B3:B5"))
Is Nothing Then Exit Sub
Application.EnableEvents = False
Target.Offset(0,3).Value = Now
Application.EnableEvents = True
Please, try the next adapted event. It will calculate how many groups of four columns exists and set a range of their first column intersected with rows 3 to 5. Only for this range the event will be triggered:
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
Dim lastCol As Long, rngCols As Range
lastCol = Me.cells(2, Me.Columns.count).End(xlToLeft).column 'last column on the second row
Set rngCols = Me.Range(trigData(Me.Range("B2", Me.cells(2, lastCol)))) 'create the range of the columns for what the event to be triggered
Set rngCols = Intersect(Me.rows("3:5"), rngCols) 'create the range inside which the change to trigger the event
If Not Intersect(rngCols, Target) Is Nothing Then
Application.EnableEvents = False
Target.Offset(0, 3).Value = Now
Application.EnableEvents = True
End If
End Sub
Function trigData(rngCols As Range) As String
Dim i As Long, strCols As String
For i = 1 To rngCols.Columns.count Step 4 'iterate from four to four and create the necessary columns string address
strCols = strCols & "," & rngCols.cells(i).EntireColumn.address
Next i
trigData = Mid(strCols, 2) 'Mid eliminates the first (unnecessary) comma...
End Function
The code will be confused if you place on the second row data after the necessary groups of four columns. If necessary, one or two such columns, the code can be adapted to work for a fix number extracting the divided integer (without decimals).
The code assumes that you need to be triggered for the mentioned rows (3 to 5). If you need something different in terms of rows to be affected, you should change Me.rows("3:5") according to your need.
Please, send some feedback after testing it.
Your request is a little unclear, and your table format may not have come across correctly in your post. Your code is written to add the current time to a cell 3 columns away from the target cell. It is dynamic, so if you set
If Intersect(Target, Range("B2:I3"))
You are going to get the value in cell 3 columns offset from the changed cell. If you always want it to update column E, then you can use the target.row property...
Cells(Target.Row,5).Value = Now
...to make the row dynamic, and the column static. Clarify your question if this is not what you're looking for. If country2 is in cell F2, where do you want to write the timestamp?
You can use this simple function:
Public Function TimeStamp(Status As Range) As Double
TimeStamp = Now
End Function
So, in Cell E3 will be the formula =TimeStamp(B3). (Format cell E3 appropriately as Time Format)
I have no experience with VBA and would love some help. As the title indicates, I'm looking for a script that fills a certain number of blank cells within column G with the average of all nonblank cells within that same range (e.g. fill all blank cells in G16:G59 with the average of all nonblank cells within G16:G59).
To make things more complicated, I'd need to vary the range somewhat dynamically as I wouldn't be sure as to how many rows I'd need to apply this script to and a work colleague who would be using this script might not have any experience with VBA either... The easiest solution I can think of is to have another cell contain the name of the last row in the range, or something like this: "Fill all blank cells in G16:Gx with the average of all nonblank cells within G16:Gx, where x = the row name listed in cell G12". Within G12 I'd have a text that states the last row to define the range, e.g. cell G12 contains the text "G80", which makes the range within the script to read G16:G80.
I know I'm asking for a lot, so if you can even just provide guidance on the first bit, I'd be very grateful! Thank you in advance for your time.
I think we can all remember what it was like when we first started out with VBA coding – which is why I’m helping you here. Normally, you’d be lucky to get any assistance with your question without providing at least some code & a description where it wasn’t doing what you wanted. Using the Record Macro button is always a good place to start.
Assumptions made here are that the data is on “Sheet1” in your file, and that there is a value in the last cell in Column G in the range you’re interested in. If that isn’t the case, let me know and I’ll show an alternative method to find the last row.
I’ve added descriptions about what (most) code does in each case to help you understand what’s going on. Let me know how you go with it.
Option Explicit '<~~ get in the habit of putting this at the top of your code
Sub FillInBlanks()
Dim ws As Worksheet '<~~ declare all variables
Dim LastRow As Long '<~~ use Long not Integer
Dim cel As Range '<~~ use intuitive variable names
Dim avg As Double '<~~ Double used here if you want decimal places in the average
Set ws = Sheets("Sheet1") '<~~ be explicit with sheet references
'Best practice to determine last used row in column
LastRow = ws.Cells(Rows.Count, "G").End(xlUp).Row '<~~ finds the last used row in column G
'Calculate the average & assign it to the variable "avg"
avg = Application.Average(ws.Range("G16:G" & LastRow))
'Loop through each cell in the defined range - one at a time
For Each cel In ws.Range("G16:G" & LastRow)
If cel = "" Then '<~~ = If the cell is blank, then...
cel = avg '<~~ ...put the average value (avg) in the cell
cel.Font.Color = RGB(51, 102, 0) '<~~ change color to suit
End If
Next cel '<~~ go to the next cell in the range
End Sub
I have a report of around 250 rows (varies +/- 15 rows, columns A – J). To make the report easier-on-the-eye for end users, I format every second row with Zebra Stripes. The problem is that whenever a filter is applied to the data, the zebra stripes (rows) sometimes bunch together which defeats the purpose of having them at all.
What I would like to do is have the zebra stripes appear on every second visible row after the filter is changed - and I know such code already exists on this site – but I furthermore want to have this happen automatically without the need to trigger any code.
My current macro works fine as far as adding the formatting – but doesn’t achieve the ideal objective stated above. Any guidance is appreciated.
Sub AddZebraStripes()
Dim c As Range, LastRow As Long
LastRow = Sheet1.Cells(Rows.Count, "A").End(xlUp).Row
Sheet1.Range("A2:J" & LastRow).Interior.ColorIndex = xlNone
For Each c In Sheet1.Range("A2:J" & LastRow)
If c.Row Mod 2 = 0 Then
c.Interior.ThemeColor = xlThemeColorDark1: c.Interior.TintAndShade = -0.15
End If
Next c
End Sub
You can also easily use conditional formatting if you have a column which will always have data in it.
The references are in the formula are pointed to the first cell in the data column of choice
Please note the absolute vs non-absolute cell reference in the formula. Set the format for true, then depending how you want the banding you can reverse "TRUE" and "FALSE" in the formula
=IF(MOD(SUBTOTAL(103,$A$2:$A2),2)=0,"TRUE","FALSE")
UPDATE
To allow for blank values in data column:
=IF(MOD(SUBTOTAL(103,$A$2:$A2)+COUNTBLANK($A$2:$A2),2)=0,"TRUE","FALSE")
I agree with Rory and Siddharth that using a table as the simplest option. However, if you’re stuck on using a VBA approach then this is how I would do it:
You can trigger the code automatically by using a Worksheet_Calculate() event handler – based on a calculation of a formula on the sheet.
First step is to add the following formula to cell K1 (for example based on your description of your data) on your sheet: =SUBTOTAL(3,$A:$A). This particular SUBTOTAL (3,) = COUNTA(). Whenever the filter changes, it will recalculate – even if the same number of rows are visible in Col A. You can hide Col K if you like – it won’t affect the code execution.
Step Two – add the following Worksheet_Calculate() event handler to the Sheet1 module
The following code will run the macro ZebraStripes whenever it detects a calculation in cell K1 on Sheet1.
Private Sub Worksheet_Calculate()
Set Target = Me.Range("K1")
If Not Intersect(Target, Range("K1")) Is Nothing Then
ZebraStripes
End If
End Sub
Step Three – add the following code to a standard module
Option Explicit
Sub ZebraStripes()
Dim LastRow As Long, i As Long
Dim c As Range, r As Range
Dim ws As Worksheet
Set ws = Sheet1
LastRow = ws.Cells(Rows.Count, 1).End(xlUp).Row
i = 1
ws.Range("A2:J" & LastRow).Interior.ColorIndex = xlNone
For Each c In ws.Range("A2:A" & LastRow)
If c.EntireRow.Hidden = False Then
i = i + 1
If i Mod 2 = 0 Then
Set r = Range(c, c.Offset(0, 9))
r.Interior.ThemeColor = xlThemeColorDark1: r.Interior.TintAndShade = -0.15
End If
End If
Next c
End Sub
If you follow the 3 steps above precisely, you should achieve your ideal objective of every second row shaded – automatically whenever there’s a change to the filter on the sheet. Looking at all the steps/code, I think you might be better off with a Table :)
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'm trying to build a formula:
=BDS(Bonds!J2& " ISIN","ISSUE_UNDERWRITER","Headers","Y")
In one sheet that takes a unique identifier from another table.
These formula builds me a table. After it builds me the table, I need to take the next row in the other sheet:
=BDS(Bonds!J3& " ISIN","ISSUE_UNDERWRITER","Headers","Y")
Then insert that formula a the end of the previous table built by the previous formula.
What I tried was getting the last row and then offsetting it by one, but I'm trying to figure out how to loop through it.
This is what i have tried:
Sub Formula2()Formula2 Macro
Range("A1").Select
ActiveCell.FormulaR1C1 = _
"=BDS(Bonds!R[1]C[9]& "" ISIN"",""ISSUE_UNDERWRITER"",""Headers"",""Y"")"
lRow = Cells(Rows.Count, 1).End(xlUp).Select
Selection.Offset(1, 0).Select
ActiveCell.FormulaR1C1 = _
"=BDS(Bonds!R[-53]C10& "" ISIN"",""ISSUE_UNDERWRITER"",""Headers"",""Y"")"
Range("A57").Select
End Sub
Image of Table, Im trying to iterate through the ISIN Column. It is column "J"
Although selection and .select are used by the macro recorder, they cause big problems when developing code. It's worth your time to learn how to replace them with range objects. So, while I'm not directly answering your question, I'm trying to give you the tools to do so.
I've shown an example below to illustrate (although I do not work with the BDS() function so I'm undoubtedly getting the details wrong). The main point is that if you learn to move around using the range object you'll be much better off.
Sub formula()
Dim r As Range, sh As Worksheet, bondR As Range, bondSh as Worksheet
set sh = ActiveSheet
set r = sh.range("A1")
Set bondSh = Worksheets("Bonds")
Set bondR = bondSh.Range("J1")
For i = 1 To 10
r.formula = "=BDS(bondR.offset(i,0) & "" ISIN"",""ISSUE_UNDERWRITER"",""Headers"",""Y"")"
Set r = r.Offset(i, 0)
Next i
End Sub
Here I'm defining one range object, r, to track the location on the active sheet, and another, bondR, for the location on the "Bonds" sheet. Once the initial locations of these ranges are defined, you can manipulate them using the .offset(row,col) function as I've done with the simple for-loop, moving down 1 row (but 0 columns) in each loop.
Feel free to ask questions.