XSLT для отображения только уникальных значений индекса глоссария?

Этот глоссарий выводит индекс из первой буквы каждой статьи. Я пытаюсь понять, как показывать только уникальные значения. Изучили предыдущую родную сестру и position(), но, похоже, не смогли найти правильный путь. Я ограничен использованием XSLT 1.0 и атрибутов.

глоссарий.xml

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="glossary.xsl"?>
<include>
    <file name="data.xml"/>
</include>

данные.xml

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<glossary>
    <entry term="cantaloupe" definition="A kind of melon"/>
    <entry term="banana" definition="A tropical yellow fruit"/>
    <entry term="apple" definition="A red fruit with seeds"/>
    <entry term="orange" definition="An orange citrus fruit"/>  
    <entry term="Cherry"  definition="A red fruit that grows in clusters "/>
    <entry term="cranberry" definition="A sour berry enjoyed at Thanksgiving"/>
    <entry term="avocado"  definition="A mellow fruit enjoyed in guacamole"/>
</glossary>

глоссарий.xsl

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="html" doctype-system="about:legacy-compat" encoding="UTF-8" indent="yes" />
    <xsl:template match="/">
        <html>
            <head></head>
            <body>
            <!-- Index: how to show unique values? -->
                <xsl:for-each select="document('data.xml')/glossary/entry" >
                    <xsl:sort select="@term" data-type="text" order="ascending" case-order="upper-first"/> 
                    <xsl:variable name="initial" select="substring(@term,1,1)" />
                    <a href="#{$initial}"><xsl:value-of select="$initial" /></a> |  
                </xsl:for-each>
            <!-- Glossary -->   
                <dl>
                    <xsl:for-each select="document('data.xml')/glossary/entry" >
                        <xsl:sort select="@term" data-type="text" order="ascending" case-order="upper-first"/> 
                        <xsl:variable name="initial" select="substring(@term,1,1)" />
                        <!-- Alphabetical header: how to only the first instance of each letter? -->
                        <a name="{$initial}"><h1><xsl:value-of select="$initial" /></h1></a> 
                        <dt><xsl:apply-templates select="@term"/></dt>
                        <dd><xsl:apply-templates select="@definition"/></dd>
                    </xsl:for-each>
                </dl> 
            </body>
        </html>
    </xsl:template>
</xsl:stylesheet>   

Вывод до сих пор

a | a | b | c | C | c | o |

a
яблоко
Красный фрукт с косточками

a
авокадо
Сочный фрукт, покрытый соусом гуакамоле

b
банан
Тропический желтый фрукты

c
мускусная дыня
Разновидность дыни

C
Вишня
Красный фрукт, растущий гроздьями

c
клюква
Кислая ягода, которой наслаждаются на День Благодарения

o
апельсин
Оранжевый цитрусовый фрукт



Желаемый результат

a | b | c | o

a
яблоко
Красный фрукт с семенами

авокадо
Мягкий фрукт в гуакамоле

b
банан
Тропический желтый фрукт

c
мускусная дыня
Разновидность дыни

Вишня
Красный фрукт, растущий гроздьями

клюква
Кислая ягода, которой наслаждаются на День Благодарения

o
апельсин
Оранжевый цитрусовый фрукт


