excel error 424 object required when calling sub - excel

I spent hours looking into my code but I can't figure out what is wrong.
I keep getting this 424 error, for no obvious reason to me. here is my piece of code.
I just want to give row as a parameter to mySub to process it.
Option Explicit
Private Sub mySub(row As Range)
Debug.Print ("mySub") ' not printed
Dim line As Collection
Set line = New Collection
End Sub
Private Sub CalcClients()
Dim data_sheet As Worksheet
Dim last_row As Long
Dim last_col As String
Dim line As Long
Dim cols As Range
Dim row As Range
Set data_sheet = Worksheets("DATA")
Let last_row = data_sheet.Range("A1").End(xlDown).row
Let last_col = Col_Letter(data_sheet.Range("A1").End(xlToRight).column)
Set cols = data_sheet.Range("A2:" & last_col & last_row)
For Each row In cols.Rows
' type_name(row) => "Range"
Debug.Print (row.Cells(1, 1).Value) '=> THEEXPECTEDVALUE
mySub (row) ' Error 424, object required
Next
End Sub

Here is the reason for the observed behavior.
Your subroutine, mySub() takes one parameter as a range type.
This means you must pass it a range.
In your code you set an object variable row to a series of ranges, one at a time.
To use that range variable row as a parameter for mySub the syntax should be like this:
mySub row
Or...
Call mySub(row)
...but instead you are doing this:
mySub (row)
So, what's the difference? When you place parentheses around any variable that is standing alone (as in the above), that variable gets evaluated immediately and prior to whatever you plan to do with it.
The parentheses are a common way to override a procedure's ByRef argument and instead force a one-time ByVal parameter pass. This is because the parentheses force an evaluation of the variable and the resultant VALUE is passed instead of a reference to the variable.
In your case, you do not want to do this (in fact, in most cases you do not want to do this). When you sandwich row with parentheses, the range object is no longer passed to the routine. Instead it is evaluated and its values are passed as a Variant Array.
And since the mySub definition calls for a range object parameter, you get error 424. mySub is complaining, "Hey, this is not a range object and a range object is required!"

Add a call before it. You could also remove the () and it should work too ^_^ If you remove (), also remove the call.

Related

Why doesn't this simple VBA code work in Excel?

I'm quite new to programming with VBA (or any language, let's be honest). I'm trying to do a project for work, adding short sections at a time to see if the code still works, and trying out new code in a separate Sub. I've come across an error that I can't get around. The results don't change when they're the only line in a separate Sub.
The following code works:
ActiveWorkbook.Sheets("Template").Copy After:=ActiveWorkbook.Sheets("Student info")
Whereas the following code, when run, breaks with a 424 run-time error (object required). I've tried selecting instead of naming, still no luck. It does successfully copy the worksheet to the correct place, despite the error, but is called 'Template (2)'.
ActiveWorkbook.Sheets("Template").Copy(After:=ActiveWorkbook.Sheets("Student info")).name = "newname"
This is very confusing because the code below does work. Is it just that trying to name something after using 'add' does work, but after 'copy', it doesn't?
ActiveWorkbook.Sheets.Add(After:=ActiveWorkbook.Sheets("Student info")).name = Student_name
Thanks in advance for any help.
The reference (to the created copy) as return value (of a function) would be useful, but as Worksheet.Copy is a method of one worksheet (in opposite to Worksheets.Add what is a method of the worksheets-collection), they didn't created it. But as you know where you created it (before or after the worksheet you specified in arguments, if you did), you can get its reference by that position (before or after).
In a function returning the reference:
Public Enum WorkdheetInsertPosition
InsertAfter
InsertBefore
End Enum
Public Function CopyAndRenameWorksheet(ByRef sourceWs As Worksheet, ByRef targetPosWs As Worksheet, ByVal insertPos As WorkdheetInsertPosition, ByVal NewName As String) As Worksheet
'If isWsNameInUse(NewName) then 'Function isWsNameInUse needs to be created to check name!
'Debug.Print NewName & " alredy in use"
'Exit Function
'End If
With sourceWs
Dim n As Long
Select Case insertPos
Case InsertAfter
.Copy After:=targetPosWs
n = 1
Case InsertBefore
.Copy Before:=targetPosWs
n = -1
Case Else
'should not happen unless enum is extended
End Select
End With
Dim NewWorksheet As Worksheet
Set NewWorksheet = targetPosWs.Parent.Worksheets(targetPosWs.Index + n) 'Worksheet.Parent returns the Workbook reference to targetPosWs
NewWorksheet.Name = NewName ' if name already in use an error occurs, should be tested before
Set CopyWorksheetAndRename = NewWorksheet
End Function
usage (insert after):
Private Sub testCopyWorkSheet()
Debug.Print CopyAndRenameWorksheet(ActiveWorkbook.Sheets("Template"), ActiveWorkbook.Sheets("Student info"), InsertAfter, Student_name).Name
End Sub
to insert the copy before the target worksheet, change third argument to InsertBefore (enumeration of options).
New Worksheet.Name needs to be unique or you'll get an error (as long you not implemented the isWsNameInUse function to check that).
Also note that there is a difference between .Sheets and .Worksheets
You can get the links to the documentation by moving the cursor (with mouse left-click) in the code over the object/method you want more infos on and then press F1

