Excel Text, Number, Variable Deliminator Split using VBA - excel

I have a very large "work request" data set that I need to clean up. The data set has some consistent elements, a series of numbers that are a set length this changes about about half way through the data set but the change is predictable. One issue with the data set is that there are multiple deliminators in places, sometimes no deliminator, sometimes text in front etc. I pulled a sample of the variables that I am dealing with and separated them manually to show the desired result.
+----+--------------------------------+------------+--------+----------------------+
| | A | B | C | D |
+----+--------------------------------+------------+--------+----------------------+
| 1 | Work Request | Cell 1 | Cell 2 | Cell 3 |
| 2 | 2097947.A | 2097947 | A | |
| 3 | 2590082.A/4900 REPLACE DXAC | 2590082 | A | 4900 Replace DXAC |
| 4 | 2679314.C | 2679314 | C | |
| 5 | 2864142B/DEMOLISH STRUCTURES | 2864142 | B | DEMOLISH STRUCTURES |
| 6 | 3173618 | 3173618 | | |
| 7 | 3251628/4800 REPLACE ASPHALT | 3251628 | | 4800 REPLACE ASPHALT |
| 8 | 4109066A | 4109066 | A | |
| 9 | 4374312D | 4374312 | D | |
| 10 | 4465402, Building 4100 | 4465402 | | Building 4100 |
| 11 | 4881715 DESIGN | 4881715 | | DESIGN |
| 12 | 4998608\ | 4998608 | | |
| 13 | ADMIN | ADMIN | | |
| 14 | PGM MGMT | PGM MGMT | | |
| 15 | FWR # 4958989 /Bldg 4000 | 4958989 | | Bldg 4000 |
| 16 | NICC FEDISR000744416/4000 UPS | R000744416 | | 4000 UPS |
| 17 | R000451086/4300 MODS TO RM5006 | R000451086 | | 4300 MODS TO RM5006 |
+----+--------------------------------+------------+--------+----------------------+
As you can see there are a few predictable variables and some that are user input errors.
Notice that in some cases the numbers have a single character behind the 7 digit work request number most of the time separated by a "." but sometimes no separation as in A8 and A9. Sometime there are deliminators, "/" or "space", or "," but this isn't consistent. I am currently working with a VBA that manages to strip the numbers for some but fails when it encounters no numbers or extra numbers. Eventual the work request numbers were changed to add the R00 this is the "new" number and over half of the data uses this in some form.
The VBA that I am using:
Option Explicit
Public Function Strip(ByVal x As String, LeaveNums As Boolean) As Variant
Dim y As String, z As String, n As Long
For n = 1 To Len(x)
y = Mid(x, n, 1)
If LeaveNums = False Then
If y Like "[A-Za-z ]" Then z = z & y 'False keeps Letters and spaces only
Else
If y Like "[0-9. ]" Then z = z & y 'True keeps Numbers and decimal points
End If
Next n
Strip = Trim(z)
End Function
=NUMBERVALUE(Strip(A1,TRUE))
=Strip(A1,FALSE)
This works in some places but not others. It also doesn't separate out C and D respectively. The most important issue is stripping out the work request number as seen in B.
Thanks for any help.

Here's a function using Regular Expressions that returns an array of the results.
Option Explicit
'Set reference to Microsoft VBScript Regular Expressions 5.5
' or use late binding
Function Splitter(S As String) As String()
Dim re As RegExp, MC As MatchCollection
Const sPat As String = "^(?:\D*?(?=R?\d)(R?\d+)[,.]?([A-Z])?\s*[/\\]?\s*(.*\S)?)|\s*(.*\S)"
Dim sTemp(2) As String
Set re = New RegExp
With re
.Global = True
.MultiLine = True
.Pattern = sPat
If .Test(S) = True Then
Set MC = .Execute(S)
With MC(0)
sTemp(0) = .SubMatches(0) & .SubMatches(3)
sTemp(1) = .SubMatches(1)
sTemp(2) = .SubMatches(2)
End With
Splitter = sTemp
End If
End With
End Function
With the data in A2:An, if you have Excel O365 with dynamic arrays, you can enter:
B2: =Splitter(A2)
and fill down. The results of the array will spill right to columns C & D.
If you do not have dynamic arrays, then:
B2: =INDEX(Splitter($A2),COLUMNS($A:A))
Fill Right to D2. Then select B2:D2 and fill down as far as necessary.