person user1910503    schedule 29.01.2013    source источник
comment
Спасибо, что показали мне, как использовать мюнхианскую группировку. Я могу заставить работать решение @JLRishe, но все еще возюсь с версией ian-roberts. Не уверен, как оценить, что более эффективно. Я думаю, что xslt 1.0 неудобен для такого рода вещей. Также - мне интересно, необходимо ли понижение корпуса? Видит ли xsl:sort верхний и нижний регистр как разные символы?   -  person user1910503    schedule 29.01.2013
comment
xsl:sort различает прописные и строчные буквы, но это на самом деле не проблема. Нижний регистр важен, потому что (1) мюнхенский метод группировки будет рассматривать их как отдельные символы, если они не имеют последовательного регистра, и (2) вам нужно иметь одинаковый регистр в ваших заголовках.   -  person JLRishe    schedule 29.01.2013
comment
Я читал о мюнхианской группировке и благодарен за рабочий пример, так что это единственный жизнеспособный подход? Разве XSLT 1.0 не может просто сравнить существующий начальный с предыдущим и показать только, если он отличается? Может быть, мне не хватает фундаментального ограничения XSLT 1.0, так как я новичок в этом...   -  person user1910503    schedule 29.01.2013
comment
Группировку можно выполнить, сравнивая значения с предыдущими значениями, но на самом деле это не чище, и такой подход не рекомендуется, поскольку он очень неэффективен с вычислительной точки зрения, особенно если в исходных данных много строк. Ранее в этом месяце Дмитрий Новачев прокомментировал ситуацию, когда он увидел, что операция группировки preceding-sibling заняла более 40 минут, а мюнхенский подход завершился за 2 секунды. С точки зрения информатики, мюнхен имеет временную сложность O(N), в то время как временная сложность группировки сравнения братьев и сестер квадратична — O(N^2).   -  person JLRishe    schedule 29.01.2013
comment
XSLT не является процедурным языком, он просто описывает, как преобразовать один XML-документ в другой. Несмотря на свое название, <xsl:for-each> не обязательно должен быть реализован XSLT-процессором как последовательный цикл, процессор может вычислять значения различных итераций в любом порядке или даже параллельно в нескольких потоках, пока конечный вывод производится в правильном порядке (например, он может обрабатывать элементы в порядке документа, а затем применять sort в конце при выводе фрагментов).   -  person Ian Roberts    schedule 29.01.2013
comment
Вы можете использовать выражения XPath для сравнения текущего элемента в for-each с его собственными предыдущими элементами того же уровня в порядке документа, но нет способа получить доступ к предыдущему элементу в порядке, установленном <xsl:sort>   -  person Ian Roberts    schedule 29.01.2013
comment
@IanRoberts Да, это правда, но можно добиться (очень неэффективной) отсортированной группировки с чем-то вроде <xsl:for-each select="item[not(. = preceding-sibling::item)]"><xsl:sort select="." />...</xsl:for-each> Не могу понять, как это сделать с частичными значениями, так что это может даже не быть вариант в этом случае.   -  person JLRishe    schedule 29.01.2013


Ответы (2)


Это пример проблемы группировки, и в XSLT 1.0 общепринятым способом группировки является использование мюнхианской группировки. К сожалению, ваш сценарий требует поиска символов нижнего регистра поверх этого, а это немного запутанно в XSLT 1.0.

Тем не менее, я подготовил решение, и оно выглядит следующим образом:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="html" doctype-system="about:legacy-compat" 
              encoding="UTF-8" indent="yes" />

  <xsl:key name="kEntryInitial" match="entry/@term"
           use="translate(substring(., 1, 1), 
             'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 
             'abcdefghijklmnopqrstuvwxyz')"/>

  <xsl:template match="/">
    <html>
      <head></head>
      <body>
        <!-- Jump into the data.xml DOM so that keys work -->
        <xsl:apply-templates select="document('data.xml')/glossary" />
      </body>
    </html>
  </xsl:template>

  <xsl:template match="/glossary">
    <!-- Select terms with distinct initials (case invariant) -->
    <xsl:variable name="termsByDistinctInitial"
                  select="entry/@term[generate-id() = 
                             generate-id(key('kEntryInitial', 
                                            translate(substring(., 1, 1), 
                                            'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 
                                            'abcdefghijklmnopqrstuvwxyz'))[1])]" />

    <!-- Header -->
    <xsl:apply-templates select="$termsByDistinctInitial" mode="header">
      <xsl:sort select="." data-type="text" order="ascending" />
    </xsl:apply-templates>

    <!-- Glossary -->
    <dl>
      <xsl:apply-templates select="$termsByDistinctInitial" mode="main">
        <xsl:sort select="." data-type="text" order="ascending" />
      </xsl:apply-templates>
    </dl>
  </xsl:template>

  <xsl:template match="@term" mode="header">
    <xsl:variable name="initial">
      <xsl:call-template name="ToLower">
        <xsl:with-param name="value" select="substring(., 1, 1)" />
      </xsl:call-template>
    </xsl:variable>

    <a href="#{$initial}">
      <xsl:value-of select="$initial" />
    </a>
    <xsl:if test="position() != last()">
      <xsl:text> |</xsl:text>
    </xsl:if>
  </xsl:template>

  <xsl:template match="@term" mode="main">
    <xsl:variable name="initial">
      <xsl:call-template name="ToLower">
        <xsl:with-param name="value" select="substring(., 1, 1)" />
      </xsl:call-template>
    </xsl:variable>
    <a name="{$initial}">
      <h1>
        <xsl:value-of select="$initial" />
      </h1>
    </a>

    <xsl:apply-templates select="key('kEntryInitial', $initial)/.." />
  </xsl:template>

  <xsl:template match="entry">
    <dt>
      <xsl:apply-templates select="@term"/>
    </dt>
    <dd>
      <xsl:apply-templates select="@definition"/>
    </dd>
  </xsl:template>

  <xsl:template name="ToLower">
    <xsl:param name="value" />
    <xsl:value-of select="translate(substring($value, 1, 1), 
                      'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 
                      'abcdefghijklmnopqrstuvwxyz')"/>
  </xsl:template>
