My string is su=45, nita = 30.8, raj = 60, gita = 40.8 . This has reference to SO question Extract maximum number from a string
I am utilizing maxNums function and getting result as 40.8 whereas I would like it to be 60. Where an amendment in code line would get me the desired result.Code reproduced below to avoid cross reference.If this string contains all numbers with decimal point then I get the correct result but the data in consideration from external sources could have whole numbers.
Option Explicit
Option Base 0 '<~~this is the default but I've included it because it has to be 0
Function maxNums(str As String)
Dim n As Long, nums() As Variant
Static rgx As Object, cmat As Object
'with rgx as static, it only has to be created once; beneficial when filling a long column with this UDF
If rgx Is Nothing Then
Set rgx = CreateObject("VBScript.RegExp")
End If
maxNums = vbNullString
With rgx
.Global = True
.MultiLine = False
.Pattern = "\d*\.\d*"
If .Test(str) Then
Set cmat = .Execute(str)
'resize the nums array to accept the matches
ReDim nums(cmat.Count - 1)
'populate the nums array with the matches
For n = LBound(nums) To UBound(nums)
nums(n) = CDbl(cmat.Item(n))
Next n
'test array
'Debug.Print Join(nums, ", ")
'return the maximum value found
maxNums = Application.Max(nums)
End If
End With
End Function
There are one or two issues with your code. The first one is that the regular expression isn't looking for decimal numbers. If you change it to
.Pattern = "\d+\.?(\d?)+"
it will work better. In short:
\d+ = At least one digit
.? = An optional dot
(\d?)+ = Optional numbers
This is not a waterproof expression, but it works to some extent at least.
The second issue is the potential problem of differing decimal symbols, in which case you will need to do some search and replace before processing.
If its always x=number I think it's simpler to loop over each delimited value then read past the = for the value:
Function MaxValue(data As String)
Dim i As Long, value As Double
Dim tokens() As String: tokens = Split(data, ",")
For i = 0 To UBound(tokens)
'// get the value after = as a double
value = CDbl(Trim$(Mid$(tokens(i), InStr(tokens(i), "=") + 1)))
If (value > MaxValue) Then MaxValue = value
Next
End Function
Without Regex:
Public Function maxNums(str As String) As Double
Dim i As Long, L As Long, s As String, wf As WorksheetFunction, brr()
Set wf = Application.WorksheetFunction
L = Len(str)
For i = 1 To L
s = Mid(str, i, 1)
If s Like "[0-9]" Or s = "." Then
Else
Mid(str, i, 1) = " "
End If
Next i
str = wf.Trim(str)
arr = Split(str, " ")
ReDim brr(LBound(arr) To UBound(arr))
For i = LBound(arr) To UBound(arr)
brr(i) = CDbl(arr(i))
Next i
maxNums = wf.Max(brr)
End Function
Related
I have done extensive research on SO and google but not coming up with a solution.
While this question may have been asked earlier, my situation is slightly different
The function works fine if there is a space between the text and numbers. However, does not work if there aren't any spaces.
Function SumNumbers(rngS As Range, Optional strDelim As String = " ") As Double
Dim xNums As Variant, lngNum As Long
xNums = Split(rngS, strDelim)
For lngNum = LBound(xNums) To UBound(xNums) Step 1
SumNumbers = SumNumbers + Val(xNums(lngNum))
Next lngNum
End Function
My cell data looks like this: su9m11w11.5th8
I tried adding an alphabet array, but had no luck. Help is welcome.
strDelim = Array("F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "U")
Well, depening on your data, you could try:
Sub Test()
Dim StrIn As String: StrIn = "su9m11w11.5th8"
Debug.Print SumDigits(StrIn)
End Sub
Function SumDigits(str As String) As Double
With CreateObject("vbscript.regexp")
.Global = True
.Pattern = "[a-z]+"
SumDigits = Application.Evaluate(.Replace(str, "+") & ".0")
End With
End Function
Here I used a regular expression to substitute all the lowercase characters ranging a-z for a "+". The resulting string then can be evaluated to return the sum.
EDIT:
Another way could be to use a regular expression that will return all the possible (negative) numbers, and sum those:
Function SumDigits(str As String) As Double
With CreateObject("vbscript.regexp")
.Global = True
.Pattern = "-?\d+(?:\.\d+)?"
If .Test(str) Then
For Each Match In .Execute(str)
SumDigits = SumDigits + Match
Next
End If
End With
EDIT 2:
To return an average we should capture the amount of matching substrings and devide the sum by that count:
Function SumDigits(str As String, Optional avg As Boolean) As Double
With CreateObject("vbscript.regexp")
.Global = True
.Pattern = "-?\d+(?:\.\d+)?"
If .Test(str) Then
Set Matches = .Execute(str)
For Each Match In Matches
SumDigits = SumDigits + Match
Next
If avg Then SumDigits = SumDigits / Matches.Count
End If
End With
End Function
Call using Debug.Print SumDigits(StrIn, True) instead.
Here is a VBA function to produce either the sum or the average of your embedded numbers... and it handles negatives as well. And this is pure Excel VBA with no references to outside libraries:
Function SumEmbeddedNumbers#(s$, Optional bAverage As Boolean)
Dim i&, p&, max&, t&, pluses&
Dim b() As Byte, res() As Byte
Static keep() As Boolean
Const VALS$ = "0123456789.-"
If (Not Not keep) = 0 Then
ReDim keep(0 To 255)
For i = 1 To Len(VALS)
keep(Asc(Mid$(VALS, i, 1))) = 1
Next
End If
max = LenB(s)
ReDim res(0 To max)
b = StrConv(s, vbFromUnicode)
For i = 0 To Len(s) - 1
t = b(i)
If keep(t) Then
res(p) = t
p = p + 1
Else
If p Then
If res(p - 1) <> 43 Then
res(p) = 43
pluses = pluses + 1
p = p + 1
End If
End If
End If
Next
SumEmbeddedNumbers = Evaluate(Left$(StrConv(res, vbUnicode), p))
If bAverage Then SumEmbeddedNumbers = SumEmbeddedNumbers / (pluses + 1)
End Function
MsgBox SumEmbeddedNumbers("su9m11w11.5th8") '<---displays: 39.5
MsgBox SumEmbeddedNumbers("su9m11w11.5th8", True) '<---displays: 9.875
Does excel vba have a function to sort a given word or string alphabetically? Also, what is this kind of a string manipulation called in technical/programming terms?
For e.g. Word = "Somestring"
Output = "egimnorSst"
Thanks.
If you have Excel O365 with the functions I've used below, you can use this formula:
=TEXTJOIN(,,SORT(MID(A1,SEQUENCE(LEN(A1)),1)))
or as indicated by #JvdV, instead of TEXTJOIN we can use the simpler:
=CONCAT(SORT(MID(A1,SEQUENCE(LEN(A1)),1)))
If y0u don't have those functions, you would need a UDF written in VBA.
Here is one that, since the sort strings should be relatively short, uses a simple Bubblesort to sort the string elements.
Option Explicit
Option Compare Text
Function sortString(S As String) As String
Dim str() As String
Dim I As Long
ReDim str(1 To Len(S))
For I = 1 To Len(S)
str(I) = Mid(S, I, 1)
Next I
BubbleSort str
sortString = Join(str, "")
End Function
Sub BubbleSort(TempArray)
'copied directly from support.microsoft.com
Dim temp As Variant
Dim I As Integer
Dim NoExchanges As Integer
' Loop until no more "exchanges" are made.
Do
NoExchanges = True
' Loop through each element in the array.
For I = LBound(TempArray) To UBound(TempArray) - 1
' If the element is greater than the element
' following it, exchange the two elements.
If TempArray(I) > TempArray(I + 1) Then
NoExchanges = False
temp = TempArray(I)
TempArray(I) = TempArray(I + 1)
TempArray(I + 1) = temp
End If
Next I
Loop While Not (NoExchanges)
End Sub
Though the question itself is very minimal I would like to answer nonetheless. If you not bothered having S and s reversed than:
Sub Test()
Dim x As Long
Dim str As String: str = "Somestring"
With CreateObject("System.Collections.ArrayList")
For x = 1 To Len(str)
.Add Mid(str, x, 1)
.Sort
Next
Debug.Print Join(.Toarray, "")
End With
End Sub
Results in:
egimnorsSt
If that is not what you want it becomes a bit more complicated I think since we cannot use ASCII codes (S = 83 and way lower than the other characters).
It may not be super pretty but try:
Sub Test()
Dim x As Long
Dim str As String, str_new As String
str = "abcdABCD"
With CreateObject("System.Collections.ArrayList")
For x = 1 To Len(str)
.Add Mid(str, x, 1)
.Sort
Next
str_new = Join(.Toarray, "")
End With
With CreateObject("vbscript.regexp")
.Global = True
.IgnoreCase = True
.Pattern = "([a-z])\1+"
If .Test(str_new) Then
For Each Match In .Execute(str_new)
str_new = Replace(str_new, Match, Application.Proper(Match)) 'Assuming no more than 1 of the same uppercase letters.
Next
End If
End With
Debug.Print str_new
End Sub
Results in:
AaBbCcDd
Another option if you have ExcelO365 with new DA-functions and value in A1:
=CONCAT(SORT(MID(A1,ROW(A1:INDEX(A:A,LEN(A1))),1)))
This would actually return egimnorSst
I created a UDF to parse time-starts from a delimited string.
- Returns an Array(0 to 23) that represent hours in the day
- Each time-start is separated by a comma
- # is used to signify multiple time-starts
For example 5#8p returns 5 as the 20th element in the 0 based array.
AssignmentList("2#12a,3#6a,10#12p,6p,5#8p")(0)
Sub Setup()
Range("A1:AA1").Value = Array("1st", "2nd", "3rd", "12PM", "1AM", "2AM", "3AM", "4AM", "5AM", "6AM", "7AM", "8AM", "9AM", "10AM", "11AM", "12PM", "1PM", "2PM", "3PM", "4PM", "5PM", "6PM", "7PM", "8PM", "9PM", "10PM", "11PM")
Range("A2:C2").Value = Array("12a", "10a,3#12p", "6p,5#8p")
Range("D2:AA2").FormulaArray = "=AssignmentList($A2:$C2)"
End Sub
Function AssignmentList(ByRef Source As Variant) As Variant
Dim Assignments(0 To 23) As Double
Dim Item As Variant, At As Variant
Dim Text As String
Text = WorksheetFunction.TextJoin(",", True, Source)
For Each Item In Split(Text, ",")
If InStr(Item, "#") > 0 Then
At = Split(Item, "#")
Assignments(Hour(At(1))) = Assignments(Hour(At(1))) + At(0)
Else
Assignments(Hour(Item)) = Assignments(Hour(Item)) + 1
End If
Next
AssignmentList = Assignments
End Function
I would like to convert this function to an Array Formula but do not know where to start. References or advice as where to start would be greatly appreciated.
I am also interested in anyway that I could improve my UDF. Ultimately, I will use whichever function gives me the best performance.
I would stick with the UDF -- it will be much simpler to maintain.
I wouldn't bother with joining.
I'd modify your routine a bit, but retain similar logic:
Unless you will be dealing with fractions or very large numbers, I'd use Long instead of Double.
Function AssignmentList(Source) As Long()
Dim Assignments(1 To 1, 1 To 24) As Long
Dim I As Long, V As Variant, W As Variant
Dim vSrc As Variant
Dim t As Date, l As Long
vSrc = Source 'assumed to be a single horizontal row
For I = LBound(vSrc, 2) To UBound(vSrc, 2)
V = Split(vSrc(1, I), ",")
For Each W In V
If InStr(W, "#") > 0 Then
l = Split(W, "#")(0)
t = Split(W, "#")(1)
Else
l = 1
t = W
End If
Assignments(1, Hour(t) + 1) = l
Next W
Next I
AssignmentList = Assignments
End Function
I have a long string of text that contains somewhere in the string a either one or two numbers followed by from 1 to 3 letters. I need to extract the number(s) and letter(s) together into a separate cell. In case there are multiple instances of this pattern I would like to have excel show them in the same cell separated by commas.
For example:
A1: In position 2G there was fault found. B1: 2G
or
A1: In case 32AB the top is missing. B1: 32AB
or
A1: In position 2G there was a fault found. In case 32AB the top is missing. B1: 2G, 32AB.
I believe I figured out the basic case here:
Function ExtractPos(ByVal text As String) As String
Dim result As String
Dim allMatches As Object
Dim RE As Object
Set RE = CreateObject("vbscript.regexp")
RE.Pattern = "(\d{1,2}\w{1,4})"
RE.Global = True
RE.IgnoreCase = True
Set allMatches = RE.Execute(text)
If allMatches.Count <> 0 Then
result = allMatches.Item(0).SubMatches.Item(0)
End If
ExtractPos = result
End Function
This seems to work for returning multiple results where the delimiter is specified in the function call:
Function RegexExtract(ByVal text As String, _
Optional seperator As String = "") As String
Dim i As Long, j As Long
Dim result As String
Dim allMatches As Object
Dim RE As Object
Set RE = CreateObject("vbscript.regexp")
RE.Pattern = "(\d{1,2}\w{1,4})"
RE.Global = True
Set allMatches = RE.Execute(text)
For i = 0 To allMatches.Count - 1
For j = 0 To allMatches.Item(i).submatches.Count - 1
result = result & seperator & allMatches.Item(i).submatches.Item(j)
Next
Next
If Len(result) <> 0 Then
result = Right(result, Len(result) - Len(seperator))
End If
RegexExtract = result
End Function
I'm still trying to determine how to eliminate any values that match this pattern from being returned:
"(\d{2}JAN|\d{2}FEB|\d{2}MAR|\d{2}APR|\d{2}MAY|\d{2}JUN|\d{2}JUL|\d{2}AUG|\d{2}SEP|\d{2}OCT|\d{2}NOV|\d{2}DEC)"
How is it possible to split a VBA string into an array of characters?
I tried Split(my_string, "") but this didn't work.
Safest & simplest is to just loop;
Dim buff() As String
ReDim buff(Len(my_string) - 1)
For i = 1 To Len(my_string)
buff(i - 1) = Mid$(my_string, i, 1)
Next
If your guaranteed to use ansi characters only you can;
Dim buff() As String
buff = Split(StrConv(my_string, vbUnicode), Chr$(0))
ReDim Preserve buff(UBound(buff) - 1)
You can just assign the string to a byte array (the reverse is also possible). The result is 2 numbers for each character, so Xmas converts to a byte array containing {88,0,109,0,97,0,115,0} or you can use StrConv
Dim bytes() as Byte
bytes = StrConv("Xmas", vbFromUnicode)
which will give you {88,109,97,115} but in that case you cannot assign the byte array back to a string. You can convert the numbers in the byte array back to characters using the Chr() function
Here's another way to do it in VBA.
Function ConvertToArray(ByVal value As String)
value = StrConv(value, vbUnicode)
ConvertToArray = Split(Left(value, Len(value) - 1), vbNullChar)
End Function
Sub example()
Dim originalString As String
originalString = "hi there"
Dim myArray() As String
myArray = ConvertToArray(originalString)
End Sub
According to this code golfing solution by Gaffi, the following works:
a = Split(StrConv(s, 64), Chr(0))
the problem is that there is no built in method (or at least none of us could find one) to do this in vb. However, there is one to split a string on the spaces, so I just rebuild the string and added in spaces....
Private Function characterArray(ByVal my_string As String) As String()
'create a temporary string to store a new string of the same characters with spaces
Dim tempString As String = ""
'cycle through the characters and rebuild my_string as a string with spaces
'and assign the result to tempString.
For Each c In my_string
tempString &= c & " "
Next
'return return tempString as a character array.
Return tempString.Split()
End Function
To split a string into an array of sub-strings of any desired length:
Function charSplitMulti(s As Variant, splitLen As Long) As Variant
Dim padding As Long: padding = 0
Dim l As Long: l = 0
Dim v As Variant
'Pad the string so it divides evenly by
' the length of the desired sub-strings
Do While Len(s) Mod splitLen > 0
s = s & "x"
padding = padding + 1
Loop
'Create an array with sufficient
' elements to hold all the sub-strings
Do Until Len(v) = (Len(s) / splitLen) - 1
v = v & ","
Loop
v = Split(v, ",")
'Populate the array by repeatedly
' adding in the first [splitLen]
' characters of the string, then
' removing them from the string
Do While Not s Like ""
v(l) = Mid(s, 1, splitLen)
s = Right(s, Len(s) - splitLen)
l = l + 1
Loop
'Remove any padding characters added at step one
v(UBound(v)) = Left(v(UBound(v)), Len(v(UBound(v))) - padding)
'Output the array
charSplitMulti = v
End Function
You can pass the string into it either as a string:
Sub test_charSplitMulti_stringInput()
Dim s As String: s = "123456789abc"
Dim subStrLen As Long: subStrLen = 4
Dim myArray As Variant
myArray = charSplitMulti(s, subStrLen)
For i = 0 To UBound(myArray)
MsgBox myArray(i)
Next
End Sub
…or already declard as a variant:
Sub test_charSplitMulti_variantInput()
Dim s As Variant: s = "123456789abc"
Dim subStrLen As Long: subStrLen = 5
s = charSplitMulti(s, subStrLen)
For i = 0 To UBound(s)
MsgBox s(i)
Next
End Sub
If the length of the desired sub-string doesn't divide equally into the length of the string, the uppermost element of the array will be shorter. (It'll be equal to strLength Mod subStrLength. Which is probably obvious.)
I found that most-often I use it to split a string into single characters, so I added another function, so I can be lazy and not have to pass two variables in that case:
Function charSplit(s As Variant) As Variant
charSplit = charSplitMulti(s, 1)
End Function
Sub test_charSplit()
Dim s As String: s = "123456789abc"
Dim myArray As Variant
myArray = charSplit(s)
For i = 0 To UBound(myArray)
MsgBox myArray(i)
Next
End Sub
Try this minicode From Rara:
Function charSplitMulti(TheString As Variant, SplitLen As Long) As Variant
'Defining a temporary array.
Dim TmpArray() As String
'Checking if the SplitLen is not less than one. if so the function returns the whole string without any changing.
SplitLen = IIf(SplitLen >= 1, SplitLen, Len(TheString))
'Redefining the temporary array as needed.
ReDim TmpArray(Len(TheString) \ SplitLen + IIf(Len(TheString) Mod SplitLen <> 0, 1, 0))
'Splitting the input string.
For i = 1 To UBound(TmpArray)
TmpArray(i) = Mid(TheString, (i - 1) * SplitLen + 1, SplitLen)
Next
'Outputing the result.
charSplitMulti = TmpArray
End Function