Try this code
Private Sub UserForm_Click()
Dim Sp() As String: Sp = Split(Strip("2590082.A/4900 REPLACE DXAC"), "|")
Sheet1.Range("B2", Sheet1.Cells(RowIndex:=2, ColumnIndex:=UBound(Sp) + 2)).Value = Sp
End Sub
Function Strip(s As String) As String
If s = "" Then Exit Function
Dim tmp As String
tmp = s
Dim Sp() As String: Sp = Split("0,1,2,3,4,5,6,7,8,9,.", ",")
For i = 0 To 10
tmp = Replace(tmp, Sp(i), "|")
Next
Dim words As String
Sp = Split(tmp, "|")
For i = 0 To UBound(Sp)
If Sp(i) <> "" Then words = words & Sp(i) & "|"
Next
If Right$(words, 1) = "|" Then words = Mid(words, 1, Len(words) - 1)
tmp = s
Sp = Split(words, "|")
For i = 0 To UBound(Sp)
tmp = Replace(tmp, Sp(i), "|" & Sp(i) & "|")
Next
If Right$(tmp, 1) = "|" Then tmp = Mid(tmp, 1, Len(tmp) - 1)
Strip = tmp
End Function

Here's an example using a regular expression.
Sub WorkRequests()
Dim re As Object, allMatches, m, rv, sep, c As Range
Set re = CreateObject("VBScript.RegExp")
re.Pattern = "(((R00)?\d{7})[\.]?([A-Z])?)"
re.ignorecase = True
re.MultiLine = True
re.Global = True
For Each c In Range("B5:B20").Cells 'for example
c.Offset(0, 1).Resize(1, 3).ClearContents 'clear output cells
If re.test(c.Value) Then
Set allMatches = re.Execute(c.Value)
For Each m In allMatches
c.Offset(0, 1).Value = m 'order#+letter
c.Offset(0, 2).Value = m.submatches(1) 'order #
c.Offset(0, 3).Value = m.submatches(3) 'letter
Next m
End If
Next c
End Sub
Regular expressions reference: https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/scripting-articles/ms974570(v=msdn.10)?redirectedfrom=MSDN

Related

Storing the first value in a sequence and use the stored values where the condition meets

