• Status: Solved
  • Priority: Medium
  • Security: Public
  • Views: 300
  • Last Modified:

Excel: Sort & Match Rows - Duplicate Rows Not Being Handled Properly - MatchEm_10

Excel 2003
MatchEm_10 - Determines what rows are similar, if any.
Author/Solution Provider: Brad Byundt
Refer to prior post: http://www.experts-exchange.com/Software/Office_Productivity/Office_Suites/MS_Office/Excel/Q_25345093.html
Files Attached:
  1) Code - Included as a Module in the attached file.
  2) Sample Data - Includes RSLT sheets with examples of mis-matched data.

PROBLEM
------------------
MatchEm_10 does not seem to handle columns with duplicate content. But it might be something else.

I've taken the following precautions:
  - Start in row 2
  - Use =Trim(ROWCOL) to strip leading & trailing whitespace
  - I've tried the longer column of data on the left, and visa-versa.

Can someone help me figure out why the matching results are incorrect? It had been working fine on other data files.
MatchEM-10.xls
0
WizeOwl
Asked:
WizeOwl
  • 4
  • 4
1 Solution
 
byundtCommented:
Duplicate entries in the second list are definitely the problem.

What would you like to happen in the event of duplicate content? Should the duplicate entry be deleted? Should a blank cell be inserted in the first list?
0
 
WizeOwlAuthor Commented:
Thank you for addressing my question.

DEFINITIONS
-----------------
For simplicity, when I say "set 1" or "set 2" I am referring to only the 1st column of each data set, since each set will of course have multiple and variable number of columns.

Set 1 is always assumed to be on the LEFT.

What follows is my attempt to explain what would seem to work best for this tool.
Please let me know if I missed anything.


HOW TO HANDLE DUPLICATES
----------------------------------------
First, after thinking about it, let's agree that this is NOT intended to be a duplicate removal tool.

Therefore, it seems all duplicates should be matched as they occur.

Solution #1:
Non-matching duplicates should be marked as a MISMATCH.

Solution #2:
Non-matching duplicates should be marked as a DUP-MISMATCH.


Here are the variations I can foresee:

1. Both sets contain the same number of duplicates of Word-X and of Word-Y
2. One set contains a different number of duplicates of Word-X and of Word-Y
3. Both sets contain the same number of duplicates of Word-X, but different number of Word-Y


COLUMN LENGTH VARIATIONS
----------------------------------------
These are the possible combinations that it seems we should test for:

1. Set 1 and Set 2 contain the exact same data : Neither has duplicate entries.
2. Set 1 and Set 2 contain the exact same data : Both have duplicate entries.

3. Set 1 and Set 2 contain different data : Same number of ROWS : Neither has duplicate entries.
4. Set 1 and Set 2 contain different data : Same number of ROWS : ONLY Set 1 has duplicate entries.
5. Set 1 and Set 2 contain different data : Same number of ROWS : ONLY Set 2 has duplicate entries.

6. Set 1 has more ROWS : Neither has duplicate entries.
7. Set 1 has more ROWS : Both have duplicate entries.
8. Set 1 has more ROWS : ONLY Set 1 has duplicate entries.
9. Set 1 has more ROWS : ONLY Set 2 has duplicate entries.

10. Set 2 has more ROWS : Neither has duplicate entries.
11. Set 1 has more ROWS : Both have duplicate entries.
12. Set 2 has more ROWS : ONLY Set 1 has duplicate entries.
13. Set 2 has more ROWS : ONLY Set 2 has duplicate entries.

.
0
 
byundtCommented:
The array formula to insert non-matching items was taking too long for recalculation, so I changed to a faster executing method using collections. I insert blank cells in List1 if an item appears in both lists but more times in List2 than List1. This item is labelled "Duplicate" in List2.

Sub MatchEm_10()
'
' You may need to start in ROW 2
'
' Be sure there are no leading or trailing space characters in the first column of each data set
' (use =TRIM(RowCol))
'
' Assumes that the two sets of data are in parallel columns. No data is to the right of the second set. _
     The two sets of data must start in the same row, but need not contain the same data types, or _
     number of columns. _
     Nothing should be underneath either set of data. _
     Blank row in the data set with the most rows terminates execution.
