:fire: Remove code.

:fire: Remove code.

import module namespace archives = "http://28.io/modules/xbrl/archives";
import module namespace entities = "http://28.io/modules/xbrl/entities";
import module namespace hypercubes = "http://28.io/modules/xbrl/hypercubes";
import module namespace conversion = "http://28.io/modules/xbrl/conversion";
import module namespace reports = "http://28.io/modules/xbrl/reports";
import module namespace components = "http://28.io/modules/xbrl/components";
import module namespace labels = "http://28.io/modules/xbrl/labels";

import module namespace multiplexer = "http://28.io/modules/xbrl/profiles/multiplexer";
import module namespace fiscal-core = "http://28.io/modules/xbrl/profiles/sec/fiscal/core";

import module namespace config = "http://apps.28.io/config";
import module namespace session = "http://apps.28.io/session";
import module namespace api = "http://apps.28.io/api";

declare option rest:response "first-item";

(: Query parameters :)
declare  %rest:case-insensitive                 variable $token         as string? external;
declare  %rest:env                              variable $request-uri   as string  external;
declare  %rest:case-insensitive                 variable $format        as string? external;
declare  %rest:case-insensitive %rest:distinct  variable $cik           as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $edinetcode    as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $tag           as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $ticker        as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $sic           as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $fiscalYear    as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $fiscalPeriod  as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $fiscalPeriodType  as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $eid           as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $aid           as string* external;
declare  %rest:case-insensitive                 variable $validate      as boolean external := false;
declare  %rest:case-insensitive                 variable $labels        as boolean external := true;
declare  %rest:case-insensitive                 variable $report        as string? external;
declare  %rest:case-insensitive                 variable $profile-name  as string  external := $config:profile-name;
declare  %rest:case-insensitive                 variable $language      as string  external := "en-US";
declare  %rest:case-insensitive                 variable $count         as boolean external := false;
declare  %rest:case-insensitive                 variable $top           as nonNegativeInteger? external := ();
declare  %rest:case-insensitive                 variable $skip          as nonNegativeInteger? external := ();
declare  %rest:case-insensitive                 variable $most-recent-version-aspect as string external :=
    switch($profile-name)
    case "sec" return "sec:Accepted"
    case "japan" return "fsa:Submitted"
    default return "NONE";

(: Post-processing :)
let $format as string? := api:preprocess-format($format, $request-uri)
let $fiscalYear as integer* := api:preprocess-fiscal-years($fiscalYear)
let $fiscalPeriod as string* := api:preprocess-fiscal-periods($fiscalPeriod)
let $fiscalPeriodType as string* := api:preprocess-fiscal-period-types($fiscalPeriodType)
let $tag as string* := api:preprocess-tags($tag)

let $cik as string* :=
    switch($profile-name)
    case "sec" return $cik
    case "japan" return $edinetcode
    default return ()

(: Entity resolution :)
let $entities := multiplexer:entities(
  $profile-name,
  $eid,
  $cik,
  $tag,
  $ticker,
  $sic,
  (),
  (),
  (),
  ())

let $entities-not-found as boolean :=
  exists(($eid, $cik, $tag, $ticker, $sic)) and empty($entities)

let $report-id as string? := $report
let $report as object? := reports:reports($report-id)

let $report-not-found as boolean := empty($report)

(: Fact resolution :)
let $filter-override as object? :=
    if($profile-name eq "sec")
    then fiscal-core:filter-override(
        $entities,
        $fiscalYear,
        $fiscalPeriod,
        $fiscalPeriodType,
        $aid)
    else components:filter-override($entities, $aid)

let $hypercube := hypercubes:hypercubes-for-components($report)[1]

let $facts as object* :=
    let $filtered-aspects := values($hypercube.Aspects)[exists(($$.Members, $$.DomainRestriction))]
    return if(count($filtered-aspects) lt $config:filtered-aspects and not exists(($filter-override)))
    then {
          { status: 403 },
          session:error("The report filters are too weak, which leads to too big an output.", $format)
    } else if (exists($report))
    then components:facts(
                $report,
                $hypercube.Name,
                {|
                    { FilterOverride: $filter-override }[exists($filter-override)],
                    { Validate: $validate },
                    { MostRecentVersionAspect: $most-recent-version-aspect }[$most-recent-version-aspect ne "NONE"]
                |}
            )
    else ()

let $concepts as object* := $report.Concepts[]
let $language as string := ( $language, $report.$components:DEFAULT-LANGUAGE , $labels:AMERICAN_ENGLISH )[1]
let $facts :=
  if($profile-name eq "sec")
  then
    for $fact in $facts
    group by $archive := $fact.Aspects."xbrl28:Archive"
    let $archive := archives:archives($archive)
    let $entity := entities:entities($archive.Entity)
    for $fact in $fact
    return
    {|
        trim($fact, ("Labels", "EntityRegistrantName")),
        { "EntityRegistrantName" : $entity.Profiles.SEC.CompanyName },
        if($labels)
        then
            let $labels as object? := labels:labels-for-facts(
              $fact,
              $labels:STANDARD_LABEL_ROLE,
              $language,
              $concepts,
              $entities,
              ()
            )
            return
                { Labels : $labels }
        else ()
    |}
  else
    for $fact in $facts
    let $labels := labels:labels-for-facts(
      $fact,
      $labels:STANDARD_LABEL_ROLE,
      $language,
      $concepts,
      $entities,
      ()
    )
    return
    {|
        trim($fact, "Labels"),
        { Labels : $labels }[exists($labels)]
    |}

let $facts := api:normalize-facts($facts)

let $result := api:top-skip-count("FactTable", $facts, $count, $top, $skip)

let $result := {|
  switch($profile-name)
  case "sec" return {
     NetworkIdentifier : $report.Role,
     Archive : $report.Archive,
     Role : $report.Role,
     TableName : $hypercube.Name,
     Label : $report.Label
  }
  default return {
     Archive : $report.Archive,
     Role : $report.Role,
     TableName : $hypercube.Name,
     Label : $report.Label
  },
  $result
|}

let $comment := {
    NumFacts : count($facts),
    TotalNumFacts: session:num-facts(),
    TotalNumArchives: session:num-archives(),
    TotalNumEntities: session:num-entities()
}

let $serializers := {
    to-xml : function($res as object) as node() {
        switch($profile-name)
        case "sec" return <FactTable NetworkIdentifier="http://bizql.io/facttable-for-report"
          TableName="xbrl:FactTableForReport">{
            conversion:facts-to-xml($facts, { Caller: "Report" })
        }</FactTable>
        default return <FactTable
            tableName="{$res.TableName}"
            label="{$res.Label}"
            archive="{$res.Archive}"
            role="{$res.Role}"
            >{
            conversion:facts-to-xml($res.FactTable[], { Caller: "Report" })
        }</FactTable>
    },
    to-csv : function($res as object) as string {
        string-join(conversion:facts-to-csv($res.FactTable[], { Caller: "Report"}))
    },
    to-odata : function($res as object) as node()* {
        <feed xmlns='http://www.w3.org/2005/Atom'
            xmlns:m='http://docs.oasis-open.org/odata/ns/metadata'
            xmlns:d='http://docs.oasis-open.org/odata/ns/data'
            xml:base='http://www.28.io/'>
        <id>{"odata:transient:"|| current-dateTime() }</id>
        <title type='text'>secxbrl.info facts</title>
        <updated>{current-dateTime()}</updated>
       {
            let $next := $res."FactTable@odata.nextLink"
            where exists($next)
            return <link rel="next">{$next}</link>,
            let $id := $res."FactTable@odata.id"
            where exists($id)
            return <link rel="self">{$id}</link>,
            conversion:facts-to-odata(
              $res.FactTable[],
              $hypercube,
              {
                token: $token,
                most-recent-version-aspect: $most-recent-version-aspect
              })
       }
       </feed>
    }
}

let $results := api:serialize($result, $comment, $serializers, $format, "facts")
return switch(true)
       case $entities-not-found return api:not-found("entity")
       case $report-not-found return api:not-found("report")
       default return api:check-and-return-results($token, $results, $format)

import module namespace archives = "http://28.io/modules/xbrl/archives";
import module namespace entities = "http://28.io/modules/xbrl/entities";
import module namespace hypercubes = "http://28.io/modules/xbrl/hypercubes";
import module namespace conversion = "http://28.io/modules/xbrl/conversion";
import module namespace reports = "http://28.io/modules/xbrl/reports";
import module namespace components = "http://28.io/modules/xbrl/components";
import module namespace labels = "http://28.io/modules/xbrl/labels";

import module namespace multiplexer = "http://28.io/modules/xbrl/profiles/multiplexer";
import module namespace fiscal-core = "http://28.io/modules/xbrl/profiles/sec/fiscal/core";

import module namespace config = "http://apps.28.io/config";
import module namespace session = "http://apps.28.io/session";
import module namespace api = "http://apps.28.io/api";

declare option rest:response "first-item";

(: Query parameters :)
declare  %rest:case-insensitive                 variable $token         as string? external;
declare  %rest:env                              variable $request-uri   as string  external;
declare  %rest:case-insensitive                 variable $format        as string? external;
declare  %rest:case-insensitive %rest:distinct  variable $cik           as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $edinetcode    as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $tag           as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $ticker        as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $sic           as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $fiscalYear    as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $fiscalPeriod  as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $fiscalPeriodType  as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $eid           as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $aid           as string* external;
declare  %rest:case-insensitive                 variable $validate      as boolean external := false;
declare  %rest:case-insensitive                 variable $labels        as boolean external := true;
declare  %rest:case-insensitive                 variable $report        as string? external;
declare  %rest:case-insensitive                 variable $profile-name  as string  external := $config:profile-name;
declare  %rest:case-insensitive                 variable $language      as string  external := "en-US";
declare  %rest:case-insensitive                 variable $count         as boolean external := false;
declare  %rest:case-insensitive                 variable $top           as nonNegativeInteger? external := ();
declare  %rest:case-insensitive                 variable $skip          as nonNegativeInteger? external := ();
declare  %rest:case-insensitive                 variable $most-recent-version-aspect as string external :=
    switch($profile-name)
    case "sec" return "sec:Accepted"
    case "japan" return "fsa:Submitted"
    default return "NONE";