I have this table and the VBA code has to fetch Value from Product where the Primary value is Yes. But, Value has to be the First Value in the Sequence.
+---------+---------+---------+
| Product | Results | Primary |
+---------+---------+---------+
| A | | |
| B | | Yes |
| C | | Yes |
| D | | |
| E | | Yes |
| F | | |
| G | | Yes |
| H | | Yes |
| I | | |
+---------+---------+---------+
Expecting results:
+---------+---------+---------+
| Product | Results | Primary |
+---------+---------+---------+
| A | | |
| B | A | Yes |
| C | A | Yes |
| D | | |
| E | D | Yes |
| F | | |
| G | F | Yes |
| H | F | Yes |
| I | | |
+---------+---------+---------+
I have tried this below vba code, but doesn't work as i expected.
Sub test()
Dim i As Long
Dim lr As Long
lr = ActiveSheet.UsedRange.Rows.Count
For i = 2 To lr
If Range("D" & i).Value = "Yes" Then
Range("C" & i).Value = Range("B" & i - 1).Value
End If
Next
End Sub
Excel Formula
Here is an example of how your formula can look. This first looks to see if the primary field is "Yes". If so, then it checks if the previous result also was a yes, and grabs it's the result if so. Otherwise, it grabs the first value based on your example.
=IF($C2="Yes", IF($C1="Yes", $B1, $A1),"")
Adjust this as needed!
VBA Code
I would suggest creating a way of finding your headers, that way it is easier to update down the road in case it changes or there are more fields added. Below I added an example of what I would attempt to do.
I used a helper function (Inject) to build the actual formula and make it easier to read/debug if there are issues.
Feel free to add your own custom error handling as well.
Just note this is one of many ways you could tackle this. I could even break this formula into even smaller components to abstract as much of it as possible.
Option Explicit
Private Sub AddResultsToTable()
Dim Ws As Worksheet
Set Ws = ActiveSheet
'FIND COLUMN HEADERS TO USE IN FORMULA REFERENCES
With Ws.UsedRange
On Error GoTo Catch
Dim Product As Range
Set Product = .Find("Product")
Dim Results As Range
Set Results = .Find("Results")
Dim Primary As Range
Set Primary = .Find("Primary")
End With
'CREATE FORMULA. Example: =IF($C2="Yes", IF($C1="Yes", $B1, $A1),"")
Dim CustomFormula As String
CustomFormula = Inject("=IF(${0}='Yes', IF(${1}='Yes', ${2}, ${3}),'')", _
Primary.Offset(1).Address(False, True), _
Primary.Address(False, True), _
Results.Address(False, True), _
Product.Address(False, True) _
)
'SET FIRST RANGE EQUAL TO FORMULA & AUTOFILL FORMULA DOWN
With Results.Offset(1)
.Value = CustomFormula
.AutoFill Range(.Address, Ws.Cells(Ws.Rows.Count, Product.Column).End(xlUp).Offset(, 1))
End With
Exit Sub
Catch:
'You can do your error handling here.
MsgBox Err.Description, vbCritical
End Sub
'METHOD THAT ALLOWS A STRING TO BE REPLACED WITH VARIABLES AND SPECIAL CHARACTERS
Public Function Inject(ByVal Source As String, ParamArray Args() As Variant) As String
'#AUTHOR: ROBERT TODAR
'#EXAMPLE: Inject("${0}, ${1}!", "Hello", "Robert") --> Hello, Robert!
'REPLACE SINGLE QUOTES WITH DOUBLE QUOTES
Inject = Source
Inject = Replace(Inject, "'", """")
'REPLACE ${#} WITH VALUES STORED IN THE VALUE IN THAT INDEX.
Dim Index As Integer
For Index = LBound(Args, 1) To UBound(Args, 1)
Inject = Replace(Inject, "${" & Index & "}", Args(Index), , , vbTextCompare)
Next Index
End Function
Assuming cols A B C use the below formula in B2
=If(C2="yes", if(B1="",A1,B1),"")
Copy this to all the linea below.
This will work as long as the first item is not primary.
U can even include this formula in vba and do the copy paste in vba

Retrieving name and location of specific Shapes from worksheet with VBA

This is a follow up to my previous question (Retrieving information of OLEObjects from Workbook with VBA)
Scenario: I am trying to retrieve data from a worksheet. The data might be normal strings or number or might be encased in check boxed (checked or not).
Data example:
+---------+-------+------------------+------+------------------+
| item1 | 2004 | | | |
+---------+-------+------------------+------+------------------+
| value x | rfd | checkbox for rfd | nfd | checkbox for nfd |
+---------+-------+------------------+------+------------------+
| ident | test7 | call3 | | |
+---------+-------+------------------+------+------------------+
Obs: In this example the "checkbox for rfd/nfd" is a normal checkbox (either form or activex), and depending on the item in that sheet, either might be selected.
Objective: What I am trying to do is read the worksheet in 2 steps: First read all the data that is directly called, so I use the code:
Sub Test_retrieve()
' this will get all non object values from the sheet
Dim array_test As Variant
Dim i As Long, j As Long
array_test = ThisWorkbook.Sheets(1).UsedRange
For i = 1 To ThisWorkbook.Sheets(1).Cells(Rows.Count, 1).End(xlUp).Row
For j = 1 To ThisWorkbook.Sheets(1).Cells(1, Columns.Count).End(xlToLeft).Column
ThisWorkbook.Sheets(2).Cells(i, j) = array_test(i, j)
Next j
Next i
End Sub
to get:
+---------+-------+------------------+------+------------------+
| item1 | 2004 | | | |
+---------+-------+------------------+------+------------------+
| value x | rfd | | nfd | |
+---------+-------+------------------+------+------------------+
| ident | test7 | call3 | | |
+---------+-------+------------------+------+------------------+
Next I am trying to reach all the objectives/shapes in my worksheet. I used the following code to get name, value (checked of not) and location of all activex objects:
Sub getavticeboxvalue()
' this will get the names and values (as binary) of all the activex controlbox objects in the sheet
Dim objx As Object
Dim i As Long
i = 1
For Each objx In ThisWorkbook.Sheets(1).OLEObjects
If objx.Object.Value = True Then
ThisWorkbook.Sheets(3).Cells(i, 1).Value = 1
ThisWorkbook.Sheets(3).Cells(i, 2).Value = objx.Name
ThisWorkbook.Sheets(3).Cells(i, 3).Value = objx.BottomRightCell.Address
ElseIf objx.Object.Value = False Then
ThisWorkbook.Sheets(3).Cells(i, 1).Value = 0
ThisWorkbook.Sheets(3).Cells(i, 2).Value = objx.Name
ThisWorkbook.Sheets(3).Cells(i, 3).Value = objx.BottomRightCell.Address
End If
i = i + 1
Next objx
End Sub
Which yields something like:
+-------+-----------+----------+
| value | name | location |
+-------+-----------+----------+
| 0 | checkbox1 | $C$2 |
+-------+-----------+----------+
| 1 | checkbox2 | $E$2 |
+-------+-----------+----------+
I would then proceed to feed the values (1s and 0s), to the first table, in the place where the checkboxes originally where (location).
Issue: When I try the same procedure for Form Control (instead of activex), I have less options, and although I can look for them (ThisWorkbook.Sheets(1).Shapes.Type = 8) I cannot find their name or location.
Question: Is there a way to find their name and location? Is there a more efficient way to reach the result?
Objective:
+---------+-------+------------------+
| item1 | 2004 | | | |
+---------+-------+------------------+
| value x | rfd | 0 | nfd | 1 |
+---------+-------+------------------+
| ident | test7 | call3 | | |
+---------+-------+------------------+

How to dynamically substitute a string in excel using relations?

I have a formula made of components and a table of components that translates each component to several strings.
I want to substitute each component with the expression of the sum of the respective strings in the table.
Example:
Formula = FOO/BAR
Table:
+-----------+---------+
| Component | Strings |
+-----------+---------+
| FOO | 1.1 |
| FOO | 1.2 |
| FOO | 1.3 |
| FOO | 5.1 |
| BAR | 101.10 |
| BAR | 101.15 |
| BAR | 5.10 |
+-----------+---------+
Result: (1.1+1.2+1.3+5.1)/(101.10+101.15+5.10)
Edit: I need the expression printed not the result of the calculation.
I think the solution probably envolves the function substitute.
I have written a quick dirty UDF to give you the idea how it can be done.
Put this code inside module and write FTEXT in any cell and give parameters.
Function test(Ref As Range, para1 As String) As String
Dim Cell As Range
Dim temp1 As String
Dim temp2 As String
For Each Cell In Ref
If Cell.Value = para1 Then
temp1 = temp1 & Cell.Offset(, 1).Value & "+"
Else
temp2 = temp2 & Cell.Offset(, 1).Value & "+"
End If
Next Cell
test = "(" + Left(temp1, Len(temp1) - 1) + ")" + "/" + "(" + Left(temp2, Len(temp2) - 1) + ")"
End Function
Assuming your data is in A2:B8 then following formula can do this.
=SUMIF(A2:A8,"FOO",B2:B8)/SUMIF(A2:A8,"BAR",B2:B8)
Is that what you want?

Returning column names if there is match in the row, looking for multiple matches [duplicate]

This question already has an answer here:
Concatenate column headers if value in rows below is non-blank
(1 answer)
Closed 5 years ago.
I have one table with parts as following;
---------------
| Part number |
---------------
| 123456 |
| 16D345 |
| 16E099 |
| 490586 |
| 970884 |
---------------
And another one like so;
---------------------------------------------------
| Part number | 940822 | 940922 | 170345 | 940222 |
---------------------------------------------------
| 123456 | X | | X | X |
| 16D345 | X | | X | |
| 16E099 | | X | | X |
| 490586 | X | | X | X |
| 970884 | | | X | |
---------------------------------------------------
The numbers in columns of the second table are 'units'.
I'm trying to figure out how to get all the unit numbers where a part has X. Basically I want to end up with the following;
----------------------------------------
| Part number | Used in |
----------------------------------------
| 123456 | 940822, 170345, 940222 |
| 16D345 | 940822, 170345 |
| 16E099 | 940922, 940222 |
| 490586 | 940822, 170345, 940222 |
| 970884 | 170345 |
----------------------------------------
Now I've just recently learned how to use INDEX and MATCH but haven't been able to get the result I want. I've tried using array formulas but I don't understand them yet.
If you have a subscription to Office 365 Excel then you can use the following array formula:
=TEXTJOIN(", ",TRUE,IF(($E$2:$H$6 = "X")*($D$2:$D$6=A2),$E$1:$H$1,""))
Being an array formula it needs to be confirmed with Ctrl-Shift-Enter instead of Enter when exiting edit mode. If done correctly then Excel will put {} around the formula.
If you do not have Office 365 then you can put this code in a module attached to the workbook and use the formula as described above:
Function TEXTJOIN(delim As String, skipblank As Boolean, arr)
Dim d As Long
Dim c As Long
Dim arr2()
Dim t As Long, y As Long
t = -1
y = -1
If TypeName(arr) = "Range" Then
arr2 = arr.Value
Else
arr2 = arr
End If
On Error Resume Next
t = UBound(arr2, 2)
y = UBound(arr2, 1)
On Error GoTo 0
If t >= 0 And y >= 0 Then
For c = LBound(arr2, 1) To UBound(arr2, 1)
For d = LBound(arr2, 1) To UBound(arr2, 2)
If arr2(c, d) <> "" Or Not skipblank Then
TEXTJOIN = TEXTJOIN & arr2(c, d) & delim
End If
Next d
Next c
Else
For c = LBound(arr2) To UBound(arr2)
If arr2(c) <> "" Or Not skipblank Then
TEXTJOIN = TEXTJOIN & arr2(c) & delim
End If
Next c
End If
TEXTJOIN = Left(TEXTJOIN, Len(TEXTJOIN) - Len(delim))
End Function

Excel: need to transpose one field of multiple values while duplicating the remaining fields

I have a spreadsheet that has column E that requires values split (comma separated values currently) and transposed to multiple rows. Columns C, D, F-S need copied down. Example:
Columns
+-----+-------+------------------+-------+--------+--------+----------+
| C | D | E | F | G | H | ... S |
+-----+-------+------------------+-------+--------+--------+----------+
| 20 | hey | one, two, three | xyz | unit | right | ... end |
+-----+-------+------------------+-------+--------+--------+----------+
I need it to look like:
+-----+-------+--------+-------+--------+--------+---------+
| C | D | E | F | G | H | ... S |
+-----+-------+--------+-------+--------+--------+---------+
| 20 | hey | one | xyz | unit | right | ... end |
| 20 | hey | two | xyz | unit | right | ... end |
| 20 | hey | three | xyz | unit | right | ... end |
+-----+-------+--------+-------+--------+--------+---------+
any help would be great!
This Code should help you...
Sub GetOutput()
Dim rndData As Range
Dim rngOutCell As Range
Dim MainIindex As Integer '<< Column index Needs to be split
Dim AllData As Variant '<< Data Range
Dim InnerData As Variant '<< Split Data
Dim intRecordCounter As Integer
MainIindex = 3
lngDataRow = 0
Set rndData = Sheet1.Range("E1").CurrentRegion
Set rngOutCell = Sheet1.Range("O1")
For intRecordCounter = 1 To rndData.Rows.Count
AllData = rndData.Rows(intRecordCounter).Value
InnerData = Split(rndData.Rows(intRecordCounter).Cells(MainIindex).Value, ",")
Set rngOutCell = Sheet1.Cells(Rows.Count, rngOutCell.Column).End(xlUp)
If rngOutCell <> vbNullString Then
Set rngOutCell = rngOutCell.Offset(1)
End If
Set rngOutCell = rngOutCell.Resize(UBound(InnerData) + 1, UBound(AllData, 2))
rngOutCell.Value = AllData
rngOutCell.Offset(, MainIindex - 1).Resize(, 1).Value = Application.Transpose(InnerData)
Next intRecordCounter
ClearMemory:
MainIindex = Empty
If IsArray(AllData) Then Erase AllData
If IsArray(InnerData) Then Erase InnerData
intRecordCounter = Empty
Set rndData = Nothing
Set rngOutCell = Nothing
End Sub

Resources