группировать по нескольким атрибутам из xml с помощью xslt - PullRequest
7 голосов
/ 14 сентября 2011

У меня есть следующий xml

<smses>
  <sms address="87654321" type="1" body="Some text" readable_date="3/09/2011 2:16:52 PM" contact_name="Person1" />
  <sms address="87654321" type="2" body="Some text" readable_date="3/09/2011 2:36:41 PM" contact_name="Person1" />
  <sms address="87654321" type="1" body="Some text" readable_date="3/09/2011 2:16:52 PM" contact_name="Person1" />
  <sms address="123" type="2" body="Some text" readable_date="3/09/2011 10:56:24 AM" contact_name="Person2" />
  <sms address="123" type="1" body="Some text" readable_date="3/09/2011 10:57:52 AM" contact_name="Person2" />
  <sms address="123" type="2" body="Some text" readable_date="3/09/2011 10:56:24 AM" contact_name="Person2" />
  <sms address="12345678" type="1" body="Some text" readable_date="3/09/2011 11:21:16 AM" contact_name="Person3" />
  <sms address="12345678" type="2" body="Some text" readable_date="3/09/2011 11:37:21 AM" contact_name="Person3" />

  <sms address="12345" type="2" body="Some text" readable_date="28/01/2011 7:24:50 PM" contact_name="(Unknown)" />
  <sms address="233" type="1" body="Some text" readable_date="30/12/2010 1:13:41 PM" contact_name="(Unknown)" />
</smses>

Я пытаюсь получить такой вывод (например, xml)

<sms contact_name="person1">
    <message type="1">{@body}</message>
    <message type="2">{@body}</message>
    <message type="1">{@body}</message>
</sms>
<sms contact_name="person2">
    <message type="2">{@body}</message>
    <message type="1">{@body}</message>
</sms>
<sms contact_name="person3">
    <message type="2">{@body}</message>
    <message type="1">{@body}</message>
</sms>
<sms contact_name="(Unknown)">
    <message type="2">{@body}</message>
    <message type="1">{@body}</message>
</sms>
<sms contact_name="(Unknown)">
    <message type="2">{@body}</message>   
</sms>

например, html

<div>
  <h1>Person: @contact_name (@address)</h1>
  <p>message @type: @body</p>
</div>

Мне удалось сделать это с помощью следующего XSLT-кода (извините, приведенный ниже код не отражает html полностью, результат - желаемый результат!)

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" indent="yes" />
    <xsl:key name="txt" match="sms" use="@contact_name" />
    <xsl:template match="smses">
        <xsl:apply-templates select="sms[generate-id(.)=generate-id(key('txt', @contact_name)[1])]">
            <xsl:sort select="@address" order="ascending" />
        </xsl:apply-templates>
    </xsl:template>
    <xsl:template match="sms">
        <h4><xsl:value-of select="@contact_name"  /></h4>
            <xsl:for-each select="key('txt', @contact_name)">
                    <br />
                    <xsl:value-of select="@body" />
            </xsl:for-each>
    </xsl:template>

</xsl:stylesheet>

Проблема, которая у меня есть, или, скорее,вопрос я задаю.У меня есть элемент sms с атрибутом @contact_name, который является «(неизвестно)», но @address уникален для обоих элементов, то есть они не должны быть сгруппированы, потому что сообщение sms пришло от другого числа / человека (дажехотя имя контакта одно и то же, его не имеет значения).Если я пытаюсь переупорядочить / изменить данные XML или есть способ заставить XSLT распознать группу для неизвестных, следует проверить, отличается ли @address, если @contact_name совпадает.

Редактировать:

Я не упомянул (или, скорее, забыл), что, хотя есть некоторые смс-сообщения с одинаковыми @contact_name и уникальными @address, есть также случаи, когда некоторые из полей @address имеют небольшое расхождение, когда онине ставьте код страны перед номером, например,

<sms contact_name="jared" address="12345" />
<sms contact_name="jared" address="+64112345" />

Но они должны быть сгруппированы, поскольку они являются от одного лица / номера.

Редактировать:

В моей ситуации возможны расхождения только в трехзначном (например, +64) коде страны и двухзначном сетевом коде (например, 21).По сути, результат должен быть, если @contact_name = то же самое и @address совершенно разные, т.е.

 <sms contact_name="jared" address="12345" />
 <sms contact_name="jared" address="5433467" />

, то они должны быть отдельными элементами, так как они от разных людей / чисел.

если @contact_name = то же самое и @address отличается только по кодам страны и сети, т.е.

 <sms contact_name="jared" address="02112345" />
 <sms contact_name="jared" address="+642112345" />

, то они должны быть сгруппированы, поскольку они принадлежат одному и тому же человеку / номеру

Редактировать:

коды стран: +64 (3 символа)

коды сети: 021 (3 символа, обычно последний символ меняется в зависимости от сети)

Номера (@address) сохраняются за <sms> как + 64-21-12345 (исключая тире) или 021-12345 (исключая тире).

1 Ответ

