1

I have created a macro that allows me to open multiple files based on their names and copy sheets into one on another workbook. Now I would like to add some criteria, I determine the last row with data. I used this:

lstRow2 = alarms.Cells(alarms.Rows.Count, "A").End(xlUp).Row

And now i want to go through each row and check if column G of each rows contains strings like ("condenser", "pump", etc) if yes copy the row but not the whole row, only a series of columns belonging to the row (for example for each row that match my criteria copy those columns A-B-X-Z) and finally copy all that in another sheet.

Thanks for your help

Pᴇʜ
  • 56,719
  • 10
  • 49
  • 73
Ibrahim
  • 79
  • 10
  • Added an innovative approach to solve your question. BTW as this is your first post have a look at SO and help other developpers to identify a good answer by marking it as accepted - see ["Someone answers"](https://stackoverflow.com/help/someone-answers). – T.M. Jul 25 '18 at 19:11
  • @T.M. What do you think of [my approach](https://stackoverflow.com/a/51526756/111794)? – Zev Spitz Jul 25 '18 at 19:58
  • @ZevSpitz - find it cool and straight ahead. BTW what about mine? – T.M. Jul 31 '18 at 18:02
  • This solved question has a slightly modified follow up question at [Copying values AND color index in an array](https://stackoverflow.com/questions/51654042/copying-values-and-color-index-in-an-array) – T.M. Aug 09 '18 at 17:17

3 Answers3

4

Flexible filter solution with multi-criteria

This approach allows a multi criteria search defining a search array and using the Application.Index function in an advanced way. This solution allows to avoid loops or ReDim s nearly completely in only a few steps:

  • [0] Define a criteria array, e.g. criteria = Array("condenser", "pump").
  • [1] Assign data A:Z to a 2-dim datafield array: v = ws.Range("A2:Z" & n), where n is the last row number and ws the set source sheet object. Caveat: If your basic data contain any date formats, it's strictly recommended to use the .Value2 property instead of the automatic default assignment via .Value - for further details see comment.
  • [2] Search through column G (=7th col) and build an array containing the found rows via a helper function: a = buildAr(v, 7, criteria).
  • [3] Filter based on this array a using the Application.Index function and reduce the returned column values to only A,B,X,Z.
  • [4] Write the resulting datafield array v to your target sheet using one command only: e.g. ws2.Range("A2").Resize(UBound(v), UBound(v, 2)) = v, where ws2 is the set target sheet object.

Main procedure MultiCriteria

Option Explicit                                 ' declaration head of code module
Dim howMany&                                    ' findings used in both procedures

Sub MultiCriteria()
' Purpose: copy defined columns of filtered rows
  Dim i&, j&, n&                                 ' row or column counters
  Dim a, v, criteria, temp                       ' all together variant
  Dim ws As Worksheet, ws2 As Worksheet          ' declare and set fully qualified references
  Set ws = ThisWorkbook.Worksheets("Sheet1")      ' <<~~ change to your SOURCE sheet name
  Set ws2 = ThisWorkbook.Worksheets("Sheet2")     ' <<~~ assign to your TARGET sheet name
' [0] define criteria
  criteria = Array("condenser", "pump")          ' <<~~ user defined criteria
' [1] Get data from A1:Z{n}
  n = ws.Range("A" & Rows.Count).End(xlUp).Row   ' find last row number n
  v = ws.Range("A2:Z" & n)                       ' get data cols A:Z and omit header row
' [2] build array containing found rows
  a = buildAr(v, 7, criteria)                    ' search in column G = 7
' [3a] Row Filter based on criteria
  v = Application.Transpose(Application.Index(v, _
      a, _
      Application.Evaluate("row(1:" & 26 & ")"))) ' all columns
' [3b] Column Filter A,B,X,Z
  v = Application.Transpose(Application.Transpose(Application.Index(v, _
      Application.Evaluate("row(1:" & UBound(a) - LBound(a) + 1 & ")"), _
      Array(1, 2, 24, 26))))                  ' only cols A,B,X,Z
' [3c] correct rows IF only one result row found or no one
  If howMany <= 1 Then v = correct(v)
' [4] Copy results array to target sheet, e.g. starting at A2
  ws2.Range("A2").offset(0, 0).Resize(UBound(v), UBound(v, 2)) = v
End Sub

Possible addition to check the filtered results array

If you want to control the results array in the VB Editor's immediate window, you could add the following section '[5] to the above code:

' [5] [Show results in VB Editor's immediate window]
  Debug.Print "2-dim Array Boundaries (r,c): " & _
              LBound(v, 1) & " To " & UBound(v, 1) & ", " & _
              LBound(v, 2) & " To " & UBound(v, 2)
  For i = 1 To UBound(v)
        Debug.Print i, Join(Application.Index(v, i, 0), " | ")
  Next i

1st helper function buildAr()

Function buildAr(v, ByVal vColumn&, criteria) As Variant
' Purpose: Helper function to check criteria array (e.g. "condenser","pump")
' Note:    called by main function MultiCriteria in section [2]
Dim found&, found2&, i&, n&, ar: ReDim ar(0 To UBound(v) - 1)
howMany = 0      ' reset boolean value to default
  For i = LBound(v) To UBound(v)
    found = 0
    On Error Resume Next    ' avoid not found error
    found = Application.Match(v(i, vColumn), criteria, 0)
    If found > 0 Then
       ar(n) = i
       n = n + 1
    End If
  Next i
  If n < 2 Then
     howMany = n: n = 2
  Else
     howMany = n
  End If
  ReDim Preserve ar(0 To n - 1)
  buildAr = ar
End Function

2nd helper function correct()

Function correct(v) As Variant
' Purpose: reduce array to one row without changing Dimension
' Note:    called by main function MultiCriteria in section [3c]
Dim j&, temp: If howMany > 1 Then Exit Function
ReDim temp(1 To 1, LBound(v, 2) To UBound(v, 2))
If howMany = 1 Then
   For j = 1 To UBound(v, 2): temp(1, j) = v(1, j): Next j
ElseIf howMany = 0 Then
   temp(1, 1) = "N/A# - No results found!"
End If
correct = temp
End Function

Edit I. due to your comment

"In column G I have a sentence for example (repair to do on the condenser) and I would like that as soon as the word "condenser" appears it implies it respects my criteria I tried ("* condenser*", "cex") like if filename like "book" but it doesn't work on an array, is there a method for that?"

Simply change the logic in helper function buildAr() to search via wild cards by means of a second loop over the search terms (citeria):

Function buildAr(v, ByVal vColumn&, criteria) As Variant
' Purpose: Helper function to check criteria array (e.g. "condenser","pump")
' Note:    called by main function MultiCriteria in section [2]
Dim found&, found2&, i&, j&, n&, ar: ReDim ar(0 To UBound(v) - 1)
howMany = 0      ' reset boolean value to default
  For i = LBound(v) To UBound(v)
    found = 0
    On Error Resume Next    ' avoid not found error
    '     ' ** original command commented out**
    '          found = Application.Match(v(i, vColumn), criteria, 0)
    For j = LBound(criteria) To UBound(criteria)
       found = Application.Match("*" & criteria(j) & "*", Split(v(i, vColumn) & " ", " "), 0)
       If found > 0 Then ar(n) = i: n = n + 1: Exit For
    Next j
  Next i
  If n < 2 Then
     howMany = n: n = 2
  Else
     howMany = n
  End If
  ReDim Preserve ar(0 To n - 1)
  buildAr = ar
End Function

Edit II. due to last comment - check for existing values in column X only

"... I saw the change you did but I wanted to apply the last simpler idea, (last comment ) not using the wild Card but instead to check if there's a value in column X."

Simply hange the logic in the helper function to check for existing values only by measuring the length of trimmed values in column 24 (=X) and change the calling code in the main procedure to

' [2] build array containing found rows
  a = buildAr2(v, 24)                            ' << check for value in column X = 24

Note: Section [0] defining criteria won't be needed in this case.

Version 2 of helper function

Function buildAr2(v, ByVal vColumn&, Optional criteria) As Variant
' Purpose: Helper function to check for existing value e.g. in column 24 (=X)
' Note:    called by main function MultiCriteria in section [2]
Dim found&, found2&, i&, n&, ar: ReDim ar(0 To UBound(v) - 1)
howMany = 0      ' reset boolean value to default
  For i = LBound(v) To UBound(v)
    If Len(Trim(v(i, vColumn))) > 0 Then
       ar(n) = i
       n = n + 1
    End If
  Next i
  If n < 2 Then
     howMany = n: n = 2
  Else
     howMany = n
  End If
  ReDim Preserve ar(0 To n - 1)
  buildAr2 = ar
End Function
T.M.
  • 9,436
  • 3
  • 33
  • 57
  • 1
    First of all I want to thank you for the time you spent writing this code, I tried it and it works, now I'm trying to make some changes. In column G I have a sentence for example (repair to do on the condenser) and I would like that as soon as the word "condenser" appears it implies it respects my criteria I tried ("* condenser*", "*cex*") like if filename like "*book*" but it doesn't work on an array, is there a method for that? – Ibrahim Jul 26 '18 at 08:26
  • then I would like to keep the same shapes and content as the source cells, after extraction the date changed from the French model (day/months/year) to the American model (months/day/year) and I don't have the color of cell A (based on the color i have to fill the cell so i would like to have the same format as the source worksheet) Thank you for your help! – Ibrahim Jul 26 '18 at 08:26
  • I came back with a simpler idea for the criteria, instead of looking if column G contains the name of one of the considered systems (condenser, pump,.... too many), I know that all the systems contain a particular data in column X. So I can say that if I have a value in column X it necessarily means in column G I have one of my systems meeting the criteria. Then the copy part (A,B,X,Z) while keeping the format of the source file – Ibrahim Jul 26 '18 at 13:38
  • @Ibrahimatto - glad having helped; added a way to search for wild cards in column G. – T.M. Jul 26 '18 at 19:05
  • it's me again I saw the change you did but I wanted to apply the last simpler idea ,(last comment ) not using the wild card but instead to check if there's a value in column X. if you can read my last comment before this one. Thank you again for your time and help @T.M. – Ibrahim Jul 26 '18 at 21:14
  • hello sorry to bother you during your weekend , I was just wondering if you get to read my message. Thank you @T.M. – Ibrahim Jul 29 '18 at 17:41
  • See Edit II. BTW I suggest to avoid too many changes to your original question in the future in comments. It's better to ask a new question instead showing what you tried by your own code - @Ibrahimatto – T.M. Jul 30 '18 at 17:41
  • Hope this 2nd edit to be helpful for you as well as other users - see my *Edit II.* Nevertheless I suggest to avoid too many changes to your original question in the future in comments. It would be better to ask a new question instead, point out your special issues and show what you've tried so far by your own code - c.f. [Minimal, Complete, and Verifiable example](https://stackoverflow.com/help/mcve) :-) @Ibrahimatto – T.M. Jul 30 '18 at 17:46
  • 1
    okay I would take your point into account, I had already created a new question but I liked your method and I wanted to deepen it. thank you @T.M. – Ibrahim Jul 31 '18 at 05:50
  • I tried it's working but i have to major isues : **1: the date on the debug screen is correct but after copying it on the target sheet the month and day are reversed. 2:The first column value (color) isnt copied. i'll leave a link below so you'll see the differences between them.** PS : i'm trying to be as clear as possible, i live in france and my english is not that good. Thank you for your understanding @T.M. . [link to picture](https://drive.google.com/open?id=14Ck6nN86UO1EwpkT4HGb-GXknOLfmL6N) – Ibrahim Jul 31 '18 at 08:09
  • ad `1)` Tested again without problem - for me dates appear identically in their (local) version. Which code do you use to display the values in the picture link? ad `2)` You **can't** copy formatting (e.g. colors) with an array containing only *pure values* - if you look at the picture link you see that the joined string first displays a `|` delimiter indicating an - empty - value as first value. **Work around** Reuse the `a` array values (=chosen item numbers) to get the source color formats and reformat the A column following the array order (+/-header row offsets) -Bonne chance @Ibrahimatto – T.M. Jul 31 '18 at 13:11
  • 1
    @Ibrahimatto Be aware that your system settings, or default excel settings, may be dictating the order of a date. Where you may desire yyyymmdd, your computer may be telling it dd/ mm/yyyy. You can format the column after the fact for a quick fix, or specify the format during the paste.. – Cyril Jul 31 '18 at 19:56
  • @Cyril my computer and excel are on the dd/mm/yyyy settings which is the standard in France and on the sheet i'm copying it's dd/mm/yyyy too. I dont' get it why it's switches to mm/dd/yyyy after pasting – Ibrahim Aug 01 '18 at 06:00
  • I just noticed something else, for all the dates whose days are <12 i have the switching problem but as soon as the day are >12 it copies correctly surely because we can't have more than 12 months.I tried to play on application.transpose and application.evaluative but I cannot understand where the error comes from. If I apply this formula `(=DATE(YEAR(A3),DAY(A3),MONTH(A3))` to the column where the dates are after copying I have the right date but since for days > 12 are already correct it modifies them too so I can't blindly apply on all the column. @T.M. @Cyril – Ibrahim Aug 01 '18 at 08:30
  • 1
    @Ibrahimatto I would recommend formatting the entire column at the end of your code. Those formatted correctly will look the same, and those incorrect will be corrected. As the date is being correctly displayed using the formula in your previous comment, this seems reasonable, as the data is there, just needs to look different. I am not positive why VBA paste is making that change; there are lots of posts if you google with the same issue, and all seem to make the same suggestion I made... to ensure your settings are correct (excel, system, OS, etc.), otherwise format the data. – Cyril Aug 01 '18 at 13:19
  • 1
    @Ibrahimatto .Columns("K").NumberFormat = "dd/mm/yyyy" should do it, provided you fix the column to the correct number... i arbitrarily chose K. – Cyril Aug 01 '18 at 13:25
  • 1
    Though the date in my tests didn't deviate, I found the following: apparently actual **dates** are silently converted to *strings* if you assign your data range to your array - so try using explicitly the **`.Value2`** property in section [1]: `v = ws.Range("A2:Z" & n).Value2` . Otherwise VBA interprets this assignment by default as pure *`.Value`* property with this unexpected treatment of dates. – T.M. Aug 01 '18 at 14:09
  • **Addendum** as to the known difference Value2:|Value: the **`Value2`** property is almost identical to the `Value` property except that the former does not use the Currency and Date data types. Depending on cell formatting (e.g. with date, currency or other formats), So these properties may return different values for the same cell. - Nevertheless I didn't expect the **silent conversion date** to string (check with `TypeName()`) with these apparently imprevisible effects (possibly depending on the current Excel version?) - thx also to @Cyril for his helpful hints. – T.M. Aug 01 '18 at 14:22
  • 1
    @T.M. it's working **value2**, i understand now. Thank you both for the explanation . Now i can focus on trying to retrieve the color, i mean every color has a code can't we retrieve that color value? like in the array to retrieve **the color index of column F** – Ibrahim Aug 02 '18 at 09:12
  • @Cyril Thank You – Ibrahim Aug 02 '18 at 09:12
  • @Ibrahimatto - `a)` Glad you can use my series of answers in which I invested considerable time; feel free to vote it up if you find it helpful or well researched - [Someone helps](https://stackoverflow.com/help/someone-answers). `b)` Kindly ask you to formulate a new question not only including the desired behavior, but the core problem or error and the shortest code necessary to reproduce it in the question itself if you didn't find s. at SO. Such a *MCVE* saves time and would be helpful for other readers - see [Minimal, Complete, and Verifiable example](https://stackoverflow.com/help/mcve) – T.M. Aug 02 '18 at 09:37
  • @T.M. i've voted, i'll ask another question for the color thing thank you – Ibrahim Aug 02 '18 at 12:30
2

I would create an SQL statement to read from the various sheets using ADODB, and then use CopyFromRecordset to paste into the destination sheet.

Add a reference (Tools -> References...) to Microsoft ActiveX Data Objects. (Choose the latest version; it's usually 6.1).

The following helper function returns the sheet names as a Collection for a given Excel file path:

Function GetSheetNames(ByVal excelPath As String) As Collection
    Dim connectionString As String
    connectionString = _
        "Provider=Microsoft.ACE.OLEDB.12.0;" & _
        "Data Source=""" & excelPath & """;" & _
        "Extended Properties=""Excel 12.0;HDR=No"""            

    Dim conn As New ADODB.Connection
    conn.Open connectionString

    Dim schema As ADODB.Recordset
    Set schema = conn.OpenSchema(adSchemaTables)

    Dim sheetName As Variant
    Dim ret As New Collection
    For Each sheetname In schema.GetRows(, , "TABLE_NAME")
        ret.Add sheetName
    Next

    conn.Close
    Set GetSheetNames = ret
End Function

Then, you can use the following:

Dim paths As Variant
paths = Array("c:\path\to\first.xlsx", "c:\path\to\second.xlsx")

Dim terms As String
terms = "'" & Join(Array("condenser", "pump"), "', '") & "'"

Dim path As Variant
Dim sheetName As Variant
Dim sql As String
For Each path In paths
    For Each sheetName In GetSheetNames(path)
        If Len(sql) > 0 Then sql = sql & " UNION ALL "
        sql = sql & _
            "SELECT F1, F2, F24, F26 " & _
            "FROM [" & sheetName & "] " & _
                "IN """ & path & """ ""Excel 12.0;"" " & _
            "WHERE F7 IN (" & terms & ")"
    Next
Next

'We're connecting here to the current Excel file, but it doesn't really matter to which file we are connecting
Dim connectionString As String
connectionString = _
    "Provider=Microsoft.ACE.OLEDB.12.0;" & _
    "Data Source=""" & ActiveWorkbook.FullName & """;" & _
    "Extended Properties=""Excel 12.0;HDR=No"""     

Dim rs As New ADODB.Recordset
rs.Open sql, connectionString

Worksheets("Destination").Range("A1").CopyFromRecordset rs
Zev Spitz
  • 13,950
  • 6
  • 64
  • 136
  • Basically like your approach as it shows an alternative +1. - Nevertheless there seem to be some issues: 1) the delimiter in `Join` function possibly should be `"', '"` (2) `path`, `sheetName` possibly to declare as `Variant`, outputFilePath is undeclared and unassigned (3) Parameter excelPath in helper function possibly only `Byval excelPath As Variant`. Could you test again? (4) In my language version I receive Error No -2147467259 'Tabelle2$' is no valid name calling `rs.Open sql, connectionString`. – T.M. Jul 26 '18 at 07:10
  • Thanks for your help but I have little knowledge of vba and sql as well as programming so I used the help form T.M which seemed simpler to me because I managed to understand a bit more in addition to his explanations. But I still wanted to thank you for the time and effort you put into trying to help me. – Ibrahim Jul 26 '18 at 08:29
  • 1
    @T.M. I've fixed the first three errors (it's kind of you to suggest that I tested this code once; I wrote it of the top of my head without testing at all). RE the fourth issue -- it works by me; could you put the full SQL in a comment here? – Zev Spitz Jul 26 '18 at 12:56
  • Zev: `SELECT F1, F2, F24, F26 FROM [Tabelle1$] IN "D:\Daten\Excel\_VBA Bsp\Stack\AllTogether.xlsx" "Excel 12.0;" WHERE F7 IN ('condenser', 'pump') UNION ALL SELECT F1, F2, F24, F26 FROM [Tabelle1$] IN "D:\Daten\Excel\_VBA Bsp\Stack\AllTogether.xlsx" "Excel 12.0;" WHERE F7 IN ('condenser', 'pump') UNION ALL SELECT F1, F2, F24, F26 FROM [Tabelle2$] IN "D:\Daten\Excel\_VBA Bsp\Stack\AllTogether.xlsx" "Excel 12.0;" WHERE F7 IN ('condenser', 'pump') UNION ALL SELECT F1, F2, F24, F26 FROM [Tabelle3$] IN "D:\Daten\Excel\_VBA Bsp\Stack\AllTogether.xlsx" "Excel 12.0;" WHERE F7 IN ('condenser', 'pump')` – T.M. Jul 26 '18 at 19:12
  • @ZevSpitz, thx having a look. [2] `connectionString = Provider=Microsoft.ACE.OLEDB.12.0;Data Source="D:\Daten\Excel\_VBA Bsp\Stack\MultiCriteria.xlsm";Extended Properties="Excel 12.0;HDR=No" ` – T.M. Jul 26 '18 at 19:14
  • @T.M. Could you write the names of the actual worksheets that are in the workbook? – Zev Spitz Jul 31 '18 at 15:15
  • My worksheet names are "Tabelle1","Tabelle2","Tabelle3". Running your edited version now with 2 test files I get Error No -2147217904 (translated message: *For at least one needed parameter wasn't indicated a value.*). Possibly missing suffix `UNION ALL SELECT F1, F2, F24, F26` in last query condition? – T.M. Jul 31 '18 at 18:00
  • @T.M. Do you have any named ranges? What happens if you try to run each individual SQL statement separately? – Zev Spitz Jul 31 '18 at 19:50
  • No, didn't have any named range, but have mistyped or left blank needed row headers (works now as intended); thanks for your patience. BTW learnt from your approach +1- @ZevSpitz. – T.M. Aug 02 '18 at 07:15
  • 1
    @T.M. Note that if you want to use your headers instead of autogenerated ones (`F1`, `F2` etc.) you can specify `HDR=Yes` in the connection string (instead of `HDR=No`. – Zev Spitz Aug 02 '18 at 08:06
  • @T.M. Also, I should point out that `Tabelle1$` seems to be selected from twice. Not sure why. – Zev Spitz Aug 02 '18 at 08:08
0

Something like this maybe:

j = 0
For i = To alarms.Rows.Count
   sheetname = "your sheet name"
   If (Sheets(sheetname).Cells(i, 7) = "condenser" Or Sheets(sheetname).Cells(i, 7) = "pump") Then
       j = j + 1
       Sheets(sheetname).Cells(i, 1).Copy Sheets("aff").Cells(j, 1) 
       Sheets(sheetname).Cells(i, 2).Copy Sheets("aff").Cells(j, 2) 
   End If
Next i
Pᴇʜ
  • 56,719
  • 10
  • 49
  • 73