Determine array size - excel

I currently facing a problem on how to determine the size of array after looping ends. Below is my coding for that particular function.
Function analyse(ByVal work_date As Date, time As String, action As String, decision As Boolean, branch As String) As String
Dim sh As Worksheet
Dim att_time(6) As String
Dim dated(1) As Date
Set sh = Worksheets("shifthr")
lastrec = sh.Range("C" & Rows.Count).End(xlUp).Row
a = 1
For i = 2 To lastrec
dated(a) = work_date
If branch = sh.Cells(i, 1) Then
att_time(a) = time
a = a + 1
End If
Next i
If att_time(a) = 4 Then
ElseIf att_time(a) = 6 Then
End If
End Function
Thanks for you guys helping hands. Appreciate it very much

I currently facing a problem on how to determine the size of array after looping ends.
You have already declared the size of the array in your dim statement. I guess you want to find the number of elements in the array? If yes, then try this (UNTESTED) Also I guess you wanted to keep dated(a) = work_date inside the IF Condition? If not then you don't need to use a to fill the dated(a) = work_date. Also I don't think you need a Function for this as you are not returning any value to analyse. Use a Sub
Sub analyse(ByVal work_date As Date, time As String, _
action As String, decision As Boolean, branch As String)
Dim sh As Worksheet
Dim att_time() As String
Dim dated() As Date
Dim a As Long
Dim boolCheck As Boolean
Set sh = Worksheets("shifthr")
lastrec = sh.Range("C" & sh.Rows.Count).End(xlUp).Row
a = 1
For i = 2 To lastrec
If branch = sh.Cells(i, 1) Then
ReDim Preserve dated(a)
ReDim Preserve att_time(a)
dated(a) = work_date
att_time(a) = time
a = a + 1
boolCheck = True
End If
Next i
If boolCheck = True Then
Debug.Print UBound(dated)
Debug.Print UBound(att_time)
'OR
Debug.Print a - 1
Else
Debug.Print "No Matching records found"
End If
End Sub

Perhaps you could alter your dim statement to dimension your initial array as () and then use redim preserve as you are looping "a" to increase the count - http://msdn.microsoft.com/en-us/library/w8k3cys2.aspx.
If you want to find out the size of the array, you could use ubound(arrayname). http://msdn.microsoft.com/en-us/library/95b8f22f(v=vs.90).aspx

Related

Random pick cycle from an array in VBA

I'm new in VBA. I want to make a random pick cycle like that:
Let's say I have seven elements in an array(1,2,3,4,5,6,7), each time when I pick one element from the array, the total number of elements will decrease by 1. After picking every element, the array will be reset to what I initially defined (1,2,3,4,5,6,7) and do the random pick cycle again.
The result of every cycle should be different.
Is it possible to do that in VBA?
Here's a stateful function that does what you described each time it is called.
Option Base 1
Dim digits, NLeft
Function RemoveDigit() as Integer
Dim Element as Integer
If IsEmpty(digits) or NLeft = 0 Then
digits = array(1,2,3,4,5,6,7)
NLeft = 7
End If
Element = WorksheetFunction.RandBetween(1,NLeft)
RemoveDigit = digits(Element)
digits(Element) = digits(NLeft)
digits(NLeft) = RemoveDigit
NLeft = NLeft - 1
End Function
It uses a well known algorithm to arrange digits in a random order. Basically you choose to swap a random element number with the last element. Then you repeat it on an n - 1 sized array, making it a tail-recursive algorithm (although this implementation of it is not recursive).
Delete this if you want to, but here is a suggestion for a test sub:
Sub TestRemoveDigit()
NLeft = 0
For i = 1 To 7
d = RemoveDigit()
Debug.Print (d)
Next i
End Sub
I think this should do what you're asking for:
Option Explicit
Global vCurrentArray As Variant
Sub ResetArray()
vCurrentArray = Array(1, 2, 3, 4, 5, 6, 7)
End Sub
Sub RemoveElementWithIndex(lIndex As Long)
Dim vTemp() As Variant '* Change the type as needed
Dim lLBound As Long: lLBound = LBound(vCurrentArray)
Dim lUBound As Long: lUBound = UBound(vCurrentArray)
Dim i As Long, v As Variant
Dim blSkipped As Boolean
If lLBound = lUBound Then '* only 1 element
Call ResetArray
Else
ReDim vTemp(lLBound To lUBound - 1)
i = lLBound
For Each v In vCurrentArray
If i <> lIndex Or blSkipped Then
vTemp(i) = v
i = i + 1
Else
blSkipped = True
End If
Next v
vCurrentArray = vTemp
End If
End Sub
Function GetRandomElement() As Variant '* Change the type as needed
Dim lRandomIndex As Long
lRandomIndex = WorksheetFunction.RandBetween(LBound(vCurrentArray), UBound(vCurrentArray))
GetRandomElement = vCurrentArray(lRandomIndex)
RemoveElementWithIndex lRandomIndex
End Function
Sub TestCycles()
Dim lCycle As Long
Dim i As Long
ResetArray
For lCycle = 1 To 3
Debug.Print
For i = 1 To 7
Debug.Print "Cycle: " & lCycle, "i: " & i, "Random Elem: " & GetRandomElement
Next i
Next lCycle
End Sub
Note: There're many ways of achieving the end result. The above is almost a literal translation of your post.
We can not remove a random element from an array. We can redim array to remove last element(s). If you want to remove random element, you can use collection instead like ..
Option Explicit
Sub RemoveRandom()
Dim coll As Collection, cl As Variant, i As Long, j As Long
Set coll = New Collection
For Each cl In Range("A1:A7")
coll.Add cl.Value
Next cl
For j = 1 To coll.Count
i = WorksheetFunction.RandBetween(1, coll.Count)
Debug.Print coll(i)
coll.Remove (i)
Next j
End Sub