(: Post-processing :)
let $format as string? := api:preprocess-format($format, $request-uri)
let $fiscalYear as integer* := api:preprocess-fiscal-years($fiscalYear)
let $fiscalPeriod as string* := api:preprocess-fiscal-periods($fiscalPeriod)
let $fiscalPeriodType as string* := api:preprocess-fiscal-period-types($fiscalPeriodType)
let $tag as string* := api:preprocess-tags($tag)

let $cik as string* :=
    switch($profile-name)
    case "sec" return $cik
    case "japan" return $edinetcode
    default return ()

(: Entity resolution :)
let $entities := multiplexer:entities(
  $profile-name,
  $eid,
  $cik,
  $tag,
  $ticker,
  $sic,
  (),
  (),
  (),
  ())

let $entities-not-found as boolean :=
  exists(($eid, $cik, $tag, $ticker, $sic)) and empty($entities)

let $report-id as string? := $report
let $report as object? := reports:reports($report-id)

let $report-not-found as boolean := empty($report)

(: Fact resolution :)
let $filter-override as object? :=
    if($profile-name eq "sec")
    then fiscal-core:filter-override(
        $entities,
        $fiscalYear,
        $fiscalPeriod,
        $fiscalPeriodType,
        $aid)
    else components:filter-override($entities, $aid)

let $hypercube := hypercubes:hypercubes-for-components($report)[1]

let $facts as object* :=
    let $filtered-aspects := values($hypercube.Aspects)[exists(($$.Members, $$.DomainRestriction))]
    return if(count($filtered-aspects) lt $config:filtered-aspects and not exists(($filter-override)))
    then {
          { status: 403 },
          session:error("The report filters are too weak, which leads to too big an output.", $format)
    } else if (exists($report))
    then components:facts(
                $report,
                $hypercube.Name,
                {|
                    { FilterOverride: $filter-override }[exists($filter-override)],
                    { Validate: $validate },
                    { MostRecentVersionAspect: $most-recent-version-aspect }[$most-recent-version-aspect ne "NONE"]
                |}
            )
    else ()

let $concepts as object* := $report.Concepts[]
let $language as string := ( $language, $report.$components:DEFAULT-LANGUAGE , $labels:AMERICAN_ENGLISH )[1]
let $facts :=
  if($profile-name eq "sec")
  then
    for $fact in $facts
    group by $archive := $fact.Aspects."xbrl28:Archive"
    let $archive := archives:archives($archive)
    let $entity := entities:entities($archive.Entity)
    for $fact in $fact
    return
    {|
        trim($fact, ("Labels", "EntityRegistrantName")),
        { "EntityRegistrantName" : $entity.Profiles.SEC.CompanyName },
        if($labels)
        then
            let $labels as object? := labels:labels-for-facts(
              $fact,
              $labels:STANDARD_LABEL_ROLE,
              $language,
              $concepts,
              $entities,
              ()
            )
            return
                { Labels : $labels }
        else ()
    |}
  else
    for $fact in $facts
    let $labels := labels:labels-for-facts(
      $fact,
      $labels:STANDARD_LABEL_ROLE,
      $language,
      $concepts,
      $entities,
      ()
    )
    return
    {|
        trim($fact, "Labels"),
        { Labels : $labels }[exists($labels)]
    |}

let $facts := if(exists($skip))
              then $facts[position() gt $skip]
              else $facts

let $facts := if(exists($top))
              then $facts[position() le $top]
              else $facts

let $facts := api:normalize-facts($facts)

let $result := api:top-skip-count("FactTable", $facts, $count, $top, $skip)

let $result := {|
  switch($profile-name)
  case "sec" return {
     NetworkIdentifier : $report.Role,
     Archive : $report.Archive,
     Role : $report.Role,
     TableName : $hypercube.Name,
     Label : $report.Label
  }
  default return {
     Archive : $report.Archive,
     Role : $report.Role,
     TableName : $hypercube.Name,
     Label : $report.Label
  },
  $result
|}

let $comment := {
    NumFacts : count($facts),
    TotalNumFacts: session:num-facts(),
    TotalNumArchives: session:num-archives(),
    TotalNumEntities: session:num-entities()
}

let $serializers := {
    to-xml : function($res as object) as node() {
        switch($profile-name)
        case "sec" return <FactTable NetworkIdentifier="http://bizql.io/facttable-for-report"
          TableName="xbrl:FactTableForReport">{
            conversion:facts-to-xml($facts, { Caller: "Report" })
        }</FactTable>
        default return <FactTable
            tableName="{$res.TableName}"
            label="{$res.Label}"
            archive="{$res.Archive}"
            role="{$res.Role}"
            >{
            conversion:facts-to-xml($res.FactTable[], { Caller: "Report" })
        }</FactTable>
    },
    to-csv : function($res as object) as string {
        string-join(conversion:facts-to-csv($res.FactTable[], { Caller: "Report"}))
    },
    to-odata : function($res as object) as node()* {
        <feed xmlns='http://www.w3.org/2005/Atom'
            xmlns:m='http://docs.oasis-open.org/odata/ns/metadata'
            xmlns:d='http://docs.oasis-open.org/odata/ns/data'
            xml:base='http://www.28.io/'>
        <id>{"odata:transient:"|| current-dateTime() }</id>
        <title type='text'>secxbrl.info facts</title>
        <updated>{current-dateTime()}</updated>
       {
            let $next := $res."FactTable@odata.nextLink"
            where exists($next)
            return <link rel="next">{$next}</link>,
            let $id := $res."FactTable@odata.id"
            where exists($id)
            return <link rel="self">{$id}</link>,
            conversion:facts-to-odata(
              $res.FactTable[],
              $hypercube,
              {
                token: $token,
                most-recent-version-aspect: $most-recent-version-aspect
              })
       }
       </feed>
    }
}

let $results := api:serialize($result, $comment, $serializers, $format, "facts")
return switch(true)
       case $entities-not-found return api:not-found("entity")
       case $report-not-found return api:not-found("report")
       default return api:check-and-return-results($token, $results, $format)


Fixed bug with cardinality.

Fixed bug with cardinality.

import module namespace util = "http://secxbrl.info/modules/util";

import module namespace request = "http://www.28msec.com/modules/http-request";
import module namespace session = "http://apps.28.io/session";
import module namespace entities = "http://28.io/modules/xbrl/entities";
import module namespace hypercubes = "http://28.io/modules/xbrl/hypercubes";
import module namespace components = "http://28.io/modules/xbrl/components";
import module namespace reports = "http://28.io/modules/xbrl/reports";
import module namespace concept-maps = "http://28.io/modules/xbrl/concept-maps";

import module namespace companies = "http://28.io/modules/xbrl/profiles/sec/companies";
import module namespace fiscal-core = "http://28.io/modules/xbrl/profiles/sec/fiscal/core";

import module namespace mongo = "http://www.28msec.com/modules/mongodb";
import module namespace credentials = "http://www.28msec.com/modules/credentials";

import module namespace csv = "http://zorba.io/modules/json-csv";
import module namespace seq = "http://zorba.io/modules/sequence";

declare namespace concepts = "http://www.28msec.com/modules/bizql/concepts";

