Return an Array of Only Numbers From a Range With Mixed Datatypes

Viewed 165
  • Related to the following screenshot, the formula

    =IF(ISNUMBER($A$1:$A$5),$A$1:$A$5)
    

    will evaluate to the following array

    {1;FALSE;2;44644;3}
    

    but I only need it to return the numbers

    {1;2;3}
    
  • How can this be achieved (getting rid of the dates and booleans)?

  • Note that ISNUMBER has already gotten rid of error values and whatnot. enter image description here

Utilization in VBA

  • Instead of the loop and whatnot in the first procedure I want to simplify by evaluating the correct formula in the second procedure.

Correct

Sub CorrectVBA()
' Result:
' 1
' 2
' 3
   
    Const rgAddress As String = "A1:A5"
    
    Dim ws As Worksheet: Set ws = ThisWorkbook.Worksheets("Sheet1")
    Dim rg As Range: Set rg = ws.Range(rgAddress)
    Dim Data As Variant: Data = rg.Value
    
    Dim Arr() As Double
    Dim Item As Variant
    Dim r As Long
    Dim n As Long
    
    For r = 1 To UBound(Data, 1)
        Item = Data(r, 1)
        If WorksheetFunction.IsNumber(Item) Then
            If Not IsDate(Item) Then
                n = n + 1
                ReDim Preserve Arr(1 To n)
                Arr(n) = Item
            End If
        End If
    Next r
       
    If n = 0 Then Exit Sub
       
    For n = 1 To UBound(Arr)
        Debug.Print Arr(n)
    Next n
       
End Sub

Wrong

  • Something like this is what I want to do in this particular case.
  • It is wrong because the formula is wrong.
Sub WrongVBA()
' Result:
'  1
' False
'  2
'  44644
'  3
    
    Const rgAddress As String = "A1:A5"
    
    Dim ws As Worksheet: Set ws = ThisWorkbook.Worksheets("Sheet1")
    
    Dim Data As Variant
    Data = ws.Evaluate("IF(ISNUMBER(" & rgAddress & ")," & rgAddress & ")")
    
    Dim r As Long
    For r = 1 To UBound(Data, 1)
        Debug.Print Data(r, 1)
    Next r
   
End Sub

Final Word

  • Thank you all.
  • I have decided to accept objectively JvdV's answer since, if not already, Office 365 is becoming a standard and its new functions make things so much easier.
  • Personally, the most useful answers were those of T.M. and Ron Rosenfeld.
  • Also, thanks to Can.U and Domenic.
4 Answers

This is not easy through formulae because Excel can not distinguish between say 44644 being an integer or meant to be a date unless you look at formatting of the cel. The only reasonable way of doing this that I can think of is to use CELL(), as per this older post here on SO. It can't return an array correctly on its own so I came up with the following using BYROW():

enter image description here

Formula in C1:

=FILTER(A1:A5,BYROW(SEQUENCE(5,,0),LAMBDA(a,CELL("format",OFFSET(A1,a,0))="G"))*ISNUMBER(A1:A5))

This would work to filter out the dates (given the format in your data) since the cells that are not formatted as date would return "G" ('General', see the link to the ms-documentation). My limited testing suggested that this would work to filter out dates from the equation as per the question.


Note: To be more specific, you can exclude any cells that have this specific formatting ('mm/dd/yyyy') you have shown, from the equation through:

=FILTER(A1:A5,BYROW(SEQUENCE(5,,0),LAMBDA(a,LEFT(CELL("format",OFFSET(A1,a,0)))<>"D"))*ISNUMBER(A1:A5))

EDIT: The above solution would be ms365 exclusive. For Excel 2019, one way to do this is:

=FILTERXML("<t><s>"&TEXTJOIN("</s><s>",,IF(ISNUMBER(A1:A5),A1:A5,""))&"</s></t>","//s")
  • It's an CSE-entered formula. If done correctly the returned array would be {1;2;44644;3};
  • We could also use =FILTERXML("<t><s>"&TEXTJOIN("</s><s>",,A1:A5)&"</s></t>","//s[.*0=0]") however, since TEXTJOIN() has a limit I thought it would be wise to proces any non-number into an empty string beforehand;
  • There is no way (I can think of) to exclude the dates in this version of Excel.

