Data validation matching with a list with VBA - vba

I have a certain range of cells in excel where I want to apply data validation on another data validation.
I want the user to only have 3 options for data input within range of cells:
either a number,
a range of numbers or by
choosing from a dropdown list that contains words and numbers.
I already implemented number 1 and 2 with the following function:
Function checkStr(ByVal str As String) As String
Dim objRegEx As Object, allMatches As Object
Set objRegEx = CreateObject("VBScript.RegExp")
With objRegEx
.MultiLine = False
.IgnoreCase = False
.Global = True
.Pattern = "^\d+(-\d+)?$"
End With
Set allMatches = objRegEx.Execute(str)
checkStr = (allMatches.Count > 0)
End Function
Since the above function will only allow numbers or a range of numbers to be inputted, any ideas on how to add a validation to allow values from a pre-defined list containing both words and numbers?

I suggest to change the return As Boolean and then just filter the str against an array of valid list entries.
Function checkStr(ByVal str As String) As Boolean
Dim objRegEx As Object, allMatches As Object
Set objRegEx = CreateObject("VBScript.RegExp")
With objRegEx
.MultiLine = False
.IgnoreCase = False
.Global = True
.Pattern = "^\d+(-\d+)?$"
End With
Set allMatches = objRegEx.Execute(str)
Dim ValidList As Variant
ValidList = Array("123", "456") 'your list of valid entries
'check if either str is in the ValidList OR matches the regex
If (UBound(Filter(ValidList, str)) > -1) Or (allMatches.Count > 0) Then
checkStr = True
End If
End Function
If the list of valid entries is in a range you can replace it with:
ValidList = WorksheetFunction.Transpose(Worksheets("SheetName").Range("A1:A10").Value)

The list is taking values from some range. Thus, take the range of the list and use the Application.Match() to check whether the str is there:
Public Function checkStr(str As String) As Boolean
Dim isItError As Variant
isItError = Application.Match(str, Worksheets(1).Range("A1:A5"), 0)
checkStr = Not IsError(isItError)
End Function
Application.Match() would return either error or true. Thus, your function can be fixed with Not IsError().
And if you want to compare Strings with Strings and Numbers as Numbers, try to pass the variable as Variant and let VBA decide what it is actually:
Public Function checkMe(someVar As Variant) As Boolean
Dim isItError As Variant
Dim formulaAddress As String
With Range("C1").Validation
formulaAddress = Right(.Formula1, Len(.Formula1) - 1)
End With
isItError = Application.Match(someVar, Range(formulaAddress))
checkMe = Not IsError(isItError)
End Function
If you explicitly define the variable as a numeric or string, the other option would be excluded in the Application.Match() comparison:
?Application.Match("1",Array(1,2,3))
Error 2042
?Application.Match(1,Array(1,2,3))
1
?Application.Match("1",Array("1","2","3"))
1
?Application.Match(1,Array("1","2","3"))
Error 2042

Related

Visio VBA public array redim fails

I have a userform that processes some data and saves it to an array. I would like to make this array available to use the NEXT TIME I open the userform. So far I have tried: static variables, global variables, and SaveSettings but nothing has worked. Here is my current attempt:
In the ThisDocument module:
Public cache ' as a global, this is outside of any sub/function
In the Userform module:
Private Function PopulateList(pat As String)
ThisDocument.cache = Array()
Dim retArr
retArr = Array()
Dim regEx As Object
Set regEx = CreateObject("vbscript.regexp")
Dim matchedRows As Integer
matchedRows = 0
With regEx
.Global = True
.MultiLine = False
.IgnoreCase = True
.pattern = pat
End With
Dim lngRowIDs() As Long
Dim vsoRecordSet As DataRecordset
Dim rowMatches As Boolean
Set vsoDataRecordset = Visio.ActiveDocument.DataRecordsets(0)
lngRowIDs = vsoDataRecordset.GetDataRowIDs("")
For lngRow = LBound(lngRowIDs) To UBound(lngRowIDs) + 1
rowMatches = False
varRowData = vsoDataRecordset.GetRowData(lngRow)
If regEx.Test(CStr(varRowData(0))) Then
rowMatches = True
End If
' THIS IS THE IMPORTANT PART BELOW HERE
If rowMatches Then
ReDim Preserve retArr(matchedRows)
retArr(matchedRows) = Array(CStr(varRowData(0)), CStr(varRowData(1)), varRowData(2), varRowData(3))
ReDim Preserve ThisDocument.cache(matchedRows)
ThisDocument.cache(matchedRows) = Array(CStr(varRowData(0)), CStr(varRowData(1)), varRowData(2), varRowData(3))
matchedRows = matchedRows + 1
End If
Next lngRow
PopulateList = retArr
End Function
Below the THIS IS THE IMPORTANT PART comment you can see I am trying to set the cache array to be the same as the retArr. When calling ReDim, retArr works as expected but the same operation for ThisDocument.cache fails to compile with a data member not found error. Why is this?
ThisDocument is not a module, it is an object. In your code, the compiler probably simply does not see it (does not understand what ThisDocument is). That is why you get the compilation error (redim is applicable to arrays only)
Maybe it is a conceptual problem. Do you expect your global vba variable values to be saved in the document between runs?

