so

My first real XProc 3.0 pipeline

Volume 5, Issue 27; 25 Oct 2021

It is a funny irony that a language implementor does not necessarily write the most interesting examples of the language they’re implementing.

I have written at least thousands of pipelines over the past few months and years, perhaps many more. But they have all been aggressively simple. They have been designed exclusively to test whether I’ve implemented this particular attribute correctly or if, presented with a pipeline that has this error, do I catch it, and do I generate the correct error code?

The shorter, simpler, and more contrived the better! If I can reduce the pipeline to a single step that fails to compile, that’s ideal.

But it isn’t interesting except maybe in a very narrow sense. It might be academically interesting or interesting to the maintainers of a test suite, but such pipelines don’t do anything interesting.

I am very, very pleased to say that I feel like I’m approaching a point where it is possible and amusing to write more interesting pipelines for XML Calabash 3.x to run. One of these days real soon now, there might even be a release you want to use. (Though not yet in production, I hasten to add.)

XProc 3.0 is a lot more interesting and useful than 1.0 ever was. The ability to process non-XML documents as easily as XML documents opens up a lot of doors.

To that end, I pulled together a Gradle plugin for XML Calabash 3.x.

The fact that, for example, you can process JSON documents with XProc means that, for a plugin like this one, you can simply use a Groovy mapYes, Groovy has yet another syntax for maps. Go figure. You can write Gradle in Kotlin as well, but I haven’t had a chance to investigate that. as an input:

input "source", ["map": true, "representation": "json"]

That example “just works”. The plugin knows that a map can be represented as JSON so it passes a JSON document to XML Calabash. The plugin can also deal with text and XML documents, of course.

This feature got me thinking about some kind of more interesting example pipeline. What I came up with is a little contrived, but it does actually do something!

<p:declare-step xmlns:p="http://www.w3.org/ns/xproc"
                xmlns:map="http://www.w3.org/2005/xpath-functions/map"
                xmlns:xs="http://www.w3.org/2001/XMLSchema"
                name="main" expand-text="true"
                version="3.0">
<p:input port="source">
  <p:inline content-type="application/json" expand-text="false">
    {
      "new-years-day":  { "title": "New Year’s Day",
                          "date":  "01-01" },
      "valentines-day": { "title": "Valentine’s Day"
                        , "date":  "02-14" },
      "earth-day":      { "title": "Earth Day",     
                          "date":  "1970-04-22" },
      "shakespeare":    { "title": "Shakespeare Day"
                        , "date":  "04-23" },
      "halloween":      { "title": "Halloween",     
                          "date":  "10-31" },
      "christmas":      { "title": "Christmas",     
                          "date":  "12-25" },
      "boxing-day":     { "title": "Boxing Day",    
                          "date":  "12-26" }
    }
  </p:inline>
</p:input>
<p:output port="result"
          serialization="map { 'method': 'text' }"/>

<p:option name="event" select="'christmas'"/>

<p:variable name="selected-event"
            as="map(*)?"
            select="map:get(., $event)"/>

<p:choose>
  <p:when test="empty($selected-event)">
    <p:identity>
      <p:with-input>
        <p:inline>Unknown event: {$event}</p:inline>
      </p:with-input>
    </p:identity>
  </p:when>
  <p:otherwise>
    <p:variable name="title" select="$selected-event?title"/>
    <p:variable name="this-year" select="year-from-date(current-date())"/>
    <p:variable name="date" as="xs:date"
                select="if ($selected-event?date castable as xs:date)
                        then xs:date($selected-event?date)
                        else xs:date($this-year || '-' || $selected-event?date)"/>
    <p:variable name="date-in-this-year" as="xs:date"
                select="if (year-from-date($date) = $this-year)
                        then $date
                        else xs:date($this-year || substring(string($date), 5))"/>
    <p:variable name="next-date" as="xs:date"
                select="if ($date-in-this-year lt current-date())
                        then $date-in-this-year + xs:yearMonthDuration('P1Y')
                        else $date-in-this-year"/>
    <p:variable name="days"
                select="days-from-duration($next-date - current-date())"/>
    <p:choose>
      <p:when test="$days = 0">
        <p:identity>
          <p:with-input>
            <p:inline>Today is {$title}!</p:inline>
          </p:with-input>
        </p:identity>
      </p:when>
      <p:when test="$days = 1">
        <p:identity>
          <p:with-input>
            <p:inline>Tomorrow is {$title}!</p:inline>
          </p:with-input>
        </p:identity>
      </p:when>
      <p:otherwise>
        <p:choose>
          <p:when test="$date-in-this-year = $date">
            <p:identity>
              <p:with-input>
                <p:inline>It will be {$title} in {$days} days.</p:inline>
              </p:with-input>
            </p:identity>
          </p:when>
          <p:otherwise>
            <p:variable name="ann"
                        select="year-from-date($next-date) - year-from-date($date)"/>
            <p:identity>
              <p:with-input>
                <p:inline>In {$days} days it will be {$ann} years since {$title}.</p:inline>
              </p:with-input>
            </p:identity>
          </p:otherwise>
        </p:choose>
      </p:otherwise>
    </p:choose>
  </p:otherwise>
</p:choose>

</p:declare-step>

It takes as input a JSON document identifying a number of significant dates. If you then pass it an option that identifies one of those dates, it will tell you how many days until the next one.

There are, for example, 61 days until Christmas.

On the one hand, examples like this are kind of cool. On the other, you have to wonder if the reader isn’t going to think “the [expletive] is wrong with you, why would you use XProc for that?”

“Because I can.”

On a slightly less flippant note, I will say that I use XPath almost exclusively for this kind of calculation. Yes, I know you can do it in Python or Perl (and probably Bash if you’re sufficiently determined), but I prefer XPath. Why? Because with XPath, I don’t have to remember that the days of the month are one based, months are zero based, and years are, what, 1900 based or something? Give me

xs:date("2021-12-25") - current-date()

any day (no pun intended).