XML en XBRL processing met Scala en yaidom

Yaidom is een uniforme XML query/transformatie API, geschreven in Scala. De API maakt gebruik van Scala's Collections API en sluit er goed op aan. Daarnaast biedt yaidom diverse specifieke DOM-achtige implementaties achter dezelfde uniforme XML query API.

In dit artikel wordt de yaidom library geïntroduceerd, gebruik maken van voorbeelden uit XBRL (eXtensible Business Reporting Language).

Introductie

De ontwikkelaars bij EBPI maken vooral bedrijfskritische keteninformatiesystemen. Dit zijn organisatie-overstijgende informatiesystemen. Complexe gegevensverwerking is dan ook bekend terrein voor EBPI. Veel van de verwerkte data is in XML formaat (meestal conformerend aan een XML Schema). Een groot deel van deze XML data is in XBRL formaat. XBRL is een belangrijke standaard voor bedrijfsrapportages (zoals jaarrekeningen en belastingaangiften). Zie Getting Started for Developers voor een beknopte en heldere uitleg over de basis van XBRL. In deze blog gebruiken we XBRL als voorbeeld voor complexe XML processing. Voorkennis over XBRL wordt niet verondersteld om dit artikel te volgen. Basiskennis van XML en van de Scala programmeertaal worden wel verwacht.

XML processing wordt typisch gedaan door inzet van standaarden als XPath, XSLT en XQuery. Het fundament daarvan is het XPath Data Model, samen met o.a. de standaard functie library. XSLT en XQuery zijn complexe standaarden met een fikse leercurve, zeker als deze standaarden worden ingezet voor XBRL processing.

Ontwikkelen in Scala (en Java) rust daarentegen op een heel ander fundament, namelijk de JVM, de Scala (of Java) programmeertaal, en talloze Java en Scala libraries. Bij gebruik van Scala is de Scala Collections API vermoedelijk de belangrijkste van de ingezette Scala API's. Als we non-triviale XML processing doen op de JVM, hebben we dus typisch 2 expertise-sets nodig: ontwikkelexpertise op het Java platform, en (standaard) XML processing expertise, zoals aangegeven in de vorige alinea.

Wat nu als we een basis van Scala (en Java) development expertise zouden inzetten voor XML processing, zonder gebruik te maken van XPath, XSLT en/of XQuery? Zouden we op die basis nog maar een kleine brug nodig hebben om hiervan een krachtige XML processing stack te maken? Zo'n brug is inderdaad mogelijk, en is door EBPI ontwikkeld. De library heet yaidom. Yaidom is een XML query en transformatie API. Het is een alternatief voor Scala's eigen XML library, gekenmerkt door uitstekende interoperabiliteit met JAXP, Saxon(-EE) en andere XML "backends". Zie yaidom op GitHub en yaidom op GitHub pages. Het is de bedoeling dat de library spoedig wordt overgebracht naar het EBPI GitHub account, met behoud van de open source Apache 2.0 licentie.

In deze blog wordt XML processing met Scala en yaidom gedemonstreerd aan de hand van een XBRL voorbeeld. Dit voorbeeld is afkomstig van Charles Hoffman, ook wel bekend als de "vader van XBRL". De bestanden van het voorbeeld zijn te vinden op zijn site. In het vervolg van het artikel wordt getoond hoe Scala en yaidom kunnen helpen om XML processing in het algemeen en XBRL processing in het bijzonder eenvoudiger te maken. Er wordt vervolgens toegewerkt naar een voorbeeld van een validatie op het XBRL voorbeeld-rapport. Deze validatie is een zogenaamde "value assertion". Deze wordt gesimuleerd door Scala code, met gebruik van yaidom. Hoewel het doorgronden van "value assertions" in XBRL moeilijk is zonder voldoende basiskennis van XBRL, is de simulatie daarvan in Scala code eenvoudig te begrijpen voor Scala ontwikkelaars. Dat is dan ook het punt van deze blog: Scala en yaidom (met eventueel een "XML backend" als Saxon-EE voor o.a. type-informatie) kunnen complexe XML processing eenvoudiger maken (met name voor Scala ontwikkelaars).

Het vervolg van het artikel is als volgt georganiseerd:

  1. Een korte introductie tot "XBRL instanties"
  2. Enkele yaidom queries op de XBRL instantie
  3. Enkele yaidom "aspect-queries" op de XBRL instantie
  4. Een "value assertion" gesimuleerd in Scala
  5. Conclusie

