1

I currently have an Excel spreadsheet with over 2000 rows of data. In one of the columns, i have an ID that is a string containing multiple decimal points. I need to sort the data in my Excel spreadsheet based off this ID. The column of ID's look like:

1.01.1.3.1
1.01.1.5.2
1.01.1.3.13
1.01.1.3.2
1.02.5.1.1.1.1
1.01.1.3.1.1
1.01.1.3.2.1

And the result needs to look like:

1.01.1.3.1
1.01.1.3.1.1
1.01.1.3.2
1.01.1.3.2.1
1.01.1.3.13
1.01.1.5.2
1.02.5.1.1.1.1

I am using VBA to pull the data from the spreadsheet and store in an array, but i am unsure how to approach sorting the string from left to right. I know i have to Split each entry by "." and sort the first index then the next index but i fear this method would take too long over the 2000+ entries. Im also unsure how to handle entries that have 5 indexes (Ex: 1.01.1.1.1) compared to an entry with 9 indexes (Ex: 1.01.1.1.2.5.1.1.1)

Another issue is that some entries contain letters. Ex: 1.01.1.4.1A

Note, I have this BubbleSort function:

Public Function BubbleSort(ByVal tempArray As Variant) As Variant
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 = 0 To UBound(tempArray) - 1

            ' Substitution when element is greater than the element following int
            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)

    BubbleSort = tempArray

End Function

If anyone has any insight for a solution, your help is kindly appreciated.

Benji Weiss
  • 406
  • 2
  • 6
  • 19
  • I think [Natural Number (Strings) Quick Sort](https://stackoverflow.com/a/19415281/1445339) should do the trick – Profex Aug 06 '18 at 14:46
  • @Profex - how would it compare `1.01.1.3.2` and `1.02.5.1.1.1.1`? (Did not check myself) – Vityata Aug 06 '18 at 14:50
  • It works great. A harder case would be 1.01.1.3 and 1.01.1.13. You can just copy the `CompareNaturalNum()` and `IsDigit()` functions and try it yourself with `Debug.Print` statements like this...`Debug.Print CompareNaturalNum("1.01.1.3", "1.01.1.13")` – Profex Aug 06 '18 at 15:44
  • @BenjiWeiss, Please select an answer the helped you the most so that the question isn't left opened. – Profex Aug 15 '18 at 17:19
  • @profex none of the mentioned solutions worked, I've been working on a solution using the insight you and the others posted..but none of them actually answered my problem. I tried using the code you posted and there are runtime errors. – Benji Weiss Aug 15 '18 at 17:21
  • @BenjiWeiss, Hmmm, I just copied the code from my post into a new workbook and it's working fine. What line does it stop at for you? – Profex Aug 15 '18 at 18:00
  • it seems to be working now. Not sure if you edited it or my excel environment was wacked out when i originally tried it .. but it does sort it as intended. Thank you! @profex – Benji Weiss Aug 15 '18 at 18:14

3 Answers3

1

I grabbed the following from my vault of Sorting routines. Please ignore some of my naming conventions :).

Upon review, I noticed an issue with my CompareNaturalNum() routine where it considered "1.01.1.3.1" and "1.01.1.3.1.1" the same. I've fixed it in the following code, and shown how to use it.

QuickSortMultiNaturalNum - A Quick sort for variant arrays, where you specify the column to be sorted.

Public Sub QuickSortMultiNaturalNum(strArray As Variant, intBottom As Long, intTop As Long, intSortIndex As Long, Optional intLowIndex As Long, Optional intHighIndex As Long = -1)
Dim strPivot As String, strTemp As String
Dim intBottomTemp As Long, intTopTemp As Long
Dim i As Long

intBottomTemp = intBottom
intTopTemp = intTop

If intHighIndex < intLowIndex Then
    If (intBottomTemp <= intTopTemp) Then
        intLowIndex = LBound(strArray, 2)
        intHighIndex = UBound(strArray, 2)
    End If