declare function local:to-csv($concepts as item*, $onlyNames as boolean) as string
{
    if ($onlyNames)
    then
        string-join(("Name", $concepts), "
")
    else
        string-join(csv:serialize($concepts, { serialize-null-as : "" }))
};

declare function local:to-xml($concepts as item*, $onlyNames as boolean) as element()*
{
    for $c in $concepts
    return
        <ReportElement>{
             if ($onlyNames)
             then
                <Name>{$c}</Name>
             else (
                <Name>{$c.Name}</Name>,
                <Label>{$c.Label}</Label>,
                <IsNillable>{$c.IsNillable}</IsNillable>,
                <IsAbstract>{$c.IsAbstract}</IsAbstract>,
                <PeriodType>{$c.PeriodType}</PeriodType>,
                <Balance>{$c.Balance}</Balance>,
                <SubstitutionGroup>{$c.SubstitutionGroup}</SubstitutionGroup>,
                <DataType>{$c.DataType}</DataType>,
                <BaseType>{$c.BaseType}</BaseType>,
                <ClosestSchemaBuiltinType>{$c.ClosestSchemaBuiltinType}</ClosestSchemaBuiltinType>,
                <IsTextBlock>{$c.IsTextBlock}</IsTextBlock>,
                <ComponentRole>{$c.ComponentRole}</ComponentRole>,
                <ComponentLabel>{$c.ComponentLabel}</ComponentLabel>,
                <AccessionNumber>{$c.AccessionNumber}</AccessionNumber>,
                <CIK>{$c.CIK}</CIK>,
                <EntityRegistrantName>{$c.EntityRegistrantName}</EntityRegistrantName>,
                <FiscalYear>{$c.FiscalYear}</FiscalYear>,
                <FiscalPeriod>{$c.FiscalPeriod}</FiscalPeriod>
             )
        }</ReportElement>
};

declare function local:concepts-for-archives($aids as string*) as object*
{
    let $conn :=
      let $credentials := credentials:credentials("MongoDB", "xbrl")
      return
        try {
            mongo:connect($credentials)
        } catch mongo:* {
            error(QName("concepts:CONNECTION-FAILED"), $err:description)
        }
    return
        mongo:find($conn, "concepts",
        {
            "Archive": { "$in" : [ $aids ] }
        })
};

declare function local:concepts-for-archives($aids as string*, $names as string*, $map as string?) as object*
{
    let $conn :=
      let $credentials := credentials:credentials("MongoDB", "xbrl")
      return
        try {
            mongo:connect($credentials)
        } catch mongo:* {
            error(QName("concepts:CONNECTION-FAILED"), $err:description)
        }
    let $map := if (exists($map)) then concept-maps:concept-maps($map) else ()

    let $all-concepts-computable-by-maps as string* := keys($map.Trees)

    let $concepts-computable-by-maps as object* :=
        for $c in $names[$$ = $all-concepts-computable-by-maps]
        return $map.Trees.$c

    let $mapped-names := (keys($concepts-computable-by-maps.To ), $concepts-computable-by-maps.To [].Name)

    let $concepts-not-computable-by-maps as string* := seq:value-except($names, $mapped-names)
    let $results-not-computed-by-maps := mongo:find($conn, "concepts",
        {
            "Name" : { "$in" : [ $concepts-not-computable-by-maps ] },
            "Archive": { "$in" : [ $aids ] }
        })
    let $results-computed-by-maps :=
        for $c in mongo:find($conn, "concepts",
            {
                "Name" : { "$in" : [ $mapped-names ] },
                "Archive": { "$in" : [ $aids ] }
            })
        group by $c.Component
        let $c := $c[1]
        let $map-concept := (for $candidate in $concepts-computable-by-maps
                            where $c.Name = (keys($candidate.To), $candidate.To[].Name)
                            return $candidate)[1]
        return
            copy $n := $c
            modify (
                replace value of json $n.Name with $map-concept.Name,
                insert json  { Origin : $c.Name } into $n)
            return $n
    return ($results-not-computed-by-maps, $results-computed-by-maps)
};

declare function local:concepts-for-archives-and-labels($aids as string*, $labels as string) as object*
{
    let $conn :=
        let $credentials := credentials:credentials("MongoDB", "xbrl")
        return
            try {
                mongo:connect($credentials)
            } catch mongo:* {
                error(QName("components:CONNECTION-FAILED"), $err:description)
            }
    return mongo:run-cmd-deterministic(
        $conn,
        {
            "text" : "concepts",
            "filter" : { "Archive" : { "$in" : [ $aids ] } },
            "search" : $labels,
            "limit" : 100,
            "score" : { "$meta" : "textScore" },
            "sort" : { score: { "$meta" : "textScore" } }
        }).results[].obj
};

session:audit-call();

(: Query parameters :)
let $format as string?         := request:param-values("format")
let $ciks as string*           := distinct-values(request:param-values("cik"))
let $tags as string*           := distinct-values(request:param-values("tag"))
let $tickers as string*        := distinct-values(request:param-values("ticker"))
let $sics as string*           := distinct-values(request:param-values("sic"))
let $fiscalYears as string*    := distinct-values(request:param-values("fiscalYear", "LATEST"))
let $fiscalPeriods as string*  := distinct-values(request:param-values("fiscalPeriod", "FY"))
let $aids as string*           := distinct-values(request:param-values("aid"))
let $labels as string*         := request:param-values("label")
let $map as string?            := request:param-values("map")
let $report as string?         := request:param-values("report")
let $names as string*          := request:param-values("name")

(: Post-processing :)
let $format as string? := (: backwards compatibility, to be deprecated  :)
    lower-case(($format, substring-after(request:path(), ".jq."))[1])
let $tags as string* := (: backwards compatibility, to be deprecated :)
    distinct-values($tags ! upper-case($$))
let $fiscalYears as integer* :=
    for $fy in $fiscalYears ! upper-case($$)
    return switch($fy)
           case "LATEST" return $fiscal-core:LATEST_FISCAL_YEAR
           case "ALL" return $fiscal-core:ALL_FISCAL_YEARS
           default return if($fy castable as integer) then integer($fy) else ()
let $fiscalPeriods as string* :=
    for $fp in $fiscalPeriods ! upper-case($$)
    return switch($fp)
           case "ALL" return $fiscal-core:ALL_FISCAL_PERIODS
           default return $fp

(: Object resolution :)
let $entities as object* :=
    companies:companies(
        $ciks,
        $tags,
        $tickers,
        $sics)
let $archives as object* := fiscal-core:filings(
    $entities,
    $fiscalPeriods,
    $fiscalYears,
    $aids)
let $entities as object* :=
    ($entities[$$._id = $archives.Entity],
    let $not-found := $archives.Entity[not $entities._id = $$]
    where exists($not-found)
    return entities:entities($not-found))
let $onlyNames   := let $o := request:param-values("onlyNames")[1] return if (exists($o)) then ($o cast as boolean) else false
let $map as item* :=
    if(exists($report))
    then reports:concept-map($report)
    else $map
let $concepts := if (exists($names))
                  then local:concepts-for-archives($archives._id, $names, $map)
                  else if (exists($labels))
                  then local:concepts-for-archives-and-labels($archives._id, $labels[1])
                  else local:concepts-for-archives($archives._id)
let $result := { ReportElements : [ if ($onlyNames)
            then distinct-values($concepts.Name)
            else for $c in $concepts
                 group by $component := $c.Component
                 let $component as object := components:components($component)
                 let $default-hc as object := hypercubes:hypercubes-for-components($component, "xbrl:DefaultHypercube")
                 let $members as object* := $default-hc.Aspects."xbrl:Concept".Domains."xbrl:ConceptDomain".Members
                 let $archive as object := $archives[$$._id = $c[1].Archive]
                 let $entity as object := $entities[$$._id = $archive.Entity]
                 return
                     for $name in if (exists($c.Origin)) then $c.Origin else $c.Name
                     return
                         copy $res := $members.$name
                         modify (
                            replace value of json $res.Name with $c.Name,
                            if (exists($c.Origin))
                            then insert json { Origin : $c.Origin } into $res
                            else (),
                            insert json { ComponentRole : $component.Role } into $res,
                            insert json { ComponentLabel : $component.Label } into $res,
                            insert json { AccessionNumber : $archive._id } into $res,
                            insert json { CIK : $entity._id } into $res,
                            insert json { EntityRegistrantName : $entity.Profiles.SEC.CompanyName } into $res,
                            insert json { FiscalYear : $archive.Profiles.SEC.Fiscal.DocumentFiscalYearFocus } into $res,
                            insert json { FiscalPeriod : $archive.Profiles.SEC.Fiscal.DocumentFiscalPeriodFocus } into $res
                         )
                         return $res ] }
let $comment := {
    NumConcepts: count($concepts),
    TotalNumConcepts: session:num-concepts()
}
let $serializers := {
    to-xml : function($res as object) as node() {
        <ReportElements>{
                    local:to-xml($res.ReportElements[], $onlyNames)
                }</ReportElements>
    },
    to-csv : function($res as object) as string {
        local:to-csv($res.ReportElements[], $onlyNames)
    }
}
let $results := util:serialize($result, $comment, $serializers, $format, "report-elements")
return
    util:check-and-return-results($entities, $results, $format)

import module namespace util = "http://secxbrl.info/modules/util";

import module namespace request = "http://www.28msec.com/modules/http-request";
import module namespace session = "http://apps.28.io/session";
import module namespace entities = "http://28.io/modules/xbrl/entities";
import module namespace hypercubes = "http://28.io/modules/xbrl/hypercubes";
import module namespace components = "http://28.io/modules/xbrl/components";
import module namespace reports = "http://28.io/modules/xbrl/reports";
import module namespace concept-maps = "http://28.io/modules/xbrl/concept-maps";

import module namespace companies = "http://28.io/modules/xbrl/profiles/sec/companies";
import module namespace fiscal-core = "http://28.io/modules/xbrl/profiles/sec/fiscal/core";

import module namespace mongo = "http://www.28msec.com/modules/mongodb";
import module namespace credentials = "http://www.28msec.com/modules/credentials";

import module namespace csv = "http://zorba.io/modules/json-csv";
import module namespace seq = "http://zorba.io/modules/sequence";

declare namespace concepts = "http://www.28msec.com/modules/bizql/concepts";

declare function local:to-csv($concepts as item*, $onlyNames as boolean) as string
{
    if ($onlyNames)
    then
        string-join(("Name", $concepts), "
")
    else
        string-join(csv:serialize($concepts, { serialize-null-as : "" }))
};

declare function local:to-xml($concepts as item*, $onlyNames as boolean) as element()*
{
    for $c in $concepts
    return
        <ReportElement>{
             if ($onlyNames)
             then
                <Name>{$c}</Name>
             else (
                <Name>{$c.Name}</Name>,
                <Label>{$c.Label}</Label>,
                <IsNillable>{$c.IsNillable}</IsNillable>,
                <IsAbstract>{$c.IsAbstract}</IsAbstract>,
                <PeriodType>{$c.PeriodType}</PeriodType>,
                <Balance>{$c.Balance}</Balance>,
                <SubstitutionGroup>{$c.SubstitutionGroup}</SubstitutionGroup>,
                <DataType>{$c.DataType}</DataType>,
                <BaseType>{$c.BaseType}</BaseType>,
                <ClosestSchemaBuiltinType>{$c.ClosestSchemaBuiltinType}</ClosestSchemaBuiltinType>,
                <IsTextBlock>{$c.IsTextBlock}</IsTextBlock>,
                <ComponentRole>{$c.ComponentRole}</ComponentRole>,
                <ComponentLabel>{$c.ComponentLabel}</ComponentLabel>,
                <AccessionNumber>{$c.AccessionNumber}</AccessionNumber>,
                <CIK>{$c.CIK}</CIK>,
                <EntityRegistrantName>{$c.EntityRegistrantName}</EntityRegistrantName>,
                <FiscalYear>{$c.FiscalYear}</FiscalYear>,
                <FiscalPeriod>{$c.FiscalPeriod}</FiscalPeriod>
             )
        }</ReportElement>
};

declare function local:concepts-for-archives($aids as string*) as object*
{
    let $conn :=
      let $credentials := credentials:credentials("MongoDB", "xbrl")
      return
        try {
            mongo:connect($credentials)
        } catch mongo:* {
            error(QName("concepts:CONNECTION-FAILED"), $err:description)
        }
    return
        mongo:find($conn, "concepts",
        {
            "Archive": { "$in" : [ $aids ] }
        })
};

declare function local:concepts-for-archives($aids as string*, $names as string*, $map as string?) as object*
{
    let $conn :=
      let $credentials := credentials:credentials("MongoDB", "xbrl")
      return
        try {
            mongo:connect($credentials)
        } catch mongo:* {
            error(QName("concepts:CONNECTION-FAILED"), $err:description)
        }
    let $map := if (exists($map)) then concept-maps:concept-maps($map) else ()

    let $all-concepts-computable-by-maps as string* := keys($map.Trees)

    let $concepts-computable-by-maps as object* :=
        for $c in $names[$$ = $all-concepts-computable-by-maps]
        return $map.Trees.$c

    let $mapped-names := (keys($concepts-computable-by-maps.To ), $concepts-computable-by-maps.To [].Name)

    let $concepts-not-computable-by-maps as string* := seq:value-except($names, $mapped-names)
    let $results-not-computed-by-maps := mongo:find($conn, "concepts",
        {
            "Name" : { "$in" : [ $concepts-not-computable-by-maps ] },
            "Archive": { "$in" : [ $aids ] }
        })
    let $results-computed-by-maps :=
        for $c in mongo:find($conn, "concepts",
            {
                "Name" : { "$in" : [ $mapped-names ] },
                "Archive": { "$in" : [ $aids ] }
            })
        group by $c.Component
        let $c := $c[1]
        let $map-concept := (for $candidate in $concepts-computable-by-maps
                            where $c.Name = (keys($candidate.To), $candidate.To[].Name)
                            return $candidate)[1]
        return
            copy $n := $c
            modify (
                replace value of json $n.Name with $map-concept.Name,
                insert json  { Origin : $c.Name } into $n)
            return $n
    return ($results-not-computed-by-maps, $results-computed-by-maps)
};

declare function local:concepts-for-archives-and-labels($aids as string*, $labels as string) as object*
{
    let $conn :=
        let $credentials := credentials:credentials("MongoDB", "xbrl")
        return
            try {
                mongo:connect($credentials)
            } catch mongo:* {
                error(QName("components:CONNECTION-FAILED"), $err:description)
            }
    return mongo:run-cmd-deterministic(
        $conn,
        {
            "text" : "concepts",
            "filter" : { "Archive" : { "$in" : [ $aids ] } },
            "search" : $labels,
            "limit" : 100,
            "score" : { "$meta" : "textScore" },
            "sort" : { score: { "$meta" : "textScore" } }
        }).results[].obj
};

session:audit-call();

(: Query parameters :)
let $format as string?         := request:param-values("format")
let $ciks as string*           := distinct-values(request:param-values("cik"))
let $tags as string*           := distinct-values(request:param-values("tag"))
let $tickers as string*        := distinct-values(request:param-values("ticker"))
let $sics as string*           := distinct-values(request:param-values("sic"))
let $fiscalYears as string*    := distinct-values(request:param-values("fiscalYear", "LATEST"))
let $fiscalPeriods as string*  := distinct-values(request:param-values("fiscalPeriod", "FY"))
let $aids as string*           := distinct-values(request:param-values("aid"))
let $labels as string*         := request:param-values("label")
let $map as string?            := request:param-values("map")
let $report as string?         := request:param-values("report")
let $names as string*          := request:param-values("name")

(: Post-processing :)
let $format as string? := (: backwards compatibility, to be deprecated  :)
    lower-case(($format, substring-after(request:path(), ".jq."))[1])
let $tags as string* := (: backwards compatibility, to be deprecated :)
    distinct-values($tags ! upper-case($$))
let $fiscalYears as integer* :=
    for $fy in $fiscalYears ! upper-case($$)
    return switch($fy)
           case "LATEST" return $fiscal-core:LATEST_FISCAL_YEAR
           case "ALL" return $fiscal-core:ALL_FISCAL_YEARS
           default return if($fy castable as integer) then integer($fy) else ()
let $fiscalPeriods as string* :=
    for $fp in $fiscalPeriods ! upper-case($$)
    return switch($fp)
           case "ALL" return $fiscal-core:ALL_FISCAL_PERIODS
           default return $fp

(: Object resolution :)
let $entities :=
    companies:companies(
        $ciks,
        $tags,
        $tickers,
        $sics)
let $archives as object* := fiscal-core:filings(
    $entities,
    $fiscalPeriods,
    $fiscalYears,
    $aids)
let $entities :=
    ($entities[$$._id = $archives.Entity],
    let $not-found := $archives.Entity[not $entities._id = $$]
    where exists($not-found)
    return entities:entities($not-found))
let $onlyNames   := let $o := request:param-values("onlyNames")[1] return if (exists($o)) then ($o cast as boolean) else false
let $map as item* :=
    if(exists($report))
    then reports:concept-map($report)
    else $map
let $concepts := if (exists($names))
                  then local:concepts-for-archives($archives._id, $names, $map)
                  else if (exists($labels))
                  then local:concepts-for-archives-and-labels($archives._id, $labels[1])
                  else local:concepts-for-archives($archives._id)
let $result := { ReportElements : [ if ($onlyNames)
            then distinct-values($concepts.Name)
            else for $c in $concepts
                 group by $component := $c.Component
                 let $component := components:components($component)
                 let $default-hc := hypercubes:hypercubes-for-components($component, "xbrl:DefaultHypercube")
                 let $members := $default-hc.Aspects."xbrl:Concept".Domains."xbrl:ConceptDomain".Members
                 let $archive := $archives[$$._id = $c[1].Archive]
                 let $entity := $entities[$$._id = $archives.Entity]
                 return
                     for $name in if (exists($c.Origin)) then $c.Origin else $c.Name
                     return
                         copy $res := $members.$name
                         modify (
                            replace value of json $res.Name with $c.Name,
                            if (exists($c.Origin))
                            then insert json { Origin : $c.Origin } into $res
                            else (),
                            insert json { ComponentRole : $component.Role } into $res,
                            insert json { ComponentLabel : $component.Label } into $res,
                            insert json { AccessionNumber : $archive._id } into $res,
                            insert json { CIK : $entity._id } into $res,
                            insert json { EntityRegistrantName : $entity.Profiles.SEC.CompanyName } into $res,
                            insert json { FiscalYear : $archive.Profiles.SEC.Fiscal.DocumentFiscalYearFocus } into $res,
                            insert json { FiscalPeriod : $archive.Profiles.SEC.Fiscal.DocumentFiscalPeriodFocus } into $res
                         )
                         return $res ] }
let $comment := {
    NumConcepts: count($concepts),
    TotalNumConcepts: session:num-concepts()
}
let $serializers := {
    to-xml : function($res as object) as node() {
        <ReportElements>{
                    local:to-xml($res.ReportElements[], $onlyNames)
                }</ReportElements>
    },
    to-csv : function($res as object) as string {
        local:to-csv($res.ReportElements[], $onlyNames)
    }
}
let $results := util:serialize($result, $comment, $serializers, $format, "report-elements")
return
    util:check-and-return-results($entities, $results, $format)


Fixed and simplified handling of default values in spreadsheets.

jsoniq version "1.0";

module namespace layout = "http://28.io/modules/xbrl/layout";

import module namespace facts = "http://28.io/modules/xbrl/facts";
import module namespace hypercubes = "http://28.io/modules/xbrl/hypercubes";

(:
    TODO: roll-up layout nodes for open structural nodes if desired
:)

(:
    Converts a structural breakdown to a layout header group.
:)
declare %private function layout:breakdown-to-group(
    $breakdown as object,
    $actual-aspect-space as object) as object
{
    let $labels := $breakdown.BreakdownLabels
    let $trees := $breakdown.BreakdownTrees[]
    let $header-rows-for-each-tree :=
        $trees ! layout:structural-node-to-header-rows($$, $actual-aspect-space)
    let $concatenated-header-rows :=
        layout:concatenate-header-rows($header-rows-for-each-tree)
    return {
        GroupLabels: $labels,
        GroupCells: $concatenated-header-rows
    }
};

(:
    Converts a structural node to a header row, and recursively
    converts children as well to bottom header rows.
:)
declare %private function layout:structural-node-to-header-rows(
    $structural-node as object,
    $actual-aspect-space as object) as array
{
    let $bottom-rows :=
        if (exists($structural-node.Children))
        then layout:concatenate-header-rows(
            $structural-node.Children[] ! layout:structural-node-to-header-rows($$, $actual-aspect-space)
        )
        else ()
    let $span as integer :=
        if(exists($bottom-rows))
        then sum(($bottom-rows[[1]])[].CellSpan)
        else 1
    let $open-header-rows :=
        layout:concatenate-header-rows(
            let $constrained-aspect as string := $structural-node.Aspect
            return switch($constrained-aspect)
            case "xbrl:Period" return
                for $actual-value in $actual-aspect-space.($constrained-aspect)[]
                let $tokens := tokenize($actual-value, "/")
                order by $actual-value descending
                return [
                    [ {|
                        {
                            CellLabels: [ $structural-node.Labels[], $actual-value ],
                            CellConstraints: {
                                "": {$constrained-aspect : $actual-value},
                                "table.periodStart" : {
                                    $constrained-aspect :
                                        if(count($tokens) eq 2)
                                        then string(date($tokens[1]) - dayTimeDuration("P1D"))
                                        else ""
                                },
                                "table.periodEnd" : {
                                    $constrained-aspect :
                                        if(count($tokens) eq 2)
                                        then $tokens[2]
                                        else ""
                                }
                            },
                            TagSelectors: [ $structural-node.TagSelectors[] ],
                            CellSpan: $span,
                            RollUp: false
                        },
                        { Id: $structural-node.Id }[exists($structural-node.Id)]
                    |} ],
                    $bottom-rows[]
                ]
            default return
                for $actual-value in $actual-aspect-space.($constrained-aspect)[]
                order by $actual-value
                return [
                    [ {|
                        {
                            CellLabels: [ $structural-node.Labels[], $actual-value ],
                            CellConstraints: {
                                "": {$constrained-aspect : $actual-value}
                            },
                            TagSelectors: [ $structural-node.TagSelectors[] ],
                            CellSpan: $span,
                            RollUp: false
                        },
                        { Id: $structural-node.Id }[exists($structural-node.Id)]
                    |} ],
                    $bottom-rows[]
                ]

        )
    let $closed-header-rows := [
        [ {|
            {
                CellLabels: [ $structural-node.Labels[], values($structural-node.ConstraintSets."") ],
                CellConstraints: ($structural-node.ConstraintSets, {"" : {}})[1],
                TagSelectors: [ $structural-node.TagSelectors[] ],
                CellSpan: $span,
                RollUp: boolean($structural-node.RollUp),
                IsRollUp: ($structural-node.IsTotal, false)[1],
                IsNegated: ($structural-node.IsNegated, false)[1]
            },
           { Id: $structural-node.Id }[exists($structural-node.Id)]
        |} ],
        $bottom-rows[]
    ]
    return
        if($structural-node.Open)
        then $open-header-rows
        else $closed-header-rows
};

(:
    Concatenates groups of header rows to a single group of header rows.
:)
declare function layout:concatenate-header-rows($header-rows-groups as array*) as array
{
    [
        for $row in 1 to size($header-rows-groups[1])
        return [
            for $row-group in $header-rows-groups ! ($$[[$row]])
            return $row-group[]
        ]
    ]
};

(:
    Builds the hypercube to query the facts for layouting.
    It's based on the supplied hypercube if present (intersecting its domains
    with the domains of the table to reduce the result size).
    If no hypercube is given, it will take the table domain.
:)
declare function layout:build-hypercube(
    $structural-model as object,
    $hypercube as object?) as object
{
    let $slice-constraints := accumulate(values(descendant-objects($structural-model).ConstraintSets))
    let $global-constraints := $structural-model.GlobalConstraintSet
    let $open-dimensions := descendant-objects($structural-model).Aspect
    let $table-domain := {|
        for $aspect in distinct-values(keys(($slice-constraints, $global-constraints)))
        return {
            $aspect: [ flatten($slice-constraints.$aspect), flatten($global-constraints.$aspect) ]
        }
    |}
    return hypercubes:user-defined-hypercube(
            {|
                for $dimension in distinct-values((keys(($hypercube.Aspects, $table-domain)), $open-dimensions))
                let $dimension-type as string? := $dimension.Type
                let $hypercube-members as atomic* := (
                    descendant-objects($hypercube.Aspects.$dimension.Members).Name,
                    $hypercube.Aspects.$dimension.DomainRestriction.Enumeration[])
                let $hypercube-default := ($hypercube.Aspects.$dimension.Default,
                                           $structural-model.DimensionDefaults.$dimension)[1]

                let $table-domain-members := $table-domain.$dimension[]

                let $actual-members :=
                    switch(true)
                    case exists($hypercube-members) and exists($table-domain-members)
                        return $hypercube-members[$$ = $table-domain-members]
                    case exists($hypercube-members)
                        return $hypercube-members
                    case exists($table-domain-members)
                        return $table-domain-members
                    default return ()

                let $inferred-type as string? :=
                    typeswitch($actual-members[not ($$ instance of null)])
                    case string* return
                            if(contains(($actual-members)[not ($$ instance of null)][1], ":"))
                            then ()
                            else "string"
                    case int* return "int"
                    case integer* return "integer"
                    case boolean* return "boolean"
                    default return "atomic"
                let $actual-type as string? := ($dimension-type, $inferred-type)[1]

                return {
                    $dimension : {|
                        { Type : $actual-type }[exists($actual-type) and exists($actual-members)],

                        if(exists($actual-members))
                        then {
                            Domain: [ distinct-values(($actual-members, $hypercube-default)) ]
                        }
                        else (),

                        if(exists($hypercube-default))
                        then {
                            Default: $hypercube-default
                        }
                        else ()
                    |}
                }
            |},
            {
                Name: $hypercube.Name,
                Label: $hypercube.Label
            }[exists($hypercube)]
        )
};

(:
    Filters the facts that match the supplied aspect
    constraints. Filters for default if a participating
    aspect is not present in the aspect constraints.
:)
declare function layout:filter-aspects(
    $facts as object*,
    $aspect-constraints as object,
    $participating-aspects as string*,
    $defaults as object) as object*
{
    for $fact in $facts
    where layout:matches-aspects($fact, $aspect-constraints, $participating-aspects, $defaults)
    return $fact
};

(:
    Checks whether a fact matches the aspect constraints.
:)
declare function layout:matches-aspects(
    $fact as object,
    $aspect-constraints as object,
    $participating-aspects as string*,
    $defaults as object?) as boolean
{
  every $aspect in $participating-aspects
  satisfies
    let $default-value := $defaults.$aspect
    let $allowed-value := $aspect-constraints.$aspect
    return
      $fact.Aspects.$aspect eq $allowed-value
      or
      empty($allowed-value) and $fact.Aspects.$aspect eq $default-value
};

(:
    Returns the slices implied by the headers with the given tag selectors.
:)
declare function layout:slices($table-headers as array, $tag-selectors as string*) as object*
{
    let $first-row-of-headers := $table-headers[[1]]
    let $bottom-rows-of-headers := [($table-headers[])[position() gt 1]]
    let $first-slices :=
        for $first-row-cell in $first-row-of-headers[]
        let $span := ($first-row-cell.CellSpan, 1)[1]
        let $tag-matches := $tag-selectors[$$ = keys($first-row-cell.CellConstraints)]
        return
            switch(count($tag-matches))
            case 0 return (1 to $span) ! ($first-row-cell.CellConstraints."", {})[1]
            case 1 return (1 to $span) ! ($first-row-cell.CellConstraints.$tag-matches, {})[1]
            default return error(
                QName("layout:MORE-THAN-ONE-MATCHING-TAG"),
                "Several tag selectors apply to a cell: " || string-join($tag-matches, " ")
            )
    return
    if(size($table-headers) le 1)
    then $first-slices
    else
        let $bottom-slices := layout:slices($bottom-rows-of-headers, $tag-selectors)
        for $i in 1 to count($first-slices)
        return
          facts:merge-objects($bottom-slices[$i], $first-slices[$i], true)
};

(:
    Returns the sequence of tag selector arrays implied by the headers.
:)
declare function layout:tag-selectors($table-headers as array) as array*
{
    let $first-row-of-headers := $table-headers[[1]]
    let $bottom-rows-of-headers := [($table-headers[])[position() gt 1]]
    let $first-tags :=
        for $first-row-cell in $first-row-of-headers[]
        let $span := ($first-row-cell.CellSpan, 1)[1]
        return (1 to $span) ! [ $first-row-cell.TagSelectors[] ]
    return
    if(size($table-headers) le 1)
    then $first-tags
    else
        let $bottom-tags := layout:tag-selectors($bottom-rows-of-headers)
        for $i in 1 to count($first-tags)
        return [($first-tags[$i])[], ($bottom-tags[$i])[]]
};

(:
    Merges the supplied header row groups by projecting them right-to-left.
:)
declare function layout:project($header-rows-groups as object*) as object*
{
    switch(count($header-rows-groups))
    case 0 return {
        GroupLabels: [],
        GroupCells: [
            [
                {
                    CellLabels: [],
                    CellConstraints: { "": {} },
                    CellSpan: 1
                }
                ]
        ]
    }
    case 1 return $header-rows-groups
    default return
        let $first := head($header-rows-groups)
        let $first-size := layout:header-rows-width($first)
        let $bottom := layout:project(tail($header-rows-groups))
        let $bottom-size as integer := layout:header-rows-width(head($bottom))
        return (
            layout:stretch-header-rows($first, $bottom-size),
            $bottom ! layout:duplicate-header-rows($$, $first-size)
        )
};

(:
    Returns the width of a header row group.
:)
declare function layout:header-rows-width($header-rows-group as object) as integer
{
    sum(($header-rows-group.GroupCells[[1]])[].CellSpan)
};

(:
    Stretches a header row group by the desired factor.
:)
declare function layout:stretch-header-rows($header-rows-group as object, $factor as integer) as object
{
    copy $g := $header-rows-group
    modify replace value of json $g.GroupCells with
    [
        for $row as array in $g.GroupCells[]
        return [
            for $cell in $row[]
            return copy $c := $cell
                   modify replace value of json $c.CellSpan with $c.CellSpan * $factor
                   return $c
        ]
    ]
    return $g
};

(:
    Duplicate the supplied rows the number of times specified in factor.
:)
declare function layout:duplicate-header-rows($header-rows-group as object, $factor as integer) as object
{
    copy $g := $header-rows-group
    modify replace value of json $g.GroupCells with
    [
        for $row as array in $g.GroupCells[]
        return [
            (1 to $factor) ! $row[]
        ]
    ]
    return $g
};

(:
   Returns the numbers of empty rows and columns
:)
declare function layout:empty-rows-and-columns($cells as array, $threshold as double?) as object
{
    {
        Rows: [
            for $i in 1 to size($cells)
            let $row := ($cells[[$i]])[]
            where count($row.Value) le count($row) * $threshold
            return $i
        ],
        Columns: [
            for $j in 1 to size($cells[[1]])
            let $column := $cells[][[$j]]
            where count($column.Value) lt count($column) * $threshold
            return $j
        ]
    }
};

(:
    Eliminates empty rows/columns in the table headers.
:)
declare function layout:eliminate-table-headers($table-headers as object, $empty-rows-and-columns as object) as object
{
    copy $th := $table-headers
    modify let $x-headers as array* := $th.x[].GroupCells
           let $x-eliminated-headers as array* := layout:eliminate($x-headers, $empty-rows-and-columns.Columns[])
           let $y-headers as array* := $th.y[].GroupCells
           let $y-eliminated-headers as array* := layout:eliminate($y-headers, $empty-rows-and-columns.Rows[])
           return (
               for $i in 1 to size($th.x)
               return replace value of json $th.x[[$i]].GroupCells with $x-eliminated-headers[$i],
               for $j in 1 to size($th.y)
               return replace value of json $th.y[[$j]].GroupCells with $y-eliminated-headers[$j]
           )
    return $th
};

(:
    Eliminates empty rows/columns in the cells.
:)
declare function layout:eliminate-cells($cells as array, $empty-rows-and-columns as object) as array
{
    [
        for $row at $i in $cells[]
        where not $i = $empty-rows-and-columns.Rows[]
        return [
            for $cell as item at $j in $row[]
            where not $j = $empty-rows-and-columns.Columns[]
            return $cell
        ]
    ]
};

(:
    Eliminates empty rows/columns in the table header's group cells.
:)
declare function layout:eliminate($header-groups as array*, $empty-columns as integer*) as array*
{
    let $first-group as array? := head($header-groups)
    let $other-groups as array* := tail($header-groups)
    return if(empty($first-group))
           then ()
           else
               let $new-first-group as array := [
                  for $row as array in $first-group[]
                  return [
                       for $i in 1 to size($row)
                       let $lower-bound as integer :=  sum((0, $row[][position() lt $i].CellSpan))
                       let $upper-bound as integer :=  sum($row[][position() le $i].CellSpan)
                       let $cell as object := $row[[$i]]
                       let $matching-empty-columns := $empty-columns[$lower-bound lt $$ and $$ le $upper-bound]
                       let $number := count($matching-empty-columns)
                       return switch(true)
                              case $cell.CellSpan eq $number
                              return ()
                              case $cell.CellSpan gt $number
                              return copy $c := $cell modify replace value of json $c.CellSpan with $c.CellSpan - $number return $c
                              default return error(QName("layout:ELIMINATION-ERROR"), ($number || " > " || $cell.CellSpan))
                    ]
               ]
               return (
                   $new-first-group,
                   layout:eliminate($other-groups, $empty-columns)
               )
};

(:~
 : Converts a structural model to a layout model.
 :
 : @param $structural-model a structural model.
 : @options - hypercube: a hypercube (null for using table domain, default: dimensionless hypercube)
 :          - filter
 :
 : @error layout:MORE-THAN-ONE-MATCHING-TAG in case of a tag selector conflict.
 : @error layout:ELIMINATION_ERROR an error occurred during elimination of empty rows/columns.
 :
 : @return a layout model.
 :)
declare function layout:layout(
    $structural-model as object,
    $options as object?
) as object
{
    let $elimination as boolean := ($options.Eliminate, false)[1]
    let $threshold as double := ($options.EliminationThreshold, 0)[1]
    let $original-hypercube :=
        if($options.Hypercube instance of null)
        then ()
        else ($options.Hypercube, hypercubes:dimensionless-hypercube())[1]
    let $hypercube := layout:build-hypercube(
        $structural-model,
        $original-hypercube
    )
    let $facts := facts:facts-for({|
        {Hypercube: $hypercube},
        trim($options, "Hypercube")
    |})
    let $actual-aspect-space :=
    {|
        for $aspect in distinct-values(keys($facts.Aspects))
        return { $aspect : [ distinct-values($facts.Aspects.$aspect) ] }
    |}
    let $defaults :=
    {|
        for $aspect in keys($hypercube.Aspects)
        let $default := $hypercube.Aspects.$aspect.Default
        where exists($default)
        return { $aspect : $default }
    |}
    let $spreadsheet := {
        ModelKind: "LayoutModel",
        ComponentAndHypercubeInformation: {
            Component: $structural-model.Component,
            Hypercube: {
                Name: $hypercube.Name[1],
                Label: $hypercube.Label[1]
            }
        },
        TableSetLabels: $structural-model.TableSetLabels,
        TableSet: [
            for $table in $structural-model.TableSet[]
            let $table-headers as object := {|
                for $axis in keys($table.Breakdowns)
                return {
                    $axis: [
                        layout:project(
                            for $breakdown in $table.Breakdowns.$axis[]
                            return layout:breakdown-to-group($breakdown, $actual-aspect-space)
                        )
                    ]
                }
            |}
            let $x-tag-selectors := layout:tag-selectors([$table-headers.x[].GroupCells[]])
            let $y-tag-selectors := layout:tag-selectors([$table-headers.y[].GroupCells[]])
            let $default-x-slices := layout:slices([$table-headers.x[].GroupCells[]], ())
            let $default-y-slices := layout:slices([$table-headers.y[].GroupCells[]], ())
            let $period-start-x-slices := layout:slices([$table-headers.x[].GroupCells[]], "table.periodStart")
            let $period-start-y-slices := layout:slices([$table-headers.y[].GroupCells[]], "table.periodStart")
            let $period-end-x-slices := layout:slices([$table-headers.x[].GroupCells[]], "table.periodEnd")
            let $period-end-y-slices := layout:slices([$table-headers.y[].GroupCells[]], "table.periodEnd")
            let $cells := [
                for $y-tag-selector at $i in $y-tag-selectors
                return [
                    for $x-tag-selector at $j in $x-tag-selectors
                    let $selectors as string* := ($x-tag-selector[], $y-tag-selector[])
                    let $x-slices as object* :=
                        switch(true)
                        case empty($selectors) return $default-x-slices
                        case deep-equal($selectors, "table.periodStart") return $period-start-x-slices
                        case deep-equal($selectors, "table.periodEnd") return $period-end-x-slices
                        default return layout:slices([$table-headers.x[].GroupCells[]], $selectors)
                    let $participating-x := distinct-values(keys($x-slices))
                    let $y-slices as object* :=
                        switch(true)
                        case empty($selectors) return $default-y-slices
                        case deep-equal($selectors, "table.periodStart") return $period-start-y-slices
                        case deep-equal($selectors, "table.periodEnd") return $period-end-y-slices
                        default return layout:slices([$table-headers.y[].GroupCells[]], $selectors)
                    let $participating-y := distinct-values(keys($y-slices))
                    let $row-facts := layout:filter-aspects(
                        $facts,
                        $y-slices[$i],
                        $participating-y,
                        $defaults)
                    let $cell-facts := layout:filter-aspects(
                        $row-facts,
                        $x-slices[$j],
                        $participating-x,
                        $defaults)
                    return typeswitch($cell-facts)
                           case object return $cell-facts
                           case () return {
                               Aspects: facts:merge-objects(
                                   facts:merge-objects($x-slices[$j], $y-slices[$i], false),
                                   $structural-model.GlobalConstraintSet,
                                   false
                               )
                           }
                           case object+ return [ $cell-facts ]
                           default return {
                               Aspects: facts:merge-objects(
                                   facts:merge-objects($x-slices[$j], $y-slices[$i], false),
                                   $structural-model.GlobalConstraintSet,
                                   false
                               )
                           }
                ]
            ]
            let $empty-rows-and-columns := layout:empty-rows-and-columns($cells, $threshold)
            let $table-headers := if($elimination)
                                  then layout:eliminate-table-headers($table-headers, $empty-rows-and-columns)
                                  else $table-headers
            let $cells := if($elimination)
                          then layout:eliminate-cells($cells, $empty-rows-and-columns)
                          else $cells
            return
            {
                TableHeaders: $table-headers,
                TableCells: {
                    AxisOrder: [ "y", "x" ],
                    Facts: $cells
                }
            }
        ],
        GlobalConstraints: [
          let $filters := $structural-model.GlobalConstraintSet
          for $key in keys($filters)
          return project($filters, $key)
        ],
        GlobalConstraintLabels: $structural-model.GlobalConstraintLabels
    }
    return if($options.Debug)
    then {|
        $spreadsheet,
        {
            DebugInfo: {
                Hypercube: $hypercube,
                OriginalHypercube: $original-hypercube
            }
        }
    |}
    else $spreadsheet
};

jsoniq version "1.0";

module namespace layout = "http://28.io/modules/xbrl/layout";

import module namespace facts = "http://28.io/modules/xbrl/facts";
import module namespace hypercubes = "http://28.io/modules/xbrl/hypercubes";

(:
    TODO: roll-up layout nodes for open structural nodes if desired
:)

(:
    Converts a structural breakdown to a layout header group.
:)
declare %private function layout:breakdown-to-group(
    $breakdown as object,
    $actual-aspect-space as object) as object
{
    let $labels := $breakdown.BreakdownLabels
    let $trees := $breakdown.BreakdownTrees[]
    let $header-rows-for-each-tree :=
        $trees ! layout:structural-node-to-header-rows($$, $actual-aspect-space)
    let $concatenated-header-rows :=
        layout:concatenate-header-rows($header-rows-for-each-tree)
    return {
        GroupLabels: $labels,
        GroupCells: $concatenated-header-rows
    }
};

(:
    Converts a structural node to a header row, and recursively
    converts children as well to bottom header rows.
:)
declare %private function layout:structural-node-to-header-rows(
    $structural-node as object,
    $actual-aspect-space as object) as array
{
    let $bottom-rows :=
        if (exists($structural-node.Children))
        then layout:concatenate-header-rows(
            $structural-node.Children[] ! layout:structural-node-to-header-rows($$, $actual-aspect-space)
        )
        else ()
    let $span as integer :=
        if(exists($bottom-rows))
        then sum(($bottom-rows[[1]])[].CellSpan)
        else 1
    let $open-header-rows :=
        layout:concatenate-header-rows(
            let $constrained-aspect as string := $structural-node.Aspect
            return switch($constrained-aspect)
            case "xbrl:Period" return
                for $actual-value in $actual-aspect-space.($constrained-aspect)[]
                let $tokens := tokenize($actual-value, "/")
                order by $actual-value descending
                return [
                    [ {|
                        {
                            CellLabels: [ $structural-node.Labels[], $actual-value ],
                            CellConstraints: {
                                "": {$constrained-aspect : $actual-value},
                                "table.periodStart" : {
                                    $constrained-aspect :
                                        if(count($tokens) eq 2)
                                        then string(date($tokens[1]) - dayTimeDuration("P1D"))
                                        else ""
                                },
                                "table.periodEnd" : {
                                    $constrained-aspect :
                                        if(count($tokens) eq 2)
                                        then $tokens[2]
                                        else ""
                                }
                            },
                            TagSelectors: [ $structural-node.TagSelectors[] ],
                            CellSpan: $span,
                            RollUp: false
                        },
                        { Id: $structural-node.Id }[exists($structural-node.Id)]
                    |} ],
                    $bottom-rows[]
                ]
            default return
                for $actual-value in $actual-aspect-space.($constrained-aspect)[]
                order by $actual-value
                return [
                    [ {|
                        {
                            CellLabels: [ $structural-node.Labels[], $actual-value ],
                            CellConstraints: {
                                "": {$constrained-aspect : $actual-value}
                            },
                            TagSelectors: [ $structural-node.TagSelectors[] ],
                            CellSpan: $span,
                            RollUp: false
                        },
                        { Id: $structural-node.Id }[exists($structural-node.Id)]
                    |} ],
                    $bottom-rows[]
                ]

        )
    let $closed-header-rows := [
        [ {|
            {
                CellLabels: [ $structural-node.Labels[], values($structural-node.ConstraintSets."") ],
                CellConstraints: ($structural-node.ConstraintSets, {"" : {}})[1],
                TagSelectors: [ $structural-node.TagSelectors[] ],
                CellSpan: $span,
                RollUp: boolean($structural-node.RollUp),
                IsRollUp: ($structural-node.IsTotal, false)[1],
                IsNegated: ($structural-node.IsNegated, false)[1]
            },
           { Id: $structural-node.Id }[exists($structural-node.Id)]
        |} ],
        $bottom-rows[]
    ]
    return
        if($structural-node.Open)
        then $open-header-rows
        else $closed-header-rows
};

(:
    Concatenates groups of header rows to a single group of header rows.
:)
declare function layout:concatenate-header-rows($header-rows-groups as array*) as array
{
    [
        for $row in 1 to size($header-rows-groups[1])
        return [
            for $row-group in $header-rows-groups ! ($$[[$row]])
            return $row-group[]
        ]
    ]
};

(:
    Builds the hypercube to query the facts for layouting.
    It's based on the supplied hypercube if present (intersecting its domains
    with the domains of the table to reduce the result size).
    If no hypercube is given, it will take the table domain.
:)
declare function layout:build-hypercube(
    $structural-model as object,
    $hypercube as object?) as object
{
    let $slice-constraints := accumulate(values(descendant-objects($structural-model).ConstraintSets))
    let $global-constraints := $structural-model.GlobalConstraintSet
    let $open-dimensions := descendant-objects($structural-model).Aspect
    let $table-domain := {|
        for $aspect in distinct-values(keys(($slice-constraints, $global-constraints)))
        return {
            $aspect: [ flatten($slice-constraints.$aspect), flatten($global-constraints.$aspect) ]
        }
    |}
    return hypercubes:user-defined-hypercube(
            {|
                for $dimension in distinct-values((keys(($hypercube.Aspects, $table-domain)), $open-dimensions))
                let $dimension-type as string? := $dimension.Type
                let $hypercube-members as atomic* := (
                    descendant-objects($hypercube.Aspects.$dimension.Members).Name,
                    $hypercube.Aspects.$dimension.DomainRestriction.Enumeration[])
                let $hypercube-default := ($hypercube.Aspects.$dimension.Default,
                                           $structural-model.DimensionDefaults.$dimension)[1]

                let $table-domain-members := $table-domain.$dimension[]

                let $actual-members :=
                    switch(true)
                    case exists($hypercube-members) and exists($table-domain-members)
                        return $hypercube-members[$$ = $table-domain-members]
                    case exists($hypercube-members)
                        return $hypercube-members
                    case exists($table-domain-members)
                        return $table-domain-members
                    default return ()

                let $inferred-type as string? :=
                    typeswitch($actual-members[not ($$ instance of null)])
                    case string* return
                            if(contains(($actual-members)[not ($$ instance of null)][1], ":"))
                            then ()
                            else "string"
                    case int* return "int"
                    case integer* return "integer"
                    case boolean* return "boolean"
                    default return "atomic"
                let $actual-type as string? := ($dimension-type, $inferred-type)[1]

                return {
                    $dimension : {|
                        { Type : $actual-type }[exists($actual-type) and exists($actual-members)],

                        if(exists($actual-members))
                        then {
                            Domain: [ distinct-values(($actual-members, $hypercube-default)) ]
                        }
                        else (),

                        if(exists($hypercube-default))
                        then {
                            Default: $hypercube-default
                        }
                        else ()
                    |}
                }
            |},
            {
                Name: $hypercube.Name,
                Label: $hypercube.Label
            }[exists($hypercube)]
        )
};