Function vs Sub(ByRef)

About
This question is not about when to use a Function or a Sub, or the difference between ByRef and ByVal (although some insights will be unavoidable).
It is about scenarios which are 'commonly' solved with a Function, but can optionally be solved with a Sub using ByRef in the sense of 'modifying'.
The Code
Consider the following function:
' Returns the worksheet (object) with a specified name in a specified workbook (object).
Function getWsF(wb As Workbook, _
ByVal wsName As String) _
As Worksheet
' 'getWsF' is 'Nothing' by default.
' Try to define worksheet.
On Error Resume Next
Set getWsF = wb.Worksheets(wsName)
End Function
You can utilize it like the following:
' Writes the name of a specified worksheet, if it exists, to the `Immediate` window...
Sub testFunction()
Const wsName As String = "Sheet1"
Dim wb As Workbook
Set wb = ThisWorkbook ' The workbook containing this code.
' Define worksheet.
Dim ws As Worksheet
Set ws = getWsF(wb, wsName)
' Test if worksheet exists.
If Not ws Is Nothing Then
Debug.Print "The worksheet name is '" & ws.Name & "'."
Else
Debug.Print "Worksheet '" & wsName & "' doesn't exist in workbook '" _
& wb.Name & "'."
End If
End Sub
But you can also write each of the procedures in the following way:
' Although 'ByRef' is not necessary, I'm using it to indicate that whatever
' its variable is referring to in another procedure (in this case
' a worksheet object), is going to be modified (possibly written to
' for other datatypes).
Sub getWsS(ByRef Sheet As Worksheet, _
wb As Workbook, _
ByVal wsName As String)
' 'Sheet' could be 'Nothing' or an existing worksheet. You could omit
' the following line if you plan to use the procedure immediately
' after declaring the worksheet object, but I would consider it
' as too risky. Therefore:
' 'Reinitialize' worksheet variable.
Set Sheet = Nothing
' Try to define worksheet.
On Error Resume Next
Set Sheet = wb.Worksheets(wsName)
End Sub
' Writes the name of a specified worksheet, if it exists, to the `Immediate` window...
Sub testSub()
Const wsName As String = "Sheet1"
Dim wb As Workbook
Set wb = ThisWorkbook ' The workbook containing this code.
' Define worksheet.
Dim ws As Worksheet
getWsS ws, wb, wsName
' Test if worksheet exists.
If Not ws Is Nothing Then
Debug.Print "The worksheet name is '" & ws.Name & "'."
Else
Debug.Print "Worksheet '" & wsName & "' doesn't exist in workbook '" _
& wb.Name & "'."
End If
End Sub
Side by Side
Procedure
Function getWsF(wb As Workbook, _ Sub getWsS(ByRef Sheet As Worksheet, _
wsName As String) _ wb As Workbook, _
As Worksheet wsName As String)
Set Sheet = Nothing
On Error Resume Next On Error Resume Next
Set getWsF = wb.Worksheets(wsName) Set Sheet = wb.Worksheets(wsName)
End Function End Sub
Usage (relevant)
' Define worksheet. ' Define worksheet.
Dim ws As Worksheet Dim ws As Worksheet
Set ws = getWsF(wb, wsName) getWsS ws, wb, wsName
The Question(s)
Is the second solution viable?
I'm looking for a proper description of what each of the relevant two procedures do and some insights
in terms of common practice, readability, efficiency, pitfalls ...
In your case, I would use the Function approach and here are a few reasons:
I can use the result of the Function without storing the returning variable:
With getWsF(ThisWorkbook, "Sheet1")
'...
End With
Obviously, I would need to be sure it never returns Nothing or have some error handling in place.
or
DoSomething getWsF(ThisWorkbook, "Sheet1")
where DoSomething is a method expecting a Worksheet/Nothing
As #TimWilliams mentioned in the comments, if you don't expect multiple return values then this is the "expected" way to do it. A well-established convention is that if the method has no return value it should be a Sub. If it has a single return value then it should be a Function. If it returns multiple values then it should also be a Function and:
you either use a Class or Type to pack them as one result
or, the Function returns a primary value as the result and the rest of the return values ByRef (see #UnhandledException's answer for an example).
If you ever need to call the method with Application.Run then Function is safe. Using a Sub often leads to Automation Errors or code simply stops running after the method is executed. It doesn't matter if you need to use the result of the Function or not, don't call a Sub with Application.Run if you don't want nasty errors. Of course, to avoid issues with Application.Run, you could have a Function that doesn't get assigned a return value and still return the Worksheet ByRef but this would be too confusing for the reader.
Edit #1
Forgot to mention that the Application.Run automation errors are happening when calling methods from a different document (for Excel - different Workbook)
Edit #2
In this section I will try to address the proper description side of your question, without doing a begginer and an advanced explanation but a combined one.
Difference between a Sub and a Function
A Sub is just a function that does not return a value after the function executes. In lots of languages, such a function is called a Void Function.
The implications is that a Sub is just a stand-alone statement. It cannot be called from inside an expression. You can only call it with one of:
MySub [argsList]
Call MySub([argsList])
On the other hand, a Function can be used inside statements like:
arguments to other methods e.g. DoSomething MyFunction(...); Debug.Print MyFunction(...)
assignment e.g. x = MyFunction(...)
With blocks e.g. With MyFunction(...)
method chaining e.g. MyFunction(...).DoSomething
The convention mentioned above:
A well-established convention is that if the method has no return value it should be a Sub. If it has a single return value then it should be a Function
becomes quite clear when we understand that a Sub does something and a Function returns a single value, by definition.
Similarity between a Sub and a Function
Both value-returning functions (Function in VBA) and void functions (Sub in VBA) are receiving values as parameters. In VBA, it is possible to return results via ByRef parameters. Not all languages support ByRef parameters (e.g. Java - except modifying members of Objects for example).
Note that porting code from a platform that supports ByRef to another one that does not, can be quite time-consuming if the ByRef approach is abused in the source platform.
Difference between ByVal and ByRef parameters
Passing by value (ByVal):
a new memory space is allocated for a new variable which will be of local scope to the method being called
the contents of the original variable are copied in the newly allocated space of the new variable (for Objects the address of the interface Virtual Table is copied instead)
contents of the original variable are NOT changed regardless of what the method does
it is much safer because the programer does not need to keep in mind/care about other parts of the program (specifically the calling method)
Passing by reference (ByRef):
a new variable is created but no new memory space is allocated. Instead the new variable points to the memory space occupied by the original variable being passed from the calling method. Note that for Objects, the original variable is passed entirely (no new variable is created) unless the interface is different (passing a Collection as an Object type parameter - Object stands for IDispatch) but this is a discussion outside of the scope of this answer. Also note that if the parameter is declared as Variant, there are more complex operations happening to facilitate the redirection
the contents of the original variable can now be changed remotely because both the original variable and the newly created one point to the same memory space
it is considered more efficient because no new memory is allocated but this comes with the downside of increasing complexity
Comparison of the presented methods
Now that we have some understanding of the differences, we can look at both the presented methods. Let's start with the Sub:
Sub getWsS(ByRef Sheet As Worksheet, wb As Workbook, ByVal wsName As String)
Set Sheet = Nothing
On Error Resume Next
Set Sheet = wb.Worksheets(wsName)
End Sub
First of all, there should be an On Error GoTo 0 statement before the End Sub because otherwise the Error 9 is propagated up the calling chain (if sheet not found) and can affect logic inside other methods, long after the getWsS method has returned.
The method name starts with the verb "get" which implies that this method returns something but the method declaration is a Sub which is, by definition, more like a Do Something than a Return Something. Readability is certainly affected.
There is a need for an extra ByRef parameter to return the single piece of result. Implications:
it affects readability
it requires a declared variable inside the calling method
the result cannot he chained/used in other expressions within the calling method
it requries the extra line Set Sheet = Nothing to make sure the original variable does not retain previous contents
Now, let's look at the Function approach:
Function getWsF(wb As Workbook, ByVal wsName As String) As Worksheet
On Error Resume Next
Set getWsF = wb.Worksheets(wsName)
End Function
Same as before, there should be an On Error GoTo 0 statement before the End Function because otherwise the Error 9 is propagated up the calling chain. Also, the Workbook can be passed ByVal as best practice.
Obvious differences:
the name getSomething is perfect for a function that returns Something. Readability is far better than the Sub couterpart
the reader/maintainer of the code instantly knows that the function returns a Worksheet just by looking at the return type (as opposed to looking through a list of ByRef parameters and figuring out which one is the return variable)
the result can be chained/used in expressions
no extra lines of code are needed, the default returning value is already Nothing
the most widely accepted convention is used
I've used CTimer and it seems like on my x64 machine, the Sub approach runs faster with about 20ms when running the methods for a million times. Are these minor efficiency gains worth the loss in readability and flexibility of use? That is something that only the maintainer of the code base can decide.
To answer your question directly:
Q: Is it viable?
A: Yes, it will compile and carry out the functionality that you're expecting.
The grey area comes about when you ask should you do this.
There's definitely nothing stopping you (assuming you aren't subject to some company coding standards or anything). Typically however, functions are used to take in parameters, perform some kind of logic and return a value or object at the end of that logic.
Functions are typically non-destructive and don't change the values or properties of the input parameters. This becomes especially important for code readability and maintenance because other developers (and even yourself a few months from now) will read the code expecting functions to behave in a certain way.
Sub routines on the other hand are not expected to return anything, and so they are used to run concise, related sections of code that carry out some kind of logic relevant to the application. Going back to the point of readability and maintenance, it's reasonable to assume that objects and properties will change inside of a sub routine and so again this makes the developer's life a little easier.
Ultimately there's no hard and fast rules - but there are years of experience and best practice which are often good advice to take on in these scenarios :)
A good example for using both, a function and a ByRef parameter is a 'Try' function:
Public Function TryGetMyValue(ByRef outMyValue As Long) As Boolean
On Error Goto Catch
outMyValue = GetMyValue() 'Does anything to retrieve the value and could raise an error...
TryGetMyValue = True
Done:
Exit Function
Catch:
Resume Done
End Function
It could be used like that:
Public Sub TestTryGetMyValue()
Dim myValue As Long
If Not TryGetMyValue(myValue) Then Exit Sub
Debug.? "MyValue is: " & myValue
End Sub