10 голосов
/ 14 сентября 2011

В этом преобразовании используется мюнхенская группировка с составными ключами :

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <xsl:key name="kContactByNameAddress" match="sms"
          use="concat(@contact_name,'+',@address)"/>

 <xsl:template match=
    "sms[generate-id()
        =
         generate-id(key('kContactByNameAddress',
                         concat(@contact_name,'+',@address)
                        )
                         [1]
                     )
        ]
    ">
     <sms contact_name="{@contact_name}">
       <xsl:apply-templates mode="inGroup"
       select="key('kContactByNameAddress',
                 concat(@contact_name,'+',@address)
                )"/>
     </sms>
 </xsl:template>

 <xsl:template match="sms" mode="inGroup">
       <message type="{@type}">
         <xsl:value-of select="@body"/>
       </message>
 </xsl:template>
 <xsl:template match="sms"/>
</xsl:stylesheet>

Применительно к предоставленному документу XML :

<smses>
    <sms address="87654321" type="1" body="Some text"
    readable_date="3/09/2011 2:16:52 PM" contact_name="Person1" />
    <sms address="87654321" type="2" body="Some text"
    readable_date="3/09/2011 2:36:41 PM" contact_name="Person1" />
    <sms address="87654321" type="1" body="Some text"
    readable_date="3/09/2011 2:16:52 PM" contact_name="Person1" />
    <sms address="123" type="2" body="Some text"
    readable_date="3/09/2011 10:56:24 AM" contact_name="Person2" />
    <sms address="123" type="1" body="Some text"
    readable_date="3/09/2011 10:57:52 AM" contact_name="Person2" />
    <sms address="123" type="2" body="Some text"
    readable_date="3/09/2011 10:56:24 AM" contact_name="Person2" />
    <sms address="12345678" type="1" body="Some text"
    readable_date="3/09/2011 11:21:16 AM" contact_name="Person3" />
    <sms address="12345678" type="2" body="Some text"
    readable_date="3/09/2011 11:37:21 AM" contact_name="Person3" />
    <sms address="12345" type="2" body="Some text"
    readable_date="28/01/2011 7:24:50 PM" contact_name="(Unknown)" />
    <sms address="233" type="1" body="Some text"
    readable_date="30/12/2010 1:13:41 PM" contact_name="(Unknown)" />
</smses>

желаемый, правильный результат получается :

<sms contact_name="Person1">
   <message type="1">Some text</message>
   <message type="2">Some text</message>
   <message type="1">Some text</message>
</sms>
<sms contact_name="Person2">
   <message type="2">Some text</message>
   <message type="1">Some text</message>
   <message type="2">Some text</message>
</sms>
<sms contact_name="Person3">
   <message type="1">Some text</message>
   <message type="2">Some text</message>
</sms>
<sms contact_name="(Unknown)">
   <message type="2">Some text</message>
</sms>
<sms contact_name="(Unknown)">
   <message type="1">Some text</message>
</sms>

Обновление : ОП отредактировал свой вопрос и опубликовал новые требования, согласно которым атрибут address может начинаться или не начинаться с кода страны. Два адреса, один с кодом страны, а другой без кода страны, являются «одинаковыми», если подстрока после кода страны равна другому адресу. В этом случае два элемента должны быть сгруппированы вместе.

Вот решение (было бы тривиально написать в XSLT 2.0, но в XSLT 1.0 сделать это за один проход довольно сложно. Решение с несколькими проходами более простое, но обычно требует xxx:node-set() функция расширения и, следовательно, потеряет переносимость):

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <xsl:key name="kContactByNameAddress" match="sms"
  use="concat(@contact_name,'+',
              concat(substring(@address,
                               4 div starts-with(@address,'+')),
                     substring(@address,
                               1 div not(starts-with(@address,'+'))
                              )
                     )
              )"/>

 <xsl:template match=
    "sms[generate-id()
        =
         generate-id(key('kContactByNameAddress',
                         concat(@contact_name,'+',
                                concat(substring(@address,
                                                 4 div starts-with(@address,'+')),
                                       substring(@address,
                                                 1 div not(starts-with(@address,'+'))
                                                 )
                                       )
                                 )
                         )
                         [1]
                     )
        ]
    ">
     <sms contact_name="{@contact_name}">
       <xsl:apply-templates mode="inGroup"
       select="key('kContactByNameAddress',
                 concat(@contact_name,'+',
                        concat(substring(@address,
                                         4 div starts-with(@address,'+')),
                               substring(@address,
                                         1 div not(starts-with(@address,'+'))
                                         )
                                )
                        )
                  )
      "/>
     </sms>
 </xsl:template>

 <xsl:template match="sms" mode="inGroup">
       <message type="{@type}">
         <xsl:value-of select="@body"/>
       </message>
 </xsl:template>
 <xsl:template match="sms"/>
</xsl:stylesheet>

Когда это преобразование применяется к следующему XML-документу (предыдущий + добавил три sms элемента с contact_name="Jared", два из которых имеют «идентичные» адреса, согласно вновь опубликованным правилам ):