</xsl:stylesheet>

При запуске на входном XML это дает следующее:

<!DOCTYPE html SYSTEM "about:legacy-compat">
<html>
  <head>
    <META http-equiv="Content-Type" content="text/html; charset=utf-8">
  </head>
  <body><a href="#a">a</a> |<a href="#b">b</a> |<a href="#c">c</a> |<a href="#o">o</a>
    <dl><a name="a"><h1>a</h1></a><dt>apple</dt>
      <dd>A red fruit with seeds</dd>
      <dt>avocado</dt>
      <dd>A mellow fruit enjoyed in guacamole</dd><a name="b"><h1>b</h1></a><dt>banana</dt>
      <dd>A tropical yellow fruit</dd><a name="c"><h1>c</h1></a><dt>cantaloupe</dt>
      <dd>A kind of melon</dd>
      <dt>Cherry</dt>
      <dd>A red fruit that grows in clusters </dd>
      <dt>cranberry</dt>
      <dd>A sour berry enjoyed at Thanksgiving</dd><a name="o"><h1>o</h1></a><dt>orange</dt>
      <dd>An orange citrus fruit</dd>
    </dl>
  </body>
</html>

Одна вещь, которую я бы предложил рассмотреть, — это использование простого XSLT для «подготовки» вашего глоссария с инициалами:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" indent="yes"/>

  <xsl:template match="@* | node()">
    <xsl:copy>
      <xsl:apply-templates select="@* | node()" />
    </xsl:copy>
  </xsl:template>

  <xsl:template match="entry">
    <xsl:copy>
      <xsl:attribute name="initial">
        <xsl:value-of select="translate(substring(@term, 1, 1),
                                'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
                                'abcdefghijklmnopqrstuvwxyz')"/>
      </xsl:attribute>
      <xsl:apply-templates select="@* | node()" />
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

Это производит:

<glossary>
  <entry initial="c" term="cantaloupe" definition="A kind of melon" />
  <entry initial="b" term="banana" definition="A tropical yellow fruit" />
  <entry initial="a" term="apple" definition="A red fruit with seeds" />
  <entry initial="o" term="orange" definition="An orange citrus fruit" />
  <entry initial="c" term="Cherry" definition="A red fruit that grows in clusters " />
  <entry initial="c" term="cranberry" definition="A sour berry enjoyed at Thanksgiving" />
  <entry initial="a" term="avocado" definition="A mellow fruit enjoyed in guacamole" />
</glossary>

