Ignore, or avoid circular reference in UDF? - vba

I am trying to make a planner. I have a table which I want to be something like this:
ID
Parent ID
Start Date
Duration
End Date
1
01/01/2021
10
11/01/2021
2
1
06/05/2021
2
08/05/2021
3
2
08/05/2021
1
09/05/2021
4
3
09/05/2021
5
14/05/2021
5
2,3,4
14/05/2021
4
18/05/2021
The Start date will be based on matching the Parent ID to the Task ID, so task 2 follows task 1, task 2 follows task 3 etc. I also want to be able to test against multiple tasks that might be happening at the same time, so task 5 can start after tasks 2, 3 or 4, which ever ends last.
I wrote this UDF for the start date calculation.
Option Explicit
Function LastPredecessor(PreList As String, TaskList As Range, TaskDueDate As Range)
Dim ID
Dim Dates
Dim nPres, i As Integer
On Error Resume Next
Pres = Split(PreList, ",")
nPres = UBound(Pres)
ReDim Dates(0 To nPres, 0)
For i = 0 To nPres
Dates(i, 0) = IDX_Match(CInt(Pres(i)), TaskList, TaskDueDate)
Next
LastPredecessor = WorksheetFunction.Max(Dates)
End Function
Function IDX_Match(LookupVal, MatchRange As Range, LookupRange As Range, Optional MatchType As Integer = 0)
IDX_Match = WorksheetFunction.Index(LookupRange.Value2, WorksheetFunction.Match(LookupVal, MatchRange.Value2, MatchType))
End Function
The function is called in the start date in the table like this
=LastPredecessor([#Parent ID],[ID],[End Date])
It works well if the end date is independent of the start date, but as soon as I try to update the end date, and add start date + duration as the calculation for end date, it generates a circular error.
I am sure I am doing something very simple wrong, but would be great to know what I need to do to fix this.
EDIT:
Thanks to #Toddleson, the following is a slightly modified version
Function LastPredecessor(PreList As String, EndDates As Range, IDColumn As Range) As Date
Dim Preds() As String, PredDates() As Long
Preds = Split(PreList, ",")
ReDim PredDates(UBound(Preds))
For i = LBound(Preds) To UBound(Preds)
PredDates(i) = IDColumn(WorksheetFunction.Match(CInt(Preds(i)), IDColumn, 0)).Offset(0, EndDates.Column - IDColumn.Column).Value2
Next i
LastPredecessor = Application.WorksheetFunction.Max(PredDates)
End Function

Ok, I rewrote the function. It should be doing exactly what you described to me.
Function LastPredecessorV2(PreList As String, EndDates As Range, IDColumn As Range) As Date
Dim Preds() As String, PredDates() As Long
Preds = Split(Replace(PreList, " ", ""), ",")
ReDim PredDates(UBound(Preds))
For i = LBound(Preds) To UBound(Preds)
PredDates(i) = IDColumn.Find( _
What:=CInt(Preds(i)), _
LookAt:=xlWhole _
).Offset(0, EndDates.Column - IDColumn.Column).Value2 'Corrected
Next i
LastPredecessorV2 = CDate(Application.WorksheetFunction.Max(PredDates))
End Function
Here's how the input of the arguments looks. Those ranges can be the entire column and it still works.
All of the End Dates are using the formula (Start Date + Duration in Days). The first task start date is the only independent value in the column, all the other ones are using the formula.
Edit:
I should mention that you can insert columns in-between the ranges and the function will still work.

Related

String Value is not passing correctly

I have a word table. I wrote a macro to get values from the table. When it runs I get a runtime error 13. When I debug and watch the value of parsing string it looks like this "2019-04-03 There is only one quote in the string. I think that is the case I couldn't convert that string into a date format. Can you help me to fix this?
The code
Sub Macro2()
Dim NumRows As Integer
Dim startDate As String
Dim days As String
Dim endDate As String
If Not Selection.Information(wdWithInTable) Then
Exit Sub
End If
NumRows = Selection.Tables(1).Rows.Count
'Loop to select each row in the current table
For J = 2 To NumRows
'Loop to select each cell in the current row
startDate = Selection.Tables(1).Rows(J).Cells(5).Range.Text
days = Selection.Tables(1).Rows(J).Cells(6).Range.Text
FormatDate = CDate(ends)
endDate = DateAdd("d", days, FormatDate)
Selection.Tables(1).Rows(J).Cells(7).Range.Text = endDate
Next J
End Sub
The table
Here's the minimal change I found that works for me when tested in Word 2013.
General points:
I added Option Explicit so that the computer would help me find errors. In this case, the variables J and FormatDate were used but not Dimed, and ends was used but never initialized (I changed it to startDate).
The Range.Text in a table cell includes whitespace and the end-of-table marker (¤). That is why CDate was giving an error.
For the dates, I used Left() to take only the left ten characters, since you seem to always be using yyyy-mm-dd-format dates.
For the counts of days, since those can be of any length, I used Range.Words(1).Text to keep only the first Word (as MS Word defines it), which is the number.
I also added the CLng() call in the parameter to DateAdd, since DateAdd wants a number* rather than a string.
For production use, I would also recommend using Selection only in one place, and doing Dim workTable as Table: Set workTable = Selection.Tables(1). That will simplify your code.
Code
<=== marks changed lines
Option Explicit ' <==
Sub Macro2()
Dim NumRows As Integer
Dim startDate As String
Dim days As String
Dim endDate As String
If Not Selection.Information(wdWithInTable) Then
Exit Sub
End If
NumRows = Selection.Tables(1).Rows.Count
'Loop to select each row in the current table
Dim J As Long ' <==
For J = 2 To NumRows
'Loop to select each cell in the current row
startDate = Selection.Tables(1).Rows(J).Cells(5).Range.Text
startDate = Left(startDate, 10) ' <== Remove the space and table mark
days = Selection.Tables(1).Rows(J).Cells(6).Range.Words(1).Text ' <===
Dim FormatDate As Date ' <==
FormatDate = CDate(startDate) ' <== not `ends`
endDate = DateAdd("d", CLng(days), FormatDate) ' <=== clng
Selection.Tables(1).Rows(J).Cells(7).Range.Text = endDate
Next J
End Sub
* DateAdd actually takes a Double, but VBA can promote Long to Double. I chose CLng since it looks like you are only using integer day spans. If not, use CDbl instead.
Try:
Sub Demo()
Dim r As Long
With Selection
If Not .Information(wdWithInTable) Then Exit Sub
With .Tables(1)
For r = 2 To .Rows.Count
.Cell(r, 7).Range.Text = _
Format(DateAdd("d", Split(.Cell(r, 6).Range.Text, vbCr)(0), CDate(Split(.Cell(r, 5).Range.Text, vbCr)(0))), "YYYY-MM-DD")
Next r
End With
End With
End Sub

Excel VBA with matches and index

I've written some VBA code with two matches and index formula. I need to pick the unique value from a sheet and compare it with the other sheet. However it is not working. I get some errors. (unable to get the match property of the worksheetfunction class vba - this is the error)
Here is my code :
Sub Post_Attendance()
Sheets("DB").Activate
'On Error Resume Next
Dim myvalue As String
Dim mydate As String
Dim mypost As String
(the date value entered in a cell)
Dim Dt As String
Dt = Range("C7").Value
(the unique id entered in a cell)
Dim empid As String
empid = Range("C8").Value
(activating another worksheet , from a cell value)
Dim strWsName As String
strWsName = Sheets("DB").Range("A7")
Sheets(Left(strWsName, 3)).Select
(match function to find the row and columns number for indexing)
mydate = WorksheetFunction.Match(Dt, Range("B1:Q1"), 0)
myvalue = WorksheetFunction.Match(empid, Range("A5:A500"), 0)
mypost = WorksheetFunction.Index(Range("B2:Q6"), myvalue, mydate)
End Sub
First off, WorksheetFunction.Match never returns a string; it either returns a number (a long integer) or an error. It is not the value from the match, it is the row or column number where the match was found.
Next, you cannot catch an #N/A error from no match with WorksheetFunction.Match but you can catch it with Application.Match into a variant.
Real dates are numbers, not strings. The raw underlying value is another long integer; e.g. a positive whole number with no decimal portion. If you had time or a datetime then you would have a decimal portion.
Resolve and reference your parent worksheet properly; do not rely upon Select or Activate.
The number returned from MATCH is the position within the range of cells searched. You are looking for a row number from row 5 to row 500 then using that to find a row within row 2 to 6; any match above row 9 (match returning 6 or above) in the original is going to be out-of-range.
If the empid values are numbers then deal with numbers; you cannot find a match to a true number from text-that-looks-like-a-number; e.g. 99 <> "99". I'm going to assume that empid should be alphanumeric and not a true number but given the errors with the previous variable assignments, it is up to you to determine the correct assignment.
Here is my best guess at an error controlled sub procedure (given that you have shown no sample data).
Option Explicit
Sub Post_Attendance()
'On Error Resume Next
Dim myvalueRow As Variant, mydateCol As Variant, dt As Long, empid As String, mypost As Variant
dt = Worksheets("DB").Range("C7").Value2
empid = Worksheets("DB").Range("C8").Value2
With Worksheets(Left(Worksheets("DB").Range("A7").Value2, 3))
'locate the column for the date
mydateCol = Application.Match(dt, .Range("B1:Q1"), 0)
If IsError(mydateCol) Then _
mydateCol = Application.Match(CStr(Worksheets("DB").Range("C7").Value2), .Range("B1:Q1"), 0)
If IsError(mydateCol) Then
Debug.Print "dt not found in row 1"
Exit Sub
End If
'locate the row for the value
myvalueRow = Application.Match(empid, .Columns("A"), 0)
If IsError(myvalueRow) Then
Debug.Print "empid not found in column A"
Exit Sub
End If
mypost = Application.Index(.Range("B:Q"), myvalueRow, mydateCol)
End With
End Sub

Creating a lot number using Julian/Ordinal date from a yyyy-mm-dd and concatenate in VBA

I need to create a lot number which consists of:
Digits: 1,2,3 ----> Three digit reagent code ----> For example:141 (this is a constant)
Digit: 4 ----> Identifier ----> For example: 2 (this is a constant)
Digits: 5,6,7 ----> Julian/Ordinal Calendar day ----> 001-365 (366 for leap year)
Digit: 8 ----> The last digit of the expiry year ----> 0-9
Therefore: 14120039 (Expiry date would be 2019-01-03)
The expiry date can be found on a sheet called "CP_sequencer" in cell "S7". This will be in the format yyyy-mm-dd.
The following is the code I’m using so far but I know something is wrong and it may not be the most efficient way of doing things. There are a few cell references that are correct but I know it may be hard to follow without the actual spreadsheet.
Dim Julian_Day As String
Dim Split_Date As String
Dim valueYear, valueLastDigit As Integer
Range("F31").Select
Julian_Day = _
ActiveCell.FormulaR1C1 = _
"=VALUE(RIGHT(YEAR('CP sequencer'!R[-24]C[13]),2)&TEXT('CP sequencer'!R[-24]C[13]-DATE(YEAR('CP sequencer'!R[-24]C[13]),1,0),""000""))"
Split_Date = _
Range("F31") = Year(CP_Sequencer.Range("S7"))
Range("F31").Select
Select Case Len(value1) 'gives a number depending on the length of the value1
Case 4 ' e.g., 2017 = 201, 7
valueYear = Left(value1, 3) ' 201
valueLastDigit = Right(value1, 7) ' 7
End Select
ActiveCell.Value = "1412" & Julian_Day & valueLastDigit
I know something isn't right because at the moment when I run this code the output is 1412False0
Any help would be much appreciated
I assume you want a VBA solution to write back your lot number code to a given cell. Your code includes many errors (references without values, undeclared variables, double assignments and so on). Maybe the code with explainations below will be of some help. I use a type Definition to structure your results and make the code more readable.
Code
Option Explicit ' obliges you to declare your variables
Type TData ' declaration head of your module
ReagentCode As String ' 3 dig .. 141
ID As String ' 1 dig .. 2
JulDays As String ' 3 dig .. 1-365/366
YearDigit As String ' 1 dig .. 7 (2017 -> 7)
End Type
Sub CreateLotNo()
' Declare variables
Dim MyData As TData
Dim yr As Integer ' expiry year extracted from cell Sz
Dim ws As Worksheet
' set fully qualified reference to your worksheet
Set ws = ThisWorkbook.Worksheets("CP Sequencer")
' get expiry year from cell S7
yr = Year(ws.Range("S7").Value) ' expiry year
With MyData
' assign values to MyData
.ReagentCode = "141" ' constant
.ID = "2" ' constant
' julian days = expiry date minus last year's ultimo date
.JulDays = Format(ws.Range("S7").Value - CDate("12.31." & yr - 1), "000")
.YearDigit = Right(yr, 1) ' last digit of the expiry year
' write lot number back to cell XY
ws.Range("F31").Value = .ReagentCode & .ID & .JulDays & .YearDigit & ""
End With
End Sub
This should return the LotNumber you're after.
I'm quite not sure what's wrong with your code, but it will be in this line:
Julian_Day = _
ActiveCell.FormulaR1C1 = _
"=VALUE(RIGHT(YEAR('CP sequencer'!R[-24]C[13]),2)&TEXT('CP sequencer'!R[-24]C[13]-DATE(YEAR('CP sequencer'!R[-24]C[13]),1,0),""000""))"
This is asking the question is the formula in the activecell the same as the text string "=VALUE(RIGHT...." and place the result in the Julian_Day variable. Pretty much guaranteed that the value won't be the same so FALSE is returned.
If you wanted to get the result of the formula using that method you'd need to place the formula in the cell first and then read the result... but I'd advise against using that method. Easier to reference the values within VBA.
The LotNumber function below should return the value you're after. You can use it as I have in the Test procedure or as a worksheet function entered directly in a cell: =LotNumber(A1,B1,C1)
Sub Test()
'Passing values to the LotNumber function.
Debug.Print LotNumber(141, 2, DateValue("3 January 2019"))
'Getting values from Sheet1.
With ThisWorkbook.Worksheets("Sheet1")
Debug.Print LotNumber(.Range("A1"), .Range("B1"), .Range("C1"))
End With
End Sub
Public Function LotNumber(Reagent As Long, Identifier As Long, Expiry As Date) As String
Dim Ordinal As Long
Ordinal = Expiry - DateSerial(Year(Expiry), 1, 1) + 1
LotNumber = Format$(Reagent, "000") & Identifier & Format$(Ordinal, "000") & Right(Format(Expiry, "yyyy"), 1)
End Function
Edit:
As an afterthought you could define the LotNumber function as:
Public Function LotNumber(Expiry As Date, Optional Reagent As Long = 141, Optional Identifier As Long = 2) As String
Using this method you must pass the date to the function, but the Reagent and Identifier will default to 141 and 2 if no alternative values are supplied.
If entered today (30th November 17) then Debug.Print LotNumber(Date) will return 14123347. As a worksheet function with 3rd Jan 2019 in cell C1: =LotNumber(C1) will return 14120039

Excel VBA Set Variable to Equal Values between Dates

In Excel using VBA, I need to set a variable to equal a list of all the dates between a start and end date (similar to equaling a range containing multiple values). The catch is only the start and end date are in a range, non of the values in between.
In SQL Server I've used the Sys.Columns table to generate a list of dates between two dates that are not actually stored on that table. Is there a way to do something similar here without having each date between the start and end date written somewhere? I googled for a couple hours and didn't find anything on how to do this.
What I'm attempting to do is have a variable I can do a For Each loop on. So for each date I will check if it exists in another worksheet, if it does nothing will happen, if it does not it will be added.
I've tried:
Dim DatesInSettings As Date
DatesInSettings = StartDate To EndDate
For Each Date In DatesInSettings
'Insert commands here
Next DatesInSetting
But that clearly isn't the answer. Help?
This searches Sheet2 for dates between the start date and end dates on Sheet1 - in cells A1 and B1:
Sub RunDates()
Dim StartDate As Date
Dim EndDate As Date
Dim i As Date
StartDate = Sheet1.Range("A1")
EndDate = Sheet1.Range("B1")
For i = StartDate To EndDate
If WorksheetFunction.CountIf(Sheet2.Range("A1:A5"), i) > 0 Then
Debug.Print i; "- date found"
Else
Debug.Print i; "- date not found"
End If
Next i
End Sub
The following subroutine calls a dictionary that will store all the dates between two given endpoints. Then it uses a simple existence comparison to check if the dates on your list is inside the dictionary's items. If it's not, it's going to print them out as not in the list.
Modify accordingly to suit your needs. ;)
CODE:
Sub GetListOfDates()
Dim StartDate As Date, EndDate As Date
Dim DictOfDates As Object, DateToCheck As Variant, ListOfDates As Variant
Dim Iter As Long
Set DictOfDates = CreateObject("Scripting.Dictionary")
StartDate = "12/31/2013"
EndDate = "01/15/2014"
For Iter = StartDate + 1 To EndDate - 1
With DictOfDates
If Not .Exists(Iter) Then
.Add Iter, Empty
End If
End With
Next Iter
'--Print them somewhere.
'Range("A1").Resize(DictOfDates.Count, 1).Value = Application.Transpose(DictOfDates.Keys)
ListOfDates = Range("B1:B15").Value
For Each DateToCheck In ListOfDates
If Not DictOfDates.Exists(DateToCheck) Then
Debug.Print Str(DateToCheck) + " is not in the list!" '--Or whatever action you want.
End If
Next DateToCheck
Set DictOfDates = Nothing
End Sub
Let us know if this helps. :)
I solved it with a vector.
I hope it helps
Sub Dates_Vector()
Public Dates() As Date
ReDim Dates(End_Dat - Start_Date)
For x = 0 To End_Dat - Start_Date
Dates(x) = Dat_Ini + x
Next x
For Each Date In Dates
'Insert commands here
Next Date
End Sub