'
' The "key" must be the first column in each set of data
'
' The number of columns in each list is determined by looking to the right for a blank cell.
'
' The number of rows in each list is determined by looking down in the column for a blank cell.
'
' In other words, make sure the first row and column are fully populated, and include a blank row below and _
        blank column to the right of each list
'
'
Dim frmla1 As String, frmla2 As String, frmla3 As String
Dim addr1 As String, addr2 As String, sRows As String
Dim celHome As Range, rgA As Range, rgB As Range, rg1 As Range, rg2 As Range, rg3 As Range, rgSort As Range
Dim i As Long, j As Long, nCols1 As Long, nCols2 As Long, nRows As Long, nRows1 As Long, nRows2 As Long, _
    n1 As Long, n2 As Long
Dim ws As Worksheet, wsRSLT As Worksheet
Dim col As New Collection

Set celHome = ActiveCell    'Set a "return address" for the end of the macro
On Error Resume Next        'Turn error handling off. _
                                If you don't pick a range, the InputBox function will fail. _
                                If RSLT worksheet doesn't exist, then trying to set a worksheet variable to it will fail.
Set rgA = Application.InputBox("Please pick the top left cell in set 1", Type:=8)
If rgA Is Nothing Then Exit Sub     'No range was picked, so exit sub
Set rgB = Application.InputBox("Please pick the top left cell in set 2", Type:=8)
If rgB Is Nothing Then Exit Sub     'No range was picked, so exit sub
Set ws = Worksheets("RSLT")
Set wsRSLT = Worksheets.Add(After:=Worksheets(Worksheets.Count))    'Add the new sheet after the last worksheet in the workbook
If ws Is Nothing Then   'RSLT worksheet does not exist, so let's add one to this workbook
    wsRSLT.Name = "RSLT"
Else
    wsRSLT.Name = "RSLT" & Worksheets.Count
End If
On Error GoTo 0         'Restore normal error handling

Application.ScreenUpdating = False      'Eliminate screen flicker while macro runs
nCols1 = 1      'Count the number of columns in the two lists. They don't have to be the same.
nCols2 = 1
If rgA.Cells(1, 2) <> "" Then nCols1 = rgA.End(xlToRight).Column - rgA.Column + 1   'The If test makes sure that List1 and List2 contain at least two columns of data
If rgB.Cells(1, 2) <> "" Then nCols2 = rgB.End(xlToRight).Column - rgB.Column + 1   'The .End(xlToRight) finds the cell to the left of the first blank in the row
    'Rows.Count is number of rows in the spreadsheet (varies with Excel version)
    'rgA.Worksheet returns the worksheet that contains List1; rgB.Worksheet returns the worksheet that contains List2 (need not be the same)
    'rgA.Column is column number of first cell in List1
nRows1 = rgA.End(xlDown).Row - rgA.Row + 1      'Look down from top of list to find last row in List1. Determine number of rows.
nRows2 = rgB.End(xlDown).Row - rgB.Row + 1      'Look down from top of list to find last row in List2. Determine number of rows.
nRows = IIf(nRows1 > nRows2, nRows1, nRows2)        'The number of rows in the longer list
Set rgA = rgA.Resize(nRows1, nCols1)       'All the data in List1. Resize grows the length of the range variable
Set rgB = rgB.Resize(nRows2, nCols2)       'All the data in List2. Resize could also grow the width, but that feature not needed here

With wsRSLT         'All objects or properties that begin with a . refer to worksheet RSLT
    rgA.Copy
    .Cells(1, 1).PasteSpecial xlPasteAll     'Paste values and number formats, starting in cell A1
    Set rg1 = .Cells(1, 1).Resize(nRows1, 1)            'The first column of List1, which must contain the "key"
    
    rgB.Copy    'Paste List2 with three blank columns between List1 and List2
    .Cells(1, nCols1 + 4).PasteSpecial xlPasteAll
    Set rg2 = .Cells(1, nCols1 + 4).Resize(nRows, 1)    'The first column of List2, which must contain the "key". If List1 is longer, pad with blank rows at bottom.
    Application.Goto .Cells(1, 1)  'Select cell A1 on the RSLT worksheet. This unselects the List2 cells that had been pasted.
End With
rg1.EntireColumn.AutoFit
rg2.EntireColumn.AutoFit