Check for consecutive characters in an excel cell

If you could help me I am in need to finding out if a character of the alphabet repeats consecutively 3 or more times in a cell, eg if a cell is "aronfff" or "aaaaaron" I want it to return true otherwise to return false eg "aaron".
Function InRowChars(cell As String) As Boolean
Dim repeats As Integer, char As String, i As Integer
repeats = 0
char = "abcdefghijklmnopqrstuvwxyz"
For i = 1 To Len(cell)
If cell.Value = " " Then
repeats = chars + 1
Else
chars = 0
End If
Next i
If chars = 3 Then
InRowChars = True
Else
InRowChars = False
End If
End Function
I don't know how to get the value of the cell to be checked against the alphabet.
This can be achieved with regular expressions. I've made a function example that also accept the number of minimum characters desired:
'Add a reference to Microsoft VBScript Regular Expressions 5.5
Function ContainsConsecutiveChars(ByRef CellRef As Range, Optional ConsecutiveCount As Long = 3) As Boolean
Dim chars() As String
chars = Split("a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z", ",")
With New RegExp
.Pattern = Join(chars, "{" & ConsecutiveCount & ",}|")
ContainsConsecutiveChars = .test(CellRef.Value2)
End With
End Function
Here is a another regex solution that returns TRUE or FALSE depending on whether or not there are three or more repeating alphabetic characters:
Option Explicit
Function TripleChars(S As String) As Boolean
Dim RE As Object
Set RE = CreateObject("vbscript.regexp")
With RE
.Global = True
.Pattern = "([a-z])\1\1"
.ignorecase = True 'edit as desired
TripleChars = .test(S)
End With
End Function
And here is an explanation of the Regex Pattern:
([a-z])\1\1
([a-z])\1\1
Options: Case insensitive; ^$ don’t match at line breaks
Match the regex below and capture its match into backreference number 1 ([a-z])
Match a single character in the range between “a” and “z” [a-z]
Match the same text that was most recently matched by capturing group number 1 \1
Match the same text that was most recently matched by capturing group number 1 \1
Created with RegexBuddy
I see you already have a RegEx answer now. Just finished my version so thought I'd post it to.
#Thunderframe - I liked the optional bit, so have blatantly taken it for my version to.
Public Function RepeatingChars(Target As Range, Optional ConsecutiveCount As Long = 3) As Variant
Dim RE As Object, REMatches As Object
Set RE = CreateObject("VBScript.RegExp")
With RE
.MultiLine = False
.Global = False
.IgnoreCase = True
.Pattern = "(.)\1{" & ConsecutiveCount - 1 & ",}"
End With
Set REMatches = RE.Execute(Target.Value)
If REMatches.Count = 0 Then
RepeatingChars = CVErr(xlErrNA)
Else
RepeatingChars = REMatches(0)
End If
End Function
The function will return duplicates of any character, or #NA if no matches found.
Edit
After a quick re-read of your question you can replace the whole If...End If block with RepeatingChars = REMatches.Count <> 0 to return TRUE/FALSE. Remember to change the return type of the function to Boolean in this case.
This is what I have came up with so far:
Option Explicit
Function checkChars(inputCell As String, Optional repeat As Long = 3) As Boolean
Dim cnt As Long
Dim previous As String
Dim countResult As Long
For cnt = 1 To Len(inputCell)
If previous = Mid(inputCell, cnt, 1) Then
countResult = countResult + 1
Else
countResult = 1
End If
If countResult = (repeat) Then
checkChars = True
Exit Function
End If
previous = Mid(inputCell, cnt, 1)
Next cnt
End Function
Public Sub TestMe()
Debug.Print checkChars("lalaaa")
Debug.Print checkChars("lalaala", 2)
Debug.Print checkChars("lalaala", 1)
Debug.Print checkChars("lflalajajala", 2)
End Sub
The idea is that you can also pass the repeat number as an optional value, if it is different than 3. This is what you get as an output from the example:
True
True
True
False

Function that returns all numerical values in a string, similar to Val

