Transform Data by Groups in Excel - vba

I have some data in Excel grouped into blocks of data in a single column. Each block has a serial number identifying at the top, then a space, and then the associated data. The format is similar to the following:
20204
0.009447773
0.008672609
0.04769611
0.041069839
-0.035158783
-0.06094639
0.059548043
0.029640036
8081
0.003970223
-0.024156246
0.063038863
0.005341972
0.124618374
0.005495709
0.098642513
0.005636186
0.063350961
0.128130779
106663
0.115009077
0.049194194
-0.057100467
0.037476741
0.063087314
0.072773643
0.003909923
0.000448073
0.008006874
0.008718021
0.009883258
-0.022708477
0.028655466
The blocks occur at regular intervals and are all the same length (spaced by 2007 cells). So the first serial number appears at D19, the next at D2026, then D4033, etc. This extends for several thousand rows.
I want to multiply each of the blocks of numbers by a different amount based on their serial number. So I'd want to multiply everything in the first block by 0.5, everything in the second by 2, everything in the third by 1.2, etc, according to a table such as:
Serial Scalar
20204 0.5
8081 2
106663 1.2
What would be the most efficient way to do this, either by VBA or in a formula in an adjacent cell?

you could expolit Areas property of Range object
Option Explicit
Sub main2()
Dim iArea As Long
Dim serialToScalarTable As Range, cell As Range
Dim multiplier As Double
Set serialToScalarTable = Range("A2:B4") '<--| table where you have serial/scalar correspondence
With Range("D19", Cells(Rows.count, 4).End(xlUp)).SpecialCells(xlCellTypeConstants, xlNumbers)
For iArea = 2 To .Areas.count Step 2
multiplier = Application.VLookup(.Areas(iArea - 1).Value, serialToScalarTable, 2, False)
For Each cell In .Areas(iArea)
cell.Value = cell.Value * multiplier
Next
Next
End With
End Sub

Related

VBA: Max of 2 columns gets sent to third column

I need to get the maximum value from two columns and send it to a third column. The columns are all uniform in size but sometimes the sizes will be different, however they will all start in the same cell. For example:
5 8 -
6 2 -
6 5 -
The column with the dashes would need to find the maximum between the other two, and the finished project would look like
5 8 8
6 2 6
6 5 6
I tried recording a macro but it used ActiveCell which isn't good. I want The two columns to be, say, anything starting from C10 that has a value, and everything starting in D10 that has a value, and the max values sent to E10.
Here's what I recorded, trying to just fill the destination cell with the formula:
ActiveCell.FormulaR1C1 = "=MAX(RC[-2],RC[-1])"
Selection.AutoFill Destination:=Range("E10:E300"), Type:=xlFillDefault
Here is a good alternative of your code, just make sure to declare the first column that you compare with. The example works with the first 10 cells of Column A:
Option Explicit
Public Sub SelectMax()
Dim rngRange As Range
Dim rngCell As Range
Set rngRange = Range("A1:A10")
For Each rngCell In rngRange
'Without visible formula in Excel:
rngCell.Offset(0, 2) = WorksheetFunction.Max(rngCell, rngCell.Offset(0, 1))
'With visible formula in Excel
rngCell.Offset(0, 2).FormulaR1C1 = "=MAX(RC[-2],RC[-1])"
Next rngCell
End Sub
The example makes a visible formula in Excel. If you want to ignore the formula, remove the line after the second comment and the formula will not appear.

Trying to create a macro to perform 100 iterations and paste resulting values (2 adjacent row cells) to a 2 x 100 array