(:
    Filters the facts that match the supplied aspect
    constraints. Filters for default if a participating
    aspect is not present in the aspect constraints.
:)
declare function layout:filter-aspects(
    $facts as object*,
    $aspect-constraints as object,
    $participating-aspects as string*,
    $defaults as object) as object*
{
    for $fact in $facts
    where layout:matches-aspects($fact, $aspect-constraints, $participating-aspects, $defaults)
    return $fact
};

(:
    Checks whether a fact matches the aspect constraints.
:)
declare function layout:matches-aspects(
    $fact as object,
    $aspect-constraints as object,
    $participating-aspects as string*,
    $defaults as object?) as boolean
{
    (
        every $aspect in $participating-aspects
        satisfies
            let $default-value := $defaults.$aspect
            let $allowed-value := $aspect-constraints.$aspect
            return (
                exists($allowed-value)
                or
                $fact.Aspects.$aspect eq $default-value
            )
    )
    and
    (
        every $aspect in keys($aspect-constraints)
        satisfies
            let $default-value := $defaults.$aspect
            let $allowed-value := $aspect-constraints.$aspect
            return (
                $allowed-value eq $default-value
                or
                $fact.Aspects.$aspect eq $allowed-value
            )
    )
};

(:
    Returns the slices implied by the headers with the given tag selectors.
:)
declare function layout:slices($table-headers as array, $tag-selectors as string*) as object*
{
    let $first-row-of-headers := $table-headers[[1]]
    let $bottom-rows-of-headers := [($table-headers[])[position() gt 1]]
    let $first-slices :=
        for $first-row-cell in $first-row-of-headers[]
        let $span := ($first-row-cell.CellSpan, 1)[1]
        let $tag-matches := $tag-selectors[$$ = keys($first-row-cell.CellConstraints)]
        return
            switch(count($tag-matches))
            case 0 return (1 to $span) ! ($first-row-cell.CellConstraints."", {})[1]
            case 1 return (1 to $span) ! ($first-row-cell.CellConstraints.$tag-matches, {})[1]
            default return error(
                QName("layout:MORE-THAN-ONE-MATCHING-TAG"),
                "Several tag selectors apply to a cell: " || string-join($tag-matches, " ")
            )
    return
    if(size($table-headers) le 1)
    then $first-slices
    else
        let $bottom-slices := layout:slices($bottom-rows-of-headers, $tag-selectors)
        for $i in 1 to count($first-slices)
        return
          facts:merge-objects($bottom-slices[$i], $first-slices[$i], true)
};