Appending to an array while looping

I've got the following:
Dim dupArray As Variant
dupArray = Array("dup")
For j = LBound(numArray) To UBound(numArray)
If IsInArray(Range(numArray(j)).Text, dupArray) Then
Range(numArray(j)).Interior.ColorIndex = 3
MsgBox "Duplicate value"
ElseIf IsInArray(Range(numArray(j)).Text, keyArray) Then
Application.OnKey Range(numArray(j)).Text, comArray(j)
ReDim Preserve dupArray(j)
Dim cellEntry As String
cellEntry = Range(numArray(j)).Text
MsgBox cellEntry
dupArray(j) = cellEntry
Else
Range(numArray(j)).Interior.ColorIndex = 3
MsgBox "Unrecognized key code in " & numArray(j)
End If
Next j
In essence, I want VBA to read the contents of a cell and append them to an array for comparison. Code to read the contents of a cell is Range(numArray(j)).Text... For some reason, cellEntry and dupArray(j) are not equal. More specifically, for the cell A6, cellEntry is "b" (which is the correct contents), but dupArray(j) is "A6"... any thoughts? There's no error code, it's just not putting the correct value in the array.
Thank you!
(Edit) Code for Function IsInArray:
Function IsInArray(stringToBeFound As String, arr As Variant) As Boolean
IsInArray = Not IsError(Application.Match(stringToBeFound, arr, 0))
End Function
(Edit 2) Don't pay attention to much else... I'm just wondering why cellEntry doesn't match dupArray(j) for all values of j when they should clearly be the same thing.
Your code seems to work, but it depends on keyArray being populated.
I've run this demo code, including populating numArray and keyArray with test values, to illustrate what happens.
If how I've populated these arrays doesn't match your code, please add that info to your Q.
Sub Demo()
Dim dupArray As Variant
Dim numArray As Variant
Dim keyArray As Variant
Dim comArray As Variant
Dim j As Long
' for testing
numArray = Application.Transpose([A1:A6].Value)
ReDim keyArray(1 To 3)
keyArray(1) = "x"
keyArray(2) = "a"
keyArray(3) = "s"
dupArray = Array("dup")
For j = LBound(numArray) To UBound(numArray)
If IsInArray(Range(numArray(j)).Text, dupArray) Then
Range(numArray(j)).Interior.ColorIndex = 3
'MsgBox "Duplicate value"
ElseIf IsInArray(Range(numArray(j)).Text, keyArray) Then
'Application.OnKey Range(numArray(j)).Text, comArray(j)
ReDim Preserve dupArray(j)
Dim cellEntry As String
cellEntry = Range(numArray(j)).Text
'MsgBox cellEntry
dupArray(j) = cellEntry
Else
Range(numArray(j)).Interior.ColorIndex = 4 ' changed to be distinct for testing
'MsgBox "Unrecognized key code in " & numArray(j)
End If
Next j
End Sub
Sheet, before
Sheet, after
Variable values at end of execution
As you can see, dupArray has been populated sparsley, in line with numArray. This is fine for how it's used with IsInArray. If it's used for something else too, you can change how it's populated.

resetting variables in excel vba