I have a worksheet that uses randomly generated numbers in calculations to produce results in two adjacent cells (let's say A1 and A2). I am trying to perform 100 iterations where I'm simply "Calculating Formulas" on the worksheet and then trying to store the results of each iteration next to A1 and A2 (so iteration 1 would be in B1 and B2 and iteration 100 would be in CW1 and CW2). Thanks in advance for your help. Using Excel 2010 if that matters.
Dim Iteration As Integer, i As Integer
Dim val As Variant
Iteration = 100
For i = 1 To Iteration
Calculate
Range("A1:A2").Select
Selection.Copy
Range("B" & Rows.Count).End(x1Up).Offset(0, 1).PasteSpecial
Paste:=xlPasteValues
Next i
End Sub
I think your major problem was with the location you were selecting for the destination address - you were finding the last unused cell in column B, then shifting over one column (i.e. to column C) and pasting the first set of results. Then you were using that same location for the second set of results, etc.
Sub Test()
Dim Iteration As Integer, i As Integer
Dim val As Variant
Iteration = 100
'Use a "With" block so that it can be easily changed in the future
'to refer to a specific sheet if needed
With ActiveSheet
For i = 1 To Iteration
Calculate
'Determine the last used column on row 1,
' offset 1 column to the right,
' resize to refer to 2 rows,
' set values to the values in A1:A2
.Cells(1, .Columns.Count).End(xlToLeft).Offset(0, 1).Resize(2, 1).Value = .Range("A1:A2").Value
Next i
End With
End Sub
As pointed out by Steve Lovell, you also had a typo in your original code. It is a good habit to include Option Explicit as the first line in every code module. That will force you to declare all the variables that you use, and the compiler would have highlighted x1Up and given a "Variable not defined" error.

How do I Count Cells in Columns of a non-rectangular range

So I have a range that will be selected by a user. It will be two columns, usually not next to each other. I need to count the number of cells in each column of the selected range to determine if the number of cells in each column is equal. (If it's not I will need to adjust the range.)
For example, a User may select B5:B10 and D6:D9. So I need the code to return 6 and 4 respectively.
I've tried:
Set rng = Selection
rng.Columns(1).Count
this returns 1, which obviously isn't the number I need.
Thanks in advance!
You can use the Areas method of the Range object to get the areas of the range. Areas are groups of contiguous ranges within a non-contiguous range.
Set rng = Selection
For i = 1 to rng.Areas.Count
debug.print rng.Areas(i).Cells.Count
Next
There is caveat here that you may need to test for, and that is if the user selects, for example, A1:B10, with one mouse drag. Since this is a contiguous range, it will only have one Area and you will not get two distinct numbers. If you need to test for this, you can do something like the below.
Set rng = Selection
'non-contiguous ranges will always return one column, if there are mutiple columns both cell counts are equal by default
If rng.Columns.Count = 1 Then
For i = 1 to rng.Areas.Count
debug.print rng.Areas(i).Cells.Count
Next
End If
Dammit #Scott - just beat me to it.
Although I'm using the Rows property - if the user selects A1:B19 it will return 19 in a single element of the array, if they select A1:A19 and then B1:B19 it will return 19 in two elements of the array.
Using Cells it will return 38 in a single element, or 19 in two elements.
Sub Test()
Dim rRange As Range
Dim lRows() As Long
Dim x As Long
Set rRange = Selection
With rRange
ReDim lRows(1 To .Areas.Count)
For x = 1 To .Areas.Count
lRows(x) = .Areas(x).Rows.Count
Next x
End With
End Sub

Updating external cell references across multiple worksheets (using vba macro)

I'm completely new to VBA and Excel macros in general so I'll try to explain my predicament as clearly as possible. Basically I've got two workbooks, the source workbook which contains a single worksheet with nearly thousands of rows and columns and another workbook with 90+ worksheets, each with two tables that references cells from the source workbook (the tables cover monthly data for the last four fiscal years).
I've shoe-stringed together an automation macro that mostly works, but my primary concern is that it could be done better, specifically I've got one section of code:
'October
cellVarO = ActiveSheet.Range("B8").Formula
cellVarO = Right(cellVarO, 5)
Range("B8").Select
ActiveCell.Formula = "=OFFSET('C:\external\[reference_sheet.xls]Mnthly Rdgs'!" & cellVarO & ",0," & fyNum * 12 & ")"
One thing to note is that this code repeats 24 times, one for each month, and another iteration to use MID so that I'm still selecting the right cell value from the active cell formula (after changing the original formula to include OFFSET). I find this bulky and unnecessary but it's the only way I can wrap my mind around the problem. Another issue, it considers that the cell reference will always be 5 characters long. There are instances where this is not the case.
But basically my months are laid out by column and my years are laid out by row, what I was aiming to do here was look in the cell formula for the cell reference, select the cell value, then use OFFSET to shift the value 12 columns to the most recent one, and print the new value to the most recent year. Suppose if I have the cell formula:
='C:\external\[reference_sheet.xls]Mnthly Rdgs'!QR938
My goal is to take the cell value here (QR938) and shift it right 12 columns. Is there any way to pick out the cell value (other than using MID/RIGHT) and assign it to a variable to offset? Is there a better way to shift the cell value 12 columns other than using OFFSET? Finally, is there any way to perform that same operation across multiple similarly formatted worksheets?
See if this helps
For testing the main code:
Sub Tester()
'offset 12 cols to right
OffsetFormulaReference ActiveSheet.Range("B8"), 0, 12
'offset 12 cols to left
OffsetFormulaReference ActiveSheet.Range("B9"), 0, -12
'offset 12 rows down
OffsetFormulaReference ActiveSheet.Range("B10"), 12, 0
'offset 12 rows up
OffsetFormulaReference ActiveSheet.Range("B11"), -12, 0
'EDIT: loop over sheets and edit a specific range
Dim c As Range, sht as WorkSheet
For Each sht in ThisWorkbook.Sheets
For each c in sht.Range("B8:B20").Cells
OffsetFormulaReference c, 12, 0
Next c
Next sht
End Sub
Utility method for taking the formula from a cell with an external reference and moving it over by the specified number of rows/columns:
Sub OffsetFormulaReference(c As Range, offsetRows, offsetCols)
Dim origForm As String, origAddr As String
Dim arr, rng As Range, newAddr As String
If c.HasFormula Then
origForm = c.Formula
'(e.g.) ='C:\external\[reference_sheet.xls]Mnthly Rdgs'!QR938
If InStr(origForm, "!") > 0 Then
arr = Split(origForm, "!") 'arr(1) = "QR938"
Set rng = ActiveSheet.Range(arr(1)) 'get a range reference
Set rng = rng.Offset(offsetRows, offsetCols) 'move the reference
newAddr = rng.Address(False, False) 'get the offset address
'replace old formula with new offset reference
c.Formula = arr(0) & "!" & newAddr
End If
End If
End Sub
Note: you'll get an error if you try to use Offset() to move the rng reference beyond the limits of the sheet (eg. row or column < 1). You can add logic to handle that if it might be an issue.

VBA - Copy Formatting of Range to Array

I'm creating a report with multiple columns. What I need is that the columns that show only whole numbers, no decimals, should be rounded to the whole number (so that it not only shows a rounded number, it actually equals a round number). The columns that show the numbers with two numbers after the decimal should not be rounded.
What I can do is:
If c.NumberFormat = "#,##0_);(#,##0)" Then
c.Formula = "=round(" & Right(strFormula, Len(strFormula) - 1) & ",0)"
End If
However, I have the entire report in an array, and I would like to just paste the whole array into the sheet rather than pasting one cell at a time. I would also rather not process and round each cell based on the cell formatting, rather I would like to copy the formatting of the range where the report will go into an array, and work from the array. I believe this will cut a few seconds off the process.
Is there a way to copy the formatting of a range into an array?
Is there a way to copy the formatting of a range into an array?
Focusing on the question as posed, yes, that can be done. Unfortunately, it can't done in a one-liner, in the way that a range's values can be assigned to an array with myArray = Range("A2:F25"), for example. (See, also, brettdj's comment.) Instead, you'd need something like:
Dim rng as Range
Dim formatArray() As String
Dim i as Long, j as Long
Set rng = Range(A2:F20) 'or whatever the range is
Redim formatArray(1 to rng.Rows.Count, 1 to rng.Columns.Count)
For i = 1 to rng.Rows.Count
For j = 1 to rng.Columns.Count
formatArray(i, j) = rng.Cells(i, j).NumberFormat
Next
Next
...
A couple of observations, though:
You actually only need to know the formatting for a single row of the range since, presumably, the number formatting in a column will not change mid-column.
That would simplify the code to:
...
Redim formatArray(1 to rng.Columns.Count)
For i = 1 to rng.Columns.Count
formatArray(i) = rng.Cells(1, i).NumberFormat
Next
...
assuming, for sake of example, that row 1 of the range has the necessary number formats.
I am curious why you would want to modify a formula on the worksheet so that it will round, since you could presumably do the calculation in your code, with rounding, and then write the resulting value back to the sheet for your report.
If you need to apply number formats to the values you are writing to the worksheet (not just modify formulas so that they will produce whole numbers), that can be done to whole columns within the range at once, i.e.,
...
For i = 1 to rng.Columns.Count
rng.Columns(i).NumberFormat = formatArray(i)
Next
...
If you need to convert the results of a cell's formula to a rounded value, you can do that with something like
rng.Cells(2, 5).Value = WorksheetFunction.Round(rng.Cells(2, 5).Value, 0)
to give an example for a single cell. This assumes, of course, that the data feeding into the formula are already in the sheet and that the formula has been recalculated.