(:
    Returns the sequence of tag selector arrays implied by the headers.
:)
declare function layout:tag-selectors($table-headers as array) as array*
{
    let $first-row-of-headers := $table-headers[[1]]
    let $bottom-rows-of-headers := [($table-headers[])[position() gt 1]]
    let $first-tags :=
        for $first-row-cell in $first-row-of-headers[]
        let $span := ($first-row-cell.CellSpan, 1)[1]
        return (1 to $span) ! [ $first-row-cell.TagSelectors[] ]
    return
    if(size($table-headers) le 1)
    then $first-tags
    else
        let $bottom-tags := layout:tag-selectors($bottom-rows-of-headers)
        for $i in 1 to count($first-tags)
        return [($first-tags[$i])[], ($bottom-tags[$i])[]]
};

(:
    Merges the supplied header row groups by projecting them right-to-left.
:)
declare function layout:project($header-rows-groups as object*) as object*
{
    switch(count($header-rows-groups))
    case 0 return {
        GroupLabels: [],
        GroupCells: [
            [
                {
                    CellLabels: [],
                    CellConstraints: { "": {} },
                    CellSpan: 1
                }
                ]
        ]
    }
    case 1 return $header-rows-groups
    default return
        let $first := head($header-rows-groups)
        let $first-size := layout:header-rows-width($first)
        let $bottom := layout:project(tail($header-rows-groups))
        let $bottom-size as integer := layout:header-rows-width(head($bottom))
        return (
            layout:stretch-header-rows($first, $bottom-size),
            $bottom ! layout:duplicate-header-rows($$, $first-size)
        )
};