I am trying to build a model with VBA doing some work for me. Assume I have 4 variables - unit, lease start date, lease p.a, and alternative lease p.a. There are more, but that does not matter for my problem. VBA loops through each line and gets the value for unit i in respective column.
So, logically, I could declare unit as string, lease start date as date, lease p.a as single, and alternative lease p.a. as single. The problem I have is that I need to distinguish between empty entry and 0. The default numerical value is going to be 0. the distinction between 0 and empty is crucial. The only way I found to get around this is to declare everything as Variant and then check if the corresponding range is empty. If it is empty, then lease the Variant default value (Empty), otherwise assign the value.
I have a feeling that this is going to seriously affect my code performance. Ultimately, there will be lots of variables and I want to refer to those variables in the code. Like, if isempty(AltLease) = true then do one thing, otherwise something else.
I also find that I can not empty single or date variables(date is actually not a problem, since it drops to 1900). Can anyone suggest something?
Here is the code:
Dim tUnitName As Variant
Dim tNumberOfUnits As Variant
Dim tLeaseCurLeaseLengthDef as Variant
Dim tLeaseCurLeaseLengthAlt as Variant
Sub tenancyScheduleNew()
Dim lRow As Long
Dim i As Long
lRow = Sheet2.Cells(Rows.Count, 2).End(xlUp).Row
For i = 3 To lRow
reAssignVariables i
Next i
End Sub
Sub reAssignVariables(i As Long)
tAssetName = checkIfEmpty(i, getColumn("Sheet4", "tAssetName", 3))
tNumberOfUnits = checkIfEmpty(i, getColumn("Sheet4", "tNumberOfUnits", 3))
tLeaseCurLeaseLengthDef = checkIfEmpty(i, getColumn("Sheet4", "tLeaseCurLeaseLengthDef", 3))
tLeaseCurLeaseLengthDef = checkIfEmpty(i, getColumn("Sheet4", "tLeaseCurLeaseLengthAlt", 3))
End Sub
Function getColumn(sh As String, wh As String, colNo As Long) As Long
Dim refSheet As Worksheet
Dim rFound As Range
Set refSheet = Sheets(sh)
With refSheet
Set rFound = .Columns(1).Find(What:=wh, After:=.Cells(1, 1), LookIn:=xlValues, LookAt:=xlWhole)
On Error GoTo 0
If Not rFound Is Nothing Then
getColumn = rFound.Offset(0, colNo - 1).Value
Else
End If
End With
End Function
This is the way I am doing it now, which I think will slow the performance down. This is only small part of variables that I have made - there is going to be much more. I just need to understand how to construct it correctly in the first place. More specifically, if there is a value in tLeaseCurLeaseLengthAlt, then code should use that, alternatively, use default value.
You can't empty a variable of type integer, since empty is not an integer. If you have a variant variable which is currently of subtype integer you can reset it to empty:
Sub test()
Dim v As Variant
Debug.Print TypeName(v)
v = 1
Debug.Print TypeName(v)
v = Empty
Debug.Print TypeName(v)
End Sub
output:
Empty
Integer
Empty
Also, the performance hit of using variants might not be as great as you fear. An informal test:
Sub InformalTest(n As Long)
Dim i As Long, sum1 As Double
Dim j As Variant, sum2 As Variant
Dim start As Double, elapsed1 As Double, elapsed2 As Double
start = Timer
For i = 1 To n
sum1 = sum1 + 1#
Next i
elapsed1 = Timer - start
start = Timer
For j = 1 To n
sum2 = sum2 + 1#
Next j
elapsed2 = Timer - start
Debug.Print "Nonvariant time: " & elapsed1 & ", Nonvariant sum: " & sum1
Debug.Print "Variant time: " & elapsed2 & ", Variant sum: " & sum2
End Sub
Sample output:
InformalTest 1000000
Nonvariant time: 0.060546875, Nonvariant sum: 1000000
Variant time: 0.099609375, Variant sum: 1000000
InformalTest 10000000
Nonvariant time: 0.521484375, Nonvariant sum: 10000000
Variant time: 0.599609375, Variant sum: 10000000
Maybe you could create your own classes? Example for Single.
Class Single2
Private m_value As Single
Private m_hasValue As Boolean
Public Property Let Initialize(ByVal source As Range)
m_hasValue = False
m_value = 0
If source Is Nothing Then _
Exit Property
' add any checks you need to recognize the source cell as non-empty
' ... to distinguish between empty entry and 0
If Trim(source.Value) = "" Then _
Exit Property
If Not IsNumeric(source.Value) Then _
Exit Property
m_value = CSng(source.Value)
m_hasValue = True
End Property
Public Property Get Value() As Single
Value = m_value
End Property
Public Property Get HasValue() As Boolean
HasValue = m_hasValue
End Property
And use the class like this:
Module:
Dim lease As Single2
Set lease = New Single2
lease.Initialize = Range("a1")
If lease.HasValue Then
Debug.Print "lease has value ... " & lease.Value
Else
Debug.Print "lease hasn't value ... "
End If

