MIN/MAX on text sort order - excel

In SQL Server, MIN and MAX can act on text that doesn't evaluate to numbers, returning the text item with the lowest or highest text sort order, or as it's known in SQL Server-speak, "collation order".
Is it possible to do that in Excel without going to a UDF that actually sorts?
For example, for MIN("bb","aa","cc") to return "aa", and MAX("bb","cc","aa") to return "cc".
Excel's MIN/MAX ignore text, and although MINA/MAXA can work on text, they break on text that doesn't resolve to a number. LARGE/SMALL don't do it either.
FWIW, a coworker asked me how to do this in a pivot. I don't see a way without going to a custom function. Am I wrong?

This array formula looks promising. since it is an array it needs to be entered with ctrl-shift-enter.
Max:
=INDEX(A2:A6,MATCH(0,COUNTIF(A2:A6,">"&A2:A6), 0))
Min:
=INDEX(A2:A6,MATCH(0,COUNTIF(A2:A6,"<"&A2:A6), 0))
Change the three ranges to what you want.
The max and min versions are the same except for the > versus <.

I believe you are correct, a custom function is best. The good thing to note is the normal comparator operators work similarly as you described.
Public Function MinStr(ByVal strVal As Range) As String
Dim i As Integer
Dim cell As Range
MinStr = ""
'Check to make sure the range is not empty
if strVal.Rows.Count > 0 then
'Initialize MinStr to a known value
MinStr = strVal.cells(1,1).Value
'Iterate through the entire range
For Each cell in strVal.Cells
if(MinStr > cell.Value) then
MinStr = cell.Value
end if
Next cell
end if
End Function
Public Function MaxStr(ByVal strVal As Range) As String
Dim i As Integer
Dim cell As Range
MaxStr = ""
'Check to make sure the range is not empty
if strVal.Rows.Count > 0 then
'Initialize MaxStr to a known value
MaxStr = strVal.cells(1,1).Value
'Iterate through the entire range
For Each cell in strVal.Cells
if(MaxStr < cell.Value) then
MaxStr = cell.Value
end if
Next cell
end if
End Function

Related

Listing unique values formula taking a long time to process