<smses>
    <sms address="87654321" type="1" body="Some text"
        readable_date="3/09/2011 2:16:52 PM" contact_name="Person1" />
    <sms address="87654321" type="2" body="Some text"
        readable_date="3/09/2011 2:36:41 PM" contact_name="Person1" />
    <sms address="87654321" type="1" body="Some text"
        readable_date="3/09/2011 2:16:52 PM" contact_name="Person1" />
    <sms address="123" type="2" body="Some text"
        readable_date="3/09/2011 10:56:24 AM" contact_name="Person2" />
    <sms address="123" type="1" body="Some text"
        readable_date="3/09/2011 10:57:52 AM" contact_name="Person2" />
    <sms address="123" type="2" body="Some text"
        readable_date="3/09/2011 10:56:24 AM" contact_name="Person2" />
    <sms address="12345678" type="1" body="Some text"
        readable_date="3/09/2011 11:21:16 AM" contact_name="Person3" />
  <sms contact_name="jared" address="12345" type="2" body="Some text"/>
  <sms contact_name="jared" address="56789" type="1" body="Some text"/>
  <sms contact_name="jared" address="+6412345" type="2" body="Some text"/>
    <sms address="12345678" type="2" body="Some text"
        readable_date="3/09/2011 11:37:21 AM" contact_name="Person3" />
    <sms address="12345" type="2" body="Some text"
        readable_date="28/01/2011 7:24:50 PM" contact_name="(Unknown)" />
    <sms address="233" type="1" body="Some text"
        readable_date="30/12/2010 1:13:41 PM" contact_name="(Unknown)" />
</smses>

желаемый, правильный результат выдается :

<sms contact_name="Person1">
   <message type="1">Some text</message>
   <message type="2">Some text</message>
   <message type="1">Some text</message>
</sms>
<sms contact_name="Person2">
   <message type="2">Some text</message>
   <message type="1">Some text</message>
   <message type="2">Some text</message>
</sms>
<sms contact_name="Person3">
   <message type="1">Some text</message>
   <message type="2">Some text</message>
</sms>
<sms contact_name="jared">
   <message type="2">Some text</message>
   <message type="2">Some text</message>
</sms>
<sms contact_name="jared">
   <message type="1">Some text</message>
</sms>
<sms contact_name="(Unknown)">
   <message type="2">Some text</message>
</sms>
<sms contact_name="(Unknown)">
   <message type="1">Some text</message>
</sms>

Подробное объяснение :

Основная трудность в этой проблеме возникает из-за того, что в XPath 1.0 нет оператора «if ... then ... else», однако мы должны указать одно выражение XPath в use атрибут инструкции xsl:key, который выбирает либо атрибут address (если он не начинается с "+"), либо его подстроку после кода страны (если его строковое значение начинается с "+").

Здесь я использую реализацию этого бедного человека

if($condition)
  then $string1
  else $string2

Следующее выражение XPath при оценке эквивалентно приведенному выше :

concat(substring($string1, 1 div $condition),
       substring($string2, 1 div not($condition))
      )

Эта эквивалентность следует из того факта, что 1 div true() совпадает с 1 div 1, и это 1, тогда как 1 div false() совпадает с 1 div 0, и это число (положительное) Infinity.

Также для любой строки $s значение substring($s, Infinity) является просто пустой строкой. И, конечно же, для любой строки $s значение substring($s, 1) представляет собой просто строку $s.

II. Решение XSLT 2.0 :

<xsl:stylesheet version="2.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <xsl:template match="/*">
  <xsl:for-each-group select="sms" group-by=
   "concat(@contact_name,'+',
           if(starts-with(@address,'+'))
             then substring(@address, 4)
             else @address
           )">
     <sms contact_name="{@contact_name}">
      <xsl:apply-templates select="current-group()"/>
     </sms>

  </xsl:for-each-group>
 </xsl:template>

 <xsl:template match="sms">
       <message type="{@type}">
         <xsl:value-of select="@body"/>
       </message>
 </xsl:template>
</xsl:stylesheet>

когда это (намного проще!) Преобразование XSLT 2.0 применяется к тому же документу XML (см. Выше), получается тот же правильный вывод :

<sms contact_name="Person1">
   <message type="1">Some text</message>
   <message type="2">Some text</message>
   <message type="1">Some text</message>
</sms>
<sms contact_name="Person2">
   <message type="2">Some text</message>
   <message type="1">Some text</message>
   <message type="2">Some text</message>
</sms>
<sms contact_name="Person3">
   <message type="1">Some text</message>
   <message type="2">Some text</message>
</sms>
<sms contact_name="jared">
   <message type="2">Some text</message>
   <message type="2">Some text</message>
</sms>
<sms contact_name="jared">
   <message type="1">Some text</message>
</sms>
<sms contact_name="(Unknown)">
   <message type="2">Some text</message>
</sms>
<sms contact_name="(Unknown)">
   <message type="1">Some text</message>
</sms>
...