Excel recursion - find last in a sequence - excel

Using Excel 2010 & have a list of parts, some of which can have their part number changed. If a part has been changed, it will be in one worksheet (Column A), and its new value (column B). There is also a date time stamp on the change.
In my list of all part numbers, I need to
Check to see if the part has been changed, and
Follow it through each of it's changes until I find the last one.
The "last one" can mean EITHER
The value in Column B (newVal) is not in Column A(oldval) OR
If it refers back to itself (a correction), in which case the newest Date Time stamp is the determining factor.
Corrections to the list are entered by adding a row which would redirect it to the previous (correct) version. An example:
oldVal A changes to newVal B 2016-04-08 11:39:04.765
oldVal B changes to newVal C 2016-04-08 12:21:39.801
(we find out the B--> C change is incorrect)
oldVal C changes to newVal B 2016-04-08 13:44.07.913
I know that a vLookup won't do this. I think that SUMPRODUCT may solve the first part, but I'm not sure how to do the recursion (or if it's possible) for the time part of it.
Any thoughts, ideas, solutions would be appreciated.
EDIT
Additional Info -
The desired output
What I want to show in the NewPartNo column is the last value in the chain for that part number. If the Part number went from A --> B --> C --> D, I would want to see D in the NewPartNo column for the PartNo in column A.
You have a 1960-1 that has a newVal of 25-1960. The problem is that the 25-1960 then has a newVal of OBB1960. How many levels of recursive misplaced vals are there?
That's the issue - in some places a part may be (originally) named any one of the ones in column A. As far as how many levels - I don't know. Possibly 5-10, maybe? I don't know exactly.
EDIT 2
#TMDean's solution mostly worked, except for when I had data like below.
If starting with A001, it maps to B001. B001's newest (most recent date) mapping is to C001. C001's mapping (to itself) has the same date-time stamp as the other one, so A001 should map to C001.
In stepping through the function, it will find the first one (A001 -> B001), but throws an error when it tries to find the second one (B001 -> C001).
In the above, the correct result of the lookup should be A001 -> C001.

Here are two UDFs¹ to retrieve the recursive newVal and it's associated date.
Option Explicit
Function newestVal(lu As Range, rng As Range)
Dim val As Variant
Static app As New Application
'truncate the rng down to the used range
Set rng = Intersect(rng, rng.Parent.UsedRange)
val = lu.Value2
With rng
If IsError(app.Match(lu.Value2, .Columns(1), 0)) Then
val = app.Index(.Columns(2), app.Match(val, .Columns(2), 0), 1)
Else
Do While Not IsError(app.Match(val, .Columns(1), 0))
val = app.Index(.Columns(2), app.Match(val, .Columns(1), 0), 1)
Loop
End If
End With
newestVal = val
End Function
Function latestValDate(lu As Range, rng As Range)
Dim d As Long, val As Variant, dbl As Double
Static app As New Application
'truncate the rng down to the used range
Set rng = Intersect(rng, rng.Parent.UsedRange)
val = lu.Value2
With rng
If IsError(app.Match(lu.Value2, .Columns(1), 0)) Then
val = app.Index(.Columns(2), app.Match(val, .Columns(2), 0), 1)
Else
Do While Not IsError(app.Match(val, .Columns(1), 0))
val = app.Index(.Columns(2), app.Match(val, .Columns(1), 0), 1)
Loop
End If
For d = app.Match(val, .Columns(2), 0) To rng.Rows.Count
If .Cells(d, 2).Value2 = val Then
If .Cells(d, 3).Value2 > dbl Then
dbl = .Cells(d, 3).Value2
End If
End If
Next d
End With
latestValDate = dbl
End Function
In F2:G2 as,
=newestVal(E2,A:C )
=latestValDate(E2,A:C )
    
¹ A User Defined Function (aka UDF) is placed into a standard module code sheet. Tap Alt+F11 and when the VBE opens, immediately use the pull-down menus to Insert ► Module (Alt+I,M). Paste the function code into the new module code sheet titled something like Book1 - Module1 (Code). Tap Alt+Q to return to your worksheet(s).

This is all psuedo code but it should get you ion the right direction. Give me a couple of hours and I can do it for you, but it might be fun for you to try and figure it out.
If Selection Offset Column + 1 <> vbNullString then
newvalue = Selection Offset Column + 1.value
for i = 1 to 100
If newvalue Offset Column + 1 = vbNullString then
Display (i)a,b,c
Exit For
ElseIf newvalue Offset Column + 1 <> vbNullString then
newvalue = newvalue Offset Column + 1.value
Next i

Inspired by Jeeped's solution, I wondered if it could be more general purpose and wrote a UDF implementation of a recursive version of VLOOKUP.
This function works just like VLOOKUP except it will follow the chain of lookup values until it gets to the end of the table, as described in the problem.
Option Explicit
Function VLOOKUPR(LookupValue As Variant, _
TableArray As Range, _
ColIndexNum As Long, _
RangeLookup As Boolean)
Dim LookupIndex
Set TableArray = Intersect(TableArray, TableArray.Parent.UsedRange)
VLOOKUPR = CVErr(xlErrNA)
Do
LookupIndex = Application.Match(LookupValue, TableArray.Columns(1), _
IIf(RangeLookup, 1, 0))
If IsError(LookupIndex) Then Exit Do
VLOOKUPR = TableArray(LookupIndex, ColIndexNum)
If LookupValue <> TableArray(LookupIndex, 1) Then Exit Do
LookupValue = TableArray(LookupIndex, ColIndexNum)
Set TableArray = Intersect(TableArray, TableArray.Offset(LookupIndex))
Loop Until TableArray Is Nothing
End Function
Since I wanted it to be general purpose, I didn't include the requirement that it return the lookup value instead of #N/A if it exists in column B. You can include this logic in the cell formula you use to call VLOOKUPR. =IFERROR(VLOOKUPR($E2,$A$2:$A$100,2,FALSE),VLOOKUP($E2,$B$2:$B$100,1,FALSE))

Related

UDF recalculates when data is entered in other occurrence of the UDF

Summary: all the occurrences of a UDF recalculate when one of them has a source changed.
I have a fairly simple UDF (code below) that calculates the stableford score of a golf round based on a couple of variables. Now I find that the UDF seems to be semi-volatile, in that as soon as I enter data in the data entry range (HoleScores) ALL of my occurrences of the UDF recalculate, even on other sheets. But if I press F9 (or choose to recalculate) they do not recalculate.
The desired situation is that only the UDF for which the data is entered recalculates. Can anybody help me achieve that?
nb: the HoleScores range is only referenced by one single UDF. All occurrences of the UDF use unique entry ranges. I have tested the recalc with the VBA screen open and closed. I am using Excel 2016
Public Function WACRondeScore(PlayingHandicap, Pars As Range, _
StrokeIndexen As Range, HoleScores As Range, _
Afgelast As String) As Variant
On Error GoTo FuncFail
Dim Hole As Long
Dim StablefordPuntenRonde As Long
Dim StablefordPunten As Long
If PlayingHandicap = "" Then
WACRondeScore = ""
Exit Function
Else
PlayingHandicap = CLng(PlayingHandicap)
End If
' Afgelast
If Not Afgelast = "" Then
WACRondeScore = "A"
Exit Function
End If
If IsEmptyRange(HoleScores) Then
WACRondeScore = ""
Exit Function
End If
For Hole = 1 To 9
If IsInteger(HoleScores(1, Hole)) Then
StablefordPunten = (Pars(1, Hole) + 2 + Int(((PlayingHandicap * 2) - StrokeIndexen(1, Hole) + 18) / 18)) - HoleScores(1, Hole)
If StablefordPunten < 0 Then StablefordPunten = 0
StablefordPuntenRonde = StablefordPuntenRonde + StablefordPunten
End If
Next Hole
WACRondeScore = StablefordPuntenRonde
Debug.Print "wacRONDESCORE"
Exit Function
FuncFail:
WACRondeScore = CVErr(xlErrValue)
End Function
I think I have found the cause of the recalculation. One of the entry values (PlayingHandicap) seems to be culprit. Don't know why, as yet, but am searching for the bug

Excel VBA Comparing strings to ascertain whether contents of cell are in the correct format if they need to be

I'm creating a validation spreadsheet, where the user will input data before it gets imported into our company database.
I had it all finished, but then we realised there was one remaining hole, which I am struggling to plug.
For my test to see if I can get something to do what I want, the user enters data in columns A and B, starting at row 2. Column A is mandatory, column B is a mix - for majority of entries in A then B is optional, but for certain entries in A then B is required AND is required in a predefined format based on A.
eg.
user enters "12345678" in A2, and "12345678" is nothing special so an entry in B2 is optional
user enters "11111111" in A3, and "11111111" is special, so they do need to put an entry in B3, and it is required in the format ab12cde (??##???)
So far I have a small table in range N2:O6 to use for a Vlookup - N is the A entry (11111111, 22222222, etc) and B is the mask required (??##???, ##?#?, etc)
I know I will need to loop the code eventually, but I removed that as want to get 1 run-through done first.
Dim b As String
Dim suf As String
Dim zzz As Integer
Last_Row_Suf = Sheets("Sheet1").Cells(Rows.Count, "A").End(xlUp).Row
Debug.Print Last_Row_Suf
zzz = 2
If zzz <= Last_Row_Suf Then
suf = "test"
suf = Application.VLookup(Range("A" & zzz), Range("N2:O6"), 2, False)
b = Range("B" & zzz).Value
If suf Like b Then
Range("D" & zzz).Value = 1
Else
Range("D" & zzz).Value = 0
End If
zzz = zzz + 1
Else
End If
Debug.Print suf
Debug.Print b
The issue I am facing is that Like is returning False when I try to compare for example xy45trn and ab12cde
I also need to put in the earlier steps to check if A2 is in the mandatory table or not as if it isn't then I will just skip as I don't care what is in B2 then, but suf doesn't get updated when the Vlookup fails.
Probably lots of issues, but hopefully someone can point me in the right direction.
Thanks
Something like this::
Sub Tester()
Dim c As Range, m, v
Dim wsData As Worksheet, wsLookup As Worksheet
Set wsData = ThisWorkbook.Sheets("Sheet1")
Set wsLookup = ThisWorkbook.Sheets("Config")
'loop over the input in ColA
For Each c In wsData.Range(wsData.Range("A2"), _
wsData.Cells(Rows.Count, 1).End(xlUp)).Cells
'lookup table is on a separate sheet
m = Application.VLookup(c.Value, wsLookup.Range("A2:B20"), 2, False)
If Not IsError(m) Then
'got a hit from the lookup table
v = Trim(c.Offset(0, 1).Value)
'using Like
c.Offset(0, 3).Value = IIf(v Like m, "OK", "Error")
'using RegExp
'c.Offset(0, 3).Value = IIF(MatchesPattern(v, m), "OK", "Error")
End If
Next c
End Sub
If Like isn't meeting your needs then you can use a regexp object to perform the vbalidation: a bit more complex in terms of pattern, but much more powerful.
Function MatchesPattern(v, patt As String) As Boolean
Static reg As Object
If reg Is Nothing Then Set reg = CreateObject("VBScript.RegExp")
reg.Pattern = patt
MatchesPattern = reg.Test(v)
End Function
RegExp reference: https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/scripting-articles/ms974570(v=msdn.10)?redirectedfrom=MSDN

Write several values in one string

I am new to both VBA and stackoverflow. So please be patient ;).
I searched for a solution but could not find it.
My problem is as follows:
I have a column (A) with names and then a column (B) where some cells contain an "X" and others do not. I want to know which names have an "X" besides them.
Example:
I want now a string as a result, in one cell.
In this example:
Noah;Jacob;Elijah;Jayden
I got not very far.
For r = 1 To 20
If Cells(r, 2) = "X" Then A = Cells(r, 1) Else
Next
Then "A" is "Noah" and I can write it in a cell, but I want it to find all values and then write them combined, preferable seperated by ; in a cell.
Does anyone have any idea?
Create a string variable, then append your results to that variable based on "X" being in column B. Here's an example of how you could do it:
Sub Foo()
Dim i As Integer
Dim result As String
For i = 1 To 20
If UCase(Cells(i, 2).Value) = "X" Then
result = result & Cells(i, 1).Value & ";"
End If
Next
'// output the result to C1
Range("C1").Value = Left$(result, Len(result) - 1)
End Sub
Excel's native worksheet formulas do not handle concatenating an unknown number of strings together and compensating for the maximum number possible can get messy. A User Defined Function¹ (aka UDF) takes advantage of VBA's ability to process loops through a large number of rows while making numerical or string comparisons 'on-the-fly'.
build_List UDF
Function build_List(rNAMs As Range, rEXs As Range, vEX As Variant, _
Optional delim As String = ";", _
Optional bCS As Boolean = False)
Dim str As String, rw As Long, cl As Long
With rNAMs.Parent
Set rNAMs = Intersect(.UsedRange, rNAMs)
Set rEXs = .Cells(rEXs.Rows(1).Row, rEXs.Columns(1).Column). _
Resize(rNAMs.Rows.Count, rNAMs.Columns.Count)
End With
With rNAMs
For rw = .Rows(1).Row To .Rows(.Rows.Count).Row
For cl = .Columns(1).Row To .Columns(.Columns.Count).Row
If (.Cells(rw, cl).Offset(0, rEXs.Column + (cl - 1) - cl) = vEX And bCS) Or _
(LCase(.Cells(rw, cl).Offset(0, rEXs.Column + (cl - 1) - cl)) = LCase(vEX)) Then _
str = str & .Cells(rw, cl).Value & delim
Next cl
Next rw
End With
build_List = Left(str, Len(str) - Len(delim))
End Function
In D7 (as per image below) as,
=build_List(A:A, B:B, "x")
                               Applying the build_Lists UDf to your sample data
¹ A User Defined Function (aka UDF) is placed into a standard module code sheet. Tap Alt+F11 and when the VBE opens, immediately use the pull-down menus to Insert ► Module (Alt+I,M). Paste the function code into the new module code sheet titled something like Book1 - Module1 (Code). Tap Alt+Q to return to your worksheet(s).
Mate Juhasz answered the question very nice and simple, but now the answer dissapeared.
Mate wrote:
For r = 1 To 20
If Cells(r, 2) = "X" Then A = A & "; " & Cells(r, 1) Else
Next
And for me that solved it perfectly. Now "A" is a string as I wanted. Thank you so much!

Dynamically read in Column

I have a problem. I spent hours designing a form which works just great with all your feedback. Today, everything went wrong. The reason for this is simple. A few new columns got added and, obviously, the data my form is reading in is now wrong.
Thus I was thinking of trying the following...
Rather than using the column number as below
TK = Cells(ActiveCell.Row, "S").Value 'everything in the form refers to the active row
I could possibly use the column headings in Row 1.
Is that possible ? This way the spreadsheet can have columns added up to as many as a user would like and the form would dynamically scan for the right heading and get the column number that way.
My thought is, on opening the form, read in all the headings, pick out the ones I need and assign them to a variable. Then I use my normal code and substitute the variable into the column section.
It sounds easy, but I have no idea how to do this.
Use the versatile Find to give you a quick method of detecting where your header is - or if it is missing
Find details here
In the code below I have specified that the search must return
an exact match (xlWhole)
a case sensitive match (False)
The match can be a partial match (xlPart) if you were looking to match say Game out of Game X
code
Const strFind = "Game"
Sub GetEm()
Dim rng1 As Range
Set rng1 = ActiveSheet.Rows(1).Find(strFind, , xlValues, xlWhole, , , False)
If Not rng1 Is Nothing Then
MsgBox "Your column is " & rng1.Column
Else
MsgBox strFind & " not found", vbCritical
End If
End Sub
Why use a loop? There's no need to.
Dim col as variant
Col = application.match("my header", rows(1), 0)
If iserror(col) then
'not found
Else
TK = cells(activecell.row, col)
End if
For this purpose I usually use a function which runs through the headers (in the first row of a sheet) and returns the number of the column which contains the value I have searched for.
Public Function FindColumn(HeaderName As String, Sht As String) As Long
Dim ColFound As Boolean
Dim StartingPoint As Range
ColFound = False
Set StartingPoint = Sheets(Sht).Range("A1")
Do While StartingPoint.Value <> ""
If UCase(Trim(StartingPoint.Value)) = UCase(Trim(HeaderName)) Then
FindColumn = StartingPoint.Column
ColFound = True
Exit Do
Else
Set StartingPoint = StartingPoint.Offset(0, 1)
End If
Loop
If Not ColFound Then FindColumn = 0
End Function
Example:
If the first row of your sheet named "Timeline" contains headers like e.g. "Date" (A1), "Time" (B1), "Value" (C1) then calling FindColumn("Time", "Timeline") returns 2, since "Time" is the second column in sheet "Timeline"
Hope this may help you a little.
Your thought is a good one. Reading in column headers to calculate addresses is one way to avoid hard coding - e.g.
Sub Test()
Dim R As Range
Set R = ActiveSheet.[A1]
Debug.Print ColNo(R, "Col1Hdr")
End Sub
Function ColNo(HdrRange As Range, ColName As String) As Integer
' 1st column with empty header is returned if string not found
ColNo = 1
Do While HdrRange(1, ColNo) <> ""
If HdrRange(1, ColNo) = ColName Then Exit Do
ColNo = ColNo + 1
Loop
End Function
Another way I frequently use - and I must admit I prefer it over the above, is to define Enum's for all my tables in a seperate "definition" module, e.g.
Public Enum T_VPN ' sheet VPN
NofHRows = 3 ' number of header rows
NofCols = 35 ' number of columns
MaxData = 203 ' last row validated
GroupNo = 1
CtyCode = 2
Country = 3
MRegion = 4
PRegion = 5
City = 6
SiteType = 7
' ....
End Enum
and use it like
Sub Test1()
Debug.Print ActiveSheet(T_VPN.NofHRows, T_VPN.Country)
End Sub
As you can see, the usage is simpler. Allthough this is again "some kind" of hardcoding, having all definition in one place reduces maintenance significantly.

How do I return a value in column C only if Column A and B matches my criteria?

SO this started as me trying to help someone else, got stumped. So basically i have values in columns B, C, and D. if have my criteria in H2 and I2 and when my criteria in H2 and I2 matches in B and C then have the corresponding answer in D to populate J2. basically a vlookup with 2 criteria.
i have something like this.
Sub test()
Dim rngCrit1 As Range
Dim rngCrit2 As Range
Dim rngAnswer As Range
Dim strTarget As String
Set rngCrit1 = Range("H2")
Set rngCrit2 = Range("I2")
Set rngAnswer = Range("J2")
Range("B2").Select
strTarget = ActiveCell.Value
Do While strTarget <> ""
With ActiveCell
If strTarget = rngCrit1 Then
If .Offset(0, 1).Value = rngCrit2 Then
rngAnswer.Value = .Offset(0, 2)
Else
.Offset(1, 0).Select
strTarget = ActiveCell.Value
End If
End If
End With
Loop
End Sub
Now this thing just crashes, no debugging or anything. I am self taught so i'm sure i screwed the pooch here somewhere.
*Note this is just to satisfy my own interest not really important, so if it takes you more than 5 min please help someone else that needs it more than I.
Val1 Val2 Val3 Crit1 Crit2 Answer
a r 12 g v 22
b r 14
c s 15
d s 16
e t 18
f t 19
g y 20
g v 22
sample data
It's great that you're trying to improve your VBA skills. The first thing I'd suggest, which will improve any macro you write, is to avoid using .Select. Work directly with the range objects. For instance:
Range("B2").Select
strTarget = ActiveCell.Value
becomes
strTarget = Range("B2").Value
Also, in general, use vbNullString or Len(variable)=0 when checking for "empty" values instead of "". As for why your program is crashing, it may be your use of With. Like Select, it should be avoided in most cases (definitely in this one). Although you update ActiveCell, it's within the scope of the With statement, so once you close it (End With), those changes to ActiveCell are undone (I would suggest stepping through the macro and watch the values of strTarget and ActiveCell). This may not be the case, but I know it holds for other variables, which is why I avoid With (and avoid reassigning values in a With statement)
Anyway, I'd add the following code and rewrite the loop as follows:
Dim r as range
set r = Range("B2") 'keep in mind this range is on the ActiveSheet, so you're better
'off explicitly naming the Sheet e.g. Sheet1.Range("B2")
strTarget1 = Range("B2").Value
strTarget2 = Range("C2").Value
Do While Len(strTarget) <> 0
If strTarget1 = rngCrit1 Then
If strTarget2 = rngCrit2 Then
rngAnswer.Value = r.Offset(0,2)
Exit Do
End If
End If
set r = r.Offset(1,0)
strTarget1 = r.Value
strTarget2 = r.Offset(0,1).Value
Loop
Keep in mind you could also loop with a Long counter i for the row, then call Sheet1.Cells(i,1).Value, Sheet1.Cells(i,2).Value and so on for the values of the different columns of that row (instead of using a range object and .Offset
EDIT: After running your code, the reason for the crash is due to your If statements. You want to go to the next cell regardless. Remove the Else and put the End If statements before the Select. Add an Exit Do after your assignment statement in the 2nd If, since you want to stop looping if your two columns meet the criteria. I've updated my code to show this, as well.
INDEX and MATCH, or SUMPRODUCT tend to work well for this. An example of the former:
http://support.microsoft.com/kb/59482
if you can guarantee val1 and val2 will be unique (e.g. when searching for g & v, there is only 1 line with g and v) then you can use sumifs
I put val1,val2 and val3 in columns A,B, & C, and the search into E,F and the answer in G, and came up with this formula
=SUMIFS(C2:C9,A2:A9,E2,B2:B9,F2)
of course, this fails if val3 is not numeric, or there are more than 1 line with the letters you are looking for

Resources