Как сгруппировать последовательные даты в XSLT?

У меня есть файл xml (пример ниже), и я хочу сгруппировать этот xml на основе последовательного Time_Off_Date.

<Root>
  <Entry>
    <Employee_ID>101</Employee_ID>
    <Time_Off_Details>
      <Time_Off_Date>2017-12-01</Time_Off_Date>
    </Time_Off_Details>
    <Time_Off_Details>
      <Time_Off_Date>2017-12-02</Time_Off_Date>
    </Time_Off_Details>
    <Time_Off_Details>
      <Time_Off_Date>2017-12-04</Time_Off_Date>
    </Time_Off_Details>
    <Time_Off_Details>
      <Time_Off_Date>2017-12-05</Time_Off_Date>
    </Time_Off_Details> 
  </Entry>
  <Entry>
    <Employee_ID>102</Employee_ID>
    <Time_Off_Details>
      <Time_Off_Date>2017-12-10</Time_Off_Date>
    </Time_Off_Details>
    <Time_Off_Details>
      <Time_Off_Date>2017-12-13</Time_Off_Date>
    </Time_Off_Details>
    <Time_Off_Details>
      <Time_Off_Date>2017-12-14</Time_Off_Date>
    </Time_Off_Details>
  </Entry>
</Root>

Конечный результат должен выглядеть так (в формате CSV).

Employee ID   Time Off Start  Time Off End
101           12/1/2017       12/2/2017
101           12/4/2017       12/5/2017
102           12/10/2017      12/10/2017
102           12/13/2017      12/14/2017

Есть ли способ добиться этого с помощью XSLT 2.0 и без использования рекурсивных функций?? Я новичок в XSLT, поэтому любые советы приветствуются.


person Ankita    schedule 11.02.2018    source источник
comment
Зачем использовать один и тот же тег для даты начала и окончания? Почему бы не иметь один <Time_Off_Details>, а там <Start_Date> и <End_Date>?! И, конечно же, у вашего Сотрудника 102 нечетное количество дат, и мы должны догадаться, что 10-е число является дубликатом...   -  person Alexis Wilke    schedule 12.02.2018
comment
Если вы просто хотите рассматривать даты в нечетных позициях как даты начала, а даты в четных позициях — как даты окончания, это легко и не требует группировки. Но когда у вас есть три даты и вы произвольно решаете, что одна из них является датой начала и окончания, я не знаю, какую логику вы применяете.   -  person Michael Kay    schedule 12.02.2018
comment
Является ли логика в том, что входной XML содержит только отдельные выходные дни (то есть один день за раз), и вы хотите сгруппировать эти отдельные дни, когда они идут подряд?   -  person Tim C    schedule 12.02.2018
comment
@AlexisWilke Вот как Workday (система, из которой я получаю данные xml) создает файл xml. Было бы проще, если бы файл xml имел разные теги ??   -  person Ankita    schedule 12.02.2018


Ответы (3)


Это можно прекрасно выразить в XQuery 3 с помощью предложения переворачивающегося окна (https://www.w3.org/TR/xquery-31/#id-tumbling-windows):

for $entry in Root/Entry
for tumbling window $date in $entry//Time_Off_Date/xs:date(.)
start $s when true()
end $e next $n when $n - $e gt xs:dayTimeDuration('P1D')
return string-join(($entry/Employee_ID, $date[1], $date[last()]), '&#9;')

http://xqueryfiddle.liberty-development.net/6qM2e25

Поскольку процессоры XSLT 2, такие как Saxon 9 или XmlPrime, также поддерживают XQuery, это может быть альтернативой использованию XSLT.

Для XSLT вам может понадобиться объяснить, почему вы не хотите использовать рекурсивную функцию.

person Martin Honnen    schedule 12.02.2018
comment
Привет, Мартин! Продукт, над которым я работаю, не позволяет мне использовать рекурсивные функции, поэтому мне придется искать альтернативные решения. - person Ankita; 12.02.2018

Если логика заключается в том, что входной XML содержит только отдельные выходные дни, и вы хотите сгруппировать эти отдельные дни, когда они идут подряд, то вы можете использовать xsl:for-each-group для выбора Time_Off_Details с group-starting-with, установленным для элементов, где Time_Off_Date не является последовательным с предыдущим элементом.

Попробуйте этот XSLT

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
                xmlns:xs="http://www.w3.org/2001/XMLSchema"
                version="2.0">

  <xsl:output method="text" />
  <xsl:strip-space elements="*" />

  <xsl:template match="Entry">
    <xsl:for-each-group select="Time_Off_Details" 
                        group-starting-with="*[not(xs:date(Time_Off_Date) = xs:date(preceding-sibling::*[1]/Time_Off_Date) + xs:dayTimeDuration('P1D'))]">
        <xsl:value-of select="../Employee_ID" />
        <xsl:text>,</xsl:text>
        <xsl:value-of select="Time_Off_Date" />
        <xsl:text>,</xsl:text>
        <xsl:value-of select="current-group()[last()]/Time_Off_Date" />
        <xsl:text>&#10;</xsl:text>
    </xsl:for-each-group>
  </xsl:template>
</xsl:stylesheet>
person Tim C    schedule 12.02.2018
comment
Большое спасибо за решение. Это сработало как шарм! - person Ankita; 12.02.2018

Ваша задача может быть выполнена в XSLT 2.0 с помощью for-each-group.

Сначала вам нужно отсортировать все элементы Time_Off_Date по их полному содержимому.

Каждая группа начинается с элемента Time_Off_Date, для которого не существует другого элемента Time_Off_Date с содержимым, равным предыдущей дате по сравнению с текущей датой.

Чтобы вычислить предыдущую дату в виде строки, необходима следующая последовательность:

  • Возьмите текущую дату.
  • Вычтите период 1 день.
  • Отформатируйте его как yyyy-mm-dd.

Затем для каждой группы нужно:

  • Прочитайте дату у первого члена группы.
  • Прочитайте дату от последнего члена группы.
  • Распечатайте Employee_ID и обе даты в нужном формате.

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

<?xml version="1.0" encoding="UTF-8" ?>
<xsl:transform version="2.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xsl:output method="text"/>

  <xsl:template match="Root">
    <xsl:text>Employee ID,Time Off Start,Time Off End&#xA;</xsl:text>
      <xsl:for-each-group select="Entry/Time_Off_Details/Time_Off_Date"
        group-starting-with=".[not(//Entry/Time_Off_Details/Time_Off_Date[. =
          format-date(xs:date(current()) - xs:dayTimeDuration('P1D'),
          '[Y0001]-[M01]-[D01]')])]">
        <xsl:sort select="."/>
        <xsl:variable name="startDate" select="current-group()[1]"/>
        <xsl:variable name="lastDate" select="current-group()[last()]"/>
        <xsl:value-of select="../../Employee_ID"/>
        <xsl:text>,</xsl:text>
        <xsl:value-of select="format-date($startDate,'[M01]/[D1]/[Y0001]')"/>
        <xsl:text>,</xsl:text>
        <xsl:value-of select="format-date($lastDate,'[M01]/[D1]/[Y0001]')"/>
        <xsl:text>&#xA;</xsl:text>
      </xsl:for-each-group>
  </xsl:template>
</xsl:transform>
person Valdi_Bo    schedule 12.02.2018
comment
Большое спасибо за решение! Ценю твою помощь. - person Ankita; 12.02.2018