Korte inleiding tot XBRL instanties

XBRL (eXtensible Business Reporting Language) is een standaard voor bedrijfsrapportages. Typische XBRL rapportages zijn financieel van aard. XBRL rapporten ("XBRL instanties") zijn XML documenten, die een gespecificeerde structuur volgen.

Stel dat we willen rapporteren dat voor een gegeven organisatie (CIK-nummer: 1234567890) het gemiddelde aantal werknemers in 2003 220 bedroeg, en dat de corresponderende aantallen in 2004 en 2005 respectievelijk 240 en 250 waren. Preciezer gezegd in XBRL terminologie, concept gaap:AverageNumberEmployees (beschreven door de zogenaamde US-GAAP taxonomie) heeft de waarde 220 in de gegeven context (CIK-nummer 1234567890, jaar 2003). Dan kunnen we bovenstaande 3 facts als volgt representeren in "XBRL instantie"-formaat:

<xbrl xmlns="http://www.xbrl.org/2003/instance" xmlns:gaap="http://xasb.org/gaap">

   <context id="D-2003">
      <entity>
         <identifier scheme="http://www.sec.gov/CIK">1234567890</identifier>
      </entity>
      <period>
         <startDate>2003-01-01</startDate>
         <endDate>2003-12-31</endDate>
      </period>
   </context>

   <context id="D-2004">
      <entity>
         <identifier scheme="http://www.sec.gov/CIK">1234567890</identifier>
      </entity>
      <period>
         <startDate>2004-01-01</startDate>
         <endDate>2004-12-31</endDate>
      </period>
   </context>

   <context id="D-2005">
      <entity>
         <identifier scheme="http://www.sec.gov/CIK">1234567890</identifier>
      </entity>
      <period>
         <startDate>2005-01-01</startDate>
         <endDate>2005-12-31</endDate>
      </period>
   </context>

   <unit id="U-Pure">
     <measure>pure</measure>
   </unit>

  <gaap:AverageNumberEmployees
    contextRef="D-2003"
    unitRef="U-Pure"
    decimals="INF">220</gaap:AverageNumberEmployees>
  <gaap:AverageNumberEmployees
    contextRef="D-2004"
    unitRef="U-Pure"
    decimals="INF">240</gaap:AverageNumberEmployees>
  <gaap:AverageNumberEmployees
    contextRef="D-2005"
    unitRef="U-Pure"
    decimals="INF">250</gaap:AverageNumberEmployees>

</xbrl>

Dit voorbeeld, geschreven door Charles Hoffman, komt van eerdergenoemde site. De rest van dit artikel werkt met deze voorbeeld XBRL instantie, die sterk lijkt op een uitgebreidere versie van bovenstaand voorbeeld. Yaidom wordt geïntroduceerd aan de hand van dit uitgebreide XBRL voorbeeld, ook al is yaidom een algemene XML library. Hoewel yaidom niet gebonden is aan het domein van XBRL, is XBRL wel een goed domein voor het tonen van de kracht van Scala en yaidom voor XML processing.

Eenvoudige XBRL queries met yaidom

Yaidom wordt nu geïntroduceerd door enkele eenvoudige queries op onze XBRL voorbeeld-instantie.

Voordat deze queries worden getoond, volgt nu eerst een korte introductie tot de basis van yaidom XML querying. We hoeven slechts 3 centrale yaidom query API functies te noemen, want de overige functies zijn daarna snel aan te leren. Deze 3 functies zijn filterChildElems, filterElems en filterElemsOrSelf.