(:
    Returns the width of a header row group.
:)
declare function layout:header-rows-width($header-rows-group as object) as integer
{
    sum(($header-rows-group.GroupCells[[1]])[].CellSpan)
};

(:
    Stretches a header row group by the desired factor.
:)
declare function layout:stretch-header-rows($header-rows-group as object, $factor as integer) as object
{
    copy $g := $header-rows-group
    modify replace value of json $g.GroupCells with
    [
        for $row as array in $g.GroupCells[]
        return [
            for $cell in $row[]
            return copy $c := $cell
                   modify replace value of json $c.CellSpan with $c.CellSpan * $factor
                   return $c
        ]
    ]
    return $g
};

(:
    Duplicate the supplied rows the number of times specified in factor.
:)
declare function layout:duplicate-header-rows($header-rows-group as object, $factor as integer) as object
{
    copy $g := $header-rows-group
    modify replace value of json $g.GroupCells with
    [
        for $row as array in $g.GroupCells[]
        return [
            (1 to $factor) ! $row[]
        ]
    ]
    return $g
};

(:
   Returns the numbers of empty rows and columns
:)
declare function layout:empty-rows-and-columns($cells as array, $threshold as double?) as object
{
    {
        Rows: [
            for $i in 1 to size($cells)
            let $row := ($cells[[$i]])[]
            where count($row.Value) le count($row) * $threshold
            return $i
        ],
        Columns: [
            for $j in 1 to size($cells[[1]])
            let $column := $cells[][[$j]]
            where count($column.Value) lt count($column) * $threshold
            return $j
        ]
    }
};

