In my quest to improve the quality of life at work, I've searched for an answer and wound up borrowing this code (posted my current attempt at bottom of the post) to extract differences between two worksheets. While it returns the basic information, it is less QoL change than my current method, which, while it works most of the time, still fails. The current method is as follows:
=IF(COUNTIFS(New!$H:$H, Old!$H2, New!$C:$C, Old!$C2,New!$B:$B, Old!$B2)<1, Old!$H2, "")
This code spans across several columns to populate the appropriate information (appointment time, date, patient name, patient ID, notes, etc). This goes on a sheet called "Removed", and I have one for "Added" where New and Old are reversed.
I attempted to modify the borrowed code to paste entire rows instead of just one column, but I seem to be failing at every turn, mainly because I am new to VBA and do not have a full grasp of it yet. Changing the first For loop to:
For i = LBound(valsM, 1) To UBound(valsM, 1)
If IsError(Application.Match(valsM(i, 1), valsQ, 0)) Then
mm = mm + 1
Worksheets("Old").Cells(i).EntireRow.Copy Destination:= Worksheets("New").Cells(mm, 1)
End If
Next i
is obviously the incorrect way, and I suspect it's due to the whole thing being based on arrays. What must I change in the script to accommodate 16 columns of information that must be moved over to appropriate pages? Bonus would be putting them all on one page and appending a 17th column Q that indicates removed or added. Appreciate the help.
Sub YouSuckAtVBA()
Dim i As Long, mm As Long
Dim valsM As Variant, valsQ As Variant, valsMM As Variant
With Worksheets("New")
valsM = .Range(.Cells(1, "A"), .Cells(.Rows.Count, "A").End(xlUp)).Value2
End With
With Worksheets("Old")
valsQ = .Range(.Cells(1, "H"), .Cells(.Rows.Count, "H").End(xlUp)).Value2
End With
ReDim valsMM(1 To (UBound(valsM, 1) + UBound(valsQ, 1)), 1 To 2)
mm = 1
valsMM(mm, 1) = "value"
valsMM(mm, 2) = "missing from"
For i = LBound(valsM, 1) To UBound(valsM, 1)
If IsError(Application.Match(valsM(i, 1), valsQ, 0)) Then
mm = mm + 1
Worksheets("Old").Cells(i).EntireRow.Copy Destination:=Worksheets("New").Cells(mm, 1)
End If
Next i
For i = LBound(valsQ, 1) To UBound(valsQ, 1)
If IsError(Application.Match(valsQ(i, 1), valsM, 0)) Then
mm = mm + 1
Worksheets("New").Cells(i).EntireRow.Copy Destination:=Worksheets("Old").Cells(mm, 1)
End If
Next i
valsMM = helperResizeArray(valsMM, mm)
With Worksheets("Test")
With .Cells(.Rows.Count, "A").End(xlUp).Offset(1, 0)
.Resize(UBound(valsMM, 1), UBound(valsMM, 2)) = valsMM
End With
End With
End Sub
Function helperResizeArray(vals As Variant, x As Long)
Dim arr As Variant, i As Long
ReDim arr(1 To x, 1 To 2)
For i = LBound(arr, 1) To UBound(arr, 1)
arr(i, 1) = vals(i, 1)
arr(i, 2) = vals(i, 2)
Next i
helperResizeArray = arr
End Function
If you have Office 365 you can use the new Filter-Function
The screenshot shows the formulas using a very basic example.
"Table old" and "Table new" are created via "Insert > Table" therefore it is possible to reference the column names within the formula instead of B or D
Related
There is a report in Oracle that I dump into Excel but it has terrible formatting. There are thousands of individual accounts and each account has one or more lines of data. I need to do intensive data manipulation on the report to whip it into shape such that there is any utility to it whatsoever. This is the way the report is laid out (the "Rows of data" lines vary for each record).
Header1
Header2
Row of data
Row of data
Row of data
----------------------
summary data
summary data
The problem is that these records can not be filtered by the headers because the data is stacked vertically. So, after I have stripped out all the extraneous rows that I don't want, leaving a delimiting blank row between each record (a primitive method of enabling exiting the inner loop), I run a very simple VBA routine I created. For each 'Row of Data', the headers print to a column to the right on the same row.
I segment the data into two or more sets because there might be over 60K lines and this is to prevent a run time error "6".
Row of data Header1 Header2
Row of data Header1 Header2
Row of data Header1 Header2
----------------------
summary data
summary data
The following routine used to run at lightening speed - less than 30 seconds, now after a change from Office 2016 to Office 365 desktop, the same routine runs painfully slow. It can take a half hour to run one segment. I am baffled. Can someone tell me what might be causing this and what I can change with this routine to make it run faster?
Sub UnifyRowData()
Dim i As Integer
Dim TtlRows As Integer
TtlRows = Cells(Rows.Count, 1).End(xlUp).Row
i = 0
Do Until i > TtlRows
i = i + 1
Heading1 = Cells(i, 1)
Heading2 = Cells(i + 1, 1)
Heading3 = Cells(i + 2, 1)
Do
Cells(i, 3).Value = Heading1
Cells(i, 4).Value = Heading2
Cells(i, 5).Value = Heading3
i = i + 1
Loop Until IsEmpty(Cells(i, 1))
Loop
End Sub
`
I don't know what I can change. I've read about screen updating causing long run times but someone would need to explain to me why that would slow down this routine that used to run at lightening speed.
This should be faster, using an array to work on the data:
Sub UnifyRowData()
Dim i As Long, ws As Worksheet, h1, h2, h3
Dim TtlRows As Long, arr, rngData As Range
Set ws = ActiveSheet
'get the data range (including additional columns to be populated)
Set rngData = ws.Range("A1", ws.Cells(ws.Rows.Count, 1).End(xlUp)).Resize(, 5)
arr = rngData.Value 'read as array
TtlRows = UBound(arr, 1) '# of rows of data
i = 0
Do While i <= TtlRows
i = i + 1
h1 = arr(i, 1)
h2 = arr(i + 1, 1)
h3 = arr(i + 2, 1)
Do
arr(i, 3) = h1
arr(i, 4) = h2
arr(i, 5) = h3
i = i + 1
If i > TtlRows Then Goto done 'exit loops if at end of array
Loop Until Len(arr(i, 1)) = 0
Loop
done:
rngData.Value = arr 'write the array back to the sheet
End Sub
I have a worksheet, and I want to be able to go through one column (O) to find the different values, then go to another column (U) and count whether the sting is paper or electronic. Then, I want to be able to take the total of paper/electronic stings from U with each instance in O (source) and put it into the following table on a different sheet with VBA.
Due to the sensitivity of the data, I quickly made a table with basically what I mean. Pretend A is O and B is U.
And I want the output in this table, or if there is a better way to present the data:
I've tried making a pivot table, but it simply counts each instance of the paper/electronic string in the sheet, and I need to cross reference the values in O with U.
Here is the formula what you desire. Remember that we need to change source value and Fillining medium value in each row. you can see from the image that in formula for Source A values are "A" and "Paper" for paper count and "A" and "Electronic" for electronic count. the formulas for Source A are written at the bottom of the table and formula for Source C you can See from formula Bar. This is to show you the change you need to make in formula for each source.
if you have excel 365 you can just use the unique/countifs function. For simplicity I assume your data is in col A & B
To get the unique values (source) col E:
=UNIQUE(A:A)
To count (manually add "paper" as header in col F:
=COUNTIFS(A:A;E2;B:B;$F$1)
Do the same for the other values.
EDIT:
Anything can be done in code:
Option Explicit
Sub DictUniqueFinal()
Dim arr, arr2, arrH, j As Long, dict As Object, id As String
'setup some arrays
arrH = Split("Source, Paper, Electronic", ",")
arr = Sheet1.Range("A1").CurrentRegion.Offset(1, 0).Value2 'load source without headers
ReDim arr2(1 To UBound(arr), 1 To 3)
'setup the dict
Set dict = CreateObject("Scripting.Dictionary") 'create dictionary lateB
For j = 1 To UBound(arr) - 1 'traverse source
id = arr(j, 1)
If Not dict.Exists(id) Then 'create key
If arr(j, 2) = "paper" Then
dict.Add id, 1 & "," & 0
Else
dict.Add id, 0 & "," & 1
End If
Else 'update key
If arr(j, 2) = "paper" Then
dict(id) = Split(dict(id), ",")(0) + 1 & "," & Split(dict(id), ",")(1)
Else
dict(id) = Split(dict(id), ",")(0) & "," & Split(dict(id), ",")(1) + 1
End If
End If
Next j
'build final array
ReDim arr2(0 To dict.Count - 1, 1 To 3)
For j = 0 To dict.Count - 1
arr2(j, 1) = dict.Keys()(j)
arr2(j, 2) = Split(dict.Items()(j), ",")(0)
arr2(j, 3) = Split(dict.Items()(j), ",")(1)
Next j
'dump to sheet
With Sheet2
.Range(.Cells(1, 1), .Cells(1, UBound(arrH) + 1)).Value2 = arrH
.Range(.Cells(2, 1), .Cells(UBound(arr2) + 2, UBound(arr2, 2))).Value2 = arr2
End With
End Sub
It's a bit long and I had to hard code, but I found a solution, thanks to #AnmolKumar I looked in to Countif and found this:
ws2.Range("F15").Value2 = Excel.WorksheetFunction.CountIfs, _
(ws3.Range("O1:O" & lstRow2), "A", ws3.Range("U1:U" & lstRow2), "Paper")
I'll just have to do it for each different section
My code need more than one hours to complete for 3500 rows but I need to work for more than 40000 rows data.
I am looking for alternatives to my code by using dictionary, with improved performance within the context of interest.
Could anyone help me?
Sub StripRow2Node()
'Read the Strip Design table
With Sheets("Design-Moment")
Sheets("Design-Moment").Activate
LastR1 = .Range("B" & Cells.Rows.Count).End(xlUp).Row
DM_arr = .Range(Cells(1, 1), Cells(LastR1, 7)) 'Col 1 to Col 7
DM_count = UBound(DM_arr, 1)
End With
'Read the x and y coordinations and thickness of a node in node design
With Sheets("Design-Shear")
Sheets("Design-Shear").Activate
LastR2 = .Range("B" & Cells.Rows.Count).End(xlUp).Row
DS_arr = .Range(Cells(1, 4), Cells(LastR2, 5)) 'Col 4 to Col 5
SX_arr = .Range(Cells(1, 26), Cells(LastR2, 27))
SY_arr = .Range(Cells(1, 30), Cells(LastR2, 31))
DS_count = UBound(DS_arr, 1)
End With
'** Find correponding reference row in Design-Moment for nodes**
'Match node to striip station and output row index
For i = 5 To DS_count
XStrip = SX_arr(i, 1)
XStation = DS_arr(i, 1)
YStrip = SY_arr(i, 1)
YStation = DS_arr(i, 2)
For j = 5 To DM_count
If DM_arr(j, 1) = XStrip Then 'X-Strip Name is matched
If DM_arr(j, 4) >= XStation And DM_arr(j - 1, 4) < XStation Then
SX_arr(i, 2) = j 'matched row reference for X-strip
End If
End If
If DM_arr(j, 1) = YStrip Then
If DM_arr(j, 5) <= YStation And DM_arr(j - 1, 5) > YStation Then
SY_arr(i, 2) = j
End If
End If
Next j
Next i
'Write the matched strip information to node
For i = 5 To LastR2
With Sheets("Design-Shear")
.Cells(i, 27) = SX_arr(i, 2)
.Cells(i, 31) = SY_arr(i, 2)
End With
Next i
End Sub
I suspect that almost all the time is being used writing back cell-by-cell to the sheet here:
'Write the matched strip information to node
For i = 5 To LastR2
With Sheets("Design-Shear")
.Cells(i, 27) = SX_arr(i, 2)
.Cells(i, 31) = SY_arr(i, 2)
End With
Next i
Writing back to Excel is much slower than reading from Excel.
I would suggest switching off screen updating and calculation, accumulating the results (currently X_arr(i, 2) and SY_arr(i, 2)) in separate arrays and then writing the arrays back to a range in a single operation rather than cell-by-cell
There are several points to improve:
1. Use qualified references to avoid.activate statements
You start off nicely with
With Sheets("Design-Shear")
...
DS_arr = .Range(Cells(1, 4), Cells(LastR2, 5)) 'Col 4 to Col 5
but fail to make the Cells objects refer to the With block. Instead use
With Sheets("Design-Shear")
...
DS_arr = .Range(.Cells(1, 4), .Cells(LastR2, 5)) 'Col 4 to Col 5
Now you do not have to activate the sheet anymore.
From the code I have to assume that there is only one possible match returned in this statement:
SX_arr(i, 2) = j
for all i; otherwise, the second, third...occurrence would overwrite this value of j. If that is indeed the case you can stop looping over j once a match is found:
SX_arr(i, 2) = j 'matched row reference for X-strip
Exit For
Shortcut both If statements if DM_arr(j, 1) can match XStrip or YStrip. If these matches are mutually exclusive, use ElseIf instead of If for the second statement.
Shortcutting the j-loop should improve the runtime noticeably. Of course, if you need the last matching index (instead of the first) then this will not apply.
edit:
For a dictionary solution, see for instance the excellent code from Jeeped here: https://codereview.stackexchange.com/questions/133664/searching-values-of-range-x-in-range-y
How can I set up a macro that will strip the letters from #####XX in column I and put them in to column L same row? Thanks!
Assuming you're working with the first sheet and you're always stripping off the last two characters while leaving the first 5 characters, the following code will work:
Public Sub StripOff()
Dim iRow as Integer
iRow = 2 'Assuming row 1 is headers, else make this 1
While Sheets(1).Range("I" + Cstr(iRow)).Value <> ""
Sheets(1).Range("L" + CStr(iRow)).Value = Right(Sheets(1).Range("I" + Cstr(iRow)).Value, 2)
Sheets(1).Range("I" + Cstr(iRow)).Value = Left(Sheets(1).Range("I" + Cstr(iRow)).Value, 5)
iRow = iRow + 1
Wend
End Sub
The operative words I'm understanding from your question are Cutting and strip. To my mind, this means that the last two letters are permanently removed from column I and placed in column L.
Sub cut2right()
Dim v As Long, vPFXS As Variant, vSFXS As Variant
With Worksheets("Sheet6")
vPFXS = .Range(.Cells(2, "I"), .Cells(Rows.Count, "I").End(xlUp))
ReDim vSFXS(1 To UBound(vPFXS), 1 To 1)
For v = LBound(vPFXS, 1) To UBound(vPFXS, 1)
If Len(vPFXS(v, 1)) > 1 Then
vSFXS(v, 1) = Right(vPFXS(v, 1), 2)
vPFXS(v, 1) = Left(vPFXS(v, 1), Len(vPFXS(v, 1)) - 2)
End If
Next v
.Cells(2, "I").Resize(UBound(vPFXS, 1), 1) = vPFXS
.Cells(2, "L").Resize(UBound(vPFXS, 1), 1) = vSFXS
End With
End Sub
Working with variant arrays should speed up working with many cells with variable length string values. If they were all the same length then manually running a Text-to-Columns command with a fixed length to an unused column and then copying and pasting the results to the appropriate column would have done just fine.
You can get the leading numeric characters from a string using the VBA Val function. To use this function on a worksheet you will need to create a User Defined Function (UDF) in a standard VBA module.
Function LeadingNumbers(Str As String) As Double
LeadingNumbers = Val(Str)
End Function
Simply enter the function in a cell and reference the cell containing the string you want "cleaned".
I am building a 5-year history where I copy two columns of data from 5 different worksheets. The column headings are Agency and Amount Billed Year xx. Each year does not have the same amount of data; for example, if I build the first year and then add another year there can be some Agencies not in the first year or some Agencies in the first year but not in the second year and so forth as I continue to add all 5 years. I adjusted each column manually to line up the agency name and it was a grueling task. I tried to find code that would automate the process but all I found were comparing two columns. I want to start with two columns, add two more columns, add two more columns, and so on and then run code to line up data based on agency name with corresponding amount billed Year xx.
I hope I am clear in my request. I appreciate any code that I can alter to accomplish this task.
You add one column in each worksheet, calling it Year. Fill that in with the year, and then you put the 5 worksheet data in the same sheet, just pasting each worksheets data below the prevoius one. Then you make a simple pivot table out of it.
Thank you for the suggestion. I used code instead and it works great. Here is the code:
Sub test()
Dim a, i As Long, ii As Long, txt As String, w, x
With Cells(1).CurrentRegion.Offset(1)
a = .Value: .Borders.LineStyle = xlNone
With CreateObject("System.Collections.SortedList")
For i = 1 To UBound(a, 1)
For ii = 1 To UBound(a, 2) Step 2
If a(i, ii) <> "" Then
txt = LCase(a(i, ii))
If Not .Contains(txt) Then
ReDim w(1 To UBound(a, 2))
.Item(txt) = w
Else
w = .Item(txt)
End If
w(ii) = a(i, ii): w(ii + 1) = a(i, ii + 1)
.Item(txt) = w
End If
Next
Next
Set x = .Clone
End With
For i = 0 To x.Count - 1
.Cells(i + 1, 1).Resize(, .Columns.Count).Value = x.GetByIndex(i)
Next
End With
With Cells(1).CurrentRegion
For i = 2 To .Columns.Count Step 2
.Columns(i).Borders(10).Weight = 2
Next
End With
End Sub