I'd like to create a function in vba to extract the first nth words from a string and to look like this
ExtractWords(affected_text, delimiter, number_of_words_to_extract)
I tried a solution but it only extracts the first two words.
Function FirstWords(myStr As Variant, delimiter,words_to_extract) As Variant
FirstWords = Left(myStr, InStr(InStr(1, myStr, delimiter) + 1, myStr, delimiter, vbTextCompare) - 1)
End Function
Any ideas? Thanks
Use Split() function. It returns array of String, split using the delimiter and limit of words you specify.
Dim Result As Variant
Result = Split("Alice,Bob,Chuck,Dave", ",") 'Result: {"Alice,"Bob","Chuck","Dave"}
Result = Split("Alice,Bob,Chuck,Dave", ",", 2) 'Result: {"Alice,"Bob"}
#Taosique's answer using Split is excellent, but if you want the result returned as a string you can do the following:
Function FirstWords(myStr As String, delimiter As String, words_to_extract As Long) As Variant
Dim i As Long, k As Long
For i = 1 To Len(myStr)
If Mid(myStr, i, 1) = delimiter Then
k = k + 1
If k = words_to_extract Then
FirstWords = Mid(myStr, 1, i)
Exit Function
End If
End If
Next I
'if you get to here -- trouble
'unless the delimiter count is words_to_extract - 1
If k = words_to_extract - 1 Then
FirstWords = myStr
Else
FirstWords = CVErr(xlErrValue)
End If End Function
Sub test()
Debug.Print FirstWords("This is a test. I hope it works", " ", 4)
Debug.Print FirstWords("This is a test. I hope it works", " ", 10)
End Sub
When test is run it first displays the string "This is a test." then prints an error condition.
Much the same effect as the above can be achieved by first splitting the string using Split and then rejoining it using Join. A subtle difference is the behavior if there are less than words_to_extract words. The Split then Join approach will return the whole string. The above code treats this as an error condition and, if used as a UDF worksheet function, will display #VALUE! in any cell that contains it.
Related
I have a string in a cell composed of several shorter strings of various lengths with blank spaces and commas in between. In some cases only one or more blanks are in between.
I want to remove every blank space and comma and only leave behind 1 comma between each string element. The result must look like this:
The following doesn't work. I'm not getting an error but the strings are truncated at the wrong places. I don't understand why.
Sub String_adaption()
Dim i, j, k, m As Long
Dim STR_A As String
STR_A = "01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
i = 1
With Worksheets("table")
For m = 1 To Len(.Range("H" & i))
j = 1
Do While Mid(.Range("H" & i), m, 1) = "," And Mid(.Range("H" & i), m - 1, 1) <> Mid(STR_A, j, 1) And m <> Len(.Range("H" & i))
.Range("H" & i) = Mid(.Range("H" & i), 1, m - 2) & Mid(.Range("H" & i), m, Len(.Range("H" & i)))
j = j + 1
Loop
Next m
End With
End Sub
I'd use a regular expression to replace any combination of spaces and comma's. Something along these lines:
Sub Test()
Dim str As String: str = "STRING_22 ,,,,,STRING_1 , , ,,,,,STRING_333 STRING_22 STRING_4444"
Debug.Print RegexReplace(str, "[\s,]+", ",")
End Sub
Function RegexReplace(x_in, pat, repl) As String
With CreateObject("vbscript.regexp")
.Global = True
.Pattern = pat
RegexReplace = .Replace(x_in, repl)
End With
End Function
Just for the sake of alternatives:
Formula in B1:
=TEXTJOIN(",",,TEXTSPLIT(A1,{" ",","}))
The following function will split the input string into pieces (words), using a comma as separator. When the input string has multiple commas, it will result in empty words.
After splitting, the function loops over all words, trims them (remove leading and trailing blanks) and glue them together. Empty words will be skipped.
I have implemented it as Function, you could use it as UDF: If your input string is in B2, write =String_adaption(B2) as Formula into any cell.
Function String_adaption(s As String) As String
' Remove duplicate Commas and Leading and Trailing Blanks from words
Dim words() As String, i As Long
words = Split(s, ",")
For i = 0 To UBound(words)
Dim word As String
word = Trim(words(i))
If word <> "" Then
String_adaption = String_adaption & IIf(String_adaption = "", "", ",") & word
End If
Next i
End Function
P.S.: Almost sure that this could be done with some magic regular expressions, but I'm not an expert in that.
If you have recent Excel version, you can use simple worksheet function to split the string on space and on comma; then put it back together using the comma deliminater and ignoring the blanks (and I just noted #JvdV had previously posted the same formula solution):
=TEXTJOIN(",",TRUE,TEXTSPLIT(A1,{" ",","}))
In VBA, you can use a similar algorithm, using the ArrayList object to collect the non-blank results.
Option Explicit
Function commaOnly(s As String) As String
Dim v, w, x, y
Dim al As Object
Set al = CreateObject("System.Collections.ArrayList")
v = Split(s, " ")
For Each w In v
x = Split(w, ",")
For Each y In x
If y <> "" Then al.Add y
Next y
Next w
commaOnly = Join(al.toarray, ",")
End Function
This preserves the spaces within the smaller strings.
Option Explicit
Sub demo()
Const s = "STRING 22,,,, ,,STRING 1,,,, ,,STRING 333 , , , STRING_22 STRING_44"
Debug.Print Cleanup(s)
End Sub
Function Cleanup(s As String) As String
Const SEP = ","
Dim regex, m, sOut As String, i As Long, ar()
Set regex = CreateObject("vbscript.regexp")
With regex
.Global = True
.MultiLine = False
.IgnoreCase = True
.Pattern = "([^,]+)(?:[ ,]*)"
End With
If regex.Test(s) Then
Set m = regex.Execute(s)
ReDim ar(0 To m.Count - 1)
For i = 0 To UBound(ar)
ar(i) = Trim(m(i).submatches(0))
Next
End If
Cleanup = Join(ar, SEP)
End Function
Code categories approach
For the sake of completeness and to show also other ways "leading to Rome", I want to demonstrate an approach allowing to group the string input into five code categories in order to extract alphanumerics by a tricky match (see [B] Function getCats()):
To meet the requirements in OP use the following steps:
1) remove comma separated tokens if empty or only blanks (optional),
2) group characters into code categories,
3) check catCodes returning alpha nums including even accented or diacritic letters as well as characters like [ -,.+_]
Function AlphaNum(ByVal s As String, _
Optional IgnoreEmpty As Boolean = True, _
Optional info As Boolean = False) As String
'Site: https://stackoverflow.com/questions/15723672/how-to-remove-all-non-alphanumeric-characters-from-a-string-except-period-and-sp/74679416#74679416
'Auth.: https://stackoverflow.com/users/6460297/t-m
'Date: 2023-01-12
'1) remove comma separated tokens if empty or only blanks (s passed as byRef argument)
If IgnoreEmpty Then RemoveEmpty s ' << [A] RemoveEmpty
'2) group characters into code categories
Dim catCodes: catCodes = getCats(s, info) ' << [B] getCats()
'3) check catCodes and return alpha nums plus chars like [ -,.+_]
Dim i As Long, ii As Long
For i = 1 To UBound(catCodes)
' get current character
Dim curr As String: curr = Mid$(s, i, 1)
Dim okay As Boolean: okay = False
Select Case catCodes(i)
' AlphaNum: cat.4=digits, cat.5=alpha letters
Case Is >= 4: okay = True
' Category 2: allow only space, comma, minus
Case 2: If InStr(" -,", curr) <> 0 Then okay = True
' Category 3: allow only point, plus, underline
Case 3: If InStr(".+_", curr) <> 0 Then okay = True
End Select
If okay Then ii = ii + 1: catCodes(ii) = curr ' increment counter
Next i
ReDim Preserve catCodes(1 To ii)
AlphaNum = Join(catCodes, vbNullString)
End Function
Note: Instead of If InStr(" -,", curr) <> 0 Then in Case 2 you may code If curr like "[ -,]" Then, too. Similar in Case 3 :-)
[A] Helper procedure RemoveEmpty
Optional clean-up removing comma separated tokens if empty or containing only blanks:
Sub RemoveEmpty(ByRef s As String)
'Purp: remove comma separated tokens if empty or only blanks
Const DEL = "$DEL$" ' temporary deletion marker
Dim i As Long
Dim tmp: tmp = Split(s, ",")
For i = LBound(tmp) To UBound(tmp)
tmp(i) = IIf(Len(Trim(tmp(i))) = 0, DEL, Trim(tmp(i)))
Next i
tmp = Filter(tmp, DEL, False) ' remove marked elements
s = Join(tmp, ",")
End Sub
[B] Helper function getCats()
A tricky way to groups characters into five code categories, thus building the basic logic for any further analyzing:
Function getCats(s, Optional info As Boolean = False)
'Purp.: group characters into five code categories
'Auth.: https://stackoverflow.com/users/6460297/t-m
'Site: https://stackoverflow.com/questions/15723672/how-to-remove-all-non-alphanumeric-characters-from-a-string-except-period-and-sp/74679416#74679416
'Note: Cat.: including:
' 1 ~~> apostrophe '
' 2 ~~> space, comma, minus etc
' 3 ~~> point separ., plus etc
' 4 ~~> digits 0..9
' 5 ~~> alpha (even including accented or diacritic letters!)
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'a) get array of single characters
Const CATEG As String = "' - . 0 A" 'define group starters (case indep.)
Dim arr: arr = Char2Arr(s) ' << [C] Char2Arr()
Dim chars: chars = Split(CATEG)
'b) return codes per array element
getCats = Application.Match(arr, chars) 'No 3rd zero-argument!!
'c) display in immediate window (optionally)
If info Then Debug.Print Join(arr, "|") & vbNewLine & Join(getCats, "|")
End Function
[C] Helper function Char2Arr
Assigns every string character to an array:
Function Char2Arr(ByVal s As String)
'Purp.: assign single characters to array
s = StrConv(s, vbUnicode)
Char2Arr = Split(s, vbNullChar, Len(s) \ 2)
End Function
I'm looking for some help please with some VBA.
I have the next table
header1
000Model Test0Model Val00User0
Perman000User0Model Name000000
000Perman00000000000000000000Name
So I need to replace all Ceros with only one "," like this
header1
,Model Test,Model Val,User,
Perman,User,Model Name,
,Perman,Name
Is there a combination of formulas to do this? or with code in VBA?
Please, try the next function:
Function replace0(x As String) As String
Dim matches As Object, mch As Object, arr, k As Long
ReDim arr(Len(x))
With CreateObject("VbScript.regexp")
Pattern = "[0]{1,30}"
.Global = True
If .test(x) Then
replace0 = .replace(x, ",")
End If
End With
End Function
It can be tested using:
Sub replaceAllzeroByComma()
Dim x As String
x = "000Perman00000000000000000000Name"
'x = "000Model Test0Model Val00User0"
'x = "Perman000User0Model Name000000"
Debug.Print replace0(x)
End Sub
Uncheck the checked lines, one at a time and see the result in Immediate Window (Ctrl + G, being in VBE)
If you have Microsoft 365, you can use:
=IF(LEFT(A1)="0",",","")&TEXTJOIN(",",TRUE,TEXTSPLIT(A1,"0"))&IF(RIGHT(A1)="0",",","")
Split on the zero's
Join the split text with a comma delimiter
Have to specially test first character
and also the last character as pointed out by #T.M.
Another option would be to check a character array as follows:
a) atomize input string to a tmp array of single characters via String2Arr()
b) check for zero characters in tmp via CheckChar
c) execute a negative filtering preserving first zeros in each 0-sequence via Filter(tmp, delChar, False)
d) return joined string
Function Rep0(ByVal s As String, Optional delChar As String = "0")
'Purp.: replace first zero in each 0-sequence by ",", delete any remaining zeros
Dim tmp: tmp = String2Arr(s) ' a) atomize string to character array
Dim i As Long
For i = LBound(tmp) To UBound(tmp) ' b) check zero characters
Dim old As String: CheckChar tmp, i, old, delChar
Next
tmp = Filter(tmp, delChar, False) ' c) negative filtering preserving non-deletes
Rep0 = Join(tmp, vbNullString) ' d) return cleared string
End Function
Help procedures
Sub CheckChar(ByRef arr, ByRef i As Long, ByRef old As String, _
ByVal delChar As String, Optional replChar As String = ",")
'Purp.: replace 1st delChar "0" in array (depending on old predecessor)
If Left(arr(i), 1) = delChar Then ' omit possible string end character
If Not old Like "[" & delChar & replChar & "]" Then arr(i) = replChar
End If
old = arr(i) ' remember prior character
End Sub
Function String2Arr(ByVal s As String)
'Purp.: atomize input string to single characters array
s = StrConv(s, vbUnicode)
String2Arr = Split(s, vbNullChar, Len(s) \ 2)
End Function
I have string containing numbers. These numbers are coordinates for scientific modeling.
I need to split this string by character "-" to partial string "before" and "after".
This works only for static number of digit (character).
Dim str As String
Dim before As String
Dim after As String
str = "3-525"
before= Left(str, InStr(str, "-") - 1) ' =3
after= Right(str, InStr(str, "-") + 1) ' =525
If input is str = "3-525" output is before = 3 and after = 525
But when it comes to str = "15-50" output is before = 15 and after = 5-50 and is annoying to retype it again and again.
I need some dynamic solution to split these coordinates by "-" character.
Use Split:
Sub Test()
Dim str As String
str = "3-525"
Dim x
x = Split(str, "-")
Debug.Print x(0) '<--- this is "before", or 3
Debug.Print x(1) '<--- this is "after", or 525
End Sub
Of course applying Split() is the most evident way to execute a split operation. -
Nevertheless your original code logic to count from an Instr() finding isn't wrong per se and you need not reject the way you form the after variable completely.
(1) Using the Right() function only needs a length as further argument, not a position. So you might calculate the remaining length to the right as difference between the total length and the "-" character and modify your code to
after = Right(s, Len(s) - InStr(s, "-"))
(btw I'd prefer s to your str variable as I don't want to mix it up with VBA's Str() function).
(2) Alternatively you could use the Mid() function and code as follows:
after = Mid(s, InStr(s, "-") + 1)
Here it suffices to pass a starting position as further argument (without need to indicate a total lenght in addition).
Lets say I have a path : stack/overflow/question/help/please .
And end result is : help/please.
Does anyone have a code where I can state how many "/" I want to parse.
its similar to text to columns but I would like to keep it in one cell.
Thanks
You could write a function something like this:
Function RightPart(s As String, d As String, n As Long) As String
Dim A As Variant
Dim i As Long, ub As Long
Dim t As String
A = Split(s, d)
ub = UBound(A)
If n >= ub Then
RightPart = s
Exit Function
End If
For i = ub - n + 1 To ub
t = t & A(i) & IIf(i < ub, d, "")
Next i
RightPart = t
End Function
Then RightPart(":stack/overflow/question/help/please","/",2) evaluates to "help/please"
you could use this code (does a bit more but should be fine):
Public Function custDelim(ByVal str As String, ByVal delim As String, ByVal num As Long) As String
Dim holder As Variant
holder = Split(str, delim)
If num = 0 Then
custDelim = ""
ElseIf num > 0 Then
If num <= UBound(holder) Then
holder = Split(str, delim, UBound(holder) - num + 2)
custDelim = holder(UBound(holder))
Else
custDelim = str
End If
ElseIf num < 0 Then
If Abs(num) <= UBound(holder) Then
ReDim Preserve holder(Abs(num) - 1)
custDelim = Join(holder, delim)
Else
custDelim = str
End If
End If
End Function
=custDelim("very-long-string-in-here","-",2) would output "in-here" while using -2 would print "very-long".
If you still have questions, just ask :)
Option 1: excel-vba
I prefer using the Split function into a variant array when dealing with multiple parts of a string.
Function trim_part_of_a_path(str As String, _
Optional keep As Integer = 1, _
Optional delim As String = "/")
Dim a As Long, tmp As Variant
tmp = Split(str, delim)
If UBound(tmp) < keep Then
trim_part_of_a_path = str
Else
trim_part_of_a_path = tmp(UBound(tmp) - keep)
For a = UBound(tmp) - keep + 1 To UBound(tmp)
trim_part_of_a_path = _
trim_part_of_a_path & delim & tmp(a)
Next a
End If
End Function
You will likely want to change the defults for the optional parameters to whatever you use most commonly.
Syntax: =trim_part_of_a_path(<original string> , [optional number to retain], [optional delimiter])
Examples: =trim_part_of_a_path(A2) =trim_part_of_a_path(A2, C2, B2) =trim_part_of_a_path(A2, 1, "/")
Option 2: excel-formula
The SUBSTITUTE function has an optional [instance_num] parameter which allows you to change one occurrence of a repeated character to something unique which can be located in subsequent function calculation.
A pair of LEN functions with another SUBSTITUTE returns the total number of occurances of a character.
The MID function can use the FIND function to identify the portion of the original text to return from a modified string produced by the functions discussed above.
IFERROR function can return the original string if the parameters are out of bounds.
'return a portion of string while retaining x number of delimiters
=IFERROR(MID(A2, FIND(CHAR(167), SUBSTITUTE(A2, B2, CHAR(167), LEN(A2)-LEN(SUBSTITUTE(A2,B2,""))-C2))+1, LEN(A2)), A2)
A formula based solution probably works best when the parameters can be put into cells that the formula references.
In Mac Excel 2011, I have two strings, each consisting of a space-separated concatenation of smaller, spaceless strings. For example:
"red green blue pink"
"horse apple red monkey pink"
From those, I'd like to extract the intersection string:
"red pink"
I can do it in VB, but I'd prefer to stay in Excel proper. Now I know I could hack something together (in Excel) by making an assumption about the number of smaller component strings within each larger string. I could then chop one of the larger strings into those components and then for each do a FIND() on the second large string, concatenating the result as I went.
Problem is, although here I'm giving only two strings, in practice I have two sets of strings, each containing 20 large strings. So the "chop and walk" approach feels like O(N^2) in terms of space in Excel, and I'm looking for a simpler way.
Any ideas?
I don't think you can do it in a single cell function without using multiple cells or VBA. Define a UDF like the one below and use the new function in the one cell with the syntax
=StringIntersect("a b c","d e b f")
which would return "b"
This function does have the nested loop but on string arrays I imagine it will be quick enough
Function StringIntersect(s1 As String, s2 As String) As String
Dim arys1() As String
Dim arys2() As String
Dim arysub() As String
Dim i as integer
Dim j as integer
arys1 = Split(s1, " ")
arys2 = Split(s2, " ")
For i = LBound(arys1) To UBound(arys1)
For j = LBound(arys2) To UBound(arys2)
If arys1(i) = arys2(j) Then StringIntersect = StringIntersect & arys1(i) & " "
Next
Next
StringIntersect = Trim(StringIntersect) 'remove trailing space
End Function
If you don't want to do to the two loops you should be able to do something with inStr which is very quick. I haven't done any speed testing but I suspect the function below is quicker, however you will get unexpected results is the string is duplicated in the first input or the string in the first input is a substring in the second. This could be avoided with more checking but you would probably loose the speed benefit.
Function StringIntersect(s1 As String, s2 As String) As String
Dim arys1() As String
arys1 = Split(s1, " ")
For i = LBound(arys1) To UBound(arys1)
If InStr(1, s2, arys1(i), vbBinaryCompare) > 0 Then StringIntersect = StringIntersect & arys1(i) & " "
Next
StringIntersect = Trim(StringIntersect) 'remove trailing space
End Function
General case for all string
Eg: StringIntersect("abcdefgh", "adefh") = "def"
Function StringIntersect(s1 As String, s2 As String) As String
Dim i As Integer
Dim j As Integer
Dim k As Integer
k = 1
For i = 1 To Len(s1)
For j = 1 To Len(s2)
Do While Mid(s1, i, k) = Mid(s2, j, k) And i + k - 1 <= Len(s1) And j + k - 1 <= Len(s2)
StringIntersect = Mid(s1, i, k)
k = k + 1
Loop
Next j
Next i
End Function