I have a worksheet that I use to place raw data in order to validate data on individual files I cut up from that raw data. Thing is, the script tends to truncate the data it validates and disregards blank rows at the tail end of the rows I bring over.
How can I change this:
Private Const numCols As Long = 76
to do a columns.count?
for example: if there are 76 columns, but row two has data until 50 columns, it will take that row and transpose it without those tailing blank cells from columns 51-76, offsetting my data and returning FALSE values when I validate. Now I have to update that long variable to make sure it doesn't cut off trailing blanks when my dataset changes.
What can I do to make it more dynamic, but also make sure the script doesn't ignore blanks when I need them included as well?
If your header determines how many columns, then you could count that row to get the number. Assume your header is in row 1.
Dim numCols as Long
numCols = Application.WorksheetFunction.Counta(sheet1.rows(1))
You could also get it from the user
numCols = Application.InputBox("Enter # of columns")
If you're using the number of columns in a few procedures, you can either pass it as an argument or use a module-level variable. Declare a module level variable at the top of the module (in the declarations section, before any Sub or Function statements) like
Private numCols As Long
You'll still need to set it in the first procedure you run. It can't be a constant because it changes depending on the width of the first row.
You can do the same with a reference to the worksheet. I used the sheet's codename (the name that the user can't see and can't change), but you could create a variable to refer to the sheet
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("MySheet")
I generally use a codename rather than a variable, but it's a personal preference.
Related
I am working on an excel workbook where the user imports text files into a "Data Importation Sheet", the amount of text files imported is dynamic. See image.
So here is what I need to happen
1) Need to find the most up-to-date Reading Date (in this example 2016)
2) Need to copy and paste the range of Depth values of the most up-to-date Reading Date to a separate sheet (in this example I would want to copy and paste values 1-17.5.
3) Need to check if all other data sets contain this same range of Depth values. For the year 2014 you can see its depth goes from 0.5-17.5. I want to be able to just copy the data at the range of the most up-to-date Reading Date so the range of 1-17.5.
Here is my code to find the most up-to-date Reading date and to copy those depths to the other sheets.
Sub Copy_Depth()
Dim dataws As Worksheet, hiddenws As Worksheet
Dim tempDate As String, mostRecentDate As String
Dim datesRng As Range, recentCol As Range, headerRng As Range, dateRow As Range, cel As Range
Dim lRow As Long
Dim x As Double
Set dataws = Worksheets("Data Importation Sheet")
Set hiddenws = Worksheets("Hidden2")
Set calcws = Worksheets("Incre_Calc_A")
Set headerRng = dataws.Range(dataws.Cells(1, 1), dataws.Cells(1, dataws.Cells(1, Columns.Count).End(xlToLeft).Column))
'headerRng.Select
For Each cel In headerRng
If cel.Value = "Depth" Then
Set dateRow = cel.EntireColumn.Find(What:="Reading Date:", LookIn:=xlValues, lookat:=xlPart)
Set datesRng = dataws.Cells(dateRow.Row + 1, dateRow.Column)
'datesRng.Select
' Find the most recent date
tempDate = Left(datesRng, 10)
If tempDate > mostRecentDate Then
mostRecentDate = tempDate
Set recentCol = datesRng
End If
End If
Next cel
Dim copyRng As Range
With dataws
Set copyRng = .Range(.Cells(2, recentCol.Column), .Cells(.Cells(2, recentCol.Column).End(xlDown).Row, recentCol.Column))
End With
hiddenws.Range(hiddenws.Cells(2, 1), hiddenws.Cells(copyRng.Rows(copyRng.Rows.Count).Row, 1)).Value = copyRng.Value
calcws.Range(calcws.Cells(2, 1), calcws.Cells(copyRng.Rows(copyRng.Rows.Count).Row, 1)).Value = copyRng.Value
Worksheets("Incre_Calc_A").Activate
lRow = Cells(Rows.Count, 1).End(xlUp).Row
x = Cells(lRow, 1).Value
Cells(lRow + 1, 1) = x + 0.5
End Sub
Any tips/help would be greatly appreciated. I am fairly new to VBA and don't know how to go about comparing the depth ranges! Thanks in advance!
Assuming that your datasets are as regularly organised as your screenshot suggests then quite a lot of processing can be done in Excel.
The image below shows a possible approach based on the data shown in your example.
The approach exploits the fact that each data set occupies 7 columns of the importation worksheet. The =ADDRESS() function is used to build text strings which look like cell addresses and these are further manipulated to create text strings which look like range addresses. The approach also assumes that the reading date is always located in the third row following the final row of depth data.
The solution is slightly different to your problem, in that it identifies the common range of depth values across all datasets. For the example in the question this amounts to the same thing as identifying the depth values associated with the latest reading date.
This approach was taken as it is not clear from the question what would happen if, say, a dataset had depth values starting at say 1.5 (so greater than the first value for the latest reading date) or ending at say 17 (so less than the the last value for the latest reading date). The approach can obviously be adapted if these possibilities will never occur.
The table shown in the image above has in its final column, a text representation of the ranges to be copied from the Data Importation Sheet. A simple bit of VBA can read this column, a cell at a time and use the text to assign an appropriate range object to which copy and paste methods can then be applied.
Additional bit of answer
The image above could be set-up as a "helper" worksheet. If there is always the same number of datasets on the Data Importation Worksheet then set up this helper sheet so that the number of rows in Table 2 is equal to this number of datasets. If the number of datasets is variable, then set up the helper sheet so that the number of rows in Table 2 is equal to the maximum number of datasets that is ever likely to be encountered. In this situation, when the number of datasets imported is fewer than this maximum, some rows of Table 2 will be unused and these unused rows will contain meaningless values in some columns.
Your VBA program should be organised to read the value in the value in cell D2 of the helper sheet and then use this to determine how many rows of Table 2 to examine with the rest of your VBA code. This will unused rows (if any) to be ignored.
If your VBA code identifies a value of, say 10, in cell D2 of the helper sheet then you will want your code to read one a time the 10 values in the range Q12:Q21 (so in a loop). Each of these cells holds, as a string, the range containing a single dataset's values and so can be assigned to a Range object using code such as
Set datasetRng = Range(datasetStr)
where datasetStr is the text string read from a cell in Q12:Q21.
Still within the loop, datasetRng can then be copied and pasted to your output worksheet.
Because the same helper worksheet can be re-used for each data importation, you should be able to incorporate it into your automation scheme. No need for copying and pasting formula down rows to create a different helper for each importation, just apply the same helper template to each data importation.
The approach adopted makes as much use of Excel as possible to determine relevant information about the imported data sets and summarises this information within the helper worksheet. This means VBA can be limited to automation of the copy/paste operations on the datasets and its reads information from the helper sheet in determining what to copy for each dataset.
It is of course possible to do everything in VBA but as you indicated you were fairly new to VBA it seemed sensible to tip the balance towards using less VBA and more Excel.
Incidentally, the problem of comparing the depth ranges is not really one of Excel or programming, it is one of analysis - ie looking at a range of cases, figuring out what needs to happen for each case, and distilling this into a set of processing rules (what some would call an algorithm). Only then should attempts be made to implement these processing rules (either via Excel formula or VBA code). I have hinted at my analysis of the problem (finding the common range of depth values across all datasets)and you should be able to track through how I have implemented this in Excel to cater for cases where some datasets might contain Depth values which are less than the minimum of the common range or which are greater than its maximum (or possibly both).
End of additional bit
The formula used are shown in the table below.
i'm quite new to Excel VBA and formulas but i need to have a cell with random values on the form that i had created to create like a unique id (I know that there might be chances of getting duplicate numbers but it is slim) to send it to people to fill up a form but the thing is that if im using the RandBetween(00000000,99999999), hiding and unhiding just keep activating the formula which is hard for me to keep track if i were to send this template for others to fill up. So is there any way where when i have the formula and when the user opens the Excel form, it will randomize a 8 digit number and copy and special pasting the value overwriting it to freeze the value.
(Other ways are also gladly welcomed, thanks in advance guys)
I'm not sure what you're actually trying to do here, but if you wanted to generate a random number and store its value in a cell, you can use this code in 'ThisWorkbook' in VBA:
Option Explicit
Const MySheetName As String = "Sheet1" ' Change this to your sheet name
Private Sub Workbook_Open()
' Called every time you open the excel document
Dim myRandNumber As Long
Randomize
myRandNumber = CLng(Rnd() * 999999999) ' CLng converts the floating point number to a long integer
Dim rowNum As Integer
Dim colNum As Integer
rowNum = 3
colNum = 5
ThisWorkbook.Sheets(MySheetName).Cells(rowNum, colNum).Value = myRandNumber
End Sub
If you wanted to make sure this number was unique, which is probably what you really want to do, you can store these values in a separate hidden sheet and check these values each time you want to generate a new number.
You could also just timestamp the number using this line of code instead of the random number:
ThisWorkbook.Sheets(MySheetName).Cells(rowNum, colNum).Value = CDbl(Now() * 100000)
This will create a new unique number every 1 second, based on the system time.
If I understand correctly, you want to generate this number once. There are a couple ways I can think of to do this:
Generate the number only if the cell is blank
Set a flag in a hidden sheet
In both cases, if you wanted to generate the number again, you just need to clear the cell containing the number (option 1) or clear the flag in the sheet (option 2)
Option 1 can be done by replacing this part of the above code:
ThisWorkbook.Sheets(MySheetName).Cells(rowNum, colNum).Value = myRandNumber
with this:
With ThisWorkbook.Sheets(MySheetName).Cells(rowNum, colNum)
If (.Value = "") Then
.Value = myRandNumber
End If
End With
Hope this helps
I've got a large Excel spreadsheet. It includes many data tables that various lookup functions are run on. To make version control easier, I'm currently in the process of pulling these data tables out into separate .csv files, so they can be diffed properly. Unfortunately, they contain a few formulae, which obviously won't work properly when I convert the file to a static .csv.
My current solution is to, wherever a calculation is unavoidable, move the calculation to its own cell in the main workbook and name the cell. Let's call it ExampleCalc. Then, in the cell on the data table where the calculation was, I instead enter ref:ExampleCalc. Then to do the lookup, I wrote the following UDF:
Function RaceLookup(lookupString As Variant, lookupTable As Range, raceID As Range, cleanIt As Boolean) As Variant
' Helper function to make the interface formulae neater
Dim temp As Variant
Dim temp2 As String
Dim inpString As Variant
Dim id As Double
id = raceID.Value
If TypeOf lookupString Is Range Then
inpString = lookupString.Value
Else
inpString = lookupString
End If
With Application.WorksheetFunction
temp = .Index(lookupTable, id, .Match(inpString, .Index(lookupTable, 1, 0), 0))
If Left(temp, 4) = "ref:" Then
temp2 = Right(temp, Len(temp) - 4)
temp = Range(temp2).Value
End If
If cleanIt Then
temp = .clean(temp)
End If
End With
RaceLookup = temp
End Function
This does a standard INDEX lookup on the data sheet. If the entry it finds doesn't start with ref:, it just returns it. If it does start with ref:, it strips the ref: and treats whatever's left as a cell name reference. So if the value the INDEX returns is ref:ExampleCalc, then it will return the contents of the ExampleCalc cell.
The problem is that ExampleCalc doesn't get added to the dependency tree. That means that if the value of ExampleCalc changes, the retrieved value doesn't update. Is there any way to make the function add ExampleCalc to the cell's dependency tree? Alternatively, is there a more sensible way to do this?
The solution i have found is to add the line
Application.Volatile True ' this causes excel to know that if the function changes, it should re calculate. You will still need to click "Calculate Now"
into the function. However, as my code comment indicates, if the only change is in the functional output, you'll have to manually trigger a recalculation.
See https://msdn.microsoft.com/en-us/library/office/ff195441.aspx for more information.
I have two sets of data stored in two different sheets. I need to run an analysis which prints out the non-duplicate rows (i.e. row is present in one and not the other) found in the sheets and print them in a new sheet.
I can do the comparison fine - it is relatively simple with ranges and the For Next method. I currently store the non-duplicates in two different collections, each representing the non-duplicates in each sheet. However I am having trouble deciding how to proceed with pasting the duplicate rows on the new sheet.
I thought about storing the entire row into a collection but printing the row out of the collection in the new sheet seems non-trivial: I would have to determine the size of the collection, set the appropriate range and then iterate through the collection and print them out. I would also like to truncate this data which would add another layer of complexity.
The other method I thought was simply storing the row number and using Range.Select.Copy and PasteSpecial. The advantage of this is that I can truncate however much I wish, however this seems incredibly hacky to me (essentially using VBA to simulate user input) and I am not sure on performance hits.
What are the relative merits or is there a better way?
I have been tackling a similar problem at work this week. I have come up with two methods:
First you could simply iterate through each collection one row at a time, and copy the values to the new sheet:
Function PasteRows1(ByRef srcRows As Collection, ByRef dst As Worksheet)
Dim row As Range
Dim curRow As Integer
curRow = 1
For Each row In srcRows
dst.rows(curRow).Value = row.Value
curRow = curRow + 1
Next
End Function
This has the benefit of not using the Range.Copy method and so the user's clipboard is preserved. If you are not copying an entire row then you will have to create a range that starts at the first cell of the row and then resize it using Range.Resize. So the code inside the for loop would roughly be:
Dim firstCellInRow as Range
Set firstCellInRow = dst.Cells(curRow,1)
firstCellInRow.Resize(1,Row.columns.Count).Value = row.Value
curRow = curRow + 1
The second method I thought of uses the Range.Copy. Like so:
Function PasteRows2(ByRef srcRows As Collection, ByRef dst As Worksheet)
Dim row As Range
Dim disjointRange As Range
For Each row In srcRows
If disjointRange is Nothing Then
Set disjointRange = row
Else
Set disjointRange = Union(disjointRange, row)
End If
Next
disjointRange.Copy
dst.Paste
End Function
While this does use the .Copy method it also will allow you to copy all of the rows in one shot which is nice because you will avoid partial copies if excel ever crashes in the middle of your macro.
Let me know if either of these methods satisfy your needs :)
I have a table of data with the top row being filters, I have a loop that changes which filter needs to be used inside the loop is the variable filterColumn that is being assigned a new value every time the loop runs through.
when i try to use filterColumn to determine which filter will be 'switched on' i get an error
Autofilter method of Range Class Failed
ActiveSheet.Range("$U$83:$CV$1217").AutoFilter Field:=filterColumn, Criteria1:="<>"
What is the correct syntax in order to use a variable to determine which field the filter is in?
Problem Solved I found the solution. I was referencing the filters columns position in terms of the whole worksheet when in fact I should have been referencing what number it was in the group of filters. For example the filter I wanted to change was in 'CF' which is the 84th column but my the filter I wanted to change is the 64th in the group.
Dim filterColumn As Integer
filterColumn = 2
ActiveSheet.Range("$U$83:$CV$1217").AutoFilter Field:=filterColumn, _
Criteria1:="<>"
EDIT: I tried #HeadofCatering's solution and initially it failed. However I filled in values in the referenced columns and it worked (my solution also failed under reverse conditions - make the column headers blank and it fails).
However this doesn't quite mesh with what I've (and probably you've) seen - you can definitely add filters to columns with blank headers. However one thing was consistent in the failures I saw - the filterColumn referenced a column that was outside of Application.UsedRange. You may want to try verifying that the column you are referencing is actually within Application.UsedRange (easy way: run Application.UsedRange.Select in the Immediate Window and see if your column is selected). Since you are referencing a decent amount of columns, it is possible that there are no values past a certain point (including column headers), and when you specify the column to filter, you are actually specifying something outside of your UsedRange.
An interesting (this is new to me as well) thing to test is taking a blank sheet, filling in values in cells A1 and B1, selecting columns A:G and manually adding AutoFilters - this will only add filters to columns A and B (a related situation can be found if you try to add filters to a completely blank sheet).
Sorry for the babble - chances are this isn't even your problem :)
Old solution (doesn't work when conditions described above are used)
I may be overkilling it, but try setting the sheet values as well (note I used a sample range here):
Sub SOTest()
Dim ws As Worksheet
Dim filterColumn As Integer
' Set the sheet object and declare your variable
Set ws = ActiveSheet
filterColumn = 2
' Now try the filter
ws.Range("$A$1:$E$10").AutoFilter Field:=filterColumn, Criteria1:="<>"
End Sub