Get the value before the ubound each time - excel

Dim txt As String
Dim i As Integer
Dim reference As Variant
Dim d As Integer
d = Worksheets("Sheet1").cells(Rows.Count, "a").End(xlUp).Row
txt = cells(3, 4).Value
reference = Split(txt, " ")
For i = 0 To UBound(reference)
cells(d + 1, [4]).Value = reference(i)
Next
txt = cells(3, 4).Value
reference = Split(txt, " ")
cells(d + 1, [12]).Value = reference(3)
Hi, im trying to pick the reference before the ubound value each time, and the copy to the reference to the last line. I got this code to work when its the 4th part of the string but im trying to always pick the value before the ubound. Is it possible to do UBOUND -1. or do i have to go another way around this. thanks max

There are basically 2 ways to pick the prelast value.
Option 1 - Using Ubound():
Sub TestMe()
Dim reference As String
reference = "Stack Overflow is my favourite VBA site!"
Dim splitted As Variant
splitted = Split(reference)
Debug.Print splitted(UBound(splitted) - 1)
End Sub
Option 2 - Using predefined function for array length and removing 2 from it:
Calling it this way:
Debug.Print splitted(GetArrayLength(splitted) - 2)
The function:
Private Function GetArrayLength(myArray As Variant) As Long
If IsEmpty(myArray) Then
GetArrayLength = 0
Else
GetArrayLength = UBound(myArray) - LBound(myArray) + 1
End If
End Function
The function is a bit better, because it checks for empty arrays.

Related

Nesting ParamArrays when declaring Excel VBA functions like SUMIFS?

