приведенный ниже код извлекает все числа из строки и даже объединяет их.
Но мне нужно извлечь только одно целое число с правилами:
1- число состоит из одной или двух цифр (плюс десятичная часть, если она существует).
2- если за номером следует "
или inch
или in
, извлеките его и игнорируйте остальные числа в строке.
3- если вышеуказанное условие (2) не найдено, то извлечь первые числа и игнорировать остальные числа в строке.
Public Function GetNumeric(CellRef As String)
Dim StringLength As Long, i As Long, Result As Variant
StringLength = Len(CellRef)
For i = 1 To StringLength
If IsNumeric(Mid(CellRef, i, 1)) Then
Result = Result & Mid(CellRef, i, 1)
End If
Next i
GetNumeric = Result
End Function
@JvdV, в последнем образце совпадений нет, потому что число должно состоять только из одного или двух символов.
Верно, тогда последнее правило немного вводило в заблуждение.
Я думаю, что ответ уже содержится в предложенных вами требованиях Вам нужно добавить оператор if в качестве проверки, чтобы сделать это, или оператор case, я рекомендую определить дополнительную переменную для проверки, прежде чем добавлять к результатам. Определите это как символ вашей строки, которую вы перебираете. Пример:
Dim Check as string
Check = Mid(CellRef, i, 1)
If Check = Chr(34) then GetNumeric = Result
Работа с несколькими числами без дальнейшего определения приведет к неразрешимым ситуациям, например, в вашем предпоследнем примере нет указания, является ли 1 или 36 правильным.
Пожалуйста, не могли бы вы добавить полный код, а то он у меня не работает?
Извините, но это всего лишь идея для дальнейшего развития. Вы должны сделать проверку для каждого из желаемых показателей длины. И как я и другие здесь утверждали. Вы обнаружите проблемы с данными, которые недостаточно конкретны для получения желаемого результата.
Возможно, создайте свой собственный UDF, используя регулярное выражение. Возможно что-то вроде:
Public Function RegexExtract(str, pat, Optional gFlag As Boolean = False, Optional pos As Integer = 0, Optional cse as Boolean = True) As String
Static RE As Object: If RE Is Nothing Then Set RE = CreateObject("vbscript.regexp")
RE.Pattern = pat
RE.Global = gFlag
RE.IgnoreCase = cse
If RE.Test(str) Then
RegexExtract = RE.Execute(str)(pos)
Else
RegexExtract = vbNullString
End If
End Function
Обратите внимание, что я создал необязательный глобальный флаг, который по умолчанию является ложным, который должен просто вытащить самое первое попадание в ячейку. Необязательная переменная pos предназначена для возврата определенного совпадения, если вы хотите каким-то образом вернуть другие числа, когда вы устанавливаете глобальный флаг в значение true. Также обратите внимание на использование флага case, для которого установлено значение true, чтобы по умолчанию не учитывать регистр.
Вы можете вызвать вышеуказанное как:
=IFERROR(--RegexExtract(A1,"\b\d\d?(?!\d)(?=\s*(?:""|''|in(?:ch)?\b)?)"),"")
Используемый шаблон означает:
\b\d\d?
- граница слова с 1 цифрой и 2-й необязательной;(?!\d)
- Отрицательный просмотр вперед, чтобы не утверждать больше цифр;(?=\s*(?:"|''|in(?:ch)?\b)?)
- Положительный просмотр вперед для утверждения позиции сопровождается 0+ (жадными) символами пробела и:
"
- Двойная кавычка или;''
- Две одинарные кавычки или;in(?:ch)?\b
- Буквально «in», за которым следует необязательный «ch» и граница слова, чтобы подтвердить, что буквы не являются частью большей подстроки, чтобы предотвратить ложные срабатывания.РЕДАКТИРОВАТЬ1:
Согласно комментариям OP ниже; есть случай, когда может быть число процентов, которое не находится на 1-й позиции. Поскольку OP позволяет также сопоставлять число без дюймов, добавление здесь состоит в том, чтобы включить отрицательный просмотр вперед, который будет утверждать, что нет второго вхождения действительного шаблона:
\b\d\d?(?!\d|.*\b\d+\s*(?:""|''|in(?:ch)?\b))(?=\s*(?:""|''|in(?:ch)?\b)?)
Я полагаю, что это неявно то же самое, что и:
\b\d\d?(?!\d|.*\b\d+\s*(?:""|''|in(?:ch)?\b))
РЕДАКТИРОВАТЬ2:
Чтобы разрешить десятичные дроби, вы можете включить необязательную группу без захвата:
=IFERROR(--RegexExtract(A2,"\b\d\d?(?:\.\d+)?(?!\d|.*\b\d+\s*(?:""|''|in(?:ch)?\b))"),"")
Я пробовал также как =RegexExtract(A1,"\b\d\d?(?!\d)(?=\s*(?:""|''|in(?:ch)?\b)?)")
и он отлично работает, так что же нужно IFERROR
?
Я играл со всеми необязательными аргументами и установил Optional gFlag As Boolean = True, Optional pos As Integer = 1
, но вывод этих строк неверен 1 Water 3" Pipe
и 4" Water 5
, правильный результат должен быть (3 , 4 )
соответственно, но теперь это (3 , 5)
@waleed, значит, ты неправильно понял переменные. Вы должны сохранить необязательные значения по умолчанию, чтобы получить правильный номер.
Да, я сохранил необязательные значения по умолчанию, но в этом образце выводится неверный результат (1 Water 3" Pipe)
он дает 1, а должен 3
Ах, я понимаю, что вы имеете в виду. Это присуще моему первому предположению, что вы хотите вернуть самый первый действительный результат. Это было бы правильно с текущим шаблоном. Вам нужно будет настроить шаблон, чтобы удовлетворить ваш текущий образец. Что-то, что я могу сделать позже сегодня вечером, когда я вернусь за компьютер.
Вот два решения, которые не требуют Regex.
Первое решение применяет ряд преобразований к строке, чтобы мы могли использовать Split для получения последовательности строк, некоторые из которых будут числами. Проблема здесь заключается в выборе правильных преобразований, чтобы можно было применить разделение для изоляции чисел. Иногда это может быть невозможно.
Второе решение просто анализирует строку до тех пор, пока не будет извлечена последовательность числовых символов, а затем возвращает эту числовую строку для дальнейшей обработки. Это, вероятно, лучшее решение в вашем случае.
Обратите внимание, что ни одно из решений не тестировалось для крайних случаев.
Учитывая, что вы пытаетесь проанализировать то, что кажется текстом произвольной формы, может быть много пограничных случаев.
Sub Test()
Dim myC As Collection
Set myC = New Collection
With myC
.Add "INSPECT - 8" & Chr$(34) & " Water 12 Pipe 8"
.Add "INSPECT- 18" & Chr$(34) & " Water 12 18"
.Add "PM-6in Pipe From M37 st 6"
.Add "PM- 6 inch Pipe From H44 6"
.Add "PM-36 Pipe From M-1T 36"
.Add "PM-123 Pipe From MT"
End With
Dim myItem As Variant
Dim myNumber As Long
For Each myItem In myC
'Option 1
' If TryGetFirstNumber(myItem, myNumber) Then
' Debug.Print myNumber
' End If
' option 2
' Debug.Print ParseFirstNumber(VBA.CStr(myItem))
Next
End Sub
' Pass ByVal so we don't alter the original string
Public Function ApplyTransforms(ByVal ipString As String) As String
ipString = VBA.LCase(ipString)
ipString = VBA.Replace(ipString, "-", " ")
ipString = VBA.Replace(ipString, VBA.Chr(34), " ")
ipString = VBA.Replace(ipString, "in ", " ")
ipString = VBA.Replace(ipString, "inch ", " ")
ApplyTransforms = ipString
End Function
'The try function indicates success by the returned boolean value, the result of the success is returned Byref in parameter opNumber
Public Function TryGetFirstNumber(ByRef ipString As Variant, ByRef opNumber As Long, Optional ipLength As Long = 2) As Boolean
Dim myArray As Variant
myArray = VBA.Split(ApplyTransforms(ipString))
Dim myItem As Variant
For Each myItem In myArray
If VBA.IsNumeric(myItem) Then
If VBA.Len(myItem) <= ipLength Then
opNumber = VBA.CLng(myItem)
TryGetFirstNumber = True
Exit Function
End If
End If
Next
TryGetFirstNumber = False
End Function
Public Function ParseFirstNumber(ByRef ipString As String) As String
Dim myIndex As Long
myIndex = 1
Dim myLen As Long
myLen = VBA.Len(ipString)
Dim myNumber As String
myNumber = vbNullString
Do While myIndex <= myLen
If VBA.InStr("0123456789", VBA.Mid$(ipString, myIndex, 1)) > 0 Then
Exit Do
End If
myIndex = myIndex + 1
Loop
If myIndex > myLen Then
ParseFirstNumber = myNumber
Exit Function
End If
Do While VBA.InStr("0123456789", VBA.Mid$(ipString, myIndex, 1)) > 0
myNumber = myNumber & VBA.Mid$(ipString, myIndex, 1)
myIndex = myIndex + 1
If myIndex > myLen Then
ParseFirstNumber = myNumber
Exit Function
End If
Loop
ParseFirstNumber = myNumber
End Function
Я попробовал второе решение, оно работает со всеми моими образцами, кроме последнего "PM-123 Pipe From MT
», результат должен быть пустым, так как я требовал, чтобы число состояло из одного или двух символов.
Как вы думаете, что означает термин «требуется дальнейшая обработка»?
хорошо понял, я добавил if
с Len
, теперь проблема в том, что он извлекает первое попадание независимо от его положения.
Правильный. Ни один из ваших примеров не показал этот пограничный случай. Как и прежде, может потребоваться дополнительная обработка.
Пожалуйста, (также) проверьте следующий способ. Он использует стандартный VBA и массив для обработки. Содержимое обработанного массива будет удалено в конце кода, поэтому он должен быть очень быстрым даже для больших диапазонов. Предполагается, что обрабатываемый диапазон начинается с "A1", заголовки, существующие в первой строке листа:
Sub extractInchesNoFromAllRange()
Dim sh As Worksheet, lastR As Long, arr, i As Long
Dim dblQ As Long, sQ As Long, strIn As Long, No As String
Set sh = ActiveSheet
lastR = sh.Range("A" & sh.rows.count).End(xlUp).row
arr = sh.Range("A2:B" & lastR).Value2
For i = 1 To UBound(arr)
dblQ = InStr(arr(i, 1), """") 'check if a double quote caracters exists and return its postition if it does
sQ = InStr(arr(i, 1), "''") 'check if two simple quote caracters exists and return its postition if it does
strIn = InStr(arr(i, 1), "in") 'the same as above for "in" string
No = "" 'reinitialize the variable to keep the extracted number (as string...)
If dblQ > 0 Or sQ > 0 Then 'if doble quote exists:
If IsNumeric(Mid(arr(i, 1), IIf(dblQ > 0, dblQ, sQ) - 1, 1)) Then 'if a number exists before the quote ch
No = Mid(arr(i, 1), IIf(dblQ > 0, dblQ, sQ) - 1, 1) 'extract first digit
arr(i, 2) = extractNo(IIf(dblQ > 0, dblQ, sQ) - 2, CStr(arr(i, 1)), No, True) 'call the function which make extraction by (backward) iteration
End If
ElseIf strIn > 0 Then 'if "in" exists:
If Mid(arr(i, 1), strIn + 2, 1) = " " Or Mid(arr(i, 1), strIn + 2, 2) = "ch" Or strIn + 1 = Len(arr(i, 1)) Then
If Mid(arr(i, 1), strIn - 1, 1) = " " Then
arr(i, 2) = extractNo(strIn - 2, CStr(arr(i, 1)), No, True)
Else
arr(i, 2) = extractNo(strIn - 1, CStr(arr(i, 1)), No, True)
End If
End If
Else
arr(i, 2) = extractNo(0, CStr(arr(i, 1)), "")
End If
Next i
'drop the processed arran content back in its range:
sh.Range("A2").Resize(UBound(arr), UBound(arr, 2)).Value2 = arr
End Sub
Function extractNo(pos As Long, str As String, No As String, Optional boolChar = False) As Variant
Dim i As Long, boolNo As Boolean
On Error GoTo WrongPatt
If boolChar Then 'if one of the searched characters has been found:
For i = pos To 1 Step -1
If IsNumeric(Mid(str, i, 1)) Or Mid(str, i, 1) = "." Then
No = CStr(Mid(str, i, 1)) & No
Else
extractNo = CDbl(No): Exit For
End If
Next i
Else 'if no searched string has been found:
For i = 1 To Len(str)
If IsNumeric(Mid(str, i, 1)) Then
boolNo = True
No = No & Mid(str, i, 1)
Else
If boolNo Then Exit For
End If
Next i
If Len(No) <= 2 And No <> "" Then
extractNo = CLng(No)
Else
extractNo = ""
End If
End If
Exit Function
WrongPatt:
extractNo = "Wrong pttern"
End Function
Но приведенный выше код будет обрабатывать только строку шаблона, которую вы указали в своем вопросе. Если, например, будет больше символов двойных кавычек с другим назначением ** перед тем, у которого есть число перед ним), код обработает только первый найденный. У него могут возникнуть проблемы, если искомые строки являются первыми в строке и т. д. Его можно адаптировать для работы с большим количеством условий, но мы здесь не умеем читать мысли, чтобы охватывать такие не показанные случаи...
Хорошо, я вспомню, что я написал вчера вечером. Я попробовал ваше последнее редактирование, и оно работает отлично 👍, я надеюсь, что ST позволит принять два ответа одновременно. со своей стороны, я немного адаптировал ваш код, чтобы использовать дополнительный массив для хранения извлеченных чисел (на самом деле я позаимствовал эту идею из других ваших ответов)
@ Валид Рад, что смог помочь! Да, я удивлен удалением комментариев. Использование другого массива — лучшая идея, если обрабатываемый диапазон содержит много столбцов, но только для двух мне кажется, что уникальный массив лучше. КПД конденсатора примерно одинаков для обоих случаев.
Пожалуйста, ты в порядке? Давно не видел от вас ответа.
@ Валид, я в порядке. Я участвую в проекте, который не позволяет мне много свободного времени. А на прошлой неделе я провел свой отпуск в Тунисе.
Почему нет совпадения в последнем примере, но есть совпадение в предпоследнем?