vba do until loop doesn't go in although condition is valid - excel

I won't bring the entire code, I'll try to show what is relevant.
The following code (which is inside a loop, but it doesn't matter) calls a function (compareStrings) which returns an integer.
Sheet1.Range("S" & i).Value = compareStrings(sheet1.Range("J" & i).Value, sheet1.Range("K" & i).Value)
So basically I have a loop that fills column S with integers.
I then sort S column in ascending order.
later I have another loop, that is supposed to do something with all the values that are less than 5.
The loop looks like this:
With Sheet1.Range("S" & i)
Do Until .Value < 5
If .Value = 0 Then
'some statement
Else
'some statement
End If
i = i + 1
Loop
End With
For some reason it doesn't go in the loop although I have many rows with values that are < 5. I actually tried to change it to <> and it doesn't go in either. It is as if it doesn't see it as an integer, although I have put integers in these cells.
Any ideas?
Thanks

"I have another loop that is supposed to do something with all the values that are less than 5."
Your logic doesn't make sense though.
Do Until .Value < 5 will not do anything with values less than 5. It's the same as saying "take action if my value is greater than or equal to 5.
Do While might be a better option.

Related

Possible faster VBA lookup with wildcards?

I need to perform a quite a lot of lookups with wildcards on the worksheet using a macro (mainly lookup for value & returning the value from another column though with proper adjustment it can be also just looking for a value with wildcard, and some lookups only as checks if the value exists in the dataset). My data can't be sorted and all the lookups are within a loop A or loops within loop A; wildcards are included mostly for condition "string begins with...". I often have to find a value in one row and find corresponding value in row N rows below or above.
I have a working code, but I wonder if it can be done faster. #response to comment about posting it on Code Review (sorry, I cannot comment yet :)) - preparation the whole code to posting would take a bit too much time for me, confidentiality etc, so I prefer to treat it as a general question to be worked on this example.
Example data (I can add more columns, if I need any helper column):
Example Data picture at Imgur
Assume 100 000 rows (max xPagesCount = 1000, typically around 400; all values for certain xPage is in one block). Due to a lot of possible rows with additional data I can't simply find one value and add numbers to the found row to find the other values by their position.
Example lookups to perform while looping through consecutive xPages (so, for each given xPage):
value in row just below row with "RESTRICTIONS:" text
find name (which is always given with height (column C) = 35)
find RSW number (which can be in several rows depending on page content, but always below name)
find all rows starting with the same four digits as RSW, in two formats: DDDD.LLL.DD and DDDD.DDDDD.DD (L letter, D digit) (I use internal loop here)
check if there is a text "MASTER" (or "MASTER " etc.)
find all values between values "DOCUMENTS:" and "OPTIONS:", which quantity can be from 1 to 50 (I use internal loop here)
I was wondering, what is the fastest way to do such lookups?
What I tried:
using a dictionary on all dataset (keys in column A or C with, values
col.D) but as dictionary can't work on wildcards, I had to add ifs
for not finding a key to perform additional Application.Match
lookup... and then realized it mostly worked on these Match lookups
and not sure I even need a dictionary. I also have duplicate values
within a page and dictionary was getting only first value, regardless
their position (for example, several attachments could have value 1).
The main use remained dict.exists("MASTER") but when I removed
dictionary and changed it to IsError(Application.Match(...)) the code
worked slightly faster.
Application.Match in whole range, typical example: Application.Match(xPage & "4???.*", sh.Range("A1:A" & LastRow), 0)
in few places I use If xValue Like "????.???.??" Then construction
I have dictionary lookups with ifs redirecting to Application.Match:
xValue = dict(xPage & "ATH.416")
If dict(xPage & "ATH.416") = "" Then xValue = Application.Match("ATH.*", Sheets(1).Range("D:D"), 0)
What I consider, but not sure it's worth the effort:
altering the code that at the beginning of the iteration I find the first and the last row for xPage, and then each later check is performed in this range
xStartPage = sh.Range("D" & Application.Match(xPage, sh.Range("A1:A" & LastRow), 0))
'or, I guess better:
xStartPage = xEndPage + 1
If xPage = xPagesCount Then
xEndPage = LastRow
Else
xEndPage = sh.Range("D" & Application.Match(xPage + 1, sh.Range("A1:A" & LastRow), 0) - 1)
End If
xValue = sh.Range("D" & Application.Match("4???.*", sh.Range("D" & xStartPage & ":D" & xEndPage), 0)).Value

How to get non-negative value using Do until or if statement?

I have a polynomial equation that i want to solve: L^3-4043L-60647=0 using goal seek in the vba.
This equation gives 3 roots : L1=70.06, L2, -54.04 and L3=-16.02 according to my calculator. But i only want my L in my excel cell to show the first positive root as my answer.
However when i do the goalseek using vba, it only gives me -16.02. How do i tell in my code to only solve for positive value?
I already tried using Do until and if statement. However Do until statement kept crashing and If statement is giving me wrong values.
Sub GoalSeek()
'GoalSeek Macro
Dim Length As Double
Dim i As Long
Range("Length") = i
If i > 0 Then
Application.CutCopyMode = False
Application.CutCopyMode = False
Range("GS").GoalSeek Goal:=0.1, ChangingCell:=Range("Length")
Else
End If
End Sub
I tried using this if statement. However my L or "Length" comes up only to be 0. I am very very beginner level in VBA. I don't know what i am doing wrong.
GoalSeek gets the nearest solutions to the starting value.
You can use the following code:
Sub GoalSeek()
Dim i As Double
'Set the initial value to a very high number
Range("Result").Value = 9999
'Ask GoalSeek to get the neares solution to that high value
Range("Formula").GoalSeek Goal:=0, ChangingCell:=Range("Result")
If Range("Result").Value > 0 Then
'If the value is positive, we need to make sure that it is the first positive solution
i = -1
Do
i = i + 1
'Set a new inital value. This time, a small one (starting from 0)
Range("Result").Value = i
'Ask GoalSeek to get the neares solution to the small initial value
Range("Formula").GoalSeek Goal:=0, ChangingCell:=Range("Result")
'If the result is negative, loop (increase the initial value and try again till you find the first positive one
Loop While Range("Result").Value < 0
Else 'If the nearest result to the high value is negative, keep it & show a message box.
MsgBox "No +ve solution found"
End If
End Sub
In your example, you have three solutions 70.06, -54.04 & -16.02
The nearest to 0 is -16.02, to 9999 is 70.6 and to -9999 is -54.04
What if the solutions are -5, 7 & 12?
The nearest to 9999 is 12, but you want 7, right?
So we ask for the nearest to 0 (-5) then, we keep increasing the initial value till the nearest solution becomes 7.
Please note that this assumes that you have an idea about what the results would be.
For example, if the solutions are -1 & 1,000,000, this code will not work because -1 is nearer to 9999 than 1,000,000.
In this case, you will need to change the initial high value more.
AND if you set it to a too high value that exceeds the limit of double data type 1.79E+308 or even to a value that makes the result of the formula exceed it, you will get an error.

How can I lookup data from one column, when the value I'm referencing changes columns?

I want to do an INDEX-MATCH-like lookup between two documents, except my MATCH's index array doesn't stay in one column.
In Vague-English: I want a value from a known column that matches another value that may be found in any column.
Refer to the image below. Let's call everything to the left of the bold vertical line on column H doc1, and the right side will be doc2.
Doc2 has a column "Find This", which will be the INDEX's array. It is compared with "ID1" from doc1 (Note that the values in "Find This" will not be in the same order as column ID1, but it's easier to undertsand this way).
The "[Result]" column in doc2 will be the value from doc1's "Want This" column from the row that matches "FIND THIS" ...However, sometimes the value from "FIND THIS" is not in the "ID1" column, and is instead in "ID2","ID3", etc.
So, I'm trying to generate Col K from Col J. This would be like pressing Ctrl+F and searching for a value in Col J, then taking the value from Col D in that row and copying it to Col K.
I made identical values from a column the same color in the other doc to make it easier to visualize where they are coming from.
Note also that in column F of doc1, the same value from doc2's "Find This" can be found after some other text.
Also note that the column headers are only there as examples, the ID columns are not actually numbered.
I would simply hard-code the correct column to search from, but I'm not in control of doc1, and I'm worried that future versions may have new "ID" columns, with other's being removed.
I'd prefer this to be a solution in the form of a formula, but VB will do.
To generate column K based on given values of column J then you could use the following:
=INDEX(doc1!$D$2:$D$14,SUMPRODUCT((doc1!$B$2:$H$14=J2)*ROW(doc1!$B$2:$H$14))-1)
Copy that formula down as far as you need to go.
It basically only returns the row of the where a matching column J is found. we then find that row in the index of your D range to get your value in K.
Proof of concept:
UPDATE:
If you are working with non unique entities n column J. That is the value on its own can be found in multiple rows and columns. Consider using the following to return the Last row where there J value is found:
=INDEX(doc1!$D$2:$D$14,AGGREGATE(14,6,(doc1!$B$2:$H$14=J2)*ROW(doc1!$B$2:$H$14),1)-1)
UPDATE 2:
And to return the first row where what you are looking in column J is found use:
=INDEX($D$2:$D$14,AGGREGATE(15,6,1/($B$2:$H$14=J2)*ROW($B$2:$H$14)-1,1))
Thanks to Scott Craner for the hint on the minimum formula.
To determine if you have UNIQUE data from column J in your range B2:H14 you can enter this array formula. In order to enter an array formula you need to press CTRL+SHFT+ENTER at the same time and not just ENTER. You will know you have done it right when you see {} around your formula in the formula bar. You cannot at the {} manually.
=IF(MAX(COUNTIF($B$2:$H$14,J2:J14))>1,"DUPLICATES","UNIQUE")
UPDATE 3
AGGREGATE - A relatively new function to me but goes back to Excel 2010. Aggregate is 19 functions rolled into 1. It would be nice if they all worked the same way but they do not. I think it is functions numbered 14 and up that will perform the same way an array formula or a CSE formula if you prefer. The nice thing is you do not need to use CSE when entering or editing them. SUMPRODUCT is another example of a regular formula that performs array formula calculations.
The meat of this explanation I believe is what is happening inside of the AGGREGATE brackets. If you click on the link you will get an idea of what the first two arguments are. The first defines which function you are using, and the second tell AGGREGATE how to deal with Errors, hidden rows, and some other nested functions. That is the relatively easy part. What I believe you want to know is what is happening with this:
(doc1!$B$2:$H$14=J2)*ROW(doc1!$B$2:$H$14)
For illustrative purpose lets reduce this formula to something a little smaller in scale that does the same thing. I'll avoid starting in A1 as that can make life a little easier when counting since it the 1st row and first column. So by placing the example range outside of it you can see some more special considerations potentially.
What I want to know is what row each of the items list in Column C occurs in column B
| B | C
3 | DOG | PLATYPUS
4 | CAT | DOG
5 | PLATYPUS |
The full formula for our mini example would be:
{=($B$3:$B$5=C2)*ROW($B$3:$B$5)}
And we are going to look at the following as an array
=INDEX($B$3:$B$5,AGGREGATE(14,6,($B$3:$B$5=C2)*ROW($B$3:$B$5),1)-2)
So the first brackets is going to be a Boolean array as you noted. Every cell that is TRUE will TRUE until its forced into a math calculation. When that happens, True becomes 1 and False becomes 0.I that formula was entered as a CSE formula and place in D2, it would break down as follows:
FALSE X 3
FALSE X 4
TRUE X 5
The 3, 4 and 5 come from ROW() returning the value of the row number that it is dealing with at the time of the array math operation. Little trick, we could have had ROW(1:3). Just need to make sure the size of the array matches! This is not matrix math is just straight across multiplication. And since the Boolean is now experiencing a math operation we are now looking at:
0 X 3 = 0
0 X 4 = 0
1 X 5 = 5
So the array of {0,0,5} gets handed back to the aggregate for more processing. The important thing to note here is that it contains ONLY 0 and the individual row numbers where we had a match. So with the first aggregate formula, formula 14 was chosen which is the LARGE function. And we also told it to ignore errors, which in this particular case does not matter. So after providing the array to the aggregate function, there was a ,1) to finish off the aggregate function. The 1 tells the aggregate function that we want the 1st larges number when the array is sorted from smallest to largest. If that number was 2 it would be the 2nd largest number and so on. So the last row or the only row that something is found on is returned. So in our small example it would be 5.
But wait that 5 was buried inside another function called Index. and in our small example that INDEX formula would be:
=INDEX($B$3:$B$5,AGGREGATE(...)-2)
Well we know that the range is only 3 rows long, so asking for the 5th row, would have excel smacking you up side the head with an error because your index number is out of range. So in comes the header row correction of -1 in the original formula or -2 for the small example and what we really see for the small example is:
=INDEX($B$3:$B$5,5-2)
=INDEX($B$3:$B$5,3)
and here is a weird bit of info, That last one does not become PLATYPUS...it becomes the cell reference to =B5 which pulls PLATYPUS. But that little nuance is a story for another time.
Now in the comments Scott essentially told me to invert for the error to get the first row. And this is important step for the aggregate and it had me running in circles for awhile. So the full equation for the first row option in our mini example is
=INDEX($B$3:$B$5,AGGREGATE(15,6,1/($B$3:$B$5=C2)*ROW($B$3:$B$5),1)-2)
And what Scott Craner was actually suggesting which Skips one math step is:
=INDEX($B$3:$B$5,AGGREGATE(15,6,ROW($B$3:$B$5)/($B$3:$B$5=C2),1)-2)
However since I only realized this after writing this all up the explanation will continue with the first of these two equations
So the important thing to note here is the change from function 14 to function 15 which is SMALL. Think of it a finding the minimum. And this time that 6 plays a huge factor along with the 1/. So our array in the middle this time equates to:
1/FALSE X 3
1/FALSE X 4
1/TRUE X 5
Which then becomes:
1/0 X 3
1/0 X 4
1/1 X 5
Which then has excel slapping you up side the head again because you are trying to divide by 0:
#div/0 X 3
#div/0 X 4
1/1 X 5
But you were smart and you protected yourself from that slap upside the head when you told AGGREGATE to ignore error when you used 6 as the second argument/reference! Therefore what is above becomes:
{5}
Since we are performing a SMALL, and we passed ,1) as the closing part of the AGGREGATE, we have essentially said give me the minimum row number or the 1st smallest number of the resulting array when sorted in ascending order.
The rest plays out the same as it did for the LARGE AGGREGATE method. The pitfall I fell into originally is I did not use the 1/ to force an error. As a result, every time I tried getting the SMALL of the array I was getting 0 from all the false results.
SUMPRODUCT works in a very similar fashion, but only works when your result array in the middle only returns 1 non zero answer. The reason being is the last step of the SUMPRODUCT function is to all the individual elements of the resulting array. So if you only have 1 non zero, you get that non zero number. If you had two rows that matched for instance 12 and 31, then the SUMPRODUCT method would return 43 which is not any of the row numbers you wanted, where as aggregate large would have told you 31 and aggregate small would have told you 12.
Something like this maybe, starting in K2 and copied down:
=IFERROR(INDEX(D:D,MAX(IFERROR(MATCH(J2,B:B,0),-1),IFERROR(MATCH(J2,E:E,0),-1),IFERROR(MATCH(J2,G:G,0),-1),IFERROR(MATCH(J2,H:H,0),-1))),"")
If you want to keep the positions of the columns for the Match variable, consider creating generic range names for each column you want to check, like "Col1", "Col2", "Col3". Create a few more range names than you think you will need and reference them to =$B:$B, =$E:$E etc. Plug all range names into Match functions inside the Max() statement as above.
When columns are added or removed from the table, adjust the range name definitions to the columns you want to check.
For example, if you set up the formula with five Matches inside the Max(), and the table changes so you only want to check three columns, point three of the range names to the same column. The Max() will only return one result and one lookup, even if the same column is matched several times.
I came up with a vba solution if I understood correctly:
Sub DisplayActiveRange()
Dim sheetToSearch As Worksheet
Set sheetToSearch = Sheet2
Dim sheetToOutput As Worksheet
Set sheetToOutput = Sheet1
Dim search As Range
Dim output As Range
Dim searchCol As String
searchCol = "J"
Dim outputCol As String
outputCol = "K"
Dim valueCol As String
valueCol = "D"
Dim r As Range
Dim currentRow As Integer
currentRow = 1
Dim maxRow As Integer
maxRow = sheetToOutput.UsedRange.Rows.Count
For currentRow = 1 To maxRow
Set search = Range("J" & currentRow)
For Each r In sheetToSearch.UsedRange
If r.Value <> "" Then
If r.Value = search.Value Then
Set output = sheetToOutput.Range(outputCol & currentRow)
output.Value = sheetToSearch.Range(valueCol & currentRow).Value
currentRow = currentRow + 1
Set search = sheetToOutput.Range(searchCol & currentRow)
End If
End If
Next
Next currentRow
End Sub
There might be better ways of doing it, but this will give you what you want. We assume headers in both "source" and "destination" sheets. You will need to adapt the "Const" declarations according to how your sheets are named. Press Control & G in Excel to bring up the VBA window and copy and paste this code into "This Workbook" under the "VBA Project" group, then select "Run" from the menu:
Option Explicit
Private Const sourceSheet = "Source"
Private Const destSheet = "Destination"
Public Sub FindColumns()
Dim rowCount As Long
Dim foundValue As String
Sheets(destSheet).Select
rowCount = 1 'Assume a header row
Do While Range("J" & rowCount + 1).value <> ""
rowCount = rowCount + 1
foundValue = FncFindText(Range("J" & rowCount).value)
Sheets(destSheet).Select
Range("K" & rowCount).value = foundValue
Loop
End Sub
Private Function FncFindText(value As String) As String
Dim rowLoop As Long
Dim colLoop As Integer
Dim found As Boolean
Dim pos As Long
Sheets(sourceSheet).Select
rowLoop = 1
colLoop = 0
Do While Range(alphaCon(colLoop + 1) & rowLoop + 1).value <> "" And found = False
rowLoop = rowLoop + 1
Do While Range(alphaCon(colLoop + 1) & rowLoop).value <> "" And found = False
colLoop = colLoop + 1
pos = InStr(Range(alphaCon(colLoop) & rowLoop).value, value)
If pos > 0 Then
FncFindText = Mid(Range(alphaCon(colLoop) & rowLoop).value, pos, Len(value))
found = True
End If
Loop
colLoop = 0
Loop
End Function
Private Function alphaCon(aNumber As Integer) As String
Dim letterArray As String
Dim iterations As Integer
letterArray = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
If aNumber <= 26 Then
alphaCon = (Mid$(letterArray, aNumber, 1))
Else
If aNumber Mod 26 = 0 Then
iterations = Int(aNumber / 26)
alphaCon = (Mid$(letterArray, iterations - 1, 1)) & (Mid$(letterArray, 26, 1))
Else
'we deliberately round down using 'Int' as anything with decimal places is not a full iteration.
iterations = Int(aNumber / 26)
alphaCon = (Mid$(letterArray, iterations, 1)) & (Mid$(letterArray, (aNumber - (26 * iterations)), 1))
End If
End If
End Function