365

=FILTER(A1:A5,ISNUMBER(-TEXT("1/1/"&A1:A5,"e/m/d")))

or older version:

=N(OFFSET(A1,SMALL(IF(ISNUMBER(-TEXT("1/1/"&A1:A5,"e/m/d")),ROW(1:5)-1),ROW(INDIRECT("1:"&COUNT(-TEXT("1/1/"&A1:A5,"e/m/d"))))),))

Alternative via XML Spreadsheet udf

Though the question shows the excel-formula tag, you might consider to use a user-defined function analyzing the column's Value(xlRangeValueXMLSpreadsheet) or simply Range.Value(11) xml content equivalent.

This approach benefits from the fact that the relevant data types of your question are explicitly distinguished between "Number" and "DateTime" formats.

As the resulting xml content includes namespace definitions like "ss:" I preferred to use late bound MsXml2 for node extraction. - Afaik namespacing can't be applied in FilterXML (a possible work-around would be reduce the whole content via split and replace any namespaces to allow this, too).

The following example udf assumes a 1-column range input (without further error handling) and gets all "real" numbers via a node select Set cells = xDoc.SelectNodes("//ss:Cell[ss:Data/@ss:Type='Number']").

Function Nums(rng As Range)
'[0]Get Value(11)
    Dim s As String
    s = rng.value(xlRangeValueXMLSpreadsheet)   ' or: rng.Value(11)
'[1]Set xml document to memory
    Dim xDoc As Object: Set xDoc = CreateObject("MSXML2.DOMDocument.6.0")
'[2]Add namespaces
    xDoc.SetProperty "SelectionNamespaces", _
    "xmlns:ss='urn:schemas-microsoft-com:office:spreadsheet' " & _
    "xmlns:ht='http://www.w3.org/TR/REC-html40'"
'[3]Get cells with Data/@Type "Number"
    If xDoc.LoadXML(s) Then            ' load wellformed string content
        Dim cell As Object, cells As Object
        Set cells = xDoc.SelectNodes("//ss:Cell[ss:Data/@ss:Type='Number']") ' XPath using namespace prefixes
        Dim tmp(): ReDim tmp(1 To cells.Length, 1 To 1)
        For Each cell In cells
            Dim i as long: i = i + 1
            tmp(i, 1) = cell.Text
        Next cell
        '[4]return 1-column array
        Nums = tmp
    End If
End Function

Example call

  • In tabular Excel e.g. Nums(A1:A5) (resulting in a spill range if you dispose of dynamic array feature, otherwise via CSE), or
  • via VBA e.g.
    Dim rng As Range
    Set rng = Sheet1.Range("A1:A5")
    Dim results
    results = Nums(rng)
    rng.Offset(, 1).Resize(UBound(results), 1) = results

As I mentioned in my comments, I don't know of a reliable method of determining if an entry is a date type, since dates in Excel are stored as numbers.

Since you have also written that excluding the dates is not an absolute requirement, here are several formulas that will work depending on your version of Excel.

O365

C1: =FILTER(A1:A5,ISNUMBER(A1:A5))

results will spill down

Earlier versions of Excel

E1: =IFERROR(INDEX($A$1:$A$5,AGGREGATE(15,6,1/ISNUMBER($A$1:$A$5)*ROW(A1:$A$5),ROW(INDEX($A:$A,1):INDEX($A:$A,SUM(--ISNUMBER($A$1:$A$5)))))),"")

entered as an array formula (with ctrl + shift + enter over a range large enough to include all the results)

G1: =IFERROR(INDEX($A$1:$A$5,AGGREGATE(15,6,1/ISNUMBER($A$1:$A$5)*ROW($A$1:$A$5),ROWS($A$1:A1))),"")

entered as a normal formula, then fill down until you start returning blanks

enter image description here

Related