I need to be able to copy cells from one column to another that contain specific characters. In this example they would be ^ and * the characters can be in any order in the cell.
Here is an example :
It looks like I might be able to use the InStr function in VBA to accomplish this if I am not mistaken.
Run a loop for each item in the list and check it with something like the following:
IF InStr(1,Range("A" & i), "^") <> 0 AND InStr(1, Range("A" & i), "*") <> 0 THEN
'copy cell to another place
End If
or might there be a more elegant solution?
I can't see your image form where I am, but Like is generally easier and faster than Instr(). You could try something like this:
If Range("A" & i) Like "*[*^]*[*^]*" Then
meaning you look for some text, then * or a ^, more text, then * or *, more text
For detailed syntax, look here.
Option for no loops - use Arrays and Filter
Option Explicit
Sub MatchCharacters()
Dim src As Variant, tmp As Variant
Dim Character As String, Character2 As String
Character = "*"
Character2 = "^"
' Replace with your sheetname
With Sheet1
src = Application.Transpose(Range(.Cells(2, 1), .Cells(.Cells(.Rows.Count, 1).End(xlUp).Row, 1)))
tmp = Filter(Filter(src, Character), Character2)
.Range(.Cells(2, 3), .Cells(.Cells(1, 3).End(xlDown).Row, 3)).ClearContents
If UBound(tmp) > -1 Then
With .Cells(2, 3)
Range(.Offset(0, 0), .Offset(UBound(tmp), 0)).Value2 = Application.Transpose(tmp)
End With
End If
End With
End Sub
Or use as a function with unlimited character searching
Public Function MatchCharacters(arr As Variant, ParamArray Characters() As Variant) As Variant
Dim i As Long
For i = LBound(Characters) To UBound(Characters)
arr = Filter(arr, Characters(i))
Next i
MatchCharacters = arr
End Function
Sub test()
Dim tmp As Variant
With Sheet1
tmp = Application.Transpose(Range(.Cells(2, 1), .Cells(.Cells(.Rows.Count, 1).End(xlUp).Row, 1)))
tmp = MatchCharacters(tmp, "*", "^")
If UBound(tmp) > -1 Then
With .Cells(2, 3)
Range(.Offset(0, 0), .Offset(UBound(tmp), 0)).Value2 = Application.Transpose(tmp)
End With
End If
End With
End Sub
Edit
Looking at this again and being inspired by Tom's answer about filtering, it got be thinking... the AdvancedFilter can do exactly what you're looking to do. It's designed into the spreadsheet side of Excel, but you can use it from VBA.
If you only want to work out of VBA, or if your filter won't be changing often, then this probably is not your best choice... but if you want something that's more visible and flexible from the workbook side of things, this would be a good choice.
To manually run Advanced Filter...
Example code and dynamic filter scenario...
(Notice you can use equations with it)
Sub RunCopyFilter()
Dim CriteriaCorner As Integer
CriteriaCorner = Application.WorksheetFunction.Max( _
Range("B11").End(xlUp).Row, _
Range("C11").End(xlUp).Row, _
Range("D11").End(xlUp).Row)
[A4:A10].AdvancedFilter xlFilterCopy, Range("B4:D" & CriteriaCorner), [E4:E10], True
End Sub
Named Ranges
AdvancedFitler automatically creates NamedRanges for it's criteria and output. That can be handy because you can reference the NamedRange as Extract and it will dynamically update.
Original Post
Here's some code for a "tolerant" InStr() function from a similar post I made... it isn't tailored exactly to your example, but it gets at the basic point of character-by-character analysis.
Function InStrTolerant(InputString As String, MatchString As String, Optional CaseInsensitiveChoice = False, Optional Tolerance As Integer = 0) As Integer
'Similar to InStr, but allows for a tolerance in matching
Dim ApxStr As String 'Approximate String to Construct
Dim j As Integer 'Match string index
j = 1
Dim Strikes As Integer
Dim FoundIdx As Integer
For i = 1 To Len(InputString)
'We can exit early if a match has been found
If StringsMatch(ApxStr, MatchString, CaseInsensitiveChoice) Then
InStrTolerant = FoundIdx
Exit Function
End If
If StringsMatch(Mid(InputString, i, 1), Mid(MatchString, j, 1), CaseInsensitiveChoice) Then
'This character matches, continue constructing
ApxStr = ApxStr + Mid(InputString, i, 1)
j = j + 1
FoundIdx = i
Else
'This character doesn't match
'Substitute with matching value and continue constructing
ApxStr = ApxStr + Mid(MatchString, j, 1)
j = j + 1
'Since it didn't match, take a strike
Strikes = Strikes + 1
End If
If Strikes > Tolerance Then
'Strikes exceed tolerance, reset contruction
ApxStr = ""
j = 1
Strikes = 0
i = i - Tolerance
End If
Next
If StringsMatch(ApxStr, MatchString, CaseInsensitiveChoice) Then
InStrTolerant = FoundIdx
Else
InStrTolerant = 0
End If
End Function
Also, I always feel obliged to mention Regex in these cases. Although it isn't the easiest to use, especially with VBA, it is designed exactly for powerful complex matching.
Related
Thanks for taking the time to look at this. I'm trying to figure out why this macro works with smaller lists of keywords but when we put in larger lists it doesn't work AND goes too slowly.
Program works like this:
Column A is the input: we paste keywords of interest
Column B is output 1: it populates with all the noise words or stop words that aren't of interest from A. (there will be a 2nd sheet where we can create a list of non-interest words like the, is, by, but, etc)
Column C is output 2: it populates with all the special characters from A.
This is what I have...works with smaller not with bigger sets of words. Been stumped for a while.
Option Explicit
Dim KeywordSearch As Range
Dim NoiseWords As Range
Dim cell As Range
Dim NoiseWord As Range
Dim i As Long , j As Long
Dim NWTable As ListObject
Dim NewRow As ListRow
Dim SCTable As ListObject
Sub Highlight()
Dim s As String
Dim offset As Integer
Dim word As String
Worksheets("Keyword Search").Activate
Set KeywordSearch = ActiveSheet.Range("B3", Range("B3").End(xlDown))
Set NWTable = ActiveSheet.ListObjects("Table1")
Set SCTable = ActiveSheet.ListObjects("SC")
Worksheets("Noise Words").Activate
Set NoiseWords = ActiveSheet.Range("B2", Range("B2").End(xlDown))
' clear table
On Error Resume Next
NWTable.DataBodyRange.ClearContents
Dim r As Range
Set r = NWTable.Range.Rows(1).Resize(2)
NWTable.Resize r
SCTable.DataBodyRange.ClearContents
Dim t As Range
Set t = SCTable.Range.Rows(1).Resize(2)
SCTable.Resize t
On Error GoTo 0
For Each cell In KeywordSearch
s = cell.Value
offset = 1
cell.Interior.Color = vbWhite
cell.Characters.Font.Color = vbBlack
Do
'Replace smart quotes
For j = 1 To Len(s)
cell.Characters(j, 1).Text = Replace(cell.Characters(j, 1).Text, Chr(147), """")
cell.Characters(j, 1).Text = Replace(cell.Characters(j, 1).Text, Chr(148), """")
' Find the special characters and add to SpecialCharacters list
If InStr("""!##$%&'+,.:;<=>?^`{|}~*()/", Mid(s, j, 1)) > 0 Then
cell.Characters(j, 1).Font.Color = vbRed
Set NewRow = SCTable.ListRows.Add
NewRow.Range.Cells(1, 1) = Mid(s, j, 1)
' Replace with spaces
Mid(s, j, 1) = " "
End If
Next
' Find the next space
i = InStr(offset, s, " ")
' If no spaces left then go to end
If i = 0 Then
i = Len(s) + 1
End If
' Extract the word
word = LCase(Mid(s, offset, i - offset))
' Capitalize AND OR NOT
If word = "and" Or word = "not" Or word = "or" Then
For j = 1 To Len(word)
cell.Characters(offset + j - 1, 1).Text = UCase(Mid(word, j, 1))
Next
End If
' Special case to capitalize w/
If word = "w/" And i < Len(s) Then
cell.Characters(i - 2, 2).Text = UCase(word)
End If
' Is the word in the NoiseWord list?
For Each NoiseWord In NoiseWords
If NoiseWord.Value = word Then
' Highlight word
cell.Characters(offset, i - offset).Font.Color = 5287936
' Add to NWList
Set NewRow = NWTable.ListRows.Add
NewRow.Range.Cells(1, 1) = word
Exit For
End If
Next
offset = i + 1
Loop Until i > Len(s)
Next
With NWTable.Sort
.SortFields.Clear
.SortFields.Add Key:=Range("Table1[[#All],[Noise Words]]"), SortOn:=xlSortOnValues, Order:=xlAscending, DataOption:=xlSortNormal
.Apply
End With
NWTable.Range.RemoveDuplicates Columns:=1, Header:=xlYes
SCTable.Range.RemoveDuplicates Columns:=1, Header:=xlYes
Worksheets("Keyword Search").Activate
End Sub
Working with the Characters collection is pretty slow, so you may be stuck with some level of poor performance.
However, there are likely some points where you can shave off time.
E.g:
For j = 1 To Len(s)
cell.Characters(j, 1).Text = Replace(cell.Characters(j, 1).Text, Chr(147), """")
cell.Characters(j, 1).Text = Replace(cell.Characters(j, 1).Text, Chr(148), """")
You don't need to use the characters collection at all here: since you just cleared all of the font color, there's no need to use the Characters approach vs. just replacing via .Value
EDIT: it might be worth setting a flag within to loop to track whether any character-level formatting has been applied, so you can avoid any unneccessary use of .Characters and rely instead on .Value
You can remove this from the loop:
cell.Interior.Color = vbWhite
cell.Characters.Font.Color = vbBlack
and replace with
KeywordSearch.Interior.Color = vbWhite
KeywordSearch.Font.Color = vbBlack
before the loop
This
If word = "and" Or word = "not" Or word = "or" Then
For j = 1 To Len(word)
cell.Characters(offset + j - 1, 1).Text = UCase(Mid(word, j, 1))
Next
End If
could be faster as:
If word = "and" Or word = "not" Or word = "or" Then
cell.Characters(offset, len(word)).Text = UCase(word)
End If
To speed up calculation where you modify values in spreadsheet you need first to disable screen updates and reenable once you finish processing:
Disabling updates:
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
Application.EnableEvents = False
Reenabling updates:
Application.ScreenUpdating = True
Application.Calculation = xlCalculationAutomatic
Application.EnableEvents = True
The problem is definitely with your use of the Characters collection. If you want to colour the different words found, fine, but do it after you manipulate all the string values.
Side Note: The repeated use of ActiveSheet scares me. Please set this to a variable at the start of the Subroutine and use the variable instead.
Dim Sheet as Worksheet
Set Sheet = ActiveSheet
.
Sheet.Range(...
.
Set Sheet = nothing
Look into reading/writing the data to a Variant instead of a range. (at least for the text manipulation) Here's an example of how to load a Range into a variant:
Dim vNoiseWords as Variant
vNoiseWords = Sheet.Range("B2", Sheet.Range("B2").End(xlDown)).Value2
Writing is just the opposite (but I usually have to transpose the array).
Then, you can go through the variant array and identify the text that needs to be coloured in the cell.
Minimize Any and All Interactions with the Sheet
...so limit any line that starts with ActiveSheet., Cell., Range. and only process it if it needs to be done.
Even Cell = UCase(Cell) is a huge waste of time.
Your much better off doing
Value = UCase(Cell.Value2)
If Value <> Cell.Value2 then Cell.Value2 = Value
Update
FYI, It's easy to pin point the slow parts of your code, by adding timestamps in between sections of code. Here is a simple routine that I use to keep track of time intervals and display the results in the immediate window.
Public Sub TimeStamp(Optional Prompt As String, Optional StartTimer As Boolean)
Static s_fTimer As Single, s_fIntervalTimer As Single
Dim fCurrTime As Single
fCurrTime = Timer
If StartTimer Then
s_fTimer = fCurrTime
s_fIntervalTimer = fCurrTime
End If
If Prompt <> vbNullString Then Prompt = " - " & Prompt
Debug.Print Format((fCurrTime - s_fTimer), "0.000s") & Format((fCurrTime - s_fIntervalTimer), "(0.000s)") & Prompt
s_fIntervalTimer = fCurrTime
End Sub
The first time you call it (or anytime you want to reset the total time counter), you should set the StartTimer = True like this:
TimeStamp "Start of Program", True
After that, just call the routine, with an optional prompt to keep track of the sections of code:
TimeStamp "After Smart Quote Loop"
TimeStamp "The End"
Then just look at the time intervals, find the largest ones and whittle them down if you don't think that they are reasonable. You'll find that every interaction with the UI/cells is the hold up, but processing data in the background takes very little time.
Can you help me with loop that will go through cells A3:A50 and replace entire cell with a new value.
Reference below:
[ita-IT] to IT
[jpn] to JA
[por-BR] to PTBR
[spa-ES] to ES
etc.
Thanks for tips!
Try:
Sub ReplaceValues()
Dim r As Range
Dim v() As Variant
Dim i As Integer
v = [{"ita-IT","IT";"jpn","JA";"por-BR","PTBR";"spa-ES","ES"}]
Set r = ActiveSheet.Range("A3:A50")
For i = LBound(v) To UBound(v)
r.Replace what:=v(i, 1), replacement:=v(i, 2), lookat:=xlWhole, MatchCase:=False
Next i
End Sub
Edit:
There's no problem having 50 (or more) replacement pairs, but this would be much easier to manage by storing them in a table in the workbook, rather than listing them in a VBA array:
You can replace cell contents which have text before / after your lookup value by using wildcards. So combining those changes, your code now becomes:
Sub ReplaceValues2()
Dim r As Range
Dim v() As Variant
Dim i As Integer
v = Sheet1.ListObjects("tbReplacement").DataBodyRange
Set r = ActiveSheet.Range("A3:A50")
For i = LBound(v) To UBound(v)
r.Replace What:="*" & v(i, 1) & "*", Replacement:=v(i, 2), LookAt:=xlWhole, MatchCase:=False
Next i
End Sub
Here is an approach. If you get a lot of codes to replace it could look like spaghetti, and if there is a logic to the replacement, it would be nice to build in the logic, but it does what you asked, and is, I hope, readable.
Sub ReplaceStrings()
Dim result As String
For Each myCell In Range("A3:A50")
Select Case myCell.Value
Case "[ita-IT]"
result = "IT"
Case "[jpn]"
result = "JA"
Case "[por-BR]"
result = "PTBR"
Case "[spa-ES]"
result = "ES"
Case Else
result = myCell.Value
End Select
myCell.Value = result
Next myCell
End Sub
EDIT - To go with the original spirit but to meet the requirement of matching within the string, I replaced Case with an If Elseif series and used Like and wildcards to match. Note To match square brackets (assuming that was what you meant), I had to enclose them in square brackets. I also amended to reference the ActiveSheet to be safe, drawing on the other answer (which I am not criticizing, just showing another way to think about it).
Sub ReplaceStrings()
Dim result, s As String
For Each myCell In ActiveSheet.Range("A3:A50")
s = myCell.Value
If s Like ("*[[]ita-IT[]]*") Then
result = "IT"
ElseIf s Like "*[[]jpn[]]*" Then
result = "JA"
ElseIf s Like "*[[]por-BR[]]*" Then
result = "PTBR"
ElseIf s Like "*[[]spa-ES[]]*" Then
result = "ES"
Else
result = s
End If
myCell.Value = result
Next myCell
End Sub
I would like to have an VBA to extract an alphanumeric value from a column G which is a sentence.
This sentence is generally a comment. So it includes characters and numbers.
The value always starts with AI0 and ends with 0. This can be 11 to 13 digits long. Sometimes the number is mentioned in the comment as AI038537500, also sometimes as AI038593790000.
I have researched through almost all the websites, but have not found any case like this. I know about the formulas, left, right, mid but in my case, it doesn't apply.
Any lead would be appreciable.
You may try something like this...
Place the following User Defined Function on a Standard Module and then use it on the sheet like
=GetAlphaNumericCode(A1)
UDF:
Function GetAlphaNumericCode(rng As Range)
Dim Num As Long
Dim RE As Object, Matches As Object
Set RE = CreateObject("VBScript.RegExp")
With RE
.Global = False
.Pattern = "AI\d{9,}0"
End With
If RE.Test(rng.Value) Then
Set Matches = RE.Execute(rng.Value)
GetAlphaNumericCode = Matches(0)
Else
GetAlphaNumericCode = "-"
End If
End Function
Why not give something like the following a try?
Sub findMatches()
Dim strLength As Integer
Dim i As Long
For i = 1 To Rows.Count
Dim AllWords As Variant
AllWords = Split(Cells(i, 7).Value, " ")
For Each Item In AllWords
strLength = Len(Item)
If strLength > 0 And strLength <= 13 And Item Like "A10*?#" Then
Cells(i, 8) = Item
End If
Next
Next i
End Sub
Test Cases:
I am apple and my batch number is: A10545440 so incase you needed to know
Result: A10545440
Some random comment… A20548650
Result: NO RESULT
A101234567891 is an awesome alphanumeric combo
Result: A101234567891
Another random comment… A10555
Result: A10555
Notice: The above example covers cases where the alphanumeric combo, starting with A10 is either:
In the middle of a sentence, or
Beginning of a sentence, or
At the end of a sentence
Also note: right now it is set to go through ALL the rows... so if you want to limit that, change the Rows.Count in the For statement to whatever your set limit is.
EDIT:
In the above code, I am explicitly asking it to look in column G
can you give this a try? I think it should do the job, also you should ammend the code with the column values, I tested it with the comments being in column C, while the code will be written in column D.
Option Explicit
Sub FindValue()
Dim i As Long
Dim lastrow As Long
Dim lFirstChr As Long
Dim lLastChr As Long
Dim CodeName As String
lastrow = activesheet.Range("c" & Rows.Count).End(xlUp).Row
' gets the last row with data in it
For i = 1 To lastrow
' shuffles through all cell in data
lFirstChr = InStr(1, Cells(i, 3), "A10") ' gets the coordinate of the first instance of "A10"
If lFirstChr = 0 Then GoTo NextIteration
lLastChr = InStr(lFirstChr, Cells(i, 3), " ") ' gets the coordinate of the first instansce of space after "A10"
If lLastChr = 0 Then 'if there is no space after A10 then sets lastchr to the lenght of the string
lLastChr = Len(Cells(i, 3))
End If
CodeName = Mid(Cells(i, 3).Value, lFirstChr, lLastChr - lFirstChr) ' extracts the codename from the string value
Range("d" & i).Value = CodeName
Goto NextTteration
NextIteration:
Next i
End Sub
I recently made a loop that takes the string in each cell, searches for a "_" in the string, and if there is one cuts off that bit and any character after it. Looking at the code I realized it might be too elaborate and could be shortened or simplified, but I'm not quite sure how to do so. Is there a way to make this bit of code more efficient?
Sub Name_Change()
Sheets("Sheet1").Activate
Dim tg_row As Integer
tg_row = 1
For Each nm_cl In Range("Table1[Name]")
If InStr(1, nm_cl, "_", vbTextCompare) = 0 Then
Range("Table1[Name]").Cells(tg_row, 1).Value = nm_cl.Value
Else
Range("Table1[Name]").Cells(tg_row, 1) = _
Left(nm_cl, InStr(1, nm_cl, "_", vbTextCompare) - 1)
End If
tg_row = tg_row + 1
Next nm_cl
End Sub
Thank you for your help!
A first attempt at optimizing this would be to note that you are calling InStr multiple times. You can speed things up by computing it once, and storing the result.
Along with that I would note that presumably Range("Table1[Name]") only has one column (otherwise you would be overwriting the first column with data from the other columns). So, you can replace Range("Table1[Name]").Cells(tg_row, 1) with nm_cl. In doing this, we notice the redundant statement of nm_cl.Value = nm_cl.Value can be removed. This leads to the following code:
Sub Name_Change()
Sheets("Sheet1").Activate
Dim index As Long
For Each nm_cl In Range("Table1[Name]")
index = InStr(1, nm_cl, "_", vbTextCompare)
If index <> 0 Then
nm_cl = Left(nm_cl, index - 1)
End If
Next nm_cl
End Sub
If you need more efficiency, beyond this, you can load your data into a variant by using
dim data as Variant
data = Range("Table1[Name]").Value
process all of your data within VBA, and then put it back to the worksheet using
Range("Table1[Name]").Value = data
This will increase your speed, as transfering data between Excel and VBA is slow and this means you will have 1 read and 1 write, instead of 1 read and 1 write per line, but it will require a (minor) rewrite of your algorithm as the syntax for working with an array within a variant is different from working with ranges. Note that this will not work if you go beyond the 65536 rows. I beleive that it is a legacy constraint from Excel 2003 and earlier.
You could adjust your loop to only modify the cells that contain "_".
If Not InStr(1, nm_cl, "_", vbTextCompare) = 0 Then
Range("Table1[Name]").Cells(tg_row, 1) = _
Left(nm_cl, InStr(1, nm_cl, "_", vbTextCompare) - 1)
End If
EDIT:
Here's a working example that includes #Degustaf's suggestions. Just change the name of the range to fit your worksheet.
Sub Name_Change()
Dim selectedRange As Range
Dim rangeData As Variant 'Array containing data from specified range
Dim col As Long 'Selected column from range
Dim row As Long 'Selected row from range
Dim cellValue As String 'Value of selected cell
Dim charPosition As Long 'Position of underscore
Sheets("Sheet1").Activate
Set selectedRange = Range("YOUR-NAMED-RANGE-HERE")
If selectedRange.Columns.Count > 65536 Then
MsgBox "Too many columns!", vbCritical
ElseIf selectedRange.Rows.Count > 65536 Then
MsgBox "Too many rows!", vbCritical
Else
rangeData = selectedRange.Value
If UBound(rangeData, 1) > 0 And UBound(rangeData, 2) > 0 Then
'Iterate through rows
For row = 1 To UBound(rangeData, 1)
'Iterate through columns
For col = 1 To UBound(rangeData, 2)
'Get value of cell
cellValue = CStr(rangeData(row, col))
'Get position of underscore
charPosition = InStr(1, cellValue, "_", vbTextCompare)
'Update cell data stored in array if underscore exists
If charPosition <> 0 Then
rangeData(row, col) = Left(cellValue, charPosition - 1)
End If
Next col
Next row
'Overwrite range with array data
selectedRange.Value = rangeData
End If
End If
End Sub
You could use a user defined function to return the truncated strings in cells.
The Worksheet-function could look like:
Public function truncateAt( s as String) as string
dim pos as integer
pos = instr (1, s,"_")
If pos> 0 then
truncateAt= left (s, pos)
Else
truncateAt= s
End If
End function
I've got a worksheet with a lot of tables in them and I'm just starting to use tables because they seem pretty handy. But I've never manipulated content in an Excel table before. And these tables are basically lists of columns with Firstname and Lastname. Based on the values on these columns, I want to generate a username. But I'm trying to write a generic Sub that takes arguments, such as worksheet and name of the table.
Previously I've done this when the data has not been in a table:
Cells(2, 2).Select
Do
strFirstName = ActiveCell.Value
strLastName = ActiveCell.Offset(0, 2).Value
strFirstName = Left(strFirstName, 1)
strUserName = strFirstName & strLastName
strUserName = LCase(strUserName)
ActiveCell.Offset(0, 5).Value = strUserName
ActiveCell.Offset(1, 0).Select
Loop Until IsEmpty(ActiveCell)
And now I'm trying to do the exact same thing, only with data from a Table. Any ideas? I've added a watch for "ActiveSheet" to see if I can find the tables, and they seem to be in ActiveSheet.ListObjects, but I couldn't see any .Select option there. Perhaps I don't need to select the Table in order to manipulate it's content?
When looping over a range (whether in a table or in a range) it is usually faster to copy the data to a variant array, manipulate that array, and then copy the result back to the sheet.
Sub zz()
Dim oUsers As ListObject
Dim v As Variant
Dim vUserName() As Variant
Dim i As Long
Dim colFirst As Long
Dim colLast As Long
Dim colUser As Long
Set oUsers = ActiveSheet.ListObjects(1)
colFirst = oUsers.ListColumns("FirstName").Index
colLast = oUsers.ListColumns("LastName").Index
colUser = oUsers.ListColumns("UserName").Index
v = oUsers.DataBodyRange
ReDim vUserName(1 To UBound(v, 1), 1 To 1)
For i = 1 To UBound(v, 1)
vUserName(i, 1) = LCase(Left(v(i, colFirst), 1) & v(i, colLast))
Next
oUsers.ListColumns("UserName").DataBodyRange = vUserName
End Sub
If you really want to loop over the range itself:
For i = 1 To oUsers.ListRows.Count
oUsers.ListColumns("UserName").DataBodyRange.Rows(i) = LCase(Left( _
oUsers.ListColumns("FirstName").DataBodyRange.Rows(i), 1) & _
oUsers.ListColumns("LastName").DataBodyRange.Rows(i))
Next
For this situation you could also just use a formula in the UserName column itself, with no vba required
=LOWER(LEFT([#FirstName],1)&[#LastName])
EDIT
Sorry, don't know of a Formula way to remove any of a list of characters from a string. You might have to revert to vba for this. Here's a user defined function to do it. Your formula will become
=DeleteChars([#UserName],{"$","#"})
To Delete the characters replace {"$","#"} with a array list of characters you want to remove (you can make the list as long as you need)
To replace the characters use {"$","#";"X","X"} where the list up to the ; is the old characters, after the ; the new. Just make sure the listsa are the same length.
UDF code:
Function DeleteChars(r1 As Range, ParamArray c() As Variant) As Variant
Dim i As Long
Dim s As String
s = r1
If UBound(c(0), 1) = 1 Then
For i = LBound(c(0), 2) To UBound(c(0), 2)
s = Replace(s, c(0)(1, i), "")
Next
Else
For i = LBound(c(0), 2) To UBound(c(0), 2)
s = Replace(s, c(0)(1, i), c(0)(2, i))
Next
End If
DeleteChars = s
End Function