Set rgSort = rg2.Offset(0, -2).Resize(, 2)   'The two blank columns for the auxiliary formulas
addr1 = rg1.Address(ReferenceStyle:=xlR1C1)                 'Address of List1 first column in R1C1 format
addr2 = rgSort.Columns(1).Address(ReferenceStyle:=xlR1C1)   'Address of auxiliary formula columns in R1C1 format
'sRows = "ROW(R1:R" & nRows & ")"        'The ROW function returns the numbers 1 through the number of rows in the longer list.
    
    'Find duplicates in List1 and arrange them together
frmla3 = "=MATCH(RC1,R1C1:RC1,0)"
rg1.Offset(, nCols1).FormulaR1C1 = frmla3
rg1.Resize(, nCols1 + 1).Sort Key1:=rg1.Cells(1, nCols1 + 1), Order1:=xlAscending, Header:=xlNo
rg1.Columns(nCols1 + 1).ClearContents
    
    'Find duplicates in List2 and arrange them together
frmla3 = "=MATCH(RC[2],R1C[2]:RC[2],0)"
rgSort.Columns(1).FormulaR1C1 = frmla3
rgSort.Resize(, 2 + nCols2).Sort Key1:=rgSort.Cells(1, 1), Order1:=xlAscending, Header:=xlNo
rgSort.Columns(1).ClearContents
    
    'Returns the index number in List1 where the key from List2 matches, or error value #N/A if no match was found
frmla1 = "=MATCH(RC[2]," & addr1 & ",0)+COUNTIF(R1C[2]:RC[2],RC[2])-1"
rgSort.Columns(1).FormulaR1C1 = frmla1      'Put frmla1 in the first auxiliary column
    'Sort the auxiliary columns and List2 by the results of frmla1. Non-matches will be at the end.
rgSort.Resize(, 2 + nCols2).Sort Key1:=rgSort.Cells(1, 1), Order1:=xlAscending, Header:=xlNo
For i = 2 To nRows2
    If rg2.Cells(i, 1) = rg2.Cells(i - 1, 1) Then
        n1 = Application.CountIf(rg1, rg2.Cells(i, 1))
        n2 = Application.CountIf(rg2, rg2.Cells(i, 1))
        If n2 > n1 And n1 > 0 Then
            rg1.Cells(rg2.Cells(i + n1, 1).Offset(0, -2)).Resize(, nCols1).Insert shift:=xlDown
            nRows1 = nRows1 + 1
        End If
    End If
Next
nRows = IIf(nRows1 > nRows2, nRows1, nRows2)        'The number of rows in the longer list
sRows = "ROW(R1:R" & nRows & ")"        'The ROW function returns the numbers 1 through the number of rows in the longer list.

For i = 1 To nRows      'Create collection with number 1 through nRows
    col.Add i, Str(i)
Next
For i = 1 To nRows2     'Delete collection items corresponding to matches between List1 and List2
    If IsNumeric(rgSort.Cells(i, 1).Value) Then
        col.Remove Str(rgSort.Cells(i, 1).Value)
    End If
Next
For i = 1 To nRows     'Assign unused numbers from collection
    If IsError(rgSort.Cells(i, 1).Value) Then
        j = j + 1
        rgSort.Cells(i, 2).Value = col.Item(j)
    Else
        rgSort.Cells(i, 2).Value = rgSort.Cells(i, 1).Value
    End If
Next