Het volgende geldt voor deze 3 functies:

  1. Zij zijn functies die opereren op een XML element, en die een Scala Collection van elementen retourneren.
  2. Zij filteren elementen uit respectievelijk de collecties van child elementen, descendant elementen, en descendant-or-self elementen.
  3. Het woord "descendant" is consequent weggelaten uit de functienamen. Met dit in het achterhoofd zijn de functies eenvoudig te onthouden.
  4. Zij zijn higher-order functions, die voor het filteren een element-predicaat (dus een boolean functie op elementen) als parameter meekrijgen.
  5. Zij lijken sterk op respectievelijk de XPath assen child, descendant en descendant-or-self, maar retourneren collecties van element nodes, en niet van willekeurige XML nodes. Uiteraard kunnen die geretourneerde element nodes zelf weer andere soorten nodes bevatten, zoals text nodes.
  6. Functies filterElems en filterElemsOrSelf hebben beknopte en zeer precieze definities in termen van functie filterChildElems.
  7. De element nodes worden in document order teruggegeven. Merk op dat functies filterElems en filterElemsOrSelf typisch een aantal elementen retourneren die descendants zijn van andere geretourneerde elementen. Indien dit niet gewenst is, kunnen functies als findTopmostElems en findTopmostElemsOrSelf gebruikt worden.
  8. Goede interoperabiliteit met Scala Collections zit niet alleen in de geretourneerde Scala collecties, maar ook in het gebruik van Scala collecties binnen de implementaties van de functies.
  9. De yaidom XML element query API (waar deze 3 functies toe behoren) ondersteunt pluggable XML "backends". Dezelfde uniforme query API werkt voor native yaidom element-implementaties, maar bijvoorbeeld ook voor wrappers om Saxon tiny trees, W3C DOM trees en Scala XML trees.

Om de precisie van yaidom's query API te tonen, volgen hier de definities van filterElemsOrSelf en filterElems:

def filterElemsOrSelf(p: E => Boolean): immutable.IndexedSeq[E] = {
  val selfResult = immutable.IndexedSeq(this).filter(p)

  // Recursive calls
  selfResult ++ findAllChildElems.flatMap(_.filterElemsOrSelf(p))
}

def filterElems(p: E => Boolean): immutable.IndexedSeq[E] = {
  findAllChildElems.flatMap(_.filterElemsOrSelf(p))
}

def findAllChildElems: immutable.IndexedSeq[E] =
  filterChildElems(_ => true)

Voor functies filterChildElems en filterElemsOrSelf bestaan respectievelijk de afkortingen \ en \\. Functie attributeOption heeft afkorting \@. Daarnaast hebben enkele element-predicaten namen, zoals withLocalName en withEName. Het begrip EName staat daarbij voor "expanded name". Yaidom heeft precieze XML namespace ondersteuning, en maakt een scherp onderscheid tussen "expanded names" enerzijds en "qualified names" anderzijds. Zie Understanding XML Namespaces voor de basis van yaidom's ondersteuning van XML namespaces. Qualified names zijn de syntactische namen, en expanded names zijn datgene dat wordt gerepresenteerd door die syntactische namen. Slechts weinig XML libraries maken dit scherpe onderscheid. Dat yaidom dat wel doet heeft yaidom's namespace support aantoonbaar geholpen.

Sommige yaidom queries op de voorbeeld XBRL-instantie zijn als volgt, ongeacht de "element tree"-implementatie:

// Let variable "doc" be a document of any yaidom document type

val docElem = doc.documentElement

// Check that all gaap:AverageNumberEmployees facts have unit U-Pure.

val xmlNs = "http://www.w3.org/XML/1998/namespace"
val xbrliNs = "http://www.xbrl.org/2003/instance"
val gaapNs = "http://xasb.org/gaap"

val avgNumEmployeesFacts =
  docElem.filterChildElems(withEName(gaapNs, "AverageNumberEmployees"))

println(avgNumEmployeesFacts.size) // prints 10

val onlyUPure =
  avgNumEmployeesFacts.forall(
    _.attributeOption(EName("unitRef")) == Some("U-Pure"))
println(onlyUPure) // prints true

// Check the unit itself, minding the default namespace

val uPureUnit =
  docElem.getChildElem(e =>
    e.resolvedName == EName(xbrliNs, "unit") &&
      (e \@ EName("id")) == Some("U-Pure"))

println(uPureUnit.getChildElem(withEName(xbrliNs, "measure")).text) // prints "pure"

// Now we get the measure element text, as QName, resolving it to an EName
// (expanded name)
println(
  uPureUnit.getChildElem(withEName(xbrliNs, "measure")).textAsResolvedQName)
// prints EName(xbrliNs, "pure")

// Having the same unit, the gaap:AverageNumberEmployees facts are uniquely
// identified by contexts.

val avgNumEmployeesFactsByContext =
  avgNumEmployeesFacts.groupBy(
    _.attribute(EName("contextRef"))).mapValues(_.head)