End If

strPivot = strArray((intBottom + intTop) \ 2, intSortIndex)

While (intBottomTemp <= intTopTemp)

' < comparison of the values is a descending sort
While (CompareNaturalNum(strArray(intBottomTemp, intSortIndex), strPivot) < 0 And intBottomTemp < intTop)
    intBottomTemp = intBottomTemp + 1
Wend

While (CompareNaturalNum(strPivot, strArray(intTopTemp, intSortIndex)) < 0 And intTopTemp > intBottom)
    intTopTemp = intTopTemp - 1
Wend

If intBottomTemp < intTopTemp Then
    For i = intLowIndex To intHighIndex
        strTemp = Var2Str(strArray(intBottomTemp, i))
        strArray(intBottomTemp, i) = Var2Str(strArray(intTopTemp, i))
        strArray(intTopTemp, i) = strTemp
    Next
End If

If intBottomTemp <= intTopTemp Then
    intBottomTemp = intBottomTemp + 1
    intTopTemp = intTopTemp - 1
End If

Wend

'the function calls itself until everything is in good order
If (intBottom < intTopTemp) Then QuickSortMultiNaturalNum strArray, intBottom, intTopTemp, intSortIndex, intLowIndex, intHighIndex
If (intBottomTemp < intTop) Then QuickSortMultiNaturalNum strArray, intBottomTemp, intTop, intSortIndex, intLowIndex, intHighIndex
End Sub

CompareNaturalNum - Custom Compare function

Function CompareNaturalNum(string1 As Variant, string2 As Variant) As Long
'string1 is less than string2 -1
'string1 is equal to string2 0
'string1 is greater than string2 1
Dim n1 As Long, n2 As Long
Dim iPosOrig1 As Long, iPosOrig2 As Long
Dim iPos1 As Long, iPos2 As Long
Dim nOffset1 As Long, nOffset2 As Long

    If Not (IsNull(string1) Or IsNull(string2)) Then
        iPos1 = 1
        iPos2 = 1
        Do While iPos1 <= Len(string1)
            If iPos2 > Len(string2) Then
                CompareNaturalNum = 1
                Exit Function
            End If
            If isDigit(string1, iPos1) Then
                If Not isDigit(string2, iPos2) Then
                    CompareNaturalNum = -1
                    Exit Function
                End If
                iPosOrig1 = iPos1
                iPosOrig2 = iPos2
                Do While isDigit(string1, iPos1)
                    iPos1 = iPos1 + 1
                Loop

                Do While isDigit(string2, iPos2)
                    iPos2 = iPos2 + 1
                Loop

                nOffset1 = (iPos1 - iPosOrig1)
                nOffset2 = (iPos2 - iPosOrig2)

                n1 = Val(Mid(string1, iPosOrig1, nOffset1))
                n2 = Val(Mid(string2, iPosOrig2, nOffset2))

                If (n1 < n2) Then
                    CompareNaturalNum = -1
                    Exit Function
                ElseIf (n1 > n2) Then
                    CompareNaturalNum = 1
                    Exit Function
                End If

                ' front padded zeros (put 01 before 1)
                If (n1 = n2) Then
                    If (nOffset1 > nOffset2) Then
                        CompareNaturalNum = -1
                        Exit Function
                    ElseIf (nOffset1 < nOffset2) Then
                        CompareNaturalNum = 1
                        Exit Function
                    End If
                End If
            ElseIf isDigit(string2, iPos2) Then
                CompareNaturalNum = 1
                Exit Function
            Else
                If (Mid(string1, iPos1, 1) < Mid(string2, iPos2, 1)) Then
                    CompareNaturalNum = -1
                    Exit Function
                ElseIf (Mid(string1, iPos1, 1) > Mid(string2, iPos2, 1)) Then
                    CompareNaturalNum = 1
                    Exit Function
                End If
                iPos1 = iPos1 + 1
                iPos2 = iPos2 + 1
            End If
        Loop
        ' Everything was the same so far, check if Len(string2) > Len(String1)
        ' If so, then string1 < string2
        If Len(string2) > Len(string1) Then
            CompareNaturalNum = -1
            Exit Function
        End If
    Else
        If IsNull(string1) And Not IsNull(string2) Then
            CompareNaturalNum = -1
            Exit Function
        ElseIf IsNull(string1) And IsNull(string2) Then
            CompareNaturalNum = 0
            Exit Function
        ElseIf Not IsNull(string1) And IsNull(string2) Then
            CompareNaturalNum = 1
            Exit Function
        End If
    End If