(:
    Eliminates empty rows/columns in the table headers.
:)
declare function layout:eliminate-table-headers($table-headers as object, $empty-rows-and-columns as object) as object
{
    copy $th := $table-headers
    modify let $x-headers as array* := $th.x[].GroupCells
           let $x-eliminated-headers as array* := layout:eliminate($x-headers, $empty-rows-and-columns.Columns[])
           let $y-headers as array* := $th.y[].GroupCells
           let $y-eliminated-headers as array* := layout:eliminate($y-headers, $empty-rows-and-columns.Rows[])
           return (
               for $i in 1 to size($th.x)
               return replace value of json $th.x[[$i]].GroupCells with $x-eliminated-headers[$i],
               for $j in 1 to size($th.y)
               return replace value of json $th.y[[$j]].GroupCells with $y-eliminated-headers[$j]
           )
    return $th
};

(:
    Eliminates empty rows/columns in the cells.
:)
declare function layout:eliminate-cells($cells as array, $empty-rows-and-columns as object) as array
{
    [
        for $row at $i in $cells[]
        where not $i = $empty-rows-and-columns.Rows[]
        return [
            for $cell as item at $j in $row[]
            where not $j = $empty-rows-and-columns.Columns[]
            return $cell
        ]
    ]
};

(:
    Eliminates empty rows/columns in the table header's group cells.
:)
declare function layout:eliminate($header-groups as array*, $empty-columns as integer*) as array*
{
    let $first-group as array? := head($header-groups)
    let $other-groups as array* := tail($header-groups)
    return if(empty($first-group))
           then ()
           else
               let $new-first-group as array := [
                  for $row as array in $first-group[]
                  return [
                       for $i in 1 to size($row)
                       let $lower-bound as integer :=  sum((0, $row[][position() lt $i].CellSpan))
                       let $upper-bound as integer :=  sum($row[][position() le $i].CellSpan)
                       let $cell as object := $row[[$i]]
                       let $matching-empty-columns := $empty-columns[$lower-bound lt $$ and $$ le $upper-bound]
                       let $number := count($matching-empty-columns)
                       return switch(true)
                              case $cell.CellSpan eq $number
                              return ()
                              case $cell.CellSpan gt $number
                              return copy $c := $cell modify replace value of json $c.CellSpan with $c.CellSpan - $number return $c
                              default return error(QName("layout:ELIMINATION-ERROR"), ($number || " > " || $cell.CellSpan))
                    ]
               ]
               return (
                   $new-first-group,
                   layout:eliminate($other-groups, $empty-columns)
               )
};

