Related
I want to divide numbers into unique sorted digits.
For example, the number can be 127425 and I would like 12457 as the result, meaning sorted and duplicate removed. I think the best is to explain with example:
+---------+--------+
| Number | Result |
+---------+--------+
| 127425 | 12457 |
+---------+--------+
| 2784425 | 24578 |
+---------+--------+
| 121 | 12 |
+---------+--------+
| 22222 | 2 |
+---------+--------+
| 9271 | 1279 |
+---------+--------+
The longest result can be only 123456789.
I don't think we need an array for that (no delimiter), but the use of substring could probably do the job. I just don't know where to begin, hence no code.
Any ideas are welcome. Thanks.
Another VBA routine to sort the unique elements of a cell, using an ArrayList:
Option Explicit
Function sortUniqueCellContents(S As String) As String
Dim arr As Object, I As Long, ch As String * 1
Set arr = CreateObject("System.Collections.ArrayList")
For I = 1 To Len(S)
ch = Mid(S, I)
If Not arr.contains(ch) Then arr.Add ch
Next I
arr.Sort
sortUniqueCellContents = Join(arr.toarray, "")
End Function
If you have a version of Excel that supports Dynaaic Arrays, then try this (for input in A2)
=CONCAT(SORT(UNIQUE(MID(A2,SEQUENCE(LEN(A2),1,1,1),1))))
How it works
SEQUENCE(LEN(A27),1,1,1) returns an array of numbers 1 .. the length of the input string
MID(A2, ... ,1)) uses that array to return a Spill range of the individual characters in the input string
UNIQUE( reduces that to a range of unique characters only
SORT sorts that range
CONCAT concatenates that range into a single string
Gearing off that to build a VBA function
Function UniqueDigits(s As String) As String
With Application.WorksheetFunction
UniqueDigits = Join(.Sort(.Unique(Split(Left$(StrConv(s, 64), Len(s) * 2 - 1), Chr(0)), 1), , , 1), "")
End With
End Function
Alternative to the newer dynamic array functions
Loving the above nice solutions it's always a challenge to think over additional approaches (via Byte array, Filter() and FilterXML() function):
Function UniqueDigits(ByVal txt) As String
Dim by() As Byte: by = txt
Dim digits: digits = Array(49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 0) ' equalling Asc values
'a) create 1-based 1-dim array with digit positions
Dim tmp: tmp = Filter(Application.Match(by, digits, 0), 11, False)
'b) get uniques
tmp = Uniques(tmp)
'c) sort it (don't execute before getting uniques)
BubbleSort tmp
'd) return function result
UniqueDigits = Join(tmp, "")
End Function
Function Uniques(arr)
'Note: using FilterXML() available since vers. 2013+
Dim content As String ' replacing "10" referring to zero indexed as 10th digit
content = Replace("<t><s>" & Join(arr, "</s><s>") & "</s></t>", "10", "0")
arr = WorksheetFunction.FilterXML(content, "//s[not(preceding::*=.)]")
Uniques = Application.Transpose(arr)
End Function
Sub BubbleSort(arr)
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
Further hints :-) tl;dr
...explaining
a) how to transform a string to a digits array,
b) how to get uniques via FilterXML instead of a dictionary
c) (executing BubbleSort needs no further explanation).
ad a) the tricky way to get a pure digits array
Transforming a string of digits into an array of single characters may need some explanation.
A string (here txt) can assigned easily to a byte array via Dim by() As Byte: by = txt. (Note that classical characters would be represented in a byte array by a pair of Asc values, where the second value mostly
is 0; so digit 1 is represented by 49 and 0, 2 by 50 and 0 up to 9 by 57 and 0).
Digits are defined in a 1-based Asc value array from 1~>49 to 9~>57, followed by the 10th item 0~>48 and
eventually the Asc value 0 as 11th item related to each second byte pair.
Dim digits: digits = Array(49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 0) ' equalling Asc values
Usually the Match() function searches for a specified item in order to get its relative position within an array (here digits) and would be executed by the following syntax: ``.
MATCH(lookup_value, lookup_array, [match_type])
where the lookup_value argument can be a value (number, text, or logical value) or a cell reference to a number, text, or logical value.
An undocumented feature is that instead searching e.g. 2 in the lookup_array digits via
Application.Match(2, digits,0) you can use the byte array as first argument serving as 1-based array pattern where VBA replaces the current Asc values by their position found within the digits array.
Application.Match(by, digits, 0)
Finally a negative filtering removes the companion Asc 0 values (11 plus argument False) via
Dim tmp: tmp = Filter(Application.Match(by, digits, 0), 11, False)
ad b) get uniques via FilterXML
Help reference for the WorksheetFunction.FilterXML method demands two string parameters
FilterXML(XMLContentString, XPathQueryString)
The first argument doesn't reference a file, but needs a valid ("wellformed") XML text string starting with a root node (DocumentElement) which is roughly comparable to a html structure starting with the enclosing pair of <html>...</html> tags.
So a wellformed content string representing e.g. number 121 could be:
<t>
<s>1</s>
<s>2</s>
<s>1</s>
</t>
The second argument (limited to 1024 characters) must be a valid XPath query string like the following find non-duplicates
"//s[not(preceding::*=.)]"
where the double slash // allows to find s nodes at any hierarchy level and under the condition that it is not
preceded by any nodes * with the same value content =.
Recommended readings
#RonRosenfeld is a pioneer author of numerous answers covering the FilterXML method, such as e.g. Split string cell....
#JvDV wrote a nearly encyclopaedic overview at Extract substrings from string using FilterXML.
Try the next function, please:
Function RemoveDuplSort(x As String) As String
Dim i As Long, j As Long, arr As Variant, temp As String
'Dim dict As New Scripting.Dictionary 'in case of reference to 'Microsoft Scripting Runtime,
'un-comment this line and comment the next one:
Dim dict As Object: Set dict = CreateObject("Scripting.Dictionary")
For i = 1 To Len(x)
dict(Mid(x, i, 1)) = 1
Next i
arr = dict.Keys
For i = LBound(arr) To UBound(arr) - 1
For j = i + 1 To UBound(arr)
If arr(i) > arr(j) Then
temp = arr(i)
arr(i) = arr(j)
arr(j) = temp
End If
Next j
Next i
RemoveDuplSort = Join(arr, "")
End Function
It can be called in this way:
Sub testRemoveDuplSort()
Dim x As String
x = "2784425" 'x = myLabel.Caption
Debug.Print RemoveDuplSort(x)
End Sub
If your number is in cell A3, then this one will return a string of unique numbers.
=CONCAT(SORT(UNIQUE(MID(A3,ROW(INDIRECT("1:"&LEN(A3))),1))))
Hi everyone I hope you're doing fine.
This is my 1st request here in the forum, as I didn't have the chance to find what I'm looking for, I've been searching for this for months but didn't get lucky, and I couldn't figure out how to code it since I'm a beginner.
So basically I have these multiple Excel files that contain columns (long,latitude,altitude, containers,spill, and volume of non contained garbage)
The problem is in the container range, it contains the number of container(num) and the type of container(char) and the volume of the container ( num) separated by "/" character and if we have more than a container in one place we put + sign then number of the second container, type, volume, separated also by "/"
What I'm really looking to do is to sum the number of container and multiply it by the sum of volumes for each cell and then add it to the spill if there is any and then output it in a total column
If there is one type of container then there is no need to do a sum , only multiplication and then add the spill value then output it to the total column
And I'm counting to use "/" sign and + sign as a wildcards to extract the numeric values if this possible
For the volume column there is no math required, only copy paste to the total column.
I apologize for the bad English, you can leave a comment in case it's hard to understand, and I'm looking forward to your help.
Example:
Another example:
You could use a User Defined Function with the code located in a module.
Option Explicit
Function CalcVolume(s As String) As Variant
Dim ar1 As Variant, ar2 As Variant
Dim i As Integer, num As Double, vol As Double, bError As Boolean
bError = False
' split into array
ar1 = Split(s, "+")
For i = LBound(ar1) To UBound(ar1)
ar2 = Split(ar1(i), "/")
If UBound(ar2) = 3 And IsNumeric(ar2(0)) And IsNumeric(ar2(3)) Then
num = num + ar2(0)
vol = vol + ar2(3)
Else
bError = True
End If
Next
If bError = False Then
CalcVolume = vol * num
Else
CalcVolume = CVErr(xlErrNA)
End If
End Function
Sub test()
Debug.Print CalcVolume("2/FUT/p/50+1/FUT/m/100")
End Sub
I have a column with formula as follows:
=(2+3*6+8) & "KB"
Basically, each cell is a formula and string concatenated (using &). I want to add all these cells up. I tried the following things:
a) =SUM(B2:B21) gives me a sum of 0.
b) Using =B2+B3... gives me a #VALUE error.
c) I tried something like this also - didn't work, gives a sum of 0: =SUM(IF(ISNUMBER(FIND("KB",$C$2:$C$14)),VALUE(LEFT($C$2:$C$14,FIND("KB",$C$2:$C$14)-1)),0))
Make your own SUM function in VBA. Try this:
=StripTextAndSum(A2:A4) - returns 60
=StripTextAndAverage(A2:A4) - returns 20
This method keeps the left most decimal number and tosses away the rest.
NOTE: You can tweak this to fit your needs. One way would be to retain the text so you can return it in the sum....like 150MB (i am assuming KB means kilobyte). Let me know if you like that idea and I'll make it.
EDIT: As pointed out by #DirkReichel, this has been made a little more efficient using IsNumeric instead, but I have retained all the other functions too. IsLetter is a useful function too.
Public Function StripTextAndSum(myRange As Range)
Dim r As Range
Dim n As Double
n = 0
For Each r In myRange.Cells
n = n + ParseNumberFromString(r.Text)
Next r
StripTextAndSum = n
End Function
Public Function StripTextAndAverage(myRange As Range)
Dim n As Double
n = StripTextAndSum(myRange)
StripTextAndAverage = n / (myRange.Cells.Count * 1#)
End Function
Public Function ParseNumberFromString(s As String) As Double
ParseNumberFromString = Left(s, GetLastNumberIndex(s))
End Function
Public Function GetFirstLetterIndex(s As String) As Integer
Dim i As Integer
For i = 1 To Len(s) Step 1
If IsLetter(Mid(s, i, 1)) = True Then
GetFirstLetterIndex = i
Exit Function
End If
Next i
End Function
Public Function GetLastNumberIndex(s As String) As Integer
Dim i As Integer
For i = Len(s) To 1 Step -1
If IsNumeric(Left(s, i)) = True Then
GetLastNumberIndex = i
Exit Function
End If
Next i
End Function
Function IsLetter(s As String) As Boolean
Dim i As Integer
For i = 1 To Len(s)
If LCase(Mid(s, i, 1)) <> UCase(Mid(s, i, 1)) = True Then
IsLetter = True
Else
IsLetter = False
Exit For
End If
Next
End Function
I'd normally just move the KB to the following column and left-justify it.
That way, it still looks identical but the first column only has real numbers that you can manipulate mathematically to your heart's content.
Or, assuming they're all in kilobytes (which seems to be a requirement if you just want to add the numeric bits), don't put KB in the data area at all.
Instead change the heading from, for example, Used memory to Used memory (KB).
Do you really want to populate your beautiful spreadsheets with butt-ugly monstrosities like the following? :-)
=SUM(IF(ISNUMBER(FIND("KB",$C$2:$C$14)),VALUE(LEFT($C$2:$C$14,FIND("KB",$C$2:$C$14)-1)),0))
If you need to keep your column as-is, you could always use an array formula to get the sum:
=sum(value(left(b2:b21,len(b2:b21)-2)))
You will need to enter this as an array formula (press Ctrl+Shift+Enter to submit it)
Basically this is taking the leftmost chunk of a cell (all but the last two characters, which we know are 'KB'), using value() to convert it into a numeric, and sum() to add it up. Entering it as an array formula just lets us do this to each cell in the list b2:b21 in one swoop.
As #paxdiablo mentioned, though, it might be best to restructure so that you don't have to deal with your values as text in the first place. My approach would be to enter the values and add the "KB" via formatting. You can use a custom formatting with something like 0.00 "KB" so the cell only holds, say, the value 17, but it displays as "17.00 KB".
I have a list of strings in excel as such:
a>b>b>d>c>a
a>b>c>d
b>b>b>d>d>a
etc.
I want to extract the last c or last d from each string whichever comes last,
e.g
a>b>b>d>c>a = C
a>b>c>d = d
b>b>b>d>d>a = d
how would I do this using VBA (or just straight excel if it is possible)?
You could use an excel formula as follows
To help explain will start with just one letter then will show full formula at the end.
First find the number of occurences of c
= LEN(A1) - LEN(SUBSTITUTE(A1,"c","")
Use this position to replace the last c with a unique character ($ as an example)
=SUBSTITUTE(A1,"c","$",LEN(A1) - LEN(SUBSTITUTE(A1,"c","")))
Next find this unique character
= FIND("$",SUBSTITUTE(A1,"c","$",LEN(A1) - LEN(SUBSTITUTE(A1,"c",""))))
This gives the position of the last c, now you can use this in a mid function to return this last c
= MID(A1,FIND("$",SUBSTITUTE(A1,"c","$",LEN(A1) - LEN(SUBSTITUTE(A1,"c","")))),1)
Finally to account for both c and d, use a max to bring back which comes last
= MID(A1,MAX(IFERROR(FIND("$",SUBSTITUTE(A1,"c","$",LEN(A1) - LEN(SUBSTITUTE(A1,"c","")))),0),IFERROR(FIND("$",SUBSTITUTE(A1,"d","$",LEN(A1) - LEN(SUBSTITUTE(A1,"d","")))),0)),1)
Assuming c/d are just examples:
?LastEither("b>b>b>d>d>a", "c", "d")
d
Using
Function LastEither(testStr As String, find1 As String, find2 As String) As String
Dim p1 As Long: p1 = InStrRev(testStr, find1)
Dim p2 As Long: p2 = InStrRev(testStr, find2)
If (p1 > p2) Then
LastEither = find1
ElseIf (p2 > 0) Then LastEither = find2
End If
End Function
General solution:
?FindLastMatch("b>b>b>d>d>a>q>ZZ", ">", "c", "d")
d
?FindLastMatch("b>b>b>d>d>a>q>ZZ", ">", "c", "d", "q")
q
?FindLastMatch("b>b>b>d>d>a>q>ZZ>ppp", ">", "c", "d", "ZZ", "q")
ZZ
Using
Function FindLastMatch(testStr As String, delimiter As String, ParamArray findTokens() As Variant) As String
Dim tokens() As String, i As Long, j As Long
tokens = Split(testStr, delimiter)
For i = UBound(tokens) To 0 Step -1
For j = 0 To UBound(findTokens)
If tokens(i) = findTokens(j) Then
FindLastMatch = tokens(i)
Exit Function
End If
Next
Next
End Function
And here is a array formula to do the same thing. (Changed formula to avoid problem with original pointed out by Grade 'Eh' Bacon)
=MID(A1,MAX((MID(A1,ROW(INDIRECT("1:"&LEN(A1))),1)={"c","d"})*ROW(INDIRECT("1:"&LEN(A1)))),1)
An array formula is entered by holding down ctrl+shift while hitting enter. If you do it correctly, Excel will place braces {...} around the formula which you can see in the formula bar.
The formula will return a #VALUE! error if there is neither c nor d in the string.
EDIT: Having seen from some of your comments that you might want to use more than single character words, I present the following User Defined Function. It allows you to use words of any length, and also you are not limited to just two words -- you can use an arbitrary number of words.
You would enter a formula such as:
=LastOne(A8,"Charlie","Delta")
or
=LastOne(A8,$I1:$I2)
where I1 and I2 contain the words you wish to check for.
The words need to be separated by some delimiter that is neither a letter nor a digit.
A Regular Expression (regex) is constructed which consists of a pipe-separated | list of the words or phrases. The pipe | , in a regex, is the same as an OR. The \b at the beginning and end of the regex indicates a word boundary -- that is the point at which a digit or letter is adjacent to a non-digit or non-letter, or the beginning or end of the string. Hence the actual delimiter does not matter, so long as it is not a letter or digit.
All of the matches are placed in a Match Collection; and we only need to look for the last item in the match. There will be MC.Count matches and, since this count is zero based, we subtract one to get the last match.
Here is the code:
===========================================
Option Explicit
Function LastOne(sSearch As String, ParamArray WordList() As Variant) As String
Dim RE As Object, MC As Object
Dim sPat As String
Dim RNG, C
For Each RNG In WordList
If IsArray(RNG) Or IsObject(RNG) Then
For Each C In RNG
sPat = sPat & "|" & C
Next C
Else
sPat = sPat & "|" & RNG
End If
Next RNG
sPat = "\b(?:" & Mid(sPat, 2) & ")\b"
Set RE = CreateObject("vbscript.regexp")
With RE
.Global = True
.Pattern = sPat
.ignorecase = True
If .test(sSearch) = True Then
Set MC = .Execute(sSearch)
LastOne = MC(MC.Count - 1)
End If
End With
End Function
===========================================
Here is a sample screenshot:
Note that an absence of a WordList word will result in a blank cell. One could produce an error if that is preferable.
In VBA you can do this using following simple logic.
Dim str As String
str = "a>b>b>d>c>a"
Dim Cet
Cet = split(str,">")
Dim i as Integer
For i= Ubound(Cet) to Lbound(Cet)
If Cet(i) = "c" or "d" or "C" or "D" then
MsgBox Cet(i)
Exit For
End if
Next i
Assuming your string is in cell A1, and there are no uses of the tilde (~) character in it, you can use the following in a worksheet:
=IF(IFERROR(FIND("~",SUBSTITUTE(A1,"c","~",LEN(A1)-LEN(SUBSTITUTE(A1,"c","")))),0)>IFERROR(FIND("~",SUBSTITUTE(A1,"d","~",LEN(A1)-LEN(SUBSTITUTE(A1,"d","")))),0),"c","d")
EDIT:
In response to a comment, here's an explanation of how this works. I've also neatened up the formula slightly having looked back at it again. The two formulae for c and d are identical, so the explanation will apply for both. So, working outwards
LEN(A1)-LEN(SUBSTITUTE(A1,"c",""))
Here we remove all instances of c from the string. By comparing the length of this calculated string and the original string, we calculate the number of times c appears in the original string.
SUBSTITUTE(A1,"c","~",LEN(A1)-LEN(SUBSTITUTE(A1,"c","")))
Now that we know the number of times c appears in our string, we
replace the last occurrence of c with the tilde character (here we assume the tilde isn't used in the string otherwise).
FIND("~",SUBSTITUTE(A1,"c","~",LEN(A1)-LEN(SUBSTITUTE(A1,"c",""))))
We then find the position of the tilde in the string, which is equivalent to the position of the last c in the string.
IFERROR(FIND("~",SUBSTITUTE(A1,"c","~",LEN(A1)-LEN(SUBSTITUTE(A1,"c","")))),0)
Wrapping this in an IFERROR ensures that we don't have errors coming through the formula - setting the value to 0 if no c exists ensures that we still get a correct answer if our string contains c but not d (and vice versa).
We then apply the same calculation to d and compare the two to see which occurs later in our string. Note: this will give an incorrect answer if there is neither c nor d in the string.
Wondering if anyone could help me. I'm stumped. It's been ages since I used excel....
I have 9 columns with different values in each cell, different numbers of cells per column.
I need a formula/macro to spit out all combinations of the cells and yet still remain in the exact same order of the columns.
For example
Columns:
D / 003 / 23 / 3 / 3R / C / VFX
... / 005 / 48 / 3 / 12 / .. / VDF
... / 007 / ... / 1 / ... /... / HSF
And it spits out like this:
D0032333RCVFX
D0032333RCVDF
D0032333RCHSF
D0034833RCVFX
D0034833RCVDF
and so on....
and so on.....
Presumably you will want to call this function with a "serial number" - so that you can call "the Nth combination". The problem then breaks into two parts:
Part 1: figure out, for a given "serial number", which element of each column you need. If you had the same number of elements E in each column it would be simple: it's like writing N in base E. When the number of elements in each column is different, it's a little bit trickier - something like this:
Option Base 1
Option Explicit
Function combinationNo(r As Range, serialNumber As Integer)
' find the number of entries in each column in range r
' and pick the Nth combination - where serialNumber = 0
' gives the top row
' assumes not all columns are same length
' but are filled starting with the first row
Dim ePerRow()
Dim columnIndex As Integer
Dim totalElements As Integer
Dim i, col
Dim tempString As String
ReDim ePerRow(r.Columns.Count)
totalElements = 1
i = 0
For Each col In r.Columns
i = i + 1
ePerRow(i) = Application.WorksheetFunction.CountA(col)
totalElements = totalElements * ePerRow(i)
Next
If serialNumber >= totalElements Then
combinationNo = "Serial number too large"
Exit Function
End If
tempString = ""
For i = 1 To UBound(ePerRow)
totalElements = totalElements / ePerRow(i)
columnIndex = Int(serialNumber / totalElements)
tempString = tempString & r.Cells(columnIndex + 1, i).Value
serialNumber = serialNumber - columnIndex * totalElements
Next i
combinationNo = tempString
End Function
You call this function with the range where your columns are, and a serial number (starting at 0 for "top row only"). It assumes that any blank space is at the bottom of each column. Otherwise, it will return a string that is the concatenation of combinations of values in each column, just as you described.
EDIT perhaps the following picture, which shows how this is used and what it actually does, helps. Note that the first reference (to the table of columns of different length) is an absolute reference (using the $ sign, so when you copy it from one cell to another, it keeps referring to the same range) while the second parameter is relative (so it points to 0, 1, 2, 3 etc in turn).