println(avgNumEmployeesFactsByContext.keySet)
// prints:
// Set("D-2006", "D-2007", "D-2008", "D-2009", "D-2010", "D-2010-BS1", "D-2010-BS2",
//   "D-2010-CON", "D-2010-E", "D-2010-ALL")

println(avgNumEmployeesFactsByContext("D-2006").text) // prints 220

Yaidom is minder beknopt in het gebruik dan Scala's XML library. Het voordeel van yaidom is meer precisie. Elke yaidom query API functie heeft een kraakheldere definitie, meestal in termen van een "basis"-functie zoals filterChildElems, zoals we eerder zagen. Ook is "chaining" van query functies in yaidom niet mogelijk, terwijl Scala XML dit wel toelaat. Ook dit maakt yaidom preciezer dan Scala XML, omdat yaidom het onderscheid tussen individuele elementen enerzijds en collecties van elementen anderzijds niet onder tafel veegt.

Eenvoudige XBRL aspect queries met yaidom

In een XBRL instantie (als XML document) zien facts met hun contexts en eventuele units er nogal "syntactisch" en weinig "semantisch" uit. De essentie van een fact is immers verspreid over meerdere XML elementen die mogelijk ver uit elkaar staan (facts en hun contexts en eventuele units). Meer semantisch heeft een fact in een instantie naast een waarde een aantal aspects. Denk bijvoorbeeld aan:

  1. Het concept aspect. Dit is de expanded name van het fact element, en duidt het unieke concept aan met die expanded name. Het fact is dus een "instance" van dat concept.
  2. Het location aspect, dus het parent element van het fact. Dit is alleen interessant voor geneste facts.
  3. Het entity identifier aspect. Deze entity identifier vinden we terug in de XBRL context van dat fact. Het entity identifier aspect is de combinatie van het scheme attribuut en de waarde van het identifier element.
  4. Het period aspect. Ook deze vinden we terug in de XBRL context van dat fact, bijvoorbeeld als duration, dus als combinatie van startdatum en einddatum.
  5. Voor elke dimensie die we tegenkomen in de XBRL context van het fact, een dimension aspect. XBRL dimensions zijn een zeer belangrijk onderdeel van XBRL. Het gebruik van dimensions helpt om een explosie in het aantal benodigde concepten te voorkomen. Dimensions worden verder niet uitgelegd in dit artikel.
  6. Voor numerieke concepten, het unit aspect. Dit is de XBRL unit van het fact.

Deze (en andere) aspects vormen de basis voor moderne XBRL specificaties rond validaties ("formulas"). Daar zit ook de link met het laatste deel van dit artikel, omdat we daar aspects gaan gebruiken voor validatie. In deze sectie gaan we fact aspects opvragen met Scala en yaidom, zodat we in de volgende sectie aspecten kunnen gebruiken om validaties op XBRL instanties te coderen.

Hieronder volgt code om (sterk vereenvoudigd) fact aspects op te vragen. Variabele idoc bevat de XBRL instantie als zogenaamd "indexed" document. "Indexed" elementen kennen hun parent elementen, maar bieden daarnaast dezelfde yaidom query API. De kennis van de ancestor elementen benutten we voor het location aspect. De code, die gebonden is aan het gebruik van een specifieke element-implementatie, maar toch grotendeels de generieke yaidom element query API gebruikt, is als volgt:

val idocElem = idoc.documentElement

val contextsById: Map[String, indexed.Elem] =
  idocElem.filterChildElems(withEName(XbrliNs, "context")).
    groupBy(_.attribute(EName("id"))).mapValues(_.head)

val unitsById: Map[String, indexed.Elem] =
  idocElem.filterChildElems(withEName(XbrliNs, "unit")).
    groupBy(_.attribute(EName("id"))).mapValues(_.head)

// See http://www.xbrl.org/Specification/variables/REC-2009-06-22/.

def conceptAspect(fact: indexed.Elem): EName = fact.resolvedName

// Yaidom Paths wijzen een element binnen een element tree aan.

def locationAspect(fact: indexed.Elem): Path =
  fact.path.parentPathOption.getOrElse(Path.Root)