'The next three statements are quite slow (recalc time) and have been replaced by the much faster three blocks of For...Next above
'Array formula that inserts the "left-over" numbers into the results from frmla1 whenever no match was found
'frmla2 = "=IF(ISNA(RC[-1]),SMALL(IF(ISNA(MATCH(" & sRows & "," & addr2 & ",0))," & sRows & _
    ",""""),COUNTIF(R" & rg2.Row & "C[-1]:RC[-1],""#N/A"")),RC[-1])"
'rgSort.Cells(1, 2).FormulaArray = frmla2    'Put frmla2 in the first cell of second auxiliary column, as an array formula
'rgSort.Columns(2).FillDown                  'Copy down the array formula

    'Sort the auxiliary columns and List2 by the results of frmla2. This will align List2 rows with their match from List1
rgSort.Resize(, 2 + nCols2).Sort Key1:=rgSort.Cells(1, 2), Order1:=xlAscending, Header:=xlNo
On Error Resume Next
Set rg3 = rg1.SpecialCells(xlCellTypeBlanks)
If Not rg3 Is Nothing Then
    Intersect(rg3.EntireRow, rgSort.Columns(1)).Value = "Duplicate"
End If
On Error GoTo 0
rgSort.Columns(2).EntireColumn.Delete      'Delete the second auxiliary column
rgSort.Columns(1).SpecialCells(xlCellTypeFormulas, xlNumbers).ClearContents
rgSort.Columns(1).SpecialCells(xlCellTypeFormulas, xlErrors).Value = "Mismatch"

Application.Goto celHome        'Return to the original cell that was active
Application.ScreenUpdating = True
End Sub

Open in new window


Brad
MatchEM-10Q27410179.xls
0
Independent Software Vendors: We Want Your Opinion

We value your feedback.

Take our survey and automatically be enter to win anyone of the following:
Yeti Cooler, Amazon eGift Card, and Movie eGift Card!

 
WizeOwlAuthor Commented:
That method should be ok; however, there seems to be some problems. I've created two sets of data to test various conditions. The results are included. (See attached)

1. When the list on the right is longer, not all it's rows are being processed. (see Test 2)
2. When there is a duplicate in the list on the right, the "Duplicate" Label is not placed in the corresponding adjacent cell.
MatchEM-11.xls
0
 
byundtCommented:
Jerry,
Thanks for preparing the shorter dataset. It is now much easier to see where the problems are and trace the logic through the data.

I fixed the problem with List2 being longer and not being all processed. This was an easy fix--just change the number of times through a loop.

I also fixed a problem in which duplicates weren't placed together in List2. This issue proved somewhat vexing because the next blank space in the list order may not have enough consecutive numbers to provide a home for all the duplicates.

Sub MatchEm_11()
'
' You may need to start in ROW 2
'
' Be sure there are no leading or trailing space characters in the first column of each data set
' (use =TRIM(RowCol))
'
' Assumes that the two sets of data are in parallel columns. No data is to the right of the second set. _
     The two sets of data must start in the same row, but need not contain the same data types, or _
     number of columns. _
     Nothing should be underneath either set of data. _
     Blank row in the data set with the most rows terminates execution.
'
' The "key" must be the first column in each set of data
'
' The number of columns in each list is determined by looking to the right for a blank cell.
'
' The number of rows in each list is determined by looking down in the column for a blank cell.
'
' In other words, make sure the first row and column are fully populated, and include a blank row below and _
        blank column to the right of each list
'
'
Dim frmla1 As String, frmla2 As String, frmla3 As String
Dim addr1 As String, addr2 As String, sRows As String
Dim celHome As Range, rgA As Range, rgB As Range, rg1 As Range, rg2 As Range, rg3 As Range, rgSort As Range
Dim i As Long, j As Long, k As Long, kk As Long, nCols1 As Long, nCols2 As Long, nRows As Long, nRows1 As Long, nRows2 As Long, _
    n1 As Long, n2 As Long
Dim ws As Worksheet, wsRSLT As Worksheet
Dim col As New Collection

Set celHome = ActiveCell    'Set a "return address" for the end of the macro
On Error Resume Next        'Turn error handling off. _
                                If you don't pick a range, the InputBox function will fail. _
                                If RSLT worksheet doesn't exist, then trying to set a worksheet variable to it will fail.
Set rgA = Application.InputBox("Please pick the top left cell in set 1", Type:=8)
If rgA Is Nothing Then Exit Sub     'No range was picked, so exit sub
Set rgB = Application.InputBox("Please pick the top left cell in set 2", Type:=8)
If rgB Is Nothing Then Exit Sub     'No range was picked, so exit sub
Set ws = Worksheets("RSLT")
Set wsRSLT = Worksheets.Add(After:=Worksheets(Worksheets.Count))    'Add the new sheet after the last worksheet in the workbook
If ws Is Nothing Then   'RSLT worksheet does not exist, so let's add one to this workbook
    wsRSLT.Name = "RSLT"
Else
    wsRSLT.Name = "RSLT" & Worksheets.Count
End If
On Error GoTo 0         'Restore normal error handling

Application.ScreenUpdating = False      'Eliminate screen flicker while macro runs
nCols1 = 1      'Count the number of columns in the two lists. They don't have to be the same.
nCols2 = 1
If rgA.Cells(1, 2) <> "" Then nCols1 = rgA.End(xlToRight).Column - rgA.Column + 1   'The If test makes sure that List1 and List2 contain at least two columns of data
If rgB.Cells(1, 2) <> "" Then nCols2 = rgB.End(xlToRight).Column - rgB.Column + 1   'The .End(xlToRight) finds the cell to the left of the first blank in the row
    'Rows.Count is number of rows in the spreadsheet (varies with Excel version)
    'rgA.Worksheet returns the worksheet that contains List1; rgB.Worksheet returns the worksheet that contains List2 (need not be the same)
    'rgA.Column is column number of first cell in List1
nRows1 = rgA.End(xlDown).Row - rgA.Row + 1      'Look down from top of list to find last row in List1. Determine number of rows.
nRows2 = rgB.End(xlDown).Row - rgB.Row + 1      'Look down from top of list to find last row in List2. Determine number of rows.
nRows = IIf(nRows1 > nRows2, nRows1, nRows2)        'The number of rows in the longer list
Set rgA = rgA.Resize(nRows1, nCols1)       'All the data in List1. Resize grows the length of the range variable
Set rgB = rgB.Resize(nRows2, nCols2)       'All the data in List2. Resize could also grow the width, but that feature not needed here

With wsRSLT         'All objects or properties that begin with a . refer to worksheet RSLT
    rgA.Copy
    .Cells(1, 1).PasteSpecial xlPasteAll     'Paste values and number formats, starting in cell A1
    Set rg1 = .Cells(1, 1).Resize(nRows1, 1)            'The first column of List1, which must contain the "key"
    
    rgB.Copy    'Paste List2 with three blank columns between List1 and List2
    .Cells(1, nCols1 + 4).PasteSpecial xlPasteAll
    Set rg2 = .Cells(1, nCols1 + 4).Resize(nRows, 1)    'The first column of List2, which must contain the "key". If List1 is longer, pad with blank rows at bottom.
    Application.Goto .Cells(1, 1)  'Select cell A1 on the RSLT worksheet. This unselects the List2 cells that had been pasted.
End With
rg1.EntireColumn.AutoFit
rg2.EntireColumn.AutoFit

Set rgSort = rg2.Offset(0, -2).Resize(, 2)   'The two blank columns for the auxiliary formulas
addr1 = rg1.Address(ReferenceStyle:=xlR1C1)                 'Address of List1 first column in R1C1 format
addr2 = rgSort.Columns(1).Address(ReferenceStyle:=xlR1C1)   'Address of auxiliary formula columns in R1C1 format
'sRows = "ROW(R1:R" & nRows & ")"        'The ROW function returns the numbers 1 through the number of rows in the longer list.
    
    'Find duplicates in List1 and arrange them together
frmla3 = "=MATCH(RC1,R1C1:RC1,0)"
rg1.Offset(, nCols1).FormulaR1C1 = frmla3
rg1.Resize(, nCols1 + 1).Sort Key1:=rg1.Cells(1, nCols1 + 1), Order1:=xlAscending, Header:=xlNo
rg1.Columns(nCols1 + 1).ClearContents
    
    'Find duplicates in List2 and arrange them together
frmla3 = "=MATCH(RC[2],R1C[2]:RC[2],0)"
rgSort.Columns(1).FormulaR1C1 = frmla3
rgSort.Resize(, 2 + nCols2).Sort Key1:=rgSort.Cells(1, 1), Order1:=xlAscending, Header:=xlNo
rgSort.Columns(1).ClearContents
    
    'Returns the index number in List1 where the key from List2 matches, or error value #N/A if no match was found
frmla1 = "=MATCH(RC[2]," & addr1 & ",0)+COUNTIF(R1C[2]:RC[2],RC[2])-1"
rgSort.Columns(1).FormulaR1C1 = frmla1      'Put frmla1 in the first auxiliary column
    'Sort the auxiliary columns and List2 by the results of frmla1. Non-matches will be at the end.
rgSort.Resize(, 2 + nCols2).Sort Key1:=rgSort.Cells(1, 1), Order1:=xlAscending, Header:=xlNo
For i = 2 To nRows2
    If rg2.Cells(i, 1) = rg2.Cells(i - 1, 1) Then
        n1 = Application.CountIf(rg1, rg2.Cells(i, 1))
        n2 = Application.CountIf(rg2.Cells(1, 1).Resize(i), rg2.Cells(i, 1))
        If n2 > n1 And n1 > 0 Then
            rg1.Cells(rg2.Cells(i, 1).Offset(1 - n2, -2)).Offset(n1, 0).Resize(, nCols1).Insert shift:=xlDown
            nRows1 = nRows1 + 1
        End If
    End If
Next
nRows = IIf(nRows1 > nRows2, nRows1, nRows2)        'The number of rows in the longer list
sRows = "ROW(R1:R" & nRows & ")"        'The ROW function returns the numbers 1 through the number of rows in the longer list.

For i = 1 To nRows      'Create collection with number 1 through nRows
    col.Add i, Str(i)
Next
For i = 1 To nRows2     'Delete collection items corresponding to matches between List1 and List2
    If IsNumeric(rgSort.Cells(i, 1).Value) Then
        col.Remove Str(rgSort.Cells(i, 1).Value)
    End If
Next
For i = 1 To nRows2     'Assign unused numbers from collection
    If IsError(rgSort.Cells(i, 1).Value) Then
        If rg2.Cells(i + 1) = rg2.Cells(i, 1) Then      'Have two or more items in List2 that aren't in List1
            n2 = Application.CountIf(rg2, rg2.Cells(i, 1))
            For k = j + 1 To col.Count
                If col.Item(k + n2 - 1) = (col.Item(k) + n2 - 1) Then
                    For kk = n2 To 1 Step -1
                        rgSort.Cells(i + kk - 1, 2).Value = col.Item(k + kk - 1)
                        col.Remove (k + kk - 1)
                    Next
                    i = i + n2 - 1
                    Exit For
                End If
            Next
        Else    'Insert a single item here
            j = j + 1
            rgSort.Cells(i, 2).Value = col.Item(j)
        End If
    Else
        rgSort.Cells(i, 2).Value = rgSort.Cells(i, 1).Value
    End If
Next

'The next three statements are quite slow (recalc time) and have been replaced by the much faster three blocks of For...Next above
'Array formula that inserts the "left-over" numbers into the results from frmla1 whenever no match was found
'frmla2 = "=IF(ISNA(RC[-1]),SMALL(IF(ISNA(MATCH(" & sRows & "," & addr2 & ",0))," & sRows & _
    ",""""),COUNTIF(R" & rg2.Row & "C[-1]:RC[-1],""#N/A"")),RC[-1])"