VBA Function to exclude parts of a string

My sub compares two lists of strings and returns the closest matches. I've found that the sub gets tripped up over some common words such as "the" and "facility". I would like to write a function that would be supplied an array of words to exclude and check each string for these words and exclude them if found.
Here is a sample input:
|aNames | bNames | words to exclude
|thehillcrest |oceanview health| the
|oceanview, the|hillCrest | health
Intended Output:
|aResults |bResuts
|hillcrest |hillcrest
|oceanview |oceanview
So far I have:
Dim ub as Integer
Dim excludeWords() As String
'First grab the words to be excluded
If sheet.Cells(2, 7).Value <> "" Then
For y = 2 To sheet.Range("G:G").End(xlDown).Row
ub = UBound(excludeWords) + 1 'I'm getting a subscript out of range error here..?
ReDim Preserve excludeWords(0 To ub)
excludeWords(ub) = sheet.Cells(y, 7).Value
Next y
End If
Then my comparison function, using a double loop, will compare each string in column A with column B. Before the comparison, the value in column a and b will go through our function which will check for these words to exclude. It's possible that there will be no words to exclude, so the parameter should be optional:
Public Function normalizeString(s As String, ParamArray a() As Variant)
if a(0) then 'How can I check?
for i = 0 to UBound(a)
s = Replace(s, a(i))
next i
end if
normalizeString = Trim(LCase(s))
End Function
There's probably a few parts in this code that won't work. Might you be able to point me in the right direction?
Thank you!
To store the list in the array, you can do this
Sub Sample()
Dim excludeWords As Variant
Dim lRow As Long
With Sheet1 '<~~ Change this to the relevant sheet
'~~> Get last row in Col G
lRow = .Range("G" & .Rows.Count).End(xlUp).Row
excludeWords = .Range("G2:G" & lRow).Value
'Debug.Print UBound(excludeWords)
'For i = LBound(excludeWords) To UBound(excludeWords)
'Debug.Print excludeWords(i, 1)
'Next i
End With
End Sub
And then pass the array to your function. The above array is a 2D array and hence needs to be handled accordingly (see commented section in the code above)
Also like I mentioned in the comments above
How does oceanview, the become Oceanview? You can replace the but that would give you oceanview, (notice the comma) and not Oceanview.
You may have to pass those special characters to Col G in the sheet or you can handle them in your function using a loop. For that you will have to use the ASCII characters. Please see this
Followup from comments
Here is something that I wrote quickly so it is not extensively tested. Is this what you are looking for?
Sub Sample()
Dim excludeWords As Variant
Dim lRow As Long
With Sheet1
lRow = .Range("G" & .Rows.Count).End(xlUp).Row
excludeWords = .Range("G2:G" & lRow).Value
'~~> My column G has the word "habilitation" and "this"
Debug.Print normalizeString("This is rehabilitation", excludeWords)
'~~> Output is "is rehabilitation"
End With
End Sub
Public Function normalizeString(s As String, a As Variant) As String
Dim i As Long, j As Long
Dim tmpAr As Variant
If InStr(1, s, " ") Then
tmpAr = Split(s, " ")
For i = LBound(a) To UBound(a)
For j = LBound(tmpAr) To UBound(tmpAr)
If LCase(Trim(tmpAr(j))) = LCase(Trim(a(i, 1))) Then tmpAr(j) = ""
Next j
Next i
s = Join(tmpAr, " ")
Else
For i = LBound(a) To UBound(a)
If LCase(Trim(s)) = LCase(Trim(a(i, 1))) Then
s = ""
Exit For
End If
Next i
End If
normalizeString = Trim(LCase(s))
End Function
First of all, you cannot call UBound function for the Array that doesn't have a size yet:
Dim excludeWords() As String
ub = UBound(excludeWords) + 1 'there is no size yet
To remove some of the unwanted words use Replace function
String1 = Replace(String1, "the", "")
To do the comparison you described I would use Like function. Here is documentation.
http://msdn.microsoft.com/pl-pl/library/swf8kaxw.aspx

