I have an excel sheet detailing a list of equipment (about 12000 rows in the sheet). I want to make a dashboard whereby I enter the name of the equipment and it returns the Manufacturer, Date of manufacture and description.
What would be the best way to go about this? I was thinking of writing VBA code in order to match an input to an object type and return the required value however I have not coded in VBA and I'm unsure how to type it out.
Forgive me in suggesting a different approach but consider the scalable, relational advantage of SQL (Structured Query Language) that can take the Equipment Name as parameter and query your data into a filtered table resultset. If using Excel for PC, Excel can run SQL using the Jet/ACE SQL Engine (Windows .dll files), the very engine that powers its sibling, MS Access. This approach avoids array formulas, loops, if/then logic, complex multiple index/matching, and vlookups.
Below example prompts user for the Equipment Name using an InputBox which is then passed as a parameter to the WHERE clause of SQL query. We hate for a malicious user to run SQL injection in input box, something like: 1; DELETE FROM [DATA];. Example assumes data exists in a tab called DATA with column headers in first row and an empty tab called RESULTS. Adjustments can be made.
Sub RunSQL()
Dim conn As Object, rst As Object, cmd As Object
Dim equipmentVar As String, strConnection As String, strSQL As String
Dim i As Integer
Const adcmdText = 1, adVarChar = 200, adParamInput = 1
Set conn = CreateObject("ADODB.Connection")
Set rst = CreateObject("ADODB.Recordset")
' RECEIVE USER INPUT
equipmentVar = InputBox("Enter name of equipment.", "EQUIPMENT SEARCH")
If equipmentVar = "" Then Exit Sub
' CONNECTION STRINGS (TWO VERSIONS)
' strConnection = "DRIVER={Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb)};" _
' & "DBQ=C:\Path\To\Workbook.xlsm;"
strConnection = "Provider=Microsoft.ACE.OLEDB.12.0;" _
& "Data Source='C:\Path\To\Workbook.xlsm';" _
& "Extended Properties=""Excel 8.0;HDR=YES;"";"
strSQL = " SELECT [DATA$].Manufacturer, [DATA$].Equipment, " _
& " [DATA$].[Date of Manufacturer], [DATA$].[Description] " _
& " FROM [DATA$]" _
& " WHERE [DATA$].[Equipment] = ?;"
' OPEN DB CONNECTION
conn.Open strConnection
' SET CMD COMMAND
Set cmd = CreateObject("ADODB.Command")
With cmd
.ActiveConnection = conn
.CommandText = strSQL
.CommandType = adcmdText
.CommandTimeout = 15
End With
' BINDING PARAMETER
cmd.Parameters.Append cmd.CreateParameter("equipParam", adVarChar, adParamInput, 255)
cmd.Parameters(0).Value = equipmentVar
' EXECUTING TO RECORDSET
Set rst = cmd.Execute
' COLUMN HEADERS
For i = 1 To rst.Fields.Count
Worksheets("RESULTS").Cells(1, i) = rst.Fields(i - 1).Name
Next i
' DATA ROWS
Worksheets("RESULTS").Range("A2").CopyFromRecordset rst
rst.Close: conn.Close
Set rst = Nothing: Set cmd = Nothing: Set conn = Nothing
End Sub
Sounds like a simple loop through the cells looking for a match, because you have not given the layout I can't tell you the exact code but you are asking for an approach so here it goes.
1) If you want it to work when you enter a value in the cell then you use worksheet_Change i.e.
Options Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
If Target.Address = "$A$1" Then
' Your code here
End If
End Sub
The quickest way to code a loop through your 12000 rows but could be any length in future (assuming they start at row 5)
Dim cell As Range
For Each cell In Range("a5", Range("a" & Cells.Rows.Count).End(xlUp))
'Your if statement to determin if it is a match goes here
'Your copy code goes here
Next cell
2)You then put in the If statement to check for a match and do any copy display you want when you find one.
Good luck
Use the filter feature / button in the Ribbon.
Related
we are trying to use ADO to read data from a closed workbook, remove any whitespace and convert any incorrectly keyed dates into a valid format. Once the data has been cleansed, it's uploaded into a custom app.
We are using ADO for speed purposes as we have found using VBA to open/manipulate/close takes too long, meaning we miss our upload target time (we have multiple workbooks we need to apply this to).
The problem we have is converting the dates to a valid format. Dates are entered into the workbook either as dd/mm/yy or dd.mm.yy - we have no control over this, the template was created years ago and we are unable to update it and apply data validation.
Ideas We Have Tried: We have a few ideas, but have not been successful, does anyone know if any of these suggestions could work / suggest alternate ideas?
Check for a "." and apply a Replace(): If InStr(rs.Fields("Date").Value, ".") > 0 Then rs.Fields("Date").Value = Replace(rs.Fields("Date").Value, ".", "/")
This works when the column is read into the record set as type 202: adVarWChar, unfortunatly as the majority of the dates are valid, the data in the record set is set as type 7: adDate, when looping, once we get to an invalid date format (with the dots), we get a debug error:
"you cannot record changes because a value you entered violates the settings defined for this table or list (for example, a value is less than the minimum or greater than the maximum). correct the error and try again"
Convert the whole column data type to 202 adVarWChar:
As the above code works for entries when they are formatted as text, we had an idea to see if we could pull the whole column of data in directly as text, we have experimented with Casting and Convert but cannot get it to work - I no longer have the sample code we were trying for that. I recall experimenting adding IMEX=1 to the connection string, but this didn't seem to make any difference.
Apply a Find/Replace query on a whole column:
Instead of retrieving the data and looping through it, we had an idea to apply a find and replace query directly on the column, similar to how we are able to trim a whole column. Again, we were unable to find any code/queries which worked.
Create an empty record set and set the column type to String:
We had an idea to create a blank/empty record set and manually set the date column to a string type, and then loop through the retrieved data and move them into the new record set. We didn't get very far with this as we weren't too sure how to create a blank RS, then we also thought, how would we write this data back to the worksheet - as I don't think you can write back to a closed workbook.
Here is the code I have at the moment:
Sub DataTesting()
On Error GoTo ErrorHandler
'set the workbook path of the file we want to read from
Dim workbookFileName As String
workbookFileName = "C:\Users\xxx\xxx\myWorkbook.xls"
'create a connection string
Dim connectionString As String
connectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" _
& workbookFileName _
& ";Extended Properties=""Excel 12.0 Xml;HDR=YES;"";" 'IMEX=1"";"
'open the connection
Dim conn As ADODB.connection
Set conn = New ADODB.connection
conn.connectionString = connectionString
conn.Open
Dim rs As ADODB.Recordset
Set rs = New ADODB.Recordset
'Convert all data in the date column to a valid date (e.g. replace dates with decimals 1.1.21 to 01/01/2021)
'set query to select all data from the date column
Dim query As String
query = "SELECT * FROM [DATA SHEET$B2:B100]" 'col B is the Date column
With rs
.ActiveConnection = conn
'.Fields.Append "Date", adVarChar, 20, adFldMayBeNull 'NOT WORKING
.CursorType = adOpenDynamic
.LockType = adLockOptimistic
.Source = query
.Open
If Not .BOF And Not .EOF Then
While (Not .EOF)
If InStr(rs.Fields("Date").Value, ".") > 0 Then rs.Fields("Date").Value = Replace(rs.Fields("Date").Value, ".", "/")
.MoveNext
Wend
End If
.Close
End With
conn.Close
GoTo CleanUp
ErrorHandler:
MsgBox Err.Description 'THIS WILL BE WRITTEN TO TXT FILE
CleanUp:
'ensure the record set is equal to nothing and closed
If Not (rs Is Nothing) Then
If (rs.State And adStateOpen) = adStateOpen Then rs.Close
Set rs = Nothing
End If
'ensure the connection is equal to nothing and closed
If Not (conn Is Nothing) Then
If (conn.State And adStateOpen) = adStateOpen Then conn.Close
Set conn = Nothing
End If
End Sub
UPDATE:
I am now able to read the data using the following query:
"SELECT IIF([Date] IS NULL, NULL, CSTR([Date])) AS [Date] FROM [DATA SHEET$B2:B10]"
This will only work if I set IMEX=1, which is only read-only. I am able to loop through each item and print out the value / detect where the dots are, but I cannot then amend them!
As mentioned by #Doug Coats I can move the data into an array, perform the manipulation on the array. But how exactly do I then put that array back into the recordset?
I guess I would need to close the first 'read only' connection, and re-open it as a 'write' connection. Then somehow run an update query - but how do I replace the existing record set values with the values from the array?
Thanks
You could try an update query
Const SQL = " UPDATE [DATA SHEET$] " & _
" SET [Date] = REPLACE([Date],""."",""/"")" & _
" WHERE INSTR([Date],""."") > 0 "
Dim n
conn.Execute SQL, n
MsgBox n & " records updated"
Sub testdata()
Dim wb, ws, i
Set wb = Workbooks.Add
Set ws = wb.Sheets(1)
ws.Name = "DATA SHEET"
ws.Cells(1, 2) = "Date"
For i = 2 To 10
If Rnd() > 0.5 Then
ws.Cells(i, 2) = "27.07.21"
Else
ws.Cells(i, 2) = "27/07/21"
End If
Next
wb.SaveAs "c:\temp\so\dates.xls"
wb.Close
End Sub
I'm developping modules on a client XLSm with 32-bits 2013 Excel.
I'd like to use datas on worksheet as if it is an Access table.
With a lot of difficulties, I think connection is now OK.
Still, I have error : 3001 Arguments are of wrong type, are out of acceptable range. Error that I cannot understand.
Here excerpts of VBA lines :
In addition, I added 20 lines in data Worksheet below the header line to permit to Excel to interpret for the type of each columns.
varCnxStr = "Data Source=" & G_sWBookREINVOICingFilePath & ";" & "Extended Properties='Excel 12.0 Xml;HDR=YES;IMEX=15';"
With conXLdb
.Provider = "Microsoft.ACE.OLEDB.12.0"
.Mode = adModeShareExclusive
.Open varCnxStr
End With
strSQL = "SELECT * "
strSQL = strSQL & " FROM [ReInvoiceDB$B2B5072] inum "
strSQL = strSQL & " WHERE inum.InvoiceNum LIKE '1712*' "
strSQL = strSQL & ";"
'>> TRIGGERs ERROR with the current Where Clause !!'
adoXLrst.Open strSQL, conXLdb, dbOpenDynamic, adLockReadOnly, adCmdText
If adoXLrst.BOF And adoXLrst.EOF Then
'no records returned'
GoTo Veloma
End If
adoXLrst.MoveFirst
Do While Not adoXLrst.EOF
'Doing stuff with row'
adoXLrst.MoveNext
Loop
sHighestSoFar = adoXLrst(1).Value '> just to try for RecordSet : Codes are not completed...
sPrefixeCURR = Mid(sHighestSoFar, 1, 4)
Highest = CInt(Mid(sHighestSoFar, 5))
'> Increment >'
Highest = Highest + 1
HighestStr = sPrefixeCURR & Format(Highest, "00")
strGSFNumber = HighestStr
adoXLrst.Close
conXLdb.Close
Veloma:
On Error Resume Next
Set adoXLrst = Nothing
Set conXLdb = Nothing
Exit Sub
Etc.
Any idea about what seems be wrong ?
Thank you
Below is an old example I have been using successfully. Note that the sheet name in the book are Sheet1 and Sheet2, but in the query I had to use sheet1$ and sheet2$. I noticed you had $ signs in the middle of your sheet names. perhaps that's the issue ?
Sub SQLUpdateExample()
Dim con As ADODB.Connection
Dim rs As ADODB.Recordset
Set con = New ADODB.Connection
con.Open "Driver={Microsoft Excel Driver (*.xls)};" & _
"DriverId=790;" & _
"Dbq=" & ThisWorkbook.FullName & ";" & _
"DefaultDir=" & ThisWorkbook.FullName & ";ReadOnly=False;"
Set rs = New ADODB.Recordset
Set rs = con.Execute("UPDATE [Sheet1$] inner join [Sheet2$] on [Sheet1$].test1 = [Sheet2$].test1 SET [Sheet1$].test3 = [Sheet2$].test2 ")
Set rs = Nothing
Set con = Nothing
End Sub
To give more details about the whole module to be implemented : it is to perform a Transaction unit.
This transaction will comprise 3 operations : get a max value from a column (Invoice number) to increment it, record the new number inside an Access table (by DAO), the same Excel file (by ADO) and generating document on HDD.
So it is aimed to use the Excel file as a table not as a file manipulated with Windows script or Excel VBA. My end user is disturbed by the pop-uping of an Excel opening file operation. As a developer, I'm feeling more comfortable with using SQL statements as much as possible inside Transaction session. Is that your opinion too ?
UPDATE 4
Posted solution.
UPDATE 3
Finally managed to find a solution to my problem, will post the answer shortly within the next few days.
UPDATE 2!
Put updates at the top for better readability.
So apparently it's not possible to use a named range from excel directly in the vba code when exporting to access.
After doing some more research I found there is a way around this: changing the dynamic range to a static range first and then using the entirety of the string as a variable.
However my current code down below says it cannot find the range even though I'm sure the syntax is correct, is this because I haven't set the reference to the excel file correctly?
Sub ExportAccess()
Dim cn As ADODB.Connection
Dim rs As ADODB.Recordset
Dim strQuery As String
myAddress = Replace(ThisWorkbook.Names("ExportData").RefersToRange.Address, "$", "")
myAddress = "[GWU$" & myAddress & "]"
' [Excel 8.0;HDR=NO;DATABASE=C:\Users\Public\test.xls] < unused code snippet
strQuery = "INSERT INTO Table1" & vbCrLf & _
"SELECT * FROM " & myAddress & ""
Set cn = CreateObject("ADODB.Connection")
Set rs = CreateObject("ADODB.Recordset")
With cn
.Provider = "Microsoft.ACE.OLEDB.12.0"
.ConnectionString = "C:\Users\Public\Database.accdb"
.Open
End With
rs.Open strQuery, cn
cn.Close
rs.Close
Set rs = Nothing
Set cn = Nothing
End Sub
UPDATE!
After racking my brain for a few hours and checking the links I posted for some much needed reference, I came up with the following:
Sub ExportAccess()
Dim cn As ADODB.Connection
Dim strQuery As String
strQuery = "INSERT INTO Table1" & vbCrLf & _
"SELECT * FROM [Excel 8.0;HDR=YES;DATABASE=C:\Users\Public\test.xls].ExportData"
Set cn = New ADODB.Connection
With cn
.Provider = "Microsoft.ACE.OLEDB.12.0"
.ConnectionString = "C:\Users\Public\Database1.accdb"
.Open
.Execute strQuery
End With
With cn
.Close
End With
End Sub
I haven't been able to test this yet (will do this first thing tomorrow)
However I'm worried about the following code snippets:
"SELECT * FROM [Excel 8.0;HDR=YES;DATABASE=C:\Users\Public\test.xls].ExportData"
This should technically select the 'ExportData' range from the test worksheet based on a different question here on Stackoverflow, I'm just not 100% sure if it will work
.Open
.Execute strQuery
End With
With cn
.Close
End With
Will this actually perform the INSERT INTO? and is closing the the connection really required? (I'd assume so, just would like to know for sure)
Kind regards,
I've been trying to get what I have in my head working for a while now, but to be honest I don't have the required expertise right now. I'm still fairly new to programming so setting up my own variables and arrays that I can actually use is still very hard for me.
Hopefully you can help me out.
The situation:
So in an Excel read-only file I basically have a 10 row by 14 column range where users put their own data into, I want to only export the rows that have actual data that was filled in by these users to an Access 2010 database.
What I tried before: At first I wanted as simple of a solution as possible, I tried doing this by connecting the Excel worksheet to the Access database and then by doing a SQL append query on the rows where data is NOT NULL. (checks if entries in last column are NOT NULL)
However since many people will use the file at the same time I feared that the Access database/Connection to the Excel worksheet might get confused and start doing things it wasn't supposed to. When I tried to Google find out whether this was actually a problem or not my search results came up empty, so I was unsure if I should continue in this direction and ultimately abandoned this solution.
What I'm trying to do now: Basically I figured that I could do the same thing in an Excel macro before exporting, setting up a named range (via name manager) and then cutting out the rows that have no data in them with a simple macro before appending the remaining rows in the named range to the access database.
There are a few examples of people that have tried to do the same after a few google searches (unfortunately I can't post more than 2 links OR post linkbu.ch links):
Search results
However all of these examples seem to use a static range, not a dynamic range. How do I insert a Range from the name manager in VBA code? If anyone has any ideas on a different solution that would also be appreciated.
Kind regards,
FSDT
Use the macro recorder. Simply turn it on and manually go through the steps to export your named range. Then look at the VBA code you generated. The macro recorder is the greatest tool ever bestowed upon mankind. Try it, you'll like it.
All right everyone, as stated before this is the solution I came up with after trial and error:
Sub ExportAccess()
Dim cn As ADODB.Connection
Dim strQuery As String
Dim dbFilepath As String
Dim xlFilepath As String
dbFilepath = "C:\Users\Public\Database1.accdb"
xlFilepath = Application.ActiveWorkbook.FullName
xlFilepath = "[Excel 8.0;HDR=Yes;DATABASE=" & xlFilepath & ";IMEX=1]"
myAddress = Replace(ThisWorkbook.Names("ExportData").RefersToRange.Address, "$", "")
myAddress = "[Sheet1$" & myAddress & "]"
myAddress = "" & xlFilepath & "." & myAddress & ""
STRcn = "Provider=Microsoft.ACE.OLEDB.12.0; Data Source=" & dbFilepath & ";"
strQuery = "INSERT INTO Table1" & vbCrLf & _
"SELECT * FROM " & myAddress & ""
Set cn = CreateObject("ADODB.Connection")
cn.Open STRcn
cn.Execute strQuery
cn.Close
Set cn = Nothing
End Sub
I got a brilliant answer to my previous question from #Ryan Wildry but I thought I'd ask a different question regarding the same code: here goes.
Background Info
I have a shared (network/server) Excel template file which is both the input file and the data source (although on different sheets). Let's call that Input.xltm.
The code basically picks up a input in a range on Input Sheet, takes the first two letters and finds the closest code from Code Sheet, then populates a UserForm ListBox with the top five results.
The problem
The problem comes when users set off the UserForm and the error usually returns:
Run-time error '-2147467259'
The Microsoft Access database engine could not find the object 'C:\Users\user.name\Documents\Input1'. Make sure the object exists and that you spell its name and the path name correctly.......etc
I think it may have something to do with the fact Excel puts a number after the filename because it's a template file although I don't actually know!
The code
And here's the code:
Public MyConnection As New ADODB.Connection
Public MyRecordset As New ADODB.Recordset
Private Sub UserForm_Initialize()
Dim ColumnName As String: ColumnName = "[Variant code]"
Dim SearchStr As String: SearchStr = Left(Sheets("Input Sheet").Range("B4").Value2, 2)
Dim dbstring As String
dbstring = ThisWorkbook.FullName
Application.ScreenUpdating = False
If MyConnection.State <> adStateOpen Then
With MyConnection
.ConnectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & dbstring & _
";Extended Properties='Excel 12.0 Xml;HDR=YES;IMEX=1';"
.Open
End With
End If
If MyRecordset.State = adStateOpen Then MyRecordset.Close
MyRecordset.Open "Select top 5 " & ColumnName & " from [Code Sheet$] where " & ColumnName & _
" like '%" & SearchStr & "%'", MyConnection, adOpenForwardOnly, adLockReadOnly
Me.ListBox1.Clear
If Not MyRecordset.EOF Then MyRecordset.MoveFirst
Application.ScreenUpdating = True
Do Until MyRecordset.EOF
Me.ListBox1.AddItem MyRecordset.Fields(0).Value
MyRecordset.MoveNext
Loop
End Sub
I just need everyone who accesses the file through the server to be able to pick up the correct data source (which is only in the next sheet) and populate the ListBox.
I'd be thankful for any suggestions! Thanks
#UPDATE
I have checked, now if you open (and then save) the actual template file so there's no '1' after the file name, then the code works as expected. It's only when the template is opened normally and the number automatically appended that it stops working.
It seems that you do not make early-binding for MyConnection and MyRecordset first.
You can make a late-binding by
step 1.
Change
Public MyConnection As New ADODB.Connection
Public MyRecordset As New ADODB.Recordset
to
Public MyConnection As object
Public MyRecordset As object
.
step 2.
Add
Set MyConnection = createobject("adodb.connection")
Set MyRecordset = createobject("adodb.recordset")
before If MyConnection.State <> adStateOpen Then
Using Excel 2010, SQL and DAO
I am trying to execute a query against tables which reside outside the current spreadsheet. This would be trivial, inside Access, with linked tables/databases, but using VBA in excel, I am stymied.
Presuming these:
ExcelFileOne; Tab; various headed field/columns
ExcelFileTwo; Tab; various headed field/columns
I want to execute a query inside a third excel file, that goes something like this [using dot notation for explanation, not coding....] -- a simple example:
SELECT FileOne.[Tab$].Fields, FileTwo.[Tab$].Fields, etc.
FROM FileOne, FileTwo, Thisworkbook
WHERE (FileOne.[Tab$].field2 <> FileTwo.[Tab$].Field2)
AND (ThisWorkbook.[Tab$].Field1 ....)
Basically, I want to duplicate what Access will do natively, for that linked file.
Pointers in the right directions ?
[[ I could use a pointer towards why using "Excel 8.0..." in a connection works or fails on Excel2010, with macro files, and how to load the 12 or 14 variant in a network/system closed to users.... ]]
You can indeed query other workbooks using DAO and ADO directly in a SQL statement and likewise query Access databases tables by simply referencing their paths. Conversely, within an Access query you can query Excel workbooks! This is testament to the fact that Jet/ACE SQL engine (Windows .dll files) is not restricted to any one MS Office product or Windows program but a tool for all.
In both examples below, macros make a direct connection to first workbook and in SQL query each indirectly connects to second workbook. You can run code inside or outside either workbooks. Also both runs genric INNER JOIN on FileOne and FileTwo worksheets but any compliant Jet/ACE SQL statement should work. And both output query results in a pre-existing RESULTS tab.
DAO
Dim dbE As Object, db As Object, rst As Object
Dim sqlString As String
Dim i As Integer
Const dbOpenDynaset = 2, dbReadOnly = 4
' OPEN DB CONNECTION
Set dbE = CreateObject("DAO.DBEngine.120") 'ALSO TRY: DAO.DBEngine.35 OR .36
Set db = dbE.OpenDatabase("C:\Path\To\FileOne.xlsm", False, True, "Excel 12.0 Xml;HDR=Yes")
' OPEN QUERY RECORDSET
sqlString = " SELECT * FROM [TAB$] t1" _
& " INNER JOIN (SELECT * FROM" _
& " [Excel 12.0 Xml;HDR=Yes;Database=C:\Path\To\FileTwo.xlsm].[TAB$]) t2" _
& " ON t1.ID = t2.ID"
Set rst = db.OpenRecordset(sqlString, dbOpenDynaset, dbReadOnly)
' COLUMNS
For i = 1 To rst.Fields.Count
Worksheets("RESULTS").Cells(1, i) = rst.Fields(i - 1).Name
Next i
' DATA ROWS
Worksheets("RESULTS").Range("A2").CopyFromRecordset rst
rst.Close
db.Close
Set rst = Nothing
Set db = Nothing
Set dbE = Nothing
ADO
Dim conn As Object, rst As Object, fld As Object
Dim strConnection As String, strSQL As String
Dim i As Integer
Set conn = CreateObject("ADODB.Connection")
Set rst = CreateObject("ADODB.Recordset")
' OPEN DB CONNECTION
strConnection = "Provider=Microsoft.ACE.OLEDB.12.0;" _
& "Data Source='C:\Path\To\FileOne.xlsm';" _
& "Extended Properties=""Excel 12.0 Xml;HDR=YES;"";"
conn.Open strConnection
' OPEN QUERY RECORDSET
strSQL = " SELECT * FROM [TAB$] t1" _
& " INNER JOIN (SELECT * FROM" _
& " [Excel 12.0 Xml;HDR=Yes;Database=C:\Path\To\FileTwo.xlsm].[TAB$]) t2" _
& " ON t1.ID = t2.ID"
rst.Open strSQL, conn
' COLUMNS
For i = 1 To rst.Fields.Count
Worksheets("RESULTS").Cells(1, i) = rst.Fields(i - 1).Name
Next i
' DATA ROWS
Worksheets("RESULTS").Range("A2").CopyFromRecordset rst
rst.Close
conn.Close
Set rst = Nothing
Set conn = Nothing