I have a formula from a previous question that's working fine. It lists the unique values of dynamic column A to column B, starting from B2. Often Column A has several thousand values, then the processing takes a long time. Increasing calculation threads hasn't saved much time. I'm looking for a better method or formula that could save me a lot of time.
=IFERROR(INDEX(A:A;AGGREGATE(15;6;ROW($A$2:INDEX(A:A;MATCH("zzz";A:A)))/(COUNTIF($B$1:B1;$A$2:INDEX(A:A;MATCH("zzz";A:A)))=0);1));"")
As mentioned in the comments to your question, using the new, "pre-official" UNIQUE function or a pivot table may be the easiest and fastest way to get the unique values. However, if you would like to use a VBA function that does not require pressing a button or using a newer version of Excel, you may want to try the VBA function "GetUniques" described below.
This is a sample of how the function could be used:
To use the function, one must do 3 things:
Add a reference to mscorlib.dll in the VBA Editor (reason explained below)
Add the code for the VBA function itself (preferably in a module of its own)
Add code to handle the workbook's SheetCalculate event (reason explained below)
The reason for the mscorlib.dll was to use the "ArrayList" class (which made life easier than using the Collection class) because it comes with a sorting method (otherwise, we would have to implement a QuickSort procedure). To avoid late binding, I added the reference to this library (located at "C:\Windows\Microsoft.NET\Framework\v4.0.30319" on my machine) in the VBA Editor. You may want to go to the link below for more info on how to use this class:
https://excelmacromastery.com/vba-arraylist/
The VBA function actually writes values outside of the formula cell from which it is called. Since Excel does not take too well to this, a workaround was needed. I tried to use the "Application.Evaluate" method as a workaround, which is suggested in various places, but it did not work for me for some reason. Therefore, I was forced to use the SheetCalculate event (as recommended in other places). In short, the function itself does not write values outside of the caller cell but leaves a "request" for it in a "quasi-queue" that is then processed whilst Excel handles the SheetCalculate event; this event will be triggered after the VBA function has finished executing. This function writes the first value within the formula cell itself and the rest of the values directly below the formula cell.
The "GetUniques" function takes two arguments:
The range with the values to process (I recommend sending the entire column as the range, unless there is a header)
An optional "data type" string that allows the function to convert the values to the right data type (to avoid errors when comparing values of different types)
The optional "data type" value can be "L" (meaning "long integers"), "D" (meaning "dates"), "F" (meaning floating-point doubles), "S" (meaning case-insensitive strings), or "S2" (meaning "case-sensitive strings"). Values that cannot be converted will simply be ignored. If no "data type" value is provided, no type conversion is attempted, but the function may error out if an invalid comparison between different data types is attempted.
The code for the VBA function, called "GetUniques", appears below. This code can be copy-pasted to a module of its own:
Option Explicit
'This is the "commands queue" that is filled up in this module and is "executed" during the SheetCalculate event
Public ExtraCalcCommands As New Collection
Function GetUniques(ByVal dataRange As Range, Optional ByVal dataType As String = "") As Variant
'Attempt to remove unused cells from the data range to make it smaller
Dim dataRng As Range
Set dataRng = Application.Intersect(dataRange, dataRange.Worksheet.UsedRange)
'If the range is completely empty, simply exit
If dataRng Is Nothing Then
GetUniques = ""
Exit Function
End If
'Read in all the data values from the range
Dim values As Variant: values = dataRng.value
'If the values do not form an array, it is a single value, so just return it
If Not IsArray(values) Then
GetUniques = values
Exit Function
End If
'Get the 2-dimensional array's bounds
Dim arrLb As Long: arrLb = LBound(values, 1)
Dim arrUb As Long: arrUb = UBound(values, 1)
Dim index2 As Long: index2 = LBound(values, 2) 'In the 2nd dimension, we only
' care about the first column
'Remember the original number of values
Dim arrCount As Long: arrCount = arrUb - arrLb + 1
'Since [values] is an array, we know that arrCount >= 2
Dim i As Long
'Using ArrayList based on ideas from https://excelmacromastery.com/vba-arraylist
'Copy the values to an ArrayList object, discarding blank values and values
' that cannot be converted to the desired data type (if one was specified)
Dim valuesList As New ArrayList
Dim arrValue As Variant
For i = arrLb To arrUb
arrValue = values(i, index2)
If (arrValue & "") = "" Then
'Skip blank values
ElseIf Not CouldConvert(arrValue, dataType) Then
'This conversion may be necessary to ensure that the values can be compared against each other during the sort
Else
valuesList.Add arrValue
End If
Next
Dim valuesCount As Long: valuesCount = valuesList.Count
'Sort the list to easily remove adjacent duplicates
If Not CouldSort(valuesList) Then
GetUniques = "#ERROR: Could not sort - consider using the data type argument"
Exit Function
End If
'Remove duplicates (which are now adjacent due to the sort)
Dim previous As Variant
If valuesCount > 0 Then previous = valuesList.Item(0)
Dim current As Variant
i = 1
Do While i < valuesCount
current = valuesList.Item(i)
If ValuesMatch(current, previous, dataType) Then 'Remove duplicates
valuesList.RemoveAt i
valuesCount = valuesCount - 1
Else
previous = current
i = i + 1
End If
Loop
'Replace the removed items with empty strings at the end of the list
' This is to get back to the original number of values
For i = 1 To arrCount - valuesCount
valuesList.Add ""
Next
'Return the first value as the function result
GetUniques = valuesList.Item(0) 'We know valuesList.Count=arrCount>=2
'Write the rest of the values below
valuesList.RemoveAt 0
WriteArrayTo valuesList, Application.Caller.Offset(1, 0)
End Function
Private Function CouldSort(ByRef valuesList As ArrayList)
On Error Resume Next
valuesList.Sort
CouldSort = Err.Number = 0
End Function
Private Function CouldConvert(ByRef value As Variant, ByVal dataType As String)
CouldConvert = True
If dataType = "" Then Exit Function
On Error Resume Next
Select Case dataType
Case "L": value = CLng(value)
Case "F": value = CDbl(value)
Case "D": value = CDate(value)
Case "S", "S2": value = value & ""
End Select
CouldConvert = Err.Number = 0
End Function
Private Function ValuesMatch(ByVal v1 As Variant, ByVal v2 As Variant, ByVal dataType As String) As Boolean
On Error Resume Next
Select Case dataType
Case "S": ValuesMatch = StrComp(v1, v2, vbTextCompare) = 0
Case "S2": ValuesMatch = StrComp(v1, v2, vbBinaryCompare) = 0
Case Else: ValuesMatch = v1 = v2
End Select
If Err.Number <> 0 Then ValuesMatch = False
End Function
Private Sub WriteArrayTo(ByVal list As ArrayList, ByRef destination As Range)
'This procedure does not do the actual writing but saves the "command" to do the writing in a "queue";
' this "commands queue" will be executed in the SheetCalculate event;
'We cannot write to cells outside the UDF's formula whilst the function is being calculated
' because of Excel restrictions; that is why we must postpone the writing for later
Dim coll As New Collection
coll.Add "DoWriteList" 'Name of the procedure to execute
coll.Add destination '1st argument used by the procedure
coll.Add list '2nd argument used by the procedure
ExtraCalcCommands.Add coll
End Sub
This code must be added in the workbook's "ThisWorkbook" module in order to handle the SheetCalculate event:
Private Sub Workbook_SheetCalculate(ByVal Sh As Object)
Dim i&
Do While ExtraCalcCommands.Count > 0
Dim cmdColl As Collection: Set cmdColl = ExtraCalcCommands.Item(1)
Select Case cmdColl.Item(1)
Case "DoWriteList": DoWriteList cmdColl.Item(2), cmdColl.Item(3)
'Other procedure names could go here in future
End Select
'Remove the processed "command" from the queue
ExtraCalcCommands.Remove 1
Loop
End Sub
Private Sub DoWriteList(ByRef destination As Range, ByVal list As ArrayList)
destination.Resize(list.Count, 1).value = WorksheetFunction.Transpose(list.ToArray)
End Sub
I hope the above is of some help, and, if so, I hope it is a speed improvement on the original IFERROR formula. I also hope the SheetCalculate event handler does not pose issues in dense workbooks with many formulas and calculations.