My for loop is too complex - Excel VBA

I'm still writing a program in Excel VBA, and I've been stuck on a specific problem for hours. I'm trying to determine if someone is available on a specific day, but the information I'm given is for when they aren't available.
So I'm trying to write a series of for loops to compare all of this information and put it into Excel. I'm facing the issue, though, of the fact that it's quite complex:
With ActiveSheet
' Check the ID_Array and Date_Array to see if any names match
For DateIndex = 0 To MinLimit
For IDIndex = 0 To ID_Number
'If (ID_Array(IDIndex, 0) = Date_Array(DateIndex, 0)) Then
' We're going to use our column not only as a match check, but as our way of ending the cycle
For colCounter = 2 To 6
' If the date in the doc and the date in the array match, continue
If (.Cells(1, colCounter).Text = Date_Array(DateIndex, 1)) Then
For rowCounter = 2 To 11
' If the time slot in this row matches up with the time slot in the array, and the name also matches up, mark the name to be unused in this row
If (.Cells(rowCounter, 1).Text = Date_Array(DateIndex, 2)) Then
If (ID_Array(IDIndex, 0) = Date_Array(DateIndex, 0)) Then
ID_Array(IDIndex, 3) = "1"
End If
End If
' If the name has not been flagged as 1, write it down in this row
If (ID_Array(IDIndex, 3) <> "1") Then
supahotstring = ", " + ID_Array(IDIndex, 1) + " " + ID_Array(IDIndex, 2)
.Cells(rowCounter, colCounter).Value = .Cells(rowCounter, colCounter).Text + supahotstring
End If
' If the name HAS been flagged as 1, unflag it for the next row, since it might need to be written there
If (ID_Array(IDIndex, 3) = "1") Then
ID_Array(IDIndex, 3) = "0"
End If
' Now that all names can be checked for availability again, move on to the next row in this column
Next
End If
Next
'End If
Next
Next
End With
Above is the series of for loops I'm using. for reference, I use SQL to draw in the data, so everything is organized by records.
Since the possible available dates are fixed, I use the values I have set up in my Excel doc as a comparison to the dates people have put in. Basically, if someone has marked a specific timeslot on a specific day, I want to flag the record with that name as "1" so that it doesn't get marked down in that row. Then, I see if the current name is marked as "1", apply the appropriate actions, unmark it as "1" so that it can be checked again later.
However, when I run the program, a variety of problems occur: sometimes, the program will freeze up and I'll be forced to end task (I save quite often these days). Other times, when I write, it will simply write the same names in the same order in every box.
I feel like this should be a relatively easy problem to fix, but at this point I need a second opinion. I need to talk it over with someone here who's willing to look at it, and perhaps even find a way to do this without using four for loops.
It's a fairly complex program, and if you need more code for reference then I'm willing to provide it. I've tried to comment this code enough that it's fairly easy to understand. I really appreciate any help you can give.
EDIT: I cannot provide an image of expected output, but I can describe it. Imagine John Doe is going to be available on the first day, after 9:00AM. The program simply writes his name, along with every else's names, eleven time in each box.

