excel vba experimenting with functions and arrays - arrays

I am experimenting with something:
There is a list with names, and what I would like to do, is to read the cell values in an array (this part works) than run a check for every cell in the worksheet and if a given cell is the same as a string inside an array, do something.
But unfortunatly I get the "type mismatch" error.
Ps. I know this doesn't make much sense and I could to that something inside the server function, but belive me I have my reasons. :-)
Edit: fixed a few things, now it looks like this (now I get the object doesn't support this property of method)
If it helps, you can also try it. You just need to add a cell with the name "Servers" and under it write some random words. Right now it should write in msgbox "ok" x times, where x is the number of rows you wrote in, under the cell, named "Servers"
1
'server name
Function server(ByVal issrvname As String)
Dim j As Integer
Dim c As Range
Dim x As Integer, y As Integer
For Each c In Sheets("Topology").UsedRange.Cells
Dim srvname() As String
j = 0
If c.Cells.Value = "Servers" Then
y = c.Column: x = c.Row + 1
Do Until IsEmpty(Cells(x, y))
ReDim Preserve srvname(0 To j) As String
srvname(j) = Cells(x, y).Value
x = x + 1
j = j + 1
Loop
End If
Next c
For Each c In Sheets("Topology").UsedRange.Cells
If IsInArray(c.Cell.Value, srvname) Then
issrvname = True
Else
issrvname = False
End If
Next c
End Function
2
Function IsInArray(stringToBeFound As String, arr As Variant) As Boolean
IsInArray = (UBound(Filter(arr, stringToBeFound)) > -1)
End Function
3
Sub test()
Dim c As Range
For Each c In Sheets("Topology").UsedRange.Cells
If server(c) = True Then
MsgBox "ok"
End If
Next c
End Sub

I think you can condense your functions:
First you need to include your Array generating block to your main sub.
Including it in the Function server is slowing code execution because it needs to generate the array in every call of the server Function
Edit1: This is tried in tested now. I've re-written your function and improve your sub a bit.
Sub test()
Dim j As Integer
Dim c As Range, c1 As Range
Dim x As Integer, y As Integer
Dim i As Long '~~> added it just to check how many is shown in MsgBox
For Each c In Sheets("Topology").UsedRange.Cells
'~~> generate array if "Servers" is encountered
If c.Value = "Servers" Then
Dim srvname() As String
j = 0
y = c.Column: x = c.Row + 1
With Sheets("Topology").UsedRange
Do Until IsEmpty(.Cells(x, y))
ReDim Preserve srvname(j)
srvname(j) = .Cells(x, y).Value
x = x + 1
j = j + 1
Loop
End With
'~~> use the generated Array of values here
i = 1
For Each c1 In Sheets("Topology").UsedRange.Cells
If IsInArray(c1.Value, srvname) Then
MsgBox "ok" & i
i = i + 1
End If
Next c1
End If
Next c
End Sub
Here's the new function: (actually, you don't need it, you can call the Match function directly in main Sub)
Function IsInArray(stringToBeFound As String, arr As Variant) As Boolean
IsInArray = Not IsError(Application.Match(stringToBeFound, arr, 0))
End Function
Maybe you do this just for testing? I just thought that the sheet you use to generate the array must be different from the sheet you want to compare the server names.

I think it might be that you define c as a range in Test, but call server with c when server is expecting a boolean.

Related

Pass of `ByVal` argument to Regex function leads to very slow code , although using Array

I am using this Regex function to (Remove numeric characters from end of string if count of numbers >= 9),
Function Remove_Number_Regex(Text As String) As String
'Remove numbers from end of string if count of numbers(characters) >= 9
With CreateObject("VBScript.RegExp")
.Global = True
.Pattern = "\d{9,}(?=\.\w+$)"
Remove_Number_Regex = .Replace(Text, "")
End With
End Function
I tried on excel (as formula) and it works as it should without any error.
Then I used it inside vba using Array , but I got this error
Compile error: ByRef argument type mismatch
I fixed that error by passing ByVal argument to the declaration of Regex function
Function Remove_Number_Regex(ByVal Text As String) As String
And that leads to a very slow code to finish 18 seconds on (10K row) ,although using any other text function inside the same array takes 0.4 seconds to finish.
In advance, grateful for any helpful comments and answers.
Sub Use_Function_Remove_Number_Regex()
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
Dim arg As Range, arr
With ActiveSheet
Set arg = .Range("O1", .Cells(.Rows.Count, "R").End(xlUp)) '10k rows
End With
arr = arg.value
Dim r As Long, j As Long
For j = 1 To 4
For r = 1 To UBound(arr)
arr(r, j) = Remove_Number_Regex(arr(r, j))
Next r
Next j
arg.value = arr
Application.Calculation = xlCalculationAutomatic
End Sub
Generally speaking; using regular expressions will slow things down. You are correct that common string-operations are faster. So, why not use them:
Function Remove_Number(Text As String) As String
Dim nr As String: nr = CStr(Val("1" & StrReverse(Split(Text, ".")(0))))
If Len(nr) > 9 Then
Remove_Number = Replace(Text, StrReverse(Mid(nr, 2)) & ".", ".")
Else
Remove_Number = Text
End If
End Function
To test this function based on your previous question:
Sub Test()
Dim arr As Variant: arr = Array("Anomaly - allhelipads1335023398818.doc", "Anomaly - oilpipingW8.doc")
For Each el In arr
Debug.Print Remove_Number(CStr(el))
Next
End Sub
Returns:
The trick used:
Split your input based on the dot, and return the 1st element from the array (zero based);
Reverse the string;
Extract the numeric value when concatenated with a '1' (to prevent trailing zeros to disappear);
Check if length is more than 9 (accounting for the '1') and if so replace the value accordingly.
Note: Depending on your version of Excel, you could just stay away from VBA alltogether. See my answer to your previous question.

