#nested #xquery #tokenize #brackets
#вложенный #xquery #маркировать #скобки
Вопрос:
Я пытаюсь написать функцию XQuery для маркирования строки с помощью разделителя, игнорируя разделители внутри вложенных выражений в квадратных скобках, например
tokenizeOutsideBrackets("1,(2,3)" , ",") => ( "1" , "(2,3)" )
tokenizeOutsideBrackets("1,(2,(3,4))" , ",") => ( "1" , "(2,(3,4))" )
tokenizeOutsideBrackets("1,(2,(3,(4,5)))" , ",") => ( "1" , "(2,(3,(4,5)))" )
tokenizeOutsideBrackets("1,(2,(3,4),5),6" , ",") => ( "1" , "(2,(3,4),5)" , "6" )
Если бы у меня были рекурсивные регулярные выражения или императивный язык, это было бы довольно тривиально, но я изо всех сил пытаюсь найти простой способ сделать это в XQuery.
Спасибо!
Комментарии:
1. «рекурсивные регулярные выражения» звучит как оксюморон… Кстати, не имеет значения, следует ли язык императивной или декларативной парадигме.
2. @Alejandro: да, я знаю, что это своего рода оксюморон … 🙂 PCRE — это то, к чему я обычно привык, и это поддерживает рекурсивные шаблоны (будь то технически обычные или нет)
Ответ №1:
Это выражение XQuery:
tokenize(replace('1,(2,(3,4),5),6','([0123456789] |(.*))(,)?','$1;'),';')
Вывод:
1 (2,(3,4),5) 6
Обновление: если будут строки типа '1,(2,3),(4,5),6'
, то вам понадобится анализатор для этой грамматики:
exp ::= term ( ',' term ) *
term ::= num | '(' exp ')'
num ::= ( '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' )
Комментарии:
1. Отличный трюк, но это приводит к сбою,
'1,(2,3),(4,5),6'
что дает1 (2,3),(4,5) 6
, а не ожидаемое1 (2,3) (4,5) 6
2. @jong: Если вы собираетесь использовать строку, отличную от предоставленной, то вам понадобится анализатор.
3. @Alejandro: Спасибо за вашу помощь в попытке. Надеялся, что мне не придется идти по пути синтаксического анализа, но думаю, что мое другое решение справится с задачей, проведет еще некоторое тестирование.
4. @jong: Другого способа для такого рода грамматики не существует. Кстати, функциональные анализаторы проще всего писать, следуя шаблонам комбинаторов синтаксических анализаторов: вам просто нужно перевести грамматику. К сожалению, для функций более высокого порядка в XQuery вам придется подождать, пока рабочий проект XQuery 3.0 не станет рекомендацией.
5. @jong, @Alejandro, @Pavel-Minaev: Вы могли бы сделать это в XSLT 2.0 FXSL 7 лет назад. Смотрите мой обобщенный анализатор LR1, полностью реализованный в XSLT 2.0 🙂
Ответ №2:
Один из способов сделать это — сначала разделить, а затем соединить токены с несбалансированными круглыми скобками с их соседями справа.
Приведенный ниже код даст вам желаемые результаты. Он использует fn: tokenize для разделения, затем (tail-) рекурсивно обрабатывает результирующие токены, объединяя, когда предыдущий токен имеет несовпадающие значения «(» и «)». У этого подхода есть некоторые недостатки, а именно неправильное сопоставление левой и правой скобок и обработка $delimiter как шаблона, так и литерала. Для правильной обработки необходимо больше кода, однако вы могли бы уловить идею.
declare function local:tokenizeOutsideBrackets($string, $delimiter)
{
local:joinBrackets(tokenize($string, $delimiter), $delimiter, ())
};
declare function local:joinBrackets($tokens, $delimiter, $result)
{
if (empty($tokens)) then
$result
else
let $last := $result[last()]
let $new-result :=
if (string-length(translate($last, "(", ""))
= string-length(translate($last, ")", ""))) then
($result, $tokens[1])
else
($result[position() < last()], concat($last, $delimiter, $tokens[1]))
return local:joinBrackets($tokens[position() > 1], $delimiter, $new-result)
};
Комментарии:
1. Аналогично моему подходу, но намного проще и приятнее.
Ответ №3:
Поигрался, и приведенная ниже функция, похоже, работает, хотя я не могу отделаться от мысли, что есть более простой способ.
Этот код использует функцию functx:index-of-string для поиска индексов всех разделителей. Затем он пытается найти каждый первый разделитель, где все, что слева, имеет равное количество открывающих и закрывающих скобок. После того, как это найдено, это повторяется со всем, что находится справа от этого разделителя.
declare function local:tokenizeOutsideBrackets(
$arg as xs:string?,
$delimiter as xs:string) as xs:string*
{
if (contains($arg, $delimiter))
then
(:find positions of all the delimiters:)
let $delimiterPositions := (
functx:index-of-string($arg,$delimiter),
string-length($arg) 1 (:Add in end of string too:)
)
(:strip out all the fragments that have matching
brackets to the left of each delimiter:)
let $fragments :=
for $endPos in $delimiterPositions
let $candidateString := substring($arg,1,$endPos - 1)
return
if (local:hasMatchedBrackets($candidateString))
then $candidateString
else ()
let $firstFragment := $fragments[1]
let $endPos := string-length($firstFragment)
(:recursively return the first matching fragment,
plus the fragments in the remaining string:)
return
(
$firstFragment,
local:tokenizeOutsideBrackets(
substring(
$arg,
$endPos string-length($delimiter) 1,
string-length($arg) - $endPos -(string-length($delimiter))
),
$delimiter
)
)
else if ($arg='') then () else ($arg)
};
declare function local:hasMatchedBrackets($arg as xs:string) as xs:boolean
{
count(tokenize($arg,'(')) = count(tokenize($arg,')'))
};