def entityIdentifierAspectOption(fact: indexed.Elem): Option[(String, String)] = {
  val contextOption =
    fact.attributeOption(EName("contextRef")).map(id => contextsById(id))

  val identifierOption =
    contextOption.flatMap(_.findElem(withEName(XbrliNs, "identifier")))
  val schemeOption =
    identifierOption.flatMap(_.attributeOption(EName("scheme")))
  val identifierValueOption =
    identifierOption.map(_.text)

  for {
    scheme <- schemeOption
    identifierValue <- identifierValueOption
  } yield (scheme, identifierValue)
}

def periodAspectOption(fact: indexed.Elem): Option[simple.Elem] = {
  val contextOption =
    fact.attributeOption(EName("contextRef")).map(id => contextsById(id))

  val periodOption =
    contextOption.flatMap(_.findElem(withEName(XbrliNs, "period")))
  periodOption.map(_.elem)
}

// Forgetting about complete segment, non-XDT segment, complete scenario and
// non-XDT scenario for now. Also ignoring typed dimensions.

def explicitDimensionAspects(fact: indexed.Elem): Map[EName, EName] = {
  val contextOption =
    fact.attributeOption(EName("contextRef")).map(id => contextsById(id))

  val memberElems =
    contextOption.toVector.flatMap(_.filterElems(
      withEName(XbrldiNs, "explicitMember")))
  memberElems.map(e =>
    (e.attributeAsResolvedQName(EName("dimension")) -> e.textAsResolvedQName)).toMap
}

// Convenience method

def explicitDimensionAspectOption(
  fact: indexed.Elem,
  dimension: EName): Option[EName] = {

  explicitDimensionAspects(fact).filterKeys(Set(dimension)).headOption.map(_._2)
}

def unitAspectOption(fact: indexed.Elem): Option[simple.Elem] = {
  val unitOption =
    fact.attributeOption(EName("unitRef")).map(id => unitsById(id))
  unitOption.map(_.elem)
}

// Compare aspects, naively, and without knowledge about XML Schema types.
// Period aspect comparisons are more tricky than this naive implementation suggests.
// Use equality on the results of the functions below for (period and unit) aspect
// comparisons.

def comparablePeriodAspectOption(fact: indexed.Elem): Option[Set[resolved.Elem]] = {
  periodAspectOption(fact).map(e =>
    e.findAllChildElems.map(che =>
      resolved.Elem(che).removeAllInterElementWhitespace).toSet)
}

def comparableUnitAspectOption(fact: indexed.Elem): Option[Set[resolved.Elem]] = {
  unitAspectOption(fact).map(e =>
    e.findAllChildElems.map(che =>
      resolved.Elem(che).removeAllInterElementWhitespace).toSet)
}