(:~
 : Converts a structural model to a layout model.
 :
 : @param $structural-model a structural model.
 : @options - hypercube: a hypercube (null for using table domain, default: dimensionless hypercube)
 :          - filter
 :
 : @error layout:MORE-THAN-ONE-MATCHING-TAG in case of a tag selector conflict.
 : @error layout:ELIMINATION_ERROR an error occurred during elimination of empty rows/columns.
 :
 : @return a layout model.
 :)
declare function layout:layout(
    $structural-model as object,
    $options as object?
) as object
{
    let $elimination as boolean := ($options.Eliminate, false)[1]
    let $threshold as double := ($options.EliminationThreshold, 0)[1]
    let $original-hypercube :=
        if($options.Hypercube instance of null)
        then ()
        else ($options.Hypercube, hypercubes:dimensionless-hypercube())[1]
    let $hypercube := layout:build-hypercube(
        $structural-model,
        $original-hypercube
    )
    let $facts := facts:facts-for({|
        {Hypercube: $hypercube},
        trim($options, "Hypercube")
    |})
    let $actual-aspect-space :=
    {|
        for $aspect in distinct-values(keys($facts.Aspects))
        return { $aspect : [ distinct-values($facts.Aspects.$aspect) ] }
    |}
    let $defaults :=
    {|
        for $aspect in keys($hypercube.Aspects)
        let $default := $hypercube.Aspects.$aspect.Default
        where exists($default)
        return { $aspect : $default }
    |}
    let $spreadsheet := {
        ModelKind: "LayoutModel",
        ComponentAndHypercubeInformation: {
            Component: $structural-model.Component,
            Hypercube: {
                Name: $hypercube.Name[1],
                Label: $hypercube.Label[1]
            }
        },
        TableSetLabels: $structural-model.TableSetLabels,
        TableSet: [
            for $table in $structural-model.TableSet[]
            let $table-headers as object := {|
                for $axis in keys($table.Breakdowns)
                return {
                    $axis: [
                        layout:project(
                            for $breakdown in $table.Breakdowns.$axis[]
                            return layout:breakdown-to-group($breakdown, $actual-aspect-space)
                        )
                    ]
                }
            |}
            let $x-tag-selectors := layout:tag-selectors([$table-headers.x[].GroupCells[]])
            let $y-tag-selectors := layout:tag-selectors([$table-headers.y[].GroupCells[]])
            let $default-x-slices := layout:slices([$table-headers.x[].GroupCells[]], ())
            let $default-y-slices := layout:slices([$table-headers.y[].GroupCells[]], ())
            let $period-start-x-slices := layout:slices([$table-headers.x[].GroupCells[]], "table.periodStart")
            let $period-start-y-slices := layout:slices([$table-headers.y[].GroupCells[]], "table.periodStart")
            let $period-end-x-slices := layout:slices([$table-headers.x[].GroupCells[]], "table.periodEnd")
            let $period-end-y-slices := layout:slices([$table-headers.y[].GroupCells[]], "table.periodEnd")
            let $cells := [
                for $y-tag-selector at $i in $y-tag-selectors
                return [
                    for $x-tag-selector at $j in $x-tag-selectors
                    let $selectors as string* := ($x-tag-selector[], $y-tag-selector[])
                    let $x-slices as object* :=
                        switch(true)
                        case empty($selectors) return $default-x-slices
                        case deep-equal($selectors, "table.periodStart") return $period-start-x-slices
                        case deep-equal($selectors, "table.periodEnd") return $period-end-x-slices
                        default return layout:slices([$table-headers.x[].GroupCells[]], $selectors)
                    let $participating-x := distinct-values(keys($x-slices))
                    let $y-slices as object* :=
                        switch(true)
                        case empty($selectors) return $default-y-slices
                        case deep-equal($selectors, "table.periodStart") return $period-start-y-slices
                        case deep-equal($selectors, "table.periodEnd") return $period-end-y-slices
                        default return layout:slices([$table-headers.y[].GroupCells[]], $selectors)
                    let $participating-y := distinct-values(keys($y-slices))
                    let $row-facts := layout:filter-aspects(
                        $facts,
                        $y-slices[$i],
                        $participating-y,
                        $defaults)
                    let $cell-facts := layout:filter-aspects(
                        $row-facts,
                        $x-slices[$j],
                        $participating-x,
                        $defaults)
                    return typeswitch($cell-facts)
                           case object return $cell-facts
                           case () return {
                               Aspects: facts:merge-objects(
                                   facts:merge-objects($x-slices[$j], $y-slices[$i], false),
                                   $structural-model.GlobalConstraintSet,
                                   false
                               )
                           }
                           case object+ return [ $cell-facts ]
                           default return {
                               Aspects: facts:merge-objects(
                                   facts:merge-objects($x-slices[$j], $y-slices[$i], false),
                                   $structural-model.GlobalConstraintSet,
                                   false
                               )
                           }
                ]
            ]
            let $empty-rows-and-columns := layout:empty-rows-and-columns($cells, $threshold)
            let $table-headers := if($elimination)
                                  then layout:eliminate-table-headers($table-headers, $empty-rows-and-columns)
                                  else $table-headers
            let $cells := if($elimination)
                          then layout:eliminate-cells($cells, $empty-rows-and-columns)
                          else $cells
            return
            {
                TableHeaders: $table-headers,
                TableCells: {
                    AxisOrder: [ "y", "x" ],
                    Facts: $cells
                }
            }
        ],
        GlobalConstraints: [
          let $filters := $structural-model.GlobalConstraintSet
          for $key in keys($filters)
          return project($filters, $key)
        ],
        GlobalConstraintLabels: $structural-model.GlobalConstraintLabels
    }
    return if($options.Debug)
    then {|
        $spreadsheet,
        {
            DebugInfo: {
                Hypercube: $hypercube,
                OriginalHypercube: $original-hypercube
            }
        }
    |}
    else $spreadsheet
};