Обозначение строки в XQuery вне вложенного выражения в квадратных скобках

#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,')'))
};