This is a very basic question and I'm sure it has been answered but I can't seem to find it elsewhere. I have a portion of vba code that works fine for a single cell. However I want to expand it to work for a range of cells (all within the same column).
Basically its a goalseeking loops that changes a value in the "b" column until the value in the "w" column matches (comes within 99%) the value in the "x" column.
What works:
Sub Goalseeker()
Do Until Range("w32").Value / Range("x32").Value > 0.99
Range("b32").Value = Range("b32").Value - 1
Loop
End Sub
I want to extend this to work for rows 32 to 107.
What I've tried:
Edit: I've made adjustment based on the comments received and tweaked and few things before it worked. If anyone is interested in the process:
Option Explicit
Sub Goalseeker()
Dim i As Integer
Dim targetcell As Double
Dim outputcell As Double
Dim variablecell As Range
For i = 32 To 107
targetcell = Cells(i, "x")
outputcell = Cells(i, "w")
Set variablecell = Range("B" & i)
Do Until outputcell / targetcell > 0.99
variablecell = variablecell - 1
targetcell = Cells(i, "x")
outputcell = Cells(i, "w")
Loop
Next
End Sub
The bit I had to tweak was
Do Until outputcell / targetcell > 0.99
variablecell = variablecell - 1
targetcell = Cells(i, "x")
outputcell = Cells(i, "w")
Loop
Redefining (i apologize if that's the wrong term) targetcell and outputcell was necessary to prevent an infinite loop.
Thanks all. I will work on making this function for relative references instead of absolute.
Few problems here. Change your For i loop to the format For i = x to y not For i = x to i = y
You can refer to targetcell and outputcell as double but the variablecell needs to be a range. And if it's a range it needs Set
You should declare ALL your variables, as below.
And finally, you might want to put in a catch to get out of infinite looping (in case the target never reaches above 0.99 ?)
Sub Goalseeker()
Dim i As Integer
Dim targetcell As Double
Dim outputcell As Double
Dim variablecell As Range
For i = 32 To 107
targetcell = Cells(i, "x")
outputcell = Cells(i, "w")
Set variablecell = Range("B" & i)
Do Until outputcell / targetcell > 0.99
variablecell = variablecell - 1
Loop
Next
End Sub
Consider the following example table:
Use the code below to find the correct values in the "B" column (as shown) in order to minimize the error between the result (next column) and the goal (two columns over).
Option Explicit
Public Sub GoalSeekMyValues()
' Call GoalSeek with inputvalues "B2:B16", having the result
' at column offset 1, and the goal in column offset 2.
' Note that Range("B2").Resize(15, 1) = Range("B2:B16"),
' But I prefer the top cell and row count of this syntax.
GoalSeek Range("B2").Resize(15, 1), 1, 2
End Sub
Public Sub GoalSeek(ByVal variables As Range, ByVal result_offset As Long, ByVal goal_offset As Long)
Dim n As Long, i As Long, pct_error As Double, last_error As Double
'x is the input value (variable)
'y is the result
'g is the goal for y
Dim x As Double, y As Double, g As Double
' Read the number of rows in the input values
n = variables.Rows.Count
Dim r As Range
' Loop through the rows
For i = 1 To n
'Set a range reference at the i-th input cell
Set r = variables.Cells(i, 1)
' Read the value, the result and the goal
x = r.Value
y = r.Offset(0, result_offset).Value
g = r.Offset(0, goal_offset).Value
pct_error = Abs(y / g - 1)
Do
'Set the next value
r.Value = x - 1
' Read the result (assume goal doesn't change)
y = r.Offset(0, result_offset).Value
' Keep last error, and calculate new one
last_error = pct_error
pct_error = Abs(y / g - 1)
' If new error is more than last then exit the loop
' and keep the previous value (with less error).
If pct_error > last_error Then
' Keep last value
r.Value = x
Exit Do
End If
' read the input value
x = r.Value
' Assume inputs must be positive so end the loop
' on zero on negative numbers
Loop Until x <= 0
Next i
End Sub
Your code has lots of points of failure.
Your code might not ever reach a solution and excel will hang (until Ctrl-Break is pressed). I have a hard break when the inputs become zero or negative. Other problems require other ways to tell that there isn't a solution.
The first time the result comes within 1% of the solution might not produce the least error. I solve this by tracking the absolute value of the relative error. Only when the error starts increasing I terminate the loop. This assumes that decreasing the input by one unit will improve the solution (at least initially). If this is not the case the code will fail.
You use absolute referencing (like reading the 12th cell down and 4th across) and that is not very reusable style of programming. I always try to use relative referencing. I start from the top left referenced cell (in this case B2) and move down and right from there using the following methods:
Range("B2").Cells(5,1) - Reference 5th row and 1st column from B2.
Range("B2").Resize(15, 1) - Expand the range to include 15 rows and one column.
Range("B2).Cells(i,1).Offset(0, 3) - From the i-th row use column offset of 3 (meaning the 4th column in the table).
I suggest to use one of the common goal seeking methods (like bisection), or better yet, use the built-in goal seek function
example:
Range("B2").Cells(i,2).GoalSeek Goal:=Range("B2").Cells(i,3).Value, ChangingCell:=Range("B2").Cells(i,1)
Related
Im trying to write a code which determines whether certain cells are empty or not and then returns a set string.
To go in detail; I was hoping for the code to look into cell B2, determine if it is empty, then go to C2 and determine if it is non-empty. If both were correct in cell B2 it would then input "Correct" and move on in the range. However, my code doesnt seem to work because it just inputs "Correct" in every cell in the loop range.
I have posted my code below; any help would be much appreciated.
Sub Fill_Rows()
Dim X As Range
Let Y = Range("C2")
For Each X In Range("B2:B5000")
If X = "" And Y <> "" Then
X = "Correct"
End If
Y = Y + 1
Next X
End Sub
If you meant to check by each row like (B2 and C2) then (B3 and C3), then you could do it like this.
Sub Fill_Rows()
Dim iRow As Long
For iRow = 2 To 5000
If Cells(iRow, "B").Value = vbNullString And Cells(iRow, "C").Value <> vbNullString Then
Cells(iRow, "B").Value = "Correct"
End If
Next iRow
End Sub
Alternative
Added two solutions:
[1] an example code as close as possible to yours and
[2] an alternative using a datafield array to demonstrate a faster way for bigger data sets.
[1] Example Code close to yours
There is no need to use a second variable Y, all the more as apparently you left it undeclared, which always can cause issues (type mismatches, no range object etc.).
So always use Option Explicit in the declaration head of your code module to force yourself to declare all variable types you are using.
Now you can simply use an offset of 1 column to the existing cell to check the neighbouring cell, too.
Option Explicit ' declaration head of your code module (obliges to declare variables)
Sub Fill_RowsViaRangeLoop()
Dim X As Range, ws As Worksheet
Set ws = ThisWorkbook.Worksheets("MySheet") ' << replace with your sheet name
For Each X In ws.Range("B2:B5000")
If X = "" And X.Offset(0, 1) <> "" Then ' column offset 1 checks next cell in C
X = "Correct"
End If
Next X
End Sub
[2] Example Code using a datafield array
Looping through a bigger range isn't very fast, you can speed up your procedure by
assigning your range values to a variant datafield array v, loop through the received array items correcting found items in column 1 and write it back to sheet.
Option Explicit ' declaration head of your code module (obliges to declare variables)
Sub Fill_RowsViaArray()
Dim v As Variant, i As Long, ws As Worksheet
Set ws = ThisWorkbook.Worksheets("MySheet") ' << replace with your sheet name
' Assign values to a 2-dim array
v = ws.Range("B2:C5000") ' or better: v = ws.Range("B2:C5000").Value2
' Check criteria looping over all rows (=first array dimension)
For i = 1 To UBound(v) ' data field arrays are one-based, i.e. they start with 1
If v(i, 1) = vbNullString And v(i, 2) <> vbNullString Then v(i, 1) = "Correct"
Next i
' Write edited array back to original range (adapt the range size to the array boundaries in both dimensions)
ws.Range("B2").Resize(UBound(v, 1), UBound(v, 2)) = v
End Sub
Further Notes
It's good use to fully qualify your sheet or range references (see e.g. object variable ws)
Each array item is identified by a row and a column index.
As such a datafield array is one based (start indices are 1), the first item in row 1 and column 1 will be referred by v(1,1), in col 2 by v(1,2).
In order to count the number of row items you check the upper boundary of its first dimension) via UBound(v,1) or even shorter via Ubound(v)
In order to count the number of columns you check the upper boundary of its second dimension) via UBound(v,2) (here the argument 2 is necessary!)
A comparation using vbNullString can be preferred in coding to "" as it takes less memory (c.f. #PEH 's answer) .
I have not used VBA for sometime so am very rusty... What I have is a number of records stored vertically (in a single column) and I want to use VBA to stack them side by side (into a table).
My general thoughts about how this would flow:
Start at first range
Copy data
Paste data in cell B3 of output page (just named Sheet2)
Loop back to previous range and offset by 51 rows
Copy data
Paste data in cell C3 of output page (offset by 1 column each time)
My attempt so far:
Sub Macro1()
FiftyOne = 51 ' Offset by 51 rows for every chunk
StartRange = "L262:L303" ' Start at this range of data to copy, each chunk is identical in size
OutputRange = B3 ' Paste in output at B3, but need to offset by one column each time
Range(StartRange).Offset(FiftyOne, 0).Select
Selection.Copy
Sheets("Sheet2").Select
Range("B3").Offset(0, 1).Select
ActiveSheet.Paste
End Sub
I know this is a rather lame attempt to tackle this flow, but I am really struggling with how to loop through this. I would appreciate some advice on how to do this, or a better approach to the general flow.
Edit after accepting Wolfie's answer:
I want to assign column headings, by getting the values from C258 and looping down (in a similar way to before) 51 rows at a time, to paste into row 2 of sheet2 (B2, C2, ...).
Here is my current attempt:
Sub NameToTable()
' Assign first block to range, using easily changable parameters
' Remember to "Dim" all of your variables, using colon for line continuation
Dim blocksize As Long: blocksize = 51
Dim firstrow As Long: firstrow = 258
Dim rng As Range
Set rng = ThisWorkbook.Sheets("Sheet1").Range("C" & firstrow & blocksize - 1)
' tablestart is the upper left corner of the "pasted" table
Dim tablestart As Range: Set tablestart = ThisWorkbook.Sheets("Sheet2").Range("B2")
Dim i As Long ' Looping variable i
Dim nblocks As Long: nblocks = 10 ' We're going to loop nblocks number of times
For i = 0 To nblocks - 1
' Do the actual value copying, using Resize to set the number of rows
' and using Offset to move down the original values and along the "pasted" columns
tablestart.Offset(0, i).Resize(blocksize, 1).Value = _
rng.Offset(blocksize * i, 0).Value
Next i
End Sub
Your logic seems alright, this code will create a 51 x n table, lining up each vertical block of 51 cells in its own column.
Note, it's much quicker to assign the .Value than copying and pasting, if you need formats too then you could copy/paste or similarly set format properties equal.
Sub ColumnToTable()
' Assign first block to range, using easily changable parameters
' Remember to "Dim" all of your variables, using colon for line continuation
Dim blocksize As Long: blocksize = 51
Dim firstrow As Long: firstrow = 262
Dim rng As Range
Set rng = ThisWorkbook.Sheets("Sheet1").Range("L" & firstrow & ":L" & firstrow + blocksize - 1)
' tablestart is the upper left corner of the "pasted" table
Dim tablestart As Range: Set tablestart = ThisWorkbook.Sheets("Sheet2").Range("B3")
Dim i As Long ' Looping variable i
Dim nblocks As Long: nblocks = 10 ' We're going to loop nblocks number of times
For i = 0 To nblocks - 1
' Do the actual value copying, using Resize to set the number of rows
' and using Offset to move down the original values and along the "pasted" columns
tablestart.Offset(0, i).Resize(blocksize, 1).Value = _
rng.Offset(blocksize * i, 0).Value
Next i
End Sub
Set the nblocks value to suit your needs, this is the number of resulting columns in your output table. You could get it dynamically by knowing the number of rows in the original column. Or you could use some while logic, careful to make sure that it does eventually exit of course!
Dim i As Long: i = 0
Do While rng.Offset(blocksize*i, 0).Cells(1).Value <> ""
tablestart.Offset(0, i).Resize(blocksize, 1).Value = rng.Offset(blocksize * i, 0).Value
i = i + 1
Loop
Edit: to get your column headings, keep in mind that the column headings are only 1 cell, so:
' Change this:
Set rng = ThisWorkbook.Sheets("Sheet1").Range("C" & firstrow & blocksize - 1)
' To this:
Set rng = ThisWorkbook.Sheets("Sheet1").Range("C" & firstrow)
Tip: + is used for adding numerical values, whilst & is used for concatenating stings.
Now when you're looping, you don't need the Resize, because you are only assigning 1 cell's value to 1 other cell. Resulting sub:
Sub NameToTable()
Dim blocksize As Long: blocksize = 51
Dim firstrow As Long: firstrow = 258
Dim rng As Range
Set rng = ThisWorkbook.Sheets("Sheet1").Range("C" & firstrow)
Dim tablestart As Range: Set tablestart = ThisWorkbook.Sheets("Sheet2").Range("B2")
Dim i As Long: i = 0
Do While rng.Offset(blocksize*i, 0).Value <> ""
tablestart.Offset(0, i).Value = rng.Offset(blocksize * i, 0).Value
i = i + 1
Loop
End Sub
When dealing with your worksheets in excel, each time you reference them adds overhead and slows down the code, what you want to do is take all of the info off your spreadsheet into an array then use Application.Transpose to transpose it for you.
You can then use 'Resize' to make sure your destination range is the same size and set the values.
Sub CopyAndTransRange(src As Range, dest As Range)
Dim arr As Variant 'Needs to be a variant to take cell values
arr = Application.Transpose(src.Value) 'Set to array of values
On Error GoTo eh1dim 'Capture error from vertical 1D range
dest.Resize( _
UBound(arr, 1) - LBound(arr, 1) + 1, _
UBound(arr, 2) - LBound(arr, 2) + 1 _
) = arr 'Set destination to array
Exit Sub
eh1dim:
dest.Resize( _
1, _
UBound(arr) - LBound(arr) + 1 _
) = arr 'Set row to 1D array
End Sub
Note, Application.Transpose will fall over with some arrays in weird circumstances like if there is more than 255 characters in a string in the given array, for those situations you can write your own Transpose function to flip the array for you.
Edit:
When you feed a vertical 1-dimensional range and transpose it, VBA converts it to a 1-dimensional array, I've rewritten so it captures the error when this happens then adjusts accordingly.
Just made this example which has values 1 through 7 populated on the first 7 rows of column A. This code effectively loops through each of the values, and transposes horizontally so all values are on a single row (1).
Dim rng As Range
Dim crng As Range
Static value As Integer
Set rng = ActiveSheet.Range("A1", Range("A1").End(xlDown))
For Each crng In rng.Cells
ActiveSheet.Range("A1").Offset(0, value).value = crng.value
If value <> 0 Then
crng.value = ""
End If
value = value + 1
Next crng
First we grab the required range and then iterate through each cell. Then using the offset method and an incrementing integer, we can assign their values horizontally to a single row.
It's worth noting that this would work when trying to transpose both vertically and horizontally. The key is the offset(column, row).
Just adjust where you place your incrementing Integer.
Hope this helps.
I would like to have an VBA to extract an alphanumeric value from a column G which is a sentence.
This sentence is generally a comment. So it includes characters and numbers.
The value always starts with AI0 and ends with 0. This can be 11 to 13 digits long. Sometimes the number is mentioned in the comment as AI038537500, also sometimes as AI038593790000.
I have researched through almost all the websites, but have not found any case like this. I know about the formulas, left, right, mid but in my case, it doesn't apply.
Any lead would be appreciable.
You may try something like this...
Place the following User Defined Function on a Standard Module and then use it on the sheet like
=GetAlphaNumericCode(A1)
UDF:
Function GetAlphaNumericCode(rng As Range)
Dim Num As Long
Dim RE As Object, Matches As Object
Set RE = CreateObject("VBScript.RegExp")
With RE
.Global = False
.Pattern = "AI\d{9,}0"
End With
If RE.Test(rng.Value) Then
Set Matches = RE.Execute(rng.Value)
GetAlphaNumericCode = Matches(0)
Else
GetAlphaNumericCode = "-"
End If
End Function
Why not give something like the following a try?
Sub findMatches()
Dim strLength As Integer
Dim i As Long
For i = 1 To Rows.Count
Dim AllWords As Variant
AllWords = Split(Cells(i, 7).Value, " ")
For Each Item In AllWords
strLength = Len(Item)
If strLength > 0 And strLength <= 13 And Item Like "A10*?#" Then
Cells(i, 8) = Item
End If
Next
Next i
End Sub
Test Cases:
I am apple and my batch number is: A10545440 so incase you needed to know
Result: A10545440
Some random comment… A20548650
Result: NO RESULT
A101234567891 is an awesome alphanumeric combo
Result: A101234567891
Another random comment… A10555
Result: A10555
Notice: The above example covers cases where the alphanumeric combo, starting with A10 is either:
In the middle of a sentence, or
Beginning of a sentence, or
At the end of a sentence
Also note: right now it is set to go through ALL the rows... so if you want to limit that, change the Rows.Count in the For statement to whatever your set limit is.
EDIT:
In the above code, I am explicitly asking it to look in column G
can you give this a try? I think it should do the job, also you should ammend the code with the column values, I tested it with the comments being in column C, while the code will be written in column D.
Option Explicit
Sub FindValue()
Dim i As Long
Dim lastrow As Long
Dim lFirstChr As Long
Dim lLastChr As Long
Dim CodeName As String
lastrow = activesheet.Range("c" & Rows.Count).End(xlUp).Row
' gets the last row with data in it
For i = 1 To lastrow
' shuffles through all cell in data
lFirstChr = InStr(1, Cells(i, 3), "A10") ' gets the coordinate of the first instance of "A10"
If lFirstChr = 0 Then GoTo NextIteration
lLastChr = InStr(lFirstChr, Cells(i, 3), " ") ' gets the coordinate of the first instansce of space after "A10"
If lLastChr = 0 Then 'if there is no space after A10 then sets lastchr to the lenght of the string
lLastChr = Len(Cells(i, 3))
End If
CodeName = Mid(Cells(i, 3).Value, lFirstChr, lLastChr - lFirstChr) ' extracts the codename from the string value
Range("d" & i).Value = CodeName
Goto NextTteration
NextIteration:
Next i
End Sub
I have a table with time values. If any value exists in a specified row that is greater than 90 minutes (1:30:00), I need to add the difference (i.e. how much greater it is) to a running total at the end of the row. So, that box could be blank, could have just one cell's value, or could have multiple values added. I already have the For loop to go through each cell in a row. I need the part to sum the values. And ideally, if there was a nested loop to do 6 separate sums for the 6 rows...
'Add break munutes
fp.Activate
Dim rng As Range
For Each rng In Range("B3:F3")
If rng.Value > TimeValue("1:31:00") Then
End If
Next rng
If you want to avoid VBA, this can actually be done by an Excel formula:
=SUMIF($B$3:$B$9,">"&1.5/24)-COUNTIF($B$3:$B$9,">"&1.5/24)*1.5/24
That sums up all values that exceed 90 minutes, and then subtracts off 90 minutes from the total for each value that has been counted.
I would recommend Excel Formula if that is an option, because of the restrictions of VBA solutions.
=SUMPRODUCT((B$3:B9-1/16)*(B$3:B9>1/16))
or a bit shorter with array formula (enter with Ctrl + Shift + Enter) :
=SUMIF(B$3:B9-1/16,">0")
Hard to say if this is fully accurate without more information, but something like this should work for you:
Sub tgr()
Dim aTimes As Variant
Dim dTime As Double
Dim aSumResults() As Double
Dim lResultIndex As Long
Dim i As Long, j As Long
Dim bFirst As Boolean
dTime = TimeValue("01:00:00")
aTimes = ActiveSheet.Range("B3:F9").Value 'Change to the full table range
ReDim aSumResults(1 To UBound(aTimes, 1) - LBound(aTimes, 1) + 1, 1 To 1)
For i = LBound(aTimes, 1) To UBound(aTimes, 1)
'Each i represents a row of the data
'Go through each column and collect the conditional sums
lResultIndex = lResultIndex + 1
bFirst = True 'Use bFirst to ignore first value greater than dTime
For j = LBound(aTimes, 2) To UBound(aTimes, 2)
If aTimes(i, j) > dTime Then
If bFirst Then
'This if the first value found for the row, ignore it
bFirst = False
Else
'Not the first value found, include in sum
aSumResults(lResultIndex, 1) = aSumResults(lResultIndex, 1) + aTimes(i, j) - dTime
End If
End If
Next j
Next i
'Output the results
ActiveSheet.Range("G3").Resize(UBound(aSumResults, 1)).Value = aSumResults
End Sub
You said you wanted the sum of the times in a row but then defined B3:B9, so I assumed you meant the sum of the times in a column.
Try this:
Dim i As Integer
Dim num1 As Date
For i = 3 To 9
If Cells(i, 2).Value > TimeValue("1:30:00") Then
num1 = Cells(10, 2).Value + Cells(i, 2).Value - TimeValue("1:30:00")
Cells(10, 2).Value = num1
End If
Next i
I've defined where the sum is put as cell B10. You could make a similar loop for each column. I tried this out and it worked for me.
I've done quite a bit of searching and can't find any code that matches my situation or to a point I can modify except for one.
Looking at the spreadsheet below. I want to have the user enter the OrderNumber then search Column A for every value of that number. As it does I want it to copy the ItemNumber and QtyOrdered to two different variables in order to put them into textboxes later on.
I want it to "stack" the information into the variable so something like ItemNumValues = ItemNumValues + Cell.Value
I tried to modify code from someone else ("their code") but I am getting a mismatch type error. The rest of the code works. There are some trace elements in the script from previous features that aren't used and I just haven't removed them yet.
'***********************************************************
'********** Their Code Follows *****************
'***********************************************************
Dim numentries As Integer
Dim i As Integer
'***********************************************************
'Get number of entries
numentries = Worksheets(Sheet1).UsedRange.Rows.Count
'*************************************************************
'Run loop to cycle through all entries (rows) to copy
For i = 1 To numentries
If (Worksheets("Sheet1").Cells(i + 2, 1).Value = InStr(1, Cell, OrderNumber, vbTextCompare)) Then
MsgBox Test
End If
Next i
End If
'***********************************************************
'********** End Their Code *****************
'***********************************************************
I recommend using a multidimensional array. If you've never used arrays before, I strongly suggest reading up on them.
Sub GatherData()
Dim c As Range
Dim aGetData() As Variant 'This is our array
Dim i As Integer
Dim a As Integer
Dim iRowCount As Integer
Dim sRange As String
'Gather data
iRowCount = Worksheets("Sheet1").UsedRange.Rows.Count
For Each c In Range("A2:A" & iRowCount)
If c.Value = 636779 Then
ReDim Preserve aGetData(2, i) 'An array must have a set size but as we
'do not know how many order numbers will be found we have to 'resize'
'the array to account for how many we do find. Using "ReDim Preserve"
'keeps any data we have placed into the array while at the same time
'changing it's size.
For a = 0 To 2 'Our first index will hold each col of data that is why
'it is set to 2 (arrays start at a base of zero, so
'0,1,2 will be each col(A,B,C)
aGetData(a, i) = c.Offset(0, a) 'This gets each value from col A,B and C
Next a
i = i + 1 'Increment for array in case we find another order number
'Our second index "aGetData(index1,index2) is being resized
'this represents each order number found on the sheet
End If
Next c
'How to read the array
For i = 0 To UBound(aGetData())
For a = 0 To 2
Debug.Print aGetData(a, i)
Next a
Next i
End Sub
It seems that the OrderNumber (column A) is sorted. Very good news (if they're not, just sort them ;) ). This simple function will get you the ItemNumbers and QtyOrdered into a bi-dimensional array, where each row is a pair of them.
Function ArrItemQty(ByVal OrderNumber As Long)
With Worksheets("Sheet1").UsedRange.Offset(1)
.AutoFilter 1, OrderNumber
ArrItemQty= .Resize(, 2).Offset(, 1).SpecialCells(xlCellTypeVisible).value
.Parent.AutoFilterMode = False
End With
End Function
And here's a little testing:
Sub Test()
Dim i As Long, j As Long, ar
ar = ArrItemQty(636779)
For i = LBound(ar, 1) To UBound(ar, 1)
Debug.Print
For j = LBound(ar, 2) To UBound(ar, 2): Debug.Print ar(i, j),: Next
Next
End Sub
p.s. be aware that the resulting array is 1-based. Use LBound and UBound as indicated is safest.