For instance, I would like these 6 numbers.
Currently when I use the sort method it puts 6.6.1.1.13 first and 6.6.1.1.2 later.
Before Sort
6.6.1.1
6.6.1.1.1
6.6.1.1.13
6.6.11.14
► 6.6.1.1.2
What I Want It To Look Like After Sort
6.6.1.1
6.6.1.1.1
► 6.6.1.1.2
6.6.1.1.13
6.6.11.14
Unfortunately the only fully general way to sort outline numbers (or more formally, "path-indexes") is with a custom comparison function. Unfortunately, the Excel sorting operations and functions do not support such a feature (not even from VBA). The Excel preferred way is with custom lists, but these are not workable for path-indexes.
This leaves two choices:
Do your sorting entirely in VBA: This works (I've done it) but is pretty involved and messy. Or,
Use a Helper Column with a VBA function: This works but is not fully general, because you have to know ahead of time what the maximum index values will be.
of these, #2 above is by far the simpler option, but it does have limitations (explained below).
Basically what we want is a VBA function that can take a string like "6.6.11.14" and make it always sortable in path index order. The problem with this string is that in text order two digit indexes like ".11" and ".14" come before ".2" rather than after it.
The obvious way to fix this is to fix this is to convert all indexes into 2-digit numbers with leading zeroes. So, 6.6.11.14 would become 06.06.11.14 and crucially 6.6.2.1 would become 06.06.02.01. Now these two path-index values will sort correctly use text sorting.
The catch, however, is that this is only true if each individual index number is never greater than two digits (99). Thus, 06.07.99 sorts correctly, but 06.07.110 does not under this scheme. This is easily fixable by simply raising it from two digits to three digits, but again, the catch is that you have to know this ahead of time.
So assuming that we do know ahead of time what the maximum size/(number of digits) will be for any single index number, we can use the following VBA function to reformat your outline numbers for a helper column:
Public Function OutlineSortingFormat(OutlineNumber As String, Digits As Integer) As String
Dim PathIndexes() As String
Dim Zeroes As String
Dim i As Integer
Zeroes = "0000000000"
PathIndexes = Split(OutlineNumber, ".")
For i = 0 To UBound(PathIndexes)
PathIndexes(i) = Right(Zeroes & PathIndexes(i), Digits)
Next i
OutlineSortingFormat = Join(PathIndexes, ".")
End Function
This just splits the outline number into individual numeric strings, prefixes the correct amount of zeroes and then concatenates them back into a sortable outline number.
You then apply this by making a helper column and then using the function like so:
=OutlineSortingFormat(M3,2)
Where M is the column that has your unformatted outline indexes and the second parameter (, 2)) indicates that your want all index numbers filled (and truncated) to 2 digits. Then instead of sorting on your original outline numbers, your sort on the "helper column" containing the reformatted values.
Manual method
Use the Text to Column function and separate out your headers using "." as a delimiter.
When you are done select all the data as follows:
Perform a sort on the selected data.
Note: My data has headers has been selected and column 6 and 7 come up a A to Z as they are currently empty and it defaults to alphabetical sort as a result. The alphabetical sort can be added by adding a dummy row of data at the start or end of your data to be sorted. This is done by either adding all 0's or a number larger than any number in your list to all columns.
After selecting ok your "Combined" data will be sorted numerically based on the outline numbers to the right.
Here is some code for multiple purposes.
The first function is a UDF which if wanted could be called from the worksheet to be used as a helper function. Therefore it's easy enough to alter the pad length if required for sorting.
The second code is a little more involved but inserts a column next to the table, adds the helper function, sorts then deletes the helper column to leave the sheet structure as before.
SortColumn should be defined as the column index where the indices are. i.e. if in the first column of the designated table then it would be set to '1'
Public Function PadIndices(Cell As Range, PadLength As Long, Optional Delimiter As String) As String
If Cell.Count > 1 Then Exit Function
If Delimiter = "" Then Delimiter = "."
Dim Arr As Variant: Arr = Split(Cell.Value, Delimiter)
Dim i As Long: For i = LBound(Arr) To UBound(Arr)
If Len(Arr(i)) < PadLength Then Arr(i) = WorksheetFunction.Rept("0", PadLength - Len(Arr(i))) & Arr(i)
Next i
PadIndices = Join(Arr, Delimiter)
End Function
Sub SortByIndices()
Dim ws As Worksheet: Set ws = ThisWorkbook.Sheets("Sheet1")
Dim Table As Range: Set Table = ws.Range("H7:I11")
Dim PadLength As Long: PadLength = 2
Dim SortColumn As Long: SortColumn = 1
Table.Columns(1).Insert Shift:=xlToRight, CopyOrigin:=xlFormatFromLeftOrAbove
Dim SortRange As Range: Set SortRange = Table.Columns(1).Offset(0, -1)
SortRange.Formula2R1C1 = "=PadIndices(RC[" & SortColumn & "], " & PadLength & ")"
With ws.Sort.SortFields
.Clear
.Add2 Key:=SortRange, SortOn:=xlSortOnValues, Order:=xlAscending, DataOption:=xlSortNormal
End With
With ws.Sort
.SetRange Application.Union(Table, SortRange)
.Header = xlGuess
.MatchCase = False
.Orientation = xlTopToBottom
.SortMethod = xlPinYin
.Apply
End With
SortRange.Delete Shift:=xlToLeft
End Sub
You could make a helper column in which you remove the points and sort that helper column.
=NUMBERVALUE(SUBSTITUTE(E4;".";))
A) User defined function without the need of a help column
In order to be able to sort outline numbers, you have to bring the individual numerical values
to a well defined uniform number format (like e.g. "00" if numbers don't exceed 99 as assumed default; note the flexible String() function in section b)).
This dynamic array approach allows argument inputs of any range definitions (plus optional digit maxima) like
=Outline(A5:A10) to sort one column (with a 2-digits default maximum) or even
=Outline(A2:E4, 3) over a multicolumn range (with an explicit 3-digits maximum)
Note: tested with the newer dynamic features of Office 2019+/MS365;
for backward compatibility you would have to change the TextJoin() function and possibly enter =Outline(...) as array formula using CSE (Ctrl+Shift+Enter).
Function Outline(rng As Range, Optional ByVal digits As Long = 2)
'Date: 2022-01-09
'Auth: https://stackoverflow.com/users/6460297/t-m
'a) create unordered 1-dim array from any contiguous range
Dim myFormula As String
myFormula = "TextJoin("","",True," & rng.Address(False, False) & ")"
Dim codes
codes = Split(rng.Parent.Evaluate(myFormula), ",")
'b) add leading zeros via number format
Dim i As Long
For i = LBound(codes) To UBound(codes)
Dim tmp: tmp = Split(codes(i), ".")
Dim ii As Long
For ii = LBound(tmp) To UBound(tmp)
tmp(ii) = Format(CInt(tmp(ii)), String(digits, "0"))
Next ii
codes(i) = Join(tmp, ".") ' join to entire string element
Debug.Print i, codes(i)
Next i
'c) sort
BubbleSort codes ' << help proc BubbleSort
'd) remove leading zeros again
For i = LBound(codes) To UBound(codes)
For ii = 1 To digits - 1 ' repeat (digits - 1) times
codes(i) = Replace(codes(i), ".0", ".")
If Left(codes(i), 1) = "0" Then codes(i) = Mid(codes(i), 2)
Next
Next
'e) return function result
Outline = Application.Transpose(codes)
End Function
Help procedure BubbleSort
Sub BubbleSort(arr)
'Date: 2022-01-09
'Auth: https://stackoverflow.com/users/6460297/t-m
Dim cnt As Long, nxt As Long, temp
For cnt = LBound(arr) To UBound(arr) - 1
For nxt = cnt + 1 To UBound(arr)
If arr(cnt) > arr(nxt) Then
temp = arr(cnt)
arr(cnt) = arr(nxt)
arr(nxt) = temp
End If
Next nxt
Next cnt
End Sub
B) Just for fun: alternative single-formula approach (with restricted number range)
Instead of extending the digit formats, I played with the idea to restrict the numeric display
by executing a temporary hexadecimal replacement.
Note that this approach based on a single-formula evaluation
allows outline sub-numbers only within a numeric range from 1 to 15 (as numbers 10 to 15 get replaced by characters A to F), but might be sufficient for low hierarchy depths! Furthermore it includes a tabular Sort() function available only in Excel version MS365!
Function Outline(rng As Range)
'Site: https://stackoverflow.com/questions/70565436/how-to-sort-outline-numbers-in-numerical-order
'Date: 2022-01-09
'Auth: https://stackoverflow.com/users/6460297/t-m
'Meth: hex replacements + sort; assuming chapters from (0)1 to 15 (10=A,11=B..15=F)
'Note: allows outline sub-numbers only up to 15! Needs Excel version MS365.
Dim pattern
pattern = String(6, "X") & "Sort(" & String(6, "X") & "$,15,""F""),14,""E""),13,""D""),12,""C""),11,""B""),10,""A"")),""A"",10),""B"",11),""C"",12),""D"",13),""E"",14),""F"",15)"
pattern = Replace(Replace(pattern, "$", rng.Address(False, False)), "X", "Substitute(")
Outline = rng.Parent.Evaluate(pattern)
End Function
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
My question is basically the opposite of THIS ONE (which had a database-based solution I can't use here).
I use SAP, which sorts characters this way:
0-9, A-Z, _
but I'm downloading data into Excel and manipulating ranges dependent on correct SAP character set sort order.
How can I force Excel to sort the same way as SAP, with underscore coming last.
After attempting a Custom Sort List of single characters in Excel's Sort feature, Excel still/always sorts like this:
_, 0-9, A-Z
Is there any way to get Excel to sort like SAP? I'm capable of doing Excel macros, if needed.
Alternatively, if anyone knows how to get native SAP tables to sort like Excel in the SAP interface, that would take care of this problem, as well.
The principle of the following solution is to insert a new column in which the cells have a formula which calculates a "sortable code" of each cell of the column that you want to sort.
If you sort this new column, the rows will be sorted in the ASCII order (0-9, A-Z, _).
It should be able to handle any number of rows. On my laptop, the calculation of cells takes 1 minute for 130.000 rows. There are two VBA functions, one for ASCII and one for EBCDIC. It's very easy to define other character sets.
Steps:
Create a module in your Excel workbook and place the code below.
Close the VB editor otherwise it will run slowly.
In the worksheet that you want to sort, insert one column for each column you want to sort, for instance let's say the sort is to be done for column A, create a new column B, in the cell B1 insert the formula =SortableCodeASCII(A1) and do the same for all the cells of column B (up to the last row of column A).
Make sure that the calculation of formulas is over (it takes 1 minute for 130.000 rows on my laptop), otherwise if you sort, the order will be incorrect because formulas are not yet calculated. You see the progress indicator (percentage) on the status bar at the bottom of the Excel window. If you don't see it, press Ctrl+Alt+F9.
Sort on column B. The values in column A should be sorted according to the ASCII order (0-9, A-Z, _)
Good luck!
Option Compare Text 'to make true "a" = "A", "_" < "0", etc.
Option Base 0 'to start arrays at index 0 (LBound(array) = 0)
Dim SortableCharactersASCII() As String
Dim SortableCharactersEBCDIC() As String
Dim SortableCharactersTEST() As String
Sub ResetSortableCode()
'Run this subroutine if you change anything in the code of this module
'to regenerate the arrays SortableCharacters*
Erase SortableCharactersASCII
Erase SortableCharactersEBCDIC
Erase SortableCharactersTEST
Call SortableCodeASCII("")
Call SortableCodeEBCDIC("")
Call SortableCodeTEST("")
End Sub
Function SortableCodeASCII(text As String)
If (Not Not SortableCharactersASCII) = 0 Then
SortableCharactersASCII = getSortableCharacters( _
orderedCharacters:=" !""#$%&'()*+,-./0123456789:;<=>?#ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}" & ChrW(126) & ChrW(127))
End If
SortableCodeASCII = getSortableCode(text, SortableCharactersASCII)
End Function
Function SortableCodeEBCDIC(text As String)
If (Not Not SortableCharactersEBCDIC) = 0 Then
SortableCharactersEBCDIC = getSortableCharacters( _
orderedCharacters:=" ¢.<(+|&!$*);-/¦,%_>?`:##'=""abcdefghi±jklmnopqr~stuvwxyz^[]{ABCDEFGHI}JKLMNOPQR\STUVWXYZ0123456789")
End If
SortableCodeEBCDIC = getSortableCode(text, SortableCharactersEBCDIC)
End Function
Function SortableCodeTEST(text As String)
If (Not Not SortableCharactersTEST) = 0 Then
SortableCharactersTEST = getSortableCharacters( _
orderedCharacters:="ABCDEF 0123456789_")
End If
SortableCodeTEST = getSortableCode(text, SortableCharactersTEST)
End Function
Function getSortableCharacters(orderedCharacters As String) As String()
'Each character X is assigned another character Y so that sort by character Y will
'sort character X in the desired order.
maxAscW = 0
For i = 1 To Len(orderedCharacters)
If AscW(Mid(orderedCharacters, i, 1)) > maxAscW Then
maxAscW = AscW(Mid(orderedCharacters, i, 1))
End If
Next
Dim aTemp() As String
ReDim aTemp(maxAscW)
j = 0
For i = 1 To Len(orderedCharacters)
'Was a character with same "sort weight" previously processed ("a" = "A")
For i2 = 1 To i - 1
If AscW(Mid(orderedCharacters, i, 1)) <> AscW(Mid(orderedCharacters, i2, 1)) _
And Mid(orderedCharacters, i, 1) = Mid(orderedCharacters, i2, 1) Then
'If two distinct characters are equal when case is ignored (e.g. "a" and "A")
'(this is possible only because directive "Option Compare Text" is defined at top of module)
'then only one should be used (either "a" or "A" but not both), so that the Excel sorting
'does not vary depending on sorting option "Ignore case".
Exit For
End If
Next
If i2 = i Then
'NO
aTemp(AscW(Mid(orderedCharacters, i, 1))) = Format(j, "000")
j = j + 1
Else
'YES "a" has same weight as "A"
aTemp(AscW(Mid(orderedCharacters, i, 1))) = aTemp(AscW(Mid(orderedCharacters, i2, 1)))
End If
Next
'Last character is for any character of input text which is not in orderedCharacters
aTemp(maxAscW) = Format(j, "000")
getSortableCharacters = aTemp
End Function
Function getOrderedCharactersCurrentLocale(numOfChars As Integer) As String
'Build a string of characters, ordered according to the LOCALE order.
' (NB: to order by LOCALE, the directive "Option Compare Text" must be at the beginning of the module)
'Before sorting, the placed characters are: ChrW(0), ChrW(1), ..., ChrW(numOfChars-1), ChrW(numOfChars).
'Note that some characters are not used: for those characters which have the same sort weight
' like "a" and "A", only the first one is kept.
'For debug, you may define constdebug=48 so that to use "printable" characters in sOrder:
' ChrW(48) ("0"), ChrW(49) ("1"), ..., ChrW(numOfChars+47), ChrW(numOfChars+48).
sOrder = ""
constdebug = 0 'Use 48 to help debugging (ChrW(48) = "0")
i = 34
Do Until Len(sOrder) = numOfChars
Select Case constdebug + i
Case 0, 7, 14, 15: i = i + 1
End Select
sCharacter = ChrW(constdebug + i)
'Search order of character in current locale
iOrder = 0
For j = 1 To Len(sOrder)
If AscW(sCharacter) <> AscW(Mid(sOrder, j, 1)) And sCharacter = Mid(sOrder, j, 1) Then
'If two distinct characters are equal when case is ignored (e.g. "a" and "A")
'("a" = "A" can be true only because directive "Option Compare Text" is defined at top of module)
'then only one should be used (either "a" or "A" but not both), so that the Excel sorting
'does not vary depending on sorting option "Ignore case".
iOrder = -1
Exit For
ElseIf Mid(sOrder, j, 1) <= sCharacter Then
'Compare characters based on the LOCALE order, that's possible because
'the directive "Option Compare Text" has been defined.
iOrder = j
End If
Next
If iOrder = 0 Then
sOrder = ChrW(constdebug + i) & sOrder
ElseIf iOrder = Len(sOrder) Then
sOrder = sOrder & ChrW(constdebug + i)
ElseIf iOrder >= 1 Then
sOrder = Left(sOrder, iOrder) & ChrW(constdebug + i) & Mid(sOrder, iOrder + 1)
End If
i = i + 1
Loop
'Last character is for any character of input text which is not in orderedCharacters
sOrder = sOrder & ChrW(constdebug + numOfChars)
getOrderedCharactersCurrentLocale = sOrder
End Function
Function getSortableCode(text As String, SortableCharacters() As String) As String
'Used to calculate a sortable text such a way it fits a given order of characters.
'Example: instead of order _, 0-9, Aa-Zz you may want 0-9, Aa-Zz, _
'Will work only if Option Compare Text is defined at the beginning of the module.
getSortableCode = ""
For i = 1 To Len(text)
If AscW(Mid(text, i, 1)) < UBound(SortableCharacters) Then
If SortableCharacters(AscW(Mid(text, i, 1))) <> "" Then
getSortableCode = getSortableCode & SortableCharacters(AscW(Mid(text, i, 1)))
Else
'Character has not an order sequence defined -> last in order
getSortableCode = getSortableCode & SortableCharacters(UBound(SortableCharacters))
End If
Else
'Character has not an order sequence defined -> last in order
getSortableCode = getSortableCode & SortableCharacters(UBound(SortableCharacters))
End If
Next
'For two texts "a1" and "A1" having the same sortable code, appending the original text allows using the sort option "Ignore Case"/"Respecter la casse"
getSortableCode = getSortableCode & " " & text
End Function
EDIT: this solution is based on the automatic calculation of a custom order list, but it doesn't work if there are too many distinct values. In my case it worked with a custom order list of maybe a total of 35.000 characters, but it failed for the big list of the original poster.
The following code sorts the requested column(s) by ASCII value, which has this kind of order:
0-9, A-Z, _, a-z
I guess the lower case being separated from the upper case is not an issue as SAP defines values mostly in upper case. If needed, the code can be easily adapted to obtain the custom order 0-9, Aa-Zz, _ (by using UCase and worksheet.Sort.MatchCase = False).
This order is different from the built-in Excel sort order which is based on the locale. For instance, in English, it would be:
_, 0-9, Aa-Zz
The principle is to use a "custom order list" whose values are taken from the Excel column, made unique, and sorted with a QuickSort3 algorithm (subroutine MedianThreeQuickSort1 provided by Ellis Dee at http://www.vbforums.com/showthread.php?473677-VB6-Sorting-algorithms-(sort-array-sorting-arrays)).
Performance notes about the Excel sorting via custom list (I'm not talking about QuickSort3):
The more the distinct values in the custom order list, the lower the performance. 4,000 rows having 20 distinct values are sorted immediately, but 4,000 rows having 4,000 distinct values takes 8 seconds to sort!
For the same number of distinct values, the performance does not change a lot if there are many rows to sort. 300,000 rows having 6 distinct values takes 3 seconds to sort.
Sub SortByAsciiValue()
With ActiveSheet.Sort
.SortFields.Clear
.SetRange Range("A:A").CurrentRegion
.SortFields.Add Key:=Columns("A"), Order:=xlAscending, _
CustomOrder:=DistinctValuesInAsciiOrder(iRange:=Columns("A"), Header:=True)
.Header = xlYes
.Apply
End With
End Sub
Function DistinctValuesInAsciiOrder(iRange As Range, Header As Boolean) As String
Dim oCell As Range
Dim oColl As New Collection
On Error Resume Next
For Each oCell In iRange.Cells
Err.Clear
If Header = True And oCell.Row = iRange.Row Then
ElseIf oCell.Row > iRange.Worksheet.UsedRange.Rows.Count Then
Exit For
Else
dummy = oColl.Item(oCell.Text)
If Err.Number <> 0 Then
oColl.Add oCell.Text, oCell.Text
totalLength = totalLength + Len(oCell.Text) + 1
End If
End If
Next
On Error GoTo 0
If oColl.Count = 0 Then
Exit Function
End If
Dim values() As String
ReDim values(1)
ReDim values(oColl.Count - 1 + LBound(values))
For i = 1 To oColl.Count
values(i - 1 + LBound(values)) = oColl(i)
Next
Call MedianThreeQuickSort1(values)
' String concatenation is complex just for better performance (allocate space once)
DistinctValuesInAsciiOrder = Space(totalLength - 1)
Mid(DistinctValuesInAsciiOrder, 1, Len(values(LBound(values)))) = values(LBound(values))
off = 1 + Len(values(LBound(values)))
For i = LBound(values) + 1 To UBound(values)
Mid(DistinctValuesInAsciiOrder, off, 1 + Len(values(i))) = "," & values(i)
off = off + 1 + Len(values(i))
Next
End Function
I need to print a string of arrays dependent on a difference of two values on my input page to separate sheets within the same PDF but I have been running into a few issues.
Based on the difference of two cells, the function will determine which arrays to print.
There are two possible solutions I have thought of but have been unsuccessful attempting both.
Indirectly reference a string of arrays in a cell to print such as "abc,bcd,cde,def,efg..."
(As Shown Below) Use conditional if-then functions to invoke the array based on the difference in these two cells
Primary Goals
Print into a single PDF
Determine specific arrays to print depending on the difference in two values contained in a cell on my input page
Allow for PageSetup values (have this figured out)
I am using MSFT 365. I tried initially using an indirect array reference to a cell with a variable value string including the arrays to be included without success.
Next, I tried to hardcode for all 100 possible values for this difference but in that case, I am running into line limits and errors associated with using _ to continue the array function on another line.
If the difference value equals 3, it is shown as below. If the difference value equals 4, you would add another array line including "schedule05","report05","p&l05"
Option Explicit
Sub PrintTest()
'if a certain difference value, use
If (Worksheets("Inputs").Range("D7") - Worksheets("Inputs").Range("D6")) = "3" Then
Dim pageArray As Variant
'set array for given difference
pageArray = Array("schedule01", "report01", "p&l01", _
"schedule02", "report02", "p&l02", _
"schedule03", "report03", "p&l03", _
"schedule04", "report04", "p&l04")
Worksheets("data").Activate
Worksheets("data").PageSetup.CenterHorizontally = True
'page setup values
With ActiveSheet.PageSetup
.FitToPagesWide = 1
.FitToPagesTall = 1
.Orientation = xlLandscape
End With
'call array for print
Worksheets("data").Range("pageArray").PrintOut
Elseif
'Here is where I could put another similar function for a difference of 4
'......
Else
'Here is where I could put another similar function for a difference of x
End If
End Sub
I expected this would get me a PDF where each of these arrays is printed on a separate sheet and will print a selection of arrays based on the difference value.
To expand on my comment, it would look like this:
Dim lDiff As Long
Dim pageArray As Variant
Dim sFormat As String
Dim i As Long, j As Long
'if a certain difference value, use
lDiff = Worksheets("Inputs").Range("D7").Value - Worksheets("Inputs").Range("D6").Value
ReDim pageArray(1 To (lDiff + 1) * 3)
For i = 1 To UBound(pageArray, 1) Step 3
j = j + 1
If j < 100 Then sFormat = "00" Else sFormat = "000"
pageArray(i) = "schedule" & Format(j, sFormat)
pageArray(i + 1) = "report" & Format(j, sFormat)
pageArray(i + 2) = "p&l" & Format(j, sFormat)
MsgBox pageArray(i)
Next i
I am working on algorithm base tool; Kindly assist me for below problem.
1.First I find row number based on one criteria (Dynamic). Assume row number is 5 and it has set of Value From (B5:F5)
Set FindRow = SearchRange.Find(Sheet1.Cells(xRow, 2).Text, LookIn:=xlValues, lookat:=xlWhole)
MyRow = FindRow.Row
2.I have header with numeric value(B1:F1)
3.Then I need to find column number, ie MyCol is column number of minimum value cell in (B1:F1)
4.Then I test one criteria with If Cells(MyRow,MyCol)="ABC" Then test fail and again I need go and find next Minimum value in (B1:F1) and column number, ie MyCol, Until I Meet the condition.
I tried array, I am not able to find solution, Any help would be much appreciated. My Thanks in advance.
If I understand correctly, what you need is an indexed sort. Many languages provide an indexed sort as a standard function. VBA has neither a sort nor an indexed sort as standard.
With a conventional array sort, values are sorted within the array. For example: suppose I have an array with values:
A D B E C
If I pass that array to a sort, it is returned as:
A B C D E
But sometimes you cannot sort the array. In your case, the array is a range of column headings. You cannot sort those headings because they belong with their columns. You would have to sort the columns which is at best impractical and probably unacceptable since the sequence of columns will mean something.
With an indexed sort, you create arrays Keys and Indices:
Keys A D B E C
Indices 1 2 3 4 5
Both these arrays are passed to the sort which leaves Keys unchanged and sorts Indices to give:
Indices 1 3 5 2 4
With the regular sort, you access the sorted entries as Array(1). Array(2) and so on. With an indexed sort, you access the sorted entries as Array(Indices(1)). Array(Indices(2)) and so on.
Going via an index to get the sorted entries can be a little difficult to understand at first and it is undoubtedly fiddlier that going directly to the source array.
Below I have given you an indexed Insertion Sort. An Insertion Sort is simple and easy to understand but is slow with large numbers of entries. You only have five entries to sort so its performance is acceptable. Look at the Wiki entry for "Insertion Sort" for a pictorial demonstration of how it works.
Macro DemoSortColumnHeadings shows how to use the sort and how to access the column headings. I have used the name ColHeads instead of Keys and ColNums instead of Indices because I believe this will make DemoSortColumnHeadings easier to understand. The sorted ColNums contains the column numbers in the sequence you require. After the sort, the array ColHeads is no longer required.
One last point. VBA is the only language I know which allows you to specify both the lower bound and the upper bound of an array. Most languages require the lower bound to be zero. I have taken advantage of this to define the dimensions of the arrays as (2 to 6) and not (0 to 4). This is why the values in array ColNums are column numbers. With most languages, I would have needed ColNums(N)+2 to get the column number.
Option Explicit
Sub DemoSortColumnHeadings()
Const ColFirst As Long = 2 ' Column B = column 2
Const ColLast As Long = 6 ' Column F = column 6
Dim ColCrnt As Long
Dim ColNums() As Long
Dim InxColNum As Long
Dim ColHeads() As String
With Worksheets("Test data")
ReDim ColHeads(ColFirst To ColLast)
ReDim ColNums(ColFirst To ColLast)
For ColCrnt = ColFirst To ColLast
ColHeads(ColCrnt) = .Cells(1, ColCrnt).Value
ColNums(ColCrnt) = ColCrnt
Next
Debug.Print "Initial sequence"
Debug.Print "|";
For ColCrnt = ColFirst To ColLast
Debug.Print .Cells(1, ColCrnt).Value & "|";
Next
Debug.Print
Call InsertionSort(ColNums, ColHeads)
Debug.Print "Final sequence"
Debug.Print "|";
For InxColNum = LBound(ColNums) To UBound(ColNums)
ColCrnt = ColNums(InxColNum)
Debug.Print .Cells(1, ColCrnt).Value & "|";
Next
Debug.Print
End With
End Sub
Public Sub InsertionSort(ByRef Indices() As Long, ByRef Keys() As String)
Dim Found As Boolean
Dim I As Long
Dim InxIFwd As Long
Dim InxIBack As Long
For InxIFwd = LBound(Indices) + 1 To UBound(Indices)
I = Indices(InxIFwd) ' Save value of current entry in Indices
' Find first entry back, if any, such that Keys(I) >= Keys(Indices(InxIBack))
' If Keys(I) < Keys(Indices(InxIBack)), set Indices(InxIBack+1) to
' Indices(InxIBack). That is move indices for keys greater that Keys(I) down
' Indices leaving a space for I nearer the beginning.
Found = False
For InxIBack = InxIFwd - 1 To LBound(Indices) Step -1
If Keys(I) >= Keys(Indices(InxIBack)) Then
' Keys(I) belongs after Keys(Indices(InxIBack))
Indices(InxIBack + 1) = I
Found = True
Exit For
End If
Indices(InxIBack + 1) = Indices(InxIBack)
Next
If Not Found Then
' Insertion point for I not found so it belongs at beginning of Indices
Indices(LBound(Indices)) = I
End If
Next
End Sub
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)