Is there a function similar to Val that will give me all the numbers in a string instead of stopping when it reaches a non-numerical character?
For example:
Dim myNum As Integer
myNum = Val("24 and 25")
'this only gives myNum the value of 24. Is there a function that would set the value to 2425?
Use this custom function
Function customVal(s As String) As Long
With CreateObject("VBScript.Regexp")
.Pattern = "\D": .Global = True
customVal = .Replace(s, "")
End With
End Function
Sub Testing()
Dim s As String: s = "24 and 25 and 26"
Debug.Print customVal(s)
End Sub
242526
Maybe create your own function that uses regular expressions to replace the non-numeric values. Something to the effect of:
Dim result As String = Regex.Replace("24 and 25", "[^0-9]", "")

Userform Textboxs are numeric (and null)

I am implementing a Userform and wish to include some checks on the input data prior to running the Userform. In particular, check all inputs into the Userform textboxs are numerical, although it is valid a textbox is blank or Null. I have tried implementing the following:
Select Case KeyAscii
Case 0, 46, 48 To 57
Case Else
MsgBox "Only numbers allowed"
End Select
But this does not work.
Please, ideas?
Thank you very much!!!!!!!!!
Maybe bit long winded - I usually use a class module and the tag property on the control to decide what can be entered in a textbox.
Create a form with four text boxes.
Give the text boxes these tags:
1;CDBL
2;CINT
3;CSTR
4;CSENTENCE
The numbers are the columns to paste the values into when the form is saved (I haven't described that bit here).
The text describes what can be entered in the textbox - CDBL is numeric with 2 decimal places, CINT is numeric with 0 decimal places, CSTR is for Proper text and CSENTENCE is for sentence text.
Create a class module called clsControlText.
Add this code to the class module:
Public WithEvents txtBox As MSForms.TextBox
Private Sub txtBox_Change()
Static LastText As String
Static SecondTime As Boolean
Const MaxDecimal As Integer = 2
Const MaxWhole As Integer = 1
With txtBox
If InStr(.Tag, ";") > 0 Then
Select Case Split(.Tag, ";")(1)
Case "CDBL", "CCur"
'Allow only numbers with <=2 decimal places
If Not SecondTime Then
If .Text Like "[!0-9.-]*" Or Val(.Text) < -1 Or _
.Text Like "*.*.*" Or .Text Like "*." & String$(1 + MaxDecimal, "#") Or _
.Text Like "?*[!0-9.]*" Then
Beep
SecondTime = True
.Text = LastText
Else
LastText = .Text
End If
End If
SecondTime = False
Case "CINT"
'Allow only whole numbers.
If .Text Like "[!0-9]" Or Val(.Text) < -1 Or .Text Like "?*[!0-9]*" Then
Beep
.Text = LastText
Else
LastText = .Text
End If
Case "CSTR"
'Convert text to proper case.
.Text = StrConv(.Text, vbProperCase)
Case "CSENTENCE"
'Convert text to sentence case (capital after full-stop).
.Text = ProperCaps(.Text)
Case Else
'Allow anything.
End Select
End If
End With
End Sub
Private Function ProperCaps(strIn As String) As String
Dim objRegex As Object
Dim objRegMC As Object
Dim objRegM As Object
Set objRegex = CreateObject("vbscript.regexp")
strIn = LCase$(strIn)
With objRegex
.Global = True
.ignoreCase = True
.Pattern = "(^|[\.\?\!\r\t]\s?)([a-z])"
If .Test(strIn) Then
Set objRegMC = .Execute(strIn)
For Each objRegM In objRegMC
Mid$(strIn, objRegM.firstindex + 1, objRegM.Length) = UCase$(objRegM)
Next
End If
ProperCaps = strIn
End With
End Function
Add this code to the user form:
Private colTextBoxes As Collection
Private Sub UserForm_Initialize()
Dim ctrlSelect As clsControlText
Dim ctrl As Control
Me.Caption = ThisWorkbook.Name
Set colTextBoxes = New Collection
For Each ctrl In Me.Controls
Select Case TypeName(ctrl)
Case "TextBox"
Set ctrlSelect = New clsControlText
Set ctrlSelect.txtBox = ctrl
colTextBoxes.Add ctrlSelect
End Select
Next ctrl
End Sub
NB: Not all this code is mine. I found ProperCaps and the code for CDBL elsewhere on this site - or maybe MrExcel.
You could use a basic LIKE or Regexp
Sub Test()
Debug.Print StrCheck("")
Debug.Print StrCheck("hello kitty")
Debug.Print StrCheck("4156")
End Sub
function
Function StrCheck(strIn As String) As String
Dim objRegex As Object
Set objRegex = CreateObject("vbscript.regexp")
objRegex.Pattern = "\d+"
'vaidate empty string
If Len(Trim(strIn)) = 0 Then
StrCheck = True
Else
'validate whether non-empty string is numeric
StrCheck = objRegex.Test(strIn)
End If
End Function

Access values in array and display on combobox

This is far beyond my skill set, frankly, I have never done anything like this and don't know if it is possible. The procedure below builds an array based on the values of column B6.
Private Sub dsbPositionBoard_Startup() Handles Me.Startup
'This event runs when the dsbPositionBoard starts. The procedure
'checks for the values in column A of the allPositionsAnualized sheet
'and populates the combobox with those values. If there are no values the box
'is disabled.
Dim xlRng As Excel.Range
Dim strRngArr As String
Dim strChkRange As String
Try
xlWB = CType(Globals.ThisWorkbook.Application.ActiveWorkbook, Excel.Workbook)
xlWS = DirectCast(xlWB.Sheets("allPositionsAnnualized"), Excel.Worksheet)
xlRng = DirectCast(xlWS.Range("B6", xlWS.Range("B6").End(Excel.XlDirection.xlDown)), Excel.Range)
strRngArr = String.Empty
strChkRange = CStr(xlWS.Range("B6").Value)
If (String.IsNullOrEmpty(strChkRange)) Then
cmbSelectPosition.Enabled = False
Else
'Build a string array delimited by commas
For i As Integer = 1 To xlRng.Rows.Count
Dim xlRngCell As Excel.Range = DirectCast(xlRng.Rows(i), Excel.Range)
strRngArr &= DirectCast(xlRngCell.Value.ToString, String) & ","
Next
strRngArr = strRngArr.Remove(strRngArr.Length - 1, 1)
cmbSelectPosition.Items.AddRange(strRngArr.Split(","c))
xlRng = Nothing
xlWS = Nothing
End If
Catch ex As Exception
MsgBox("There no positions available to select", CType(vbOKOnly, MsgBoxStyle), "Empty Selection")
End Try
End Sub
Now, the function below is used to select the value of cell range, pass it to a helper cell (B37) and then select the corresponding sheet. The value that this function passes to the helper cell has an equal value in the array above.
Private Function MoveBtwSheets(range As String) As String
'This function is used to toggle between the position board
'and the employee board. The function is utilized to select
'the employees listed in the position board, click on the radio button
' and open that employees information in the employee board
'#parameter range: Selects the cell with the employee name
Dim xlCalc As Excel.Worksheet
strMessage = "This employee does not exist. Please verify the employee name"
strCaption = "Selection Error"
msgBoxType = MessageBoxIcon.Error
msgBoxBtns = MessageBoxButtons.OK
xlWB = CType(Globals.ThisWorkbook.Application.ActiveWorkbook, Excel.Workbook)
xlCalc = CType(xlWB.Worksheets("calculationSheets"), Excel.Worksheet)
xlWSEE = CType(xlWB.Worksheets("employeeBoard"), Excel.Worksheet)
xlWSPOS = CType(xlWB.Worksheets("positionBoard"), Excel.Worksheet)
Application.ScreenUpdating = False
Try
xlCalc.Range("B36").Value = xlWSPOS.Range(range).Value
With xlWSEE
.Select()
.Range("E37").Select()
End With
Application.ScreenUpdating = True
Catch ex As Exception
MessageBox.Show(strMessage, strCaption, msgBoxBtns, msgBoxType)
End Try
Return ""
End Function
So what I wanted to do add to my function is a way to search my array for the value on B37 and then display that value in the combobox in the first procedure. Basically, instead of me dropping down and selecting the item from the array, function would search the array for me and select that item.
If I am not very clear, I can clarify or post screen shots.
This would be a great time to use LINQ. In your initial method (dsbPositionBoard_Startup()), you can add each string in Column A into a List(Of String). Then you can query the list using the value of B37 as your search parameter.
Declare the list at the top of your class (outside of any methods)
Private _myList As New List(Of String)
Add this code to your first method
strRngArr = strRngArr.Remove(strRngArr.Length - 1, 1)
cmbSelectPosition.Items.AddRange(strRngArr.Split(","c))
_myList.Add(strRngArr.Split(","c))
xlRng = Nothing
xlWS = Nothing
Now add a function along the following lines:
Private Function QueryValues(ByVal myParameter as String) As String
Dim results = From result In _myList Where result = myParameter Select result Distinct
Return results(0)
End Function
Call that function (add some error handling/null reference checks though) with your parameter being the value of cell B37 (or any cell value as string).