how do I remove duplication of objects in a Collection? This is what I tried:
dim unique_students as Collection
dim no_duplicate_student as cls_Student
dim no_duplication as boolean
For Each student as cls_Student In list_Student 'list_Students = original unsorted collection
no_duplication = True
Dim s As cls_Student
For Each s In unique_students
If s.name = student.name Then
no_duplication = False 'Duplication found
Exit For
End If
next s
If no_duplication Then
'Inserted into new sorted collection if no values matches that of the sorted collection
Set no_duplicate_student = New clsOverlap
no_duplicate_student.name = student.name
unique_students.Add no_duplicate_student
End If
Next student
This however still takes a long time (if list_Student.Count > 5000, then it'll take 30min+ to run). Is there a more efficient way (if possible, decrease time complexity) of removing duplication in a Collection?
Add the student names to a dictionary, which has the .Exists method to check if an item is already in the dictionary.
You can get some ideas from CollectionToDictionary in Collection And Dictionary Procedures
Something like this in your For Each student loop:
If Dict.Exists(Key:=student.name) Then
' is duplicate!
Else
Dict.Add Key:=student.name, Item:=student.name
' you could also do Item:=student if you want the de-duplicated list in a dictionary
End If
The inner loop is not needed. The function will probably run almost instantaneously.
I usually use a dictionary like what Andre451 suggested. Alternatively you could use an ArrayList like this. I'm not sure if there's much of a performance difference between the two but this method also produces a sorted list if that's desirable. The dictionary though can carry key/value pairs, so it just depends on what you're going after.
Sub Demo()
Set AL = CreateObject("System.Collections.ArrayList")
AL.Add "A"
AL.Add "B"
AL.Add "A"
AL.Add "A"
AL.Add "C"
'Sorting allows sequential comparisons to determine uniqueness
'You could also do something similar to the dictionary method with ArrayList.Contains
'but the evluation of ArrayList.Contains runs slower than this
AL.Sort
For i = 0 To AL.Count - 2
If AL(i) <> AL(i + 1) Then
'Prints unique values
Debug.Print AL(i)
End If
Next
If AL(i) <> AL(i - 1) Then
'Prints last value if unique by comparing to one before it
Debug.Print AL(i)
End If
End Sub
Edit: After testing I confirmed that the dictionary method is about twice as fast at 7.7 seconds versus 13 seconds per million. However, at the OP count of 5000 the difference is only 40 vs 80 ms.
Testing Code Here...
Public Declare Function GetTickCount Lib "kernel32.dll" () As Long
Sub DictionaryDemo()
Set D = CreateObject("Scripting.Dictionary")
Set AL = CreateObject("System.Collections.ArrayList")
For i = 0 To 10 ^ 6
AL.Add Round(Rnd * 10, 0)
Next
Start = GetTickCount
For i = 0 To AL.Count - 1
If Not (D.Exists(AL(i))) Then
D.Add AL(i), ""
Debug.Print AL(i)
End If
Next
Debug.Print GetTickCount - Start
End Sub
Sub ArrayListDemo()
Set AL = CreateObject("System.Collections.ArrayList")
For i = 0 To 10 ^ 6
AL.Add Round(Rnd * 10, 0)
Next
'Sorting allows sequential comparisons to determine uniqueness
Start = GetTickCount
AL.Sort
For i = 0 To AL.Count - 2
If AL(i) <> AL(i + 1) Then
'Prints unique values
Debug.Print AL(i)
End If
Next
If AL(i) <> AL(i - 1) Then
'Prints last value if unique by comparing to one before it
Debug.Print AL(i)
End If
Debug.Print GetTickCount - Start
End Sub
Edit Again: Ok, so I find this very interesting. What's most important appears to be the actual type itself. So for example, the testing above creates an ArrayList from which unique values are to be derived. If this is changed to a basic integer array Dim AL(10 ^ 6) As Integer, then the time is slashed from 7.7 to 0.8 seconds. Likewise, the ArrayList method can be slashed from 13 seconds down to 0.5 seconds simply by adding the line A = AL.ToArray after the sorting operation and looping through the array A instead.
This makes sense, since the memory allocation for arrays allows them to be processed really quick. It's also why some people prefer to create their own sorting and uniqueness algorithms rather than go with a less efficient but easy to use method employing a Dictionary or ArrayList as originally suggested here. The dictionary and ArrayLists are still powerful tools and, as mentioned above, they can still extract unique values from a 1 million length in a fraction of a second, but it's worth noting that when it comes to raw efficiency a simple array is wicked fast at looping.
The code below will extract unique values from a 1 million length array in about 0.3 seconds. It's not much different than the OP, but it's a lot more efficient. This is because looping through a collection is horribly slow, not because there was anything inefficient with the basic strategy. Also, notice that the efficiency will decrease as the number of unique values increases (this test only used the 10 unique of 1-10).
Sub ArrayDemo()
Dim A(10 ^ 6) As Integer
Dim B(10) As Integer
For i = 0 To 10 ^ 6
A(i) = Round(Rnd * 10, 0)
Next
Start = GetTickCount
k = 0
For i = 0 To 10 ^ 6
For j = 0 To k
If B(j) = A(i) Then GoTo nxt
Next
B(k) = A(i)
Debug.Print B(k)
k = k + 1
nxt:
Next
Debug.Print GetTickCount - Start
End Sub
Related
I have a table named 'odonto' and it has the fields code (autoincremental), history, surnames and names. I need to generate the code so that it autogenerates the HISTORY obtaining the first letter of the last name which will then have to be concatenated with consecutive numbers for each letter. That is to say that if we have four "FLORES" and a "MENDOZA" in the register it shows in a text box the next samples:
F001
F002
F003
F004
M001
...
Also I need to keep in mind that if a record is deleted it will be replaced by incrementing it again.
I did it and it functions for the asigning value, but it doesn't replace the deleted one if it.
Private Sub APELLIDO_AfterUpdate()
Dim MyStr
MyStr = Left([APELLIDO], 1)
Me.LETRA = MyStr
If IsNull(Me.HISTORIA) Then
Me!HISTORIA = ((MyStr) & "0000" & ([Cant] + 1))
Else
HISTORIA = Me.HISTORIA
End If
Me.Refresh
End Sub
Please your help.
I need help at Auto-Incrementing a letter.
A description field in Table1 has values like: B39
This Table1 Record, has related records in Table2:
B39_a
B39_b
B39_c
B39_d
All I want to do is that the description in Table2 automatically takes the record at table1 and adds the specific letter. It always starts with "a" and never reaches the full alphabet.
I already tried some code from this site: http://www.freevbcode.com/ShowCode.asp?ID=5440
Function IncrementString(ByVal strString As String) As String
'
' Increments a string counter
' e.g. "a" -> "b"
' "az" -> "ba"
' "zzz" -> "aaaa"
'
' strString is the string to increment, assumed to be lower-case alphabetic
' Return value is the incremented string
'
Dim lngLenString As Long
Dim strChar As String
Dim lngI As Long
lngLenString = Len(strString)
' Start at far right
For lngI = lngLenString To 0 Step -1
' If we reach the far left then add an A and exit
If lngI = 0 Then
strString = "a" & strString
Exit For
End If
' Consider next character
strChar = Mid(strString, lngI, 1)
If strChar = "z" Then
' If we find Z then increment this to A
' and increment the character after this (in next loop iteration)
strString = Left$(strString, lngI - 1) & "a" & Mid(strString, lngI + 1, lngLenString)
Else
' Increment this non-Z and exit
strString = Left$(strString, lngI - 1) & Chr(Asc(strChar) + 1) & Mid(strString, lngI + 1, lngLenString)
Exit For
End If
Next lngI
IncrementString = strString
Exit Function
End Function
Apparently it is not working like it should. It increases the letter, but twice! (i , i , j , j , etc.)
Description textbox (for Table2 Record ) has as default value:
=IncrementString(DLast("[SeqNo]","[table2]"))
But like I said it increases the number by doing it double. I also have to start the process manually by entering an "a".
Neither the function nor the calling code presently allows for the "A##_" prefix. If you really MUST save this prefix to Table2, code would have to be adjusted to deal with it. As is, suggest not saving the "A##" group identifier as a prefix in Table2. Use a query that joins tables on PK/FK fields to retrieve related data for export.
The DLast() search must account for the "A##" group identifier because the sequence is repeated for each group.
Unfortunately, trying to set a DefaultValue property with a dynamic parameter dependent on main form ID is impractical. For one thing, subform loads before main form so the default value cannot be built since the main form data and controls are not available. Also, when the main form is moved to a new record, again there is no data for the default value to build with. The result is error displays for the control on new record row.
Use PK/FK fields for the search.
Code in subform Current event to call your incrementing function:
If Me.NewRecord And Not IsNull(Me.Parent.ReferenzNR) Then
Me!SerienBezeichnung = IncrementString(Nz(DLast("SerienBezeichnung", "tbl_GrundminenSerie", "ID_FK=" & Me.Parent.ReferenzID), ""))
End If
Be aware that DLast(), even though working now, could eventually fail because records do not have inherent order. An alternative would likely involve a recordset or nested domain aggregate. Example tested in VBA Immediate Window:
?DMax("SerienBezeichnung","tbl_GrundminenSerie","ID_FK=5 AND Len([SerienBezeichnung])=" & DMax("Len([SerienBezeichnung])","tbl_GrundminenSerie","ID_FK=5"))
Or if you feel autonumber PK can be depended on to always be increasing (which has always been my observation although there is no guarantee with autonumber):
?DLookup("SerienBezeichnung","tbl_GrundminenSerie","ID_FK=5 AND SerienID=" & DMax("SerienID","tbl_GrundminenSerie","ID_FK=5"))
Consider the following VBA function:
Function IncAlpha(ByVal strA As String, ByVal lngI As Long) As String
If lngI <= 0 Then
IncAlpha = strA
ElseIf strA = vbNullString Then
IncAlpha = IncAlpha("a", lngI - 1)
Else
lngI = lngI + Asc(Right(strA, 1)) - 97
IncAlpha = IncAlpha(Left(strA, Len(strA) - 1), lngI \ 26) & Chr(97 + lngI Mod 26)
End If
End Function
Supplied with a lowercase alphabetical string, this recursive function will increment the string by the supplied long integer argument, with z incrementing to aa, az incrementing to ba and so on.
Supplied with an empty string (""), the above function will return a.
?IncAlpha("", 1)
a
?IncAlpha("", 26)
z
?IncAlpha("", 27)
aa
?IncAlpha("", 42)
ap
?IncAlpha("", 314159)
qvsa
With this function, the suffix may therefore be calculated using:
<prefix> & IncAlpha("", DCount("[SeqNo]","[table2]") + 1)
Or to account for multiple prefixes:
<prefix> & IncAlpha("", DCount("SeqNo","table2","SeqNo like '" & <prefix> & "*'") + 1)
I am trying to create a function which, after having search the entire active worksheet, will return the total number of cells that contain a certain string. A lot like how the "x cell(s) found" within Find and Replace works.
I have this so far:
Function FINDIST(stringToFind)
Dim counter As Integer: counter = 0
For Each Cell In ActiveSheet.UsedRange.Cells
If InStr (Cell, stringToFind) > 0
Then counter = counter + 1
End If
Next
End Function
Another way of doing this:
Function FINDIST(stringToFind) As Long
FINDIST = Evaluate("SUM(IFERROR(SEARCH(" & Chr(34) _
& "*" & stringToFind & "*" & Chr(34) & "," _
& ActiveSheet.UsedRange.Address & ",1),0))")
End Function
This searches for stringToFind in every cell in used range, and returns an array with a 1 if that string is found in a cell and error if it is not found. The error is casted to zero with the IFERROR part, and the SUM sums the resulting binary array.
This will count only once the occurrence of stringToFind within each cell, even if it occurs more than once, but looking at your code I assume that this is what you are looking for.
I hope it helps!
UPDATE
Out of curiosity, I did some testing to see how the two approaches compare (read from range directly vs using evaluate). Here is the code I used:
Option Explicit
Private Declare Function GetTickCount Lib "kernel32.dll" () As Long
Sub test()
Dim ticks As Long
Range("A1:AA100000").Value = "adlrkjgalbabyajglakrjg"
ticks = GetTickCount
FINDIST1 ("baby")
Debug.Print "Read from range: ", GetTickCount - ticks
ticks = GetTickCount
FINDIST ("baby")
Debug.Print "Evaluate: ", GetTickCount - ticks
End Sub
Function FINDIST(stringToFind) As Long
FINDIST = Evaluate("SUM(IFERROR(SEARCH(" & Chr(34) _
& "*" & stringToFind & "*" & Chr(34) & "," _
& ActiveSheet.UsedRange.Address & ",1),0))")
End Function
Function FINDIST1(stringToFind) As Long
Dim counter As Long: counter = 0
Dim c As Range
Dim firstAddress As String
With ActiveSheet.UsedRange
Set c = .Find(stringToFind, LookIn:=xlValues, LookAt:=xlPart)
If Not c Is Nothing Then
firstAddress = c.Address
Do
counter = counter + 1
Set c = .FindNext(c)
Loop While Not c Is Nothing And c.Address <> firstAddress
End If
End With
FINDIST1 = counter
End Function
UPDATE 2
Chris Nielsen made two very good points in the comments below:
ActiveSheet.Evaluate is faster than Application.Evaluate. The link to the text by Charles Williams in the comment explains this behavior.
Good old Variant array will perform better than any other method.
For completeness, I post the version of the variant array method that I tested:
Function FINDIST_looping(stringToFind) As Long
Dim vContents, lRow As Long, lCol As Long, lCounter As Long
vContents = ActiveSheet.UsedRange.Value2
For lRow = LBound(vContents, 1) To UBound(vContents, 1)
For lCol = LBound(vContents, 2) To UBound(vContents, 2)
lCounter = IIf(InStr(vContents(lRow, lCol), stringToFind), _
lCounter + 1, lCounter)
Next lCol
Next lRow
FINDIST_looping = lCounter
End Function
Doug Glancy made another very good point, namely that COUNTIF can be used instead of SEARCH. This leads to a non-array formula solution and should dominate my original formula, performance-wise.
Here is Doug's formula:
FINDIST_COUNTIF = ActiveSheet.Evaluate("COUNTIF(" _
& ActiveSheet.Cells.Address & "," & Chr(34) & "*" _
& stringToFind & "*" & Chr(34) & ")")
In fact, Doug's point implies that no Evaluate() is necessary. We can call Countif from the WorksheetFunction object. Therefore, if the goal is to call this function from a spreadsheet, there is no need to use Evaluate() or to wrap it up in a UDF - it is a typical COUNTIF application with wildcards.
Results:
Read from range: 247,495 ms (~ 4 mins 7 secs)
Application.Evaluate: 3,261 ms (~ 3.2 secs)
Variant Array: 1,706 ms (~ 1.7 secs)
ActiveSheet.Evaluate: 1,257 ms (~ 1.3 secs)
ActiveSheet.Evaluate (DG): 602 ms (~ 0.6 secs)
WorksheetFunction.CountIf (DG):550 ms (~ 0.55 secs)
It appears that Application.Evaluate is about 75 times faster compared to using Range.Find()(?!) Also, the original code (with Integer changed to Long) runs in ~8 seconds.
Also, it seems that Activesheet.Evaluate is actually faster than the Variant array in this particular case. The difference between calling CountIf as a WorksheetFunction method vs Evaluateing it seems quite small.
CAVEAT: the frequency by which stringToFind is found within the UsedRange might affect the relative performance of the several methods. I ran the Activesheet.Evaluate and Variant Array methods with the above range (A1:AA100000) but with only the ten first cells having the matching string.
Results (average of 6 runs, variance pretty much minimal):
Activesheet.Evaluate: 920 ms (~ 1. sec)
Variant Array: 1654 ms (~ 1.7 secs)
This is interesting - it seems that ActiveSheet.Evaluate in this case has a slightly better performance than variant arrays (unless I have done something horrible in the looping code, in which case please let me know). Also, the Variant method's performance is actually.. invariant with respect to the string's frequency.
runs were made on EXCEL 2010 under Win7.
Working with Tony Dallimore's advice of using Find and also changing your return type to Long.
MSDN article: http://msdn.microsoft.com/en-us/library/office/ff839746(v=office.15).aspx
Function FINDIST(stringToFind) As Long
Dim counter As Long: counter = 0
Dim c As Range
Dim firstAddress As String
With ActiveSheet.UsedRange
Set c = .Find(stringToFind, LookIn:=xlValues, , LookAt:=xlPart)
If Not c Is Nothing Then
firstAddress = c.Address
Do
counter = counter + 1
Set c = .FindNext(c)
Loop While Not c Is Nothing And c.Address <> firstAddress
End If
End With
FINDIST = counter
End Function
Find is generally quicker than a coded equivalent but I've not speed tested against anything else and will be interested to here if it is quick or slow.
My form is hanging for several seconds every time the user goes to a new record. The recordset for a listbox on the form is a query. The form is hanging until that query finishes and the listbox is populated.
My users need to be able to scroll through the records quickly. Currently, the user must wait for the listbox query to finish before moving to the next record. How can I stop the form from hanging?
Is there a way for DoEvents to be used to solve this problem?
Below is my code. I suspect that seeing all this code is not necessary, but I am sharing it all just in case.
I am using Access.
Thanks!
Option Compare Database 'Use database order for string comparisons
Option Explicit
Dim QuoteLogForm As Form
Public KeystrokeCount As Integer
'Define the similarity threshold for the matches list
Const SIMIL_THRESHOLD As Single = 0.83
Private m_strDialogResult As String
'The basis of this code was derived from http://www.accessmvp.com/tomvanstiphout/simil.htm
Private Sub Form_Current()
Matches
End Sub
Private Sub Matches()
'This sub calls the functions necessary to generate a query that lists
'the KFC RFQ #'s whose similarity exceeds the threashold, as defined above.
Dim sql As String
Dim strOpenArgs As String
Dim strInClause As String
'OpenArgs contains the part # to find similars for.
strOpenArgs = Replace(Replace(Nz(Me.Part_Number_Textbox.Value), "-", ""), " ", "") 'Nz changes Nulls to blanks
'Call the GetSimilarPartNos function below.
'This function returns a string of KFC RFQ #'s that exceed the threashold, wrapped in single quotes and separated by commas.
strInClause = GetSimilarPartNos(strOpenArgs)
'If any similar part numbers were found, run a query to select all the listed records
If VBA.Len(strInClause) > 0 Then
'Select records whose KFC RFQ #'s are found in the strInClause list, sort from most to least similar
sql = "select * from [Matches List Query] where [KFC RFQ #] in (" & strInClause & ")" ' order by SimilPct desc, DateShort desc"
'[Forms]![Price Form Parent]![Price Form].[Form].Customer_Filter_Box
Set Me.[Matches List Form].Form.Recordset = CurrentDb.OpenRecordset(sql, dbOpenSnapshot)
Else
'If no similar KFC RFQ #'s were found, select no records
sql = "select * from [Matches List Query] where 1 = 0"
Set Me.[Matches List Form].Form.Recordset = CurrentDb.OpenRecordset(sql, dbOpenSnapshot)
End If
End Sub
Private Function GetSimilarPartNos(ByVal strPartNo As String) As String
'The GetSimilarPartNos function calls the fnSimil function and compiles a list (strInClause)
'of KFC RFQ #'s whose part numbers exceed the threashold
Dim rs As DAO.Recordset
Dim strInClause As String
Dim sngSimil As Single
'Erase all previous values in the [Quote Log].Simil field
CurrentDb.Execute "update [Quote Log] set Simil = 0", dbFailOnError
Set rs = CurrentDb.OpenRecordset("Quote Log") ', dbOpenTable)
'Loop to calculate the similarity of all part numbers
While Not rs.EOF 'Loop until the end
Dim curPartNo As String
curPartNo = Replace(Replace(Nz(rs![Part #]), "-", ""), " ", "")
If rs![KFC RFQ #] = Me.[KFC RFQ #] Then
GoTo 120
End If
sngSimil = fnSimil(curPartNo, strPartNo)
'If the part number similarity value of a single record is greater than the
'threashold (as defined above), add the record's KFC RFQ # to strInClause
'strInClause forms a list of KFC RFQ #'s whose part numbers exceed the threashold
'in similarity, wrapped in single quotes and separated by commas
If sngSimil >= SIMIL_THRESHOLD Then
strInClause = strInClause & "'" & rs![KFC RFQ #] & "',"
'Show the Simil value on this form
rs.Edit
rs!Simil = sngSimil
rs.Update
End If
120 rs.MoveNext
Wend
rs.Close
Set rs = Nothing
'Once the strInClause is completed, remove the last comma from the list
If Len(strInClause) > 0 Then strInClause = VBA.Left$(strInClause, Len(strInClause) - 1)
GetSimilarPartNos = strInClause
End Function
The UI is hanging because the work is being done by the UI thread. If you want (or need) a more responsive application, you need to offload the work to a background thread. As far as I know, for VBA, that is not something for the feint of heart, but you can take a look, VBA + Threads in MS Access.
As access is a database, it suffers from all the drawbacks of any database, mainly finding data stored on slow, usually spinning, media. I suggest you take a look at this article: Create and use an index to improve performance to help you create efficient indexes for your queries, if you have not indexed for them already. You also need to consider the performance implications of WHERE, JOIN, and ORDER BY clauses in your queries. Make sure your indexes are optimized for your queries and your data is stored in a logical fashion for the way it will be queries out. Beyond that, if the database does not reside on the machine from which the queries are being executed, you have network I/O latency on top of expected Disk I/O latency. This can significantly impact the read performance of the database.
I think you might possibly have the wrong form event.
The form_Current event fires between each record and I can't imagine that's what you really need. Try moving your "Matches" routine into the OnLoad event instead.
I receive data monthly from an external company and need to change the field name to a sequential number. example contract 11 15 17 to 1 2 3. I am trying to use the following code but get an error that I cannot define the field more than once at "fld.Name = (n) + 1". How can I correct this?
Function ChangeFieldName()
Dim db As DAO.Database
Dim tbl As DAO.TableDef
Dim fld As DAO.Field
Dim n As Integer
Set db = CurrentDb
Set tbl = db.TableDefs("tdf1")
On Error Resume Next
n = 0
For Each fld In tbl.Fields
fld.Name = (n) + 1
Next fld
Set fld = Nothing
Set tbl = Nothing
Set db = Nothing
End Function
That code attempts to rename each field to n + 1, but since n is never incremented, it actually attempts to rename every field to 1. The following change may do what you want.
n = 1
For Each fld In tbl.Fields
fld.Name = n
n = n + 1
Next fld
However there are some other issues you should consider with that approach. The For Each loops through the fields based on fld.OrdinalPosition. If your numbered field names were not defined in the order you expect, you will have a problem. For example, these fields in OrdinalPostion order: 11; 15; 2. In that case 11 would be renamed to 1, but the code would throw an error when attempting to rename 15 to 2.
Also that code will attempt to rename every field to a number. If the table only contains numbered field names, that may not be a problem. But if the table also contains other field names you wish to preserve, you've got more work to do.
A minor point is that fld.Name is text type. When you attempt to rename a field to a number, Access actually uses the number's string equivalent. That may be fine, but I would prefer to explicitly cast the number to a string myself.
fld.Name = CStr(n)
Finally please reconsider this ...
On Error Resume Next
That instructs Access to silently ignore all errors. I think you should get rid of that and add a proper error handler code block instead.