In VBA if I have a string of numbers lets say ("1,2,3,4,5,2,2"), how can I remove the duplicate values and only leave the first instance so the string says ("1,2,3,4,5").
Here is a function you can use to dedupe a string as you've described. Note that this won't sort the deduped string, so if yours was something like "4,2,5,1,3,2,2" the result would be "4,2,5,1,3". You didn't specify you needed it sorted, so I didn't include that functionality. Note that the function uses , as the default delimiter if not specified, but you can specify a delimiter if you choose.
Function DeDupeString(ByVal sInput As String, Optional ByVal sDelimiter As String = ",") As String
Dim varSection As Variant
Dim sTemp As String
For Each varSection In Split(sInput, sDelimiter)
If InStr(1, sDelimiter & sTemp & sDelimiter, sDelimiter & varSection & sDelimiter, vbTextCompare) = 0 Then
sTemp = sTemp & sDelimiter & varSection
End If
Next varSection
DeDupeString = Mid(sTemp, Len(sDelimiter) + 1)
End Function
Here's some examples of how you would call it:
Sub tgr()
MsgBox DeDupeString("1,2,3,4,5,2,2") '--> "1,2,3,4,5"
Dim myString As String
myString = DeDupeString("4-2-5-1-3-2-2", "-")
MsgBox myString '--> "4-2-5-1-3"
End Sub
I suggest writing a Join function to combine the unique parts back into a single string (there is one available for arrays, but not for any other collection):
Function Join(Iterable As Variant, Optional Delimiter As String = ",") As String
Dim notFirst As Boolean
Dim item As Variant
For Each item In Iterable
If notFirst Then
Join = Join & Delimiter
Else
notFirst = True
End If
Join = Join & item
Next
End Function
Then, use Split to split a string into an array, and Scripting.Dictionary to enforce uniqueness:
Function RemoveDuplicates(s As String, Optional delimiter As String = ",") As String
Dim parts As String()
parts = Split(s,delimiter)
Dim dict As New Scripting.Dictionary
Dim part As Variant
For Each part In parts
dict(part) = 1 'doesn't matter which value we're putting in here
Next
RemoveDuplicates = Join(dict.Keys, delimiter)
End Function
try this:
Sub test()
Dim S$: S = "1,2,3,4,5,2,2,5,6,6,6"
Dim Dic As Object: Set Dic = CreateObject("Scripting.Dictionary")
Dim Key As Variant
For Each Key In Split(S, ",")
If Not Dic.exists(Trim(Key)) Then Dic.Add Trim(Key), Nothing
Next Key
S = Join(Dic.Keys, ","): MsgBox S
End Sub
Heres my crack at it:
Function Dedupe(MyString As String, MyDelimiter As String)
Dim MyArr As Variant, MyNewArr() As String, X As Long, Y As Long
MyArr = Split(MyString, MyDelimiter)
ReDim MyNewArr(0)
MyNewArr(0) = MyArr(0)
Y = 0
For X = 1 To UBound(MyArr)
If InStr(1, Join(MyNewArr, MyDelimiter), MyDelimiter & MyArr(X)) = 0 Then
Y = Y + 1
ReDim Preserve MyNewArr(Y)
MyNewArr(Y) = MyArr(X)
End If
Next
Dedupe = Join(MyNewArr, MyDelimiter)
End Function
Call it like this in code:
Dedupe(Range("A1").Text,",")
Or like this in the sheet:
=Dedupe(A1,",")
The first parameter is the cell to test and the second is the delimiter you want to use (in your example it is the comma)
vb6,Find Duplicate letter in word when there is no delimiter.
Function RemoveDuplicateLetter(ByVal MyString As String) As String
Dim MyArr As Variant, MyNewArr() As String, X As String,str as String
Dim bValue As Boolean
Dim i As Long, j As Long
For i = 0 To Len(MyString)
str = str & Mid$(MyString, i + 1, 1) & vbNullChar
Next
i = 0
MyArr = Split(str, vbNullChar)
ReDim MyNewArr(0)
MyNewArr(0) = MyArr(0)
For i = LBound(MyArr) To UBound(MyArr)
bValue = True
For j = i + 1 To UBound(MyArr)
If MyArr(i) = MyArr(j) Then
bValue = False
Exit For
End If
Next
If bValue Then X = X & " " & MyArr(i)
Next
RemoveDuplicateLetter = X
End Function
Related
I'm trying to add some code in the following Function in order to keep the hyperlinks from the data i concatenated together, but can't figure out how to do it.
Function ConcatenateRange(ByVal cell_range As Range, _
Optional ByVal seperator As String) As String
Dim cell As Range
Dim newString As String
Dim cellArray As Variant
Dim i As Long, j As Long
cellArray = cell_range.Value
For i = 1 To UBound(cellArray, 1)
For j = 1 To UBound(cellArray, 2)
If Len(cellArray(i, j)) <> 0 Then
newString = newString & (seperator & cellArray(i, j)) & vbLf
End If
Next
Next
If Len(newString) <> 0 Then
newString = Right$(newString, (Len(newString) - Len(seperator)))
End If
ConcatenateRange = newString
End Function
Any tips is much appreaciated.
Best
Benjamin
I have a column containing formulas as "strings", i.e. "=+I11+I192+I245+I280"
I need to replace the cells (I11, I192,I245andI280`) ID with the content (strings) contained in the cells themselves.
Example:
Cell X --> "=+I11+I192+I245+I280"
Cell I11 = 'A'
Cell I192 = 'B'
Cell I245 = 'C'
Cell I280 = 'D'
The formula should generate "=+A+B+C+D".
This?
="=+" & I11 &"+" & I192 &"+" & I245 & "+" & I280
Well, how about :
=I11 & I192 & I245 & I280
Or you can include spaces
=I11 & " " & I192
But straight quotes - my phone is being funny...
The formula should generate --> "=+A+B+C+D"
Try,
="=+"&textjoin("+", true, I11, I192, I245, I280)
Don't know what you will be doing with empty cells so here is draft
Public Sub test()
[I11] = "A": [I192] = "B": [I245] = "C": [I280] = "D"
Debug.Print ConvertedString("=+I11+I192+I245+I280")
End Sub
Public Function ConvertedString(ByVal inputString As String) As Variant
Dim arr() As String, i As Long
On Error GoTo errHand
If Not InStr(inputString, Chr$(43)) > 0 Then
ConvertedString = CVErr(xlErrNA)
Exit Function
End If
arr = Split(inputString, Chr$(43))
For i = 1 To UBound(arr)
arr(i) = Range(arr(i))
Next i
ConvertedString = Join(arr, Chr$(43))
Exit Function
errHand:
ConvertedString = CVErr(xlErrNA)
End Function
I think you mean something like
=INDIRECT(I11,TRUE)+INDIRECT(I192,TRUE)+INDIRECT(I245,TRUE)+INDIRECT(I280,TRUE)
but please note that Indirect is a volatile function, and can slow your calculations down if used extensively.
Using VBA (with only single delimiter):
Function ReplaceAddr(sInput As String, Optional sDelimiter As String = "+") As String
Dim sArr
Dim i As Long
sArr = Split(sInput, sDelimiter)
For i = 1 To UBound(sArr)
sArr = Range(sArr(i))
Next i
ReplaceAddr = Join(sArr, sDelimiter)
End Function
From OP's comment:
The problem is that formulas changes, so I can't only change manually. The one I gave you is only an example, but I have so many different ones with all math operators.
You can try finding cell addresses with regular expression and replace with cell's value:
Function ReplaceAddr2(sInput As String) As String
Dim oRegEx As Object
Dim oMatches As Object
Dim i As Long, lStart As Long, lLength As Long
Set oRegEx = CreateObject("vbscript.regexp")
oRegEx.Pattern = "[A-Za-z]{1,3}\d{1,7}"
oRegEx.Global = True
oRegEx.MultiLine = True
Set oMatches = oRegEx.Execute(sInput)
lStart = 0
For i = 0 To oMatches.Count - 1
lLength = oMatches(i).FirstIndex - lStart
ReplaceAddr2 = ReplaceAddr2 & Mid$(sInput, lStart + 1, lLength) & Range(oMatches(i).Value)
lStart = lStart + lLength + oMatches(i).length
Next
ReplaceAddr2 = ReplaceAddr2 & Mid(sInput, lStart + 1, Len(sInput) - lStart)
End Function
Pattern is 1-3 letters followed by 1-7 digits.
Both functions are not volatile - will be recalculated only when input string changes, but not when cells addressed there change. Adding this line:
Application.Volatile True
will make it recalculate on every change, but it may affect performance.
I want apply a user defined aggregation function (string-value from a certain cell, like "SUM" or "STDEV") on an array of values. Here's a simplified example. However I don't know how to make the aggregation (last line):
Sub test()
Dim values() As Double
ReDim values(1 To 3)
values(1) = 3.5
values(2) = 5
values(3) = 4.8
Dim aggregate_fn As String
aggregate_fn = "SUM"
Dim result As Double
result = Evaluate("=" & aggregate_fn & "(" & values & ")") ' <-- This doesn't work, but hopefully it's clear what it should do
End Sub
EDIT
My original code is also creating the values array dynamically from a spreadsheet which uses , as decimal sign. This causes an issue with Scott's answer below.
Const datasht = "Daten"
Const aggregate_cell = "G1"
Sub run()
Dim sht As Worksheet
Dim n_rows As Integer
Dim rw As Integer
Application.DecimalSeparator = "."
Set sht = ActiveWorkbook.Worksheets(datasht)
n_rows = sht.Cells(1, 1).CurrentRegion.Rows.Count ' Get range of data
Dim values() As String
ReDim values(1 To n_rows)
For rw = 1 To n_rows
values(rw) = sht.Cells(rw, 1).Value
Next rw
Debug.Print (aggregate(values))
End Sub
Function aggregate(values() As String)
' Get aggregated value
Dim aggregate_fn As String
aggregate_fn = ActiveWorkbook.Worksheets(datasht).Range(aggregate_cell).Value
aggregate = Evaluate("=" & aggregate_fn & "(" & Join(values, ",") & ")") ' <-- doesn't work as intended
End Function
This should work for any function available via Application.WorksheetFunction, and is less likely to give an error if you pass too many values.
Sub Tester()
Dim arr(0 To 1000), res, x As Long, f as String
'get some values to work with...
For x = 0 To 1000
arr(x) = Rnd() * 10
Next x
f = "SUM"
res = CallByName(Application.WorksheetFunction, f, VbMethod, arr)
Debug.Print res
End Sub
I was able to get this to work by creating a string from your array, then using that instead:
Sub test()
Dim values() As Double
ReDim values(1 To 3)
values(1) = 3.5
values(2) = 5
values(3) = 4.8
Dim mystring As String, i As Long
mystring = values(LBound(values))
For i = LBound(values) + 1 To UBound(values)
mystring = mystring & "," & values(i)
Next i
Dim aggregate_fn As String
aggregate_fn = "SUM"
Dim result As Double
result = Evaluate("=" & aggregate_fn & "(" & mystring & ")")
Debug.Print result
End Sub
Join works with Strings change the values to String instead of Double:
Sub test()
Dim values() As String
ReDim values(1 To 3)
values(1) = 3.5
values(2) = 5
values(3) = 4.8
Dim aggregate_fn As String
aggregate_fn = "SUM"
Dim result As Double
result = Evaluate("=" & aggregate_fn & "(" & Join(values, ",") & ")")
Debug.Print result
End Sub
As per your edit there is no need for the array at all, just pass the range and worksheet to the function and use .Address on the range.
Const datasht = "Daten"
Const aggregate_cell = "G1"
Sub run()
Dim sht As Worksheet
Dim n_rows As Integer
Dim rw As Integer
'Application.DecimalSeparator = "."
Set sht = ActiveWorkbook.Worksheets(datasht)
n_rows = sht.Cells(1, 1).CurrentRegion.Rows.Count ' Get range of data
Dim rng As Range
Set rng = sht.Range(sht.Cells(1, 1), sht.Cells(n_rows, 1))
Debug.Print (aggregate(sht, rng))
End Sub
Function aggregate(sht As Worksheet, rng As Range)
' Get aggregated value
Dim aggregate_fn As String
aggregate_fn = sht.Range(aggregate_cell).Value
aggregate = sht.Evaluate("=" & aggregate_fn & "(" & rng.Address(1, 1) & ")")
End Function
I need defines each Cells are in a range row in sequence If confront a Hidden column.
In pointed area of code below, when reached a hidden column in the specific row, jump to next row and at last refer to continue of this row and assign to RngCell variable.
I want when For Each loop reached a hided column, continue the cells assigning to next visible cell of appropriated row.
Sub CsvExportRange(rngRange As Range, strFileName As Variant, strCharset As String)
Dim rngRow As Range
Dim objStream As Object
Set objStream = CreateObject("ADODB.Stream")
objStream.Type = 2
objStream.Charset = strCharset
objStream.Open
OffColumns (True)
Call Tax_WP
For Each rngRow In rngRange.Rows 'The problem becuase Here
objStream.WriteText CsvFormatRow(rngRow)
Next rngRow
objStream.SaveToFile strFileName, 2
objStream.Close
End Sub
Function CsvFormatRow(rngRow As Range) As String
Dim arrCsvRow() As String
Dim strRowEnd As String
strRowEnd = vbCrLf
ReDim arrCsvRow(rngRow.Cells.Count - 1)
Dim rngCell As Range
Dim lngIndex As Long
lngIndex = 0
For Each rngCell In rngRow.Cells '***Problem is Here***
arrCsvRow(lngIndex) = CsvFormatString(rngCell.Value)
lngIndex = lngIndex + 1
Next rngCell
CsvFormatRow = Join(arrCsvRow, ",") & vbCrLf
End Function
Function CsvFormatString(strRaw As String) As String
Dim boolNeedsDelimiting As Boolean
Dim strDelimiter, strSeparator, strDelimiterEscaped As String
strDelimiter = """"
strSeparator = strSeparator = ","
strDelimiterEscaped = strDelimiter & strDelimiter
boolNeedsDelimiting = InStr(1, strRaw, strDelimiter) > 0 _
Or InStr(1, strRaw, chr(10)) > 0 _
Or InStr(1, strRaw, strSeparator) > 0
CsvFormatString = strRaw
If boolNeedsDelimiting Then
CsvFormatString = strDelimiter & _
Replace(strRaw, strDelimiter, strDelimiterEscaped) & _
strDelimiter
End If
End Function
I'm having a hard time understanding what you want to happen when you encounter a hidden column. In your post you say "jump to next row", but then in bold, you say, "continue the cells assigning to next visible cell of appropriated row" which is unclear but might mean you wish to continue on the row, but skip the hidden column?
You also use an array combined with rngRow.Cells.Count - 1 which is always going to get you 16,383 to define your array. It might be easier if you dynamically updated your array while you looped through your rows, or simply string together your text to a single variable?
Are you ultimately trying to avoid a bunch of ",,"? If so, this method may work better.
In summary, the below code will:
End your CSVFormatRow function when rngCell is in a hidden column.
End your CSVFormatRow function when rngCell is in a column that is outside CSV file's used range of ANY row (not just the row it's looping through).
Instead of using an array, the code just string together members to variable StrinCVSRow
Exclude the final , in StrinCVSRow
If that doesn't address your question, please clarify the requirements. Hope this helps.
Function CsvFormatRow(rngRow As Range) As String
'Dim arrCsvRow() As String
Dim StrinCVSRow As String 'new variable to string row text together.
Dim strRowEnd As String 'these two lines aren't doing anything
strRowEnd = vbCrLf 'these two lines aren't doing anything
'ReDim arrCsvRow(rngRow.Cells.Count - 1) ' always equals 16383
Dim rngCell As Range
'Dim lngIndex As Long ' (not needed if Array omitted)
'lngIndex = 0
For Each rngCell In rngRow.Cells '***Problem is Here***
If rngCell.EntireColumn.Hidden = True Then
'The "Exit For" will end the code here and jump to next row.
'If you wanted to continue through the row you can leave
'this condition in, but delete the "Exit For"
'and this will simply "skip" this particular column.
Exit For
ElseIf Intersect(rngCell, Sheets(rngCell.Sheet.Name).UsedRange) Is Nothing Then
'ends loop if last column with any data in entire CSV file is reached.
Exit For
Else
StrinCVSRow = CsvFormatString(rngCell.Value) & ","
' lngIndex = lngIndex + 1
End If
Next rngCell
'CsvFormatRow = Join(arrCsvRow, ",") & vbCrLf
'left function trims off final ",".
CsvFormatRow = Left(StrinCVSRow, Len(StrinCVSRow) - 1) & vbCrLf
End Function
In code bellow I reached a way that truly catch all visible cells are in a table row with integrating SpecialCells(12), Fore each and For i:
(with writing inserting comments in above positions)
Sub CsvExportRange(rngRange As Object, strFileName As String, strCharset As String)
Dim rngRow As Range
Dim objStream As Object
Dim i, lngFR As Long 'First Row
lngFR = rngRange.SpecialCells(xlCellTypeVisible).Rows(1).Row - rngRange.Rows(1).Row + 1 'giving absolute Row number of first Table's row.
Set objStream = CreateObject("ADODB.Stream")
objStream.Type = 2
objStream.Charset = strCharset
objStream.Open
For i = lngFR To lngFR + rngRange.SpecialCells(xlCellTypeVisible).Rows.Count - 1
objStream.WriteText CsvFormatRow(rngRange.Rows(i)) 'Gives all visible lines are in table entirely of sheet. (Here using Fore i... is suitable)
Next i
objStream.SaveToFile strFileName, 2
objStream.Close
End Sub
Function CsvFormatRow(rngRow As Variant) As String
Dim arrCsvRow() As String
Dim strRowEnd As String
strRowEnd = vbCrLf
ReDim arrCsvRow(rngRow.SpecialCells(xlCellTypeVisible).Cells.Count - 1) 'Defining array dimension for saving each cell in a array room and at last using Join() command
Dim rngCell As Range
Dim lngIndex As Long
lngIndex = 0
For Each rngCell In rngRow.SpecialCells(xlCellTypeVisible).Cells 'Here we used For Each to give only visible cells of above entire line so thats line is there in Table range.
arrCsvRow(lngIndex) = CsvFormatString(rngCell.Value)
lngIndex = lngIndex + 1
Next rngCell
CsvFormatRow = Join(arrCsvRow, ",") & vbCrLf 'At last, here generating destination CSV file data file.
End Function
Function CsvFormatString(strRaw As String) As String
Dim boolNeedsDelimiting As Boolean
Dim strDelimiter, strSeparator, strDelimiterEscaped As String
strDelimiter = """"
strSeparator = strSeparator = ","
strDelimiterEscaped = strDelimiter & strDelimiter
boolNeedsDelimiting = InStr(1, strRaw, strDelimiter) > 0 _
Or InStr(1, strRaw, chr(10)) > 0 _
Or InStr(1, strRaw, strSeparator) > 0
CsvFormatString = strRaw
If boolNeedsDelimiting Then
CsvFormatString = strDelimiter & _
Replace(strRaw, strDelimiter, strDelimiterEscaped) & _
strDelimiter
End If
End Function
I want to compare if two cells in Excel contain the same characters, which could be in any order, using Excel based formulae.
For example, if:
A1= Japan;US
A2= US;Japan
--EDIT--
Another example,
C1= abcefg
C2= efgabc
In both these cases, when I check whether A1=A2 or C1=C2, it should give me TRUE since all the characters in both the cells are exactly the same, albeit in different order.
Ps - These cells could have any character length.
The idea is the following: we take the two strings (in Sub Test) and we compare them in the CompareMe function. The idea of the comparison is to cast them to arrays (StrToArray), then sort the array (BubbleSort). At the end, the result of (Join(varStr1, "") = Join(varStr2, "")) is the answer of the question.
Option Explicit
Public Sub Test()
Debug.Print CompareMe("Japan;US", "US;Japan")
Debug.Print CompareMe("abcefg", "efgabc")
Debug.Print CompareMe("abcefg", "e1fgabc")
Debug.Print CompareMe("vit", "vitt")
End Sub
Public Function CompareMe(str1 As String, str2 As String) As Boolean
Dim varStr1 As Variant
Dim varStr2 As Variant
ReDim varStr1(Len(str1))
ReDim varStr2(Len(str2))
varStr1 = StrToArray(str1)
varStr2 = StrToArray(str2)
Call BubbleSort(varStr1)
Call BubbleSort(varStr2)
CompareMe = (Join(varStr1, "") = Join(varStr2, ""))
End Function
Public Sub BubbleSort(ByRef list As Variant)
Dim First As Long
Dim Last As Long
Dim i As Long
Dim j As Long
Dim Temp As String
First = LBound(list)
Last = UBound(list)
For i = First To Last - 1
For j = i + 1 To Last
If list(i) > list(j) Then
Temp = list(j)
list(j) = list(i)
list(i) = Temp
End If
Next j
Next i
End Sub
Public Function StrToArray(str As String) As Variant
Dim buff() As String
Dim i As Long
ReDim buff(Len(str) - 1)
For i = 1 To Len(str)
buff(i - 1) = Mid$(str, i, 1)
Next
StrToArray = buff
End Function
I don't think there is a single function that will answer your problem.
I made a little script that will compare two cells, to test it, put your values in Range("A1") and Range("A2"). Being that you are only comparing two values, looping like this will have a very minimal performance hit.
Sub d()
Dim ValueOne As String
Dim ValueTwo As String
Dim charVal
Dim sString
Dim char
Dim boolVal As Boolean
ValueOne = Cells(1, 1).Value
ValueTwo = Cells(2, 1).Value
boolVal = True
If Len(ValueTwo) = Len(ValueOne) Then
charVal = StrConv(ValueOne, vbUnicode)
charVal = Left(charVal, Len(charVal) - 1)
sString = Split(charVal, Chr(0))
For Each char In sString
If Len(ValueTwo) = Len(Replace(ValueTwo, char, "")) Or Len(Replace(ValueTwo, char, "")) <> Len(Replace(ValueOne, char, "")) Then
boolVal = False
GoTo nxt
End If
Next
charVal = StrConv(ValueTwo, vbUnicode)
charVal = Left(charVal, Len(charVal) - 1)
sString = Split(charVal, Chr(0))
For Each char In sString
If Len(ValueOne) = Len(Replace(ValueOne, char, "")) Or Len(Replace(ValueTwo, char, "")) <> Len(Replace(ValueOne, char, "")) Then
boolVal = False
GoTo nxt
End If
Next
Else
boolVal = False
End If
nxt:
If boolVal = False Then
MsgBox "Strings are different"
Else
MsgBox "Strings are the same"
End If
End Sub
Also, if you wanted this as a function, you could very easily change to one just by doing this,
Function Compare(ValueOne As String, ValueTwo As String)
Dim charVal
Dim sString
Dim char
Dim boolVal As Boolean
' Insert same code as above here
'End Insert same code as above here
If boolVal = False Then
Compare = True
Else
Compare = True
End If
End Function