Consider the following example: Lets say you want to make a function "JoinIfs" that works just like SUMIFS except instead of adding the values in the SumRange, it concatenates the values in "JoinRange". Is there a way to nest the ParamArray as it seems to be done in SUMIFS?
SUMIFS(sum_range, criteria_range1, criteria1, [criteria_range2, criteria2], ...)
I imagine the declaration should look something like this:
Function JoinIfs(JoinRange As Variant, _
Delim As String, _
IncludeNull As Boolean, _
ParamArray CritArray(CriteriaRange As Variant, Criteria As Variant)) As String
But nothing I try seems to compile and there might not be a way to nest ParamArrays. But the existence of functions like SUMIFS and COUNTIFS seems to suggest there might be a way to nest the ParamArrays.
This question duplicates AlexR's question Excel UDF with ParamArray constraint like SUMIFS. But that was posted a few years ago with no response so either the question didn't get enough attention or it was misunderstood.
Edit for clarification: This question is specifically about nesting ParamArrays. I'm not trying to find alternative methods of achieving the outcome of the example above. Imagine nesting ParamArrays on a completely different fictional function like "AverageIfs"
As per the documentation for the Function statement and Sub statement, a Function or Sub can only contain 1 ParamArray, and it must be the last argument.
However, you can pass an Array as an Argument to a ParamArray. Furthermore, you can then check how many elements are in the ParamArray, and throw an error if it isn't an even number. For example, this demonstration takes a list of Arrays, and which element in that array to take, and outputs another array with the results:
Sub DemonstrateParamArray()
Dim TestArray As Variant
TestArray = HasParamArray(Array("First", "Second"), 0)
MsgBox TestArray(0)
Dim AnotherArray As Variant
AnotherArray = Array("Hello", "World")
TestArray = HasParamArray(AnotherArray, 0, AnotherArray, 1)
MsgBox Join(TestArray, " ")
End Sub
Function HasParamArray(ParamArray ArgList() As Variant) As Variant
Dim ArgumentCount As Long, WhichPair As Long, Output() As Variant, WhatElement As Long
ArgumentCount = 1 + UBound(ArgList) - LBound(ArgList)
'Only allow Even Numbers!
If ArgumentCount Mod 2 = 1 Then
Err.Raise 450 '"Wrong number of arguments or invalid property assignment"
Exit Function
End If
ReDim Output(0 To Int(ArgumentCount / 1) - 1)
For WhichPair = LBound(ArgList) To ArgumentCount + LBound(ArgList) - 1 Step 2
WhatElement = ArgumentCount(WhichPair + 1)
Output(Int(WhichPair / 2)) = ArgumentCount(WhichPair)(WhatElement)
Next WhichPair
HasParameterArray = Output
End Function
(A list of built-in error codes for Err.Raise can be found here)
It seems like nesting a ParamArray is not possible.
I was hoping to get a function that looks like Excel's built in functions.
SUMIFS, for example seems to group pairs of parameters in a very neat way.
Based on the inputs of some users I made the following Function which seems to work quite well.
Function SJoinIfs(JoinRange As Variant, Sep As String, IncludeNull As Boolean, ParamArray CritArray() As Variant) As Variant
'Concatenates text based on multple criteria similar to SUMIFS.
'Sizes of ranges CritArray (0, 2, 4 ...) must match size of range JoinRange. CritArray must have an even amount of elements
'Elements of CritArray (1, 3, 5 ...) must be single values
Set JoinList = CreateObject("System.Collections.Arraylist")
'Set FinalList = CreateObject("System.Collections.Arraylist")
For Each DataPoint In JoinRange
JoinList.Add (CStr(DataPoint))
Next
JoinArray = JoinList.ToArray
CriteriaCount = UBound(CritArray) + 1
If CriteriaCount Mod 2 = 0 Then
CriteriaSetCount = Int(CriteriaCount / 2)
Set CriteriaLists = CreateObject("System.Collections.Arraylist")
Set CriteriaList = CreateObject("System.Collections.Arraylist")
Set MatchList = CreateObject("System.Collections.Arraylist")
For a = 0 To CriteriaSetCount - 1
CriteriaList.Clear
For Each CriteriaTest In CritArray(2 * a)
CriteriaList.Add (CStr(CriteriaTest))
Next
If CriteriaList.count <> JoinList.count Then 'Ranges are different sizes
SJoinIfs = CVErr(xlErrRef)
Exit Function
End If
MatchList.Add (CStr(CritArray((2 * a) + 1)))
CriteriaLists.Add (CriteriaList.ToArray)
Next
JoinList.Clear
For a = 0 To UBound(JoinArray)
AllMatch = True
For b = 0 To MatchList.count - 1
AllMatch = (MatchList(b) = CriteriaLists(b)(a)) And AllMatch
Next
If AllMatch Then JoinList.Add (JoinArray(a))
Next
SJoinIfs = SJoin(Sep, IncludeNull, JoinList)
Else 'Criteria Array Size is not even
SJoinIfs = CVErr(xlErrRef)
Exit Function
End If
End Function
This function makes use of another function SJoin() which I adapted some time ago based on the answer provided by Lun in his answer to How to replicate Excel's TEXTJOIN function in VBA UDF that allows array inputs.
I have adapted this Function to include the use of Numericals, VBA Arrays and Arraylists as well.
On Error Resume Next
'Sep is the separator, set to "" if you don't want any separator. Separator must be string or single cell, not cell range
'TxtRng is the content you want to join. TxtRng can be string, single cell, cell range or array returned from an array function. Empty content will be ignored
Dim OutStr As String 'the output string
Dim i, j, k, l As Integer 'counters
Dim FinArr(), element As Variant 'the final array and a temporary element when transfering between the two arrays
'Go through each item of TxtRng(), depending on the item type, transform and put it into FinArray()
i = 0 'the counter for TxtRng
j = 0 'the counter for FinArr
k = 0: l = 0 'the counters for the case of array from Excel array formula
Do While i < UBound(TxtRng) + 1
If TypeName(TxtRng(i)) = "String" Then 'specified string like "t"
ReDim Preserve FinArr(0 To j)
FinArr(j) = "blah"
FinArr(j) = TxtRng(i)
j = j + 1
ElseIf TypeName(TxtRng(i)) = "Range" Then 'single cell or range of cell like A1, A1:A2
For Each element In TxtRng(i)
ReDim Preserve FinArr(0 To j)
FinArr(j) = element
j = j + 1
Next
ElseIf TypeName(TxtRng(i)) = "Variant()" Then 'array returned from an Excel array formula
For k = LBound(TxtRng(0), 1) To UBound(TxtRng(0), 1)
For l = LBound(TxtRng(0), 2) To UBound(TxtRng(0), 2)
ReDim Preserve FinArr(0 To j)
FinArr(j) = TxtRng(0)(k, l)
j = j + 1
Next
Next
Else
TJoin = CVErr(xlErrValue)
Exit Function
End If
i = i + 1
Loop
'Put each element of the new array into the join string
For i = LBound(FinArr) To UBound(FinArr)
If FinArr(i) <> "" Then 'Remove this line if you want to include empty strings
OutStr = OutStr & FinArr(i) & Sep
End If
Next
TJoin = Left(OutStr, Len(OutStr) - Len(Sep)) 'remove the ending separator
End Function
Thanks to all who contributed to this question.

Function changes value in sheet