Is possible find the value not equal to one in the column and get all position in the same field?

I need to find the value not equal 1 in the column A and get all the position in the same field.
I want to get the all not equal 1 position into the B11.
I want to use the excel formula function and don't use vba or other code. How can I do ? Is possible ?
Since you are working with Excel-2007, unfortunately you won't be able to use the new TEXTJOIN() and CONCAT() functions (nor power query). However you can get quite creative with a function like CONCATENATE(), it will most likely mean at some point you'll have to do some manual 'labour'. Who wants to do manual labour? ;)
So in this case I would prefer to go the UDF way. Below a tested example:
Function GetPos(RNG As Range, VAL As Double) As String
Dim CL As Range, ARR() As String, X as double
X = 1
For Each CL In RNG
If CL.Value <> VAL Then
ReDim Preserve ARR(X)
ARR(X) = CL.Address(False, False)
X = X + 1
End If
Next CL
If IsEmpty(ARR) Then
GetPos = "No hits"
Else
GetPos = Join(ARR, ",")
GetPos = Right(GetPos, Len(GetPos) - 1)
End If
End Function
This one takes two criteria, a range and a numeric value indicating what the cells in your range must NOT be. It will return a string value.
Call it from your worksheet through =GETPOS(A1:A10,1) and it should return A2,A7,A9
EDIT
If you are fine using a helper column you could do it like so:
Formula in B1:
=IF(A1<>1,"A"&ROW()&",","")
Formula in B11:
=LEFT(B1&B2&B3&B4&B5&B6&B7&B8&B9&B10,SUM(LEN(B1:B10))-1)
Enter through CtrlShiftEnter
Note: If you don't want to use a helper column you'll have to use TRANSPOSE() to 'load' an array of text values but it involves manual labour and IMO you'll surpass your goal.