Split data string over columns AND rows using VBA

I am trying to speed up a currently working automated workbook.
PHP sends a string similar to the below to VBA:
1[|:#:|]text-one[|:#:|]code-one[|:#:|]qty-one[{:#:}]
2[|:#:|]text-two[|:#:|]code-two[|:#:|]qty-two[{:#:}]
where
[|:#|] represents "new column"
[{:#:}] represents "new row"
When it is parsed by the VBA this is the output:
I currently use the following VBA code to parse this into a workbook:
myArray = Split(myReply, "[{:#:}]")
myRow = 1
For Each element In myArray
myRow = myRow + 1
subArray = Split(element, "[|:#:|]")
myCol = 2
For Each subelement In subArray
myCol = myCol + 1
Cells(myRow, myCol).Value = subelement
Next subelement
Next element
I am about to start optimising the code in this workbook and I am aware I can do something like (pseudo code):
for each element....
Range("C2:F2").Value = Split(element, "[|:#:|]") 'Example row number would be incremental
However is there a way to do it so that I can split into the entire Range?
For example, If I know there are 29 "rows" within the data that has been returned, I would like to be able to use split to place the data into all the rows.
I imagine the syntax would be something similar to the below, however this doesn't seem to work:
Range("C2:F29").Value = Split(Split(element, "[|:#:|]"),"[{:#:}]")
The optimal thing to do is to do everything in native VBA code and not interact with the Excel sheet until the end. Writing to sheet is a time consuming operation, so this procedure does it once and once only, writing the whole two-dimensional array at once, rather than writing it line by line. Therefore no need to disable screen updating, calculation, or anything else.
Function phpStringTo2DArray(ByVal phpString As String) As Variant
Dim iRow As Long
Dim iCol As Long
Dim nCol As Long
Dim nRow As Long
Dim nColMax As Long
Dim lines() As String
Dim splitLines() As Variant
Dim elements() As String
lines = Split(phpString, "[{:#:}]")
nRow = UBound(lines) - LBound(lines) + 1
ReDim splitLines(1 To nRow)
For iRow = 1 To nRow
splitLines(iRow) = Split(lines(iRow - 1), "[|:#:|]")
nCol = UBound(splitLines(iRow)) - LBound(splitLines(iRow)) + 1
' in case rows have different number of columns:
If nCol > nColMax Then nColMax = nCol
Next iRow
Erase lines
'We now have a (Variant) array of arrays. Convert this to a regular 2D array.
ReDim elements(1 To nRow, 1 To nColMax)
For iRow = 1 To nRow
nCol = UBound(splitLines(iRow)) - LBound(splitLines(iRow)) + 1
For iCol = 1 To nCol
elements(iRow, iCol) = splitLines(iRow)(iCol - 1)
Next iCol
Next iRow
Erase splitLines
phpStringTo2DArray = elements
End Function
Example usage:
Dim s As String
Dim v As Variant
s = "1[|:#:|]text-one[|:#:|]code-one[|:#:|]qty-one[{:#:}]2[|:#:|]text-two[|:#:|]code-two[|:#:|]qty-two[{:#:}]"
v = phpStringTo2DArray(s)
'Write to sheet
Range("A1").Resize(UBound(v, 1), UBound(v, 2)) = v
If you want to ignore the final line break [{:#:}], could add this line at the top of the function:
If Right(phpString, 7) = "[{:#:}]" Then phpString = Left(phpString, Len(phpString) - 7)
This wasn't as easy as I originally thought. I can get rid of one loop easily. But there's still an if test, so it doesn't break on empty strings etc. I feel a guru could make this even more efficient.
My worry is that for you this process is taking a lot of time. If you are trying to speed things up, your code doesn't look too horribly inefficient.
More likely if it's running slowly, is that the application.calculation & application.screenUpdating settings are set incorrectly.
Sub takePHP(myString As String)
'This sub takes specially formatted strings from a PHP script,
'and parses into rows and columns
Dim myRows As Variant
Dim myCols As Variant
Dim subRow As Variant
Application.ScreenUpdating = False
Application.Calculation = xlCalculateManual
myRows = Split(myString, "[{:#:}]")
x = 1
For Each subRow In myRows
bob = Split(subRow, "[|:#:|]")
If UBound(bob) <> -1 Then
Range(Cells(x, 1), Cells(x, UBound(bob) + 1)).Value = bob
x = x + 1
End If
Next
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
End Sub

Resources