Enigszins onopvallend zijn hier nog 2 element-implementatie gebruikt, namelijk "simple" en "resolved" elements. Simple elements zijn de default native (immutable) element-implementatie van yaidom. Terwijl "indexed" elements iets toevoegen (aan "simple" elements), namelijk de ancestry als element context, laten "resolved" elements iets weg, namelijk namespace prefixes (de namespace URI's blijven behouden). Dat maakt deze element-implementatie een goede basis voor namespace-aware XML equality-vergelijkingen. Weer geldt dat de kern van de yaidom XML query API hetzelfde is, ook voor "resolved" elements.

Het valt op dat we met weinig Scala/yaidom code redelijk ver komen in de ondersteuning van fact aspects. Het was in dit geval beduidend minder natuurlijk geweest om in plaats hiervan XSLT of XQuery in te zetten. Dit illustreert het nut van Scala en yaidom voor XML processing, als mogelijk alternatief voor standaarden zoals XSLT. Hoe meer we Scala (en Java) API's willen inzetten voor een groot deel van de code, hoe aantrekkelijker deze alternatieve aanpak wordt. Dit voorkomt dat we in de code steeds moeten schakelen tussen 2 sterk verschillende paradigma's: Scala/Java enerzijds en het XPath Data Model anderzijds. De semantiek verschilt ook sterk tussen deze paradigma's. Zo zijn bijvoorbeeld XDM sequences totaal verschillend van Scala of Java collecties (een XDM item is hetzelfde als een singleton sequence met dat item, en sequences kunnen niet genest worden). Equality in XPath verschilt ook sterk van equality in Scala of Java.

Een value assertion, gesimuleerd in Scala code

Nu komen we bij een validatie terecht, die we op de XBRL instance kunnen loslaten. De validatieregel bevindt zich samen met andere validatieregels in US-GAAP formulas. Zonder verdere XBRL kennis zijn deze formulas niet of zeer moeilijk te doorgronden. Dat is geen probleem. We nemen 1 van deze formulas (een zogenaamde "value assertion"), en simuleren deze validatieregel in Scala code. Er wordt dus een validatieregel uitgelegd zonder dat we het origineel hoeven te begrijpen (of zelfs hoeven kunnen aan te wijzen in deze zogenaamde "formula linkbase"). Weer wordt getoond hoe Scala en yaidom (al dan niet met een Saxon "backend") een aantrekkelijke XML stack kunnen vormen.

Let wel: dit is een alternatieve representatie van een validatieregel voor educatieve doeleinden. In werkelijkheid worden XBRL formulas (waaronder value assertions) op een geheel andere manier verwerkt, al worden ook daar mogelijk Scala en yaidom als onderdeel van de oplossing ingezet.

De value assertion voert het equivalent uit van XPath expressie:

$v:VARIABLE_BalanceStart + $v:VARIABLE_Change = $v:VARIABLE_BalanceEnd

Daarbij wordt binnen de XBRL instantie geitereerd over combinaties van 3 bij elkaar horende facts: een "start balance", en "change" en een "end balance". Voor elk van die combinaties wordt het equivalent van de XPath expressie uitgevoerd als validatie-check. Alleen, welke facts worden ingevuld voor de 3 variabelen ("explicit filtering"), en welke combinaties van die facts worden beschouwd ("implicit filtering")? Dat is te zien in onderstaande code. "Implicit filtering" is gebaseerd op gelijkheid van zogenaamde "uncovered" aspecten. Dit zijn alle aspecten behalve de "covered" concept en period aspecten. (Deze 2 laatste aspecten zijn "covered" want zij zijn expliciet gebruikt om facts te filteren.) Hier volgt eerst een skelet van de code:

val balanceFacts =
  facts.filter(withEName(GaapNs, "CashAndCashEquivalentsPerCashFlowStatement"))

val changeFacts =
  facts.filter(withEName(GaapNs, "CashFlowNet"))

// Function mustBeEvaluated filters a combination of start balance, change and
// end balance, based on equality of so-called uncovered aspects.

// Function performAssertionTest evaluates one such combination of 3 facts.

val evalResults =
  for {
    startBalance <- balanceFacts
    change <- changeFacts
    endBalance <- balanceFacts
    if mustBeEvaluated(startBalance, change, endBalance)
  } yield {
    performAssertionTest(startBalance, change, endBalance)
  }

De meer complete code (voor educatieve doeleinden) is als volgt:

final case class EvaluationResult(
  val facts: Map[String, indexed.Elem], val result: Boolean) {

  override def toString: String = {
    s"EvaluationResult(result: $result, facts: ${facts.mapValues(_.elem)})"
  }
}

val topLevelFacts =
  idocElem.filterChildElems(e =>
    !Set(XbrliNs, LinkNs).contains(e.resolvedName.namespaceUriOption.getOrElse("")))
val facts = topLevelFacts.flatMap(_.findAllElemsOrSelf)

val balanceFacts =
  facts.filter(withEName(GaapNs, "CashAndCashEquivalentsPerCashFlowStatement"))

val changeFacts =
  facts.filter(withEName(GaapNs, "CashFlowNet"))

// Implicit filtering, to filter the cartesian product of 3 fact value spaces

def mustBeEvaluated(
  balanceStartFact: indexed.Elem,
  changeFact: indexed.Elem,
  balanceEndFact: indexed.Elem): Boolean = {

  // Compare on so-called uncovered aspects, so all ones except concept and period

  val currFacts = List(balanceStartFact, changeFact, balanceEndFact)

  val dimensions = currFacts.flatMap(e => explicitDimensionAspects(e).keySet).toSet

  currFacts.map(e => locationAspect(e)).distinct.size == 1 &&
    currFacts.map(e => entityIdentifierAspectOption(e)).distinct.size == 1 &&
    dimensions.forall(dim =>
      currFacts.map(e => explicitDimensionAspectOption(e, dim)).toSet.size == 1) &&
    currFacts.map(e => unitAspectOption(e)).distinct.size == 1 && {
      // Instant-duration
      // The comparison is naive, but still verbose

      import LocalDate.parse

      val balanceStartInstantOption =
        periodAspectOption(balanceStartFact).flatMap(
          _.findElem(withEName(XbrliNs, "instant"))).map(e => parse(e.text))
      val balanceEndInstantOption =
        periodAspectOption(balanceEndFact).flatMap(
          _.findElem(withEName(XbrliNs, "instant"))).map(e => parse(e.text))
      val changeStartOption =
        periodAspectOption(changeFact).flatMap(
          _.findElem(withEName(XbrliNs, "startDate"))).map(e => parse(e.text))
      val changeEndOption =
        periodAspectOption(changeFact).flatMap(
          _.findElem(withEName(XbrliNs, "endDate"))).map(e => parse(e.text))

      balanceStartInstantOption.isDefined && balanceEndInstantOption.isDefined &&
        changeStartOption.isDefined && changeEndOption.isDefined && {

        val balanceStart = balanceStartInstantOption.get
        val balanceEnd = balanceEndInstantOption.get
        val changeStart = changeStartOption.get
        val changeEnd = changeEndOption.get

        (balanceStart == changeStart || balanceStart.plusDays(1) == changeStart) &&
          (balanceEnd == changeEnd)
      }
    }
}

// The assertion test itself

def performAssertionTest(
  balanceStartFact: indexed.Elem,
  changeFact: indexed.Elem,
  balanceEndFact: indexed.Elem): EvaluationResult = {

  // Here we recognize the XPath expression shown earlier
  val result =
    balanceStartFact.text.toInt + changeFact.text.toInt == balanceEndFact.text.toInt

  EvaluationResult(
    Map(
      "startBalance" -> balanceStartFact,
      "change" -> changeFact,
      "endBalance" -> balanceEndFact), result)
}

// Executing the assertion

val evalResults =
  for {
    startBalance <- balanceFacts
    change <- changeFacts
    endBalance <- balanceFacts
    if mustBeEvaluated(startBalance, change, endBalance)
  } yield {
    performAssertionTest(startBalance, change, endBalance)
  }

assertResult(2) {
  evalResults.size
}
assertResult(true) {
  evalResults.forall(_.result)
}
assertResult(Set(
  Map("startBalance" -> 1000, "change" -> -1000, "endBalance" -> 0),
  Map("startBalance" -> -3000, "change" -> 4000, "endBalance" -> 1000))) {

  evalResults.map(_.facts.mapValues(_.text.toInt)).toSet
}

De uitdrukkingskracht van (eenvoudig gebruik van) Scala en de waarde van de yaidom library zijn duidelijk in dit voorbeeld. Yaidom is zelf in dit voorbeeld wat minder te zien, maar is vooral achter de schermen aanwezig, in de implementatie van de verschillende aspect-functies.

Conclusie

Concluderend kan gezegd worden: als je niet-triviale XML processing wilt doen, en daarbij (zonder omwegen) wilt profiteren van Scala en Java libraries, overweeg dan het gebruik van Scala plus de yaidom library als "XML stack", als alternatief voor standaarden als XSLT en XQuery. Yaidom in combinatie met Saxon-EE is een nog krachtiger XML stack, o.a. door Saxon's kennis van XML Schema types.

Scala's eigen XML library mag beknoptere code opleveren, maar het heeft niet de precisie van yaidom, met name met betrekking tot XML namespaces. Bovendien ondersteunt Scala XML geen wrappers rond bestaande volwassen Java XML libraries, en ook ondersteunt het niet meerdere "native" element-implementaties met verschillende sterke punten. Yaidom gaat zelfs nog verder in "extensibility". Het is zelfs mogelijk om type-safe XML dialecten te ondersteunen, zoals het dialect voor XBRL instanties. Dat is in dit artikel niet aan bod gekomen, maar dat maakt yaidom nog veel krachtiger en eleganter in het gebruik dan hier is getoond.

Yaidom is gepresenteerd bij XML London 2015. De library wordt bij EBPI intensief ingezet in productiecode. Daarnaast helpt yaidom bij snelle XML scripting taken. Yaidom helpt ons bij EBPI vooral om gelaagde modellen te bouwen waarmee we bijvoorbeeld XBRL processing robuust maar met een korte time-to-market kunnen realiseren.