затем, если вы используете эту подготовленную версию в качестве глоссария, основной XSLT может избавиться от всех этих уродливых translate() функций и станет намного чище:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="html" doctype-system="about:legacy-compat" 
              encoding="UTF-8" indent="yes" />

  <xsl:key name="kEntryInitial" match="entry/@initial" use="."/>

  <xsl:template match="/">
    <html>
      <head></head>
      <body>
        <!-- Jump into the data.xml DOM so that keys work -->
        <xsl:apply-templates select="document('data2.xml')/glossary" />
      </body>
    </html>
  </xsl:template>

  <xsl:template match="/glossary">
    <!-- Select terms with distinct initials (case invariant) -->
    <xsl:variable name="termsByDistinctInitial"
                  select="entry/@initial[generate-id() = 
                             generate-id(key('kEntryInitial', .)[1])]" />

    <!-- Header -->
    <xsl:apply-templates select="$termsByDistinctInitial" mode="header">
      <xsl:sort select="." data-type="text" order="ascending" />
    </xsl:apply-templates>

    <!-- Glossary -->
    <dl>
      <xsl:apply-templates select="$termsByDistinctInitial" mode="main">
        <xsl:sort select="." data-type="text" order="ascending" />
      </xsl:apply-templates>
    </dl>
  </xsl:template>

  <xsl:template match="@initial" mode="header">
    <a href="#{.}">
      <xsl:value-of select="." />
    </a>
    <xsl:if test="position() != last()">
      <xsl:text> |</xsl:text>
    </xsl:if>
  </xsl:template>

  <xsl:template match="@initial" mode="main">
    <a name="{.}">
      <h1>
        <xsl:value-of select="." />
      </h1>
    </a>

    <xsl:apply-templates select="key('kEntryInitial', .)/.." />
  </xsl:template>

  <xsl:template match="entry">
    <dt>
      <xsl:apply-templates select="@term"/>
    </dt>
    <dd>
      <xsl:apply-templates select="@definition"/>
    </dd>
  </xsl:template>
</xsl:stylesheet>

Конечно, конечный результат такой же, как и в первом примере. Если ваш XSLT-процессор поддерживает функцию node-set(), также можно выполнить оба этих шага обработки в одном XSLT.

person JLRishe    schedule 29.01.2013

Необходимый вам метод называется группировка по Мюнху. Сначала определите ключ, который группирует элементы ввода по прописной первой букве их термина.

<xsl:key name="entryByInitial" match="entry" use="translate(substring(@term, 1, 1), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')" />

Затем вы используете трюк с generate-id, чтобы извлечь только первый элемент, соответствующий каждому ключу.

<xsl:for-each select="document('data.xml')">
  <!-- iterate over the "groups" to build the top links -->
  <xsl:for-each select="glossary/entry[generate-id() = generate-id(key('entryByInitial', translate(substring(@term, 1, 1), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'))[1])]">
    <xsl:sort select="translate(@term, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')" data-type="text" order="ascending"/>
    <xsl:variable name="initial" select="translate(substring(@term, 1, 1), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')" />
    <!-- insert a leading | before all but the first link -->
    <xsl:if test="position() &gt; 1"> | </xsl:if>
    <a href="#{$initial}"><xsl:value-of select="$initial" /></a>
  </xsl:for-each>

  <!-- iterate over the groups again -->
  <xsl:for-each select="glossary/entry[generate-id() = generate-id(key('entryByInitial', translate(substring(@term, 1, 1), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'))[1])]">
    <xsl:sort select="translate(@term, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')" data-type="text" order="ascending"/>
    <xsl:variable name="initial" select="translate(substring(@term, 1, 1), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')" />
    <a name="{$initial}"><h1><xsl:value-of select="$initial" /></h1></a>
    <dl>
      <!-- apply templates for all entries with this key value -->
      <xsl:apply-templates select="key('entryByInitial', $initial)">
        <xsl:sort select="translate(@term, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')" data-type="text" order="ascending"/>
      </xsl:apply-templates>
    </dl>
  </xsl:for-each>
</xsl:for-each>

и определить отдельный шаблон

<xsl:template match="entry">
  <dt><xsl:apply-templates select="@term"/></dt>
  <dd><xsl:apply-templates select="@definition"/></dd>
</xsl:template>
person Ian Roberts    schedule 29.01.2013