VBA Excel Function Error Runtimeerror 13

I cannot find the mistake. The error msg gives me "runtime error '13'" data types don't match. The error seems to be in the function "fillcalweeks".
Fillcalweeks should return an array which is filled with start and end dates which are located in a sheet.
Here's my code:
'For every calenderweek we need the start and end dates in an array to produce the timeline
Sub get_cal_weeks()
Dim weeks As Integer, i As Integer, col As String, weekstart As Date, weekend As Date
'start column is D
col = "D"
'get amount of weeks
weeks = countcalweeks()
'populate array calweeks
calweeks = fillcalweeks(weeks)
For i = 0 To weeks
Sheets("Kalenderwochen").Range("E" & i + 1) = calweeks(i, 1)
Next
End Sub
Function fillcalweeks(weeks As Integer) As String()
Dim i As Integer, datestart As Date, dateend As Date, calweek As Integer, returnarray() As String
For i = 0 To weeks
'date start & date end
datestart = Sheets("Kalenderwochen").Range("A" & i + 1).Value
dateend = Sheets("Kalenderwochen").Range("B" & i + 1).Value
calweek = Sheets("Kalenderwochen").Range("C" & i + 1).Value
returnarray(i, 1) = datestart
returnarray(i, 2) = dateend
returnarray(i, 3) = calweek
fillcalweeks = returnarray
Next
End Function
'Counts the calenderweeks in the Kalenderwochen sheet
Function countcalweeks() As Integer
countcalweeks = Sheets("Kalenderwochen").Range("A2").End(xlDown).row
End Function
Thx for the help
You are getting an error on the line calweeks = fillcalweeks(weeks) because you are assigning a String array (the result of the function fillcalweeks to a Variant.
You'll notice you declared every variable except calweeks. Since VBA doesn't have an explicit declaration for this variable it assigns it to a Variant.
To fix the problem, start by putting Option Explicit at the top of every module. When you compile the project, it'll alert you to errors such as these (Debug -> Compile VBA Project). Then all you need to do is declare calweeks as a String().
There is a second problem you have and that is that you are trying to store a Date data type in your String() array within the fillcalweeks. You either need to convert the datestart, dateend and calweek variables to Strings (you can use the VBA.CStr() function to do this) or change the function fillcalweeks to return a Date array.
Finally, you need to declare a size range for the returnarray() within fillcalweeks. VBA needs to know how big this is before it can fill the values. Since you know how many rows there are (its an input to the function) this is as simple as replacing the declaration of returnarray with ReDim returnarray(0 To weeks - 1, 1 to 3) As String. Note you want to dimension the array to weeks - 1 since you have a base of zero not one.
One more error is that when you output back to the worksheet the loop needs to be For i = 0 To weeks - 1 otherwise the array will be out of bounds...
Thx. I found all the mistakes in the code. 1. declaration of calweeks and 2. the array dimension:
'For every calenderweek we need the start and end dates in an array to produce the timeline
Sub get_cal_weeks()
Dim weeks As Integer, i As Integer, col As String, weekstart As Date, weekend As Date, calweeks() As Variant
'start column is D
col = "D"
'get amount of weeks
weeks = countcalweeks()
'populate array calweeks
calweeks = fillcalweeks(weeks)
For i = 0 To weeks
field = i + i + 4
weekstart = calweeks(i, 0)
weekend = calweeks(i, 1)
Cells(5, field) = monetary_calc_week(weekstart, weekend)
Next
End Sub
Function fillcalweeks(weeks As Integer) As Variant()
Dim i As Integer, datestart As Date, dateend As Date, calweek As Integer, arraysize As Integer, returnarray() As Variant
arraysize = 52
weeks = weeks - 2
ReDim Preserve returnarray(arraysize, 3)
For i = 0 To weeks
If i > arraysize Then
arraysize = arraysize * 2
ReDim Preserve returnarray(arraysize, 3)
End If
'date start & date end
datestart = Sheets("Kalenderwochen").Range("A" & i + 2).Value
dateend = Sheets("Kalenderwochen").Range("B" & i + 2).Value
calweek = Sheets("Kalenderwochen").Range("C" & i + 2).Value
returnarray(i, 0) = datestart
returnarray(i, 1) = dateend
returnarray(i, 2) = calweek
Next
fillcalweeks = returnarray
End Function
'Counts the calenderweeks in the Kalenderwochen sheet
Function countcalweeks() As Integer
countcalweeks = Sheets("Kalenderwochen").Range("A2").End(xlDown).row
End Function