I am creating a function which prototype is:
Function TableToText(ByVal Table As Range) As String
This function is supposed to give me a string and not modify the sheet at all. However the operations that I perform on Table (which is a Range) inside the function also modify my sheet.
I thought ByVal which is the default was supposed to prevent that?
I tried in my function to make another range but to make another Range you use Set so it wouldn't have solved the problem anyway...
Can someone point out what I am missing here? Thank you in advance! :)
The whole function is
Function TableToText(Table As Range) As String
Dim nbColumns As Integer, nbRows As Integer
Dim i As Integer, j As Integer, s As Integer
Dim max As Integer, difference As Integer
nbColumns = Table.Columns.Count
nbRows = Table.Rows.Count
With Table
'' adding the spaces
For j = 1 To nbColumns
max = 0
' Find the longest string in the column
For i = 1 To nbRows
If Len(.Cells(i, j).Value) > max Then
max = Len(.Cells(i, j).Value)
End If
Next i
' Adding the spaces and the |
For i = 1 To nbRows
If Len(.Cells(i, j).Value) < max Then
difference = max - Len(.Cells(i, j).Value)
For s = 1 To difference
.Cells(i, j) = CStr(.Cells(i, j).Value) + " "
Next s
End If
.Cells(i, j) = CStr(.Cells(i, j).Value) + "|"
Next i
Next j
'' Creating the plain text table string
For i = 1 To nbRows
For j = 1 To nbColumns
TableToText = TableToText + .Cells(i, j).Value
Next j
TableToText = TableToText & vbCrLf
Next i
End With
End Function
There's a lot of misleading information and confusion on this page.
I thought ByVal which is the default was supposed to prevent that?
The default is ByRef in VBA. This is unfortunate, because the vast majority of the time, what you mean to do is to pass things ByVal.
Objects are always passed by reference [...] ByVal is ignored.
No. Objects are never "passed", period. What's passed byref/byval is a reference to the object, i.e. a pointer. That does not mean object parameters are always passed ByRef at all.
Let's debunk this claim once and for all.
Public Sub DebunkObjectsAreAlwaysPassedByRefClaim()
Dim thing As Collection 'any object type will do
Set thing = New Collection
DoSomethingByVal thing 'we pass a COPY of the pointer
Debug.Print thing.Count 'no problems here
DoSomethingByRef thing 'we pass a reference to our local pointer; what could possibly go wrong?
Debug.Print thing.Count 'error 91! the object reference is gone!
End Sub
Private Sub DoSomethingByVal(ByVal o As Object)
Set o = Nothing 'affects the local copy only
End Sub
Private Sub DoSomethingByRef(ByRef o As Object)
Set o = Nothing 'affects the same object pointer the caller gave us. this is bad.
End Sub
ByRef vs ByVal makes a major difference: give a procedure your object pointer (ByRef), and they can do anything they like with it - including Set-assigning it to a completely different object reference, or making it Nothing. Give a procedure a copy of your object pointer (ByVal), and whatever they do with it (including Set-assigning it to a completely different object reference or making it Nothing) will only affect that copy.
In both cases, whether you've passed the pointer itself or a copy of it, either way you're giving the procedure access to the same object, so as GSerg explained, any instruction that affects a Range (which you can't create - all Range objects belong to Excel, all you ever get is a pointer to one), regardless of where the pointer comes from, will affect the Range instance state.
So if you don't want to affect any worksheet, don't affect any Range and work with arrays instead.
Objects are always passed by reference even if you specify the byval keyword.
You should use a temporary array to store your values.
For example, something like that :
Function TableToText(Table As Range) As String
Dim nbColumns As Integer, nbRows As Integer
Dim i As Integer, j As Integer, s As Integer
Dim max As Integer, difference As Integer
nbColumns = Table.Columns.Count
nbRows = Table.Rows.Count
Dim tmpValues(nbRows, nbColumns) As String
With Table
'' adding the spaces
For j = 1 To nbColumns
max = 0
' Find the longest string in the column
For i = 1 To nbRows
If Len(.Cells(i, j).Value) > max Then
max = Len(.Cells(i, j).Value)
End If
Next i
' Adding the spaces and the |
For i = 1 To nbRows
If Len(.Cells(i, j).Value) < max Then
difference = max - Len(.Cells(i, j).Value)
For s = 1 To difference
tmpValues(i, j) = CStr(.Cells(i, j).Value) + " "
Next s
End If
tempValues(i, j) = CStr(.Cells(i, j).Value) + "|"
Next i
Next j
'' Creating the plain text table string
For i = 1 To nbRows
For j = 1 To nbColumns
TableToText = TableToText + tmpValues(i, j)
Next j
TableToText = TableToText & vbCrLf
Next i
End With
End Function
Hope it helps.

VBA text loop optimisation - Extract emails from text

I need a bit of help with a small project. I just started VBA and I think I could use learning to optimise my code.
Cell A2, contains a text with many email address separated by ",". I managed to extract all the email addresses but I think I made too much use of cells, and I was wondering if you can help me reduce that and use the variables defined instead.
Screenshot of the working code
Sub fpor()
Dim Text As String
Dim full As Integer
Dim i As Integer
Dim e As Integer
Dim part As String
Dim part_len As Integer
Dim part_a As Integer
Dim Text_2 As String
x = 5
ActiveCell = Range("A2")
Text = Range("A2")
full = Len(Text)
'full = InStrRev(Text, ",")
For i = 1 To full
Cells((x + i), 1).Value = Text
part = InStr(Text, ",")
Cells((x + i), 2).Value = part
Cells((x + i), 3) = Left(Text, part)
Cells((x + i), 4) = full - part
Text = Right(Cells((x + i), 1), Cells((x + i), 4))
If part = 0 Then
full = 0
Cells((x + i), 3) = Text
Exit For
Else:
full = Len(Text)
End If
Next i
MsgBox (full)
MsgBox (part)
End Sub `
How do you think I can better optimise the For Loop?
Thank you all for your answers you awesome people : )
you can greatly simplify your code with the use of Split() Function as follows:
Option Explicit
Sub fpor()
Dim emailsArr As Variant
With Worksheets("emails") '<--change "emails" with your actual sheet name
emailsArr = Split(.Range("a2"), ",") '<--| split all emails names delimited by a ',' into an array
.Range("A6").Resize(UBound(emailsArr)).value = Application.Transpose(emailsArr) '<--| write array content from cell A6 downwards
End With
End Sub

Type-mismatch in excel VBA

I try to learn VBA. This code:
Dim i As Integer
Dim damage As String
i = 1
Do While 1
damage = CStr(Worksheets("charakters").Range("d14").Value)
you_min_damage = CInt(Left(damage, i))
If Right(i, 0) = "-" Then
Trim (you_min_damage)
Exit Do
End If
i = i + 1
Loop
cause this problem (in 4 iteration):
In cell D14 I have "4 - 11". I want to separate first number nad change it to integer.
You_min_damage is integer.
Try this one:
Dim you_min_damage As Integer, you_max_damage As Integer
Dim arr
'store all values in array
arr = Split(Worksheets("charakters").Range("d14").Value, "-")
'get first value
you_min_damage = CInt(arr(0))
'get last value
you_max_damage = CInt(arr(UBound(arr)))

Split and sort strings components using Excel

I have a column in Excel with the format:
A01G45B45D12
I need a way to format it like this, that is divide the string into groups of three characters, sort the groups alphabetically and then join them together with a + sign between:
A01+B45+D12+G45
I wonder it this is possible using the built in formulas in Excel or if I have to do this using VBA or something else, I already have the code for this in C# if there is an easy way to use that from Excel. I have not written plugins for Excel before.
Edit to add:
The above is just an example, the string can be of "any length" but its always divisible by three and the order is random so I cannot assume anything about the order beforehand.
Sub ArraySort()
Dim strStarter As String
Dim strFinish As String
Dim intHowMany As Integer
Dim intStartSlice As Integer
strStarter = ActiveCell.Offset(0, -1).Value 'Pulls value from cell to the left
intHowMany = Int(Len(strStarter) / 3)
ReDim arrSlices(1 To intHowMany) As String
intStartSlice = 1
For x = 1 To intHowMany
arrSlices(x) = Mid(strStarter, intStartSlice, 3)
intStartSlice = intStartSlice + 3
Next x
Call BubbleSort(arrSlices)
For x = 1 To intHowMany
strFinish = strFinish + arrSlices(x) & "+"
Next x
strFinish = Left(strFinish, Len(strFinish) - 1)
ActiveCell.Value = strFinish 'Puts result into activecell
End Sub
Sub BubbleSort(list() As String)
'Taken from power programming with VBA
'It’s a sorting procedure for 1-dimensional arrays named List
'The procedure takes each array element, if it is greater than the next element, the two elements swap positions.
'The evaluation is repeated for every pair of items (that is n-1 times)
Dim First As Integer, Last As Long
Dim i As Long, j As Long
Dim temp As String
First = LBound(list)
Last = UBound(list)
For i = First To Last - 1
For j = i + 1 To Last
If list(i) > list(j) Then
temp = list(j)
list(j) = list(i)
list(i) = temp
End If
Next j
Next i
End Sub

Resources