Open MsgBox if a date on birthday list matches today

Every time I try and execute my code it says "object required."
I want a MsgBox to open if one a date on my birthday list matches today.
The birthday list extends from b2 to b100 and I want to look through to find the birthday.
This is a piece of code I took from a YouTube video.
The MsgBox pops up every time I open the workbook.
Private Sub Workbook_Open()
Dim cl As Range
Set cl = ThisWorbook.Sheets("Birthdays").Range("B2:B100")
If IsDate(cl) Then
If Now >= cl Then
MsgBox "Somebody's had a birthday!"
End If
End If
End Sub
You are getting Object Required error because of a typo ThisWorbook should be ThisWorkbook
It is very normal to encounter such errors. So always use Option Explicit. I have covered it in To ‘Err’ is Human
So Can I have it added to my code automatically?
Yes you can. To have it added to all new files you create, simply select "Tools" -> "Options" in the VBE, and tick the "Require Variable Declaration" box.
Note: This will effect only new files that you create. You will need to add it yourself to existing files.
I basically just want my excel to create a msgbox when I open it, if one of the date on my birthday list matches today.
You can use Application.WorksheetFunction.CountIf to check if there is today's date in a range.
Sub Sample()
Dim ws As Worksheet
Dim rng As Range
Dim matchFound As Long
Set ws = ThisWorkbook.Sheets("Birthdays")
Set rng = ws.Range("B2:B100")
matchFound = Application.WorksheetFunction.CountIf(rng, Date)
If matchFound > 0 Then
MsgBox "Birthday Found"
Else
MsgBox "Birthday Not Found"
End If
End Sub
Screenshot
cl is a Range object representing 99 dalmatians cells, each encapsulating a Variant value.
The IsDate function is happy to take a Variant, but doesn't know what to do with 99 of them.
Because Range has a hidden default property, you can use it as if it were a value - but, especially for someone that's just beginning to learn VBA, it makes for confusing, implicit, "magic" code that says one thing, and does another.
If IsDate(cl.Value) Then
The implicit Range.Value member call here, yields a Variant representing the cell's value itself if the range represents only a single cell, otherwise (i.e. if the range is for more than one cell) it yields a Variant pointing to a 2D Variant array (in this case 99x1) that's holding every single value.
IsDate wants one value, so if we have 99 of them, we need a loop. But here's the thing: the last thing we want to do is iterate individual cells, get their Value, and verify that - because that would be very slow.
So instead, we grab that 2D Variant array, and iterate that.
If cl.Count = 1 Then
'single-cell range: not a 2D array
If Now >= cl.Value Then
MsgBox "Somebody's had a birthday on " & Format(cl.Value, "yyyy-mm-dd")
End If
Exit Sub
End If
Dim values As Variant
values = cl.Value
Dim currentRow As Long
For currentRow = LBound(values, 1) To UBound(values, 1)
Dim currentCol As Long
For currentCol = LBound(values, 2) To UBound(values, 2)
Dim currentValue As Variant
currentValue = values(currentRow, currentCol)
If IsDate(currentValue) Then
If Now >= currentValue Then
MsgBox "Somebody's had a birthday on " & Format(currentValue, "yyyy-mm-dd")
Exit Sub
End If
End If
Next
Next
Right now the msgbox just pops up every time I open the excel whether a birthday matches or not.
Sounds like your actual code has On Error Resume Next somewhere - that makes VBA ignore any run-time errors and merrily keep running, ...and you definitely don't want that. Rule of thumb, never use On Error Resume Next to side-step an error. Execution is normally halted when there's an "object required" error: an unconditional MsgBox popping means execution is allowed to continue in an error state, and that can't be a good thing.
As Sid found out, the type mismatch is caused by a typo -- this shouldn't be allowed to happen: make sure every module you ever type any code in says Option Explicit at the top, and it'll never happen again... for your early-bound code (late-bound code is still vulnerable to typos, but that's another story for another time).
Lastly, note that several of the above mentioned issues would have been reported by Rubberduck's code inspections (disclaimer: I manage this open-source project) - including the typo, the implicit default member calls, and the absence of Option Explicit.

Error 91 in a function but not in a similar sub

I want to convert a string into a range variable that I will use somewhere else. My first idea was to create a text-to-range function and to avoid dealing with the sintaxis ever again. I know it is probably a very basic question but I couldn't figure it.
My first attempt was this. It prints "indirect" were I want to (this was just a test), but running the macro step by step I see that there is an error 91 at the time to "exit" the function.
Dim rng As Range
rng = TXT2RNG(Range("A1").Value)
'This is the function, located in Modulo1
'Function TXT2RNG(text As String) As Range
'Set TXT2RNG = Range(text)
'TXT2RNG.Value = "indirect"
'End Function
End Sub
I have attempted the same but without the function, and it works as I expected.
Dim rng As Range
Set rng = Range(Range("A2").Value)
'Set rng = Range(Range("A1").Value)
rng.Value = "direct"
End Sub
Summary: The second code works as a workaround but I want to know why the first one doesn't so can learn from it and use similar structures in the future. Thank you
Basically, you are simply missing a Set when assigning the result from the function to your variable - you do it correct in your direct example.
Whenever you are dealing with objects (eg worksheet, range), you need to use Set when assigning it to a variable. A good explanation can be found at https://stackoverflow.com/a/18928737/7599798
Omitting the Set will cause an error 91 when assigning it to an object variable. However, if you would declare your rng-variable as Variant, you wouldn't get a runtime error. Instead, VBA would use the so called default property, for a Range this is the Value, so you would end up having the content of the Range in your variable ("indirect" in your example). This is the reason to use the data type Variant only if really needed.
That said, there are at least 2 issues you should take care about:
when you use the Range-function as you do, it refers to ActiveSheet, which is the sheet that currently has the focus. When coding, that's not always what you want, so think about if you need to approve your function. You should really take the time to read the answers of How to avoid using Select in Excel VBA to get an understanding.
You should think about what should happen when the text you pass to your function doesn't contain a valid range-address. Currently, you would get a runtime error (1004). Error handling in VBA is done with On Error-statements. You should avoid On Error Resume Next.
You could change your function to:
Function TXT2RNG(ws as Worksheet, text As String) As Range
On Error Goto InvalidRange
Set TXT2RNG = ws.Range(text)
' TXT2RNG.Value = "indirect"
Exit Function
InvalidRange:
' Think about what to do here, show a message, simply ignore it...
Set TXT2RNG = Nothing
End Function
And the call to it would be
Dim rng as Range, address as string
address = Range("A1").Value
Set rng = TXT2RNG(activeSheet, address)
if not rng is Nothing then
(...)
Welcome to stack overflow.
From walking through your code, you're not declaring "text" as anything.
If you use the "Watches" feature, you can see it's a blank string
I believe you need to have a function that pulls the range, then a second function to pull the string of that. A Private Sub is much better
See this answer https://stackoverflow.com/a/2913690/2463166

Difference between range and variable dim as range

I've built a class and need to record the data into a cell. Therefore I write a function doing this.
Codes below:
Option Explicit
Private sName As String
Public Property Let Name(ByVal strValue As String)
sName = strValue
End Property
Public Property Get Name() As String
Name = sName
End Property
Public Function ItemToCell(ByRef tgtCell As Range)
tgtCell = sName
End Function
And I also set a button to trigger this process:
Private Sub CommandButton1_Click()
Dim tmpData As New MyClass
tmpData.Name = "Tom"
Dim tgtCell As Range
Set tgtCell = Worksheets("Sheet1").Range("A1")
'Method 1, this failed with error 424
tmpData.ItemToCell (tgtCell)
'Method 2, it works
tmpData.ItemToCell (Worksheets("Sheet1").Range("B1"))
End Sub
I thought that these two methods were the same, but apparently they are not.
Why? Isn't the variable tgtCell an object?
Note that method 1A below, with the parentheses removed, DOES work as expected:
Public Sub CommandButton1_Click()
On Error GoTo EH
Dim tmpData As New MyClass
tmpData.Name = "Tom"
Dim tgtCell As Range
Set tgtCell = Worksheets("Sheet1").Range("A1")
'Method 1, this failed with error 424
tmpData.ItemToCell (tgtCell)
'Method 1A, this works
tmpData.ItemToCell tgtCell
'Method 2, it works
tmpData.ItemToCell (Worksheets("Sheet1").Range("B1"))
XT: Exit Sub
EH: MsgBox Err.Description, vbOKOnly, Err.Source
Resume Next
End Sub
The difficulty arises because the call to tmpData.ItemToCell requires an l-value (ie it is a ByRef argument) but the invocation statement IS NOT a function call, and so the parentheses are not the parentheses of invocation, but rather the parentheses of grouping. This can be a confusing issue in VBA.
The effect of the parentheses of grouping is to return the value of the variable tgtCell rather than its storage location, and implicitly evaluating the default member Value of the Range object. However, as you stumbled into with Method 2, there are circumstances where VBA does not implicitly evaluate the default member. Yes, it's confusing to everyone; don't feel alone.
One way to minimize the occurrence of this annoyance is to explicitly specify parameters for Functions and Subs (And Set/Let properties) as ByVal unless you actually desire to pass back a changed value to the caller. This wins by:
eliminating many instances of this annoying feature;
eliminating many subtle bugs when you treat the parameter as a local variable, and change its value expecting those changes to be local in scope, when they are actually non-local.
However, your circumstance is the rare one where this does not help. In these case it is imply best to not add parentheses to method calls until VBA complains of their absence, which is generally just for Functions rather than Subs and Property Setters/Letters.
In summary:
- Parameters should be explicitly specified as ByVal (rather than the default of ByRef) unless you really are passing back a calculated value (in which case a Function is a better implementation and usually sufficient) or when the language requires you to pass a ByRef argument.
- Parentheses should only be used in method invocations when VBA complains of their absence.

Resources