Excel VBA - Find the address of maximum (or any specific value) in range, if the range containes DOUBLE values

The problem:
There is a range with double values. I would like to get the address of the maximum value.
I tried with the match function, but it can't compare doubles (gives false results), and my range isn't ordered.
There are ugly solutions (for example I can multiply my numbers by 10000, if I want 5 digit precision, then get the integer part and compare that, but it is very slow with more than 20000 rows) and maybe there are more elegant solutions.
Thanks :)
Sample Data: These are the numbers after Debug.Print
B 7.59999999999934E-02
C 7.00000000000074E-02
D 0.335000000000008
E 8.19999999999936E-02
F 8.49999999999937E-02
G 7.39999999999981E-02
H 5.49999999999926E-02
I 0.070999999999998
J 0.165000000000006
K 7.59999999999934E-02
Why not just iterate your range and change a variable if the next cell double value is greater? Once you finish this range use the find to return cell row and column. See below
Dim i as Variant
For Each i In Worksheets("yourWorkSheet").Range(Range("youStartCell"), Range("yourStartCell").End(xlDown)).Cells
If i.Offset(-1) < i Then
i = i.value
End if
Next
Should look something like this. Need to handle the first row case in loop but if i did that you would have no work to do.
This is my final solution:
'Find max and its address in range
Public Function maxAddress(rng As Range) As DoubleLong
Dim cell As Range
For Each cell In rng
If IsNumeric(cell.Value2) Then
If cell.Value2 > maxAddress.db Then
maxAddress.db = cell.Value2
maxAddress.lg = cell.Row
End If
End If
Next cell
End Function
where is defined a type:
Public Type DoubleLong
db As Double
lg As Long
End Type

Excel - How do programmatically convert 'number stored as Text' to Number?

I'm looking for a simple Excel VBA or formula that can convert an entire row in Excel from 'number stored as Text' to an actual Number for vlookup reasons.
Can anyone point me in the right direction?
Better Approach
You should use INDEX(MATCH) instead of VLOOKUP because VLOOKUP behaves in an unpredictable manner which causes errors, such as the one you're presumably experiencing.
INDEX ( <return array> , MATCH ( <lookup value> , <lookup array> , 0) )
Using 0 as the last argument to MATCH means the match must be exact
Here is some more in-depth information on INDEX(MATCH)-ing
Further
Add zero +0 to convert a value to a number.
This can be (dangerously) extended with IFERROR() to turn non-numeric text into a zero:
=A2+0
=IFERROR(A2+0,0)
For the inverse, you can catenate an empty string &"" to force the value to be a string.
Notes
If 0 is not used as the last argument to MATCH, it will find all sorts of unexpected "matches" .. and worse, it may find a different value even when an exact match is present.
It often makes sense to do some extra work to determine if there are duplicates in the MATCH lookup column, otherwise the first value found will be returned (see example).
Help with MATCH comes from here, notably the matching logic the 3rd argument controls.
This should work if you add it before your vlookup or index/match lines:
Sheets("Sheet1").UsedRange.Value = Sheets("Sheet1").UsedRange.Value
I did find this, but does anyone have a formula as well?
Sub macro()
Range("F:F").Select 'specify the range which suits your purpose
With Selection
Selection.NumberFormat = "General"
.Value = .Value
End With
End Sub
http://www.ozgrid.com/forum/showthread.php?t=64027
Try this:
Sub ConvertToNumber()
Application.ScreenUpdating = False
Dim cl As Range
For Each cl In Selection.Cells
cl.Value = CInt(cl.Value)
Next cl
Application.ScreenUpdating = True
End Sub
To use it, simply select the relevant block of cells with the mouse, and then run the macro (Alt+F8 to bring up the dialogue box). It will go through each cell in the selected range and convert whatever value it holds into a number.
I wrote a custom vlookup function that doesn't care about data formats. Put this into a module in VBA and use = VLOOK instead of = VLOOKUP
Public Function VLook(sValue As String, rDest As Range, iColNo As Integer)
' custom vlookup that's insensitive to data formats
Dim iLastRow As Long
Dim wsDest As Worksheet
Set wsDest = Sheets(rDest.Parent.Name)
iLastRow = wsDest.Range(wsDest.Cells(100000, rDest.Column).Address).End(xlUp).Row
If iLastRow < rDest.Row + rDest.Rows.Count Then
For X = rDest.Column To rDest.Column + rDest.Columns.Count
If wsDest.Cells(100000, X).End(xlUp).Row > iLastRow Then iLastRow = wsDest.Cells(100000, X).End(xlUp).Row
Next X
End If
sValue = UCase(Application.Clean(Trim(sValue)))
For X = rDest.Row To iLastRow
If UCase(Application.Clean(Trim(wsDest.Cells(X, rDest.Column)))) = sValue Then
VLookDM = wsDest.Cells(X, rDest.Column + iColNo - 1)
Exit For
End If
Next X
End Function
The easiest way I can think of is using the built-in function =VALUE(TEXT_TO_CONVERT_TO_STRING).