'rgSort.Cells(1, 2).FormulaArray = frmla2    'Put frmla2 in the first cell of second auxiliary column, as an array formula
'rgSort.Columns(2).FillDown                  'Copy down the array formula

    'Sort the auxiliary columns and List2 by the results of frmla2. This will align List2 rows with their match from List1
rgSort.Resize(, 2 + nCols2).Sort Key1:=rgSort.Cells(1, 2), Order1:=xlAscending, Header:=xlNo
On Error Resume Next
Set rg3 = rg1.SpecialCells(xlCellTypeBlanks)
If Not rg3 Is Nothing Then
    Intersect(rg3.EntireRow, rgSort.Columns(1)).Value = "Duplicate"
End If
On Error GoTo 0
rgSort.Columns(2).EntireColumn.Delete      'Delete the second auxiliary column
rgSort.Columns(1).SpecialCells(xlCellTypeFormulas, xlNumbers).ClearContents
rgSort.Columns(1).SpecialCells(xlCellTypeFormulas, xlErrors).Value = "Mismatch"

Application.Goto celHome        'Return to the original cell that was active
Application.ScreenUpdating = True
End Sub

Open in new window


Brad
MatchEM-11Q27410179.xls
0
 
WizeOwlAuthor Commented:
Thanks Brad, that seems to do it.
0
 
byundtCommented:
Jerry,
If you have several non-matching duplicates in a row near the very end of List2, there may not be enough contiguous blank spaces to find a home for them. If so, your List2 results may be out of sequence at the very bottom. The workaround is to make sure that List2 ends with a sufficient number of unique mismatched items, thereby moving the duplicate items higher in the list.

I expect this potential problem to be very unlikely given your sample data, but you should be aware of the possibility.

Brad
0
 
WizeOwlAuthor Commented:
An update to this question has been posted, #Q_27444226
0

Featured Post

Independent Software Vendors: We Want Your Opinion

We value your feedback.

Take our survey and automatically be enter to win anyone of the following:
Yeti Cooler, Amazon eGift Card, and Movie eGift Card!

  • 4
  • 4
Tackle projects and never again get stuck behind a technical roadblock.
Join Now