End Function

isDigit - Simple function to let you know if the string value is a digit (0-9)

Function isDigit(ByVal str As String, pos As Long) As Boolean
Dim iCode As Long
    If pos <= Len(str) Then
        iCode = Asc(Mid(str, pos, 1))
        If iCode >= 48 And iCode <= 57 Then isDigit = True
    End If
End Function

Var2Str - Since it deals with Variants, the values could be Null, so convert it to a string

Public Function Var2Str(Value As Variant, Optional TrimSpaces As Boolean = True) As String
    If IsNull(Value) Then
        'Var2Str = vbNullString
        Exit Function
    End If
    If TrimSpaces Then
        Var2Str = Trim(Value)
    Else
        Var2Str = CStr(Value)
    End If
End Function

Test - Here is sample code of how to use it. Just change the Range values. The last 1 in the call to QuickSortMultiNaturalNum is the column to be sorted (the column the ID's are in).

Sub Test()
Dim Target As Range
Dim vData 'as Variant
Dim Rows As Long
    ' Set Target to the CurrentRegion of cells around "A1"
    Set Target = Range("A1").CurrentRegion
    ' Copy the values to a variant
    vData = Target.Value2
    ' Get the high/upper limit of the array
    Rows = Target.Rows.Count    'UBound(vData, 1)
    ' Sor The variant array, passing the variant, lower limit, upper limit and the index of the column to be sorted.
    QuickSortMultiNaturalNum vData, 1, Rows, 1
    ' Paste the values back onto the sheet.  For testing, you may want to paste it to another sheet/range
    Range("A1").Resize(Target.Rows.Count, Target.Columns.Count).Value = vData
End Sub
Profex
  • 1,370
  • 8
  • 20
  • follow up question @profex: how would i modify your code to work from column D (starting at row 6..ending at the last row in this column) and also sort the other columns with column D.... so for example: Column A, B and C all contain data thats respective to Column D.. the data in column D is just the ID. So in sorting the IDs in Column D, i would like the cells in column A, B and C to follow the changes in Column D – Benji Weiss Aug 15 '18 at 18:21
  • Just make sure that the Target range is set to include all four columns (A-D), and then sort by the 4th index. I used the `.CurrentRegion` for simplicity, to get the area around cell "A1" (just like hitting Ctrl+A). If I assume that you have data in rows 1-5 as well, with no blank rows, then `Set Target = Range("D6").CurrentRegion` will select everything from A1- D[lastrow in table]. Then call the sort starting the lower end of the sort index at (row) 6, sorted on column 4. `QuickSortMultiNaturalNum vData, 6, Rows, 4` – Profex Aug 15 '18 at 18:34
  • @BenjiWeiss, You could also use `LastRow = Sht.Range("D" & Sht.Rows.Count).End(xlUp).Row)`, where `Sht` is set to the worksheet you're working with. Then `Set Target = Sht.Range("A6:D" & LastRow)` and `QuickSortMultiNaturalNum vData, 1, Rows, 4` – Profex Aug 15 '18 at 18:42
0

If you are allowed to use additional columns do the following:

  • Copy the ID column to a new column
  • Check the maximum number of dots in every cell
  • Remove each non-numeric [^0-9] and non-dot [^.] from every cell
  • Amend each cell, including the maximal number of dots like this:

From:

1.01.1.3.13

To:

1.01.01.03.13.00
  • E.g. adding a zero, if it is consisting only of 1 value and adding additional points, to equalize to the maximal value with points.

  • In the new column remove the dots

  • Sort by the new column
  • Delete the new column
  • This is it!

If you are not allowed to use additional columns, then you should use some mapping techniques.

Vityata
  • 42,633
  • 8
  • 55
  • 100
  • 1
    i am allowed to use additional columns. This fix does seem doable, but the only other predicament is that some entries contain a letter at the end. ( i edited the post, as i was sifting through the spreadsheet and found a few entries with a letter appended to the end of the ID) – Benji Weiss Aug 06 '18 at 14:52
  • @BenjiWeiss - then adding an additional point - strip the string from anything which is not numeric or a point. – Vityata Aug 06 '18 at 14:55
  • Sorting Numbers & Text doesn't work well in Excel...you actually need 2 columns for each value. The first column is to detect is the value is a Number or Text `IsNumber()` and the second column would be `=IFERROR(VALUE(),0)`. Kind of a pain. – Profex Aug 06 '18 at 15:41
  • @Profex - my idea is not to sort numbers and text, but to make a number from the text and sort by it. The making of a number is in the example - first adding zeroes where needed, then removing the dots, thus we get a number. Then sorting by a number is quite an easy task. – Vityata Aug 06 '18 at 15:44
  • 1
    @Vityata OK, It's not a bad idea, but he has letters mixed in at the end as well that has to be taken into account. Also, you're padding the `0` on the wrong side; `3` should turn into `03`, not `30`. you don't want `30 > 13` in the case above when it should be `03 < 13`. – Profex Aug 06 '18 at 15:53
  • 1
    @Profex - agree for the padding on the wrong side. Edited. Concerning the letters, my idea is to ignore them completely, as far as I guess they do not bring any further meaning in the sorting, as far as I do not see them in the OPs example, but just mentioned that they exist. – Vityata Aug 06 '18 at 15:56
0

This code splits the range using the . delimiter.
It then adds a 0 to the blank cells in the split before sorting based on the split, but also including the original text.
The split cells are then cleared leaving just the sorted original values.
1.01.1.4.1A appears between 1.01.1.3.13 and 1.01.1.5.2.

Sub Test()

    Dim wrkSht As Worksheet
    Dim rng As Range
    Dim rng_Split As Range
    'Dim rng_Blanks As Range - EDIT: Not needed.
    Dim lLastCol As Long
    Dim rCol As Range

    Set wrkSht = ThisWorkbook.Worksheets("Sheet1")

    'Split the value and find the last column it splits to.
    With wrkSht
        'Adjust the range to yours.
        Set rng = .Range("A31:A38")

        rng.TextToColumns _
            Destination:=rng.Offset(, 1), _
            DataType:=xlDelimited, _
            Other:=True, _
            OtherChar:="."

        lLastCol = rng.EntireRow.Cells.Find("*", , , , xlByColumns, xlPrevious).Column
    End With

    'Add a 0 to all blank cells.
    Set rng_Split = rng.Offset(, 1).Resize(rng.Rows.Count, lLastCol - 1)
    rng_Split.SpecialCells(xlCellTypeBlanks).Value = 0

    With wrkSht
        With .Sort
            .SortFields.Clear
            For Each rCol In rng_Split.Columns
                .SortFields.Add Key:=rCol, SortOn:=xlSortOnValues, Order:=xlAscending, DataOption:=xlSortNormal
            Next rCol
            'Adjust this range to include all columns to be sorted.
            .SetRange rng_Split.Offset(, -1).Resize(, lLastCol)
            .Header = xlNo
            .MatchCase = False
            .Orientation = xlTopToBottom
            .Apply
        End With
    End With

    rng_Split.ClearContents

End Sub

Edit: Using this method 01 and 1 are considered the same.

Darren Bartrup-Cook
  • 18,362
  • 1
  • 23
  • 45