Store data by using FILTER Function within VBA [duplicate]

I'm trying to make a function MonstersInLevel() that filters the second column of my "LevelMonsters" named range based on the value of the first column. The range's first column represents a game level ID and the second column represents a monster ID that appears in that level. Here's what my range looks like.
If I call MonstersInLevel(2) I expect the function to return a range consisting of "2", "3" and "4".
Function MonstersInLevel(level As Integer) As Range
MonstersInLevel = Application.WorksheetFunction.Filter(Range("LevelMonsters").Columns(2), Range("LevelMonsters").Columns(1) = level)
End Function
I get:
A value used in the formula is of the wrong data type
I'm using the FILTER function as I would as an Excel formula. I assume there's some difference in the Excel and VBA syntax for FILTER's criteria.
Just encountered this problem myself and wanted to post my workaround.
We need to return an array of True/False to the worksheet function. To do this I created a Function that takes a 2D array, the column wanted and the value to compare. It then returns a 2d single column array of the necessary True/False.
Function myeval(arr() As Variant, clm As Long, vl As Variant) As Variant()
Dim temp() As Variant
ReDim temp(1 To UBound(arr, 1), 1 To 1)
Dim i As Long
For i = 1 To UBound(arr, 1)
temp(i, 1) = arr(i, clm) = vl
Next i
myeval = temp
End Function
So in this particular case it would be called:
Function MonstersInLevel(level As Integer) As Variant
MonstersInLevel = Application.WorksheetFunction.Filter(Range("LevelMonsters").Columns(2), myeval(Range("LevelMonsters").Value, 1, level),"""")
End Function
Avoid type mismatch in Worksheetfunction via VBA
Keeping in mind that the 2nd argument reflects a dynamic matrix condition
based entirely on â–ºworksheet logic (returning an array of 0 or 1 cell values /False or True])
it seems that you have
to execute an evaluation at least within this argument and
declare the function type (explicitly or implicitly) as Variant
Function MonstersInLevel(level As Integer) As Variant
'' Failing assignment:
' MonstersInLevel = Application.WorksheetFunction.Filter(Range("LevelMonsters").Columns(2), _
' Range("LevelMonsters").Columns(1) = level _
' )
MonstersInLevel = Application.WorksheetFunction.Filter( _
Range("LevelMonsters").Columns(2), _
Evaluate(Range("LevelMonsters").Columns(1).Address & "=" & level) _
)
End Function
...or to evaluate the complete function
Function MonstersInLevel(level As Integer) As Variant
Dim expr As String
expr = "=Filter(" & _
Range("LevelMonsters").Columns(2).Address & "," & _
Range("LevelMonsters").Columns(1).Address & "=" & level & _
")"
'Debug.Print expr
MonstersInLevel = Evaluate(expr)
End Function
Example call writing results to any target
Dim v
v = MonstersInLevel(2)
Sheet1.Range("D2").Resize(UBound(v), UBound(v, 2)) = v
Of course it would be possible as well to write .Formula2 expressions programmatically, even splitting into spill range references.
Addendum ........... //as of Jan 10th
Backwards compatible workaround via VBA.Filter()
"If you guys know any other VBA function that would be more appropriate
than Application.WorksheetFunction.Filter I'd be ok."
In order to provide also a backwards compatible alternative,
I demonstrate the following approach using the classic (VBA.)Filter() function (see section [3]) based upon
prior matching results (see [1]).
Note that Application.Match() comparing two (!) array inputs
delivers a whole array of possible findings (instead of a single result as most frequently executed).
Non findings are identified by IsError() values of -1; adding +1 results in a set
of zeros and ones. Section [2] enters corresponding data for positive findings.
Eventually non-findings (i.e. 0or zero) are removed by a tricky negative filtering.
Function getLevels()
Function getLevels(rng As Range, ByVal level As Long)
'Site: https://stackoverflow.com/questions/65630126/how-to-remove-only-the-duplicate-row-instead-of-removing-all-the-rows-that-follo
'[0] get datafield array
Dim v, v2
v = Application.Index(rng.Value2, 0, 1) ' 1st column
v2 = Application.Index(rng.Value2, 0, 2) ' 2nd column
'[1] check data (with Match comparing 2 arrays :-)
Dim results
results = Application.Transpose(Application.Match(v, Array(level), 0))
'[2] rebuild with False/True entries
Dim i As Long
For i = 1 To UBound(results)
results(i) = IsError(results(i)) + 1 ' 0 or 1-values
If results(i) Then results(i) = v2(i, 1) ' get current value if true
Next i
'[3] remove zeros (negative filtering)
results = Filter(results, "0", False)
'[4] return results as vertical 1-based array
getLevels = Application.Transpose(results)
End Function
Example call
Const LVL = 2 ' define level
With Sheet1 ' change to project's sheet Code(Name)
'define data range (assuming columns A:B)
Dim rng As Range
Set rng = .UsedRange.Resize(, 2)
'function call getLevels()
Dim levels
levels = getLevels(rng, level:=LVL)
'write to target
.Columns("I:I").Clear
.Range("I2").Resize(UBound(levels), 1) = levels
End With
Solution without any supporting VBA function:
Function MonstersInLevel(level As Integer) As Variant
With Application.WorksheetFunction
MonstersInLevel = .Filter(Range("LevelMonsters").Columns(2), _
.IfError(.XLookup(Range("LevelMonsters").Columns(1), level, True), False))
End With
End Function
XLookup returns an array of #N/A or True. IfError replaces errors with False. Finally, the Filter function receives an array of booleans as the second parameter.
EDIT
Removed the IfError function thanks to #ScottCraner:
Function MonstersInLevel(level As Integer) As Variant
With Application.WorksheetFunction
MonstersInLevel = .Filter(Range("LevelMonsters").Columns(2), _
.XLookup(Range("LevelMonsters").Columns(1), level, True, False))
End With
End Function
I couldn't resolve your question but as I did some testing on the subject trying to do so, I thought I'd share my findings:
Based on this Microsoft community post, or at least the answers there, it seems you will need to loop through the output in one way or another...
That question seems to want to achieve the same as what you are wanting to do (I think?).
On the other hand, I have never used the WorksheetFunction.Filter method, and the closest I could get it to working was like so:
Here is my sample data - RangeOne is Column A and RangeTwo is Column B. I have used the =FILTER() function in cell C1 evaluating the input in D1 for reference of expected results. Naturally this function is working as expected! The VBA routine is outputting to Columns E, F and G.
Sub TestFilterFunction()
Dim TestArray As Variant
Range("E1:E3") = Application.Filter(Range("RangeTwo"), Range("RangeOne"), Range("D1"))
Range("F1:F3") = Application.Filter(Range("RangeTwo"), Range("RangeOne") = Range("D1")) 'Runtime Error 13
Range("G1:G3") = Application.Filter(Range("RangeTwo"), Range("RangeOne"))
TestArray = Application.Filter(Range("RangeTwo"), Range("RangeOne"), Range("D1"))
TestArray = Application.Filter(Range("RangeTwo"), Range("RangeOne") = Range("D1")) 'Runtime Error 13
TestArray = Application.Filter(Range("RangeTwo"), Range("RangeOne"))
Range("H1:H3") = Application.Filter(Range("RangeTwo", "RangeOne"), Range("RangeOne"), Range("D1"))
TestArray = Application.Filter(Range("A1:B9"), Range("RangeOne"), "2")
End Sub
Column E returned the first 3 values from RangeTwo.
Column F has not been populated - This is because that line threw the Runtime error 13 - Type Mismatch
Column G returned the first 3 values from RangeTwo.
Column H returned the first 3 values from "A1:B9" (both ranges together) - specifically the first 3 values of column A.
I thought this was odd so I threw in an array to assign the values to rather than directly to the worksheet;
The first TestArray line and the third TestArray line both populated the array with the entire RangeTwo values;
I realised with the syntax of the first and third attempt at the WorksheetFunction.Filter, the entire range is returned (that being the first argument - Arg1 - range), but when trying to include the = Range("D1") , it returns the Type Mismatch error.
The final TestArray attempt being the same syntax as the Column H test, returned both columns in a 2D array (now TestArray(1 To 9, 1 To 2)).
I should note I couldn't find any documentation at all on WorksheetFunction.Filter so I'm assuming it does follow the same syntax as the Excel Sheet Function has.
If I find anything more on this topic I'll come back and edit it in, but for now it's looking like perhaps a solution using either loops or Index/Match functions also will need to happen to have the data returned in VBA.
I thought about perhaps writing the sheet formula to a cell and then grab that into an array or something but Excel inserts # into it now which only returns a single cell result, i.e.
Range("J1").Formula = "=FILTER(B1:B9, A1:A9 = D1)"
Would return in J1:
=#FILTER(B1:B9, A1:A9 = D1)
Which with our sample data, would only return 2 in J1 as opposed to the expected/desired 2, 3 and 4 in J1:J3.
I can't work out a way to remove the # as it is applied when the function is written to the cell unfortunately, but hopefully any of the above helps someone find a solution.
Just some comments to help you out.
If you are using the new FILTER() function from either a worksheet cell or within some VBA code, the first argument should be a range and the second argument should a a Boolean array. (if you don't enter something that can evaluate to a Boolean array, VBA may complain the the data type is wrong)
You would be best served (in VBA) if you:
explicitly declared a 2 dimensional, column-compatible, Boolean array
filled the array
used the array in the function call
Here is a super simple example. Say we want to filter the data from A1 to A6 to remove blanks. We could pick a cell and enter:
=FILTER(A1:A6,A1:A6<>"")
Looks like:
Now we want to perform the same activities with a VBA sub and put the result in a block starting with B9. The code:
Sub SingleColumn()
Dim r As Range, wf As WorksheetFunction, i As Long
Dim arr, s As String, dq As String, boo, rc As Long
Set wf = Application.WorksheetFunction
Set r = Range("A1:A6")
rc = r.Rows.Count
ReDim boo(1 To r.Rows.Count, 1 To 1) As Boolean
i = 1
For Each rr In r
If rr.Value = "" Then
boo(i, 1) = False
Else
boo(i, 1) = True
End If
i = i + 1
Next rr
arr = wf.Filter(r, boo)
MsgBox LBound(arr, 1) & "-" & UBound(arr, 1) & vbCrLf & LBound(arr, 2) & "-" & UBound(arr, 2)
Range("B9").Resize(UBound(arr, 1), UBound(arr, 2)) = arr
End Sub
Result:
On Excel version 15.0 (2013), I don't see Application.WorksheetFunction.Filter (tried with Show Hidden Members):
So maybe this is a newer function in later versions ?
My top Google search directs me to this question ;)
So, my answer is to avoid the function primarily from the point of view of backwards compatibility.
Alternate code options presented below returning e.g. a Range and a Variant.
Input:
Code:
Option Explicit
Sub Test()
Dim rngInput As Range
Dim rngFiltered As Range
Dim varFiltered As Variant
Dim varItem As Variant
Set rngInput = ThisWorkbook.Worksheets("Sheet1").Range("A2:B10")
' as range
Debug.Print "' Output as Range"
Set rngFiltered = MonstersInLevel_AsRange(rngInput, 2, 1, 2)
Debug.Print "' " & rngFiltered.Address ' expect B5, B6, B8
Debug.Print "' ---------------"
' as variant
Debug.Print "' Output as Variant"
varFiltered = MonstersInLevel_AsVariant(rngInput, 2, 1, 2)
For Each varItem In varFiltered
Debug.Print "' " & varItem ' expect 3, 4, 5
Next varItem
Debug.Print "' ---------------"
End Sub
Function MonstersInLevel_AsRange(rngToFilter As Range, _
ByVal lngLevel As Long, _
ByVal lngColIxToFilter As Long, _
ByVal lngColIxForValue As Long) As Range
Dim rngResult As Range
Dim lngRowIndex As Long
Dim lngResultIndex As Long
Set rngResult = Nothing
For lngRowIndex = 1 To rngToFilter.Rows.Count
If rngToFilter.Cells(lngRowIndex, lngColIxToFilter) = lngLevel Then
If rngResult Is Nothing Then
Set rngResult = rngToFilter.Cells(lngRowIndex, lngColIxForValue)
Else
Set rngResult = Union(rngResult, rngToFilter.Cells(lngRowIndex, lngColIxForValue))
End If
End If
Next lngRowIndex
Set MonstersInLevel_AsRange = rngResult
End Function
Function MonstersInLevel_AsVariant(rngToFilter As Range, _
ByVal lngLevel As Long, _
ByVal lngColIxToFilter As Long, _
ByVal lngColIxForValue As Long) As Variant
Dim varResult As Variant
Dim lngRowIndex As Long
Dim lngResultIndex As Long
lngResultIndex = 0
ReDim varResult(0)
For lngRowIndex = 1 To rngToFilter.Rows.Count
If rngToFilter.Cells(lngRowIndex, lngColIxToFilter) = lngLevel Then
lngResultIndex = lngResultIndex + 1
ReDim Preserve varResult(1 To lngResultIndex)
varResult(lngResultIndex) = rngToFilter.Cells(lngRowIndex, lngColIxForValue)
End If
Next lngRowIndex
MonstersInLevel_AsVariant = varResult
End Function
Test output:
' Output as Range
' $B$5:$B$6,$B$8
' ---------------
' Output as Variant
' 3
' 5
' 4
' ---------------
Based on Christian Buses answer (https://stackoverflow.com/a/65671334/16578424) I wrote a generic function to use the FILTER-function.
It returns a one-dimensional array with the filtered values.
Public Function getFILTERValuesFromRange(rgResult As Range, rgFilter As Range, varValue As Variant) As Variant
If rgResult.Columns.count > 1 Or rgFilter.Columns.count > 1 Then
Err.Raise vbObjectError + 512, , "Only ranges with one column are allowed."
ElseIf rgResult.Rows.count <> rgFilter.Rows.count Then
Err.Raise vbObjectError + 512, , "Both ranges have to be of the same size."
End If
Dim arr1 As Variant
With Application.WorksheetFunction
arr1 = .filter(rgResult, .XLookup(rgFilter, varValue, True, False))
End With
getFILTERValuesFromRange = getOneDimensionalArrayFromRangeArray(arr1)
End Function
Private Function getOneDimensionalArrayFromRangeArray(arr1 As Variant) As Variant
Dim arr2 As Variant
ReDim arr2(LBound(arr1, 1) To UBound(arr1, 1))
Dim i As Long
For i = 1 To UBound(arr1, 1)
arr2(i) = arr1(i, 1)
Next
getOneDimensionalArrayFromRangeArray = arr2
End Function

Prevent recalculations of User Defined Functions used in named ranges

I have three UDF's:
Private Function IsInArray(stringToBeFound As Variant, arr As Variant) As Boolean
IsInArray = Not IsError(Application.Match(stringToBeFound, arr, 0))
End Function
This function checks if something is in the array.
Private Function data_to_array(data As Range)
Dim arrArray As Variant
Dim cell As Range
Dim z As Integer
z = 0
ReDim arrArray(1 To data.Cells.Count)
For Each cell In data
z = z + 1
arrArray(z) = cell.Value
Next cell
data_to_array = arrArray
End Function
This function extracts selected range values and puts them into an array.
Private Function plot_vals(data As Variant, custom_arr As Variant)
Dim arrPlot As Variant
ReDim arrPlot(1 To UBound(data)) As Variant
Dim c As Integer
Dim cl As Integer
cl = 0
For c = 1 To UBound(data)
cl = cl + 1
If IsInArray(cl, custom_arr) Then
arrPlot(cl) = data(cl)
Else
arrPlot(cl) = CVErr(xlErrNA)
End If
Next c
plot_vals = arrPlot
End Function
The last UDF loops through the data array from second UDF and if index/position of value in data_array is in custom_array, then it returns its value. Otherwise it puts an error into an array.
Data looks like this:
These functions are used like this in Excel:
data_to_array(A1:A5) - this UDF creates an array(1 to 5) with values from cells A1:A5.
plot_vals(data_to_array(A1:A5), {1,5}) - this UDF creates an array(1 to 5), and uses second argument to retrieve first and fifth values while putting errors in the other indexes. The result is array of for example: {5,error,error,error,1}
If I used the function on above data like this: plot_vals(data_to_array(A1:A5), {1,2}) then the result would be an array {5,4,error,error,error}
That plot_vals UDF is used in named range and that named range is used to plot values on chart.
Data is stored in named range myData and the function in second named range is used like this:
plot_vals(myData,{1,5}).
Everything works, I can plot it on chart, all is good but when the named ranges are used on charts, every time I change something in my workbook all functions are recalculated like... 10 times each one, instead of once. It causes Excel to slow down/freeze if those functions are used many times. I have tried to search about function volatility and how to turn it off (it should be turned off by default?), but nothing seems to be working and I do not know how to stop that from happening. I have tried to recreate this in Excel using standard Excel functions in named ranges, but I cannot find a correct function to do what I want. UDF is exactly what I need.
When these named ranges are NOT used in charts then nothing happens, but as soon as I use the named range on chart then it recalculates everything on even a minor change of a workbook. Minor change meaning - copying / pasting / adding rows etc.
How can I stop that from happening? How to recalculate UDF's only once?
EDIT on further research:
I have tried the potential solutions provided by Charles Williams:
https://fastexcel.wordpress.com/2011/11/25/writing-efficient-vba-udfs-part-7-udfs-calculated-multiple-times/
His potential solutions do not change anything.
I have also tried using Sheet_Change event, changing calculations to manual and then back to automatic. It helps but it clears the clipboard (unacceptable) and it causes issues with my other macros, so it is a "no-go" solution.
It is also worth noting, that as soon as the chart is deleted and the UDF's remain in named ranges, everything is working nice and smooth. But when those named ranges are in chart series formulas, everything is recalculating 100's of times.
Aside from trying to fix the basic problem of how many times your functions get called, you can partially address the slow-down by optimizing the basic performance:
Application.Match is relatively slow unless the data being searched in on a worksheet
Reading a range into an array is slower than reading the whole range at once using its .Value (assuming range is a single area)
So:
Sub PerfTester()
Const ARR_SZ As Long = 10
Dim arr(1 To ARR_SZ), i, n, t, v, m
'populate a test array
For i = 1 To ARR_SZ
arr(i) = i
Next i
t = Timer
For n = 1 To 100000
v = Round(Rnd * ARR_SZ, 0)
m = IsInArray(v, arr) 'using match
Next n
Debug.Print Timer - t '~ 1.7 sec
t = Timer
For n = 1 To 100000
v = Round(Rnd * ARR_SZ, 0)
m = IsInArray2(v, arr) 'using a loop
Next n
Debug.Print Timer - t '~0.11 sec
t = Timer
For n = 1 To 100000
v = data_to_array(Range("A1:A50")) 'using cell-by-cell
Next n
Debug.Print Timer - t '~ 11.5 sec
t = Timer
For n = 1 To 100000
v = data_to_array2(Range("A1:A50")) 'using single read from range
Next n
Debug.Print Timer - t '~ 2.8 sec
End Sub
Private Function IsInArray(stringToBeFound As Variant, arr As Variant) As Boolean
IsInArray = Not IsError(Application.Match(stringToBeFound, arr, 0))
End Function
Private Function IsInArray2(stringToBeFound As Variant, arr As Variant) As Boolean
Dim i
For i = LBound(arr) To UBound(arr)
If arr(i) = stringToBeFound Then
IsInArray2 = True
Exit For
End If
Next i
End Function
Private Function data_to_array(data As Range)
Dim arrArray As Variant, cell As Range, z As Integer
z = 0
ReDim arrArray(1 To data.Cells.Count)
For Each cell In data
z = z + 1
arrArray(z) = cell.Value
Next cell
data_to_array = arrArray
End Function
Private Function data_to_array2(data As Range)
Dim arrArray As Variant, cell As Range, z As Long, v
v = data.Value
ReDim arrArray(1 To UBound(v, 1))
For z = 1 To UBound(v, 1)
arrArray = v(z, 1)
Next z
data_to_array2 = arrArray
End Function
You should be able to prevent unnecessary additional calculations by including
Application.EnableEvent = False
Application.Calculation = xlManual
at the start of your functions and
Application.EnableEvents = True
Application.Calculation = xlAutomatic
at the end of your functions. This prevents your spreadsheet from attempting to update and recalculate whenever you make a minor change. If you feel it necessary, you can add
Worksheet.Calculate
at some point in your code to force a recalculation of the current sheet.

Stopping an 1 by n array from being converted to a 1-dimensional one

Below is some code I wrote to handle arrays and ranges uniformly (Accepting a range as an array parameter). It contains a function called sanitise which is meant to be a function you can call on some 2D collection of numbers A, and get the same numbers back as a 2D array of Doubles.
Public Function item(ByRef A As Variant, i As Integer, j As Integer) As Double
If TypeName(A) = "Range" Then
item = A.Cells(i, j)
Else
item = A(i, j)
End If
End Function
Public Function rows(ByRef A As Variant) As Integer
If TypeName(A) = "Range" Then
rows = A.rows.Count
Else
rows = UBound(A, 1) - LBound(A, 1) + 1
End If
End Function
Public Function cols(ByRef A As Variant) As Integer
If TypeName(A) = "Range" Then
cols = A.columns.Count
Else
cols = UBound(A, 2) - LBound(A, 2) + 1
End If
End Function
Public Function sanitise(ByRef A As Variant) As Double()
Debug.Print TypeName(A)
If TypeName(A) = "Double()" Then
sanitise = A
Else
Debug.Print rows(A)
Dim B() As Double
ReDim B(1 To rows(A), 1 To cols(A))
Dim i As Integer, j As Integer
For i = 1 To rows(A)
For j = 1 To cols(A)
B(i, j) = item(A, i, j)
Next j
Next i
sanitise = B
End If
End Function
The implementation works exactly as you'd expect it: select a range in the worksheet, say A1:B2, call sanitize on it and you'll have two copies of the same thing:
What goes wrong however, is sanitise^2.
Calling sanitise twice breaks down, but only if you call it on single row. Multiple rows: fine, single column: fine.
I know why it happens: after the first sanitise, Excel forgets what shape array was returned. (It also forgets the type: instead of Double() the input to the second sanitise is Variant())
Does anybody know how to work around this issue?
While it's unlikely that I'd ever want to use sanitise twice in a row, the above example illustrates why it's difficult to compose two functions along a 2 dimensional array.
Note: this is issue only happens when sanitise is called from a worksheet.
Update, I've figured it out: for the worksheets 1D storage in synonymous with row, so that needs to be taken into consideration
My final version:
Public Function get_2D(ByRef A As Variant) As Double()
'turns various forms of input into a 2D array of Doubles
Dim result() As Double
Dim i As Integer
If TypeOf A Is Range Or dims(A) = 2 Then
ReDim result(1 To rows(A), 1 To cols(A))
Dim j As Integer
For i = 1 To rows(A)
For j = 1 To cols(A)
result(i, j) = item(A, i, j)
Next j
Next i
Else
'1D storage is treated as a row
ReDim result(1 To 1, 1 To rows(A)) 'rows(A) gets length of the first axis
For i = 1 To rows(A)
result(1, i) = A(i)
Next i
End If
sanitise = result
End Function
dims is a function that returns the number of dimensions of an array: https://support.microsoft.com/en-us/kb/152288
I think this is somewhat aligned with your specification and it has the benefit of solving the single row problem you demonstrate. Would it work for your purposes?
Function sanitise_sugg(inp As Variant) As Variant
Dim result As Variant
If TypeOf inp Is Object Then
result = inp.Value
Else
result = inp
End If
sanitise_sugg = result
End Function
Edit: Taking one step back I think you should divide the task at hand into two: First use "sanitise_sugg" to use excel ranges and excel-vba arrays interchangeabely. Then if you for a special need demand the input to specifcally be some sort of Array of doubles, write a separate function that tests and if possible casts a variant input to this type.
Edit 2: Taking one step forward instead, let me claim that in the case the elements fed to Function sanitise_sugg(inp As Variant) As Variant contain doubles from within vba, or cells with numeric values from an excel sheet, it meets the specifciation demanded for Public Function sanitise(ByRef A As Variant) As Double()
Edit 3: To see how the function keeps track of its input Array layout independently of beeing Row vector, Column vector or full Matrix, independently of beeing passed the Array from an excel range or from within VBA, please refer to the below worksheet;
I can't think of any practical use for this unless you do a lot of calculations on the 2D array of Doubles, so if you give more information on what exactly you are trying to do, we can probably recommend something easier/better/more efficient etc.
.Value and .Value2 return 2D Variant array when more than one cell in the range:
v = [a1:b1].Value2 ' Variant/Variant(1 to 1, 1 to 2)
v = [a1:a2].Value2 ' Variant/Variant(1 to 2, 1 to 1)
v = [a1].Value2 ' Variant/Double
so a naive approach can be something like:
Function to2D(v) As Double()
'Debug.Print TypeName(v), VarType(v)
Dim d2() As Double, r As Long, c As Long
If IsArray(v) Then
If TypeOf v Is Range Then v = v.Value2
ReDim d2(1 To UBound(v, 1), 1 To UBound(v, 2))
For r = 1 To UBound(v, 1)
For c = 1 To UBound(v, 2)
If IsNumeric(v(r, c)) Then d2(r, c) = v(r, c)
Next c
Next r
Else
ReDim d2(1 To 1, 1 To 1)
If IsNumeric(v) Then d2(1, 1) = v
End If
to2D = d2
End Function
and tested with:
d2 = to2D([a1:b2])
d2 = to2D([a1:b1])
d2 = to2D([a1:a2])
d2 = to2D([a1])
d2 = to2D(1)
d2 = to2D([{" 1 ";" 2.0 "}]) ' works with strings too
d2 = to2D([{1,2;3,4}])
'd2 = to2D([{1,2}]) ' doesn't work with 1D arrays

value of type integer cannot be converted to object because integer in not reference type

Module Module1
Sub main()
Dim pl(), pll() As Integer
Dim a, b As Integer
ReDim pl(0)
ReDim pll(0)
Do
a = InputBox("insert number:")
If a <> 0 Then
b = b + 1
ReDim Preserve pl(b)
pl(b) = a
End If
Loop Until a = 0
pll = **se(pl)**
End Sub
Function se(pol()) As Integer()
Dim r, t, w, m As Integer
Dim fix() As Integer
ReDim fix(0)
r = UBound(pol)
w = 2
For t = 1 To r
For m = 1 To r
If w <= r Then
If pol(w) < pol(t) Then
ReDim Preserve fix(t)
fix(t) = pol(w)
End If
End If
w = w + 1
Next
Next
se = fix
End Function
End Module
Hi, i created this function(dont know if its working) se(pl) that take array of numbers and return that array but in ascending order. But when I want assign that function into array - pll=se(pl) it gives me this error ==> "value of type integer cannot be converted to object because integer in not reference type"
Im sorry im noob, Can anybody help?
I adjusted your code to make it work. Yet, I tried to stick as much as possible to the original code to make it easier for you to learn from this solution:
Option Base 0
Option Explicit
Sub main()
Dim pl() As Integer
Dim a As Integer, b As Integer
ReDim pl(0)
Do
a = InputBox("insert number:")
If IsNumeric(a) And a <> 0 Then
b = b + 1
ReDim Preserve pl(b)
pl(b) = a
End If
Loop Until a = vbNullString
Debug.Print "Unsorted:"
For a = LBound(pl) To UBound(pl)
Debug.Print pl(a)
Next a
se intArray:=pl
Debug.Print "Sorted:"
For a = LBound(pl) To UBound(pl)
Debug.Print pl(a)
Next a
End Sub
Function se(ByRef intArray() As Integer)
Dim t As Integer, w As Integer, m As Integer
For t = LBound(intArray) To UBound(intArray)
For m = LBound(intArray) To UBound(intArray)
If intArray(t) < intArray(m) Then
w = intArray(t)
intArray(t) = intArray(m)
intArray(m) = w
End If
Next m
Next t
End Function
Some important notes:
(1) If you want to Dim multiple variables in one row then you'll have to repeat the DataType for each variable. So, it is Dim a as Integer, b as Integer and not Dim a, b as Integer. In the latter of the two cases a will be of DataType variant (and not as possibly expected Integer).
(2) To pass arrays in VBA from a procedure to a function you'll have to pass it ByRef. As such, there is no need to created a second or third array (such as pll() or fix()).
(3) There is a VBA command Fix. Hence, you cannot use it for a variable.
Let me know if the above helped or if you require more background or a slight adjustment.
you're tagging both "VBA" and "Visual Studio" but what follows is for VBA only
as to your very code I made it work (meaning it run to the end with no errors) only by substituting fix with fixed, since my Excel VBA "complier" throws a "Syntax error" message due to the existence of the "FIX()" VBA function.
so, after that substitution Function se was called and it returned an integer array with no problems.
but it didn't do what you stated as its due i.e.: "return the passed array in ascending order".
also, both your variable declaration and your input procedure would allow for strings to be passed.
so here follow my proposal to have your code do what you said you need ("return the passed array in ascending order") with a more robust input procedure
Option Explicit
Sub main()
Dim pl() As Integer, pll() As Integer
pl = TryConvertToInt(Split(InputBox("insert numbers (separated by space):")))
pll = se(pl)
End Sub
Function TryConvertToInt(arr As Variant) As Integer()
Dim i As Long, n As Long
ReDim myint(1 To UBound(arr) - LBound(arr) + 1) As Integer
For i = LBound(arr) To UBound(arr)
If IsNumeric(arr(i)) And arr(i) <> 0 Then
n = n + 1
myint(n) = CInt(arr(i))
End If
Next i
ReDim Preserve myint(1 To n) As Integer
TryConvertToInt = myint
End Function
Function se(pol() As Integer) As Integer()
'adapted from https://support.microsoft.com/en-us/kb/133135
Dim Temp As Integer
Dim i As Integer
Dim NoExchanges As Boolean
Dim fixed() As Integer
ReDim fixed(1 To UBound(pol) - LBound(pol) + 1)
fixed = pol
' Loop until no more "exchanges" are made.
Do
NoExchanges = True
' Loop through each element in the array.
' For i = 1 To UBound(pol) - 1
For i = LBound(fixed) To UBound(fixed) - 1
' If the element is greater than the element following it, exchange the two elements.
If fixed(i) > fixed(i + 1) Then
NoExchanges = False
Temp = fixed(i)
fixed(i) = fixed(i + 1)
fixed(i + 1) = Temp
End If
Next i
Loop While Not (NoExchanges)
se = fixed
End Function
as you can see:
the main sub is now very simple and reduced to three statements
the first being the variables declarations one
the second statements nests three calls:
the call to InpuBox function where the user is asked to input all wanted numbers separated by spaces (and no need to have a zero to close the sequence)
the call to Split function to have it parse the input string into a strings array made of as many strings as input ones separated by spaces
the call to TryConvertToInt function that takes care of analyzing the strings array and convert it to an integer array, allowing only numbers other than zeros
the third statement calls se function and puts its return integer array into pll integer array
the se function has a sorting algorithm derived from https://support.microsoft.com/en-us/kb/133135, only adapted to handle integer arrays only.
with such a structure you can now be more effective in coding along since you can concentrate on the main sub to do "main" works (do this, do that, ...) via calls to specific subs and/or functions.
this leaving "non-main" works into specific subs or functions where to concentrate on for specific purpose only: for instance you may want to customize the TryConvertToInt function for more detailed filtering actions

Resources