If "iserror" FOR loop with varying object type

I have a column that has either string types (ex. "N/A") or numbers in it. I'm trying to write a vba code that if the FOR loop comes across a string type it'll be converted to a 0. Essentially what's happening is the code goes down a column (K) of values (ex. $10, $600, $5200, N/A, $5), and keeps the cell value if it's a number and if a cell has text in it then the text is converted to a 0. The range is (K6:K10), the formula would look something like,
=If(iserror(K6/10) = TRUE, 0, K6)
Code:
Dim a As Integer
Dim DilutedRev As Integer
For a = 6 To 10
DilutedRev = ActiveCell(a, 11).Value
If IsError(DilutedRev / 100) = True Then
ActiveCell(a, 11) = 0
Else
ActiveCell(i, 11) = DilutedRev
End If
Use the IsNumeric function to determine if the cell contains a number.
Dim a As Long
With ActiveSheet
For a = 6 To 10
If IsNumeric(.Cells(a, "K").Value2) Then
.Cells(a, 11) = CLng(.Cells(a, "K").Value2)
Else
.Cells(a, 11) = 0
End If
Next a
End With
Using the ActiveCell property like you did was not 'best practise' due to the relative origin point¹; better to use the Range.Cells property.
VBA functions that return boolean values (e.g. IsNumeric, IsError, etc) do not have to be compared to True or False. They are already either True or False.
I've used the Range.Value2 property to check but the Range.Value property could be used just as well.
It is usually worthwhile explicitly defining the Range.Parent worksheet property rather then relying implicitly on the ActiveSheet property.
¹ Strictly speaking, there is nothing wrong with offsetting the ActiveCell as you did but the result is completely relative to the current selection on the worksheet and is generally not considered 'best practise'. With D5 selected on the worksheet, activecell(1,1) references D5 and activecell(2,3) references F6. In a very special circumstance, this behavior may be desirable but generally speaking it is better to use the Range.Cells property and use the row_number and column_number to reference the cell from the Worksheet Object perspective, not as an offset position from the ActiveCell.
First as it was pointed out in your last question; Activecell refers to just that the activecell it is one cell and not a range. You want Cells().
Next by redeclaring the variable inside the loop it allows Excel to determine its type each time. If not it will be the type that excel assigns the first time and you cannot store a text value in an Integer type.
Then we test that variable if it is numeric or not.
But you could skip that and do what #Jeeped suggests.
Dim a As Integer
For a = 6 To 10
Dim DilutedRev
DilutedRev = Cells(a, 11).Value
If Not isnumeric(DilutedRev) Then
Cells(a, 11) = 0
Else
Cells(a, 11) = DilutedRev
End If

Resources