In VBA I wrote this
Function SPLITTER(data As String, delimiter As String) As String()
SPLITTER = Split(data, delimiter)
End Function
Which works great for me
However, SPLITTER returns strings but I will exclusively use it for numbers. I now that I return it As String () but using any other type didnt work for me. I guess it interferes with the Split function
I use that function to automize things to converting the value of the cells to numbers manually does not work for me. Is there a way to let the function return the values as decimal?
Sorry if I may be incorrect in my terminology but I'm new to VBA.
Either you use the VALUE formula as #norie suggested or you need to change your function so it converts the strings you get form Split into Double values.
Note that this cannot be done for the whole array at once and you have to convert each value. This might be slightly slower (on huge data or extensive use of the function) than using the VALUE formula. For small data you won't see a difference.
Option Explicit
Public Function SPLITTER(ByVal Data As String, ByVal Delimiter As String) As Variant()
' split strings
Dim SplittedStrings() As String
SplittedStrings = Split(Data, Delimiter)
' create variant array of the same size (variant so we can return errors)
Dim Values() As Variant
ReDim Values(LBound(SplittedStrings) To UBound(SplittedStrings)) As Variant
' convert each value into double
Dim i As Long
For i = LBound(Values) To UBound(Values)
If IsNumeric(SplittedStrings(i)) Then
' return value as double
Values(i) = CDbl(SplittedStrings(i))
Else
' return #VALUE! error if a value is not numeric
Values(i) = CVErr(xlErrValue)
End If
Next i
SPLITTER = Values
End Function
Note that the return array is defined as As Variant() and not As Double() so in case any of the splitted strings is not numeric it can return a #VALUE! error for this one value and still output the others. If you don't do that the whole function fails and does output #VALUE! for all values even if only one cannot be converted.
Since you re-write the whole array anyway you could even bring it into the correct direction: Output as row or as column:
Option Explicit
Public Function SPLITTER(ByVal Data As String, ByVal Delimiter As String, Optional ByVal OutputAsRow As Boolean = False) As Variant()
' split strings
Dim SplittedStrings() As String
SplittedStrings = Split(Data, Delimiter)
' create variant array of the same size (variant so we can return errors)
Dim Values() As Variant
If OutputAsRow Then
' 2-dimensional array with 1 row and n columns
ReDim Values(1 To 1, LBound(SplittedStrings) To UBound(SplittedStrings)) As Variant
Else
' 2-dimensional array with n rows and 1 column
ReDim Values(LBound(SplittedStrings) To UBound(SplittedStrings), 1 To 1) As Variant
End If
' convert each value into double
Dim i As Long
For i = LBound(SplittedStrings) To UBound(SplittedStrings) ' for each value in the input string string
Dim RetVal As Variant
If IsNumeric(SplittedStrings(i)) Then ' check if it is a number
' return value as double
RetVal = CDbl(SplittedStrings(i))
Else
' return #VALUE! error if a value is not numeric
RetVal = CVErr(xlErrValue)
End If
If OutputAsRow Then
Values(1, i) = RetVal ' fill columns
Else
Values(i, 1) = RetVal ' fill rows
End If
Next i
SPLITTER = Values
End Function
So =SPLITTER($A$1,";") or =SPLITTER($A$1,";",0) will output it as a column and =SPLITTER($A$1,";",1) as a row.
You could return numbers without changing the function by adding VALUE to the formula.
=VALUE(TRANSPOSE(SPLITTER(D5,";")))
It seems you have a spilled range of values, which indicates you are using Microsoft 365. I'd suggest to not even use UDF's if not needed (and keep an .xlsx file instead):
=FILTERXML("<t><s>"&SUBSTITUTE(A1,";","</s><s>")&"</s></t>","//s")
This will then "split" your string through valid xml/xpath expressions and excel will auto-recognize the numbers.
Extension on JvdV 's solution
Assuming that the cell content BV71 contains non-numeric tokens, you might even restrict all xml node contents to numeric elements only via XPath "//s[.*0=0]":
=FILTERXML("<t><s>"&SUBSTITUTE(A8,";","</s><s>")&"</s></t>","//s[.*0=0]")
Explanation: The XPath expression to search all s nodes (at any hierarchy level) gets an additional condition in [] brackets. Multiplying the node content (symbolized by .) with zero (*0) and checking if the result equals a number (i.e. =0) allows to get only numbers, as this multiplication fails with strings.
Note that regional date settings with point delimiters may interprete tokens like 8.2 as date, which would need further conversions.
Related
As mentioned in the title, I wonder if there is any way to use built-in functions in excel to see whether a cell contains a specific number and count the total numbers in the cell. The cell can contain a list of numbers seperated by comas, for instance, "1,4,7" or ranges "10-25" or a combination of both. See the print screen.
No, there is not, but you could write a VBA function to do that, something like:
Function NumberInValues(number As String, values As String) As Boolean
Dim n As Integer
n = CInt(number)
Dim parts() As String
parts = Split(values, ",")
For i = LBound(parts) To UBound(parts)
parts(i) = Replace(parts(i), " ", "")
Next
Dim p() As String
Dim first As Integer
Dim last As Integer
Dim tmp As Integer
For i = LBound(parts) To UBound(parts)
p = Split(parts(i), "-")
' If there is only one entry, check for equality:
If UBound(p) - LBound(p) = 0 Then
If n = CInt(p(LBound(p))) Then
NumberInValues = True
Exit Function
End If
Else
' Check against the range of values: assumes the entry is first-last, does not
' check for last > first.
first = CInt(p(LBound(p)))
last = CInt(p(UBound(p)))
If n >= first And n <= last Then
NumberInValues = True
Exit Function
End If
End If
Next
NumberInValues = False
End Function
and then your cell C2 would be
=NumberInValues(B2,A2)
Calculating how many numbers there are in the ranges would be more complicated as numbers and ranges could overlap.
The key part of implementing this is to create a List or Array of individual numbers that includes all the Numbers represented in the first column.
Once that is done, it is trivial to check for an included, or do a count.
This VBA routine returns a list of the numbers
Option Explicit
Function createNumberList(s)
Dim AL As Object
Dim v, w, x, y, I As Long
Set AL = CreateObject("System.Collections.ArrayList")
v = Split(s, ",")
For Each w In v
'If you need to avoid duplicate entries in the array
'uncomment the If Not lines below and remove the terminal double-quote
If IsNumeric(w) Then
'If Not AL.contains(w) Then _"
AL.Add CLng(w)
Else
x = Split(w, "-")
For I = x(0) To x(1)
'If Not AL.contains(I) Then _"
AL.Add I
Next I
End If
Next w
createNumberList = AL.toarray
End Function
IF your numeric ranges might be overlapping, you will need to create a Unique array. You can do that by changing the AL.Add function to first check if the number is contained in the list. In the code above, you can see instructions for that modification.
You can then use this UDF in your table:
C2: =OR($B2=createNumberList($A2))
D2: =COUNT(createNumberList($A2))
Here is a possible formula solution using filterxml as suggested in the comment:
=LET(split,FILTERXML("<s><t>+"&SUBSTITUTE(A2,",","</t><t>+")&"</t></s>","//s/t"),
leftn,LEFT(split,FIND("-",split&"-")-1),
rightn,IFERROR(RIGHT(split,LEN(split)-FIND("-",split)),leftn),
SUM(rightn-leftn+1))
The columns from F onwards show the steps for the string in A2. I had to put plus signs in because Excel converted a substring like "10-15" etc. into a date as usual.
Then to find if a number (in C2 say) is present:
=LET(split,FILTERXML("<s><t>+"&SUBSTITUTE(A2,",","</t><t>+")&"</t></s>","//s/t"),
leftn,LEFT(split,FIND("-",split&"-")-1),
rightn,IFERROR(RIGHT(split,LEN(split)-FIND("-",split)),leftn),
SUM((--leftn<=C2)*(--rightn>=C2))>0)
As noted by #Ron Rosenfeld, it's possible that there may be duplication within the list: the Count formula would be susceptible to double counting in this case, but the Check (to see if a number was in the list) would give the correct result. So the assumptions are:
(1) No duplication (I think it would be fairly straightforward to check for duplication, but less easy to correct it)
(2) No range in wrong order like 15-10 (although this could easily be fixed by putting ABS around the subtraction in the first formula).
Here is a little cheeky piece of code for a VBA solution:
Function pageCount(s As String)
s = Replace(s, ",", ",A")
s = Replace(s, "-", ":A")
s = "A" & s
' s now looks like a list of ranges e.g. "1,2-3" would give "A1,A2:A3"
pageCount = Union(Range(s), Range(s)).Count
End Function
because after all the ranges in the question behave exactly like Excel ranges don't they?
and for inclusion (of a single page)
Function includes(s As String, m As String) As Boolean
Dim isect As Range
s = Replace(s, ",", ",A")
s = Replace(s, "-", ":A")
s = "A" & s
Set isect = Application.Intersect(Range(s), Range("A" & m))
includes = Not (isect Is Nothing)
End Function
I need your support to get the result in column B, it is basically see if there is 5 digits between a comma and a dash sign ( , & - ) in A cells and delete the text from , to -, the rest of the text need to be same without any changes as shown in the picture. I am looking for a normal excel equation not a vba code. Thanks in advance!
Based on your screenshot and not what you wrote, you seem to want to retain substrings that are in the format of aaaa-nnnn where nnnnn represents four or more digits. If you have the FILTERXML and TEXTJOIN functions, you can use this formula:
=SUBSTITUTE(TEXTJOIN(",",TRUE,FILTERXML("<t><s>" & SUBSTITUTE(SUBSTITUTE(A1,"-",",-"),",","</s><s>") & "</s></t>","//s[number(.)<-999] /preceding::*[1] | //s[number(.)<-999]")),",-","-")
Create an XML splitting on the commas and also the hyphen (but retain the hyphen)
Construct an xPath which selects both the node preceding a numeric node with a value of less than -999 and that numeric node itself.
The negative number comes from retaining the hyphen
Put the values back together using TEXTJOIN with a comma separator
Remove the comma that is now preceding the hyphen.
If your version of Excel does not have those functions, VBA or possibly Power Query would be a better solution.
If wind up preferring a VBA solution, I suggest looking for substrings that meet your apparent criteria of a hyphenated substring where the right half is a number > 999. If necessary, checking the left side for being all capital letters could be easily added.
Option Explicit
Function getStr(S As String) As String
Dim V, W
Dim sTemp As String
V = Split(S, ",")
For Each W In V
If Val(Split(W, "-")(1)) > 999 Then _
sTemp = sTemp & "," & W
Next W
getStr = Mid(sTemp, 2)
End Function
Regrettably Substitute Doesn't Allow Wild Characters
Using SEARCH and REPLACE the only thing I could come up with was the following formula:
=IF(ISNUMBER(SEARCH("????-, ",A1)),REPLACE(A1,SEARCH("????-, ",A1),7,""),IF(ISNUMBER(SEARCH(", ????-",RIGHT(A1,7))),REPLACE(A1,LEN(A1)-6,7,""),A1))
which is removing only the first occurrence of the strings you want removed.
As a consolation I'm offering a simple VBA solution, which by default removes all 5-character sub strings in a ", "-delimited string.
In VBA (CTRL-F11) insert a new module into the workbook where you need it. In the code sheet of the module (probably Module1) copy/paste the following code:
Option Explicit
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: From a delimited string, removes all sub strings containing
' a specified number of characters and returns the remainder
' of the string.
' Returns: A string, if there are any substrings with a different number
' of characters than the specified number of characters,
' or "", if not.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function FilterC(SourceValue As Variant, _
Optional NumberOfCharacters As Long = 5, _
Optional Delimiter As String = ", ") As String
Dim vntS As Variant ' Source Array
Dim vntT As Variant ' Target Array
Dim i As Long ' Source Array Elements Counter
Dim iTA As Long ' Target Array Elements Counter
Dim strC As String ' Current String
' Check if SourceValue is text.
If VarType(SourceValue) <> vbString Then Exit Function
' Check if SourceValue is "". For a cell in Excel this refers to an empty
' cell or a cell with a formula evaluating to "".
If SourceValue = "" Then Exit Function
' Initialize Target Array Elements Counter.
iTA = -1
' Write SourceValue to elements of Source Array (using 'Split').
vntS = Split(SourceValue, Delimiter)
' Loop through elements of Source Array.
For i = 0 To UBound(vntS)
' Write current element in Source Array to Current String.
strC = vntS(i)
' Check if the length of Current String is NOT equal
' to NumberOfCharacters.
If Len(strC) <> 5 Then GoSub TargetArray
Next
' If only 'NumberOfCharacters' character strings are found.
If iTA = -1 Then Exit Function
' Write elements of Target Array to FilterC (using "Join").
FilterC = Join(vntT, Delimiter)
Exit Function
' Write String to Target Array.
TargetArray:
' Increase Target Array Elements Counter.
iTA = iTA + 1
' Check if Target Array Elements Counter is greater than 0 i.e. if
' there already are any elements in Target Array.
If iTA > 0 Then
' All, except the first element.
ReDim Preserve vntT(iTA)
Else
' Only the first element.
ReDim vntT(0)
End If
' Write Current String to Target Array.
vntT(iTA) = strC
' Note: Target Array Elements Counter (iTA) was initalized with -1, so when
' the first time the code redirects to TargetArray (Subroutine),
' iTA will be 0 and only this time run through the Else clause
' and finally write Current String to Target Array.
Return
End Function
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
In Excel for a result from A1 use the following formula:
=FilterC(A1)
which is the short, default behavior of the fully qualified formula:
=FilterC(A1,5,", ")
I have a .csv with phone numbers, and I'd like to extract specific info from a column so I can import it into another program.
Basically, the data is phone numbers that are scrubbed through the DNC list -- but, the phone numbers show like this:
9995551212, 1234567890 DNC, 4432325555
So, the first and last numbers are safe to call, the middle one should be removed.
I'd like to create a new column where the clean data is stored -- it should look at each of the numbers, separated by a comma, and determine if "DNC" is the last three letters. If DNC shows, that phone # should be excluded. If it's just numbers, we should include that data.
I'm honestly at a loss as to how to do it.
Can anyone help?
Try
=if(iferror(find("dnc",A1,1),0)>0,"",A1)
So find looks for dnc if it finds it then if puts a blank.
Edit, reading the comment, if the data is just text in one cell, then "text to columns" will extract...
Extract Phone Number (UDF)
Copy the following code to a standard module (In VBE: Insert>Add Module).
The parameters of the 2nd (",") and 3rd (", ") arguments are adjusted (default) for your convenience, but can be changed.
The Code
Function EPN(SourceString As Variant, Optional ByVal SplitDelimiter = ",", _
Optional ByVal JoinDelimiter = ", ") As String
Dim strC As String ' Current String
Dim strT As String ' Target String
Dim vntS As Variant ' String Array
Dim i As Long ' String Array Element Counter
' Split Source String to String Array.
vntS = Split(CStr(SourceString), SplitDelimiter)
' Loop through elements of String Array.
For i = 0 To UBound(vntS)
' Write current element of String Array to Current String.
strC = Trim(vntS(i))
' Check if current string is 'recognized' as a number.
If IsNumeric(strC) Then
' Check if Target String is not empty ("").
If strT <> "" Then ' Target String is NOT empty.
' Concatenate current Target String, Join Delimiter _
' and Current String.
strT = strT & JoinDelimiter & strC
Else ' Target String IS empty (only once).
' Write Current String to Target String.
strT = strC
End If
End If
Next
' Write Target String to EPN (Extract Phone Number).
EPN = strT
End Function
Usage In Excel
Use e.g. =EPN(A1) and copy down.
=EPN("asdfasf, 9999,234324 ,234DNC5342,erter,E234,dsfa")
will result in 9999, 234324.
I'm checking to see if there is any way to pass an array of results in an UDF.
My function is working as expected if I pass a range, for eg - $A$2:$A$5, but not if I make array calculations before passing the range, for eg - IF($A$2:$A$5 < 3, $A$2:$A$5 ,"") with control + shift + enter.
Here's the code I'm using-
Public Function ReverseTextToColumns(Rg As Range, Optional D As String = " ") As String
Dim xArr
xArr = Application.WorksheetFunction.Transpose((Rg.Value))
ReverseTextToColumns = Join(xArr, D)
End Function
I don't want to create a complicated function and just to know how to pass an array of results in an UDF.
Pass in first argument as Variant and drop the .Value
Option Explicit
Public Function ReverseTextToColumns(n As Variant, Optional D As String = " ") As String
Dim xArr
xArr = Application.WorksheetFunction.Transpose((n))
ReverseTextToColumns = Join(xArr, D)
End Function
Worksheet:
The problem appears to be that this condition:
$A$2:$A$5 < 3
returns FALSE, which means that the empty string "" is passed into the function. This is incompatible with the type of the parameter -- Range; the following function:
Function TestRng(rng As Range) As Boolean
TestRng = True
End Function
also returns #VALUE! when used in a formula like so:
=TestRng("")
I suggest you make the parameter of type Variant, and use the VBA TypeName function within your UDF to check whether it is actually a Range.
NB. What exactly are you aiming for with this condition -- $A$2:$A$5 < 3? What does it mean to compare a range of cells against a number?
I have two files one is a Project Register that holds key information on a project and the other is a Risk log.
There is a 1:m relationship between entries in the Register and the Risk log. What I need to do is combine all of a project risks into one cell inside the project register file.
The matching field in both files is the Project ID field
Is there a way I can do this using a vlookup variant or multiple nested vlookups?
Here's the user-defined function approach I mentioned (adapted from a different VLOOKUP-variant I already had made):
' Acts like VLOOKUP in a 1-to-many scenario by concatenating all values in matching rows
' instead of just returning the first match
Public Function VLOOKUP_MANY(lookup_value As String, lookup_range As Range, column_number As Integer, Optional delimiter As Variant) As Variant
Dim vArr As Variant
Dim i As Long
Dim found As Boolean: found = False
' Set default delimiter
If IsMissing(delimiter) Then delimiter = ", "
' Get values
vArr = lookup_range.Value2
' If column_number is outside of the specified range, return #REF
If column_number < LBound(vArr, 2) Or column_number > UBound(vArr, 2) Then
VLOOKUP_MANY = CVErr(xlErrRef)
Exit Function
End If
' Search for matches and build a concatenated list
VLOOKUP_MANY = ""
For i = 1 To UBound(vArr, 1)
If UCase(vArr(i, 1)) = UCase(lookup_value) Then
VLOOKUP_MANY = VLOOKUP_MANY & delimiter & vArr(i, column_number)
found = True ' Mark at least 1 result
End If
Next
If found Then
VLOOKUP_MANY = Right(VLOOKUP_MANY, Len(VLOOKUP_MANY) - Len(delimiter)) ' Remove first delimiter
Else
VLOOKUP_MANY = CVErr(xlErrNA) ' If no matches found, return #N/A
End If
End Function
This will search the first column in the specified range for the specified value (same as VLOOKUP), but returns the values in the specified column number concatenated. It will return #N/A when no matches are found, and #REF if an invalid value is specified for the column number (e.g. you choose column 5 but only had a 4-column table).
In case you don't know about user-defined functions - you can just copy this VBA code into the VBE for a module in your workbook. Hit Alt+F11, go to Insert > Module at the top of the screen, then paste this code into the blank file that opens up. When you go to save, you'll have to save your workbook as Macro-Enabled (.xlsm) to keep the code working - Excel will remind you about this in the save screen.
Be forewarned: it's going to be slower than VLOOKUP as a result of having to look through the entire lookup range instead of being able to stop at the first match it finds.
If you're open to using an array formula instead, there are ways to speed up this sort of functionality for very large datasets...
Different version that leverages some of the benefits of array formulas to store lookup values and speedup subsequent calls:
' Acts like VLOOKUP in a 1-to-many scenario by concatenating all values in matching rows
' instead of just returning the first match
' Utilizes a dictionary to speedup multiple matches (great for array formulas)
Public Function VLOOKUP_MANY_ARRAY(lookup_values As Range, lookup_range As Range, column_number As Integer, Optional delimiter As Variant) As Variant
Dim vHaystack As Variant, vNeedles As Variant
Dim i As Long
Dim found As Boolean: found = False
Dim dict As Object: Set dict = CreateObject("Scripting.Dictionary")
' Set default delimiter
If IsMissing(delimiter) Then delimiter = ", "
' Get values
vHaystack = lookup_range
vNeedles = lookup_values
' If column_number is outside of the specified range, return #REF
If column_number < LBound(vHaystack, 2) Or column_number > UBound(vHaystack, 2) Then
VLOOKUP_MANY_ARRAY = CVErr(xlErrRef)
Exit Function
End If
' Add values to a lookup dictionary
For i = 1 To UBound(vHaystack, 1)
If dict.Exists(UCase(vHaystack(i, 1))) Then
dict.Item(UCase(vHaystack(i, 1))) = dict.Item(UCase(vHaystack(i, 1))) & delimiter & vHaystack(i, column_number)
Else
dict.Add UCase(vHaystack(i, 1)), vHaystack(i, column_number)
End If
Next
Dim outArr As Variant
If IsArray(vNeedles) Then ' Check number of lookup cells
' Build output array
ReDim outArr(1 To UBound(vNeedles, 1), 1 To 1) As Variant
For i = 1 To UBound(vNeedles, 1)
If dict.Exists(UCase(vNeedles(i, 1))) Then
outArr(i, 1) = dict.Item(UCase(vNeedles(i, 1)))
Else
outArr(i, 1) = CVErr(xlErrNA)
End If
Next
Else
' Single output value
If dict.Exists(UCase(vNeedles)) Then
outArr = dict.Item(UCase(vNeedles))
Else
outArr = CVErr(xlErrNA)
End If
End If
VLOOKUP_MANY_ARRAY = outArr
End Function
This creates a Dictionary, which is a special structure that's really good for looking up values. There's a little extra overhead involved in building it, but once you have the structure, you can do lookups into it very quickly. This is especially nice with array formulas, which is basically when the exact same formula gets put into a whole collection of cells, then the function executes once and returns values for every cell (instead of just executing once, separately, for a bunch of cells). Enter it like an array formula with CTRL+SHIFT+ENTER, and make the first argument refer to all your lookup values instead of just one.
It will work without being used as an array formula, but it will be somewhat slower than the first function in that situation. However, if you use it in an array formula, you'll see huge speedups.
RE-EDIT:
You might need to write a user defined function or write a macro (code on same link)