Load n Record of Spreadsheet where parameter matches

Working in Microsoft Excel, I have a spreadsheet with the equivalent of a shopping list, if the Quantity is greater than 0 then I wish to display the description on another sheet.
This is something that is quite simple using the INDEX function, however this only returns the first value that matches.
How should I re-factor the query below, to return the value of (n)
=INDEX(Software!B23:Software!B34,MATCH(TRUE,INDEX(Software!A23:Software!A34<>0,0),0))
Having assumed this to be an array, I mistaking thought I could call Array[n] for the location, however this has proven incorrect.
Thank you for your assistance.
In this case, you don't want to use MATCH(), you want to use the SMALL() and IF() functions together...
=INDEX(Software!$B$23:$B$34,SMALL(IF(Software!$A$23:$A$34>0,ROW(Software!$A$23:$A$34)),ROW(A1))-ROW(Software!$B$23)+1)
Entered as an array formula
Basically, what you're saying is:
Give me the row for which the data in Column A > 0 for the Kth smallest time
Now take that row and subtract from it the starting row of my dataset and give me back that entry from the array I have in column A.
And you could wrap the whole thing in an IFERROR() statement too to not have error values pop up.
One place to find more data about this would be here
Hope that makes sense and does the trick!
This might be useful (dont know exactly this is what u want)
Sub test_index()
Dim ii As Integer, jj As Integer
ii = 23
jj = 24
With ActiveSheet
For ii = 23 To 34
Cells(ii, 12).Formula = "=INDEX(Software!B" & ii & ":Software!B" & ii & _
",MATCH(TRUE,INDEX(Software!A" & ii & ":Software!A" & ii & ",0,0),0))"
Next
End With
End Sub

Resources