:green_heart: Fix model structure tests.

jsoniq version "1.0";

(:
 : Copyright 2012-2013 28msec Inc.
 :)

(:~
 :
This module provides functionality for querying SEC networks in SEC filings.

 :
SEC Networks are actually XBRL Components. In the SEC profiles, all XBRL networks
 : in an XBRL component must be consistent to each other. A way to look at it is that
 : the SEC Network can be identified with the XBRL presentation network it contains.

 :
 :
For XBRL-generic operations on XBRL components, use the
 : generic components module.

 :
 : @author Charles Hoffman
 : @author Matthias Brantner
 : @author Dennis Knochenwefel
 : @author Ghislain Fourny
 :)
module namespace sec-networks = "http://28.io/modules/xbrl/profiles/sec/networks";

import module namespace mw = "http://28.io/modules/xbrl/mongo-wrapper";

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

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

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

declare namespace ver = "http://zorba.io/options/versioning";
declare option ver:module-version "1.0";

(:~
 : Disclosure for the balance sheet.
 :)
declare variable $sec-networks:BALANCE_SHEET as xs:string := "BalanceSheet";

(:~
 : Disclosure for the parenthetical balance sheet.
 :)
declare variable $sec-networks:BALANCE_SHEET_PARENTHETICAL as xs:string := "BalanceSheetParenthetical";

(:~
 : Disclosure for the income statement.
 :)
declare variable $sec-networks:INCOME_STATEMENT as xs:string := "IncomeStatement";

(:~
 : Disclosure for the parenthetical income statement.
 :)
declare variable $sec-networks:INCOME_STATEMENT_PARENTHETICAL as xs:string := "IncomeStatementParenthetical";

(:~
 : Disclosure for the statement of comprehensive income.
 :)
declare variable $sec-networks:STATEMENT_OF_COMPREHENSIVE_INCOME as xs:string := "StatementOfComprehensiveIncome";

(:~
 : Disclosure for the cash flow statement.
 :)
declare variable $sec-networks:CASH_FLOW_STATEMENT as xs:string := "CashFlowStatement";

(:~
 : Disclosure for the parenthetical cash flow statement.
 :)
declare variable $sec-networks:CASH_FLOW_STATEMENT_PARENTHETICAL as xs:string := "CashFlowStatementParenthetical";

(:~
 : Disclosure for the default component.
 :)
declare variable $sec-networks:DEFAULT_COMPONENT as xs:string := "DefaultComponent";

(:~
 : Disclosure for the document and entity information
 :)
declare variable $sec-networks:DOCUMENT_AND_ENTITY_INFO as xs:string := "DocumentAndEntityInformation";

(:~
 :
Retrieves all SEC Networks in a given filing.

 :
 : @param $archive-or-ids an arbitrary number of archive identifiers (AIDs)
 :                          or archive objects.
 :
 : @return all networks in the supplied filings.
 :)
declare function sec-networks:networks-for-filings(
    $archives-or-ids as item*) as object*
{
  components:components-for-archives($archives-or-ids)
};

(:~
 :
Retrieves all SEC Networks that bear the supplied disclosures.

 :
 : @param $disclosure a list of disclosures.
 :
 : @return all models that match one of the disclosures.
 :)
declare function sec-networks:networks-for-disclosures(
    $disclosures as string*) as object*
{
  for $disclosure in $disclosures
  return mw:find($components:col,{ "Profiles.SEC.Disclosure": $disclosure })
};

(:~
 :
Retrieves all models that belong to the archives given as first
 : parameter and that match the supplied disclosures.

 :
 : @param $archive-or-ids an arbitrary number of archive identifiers (AIDs)
 :                          or archive objects.
 : @param $disclosures a list of disclosures.
 :
 : @return all models in the archives and that match one of the disclosures.
 :)
declare function sec-networks:networks-for-filings-and-disclosures(
    $archive-or-ids as item*,
    $disclosures as string*) as object*
{
  mw:find($components:col,
  {
    $components:ARCHIVE: { "$in" : [ $archive-or-ids ! archives:aid($$) ] },
    "Profiles.SEC.Disclosure": { "$in" : [ $disclosures ] }
  })
};

(:~
 :
Retrieves all models that belong to the archives given as first
 : parameter and that match the supplied category.

 :
 :
A category can be any of "Statement", "Disclosure", "Document",
 : "Schedule", or "Unknown".

 :
 : @param $archive-or-ids an arbitrary number of archive identifiers (AIDs)
 :                          or archive objects.
 : @param $categories a list of categories.
 :
 : @return all said models
 :)
declare function sec-networks:networks-for-filings-and-categories(
    $archive-or-ids as item*,
    $categories as string*) as object*
{
  for $aid_or_archive in $archive-or-ids
  let $aid as atomic := archives:aid($aid_or_archive)
  for $category in $categories
  return mw:find($components:col,
  {
    $components:ARCHIVE: $aid,
    "Profiles.SEC.Category": $category
  })
};

(:~
 :
Retrieves all models that belong to the archives given as first
 : parameter and that match the supplied roles/network identifier.

 :
 : @param $archive-or-ids an arbitrary number of archive identifiers (AIDs)
 :                          or archive objects.
 : @param $roles a list of roles.
 :
 : @return all said models
 :)
declare function sec-networks:networks-for-filings-and-roles(
    $archive-or-ids as item*,
    $roles as string*) as object*
{
  mw:find($components:col,
  {
    $components:ARCHIVE: { "$in" : [ $archive-or-ids ! archives:aid($$) ] },
    "Role": { "$in" : [ $roles ] }
  })
};

(:~
 :
Retrieves all models that belong to the archives given as first
 : parameter and that contain at least one report element with a given name.

 :
 : @param $archive-or-ids an arbitrary number of archive identifiers (AIDs)
 :                          or archive objects.
 : @param $report-elements a list of contained report element names.
 :
 : @return all said models
 :)
declare function sec-networks:networks-for-filings-and-reportElements(
    $archive-or-ids as item*,
    $report-elements as string*) as object*
{
  let $ids := mw:find($concepts:col,
      {
         $components:ARCHIVE: { "$in" : [ $archive-or-ids ! archives:aid($$) ] },
         "Name" : { "$in" : [ $report-elements ] }
      }).Component
  return components:components($ids)
};

(:~
 :
Retrieves all models that belong to the archives given as first
 : parameter and whose label matches the given search string.

 :
 :
The result of the function is limited to 100.

 :
 : @param $archive-or-ids an arbitrary number of archive identifiers (AIDs)
 :                          or archive objects.
 : @param $label-search-term a search term to search in labels
 :
 : @return all said models
 :)
declare function sec-networks:networks-for-filings-and-label(
    $archive-or-ids as item*,
    $label-search-term as string*) as object*
{
  sec-networks:networks-for-filings-and-label(
    $archive-or-ids, $label-search-term, 100)
};

(:~
 :
Retrieves all models that belong to the archives given as first
 : parameter and whose label matches the given search string.

 :
 :
The result of the function is limited to 100.

 :
 : @param $archive-or-ids an arbitrary number of archive identifiers (AIDs)
 :                          or archive objects.
 : @param $label-search-term a search term to search in labels
 : @param $limit limit the number of search results
 :
 : @return all said models
 :)
declare function sec-networks:networks-for-filings-and-label(
    $archive-or-ids as item*,
    $label-search-term as string*,
    $limit as integer) as object*
{
  mw:run-cmd-deterministic(
  {
    "text" : $components:col,
    "filter" : { "Archive" : { "$in" : [ $archive-or-ids ! archives:aid($$) ] } },
    "search" : $label-search-term,
    "limit" : $limit,
    "score" : { "$meta" : "textScore" },
    "sort" : { score: { "$meta" : "textScore" } }
  }).results[].obj
};

(:~
:)
declare %private function sec-networks:model-structures-recursive(
    $component as object,
    $xbrl-concepts as object*,
    $depth as integer)
    as object*
{
  for $xbrl-concept in $xbrl-concepts
  order by $xbrl-concept.Order
  let $main-object := ($component.Concepts[])[$$.Name eq $xbrl-concept.Name]
  let $kind := $main-object.Kind
  return {|
    {
      "Name" : $main-object.Name,
      "Label" : $main-object.Label,
      "Depth" : $depth
    },
    if (exists($xbrl-concept.Order))
    then { "Order" : $xbrl-concept.Order }
    else (),
    if ($kind eq "Concept")
    then trim($main-object, ("Name", "Label"))
    else (),
    let $children := sec-networks:model-structures-recursive(
                         $component,
                         (values($xbrl-concept.To), $xbrl-concept.To[]),
                         $depth + 1
                     )
    return if (exists($children)) then { Children: [ $children ] } else ()
  |}
};

(:~
 :
Computes the model structure of the supplied SEC Network, which is a hierarchy
 : of SEC Report Elements (Tables, Axes, Members, LineItems, Abstracts, Concepts).

 :
 : @param $networks a sequence of SEC Network objects.
 :
 : @return the model structures of these SEC Networks.
 :
 :)
declare function sec-networks:model-structures($networks as object*) as object*
{
  for $sec-network in $networks
  let $presentation-network := networks:networks-for-components-and-short-names(
      $sec-network,
      $networks:PRESENTATION_NETWORK)
  return sec-networks:model-structures-recursive(
      $sec-network,
      $presentation-network.Trees[],
      1)
};

(:~
 :
Retrieves the components with the given CID, or belonging to the given archives and corresponding to one of report
 : elements, concepts, disclosures, roles or label search.

 :
 : @param $archives-or-aids a sequence of archives or AIDs.
 : @param $components-or-ids a sequence of components or their CIDs.
 : @param $report-elements a sequence of report element names.
 : @param $concepts a sequence of concept names.
 : @param $disclosures a sequence of disclosures.
 : @param $roles a sequence of role URIs.
 : @param $search a sequence of label search strings.
 :
 : @return a sequence of components.
 :)
declare function sec-networks:components(
    $archives-or-aids as item*,
    $components-or-ids as item*,
    $report-elements as string*,
    $disclosures as string*,
    $roles as string*,
    $search as string*) as object*
{
    let $archives as object* := archives:archives($archives-or-aids)
    return(
        components:components($components-or-ids)[exists($components-or-ids)],
        if (exists(($report-elements, $disclosures, $roles, $search)))
        then (
                components:components-for-archives-and-concepts($archives, $report-elements),
                sec-networks:networks-for-filings-and-disclosures($archives, $disclosures),
                components:components-for-archives-and-roles($archives, $roles),
                sec-networks:networks-for-filings-and-label($archives, $search)
            )
        else components:components-for-archives($archives)
    )
};

(:~
 :
Builds a standard definition model out of the specified component.

 :
The concepts will be put on the y axis according to the presentation network.

 :
The other dimensions are put on the x axis, with one breakdown for each.

 :
Explicit dimensions are organized according to the dimension hierarchy from the domain-member network.

 :
Typed dimensions are organized according to the actual values.

 :
 :
One of the non-default hypercubes will be arbitrarily chosen. If none is available, the default hypercube will be picked.

 :
Auto slicing will be performed against the fact table
 :
 : @param $component a component object.
 :
 : @return a definition model
 :)
declare function sec-networks:standard-definition-models-for-components($components as object*) as object
{
    sec-networks:standard-definition-models-for-components($components, ())
};

(:~
 :
Builds a standard definition model out of the specified component.

 :
The concepts will be put on the y axis according to the presentation network.

 :
The other dimensions are put on the x axis, with one breakdown for each.

 :
Explicit dimensions are organized according to the dimension hierarchy from the domain-member network.

 :
Typed dimensions are organized according to the actual values.

 :
 : @param $component a component object.
 : @param $options
some optional parameters, including:

 :

 :
HypercubeName: a string specifying which hypercube to use. By default, one of the non-default hypercubes will be arbitrarily chosen. If none
 :  is available, the default hypercube will be picked.

 :
AutoSlice: a boolean specifying whether or not slicing should be done automatically, looking at the fact table. Deactivating auto slicing will
 :  lead to better performance, but a more verbose table. If AutoSlice is active, dimensions with only one value in the fact pool will become
 : global filters rather than breakdowns on the x axis.

 :
Slicers: an object with forced slicers.

 :

 :
 : @error components:HYPERCUBE-DOES-NOT-EXIST if the specified hypercube is not found.
 : @return a definition model
 :)
declare function sec-networks:standard-definition-models-for-components($components as object*, $options as object?) as object
{
    for $component in $components
    let $table as object := components:select-table($component, $options)

    let $auto-slice as boolean := empty($options.AutoSlice) or $options.AutoSlice
    let $facts as object*:=
        if($auto-slice)
        then hypercubes:facts($table)
        else ()
    let $dimensions as string*:= keys($table.Aspects)
    let $values-by-dimension as object := {|
        for $d in $dimensions
        return { $d : [ distinct-values($facts.Aspects.$d) ] }
    |}
    let $auto-slice-dimensions as string* :=
    (
        "xbrl:Entity"[size($values-by-dimension."xbrl:Entity") eq 1],
        keys($values-by-dimension)[
            size($values-by-dimension.$$) eq 1 and
            not $$ = ("xbrl:Entity",
                      "xbrl:Period",
                      "xbrl28:Archive",
                      "sec:Accepted",
                      "sec:FiscalYear",
                      "sec:FiscalPeriod",
                      "sec:FiscalPeriodType") ]
    )

    let $user-slice-dimensions as string* :=
        keys($options.Slicers)

    let $column-dimensions as string* := keys($values-by-dimension)[not $$ =
        ("xbrl:Concept",
        "xbrl:Period",
        "xbrl:Unit",
        "xbrl:Entity",
        "sec:Accepted",
        "xbrl28:Archive",
        "sec:FiscalYear",
        "sec:FiscalPeriod",
        "sec:FiscalPeriodType",
        $auto-slice-dimensions,
        $user-slice-dimensions)]

    let $x-breakdowns as object* := (
        components:standard-typed-dimension-breakdown($components, "xbrl:Period", $options)
          [not (($auto-slice-dimensions, $user-slice-dimensions) = "xbrl:Period")],
        for $d as string in $column-dimensions
        let $dimension-object as object := $table.Aspects.$d
        return
            if($d = ("sec:Accepted", "sec:FiscalYear", "sec:FiscalPeriod", "sec:FiscalPeriodType"))
            then components:standard-typed-dimension-breakdown(
                $components,
                $d,
                $options)
            else components:standard-explicit-dimension-breakdown(
                $components,
                $d,
                $dimension-object.Members[].Name,
                $component.Role,
                $options),
        components:standard-typed-dimension-breakdown($components, "xbrl:Entity", $options)
          [not (($auto-slice-dimensions, $user-slice-dimensions) = "xbrl:Entity")]
    )

    let $lineitems as string* := components:line-items($component)
    let $presentation-network as object? := networks:networks-for-components-and-short-names($component, "Presentation")
    let $roots as string* := distinct-values($presentation-network.Trees[].Name)
    let $lineitems as string* := if(exists($lineitems)) then $lineitems else $roots
    let $y-breakdowns as object := components:standard-concept-breakdown($lineitems, $component.Role)

    return {
        ModelKind: "DefinitionModel",
        Labels: [$component.Label],
        Parameters: {},
        Breakdowns: {
            "x" : [
                $x-breakdowns
            ],
            "y": [
                $y-breakdowns
            ]
        },
        TableFilters: {|
            for $d as string in distinct-values(($auto-slice-dimensions, $user-slice-dimensions))
            return if($d = $user-slice-dimensions)
                   then { $d : $options.Slicers.$d }
                   else { $d : $values-by-dimension.$d[] },
            if (not $auto-slice)
            then { "xbrl28:Archive" : $component.Archive }
            else ()
        |}
    }
};

(:~
 :
Returns all SEC Tables contained in the supplied SEC Networks.

 :
 :
SEC Tables are XBRL hypercubes.

 :
 : @param $networks a sequence of SEC Network objects.
 : @param $options standard SEC BizQL options.
 :
 : @return the SEC Tables.
 :
 :)
declare function sec-networks:tables($networks as object*, $options as object?) as object*
{
  let $include-implied-table as boolean := if(exists($options("IncludeImpliedTable")))
                                           then $options("IncludeImpliedTable")
                                           else false
  for $sec-network in $networks
  let $hypercube-metadata as object* := ($sec-network.Concepts[])[$$.Kind eq "Hypercube"]
  return if (exists($hypercube-metadata) or not $include-implied-table)
         then $hypercube-metadata
         else {
           Name: "xbrl28:ImpliedTable",
           Label: "BizQL SEC Implied Table"
         }
};

(:~
 :
Returns the names of all SEC Tables contained in the supplied SEC Networks.

 :
 :
SEC Tables are XBRL hypercubes.

 :
 : @param $networks a sequence of SEC Network objects.
 : @param $options standard SEC BizQL options.
 :
 : @return the names of the SEC Tables.
 :
 :)
declare function sec-networks:table-names($networks as object*) as string*
{
  let $names := $networks.Hypercubes[].Name
  return if (exists($names)) then $names else "xbrl28:ImpliedTable"
};

(:~
 :
 :
Retrieves all facts belonging to the SEC Network.

 :
 : @param $networks-or-ids a sequence of SEC Network objects, or their XBRL Component IDs.
 : @return a sequence of facts.
 :)
declare function sec-networks:facts(
    $networks-or-ids as item*
)
as object*
{
  sec-networks:facts($networks-or-ids, ())
};

(:~
 :
 :
Retrieves all facts belonging to the SEC Network.

 :
 : @param $networks-or-ids a sequence of SEC Network objects, or their XBRL Component IDs.
 : @return a array populated with fact values.
 :)
declare function sec-networks:fact-tables(
    $networks-or-ids as item*
)
as array
{
  sec-networks:fact-tables($networks-or-ids, ())
};

(:~
 :
 :
Retrieves all facts belonging to the SEC Networks and populates the model structures.

 :
 : @param $networks-or-ids a sequence of SEC Network objects, or their XBRL Component IDs.
 :
 : @return the populated model structures (Facts array fields).
 :)
declare function sec-networks:populated-model-structures(
    $networks-or-ids as item*
)
as object*
{
  sec-networks:populated-model-structures($networks-or-ids, ())
};

(:~
 :
 :
Retrieves all facts belonging to the SEC Network.

 :
 : @param $networks a sequence of SEC Network objects.
 : @param $options standard SEC BizQL options.
 :
 : @return a sequence of facts.
 :)
declare function sec-networks:facts(
    $networks as object*,
    $options as object?
)
as object*
{
  for $component as object in $networks
  for $table as string? allowing empty in sec-networks:table-names($component)[$$ ne "xbrl28:ImpliedTable"]
  let $hypercube as object? := hypercubes:hypercubes-for-components($component, $table)
  let $hypercube as object := if (exists($hypercube))
                              then $hypercube
                              else sec:dimensionless-hypercube({
                                  Concepts: [ components:concepts($component) ]
                              })
  let $facts := hypercubes:facts-for-hypercube(
    $hypercube,
    $component.Archive,
    if (exists($options.Filter.Profiles.SEC.Fiscal))
    then $options
    else facts:merge-objects(
        { Filter: { "Profiles.SEC.Fiscal" : { "$exists" : true } } },
        $options,
        true
    )
  )
  return sec:hide-amended-facts($facts, $options)
};

(:~
 :
 :
Retrieves all facts belonging to the SEC Network.

 :
 : @param $networks a sequence of SEC Network objects.
 : @param $options standard SEC BizQL options.
 :
 : @return a array of arrays filled with fact values.
 :)
declare function sec-networks:fact-tables(
    $networks as object*,
    $options as object?
)
as array
{
  for $component as object in $networks
  for $table as string? allowing empty in sec-networks:table-names($component)[$$ ne "xbrl28:ImpliedTable"]
  let $hypercube as object? := hypercubes:hypercubes-for-components($component, $table)
  let $hypercube as object := if (exists($hypercube))
                              then $hypercube
                              else sec:dimensionless-hypercube({
                                  Concepts: [ components:concepts($component) ]
                              })
  return hypercubes:fact-table-for-hypercube(
      $hypercube,
      $component.Archive,
      {|
        $options,
        { "facts-for-archives-and-concepts": sec:facts-for-archives-and-concepts#3 }
      |}
  )
};

(:~
 :
 :
Retrieves all facts belonging to the SEC Networks and populates the model structures.

 :
 : @param $networks-or-ids a sequence of SEC Network objects, or their XBRL Component IDs.
 : @param $options standard SEC BizQL options.
 :
 : @return the populated model structures (Facts array fields).
 :)
declare function sec-networks:populated-model-structures(
    $networks-or-ids as item*,
    $options as object?
)
as object*
{
  let $fact-table-by-concepts := {|
    for $f in sec-networks:facts($networks-or-ids, $options)
    group by $concept := $f.Aspects."xbrl:Concept"
    return { $concept : [ $f ] }
  |}
  return
    copy $result := sec-networks:model-structures($networks-or-ids)
    modify
      for $concept in ($result, descendant-objects(values($result)))[$$.Kind eq "Concept"]
      let $name := $concept.Name
      return insert json { "Facts" : $fact-table-by-concepts.$name } into $concept
    return $result
};

(:~
 :
 :
Returns the disclosures of the suplied networks.

 :
 : @param $networks a sequence of SEC Network objects.
 : @return the disclosure names, or "UncategorizedInformation" if none.
 :
 :)
declare function sec-networks:disclosures($networks as object*) as string+
{
  for $component in $networks
  let $disclosure := $component.Profiles.SEC.Disclosure
  return if (exists($disclosure))
         then $disclosure
         else "UncategorizedInformation"
};

(:~
 :
 :
Return the categories of the supplied SEC networks (Statement, Disclosure, Document or Schedule).

 :
 : @param $networks a sequence of SEC Network objects.
 :
 : @return the category of each network.
 :)
declare function sec-networks:categories($networks as object*) as string*
{
  for $component in $networks
  return normalize-space(tokenize($component.Label, "-")[2])
};

(:~
 :
 :
Return the sub-categories of the supplied SEC networks (Detail, TextBlockLevel4, TextBLockLevel1to3).

 :
 : @param $networks a sequence of SEC Network objects.
 :
 : @return the sub-category of each network.
 :)
declare function sec-networks:sub-categories($networks as object*) as string*
{
  for $network in $networks
  let $is-text-blocks as boolean* := ($network.Concepts[])
    [not $$.SubstitutionGroup = ("xbrldt:hypercubeItem", "xbrldt:dimensionItem") and not $$.IsAbstract].IsTextBlock
  let $exists-text-blocks as boolean := exists($is-text-blocks[$$])
  let $exists-non-text-blocks as boolean := exists($is-text-blocks[not $$])
  return switch(true)
      case (: no text blocks, but still non-text-blocks :) $exists-non-text-blocks and not $exists-text-blocks return "Detail"
      case (: both :)  $exists-non-text-blocks (: and $exists-text-blocks :) return "TextBlockLevel4"
      case (: only text blocks :) $exists-text-blocks return "TextBlockLevel1To3"
      default (: exists nothing :) return "Unknown"
};

(:~
 :
 :
Return summary information for the supplied SEC networks.

 :
 : @param $networks a sequence of SEC Network objects.
 :
 : @return one object per network, containing a summary.
 :)
declare function sec-networks:summaries($networks as object*) as object*
{
  for $component in $networks
  return {
    NetworkLabel : $component.Label,
    NetworkIdentifier : $component.Role,
    Category : sec-networks:categories($component),
    SubCategory : sec-networks:sub-categories($component),
    Table : components:hypercubes($component)[1],
    Disclosure : sec-networks:disclosures($component),
    ReportElements : components:num-report-elements($component),
    Tables : components:num-hypercubes($component),
    Axis : components:num-dimensions($component),
    Members : components:num-members($component),
    LineItems : components:num-line-items($component),
    Concepts : components:num-concepts($component),
    Abstracts : components:num-abstracts($component)
  }
};

(:~
 :
Returns an XML representation of the given component summaries.

 :
 : @param $summaries summaries of the components to convert to XML.
 :
 : @return a sequence of elements, each one representing one component summary as XML
 :)
declare function sec-networks:summaries-to-xml($summaries as object*) as element()*
{
  for $s in $summaries
  return
    <Component>
        <NetworkLabel>{$s.NetworkLabel}</NetworkLabel>
        <NetworkIdentifier>{$s.NetworkIdentifier}</NetworkIdentifier>
        <Category>{$s.Category}</Category>
        <SubCategory>{$s.SubCategory}</SubCategory>

{jn:flatten($s.Table)}

        <Disclosure>{$s.Disclosure}</Disclosure>
        <Tables>{$s.Tables}</Tables>
        <Axis>{$s.Axis}</Axis>
        <Members>{$s.Members}</Members>
        <LineItems>{$s.LineItems}</LineItems>
        <Concepts>{$s.Concepts}</Concepts>
        <Abstracts>{$s.Abstracts}</Abstracts>
    </Component>
};

(:~
 :
Returns a CSV representation of the given network summaries.

 :
 :
Commas are used as column separators. Abscent values are serialized
 : as empty strings.

 :
 : @param $summaries summaries of the networks to convert to CSV.
 :
 : @return a sequence of strings, each one representing one networks summary as CSV
 :)
declare function sec-networks:summaries-to-csv($summaries as object*) as string*
{
  csv:serialize($summaries, { serialize-null-as : "" })
};

jsoniq version "1.0";

(:
 : Copyright 2012-2013 28msec Inc.
 :)

(:~
 :
This module provides functionality for querying SEC networks in SEC filings.

 :
SEC Networks are actually XBRL Components. In the SEC profiles, all XBRL networks
 : in an XBRL component must be consistent to each other. A way to look at it is that
 : the SEC Network can be identified with the XBRL presentation network it contains.

 :
 :
For XBRL-generic operations on XBRL components, use the
 : generic components module.

 :
 : @author Charles Hoffman
 : @author Matthias Brantner
 : @author Dennis Knochenwefel
 : @author Ghislain Fourny
 :)
module namespace sec-networks = "http://28.io/modules/xbrl/profiles/sec/networks";

import module namespace mw = "http://28.io/modules/xbrl/mongo-wrapper";

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

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

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

declare namespace ver = "http://zorba.io/options/versioning";
declare option ver:module-version "1.0";

(:~
 : Disclosure for the balance sheet.
 :)
declare variable $sec-networks:BALANCE_SHEET as xs:string := "BalanceSheet";

(:~
 : Disclosure for the parenthetical balance sheet.
 :)
declare variable $sec-networks:BALANCE_SHEET_PARENTHETICAL as xs:string := "BalanceSheetParenthetical";

(:~
 : Disclosure for the income statement.
 :)
declare variable $sec-networks:INCOME_STATEMENT as xs:string := "IncomeStatement";

(:~
 : Disclosure for the parenthetical income statement.
 :)
declare variable $sec-networks:INCOME_STATEMENT_PARENTHETICAL as xs:string := "IncomeStatementParenthetical";

(:~
 : Disclosure for the statement of comprehensive income.
 :)
declare variable $sec-networks:STATEMENT_OF_COMPREHENSIVE_INCOME as xs:string := "StatementOfComprehensiveIncome";

(:~
 : Disclosure for the cash flow statement.
 :)
declare variable $sec-networks:CASH_FLOW_STATEMENT as xs:string := "CashFlowStatement";

(:~
 : Disclosure for the parenthetical cash flow statement.
 :)
declare variable $sec-networks:CASH_FLOW_STATEMENT_PARENTHETICAL as xs:string := "CashFlowStatementParenthetical";

(:~
 : Disclosure for the default component.
 :)
declare variable $sec-networks:DEFAULT_COMPONENT as xs:string := "DefaultComponent";

(:~
 : Disclosure for the document and entity information
 :)
declare variable $sec-networks:DOCUMENT_AND_ENTITY_INFO as xs:string := "DocumentAndEntityInformation";

(:~
 :
Retrieves all SEC Networks in a given filing.

 :
 : @param $archive-or-ids an arbitrary number of archive identifiers (AIDs)
 :                          or archive objects.
 :
 : @return all networks in the supplied filings.
 :)
declare function sec-networks:networks-for-filings(
    $archives-or-ids as item*) as object*
{
  components:components-for-archives($archives-or-ids)
};

(:~
 :
Retrieves all SEC Networks that bear the supplied disclosures.

 :
 : @param $disclosure a list of disclosures.
 :
 : @return all models that match one of the disclosures.
 :)
declare function sec-networks:networks-for-disclosures(
    $disclosures as string*) as object*
{
  for $disclosure in $disclosures
  return mw:find($components:col,{ "Profiles.SEC.Disclosure": $disclosure })
};

(:~
 :
Retrieves all models that belong to the archives given as first
 : parameter and that match the supplied disclosures.

 :
 : @param $archive-or-ids an arbitrary number of archive identifiers (AIDs)
 :                          or archive objects.
 : @param $disclosures a list of disclosures.
 :
 : @return all models in the archives and that match one of the disclosures.
 :)
declare function sec-networks:networks-for-filings-and-disclosures(
    $archive-or-ids as item*,
    $disclosures as string*) as object*
{
  mw:find($components:col,
  {
    $components:ARCHIVE: { "$in" : [ $archive-or-ids ! archives:aid($$) ] },
    "Profiles.SEC.Disclosure": { "$in" : [ $disclosures ] }
  })
};

(:~
 :
Retrieves all models that belong to the archives given as first
 : parameter and that match the supplied category.

 :
 :
A category can be any of "Statement", "Disclosure", "Document",
 : "Schedule", or "Unknown".

 :
 : @param $archive-or-ids an arbitrary number of archive identifiers (AIDs)
 :                          or archive objects.
 : @param $categories a list of categories.
 :
 : @return all said models
 :)
declare function sec-networks:networks-for-filings-and-categories(
    $archive-or-ids as item*,
    $categories as string*) as object*
{
  for $aid_or_archive in $archive-or-ids
  let $aid as atomic := archives:aid($aid_or_archive)
  for $category in $categories
  return mw:find($components:col,
  {
    $components:ARCHIVE: $aid,
    "Profiles.SEC.Category": $category
  })
};

(:~
 :
Retrieves all models that belong to the archives given as first
 : parameter and that match the supplied roles/network identifier.

 :
 : @param $archive-or-ids an arbitrary number of archive identifiers (AIDs)
 :                          or archive objects.
 : @param $roles a list of roles.
 :
 : @return all said models
 :)
declare function sec-networks:networks-for-filings-and-roles(
    $archive-or-ids as item*,
    $roles as string*) as object*
{
  mw:find($components:col,
  {
    $components:ARCHIVE: { "$in" : [ $archive-or-ids ! archives:aid($$) ] },
    "Role": { "$in" : [ $roles ] }
  })
};

(:~
 :
Retrieves all models that belong to the archives given as first
 : parameter and that contain at least one report element with a given name.

 :
 : @param $archive-or-ids an arbitrary number of archive identifiers (AIDs)
 :                          or archive objects.
 : @param $report-elements a list of contained report element names.
 :
 : @return all said models
 :)
declare function sec-networks:networks-for-filings-and-reportElements(
    $archive-or-ids as item*,
    $report-elements as string*) as object*
{
  let $ids := mw:find($concepts:col,
      {
         $components:ARCHIVE: { "$in" : [ $archive-or-ids ! archives:aid($$) ] },
         "Name" : { "$in" : [ $report-elements ] }
      }).Component
  return components:components($ids)
};

(:~
 :
Retrieves all models that belong to the archives given as first
 : parameter and whose label matches the given search string.

 :
 :
The result of the function is limited to 100.

 :
 : @param $archive-or-ids an arbitrary number of archive identifiers (AIDs)
 :                          or archive objects.
 : @param $label-search-term a search term to search in labels
 :
 : @return all said models
 :)
declare function sec-networks:networks-for-filings-and-label(
    $archive-or-ids as item*,
    $label-search-term as string*) as object*
{
  sec-networks:networks-for-filings-and-label(
    $archive-or-ids, $label-search-term, 100)
};

(:~
 :
Retrieves all models that belong to the archives given as first
 : parameter and whose label matches the given search string.

 :
 :
The result of the function is limited to 100.

 :
 : @param $archive-or-ids an arbitrary number of archive identifiers (AIDs)
 :                          or archive objects.
 : @param $label-search-term a search term to search in labels
 : @param $limit limit the number of search results
 :
 : @return all said models
 :)
declare function sec-networks:networks-for-filings-and-label(
    $archive-or-ids as item*,
    $label-search-term as string*,
    $limit as integer) as object*
{
  mw:run-cmd-deterministic(
  {
    "text" : $components:col,
    "filter" : { "Archive" : { "$in" : [ $archive-or-ids ! archives:aid($$) ] } },
    "search" : $label-search-term,
    "limit" : $limit,
    "score" : { "$meta" : "textScore" },
    "sort" : { score: { "$meta" : "textScore" } }
  }).results[].obj
};

(:~
:)
declare %private function sec-networks:model-structures-recursive(
    $component as object,
    $xbrl-concepts as object*,
    $depth as integer)
    as object*
{
  for $xbrl-concept in $xbrl-concepts
  order by $xbrl-concept.Order
  let $main-object := ($component.Concepts[])[$$.Name eq $xbrl-concept.Name]
  let $kind := $main-object.Kind
  return {|
    {
      "Kind" : $kind,
      "Name" : $main-object.Name,
      "Label" : $main-object.Label,
      "Depth" : $depth
    },
    if (exists($xbrl-concept.Order))
    then { "Order" : $xbrl-concept.Order }
    else (),
    if ($kind eq "Concept")
    then trim($main-object, ("Name", "Label"))
    else (),
    let $children := sec-networks:model-structures-recursive(
                         $component,
                         (values($xbrl-concept.To), $xbrl-concept.To[]),
                         $depth + 1
                     )
    return if (exists($children)) then { Children: [ $children ] } else ()
  |}
};

(:~
 :
Computes the model structure of the supplied SEC Network, which is a hierarchy
 : of SEC Report Elements (Tables, Axes, Members, LineItems, Abstracts, Concepts).

 :
 : @param $networks a sequence of SEC Network objects.
 :
 : @return the model structures of these SEC Networks.
 :
 :)
declare function sec-networks:model-structures($networks as object*) as object*
{
  for $sec-network in $networks
  let $presentation-network := networks:networks-for-components-and-short-names(
      $sec-network,
      $networks:PRESENTATION_NETWORK)
  return sec-networks:model-structures-recursive(
      $sec-network,
      $presentation-network.Trees[],
      1)
};

(:~
 :
Retrieves the components with the given CID, or belonging to the given archives and corresponding to one of report
 : elements, concepts, disclosures, roles or label search.

 :
 : @param $archives-or-aids a sequence of archives or AIDs.
 : @param $components-or-ids a sequence of components or their CIDs.
 : @param $report-elements a sequence of report element names.
 : @param $concepts a sequence of concept names.
 : @param $disclosures a sequence of disclosures.
 : @param $roles a sequence of role URIs.
 : @param $search a sequence of label search strings.
 :
 : @return a sequence of components.
 :)
declare function sec-networks:components(
    $archives-or-aids as item*,
    $components-or-ids as item*,
    $report-elements as string*,
    $disclosures as string*,
    $roles as string*,
    $search as string*) as object*
{
    let $archives as object* := archives:archives($archives-or-aids)
    return(
        components:components($components-or-ids)[exists($components-or-ids)],
        if (exists(($report-elements, $disclosures, $roles, $search)))
        then (
                components:components-for-archives-and-concepts($archives, $report-elements),
                sec-networks:networks-for-filings-and-disclosures($archives, $disclosures),
                components:components-for-archives-and-roles($archives, $roles),
                sec-networks:networks-for-filings-and-label($archives, $search)
            )
        else components:components-for-archives($archives)
    )
};

(:~
 :
Builds a standard definition model out of the specified component.

 :
The concepts will be put on the y axis according to the presentation network.

 :
The other dimensions are put on the x axis, with one breakdown for each.

 :
Explicit dimensions are organized according to the dimension hierarchy from the domain-member network.

 :
Typed dimensions are organized according to the actual values.

 :
 :
One of the non-default hypercubes will be arbitrarily chosen. If none is available, the default hypercube will be picked.

 :
Auto slicing will be performed against the fact table
 :
 : @param $component a component object.
 :
 : @return a definition model
 :)
declare function sec-networks:standard-definition-models-for-components($components as object*) as object
{
    sec-networks:standard-definition-models-for-components($components, ())
};

(:~
 :
Builds a standard definition model out of the specified component.

 :
The concepts will be put on the y axis according to the presentation network.

 :
The other dimensions are put on the x axis, with one breakdown for each.

 :
Explicit dimensions are organized according to the dimension hierarchy from the domain-member network.

 :
Typed dimensions are organized according to the actual values.

 :
 : @param $component a component object.
 : @param $options
some optional parameters, including:

 :

 :
HypercubeName: a string specifying which hypercube to use. By default, one of the non-default hypercubes will be arbitrarily chosen. If none
 :  is available, the default hypercube will be picked.

 :
AutoSlice: a boolean specifying whether or not slicing should be done automatically, looking at the fact table. Deactivating auto slicing will
 :  lead to better performance, but a more verbose table. If AutoSlice is active, dimensions with only one value in the fact pool will become
 : global filters rather than breakdowns on the x axis.

 :
Slicers: an object with forced slicers.

 :

 :
 : @error components:HYPERCUBE-DOES-NOT-EXIST if the specified hypercube is not found.
 : @return a definition model
 :)
declare function sec-networks:standard-definition-models-for-components($components as object*, $options as object?) as object
{
    for $component in $components
    let $table as object := components:select-table($component, $options)

    let $auto-slice as boolean := empty($options.AutoSlice) or $options.AutoSlice
    let $facts as object*:=
        if($auto-slice)
        then hypercubes:facts($table)
        else ()
    let $dimensions as string*:= keys($table.Aspects)
    let $values-by-dimension as object := {|
        for $d in $dimensions
        return { $d : [ distinct-values($facts.Aspects.$d) ] }
    |}
    let $auto-slice-dimensions as string* :=
    (
        "xbrl:Entity"[size($values-by-dimension."xbrl:Entity") eq 1],
        keys($values-by-dimension)[
            size($values-by-dimension.$$) eq 1 and
            not $$ = ("xbrl:Entity",
                      "xbrl:Period",
                      "xbrl28:Archive",
                      "sec:Accepted",
                      "sec:FiscalYear",
                      "sec:FiscalPeriod",
                      "sec:FiscalPeriodType") ]
    )

    let $user-slice-dimensions as string* :=
        keys($options.Slicers)

    let $column-dimensions as string* := keys($values-by-dimension)[not $$ =
        ("xbrl:Concept",
        "xbrl:Period",
        "xbrl:Unit",
        "xbrl:Entity",
        "sec:Accepted",
        "xbrl28:Archive",
        "sec:FiscalYear",
        "sec:FiscalPeriod",
        "sec:FiscalPeriodType",
        $auto-slice-dimensions,
        $user-slice-dimensions)]

    let $x-breakdowns as object* := (
        components:standard-typed-dimension-breakdown($components, "xbrl:Period", $options)
          [not (($auto-slice-dimensions, $user-slice-dimensions) = "xbrl:Period")],
        for $d as string in $column-dimensions
        let $dimension-object as object := $table.Aspects.$d
        return
            if($d = ("sec:Accepted", "sec:FiscalYear", "sec:FiscalPeriod", "sec:FiscalPeriodType"))
            then components:standard-typed-dimension-breakdown(
                $components,
                $d,
                $options)
            else components:standard-explicit-dimension-breakdown(
                $components,
                $d,
                $dimension-object.Members[].Name,
                $component.Role,
                $options),
        components:standard-typed-dimension-breakdown($components, "xbrl:Entity", $options)
          [not (($auto-slice-dimensions, $user-slice-dimensions) = "xbrl:Entity")]
    )

    let $lineitems as string* := components:line-items($component)
    let $presentation-network as object? := networks:networks-for-components-and-short-names($component, "Presentation")
    let $roots as string* := distinct-values($presentation-network.Trees[].Name)
    let $lineitems as string* := if(exists($lineitems)) then $lineitems else $roots
    let $y-breakdowns as object := components:standard-concept-breakdown($lineitems, $component.Role)

    return {
        ModelKind: "DefinitionModel",
        Labels: [$component.Label],
        Parameters: {},
        Breakdowns: {
            "x" : [
                $x-breakdowns
            ],
            "y": [
                $y-breakdowns
            ]
        },
        TableFilters: {|
            for $d as string in distinct-values(($auto-slice-dimensions, $user-slice-dimensions))
            return if($d = $user-slice-dimensions)
                   then { $d : $options.Slicers.$d }
                   else { $d : $values-by-dimension.$d[] },
            if (not $auto-slice)
            then { "xbrl28:Archive" : $component.Archive }
            else ()
        |}
    }
};

(:~
 :
Returns all SEC Tables contained in the supplied SEC Networks.

 :
 :
SEC Tables are XBRL hypercubes.

 :
 : @param $networks a sequence of SEC Network objects.
 : @param $options standard SEC BizQL options.
 :
 : @return the SEC Tables.
 :
 :)
declare function sec-networks:tables($networks as object*, $options as object?) as object*
{
  let $include-implied-table as boolean := if(exists($options("IncludeImpliedTable")))
                                           then $options("IncludeImpliedTable")
                                           else false
  for $sec-network in $networks
  let $hypercube-metadata as object* := ($sec-network.Concepts[])[$$.Kind eq "Hypercube"]
  return if (exists($hypercube-metadata) or not $include-implied-table)
         then $hypercube-metadata
         else {
           Name: "xbrl28:ImpliedTable",
           Label: "BizQL SEC Implied Table"
         }
};

(:~
 :
Returns the names of all SEC Tables contained in the supplied SEC Networks.

 :
 :
SEC Tables are XBRL hypercubes.

 :
 : @param $networks a sequence of SEC Network objects.
 : @param $options standard SEC BizQL options.
 :
 : @return the names of the SEC Tables.
 :
 :)
declare function sec-networks:table-names($networks as object*) as string*
{
  let $names := $networks.Hypercubes[].Name
  return if (exists($names)) then $names else "xbrl28:ImpliedTable"
};

(:~
 :
 :
Retrieves all facts belonging to the SEC Network.

 :
 : @param $networks-or-ids a sequence of SEC Network objects, or their XBRL Component IDs.
 : @return a sequence of facts.
 :)
declare function sec-networks:facts(
    $networks-or-ids as item*
)
as object*
{
  sec-networks:facts($networks-or-ids, ())
};

(:~
 :
 :
Retrieves all facts belonging to the SEC Network.

 :
 : @param $networks-or-ids a sequence of SEC Network objects, or their XBRL Component IDs.
 : @return a array populated with fact values.
 :)
declare function sec-networks:fact-tables(
    $networks-or-ids as item*
)
as array
{
  sec-networks:fact-tables($networks-or-ids, ())
};

(:~
 :
 :
Retrieves all facts belonging to the SEC Networks and populates the model structures.

 :
 : @param $networks-or-ids a sequence of SEC Network objects, or their XBRL Component IDs.
 :
 : @return the populated model structures (Facts array fields).
 :)
declare function sec-networks:populated-model-structures(
    $networks-or-ids as item*
)
as object*
{
  sec-networks:populated-model-structures($networks-or-ids, ())
};

(:~
 :
 :
Retrieves all facts belonging to the SEC Network.

 :
 : @param $networks a sequence of SEC Network objects.
 : @param $options standard SEC BizQL options.
 :
 : @return a sequence of facts.
 :)
declare function sec-networks:facts(
    $networks as object*,
    $options as object?
)
as object*
{
  for $component as object in $networks
  for $table as string? allowing empty in sec-networks:table-names($component)[$$ ne "xbrl28:ImpliedTable"]
  let $hypercube as object? := hypercubes:hypercubes-for-components($component, $table)
  let $hypercube as object := if (exists($hypercube))
                              then $hypercube
                              else sec:dimensionless-hypercube({
                                  Concepts: [ components:concepts($component) ]
                              })
  let $facts := hypercubes:facts-for-hypercube(
    $hypercube,
    $component.Archive,
    if (exists($options.Filter.Profiles.SEC.Fiscal))
    then $options
    else facts:merge-objects(
        { Filter: { "Profiles.SEC.Fiscal" : { "$exists" : true } } },
        $options,
        true
    )
  )
  return sec:hide-amended-facts($facts, $options)
};

(:~
 :
 :
Retrieves all facts belonging to the SEC Network.

 :
 : @param $networks a sequence of SEC Network objects.
 : @param $options standard SEC BizQL options.
 :
 : @return a array of arrays filled with fact values.
 :)
declare function sec-networks:fact-tables(
    $networks as object*,
    $options as object?
)
as array
{
  for $component as object in $networks
  for $table as string? allowing empty in sec-networks:table-names($component)[$$ ne "xbrl28:ImpliedTable"]
  let $hypercube as object? := hypercubes:hypercubes-for-components($component, $table)
  let $hypercube as object := if (exists($hypercube))
                              then $hypercube
                              else sec:dimensionless-hypercube({
                                  Concepts: [ components:concepts($component) ]
                              })
  return hypercubes:fact-table-for-hypercube(
      $hypercube,
      $component.Archive,
      {|
        $options,
        { "facts-for-archives-and-concepts": sec:facts-for-archives-and-concepts#3 }
      |}
  )
};

(:~
 :
 :
Retrieves all facts belonging to the SEC Networks and populates the model structures.

 :
 : @param $networks-or-ids a sequence of SEC Network objects, or their XBRL Component IDs.
 : @param $options standard SEC BizQL options.
 :
 : @return the populated model structures (Facts array fields).
 :)
declare function sec-networks:populated-model-structures(
    $networks-or-ids as item*,
    $options as object?
)
as object*
{
  let $fact-table-by-concepts := {|
    for $f in sec-networks:facts($networks-or-ids, $options)
    group by $concept := $f.Aspects."xbrl:Concept"
    return { $concept : [ $f ] }
  |}
  return
    copy $result := sec-networks:model-structures($networks-or-ids)
    modify
      for $concept in ($result, descendant-objects(values($result)))[$$.Kind eq "Concept"]
      let $name := $concept.Name
      return insert json { "Facts" : $fact-table-by-concepts.$name } into $concept
    return $result
};

(:~
 :
 :
Returns the disclosures of the suplied networks.

 :
 : @param $networks a sequence of SEC Network objects.
 : @return the disclosure names, or "UncategorizedInformation" if none.
 :
 :)
declare function sec-networks:disclosures($networks as object*) as string+
{
  for $component in $networks
  let $disclosure := $component.Profiles.SEC.Disclosure
  return if (exists($disclosure))
         then $disclosure
         else "UncategorizedInformation"
};

(:~
 :
 :
Return the categories of the supplied SEC networks (Statement, Disclosure, Document or Schedule).

 :
 : @param $networks a sequence of SEC Network objects.
 :
 : @return the category of each network.
 :)
declare function sec-networks:categories($networks as object*) as string*
{
  for $component in $networks
  return normalize-space(tokenize($component.Label, "-")[2])
};

(:~
 :
 :
Return the sub-categories of the supplied SEC networks (Detail, TextBlockLevel4, TextBLockLevel1to3).

 :
 : @param $networks a sequence of SEC Network objects.
 :
 : @return the sub-category of each network.
 :)
declare function sec-networks:sub-categories($networks as object*) as string*
{
  for $network in $networks
  let $is-text-blocks as boolean* := ($network.Concepts[])
    [not $$.SubstitutionGroup = ("xbrldt:hypercubeItem", "xbrldt:dimensionItem") and not $$.IsAbstract].IsTextBlock
  let $exists-text-blocks as boolean := exists($is-text-blocks[$$])
  let $exists-non-text-blocks as boolean := exists($is-text-blocks[not $$])
  return switch(true)
      case (: no text blocks, but still non-text-blocks :) $exists-non-text-blocks and not $exists-text-blocks return "Detail"
      case (: both :)  $exists-non-text-blocks (: and $exists-text-blocks :) return "TextBlockLevel4"
      case (: only text blocks :) $exists-text-blocks return "TextBlockLevel1To3"
      default (: exists nothing :) return "Unknown"
};

(:~
 :
 :
Return summary information for the supplied SEC networks.

 :
 : @param $networks a sequence of SEC Network objects.
 :
 : @return one object per network, containing a summary.
 :)
declare function sec-networks:summaries($networks as object*) as object*
{
  for $component in $networks
  return {
    NetworkLabel : $component.Label,
    NetworkIdentifier : $component.Role,
    Category : sec-networks:categories($component),
    SubCategory : sec-networks:sub-categories($component),
    Table : components:hypercubes($component)[1],
    Disclosure : sec-networks:disclosures($component),
    ReportElements : components:num-report-elements($component),
    Tables : components:num-hypercubes($component),
    Axis : components:num-dimensions($component),
    Members : components:num-members($component),
    LineItems : components:num-line-items($component),
    Concepts : components:num-concepts($component),
    Abstracts : components:num-abstracts($component)
  }
};

(:~
 :
Returns an XML representation of the given component summaries.

 :
 : @param $summaries summaries of the components to convert to XML.
 :
 : @return a sequence of elements, each one representing one component summary as XML
 :)
declare function sec-networks:summaries-to-xml($summaries as object*) as element()*
{
  for $s in $summaries
  return
    <Component>
        <NetworkLabel>{$s.NetworkLabel}</NetworkLabel>
        <NetworkIdentifier>{$s.NetworkIdentifier}</NetworkIdentifier>
        <Category>{$s.Category}</Category>
        <SubCategory>{$s.SubCategory}</SubCategory>

{jn:flatten($s.Table)}

        <Disclosure>{$s.Disclosure}</Disclosure>
        <Tables>{$s.Tables}</Tables>
        <Axis>{$s.Axis}</Axis>
        <Members>{$s.Members}</Members>
        <LineItems>{$s.LineItems}</LineItems>
        <Concepts>{$s.Concepts}</Concepts>
        <Abstracts>{$s.Abstracts}</Abstracts>
    </Component>
};

(:~
 :
Returns a CSV representation of the given network summaries.

 :
 :
Commas are used as column separators. Abscent values are serialized
 : as empty strings.

 :
 : @param $summaries summaries of the networks to convert to CSV.
 :
 : @return a sequence of strings, each one representing one networks summary as CSV
 :)
declare function sec-networks:summaries-to-csv($summaries as object*) as string*
{
  csv:serialize($summaries, { serialize-null-as : "" })
};


Use string interpolation where sensible.

Simplifies filenames.

#!/usr/bin/env jq
# This is a collection of filters for `jq`, intended to manipulate JSON objects
# into JSON representing a website index, for further processing in a template.

# Convert "truthy" value to actual boolean.
def bool:
    [. == (null,false,0,{},[])] | any | not
;

# Is a given array at least length $n?
def at_least($n):
    (. | length) >= $n
;

# Generate this object and all its children.
def all_children:
    ., recurse(.contents?[]?)
;

# To remove an object, we first mark it for removal, then actually remove it
# later. It would be nicer if we could just do `all_children ... |= empty`,
# which works in some cases but not all. I suppose it has to do with changing
# objects as we are iterating over them.
def mark:
    .remove = true
;

# Remove all objects marked for removal.
def remove_marked:
    if .remove == true
    then empty
    else (.contents // empty) |= map(remove_marked)
    end
;

# Enumerate the `make` formats of a page.
def target_formats($all_formats):
    (.make // empty)
    |   if . == "null" then empty else . end # fix for wrong YAML parse
    |   if (. | type) == "array" then .[] else . end
    |   tostring
    |   ascii_downcase
    |   if [. == $all_formats[]] | any then .
        elif . == "all" then $all_formats[]
        else error("unrecognized format: \(.)")
        end
;

# Wrap an object in other objects so that the original object exists at a
# particular "path". Like `{} | setpath(["a","b"], …)`, but instead of making
# objects like `{"a":{"b":…}}`, this makes objects like `{"name":"a",
# "contents":[{"name":"b",…}]}`. We use `name` and `contents` to be compatible
# with the output of the `tree` program on the shell.
def tree($path):
    if $path | at_least(2)
    then { "contents": [ tree($path[1:]) ] }
    else .
    end | .name = $path[0]
;

# Merge an array of page objects into a single page object.
def merge:
    map(to_entries) | add | group_by(.key) | map(
        { "key": (.[0].key)
        , "value": (
            if .[0].key == "contents"
            then [ .[].value ] | add | group_by(.name) | map(merge)
            elif [ .[].value == .[0].value ] | all
            then .[0].value
            else error("can't merge incompatible values")
            end
        ) }
    ) | from_entries
;

# Add paths to each object, that is, the names of the ancestors.
def directory:
    def f($d):
        .dir = ($d | join("/") | ltrimstr("./"))
        | .name as $n | .contents[]? |= f($d+[$n])
    ;
    f([])
;

# Every leaf node gets a basename, eg name without extension.
def basename:
    (all_children | select(has("contents") | not)) |= (
        .basename = (.name | sub(".([A-z]*)$";""))
    )
;

# Any page gets its target formats.
def formats($all_formats):
    all_children |= (
        .formats = [ target_formats($all_formats) ]
    )
;

# Any page is linked to its first target format.
def link:
    (all_children | select(has("dir") and has("basename") and (.formats | at_least(1)))) |= (
        .link = "\(.dir)/\(.basename).\(.formats[0])"
    )
;

# Front matter is the page to be associated with the enclosing directory rather
# than itself.
def frontmatter:
    (all_children | select(has("contents"))) |= (
        (.frontmatter = [.contents[] | select(.frontmatter | bool)][0]) |
        (.contents = .contents - [.frontmatter])
    )
;

# Drafts can be excluded from being uploaded and included in the table of
# contents. To not count as a draft, a document should be explicitly marked as
# "make" in the metadata.
# Anything that does not have a link, nor has any children, should be removed
# from the index.
def remove_unlinked:
    def linked: has("contents") or has("link");
    (( all_children | select(linked | not)) |= mark) | remove_marked
;

# Sort content first according to sort order in metadata.
def sort_content:
    (all_children | select(has("contents")) | .contents) |= (
        sort_by(.sort // .title // .name)
    )
;

# Add a note that tells us whether this has only children who have no more
# subchildren.
def annotate_leaves:
    (all_children | select(has("contents"))) |= (
        .only_leaves = (.contents | all(has("contents") | not))
    )
;

# Combines the given stream of JSON objects by merging them, and performs the
# given operations to turn it into a proper index.
def index($all_formats):
    merge
    | directory
    | basename
    | formats($all_formats)
    | link
    | remove_unlinked
    | frontmatter
    | sort_content
    | annotate_leaves
;

# Get target files and their dependencies as an enumeration of {"target":...,
# "dependencies":...} objects.
def targets($dest; $default_style):
    [ all_children
        | ., (.frontmatter // empty)
        | .formats[] as $format
        | "\($dest)/\(.dir)/\(.basename).\($format)" as $doc
        | ["\($dest)/\(.dir)/\(.resources?[])"] as $external
        | ["\($dest)/\(.style // $default_style).css"] as $css
        | (if $format == "html" then ($css + $external) else [] end) as $linked
        | (if $format == "pdf"  then ($css + $external) else [] end) as $embedded
        | {"target": $format, "dependencies": ([$doc] + $linked) }
        , {"target": $doc, "dependencies": $embedded }
    ]
    | group_by(.target)
    | map(
        {"target": (.[0].target)
        , "dependencies": (map(.dependencies) | add | unique) }
    )
    | .[]?
;

#!/usr/bin/env jq
# This is a collection of filters for `jq`, intended to manipulate JSON objects
# into JSON representing a website index, for further processing in a template.

# Convert "truthy" value to actual boolean.
def bool:
    [. == (null,false,0,{},[])] | any | not
;

# Is a given array at least length $n?
def at_least($n):
    (. | length) >= $n
;

# Generate this object and all its children.
def all_children:
    ., recurse(.contents?[]?)
;

# To remove an object, we first mark it for removal, then actually remove it
# later. It would be nicer if we could just do `all_children ... |= empty`,
# which works in some cases but not all. I suppose it has to do with changing
# objects as we are iterating over them.
def mark:
    .remove = true
;

# Remove all objects marked for removal.
def remove_marked:
    if .remove == true
    then empty
    else (.contents // empty) |= map(remove_marked)
    end
;

# Enumerate the `make` formats of a page.
def target_formats($all_formats):
    (.make // empty)
    |   if . == "null" then empty else . end # fix for wrong YAML parse
    |   if (. | type) == "array" then .[] else . end
    |   tostring
    |   ascii_downcase
    |   if [. == $all_formats[]] | any then .
        elif . == "all" then $all_formats[]
        else error("unrecognized format: " + .)
        end
;

# Wrap an object in other objects so that the original object exists at a
# particular "path". Like `{} | setpath(["a","b"], …)`, but instead of making
# objects like `{"a":{"b":…}}`, this makes objects like `{"name":"a",
# "contents":[{"name":"b",…}]}`. We use `name` and `contents` to be compatible
# with the output of the `tree` program on the shell.
def tree($path):
    if $path | at_least(2)
    then { "contents": [ tree($path[1:]) ] }
    else .
    end | .name = $path[0]
;

# Merge an array of page objects into a single page object.
def merge:
    map(to_entries) | add | group_by(.key) | map(
        { "key": (.[0].key)
        , "value": (
            if .[0].key == "contents"
            then [ .[].value ] | add | group_by(.name) | map(merge)
            elif [ .[].value == .[0].value ] | all
            then .[0].value
            else error("can't merge incompatible values")
            end
        ) }
    ) | from_entries
;

# Add paths to each object, that is, the names of the ancestors.
def directory:
    def f($d): .directory = $d | .name as $n | .contents[]? |= f($d+[$n]);
    f([])
;

# Every leaf node gets a basename, eg name without extension.
def basename:
    (all_children | select(has("contents") | not)) |= (
        .basename = (.name | sub(".([A-z]*)$";""))
    )
;

# Any page gets its target formats.
def formats($all_formats):
    all_children |= (
        .formats = [ target_formats($all_formats) ]
    )
;

# Any page is linked to its first target format.
def link:
    (all_children | select(has("directory") and has("basename") and (.formats | at_least(1)))) |= (
        .link = ((.directory + [.basename + "." + .formats[0]]) | join("/") | ltrimstr("./"))
    )
;

# Front matter is the page to be associated with the enclosing directory rather
# than itself.
def frontmatter:
    (all_children | select(has("contents"))) |= (
        (.frontmatter = [.contents[] | select(.frontmatter | bool)][0]) |
        (.contents = .contents - [.frontmatter])
    )
;

# Drafts can be excluded from being uploaded and included in the table of
# contents. To not count as a draft, a document should be explicitly marked as
# "make" in the metadata.
# Anything that does not have a link, nor has any children, should be removed
# from the index.
def remove_unlinked:
    def linked: has("contents") or has("link");
    (( all_children | select(linked | not)) |= mark) | remove_marked
;

# Sort content first according to sort order in metadata.
def sort_content:
    (all_children | select(has("contents")) | .contents) |= (
        sort_by(.sort // .title // .name)
    )
;

# Add a note that tells us whether this has only children who have no more
# subchildren.
def annotate_leaves:
    (all_children | select(has("contents"))) |= (
        .only_leaves = (.contents | all(has("contents") | not))
    )
;

# Combines the given stream of JSON objects by merging them, and performs the
# given operations to turn it into a proper index.
def index($all_formats):
    merge
    | directory
    | basename
    | formats($all_formats)
    | link
    | remove_unlinked
    | frontmatter
    | sort_content
    | annotate_leaves
;

# Get target files and their dependencies as an enumeration of {"target":...,
# "dependencies":...} objects.
def targets($dest; $default_style):
    def in_dir($dir; $file):
        $dir + [$file] | join("/") | ltrimstr("./") | ($dest + "/" + .)
    ;
    [ all_children
        | ., (.frontmatter // empty)
        | select(has("directory") and has("basename"))
        | .formats[] as $format
        | in_dir(.directory; .basename + "." + $format) as $doc
        | [in_dir(.directory; .resources?[])] as $external
        | [in_dir([]; (.style // $default_style) + ".css")] as $css
        | (if $format == "html" then ($css + $external) else [] end) as $linked
        | (if $format == "pdf"  then ($css + $external) else [] end) as $embedded
        | {"target": $format, "dependencies": ([$doc] + $linked) }
        , {"target": $doc, "dependencies": $embedded }
    ]
    | group_by(.target)
    | map(
        {"target": (.[0].target)
        , "dependencies": (map(.dependencies) | add | unique) }
    )
    | .[]?
;


Links now determine presence in index.

Links now determine presence in index.

Before, we would need to explicitly determine whether an entry shows up
in the index. We now use the `link` instead, which is only present for
entries with a target format, so the effect is the same.

It also removes the necessity of having an `external` marker for
external links in the index.

#!/usr/bin/env jq
# This is a collection of filters for `jq`, intended to manipulate JSON objects
# into JSON representing a website index, for further processing in a template.

# Convert "truthy" value to actual boolean.
def bool:
    [. == (null,false,0,{},[])] | any | not
;

# Is a given array at least length $n?
def at_least($n):
    (. | length) >= $n
;

# Generate this object and all its children.
def all_children:
    ., recurse(.contents?[]?)
;

# To remove an object, we first mark it for removal, then actually remove it
# later. It would be nicer if we could just do `all_children ... |= empty`,
# which works in some cases but not all. I suppose it has to do with changing
# objects as we are iterating over them.
def mark:
    .remove = true
;

# Remove all objects marked for removal.
def remove_marked:
    if .remove == true
    then empty
    else (.contents // empty) |= map(remove_marked)
    end
;

# Enumerate the `make` formats of a page.
def target_formats($all_formats):
    (.make // empty)
    |   if . == "null" then empty else . end # fix for wrong YAML parse
    |   if (. | type) == "array" then .[] else . end
    |   tostring
    |   ascii_downcase
    |   if [. == $all_formats[]] | any then .
        elif . == "all" then $all_formats[]
        else error("unrecognized format: " + .)
        end
;

# Wrap an object in other objects so that the original object exists at a
# particular "path". Like `{} | setpath(["a","b"], …)`, but instead of making
# objects like `{"a":{"b":…}}`, this makes objects like `{"name":"a",
# "contents":[{"name":"b",…}]}`. We use `name` and `contents` to be compatible
# with the output of the `tree` program on the shell.
def tree($path):
    if $path | at_least(2)
    then { "contents": [ tree($path[1:]) ] }
    else .
    end | .name = $path[0]
;

# Merge an array of page objects into a single page object.
def merge:
    map(to_entries) | add | group_by(.key) | map(
        { "key": (.[0].key)
        , "value": (
            if .[0].key == "contents"
            then [ .[].value ] | add | group_by(.name) | map(merge)
            elif [ .[].value == .[0].value ] | all
            then .[0].value
            else error("can't merge incompatible values")
            end
        ) }
    ) | from_entries
;

# Add paths to each object, that is, the names of the ancestors.
def directory:
    def f($d): .directory = $d | .name as $n | .contents[]? |= f($d+[$n]);
    f([])
;

# Every leaf node gets a basename, eg name without extension.
def basename:
    (all_children | select(has("contents") | not)) |= (
        .basename = (.name | sub(".([A-z]*)$";""))
    )
;

# Any page gets its target formats.
def formats($all_formats):
    all_children |= (
        .formats = [ target_formats($all_formats) ]
    )
;

# Any page is linked to its first target format.
def link:
    (all_children | select(has("directory") and has("basename") and (.formats | at_least(1)))) |= (
        .link = ((.directory + [.basename + "." + .formats[0]]) | join("/") | ltrimstr("./"))
    )
;

# Front matter is the page to be associated with the enclosing directory rather
# than itself.
def frontmatter:
    (all_children | select(has("contents"))) |= (
        (.frontmatter = [.contents[] | select(.frontmatter | bool)][0]) |
        (.contents = .contents - [.frontmatter])
    )
;

# Drafts can be excluded from being uploaded and included in the table of
# contents. To not count as a draft, a document should be explicitly marked as
# "make" in the metadata.
# Anything that does not have a link, nor has any children, should be removed
# from the index.
def remove_unlinked:
    def linked: has("contents") or has("link");
    (( all_children | select(linked | not)) |= mark) | remove_marked
;

# Sort content first according to sort order in metadata.
def sort_content:
    (all_children | select(has("contents")) | .contents) |= (
        sort_by(.sort // .title // .name)
    )
;

# Add a note that tells us whether this has only children who have no more
# subchildren.
def annotate_leaves:
    (all_children | select(has("contents"))) |= (
        .only_leaves = (.contents | all(has("contents") | not))
    )
;

# Combines the given stream of JSON objects by merging them, and performs the
# given operations to turn it into a proper index.
def index($all_formats):
    merge
    | directory
    | basename
    | formats($all_formats)
    | link
    | remove_unlinked
    | frontmatter
    | sort_content
    | annotate_leaves
;

# Get a list of target documents and their Makefile dependencies as an array of
# {"target":..., "deps":...} objects.
def targets($dest; $default_style):
    def in_dir($dir; $file):
        $dir + [$file] | join("/") | ltrimstr("./") | ($dest + "/" + .)
    ;
    [ all_children
        | ., (.frontmatter // empty)
        | select(has("directory") and has("basename"))
        | .formats[] as $format
        | in_dir(.directory; .basename + "." + $format) as $doc
        | [in_dir(.directory; .resources?[])] as $external
        | [in_dir([]; (.style // $default_style) + ".css")] as $css
        | (if $format == "html" then ($css + $external) else [] end) as $linked
        | (if $format == "pdf"  then ($css + $external) else [] end) as $embedded
        | {"target": $format, "deps": ([$doc] + $linked) }
        , {"target": $doc, "deps": $embedded }
    ]
    | group_by(.target)
    | map({"target":(.[0].target), "deps": (map(.deps) | add | unique)})
    | . // []
;

# Turn a target object into makefile recipes.
def as_makefile:
    .[] | (.target + ": " + (.deps | join(" ")))
;

#!/usr/bin/env jq
# This is a collection of filters for `jq`, intended to manipulate JSON objects
# into JSON representing a website index, for further processing in a template.

# Convert "truthy" value to actual boolean.
def bool:
    [. == (null,false,0,{},[])] | any | not
;

# Is a given array at least length $n?
def at_least($n):
    (. | length) >= $n
;

# Generate this object and all its children.
def all_children:
    ., recurse(.contents?[]?)
;

# To remove an object, we first mark it for removal, then actually remove it
# later. It would be nicer if we could just do `all_children ... |= empty`,
# which works in some cases but not all. I suppose it has to do with changing
# objects as we are iterating over them.
def mark:
    .remove = true
;

# Remove all objects marked for removal.
def remove_marked:
    if .remove == true
    then empty
    else (.contents // empty) |= map(remove_marked)
    end
;

# Enumerate the `make` formats of a page.
def target_formats($all_formats):
    (.make // empty)
    |   if . == "null" then empty else . end # fix for wrong YAML parse
    |   if (. | type) == "array" then .[] else . end
    |   tostring
    |   ascii_downcase
    |   if [. == $all_formats[]] | any then .
        elif . == "all" then $all_formats[]
        else error("unrecognized format: " + .)
        end
;

# Wrap an object in other objects so that the original object exists at a
# particular "path". Like `{} | setpath(["a","b"], …)`, but instead of making
# objects like `{"a":{"b":…}}`, this makes objects like `{"name":"a",
# "contents":[{"name":"b",…}]}`.
def tree($path):
    { "name": $path[0] } +
    if $path | at_least(2)
    then { "contents": [ tree($path[1:]) ] }
    else .
    end
;

# Merge an array of page objects into a single page object.
def merge:
    map(to_entries) | add | group_by(.key) | map(
        { "key": (.[0].key)
        , "value": (
            if .[0].key == "contents"
            then [ .[].value ] | add | group_by(.name) | map(merge)
            elif [ .[].value == .[0].value ] | all
            then .[0].value
            else error("can't merge incompatible values")
            end
        ) }
    ) | from_entries
;

# Add paths to each object, that is, the names of the ancestors.
def directory:
    def f($d): .directory = $d | .name as $n | .contents[]? |= f($d+[$n]);
    f([])
;

# Every leaf node gets a basename, eg name without extension.
def basename:
    (all_children | select(has("contents") | not)) |= (
        .basename = (.name | sub(".([A-z]*)$";""))
    )
;

# Any page gets its target formats.
def formats($all_formats):
    all_children |= (
        .formats = [ target_formats($all_formats) ]
    )
;

# Any page is linked to its first target.
def link:
    (all_children | select(has("directory") and has("basename") and (.formats | at_least(1)))) |= (
        .link = ((.directory + [.basename + "." + .formats[0]]) | join("/") | ltrimstr("./"))
    )
;

# Front matter is the page to be associated with the enclosing directory rather
# than itself.
def frontmatter:
    (all_children | select(has("contents"))) |= (
        (.frontmatter = [.contents[] | select(.frontmatter | bool)][0]) |
        (.contents = .contents - [.frontmatter])
    )
;

# Drafts can be excluded from being uploaded and included in the table of
# contents. To not count as a draft, a document should be explicitly marked as
# "make" in the metadata.
def explicit_make:
    def make: has("contents") or .external or (.formats | at_least(1));
    (( all_children | select(make | not)) |= mark) | remove_marked
;

# Sort content first according to sort order in metadata.
def sort_content:
    (all_children | select(has("contents")) | .contents) |= (
        sort_by(.sort // .title // .name)
    )
;

# Add a note that tells us whether this has only children who have no more
# subchildren.
def annotate_leaves:
    (all_children | select(has("contents"))) |= (
        .only_leaves = (.contents | all(has("contents") | not))
    )
;

# Combines the given stream of JSON objects by merging them, and performs the
# given operations to turn it into a proper index.
def index($all_formats):
    merge
    | directory
    | basename
    | formats($all_formats)
    | link
    | explicit_make
    | frontmatter
    | sort_content
    | annotate_leaves
;

# Get a list of target documents and their Makefile dependencies as an array of
# {"target":..., "deps":...} objects.
def targets($dest; $default_style):
    def in_dir($dir; $file):
        $dir + [$file] | join("/") | ltrimstr("./") | ($dest + "/" + .)
    ;
    [ all_children
        | ., (.frontmatter // empty)
        | select(has("directory") and has("basename") and (has("external") | not))
        | .formats[] as $format
        | in_dir(.directory; .basename + "." + $format) as $doc
        | [in_dir(.directory; .resources?[])] as $external
        | [in_dir([]; (.style // $default_style) + ".css")] as $css
        | (if $format == "html" then ($css + $external) else [] end) as $linked
        | (if $format == "pdf"  then ($css + $external) else [] end) as $embedded
        | {"target": $format, "deps": ([$doc] + $linked) }
        , {"target": $doc, "deps": $embedded }
    ]
    | group_by(.target)
    | map({"target":(.[0].target), "deps": (map(.deps) | add | unique)})
    | . // []
;

# Turn a target object into makefile recipes.
def as_makefile:
    .[] | (.target + ": " + (.deps | join(" ")))
;


Update facts.jq

Update facts.jq

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

import module namespace conversion = "http://28.io/modules/xbrl/conversion";
import module namespace hypercubes = "http://28.io/modules/xbrl/hypercubes";
import module namespace entities = "http://28.io/modules/xbrl/entities";
import module namespace reports = "http://28.io/modules/xbrl/reports";
import module namespace concepts = "http://28.io/modules/xbrl/concepts";
import module namespace facts = "http://28.io/modules/xbrl/facts";
import module namespace rules = "http://28.io/modules/xbrl/rules";
import module namespace components = "http://28.io/modules/xbrl/components";

import module namespace sec = "http://28.io/modules/xbrl/profiles/sec/core";
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 request = "http://www.28msec.com/modules/http-request";

declare function local:param-values($name as string) as string*
{
    switch(true)
     case $name eq "xbrl:Concept"
        return (request:param-values("concept"), request:param-values("xbrl:Concept"))

     case $name eq "sec:FiscalYear" and $profile-name eq "sec"
        return ($fiscalYear, request:param-values("sec:FiscalYear"))[$$ ne "LATEST"]
     case $name eq "sec:FiscalYear::type" and $profile-name eq "sec"
        return "integer"
     case $name eq "sec:FiscalPeriod" and $profile-name eq "sec"
        return ($fiscalPeriod, request:param-values("sec:FiscalPeriod"))
     case $name eq "dei:LegalEntityAxis" and $profile-name eq "sec"
        return
         if(empty((request:param-values("sec:LegalEntityAxis"), request:param-values("sec:LegalEntityAxis::default"))))
         then "sec:DefaultLegalEntity"
         else request:param-values("sec:LegalEntityAxis")
     case $name eq "dei:LegalEntityAxis::default" return
         if(empty((request:param-values("sec:LegalEntityAxis"), request:param-values("sec:LegalEntityAxis::default"))))
         then "sec:DefaultLegalEntity"
         else request:param-values("sec:LegalEntityAxis::default")
     case $name eq "xbrl:Entity" and $profile-name eq "sec" return (
         let $companies := companies:companies(
            $cik,
            api:preprocess-tags($tag),
            $ticker,
            $sic)._id
        return if(empty(($cik,$tag,$ticker,$sic)) or exists($companies))
               then $companies
               else "dummy",
        request:param-values("xbrl:Entity"))
     case $name eq "sec:Archive" and $profile-name eq "sec" return (
            let $fiscalYears := ($fiscalYear, request:param-values("sec:FiscalYear"))
            let $fiscalPeriods := local:param-values("sec:FiscalPeriod")
            let $entities := entities:entities(local:param-values("xbrl:Entity"))
            return
                if($fiscalYears = "LATEST")
                then fiscal-core:latest-filings($entities, $fiscalPeriods)._id
                else (),
            $aid,
            request:param-values("sec:Archive")
        )
     default return request:param-values($name)
};

declare function local:param-names() as string*
{
    let $names := request:param-names()
    return distinct-values((
        $names[contains($$, ":")],
        "xbrl:Concept"[$names = "concept"],

        "sec:Accepted"[$profile-name eq "sec"],
        "sec:FiscalPeriod"[$profile-name eq "sec"],
        "sec:FiscalYear"[$profile-name eq "sec"],
        "xbrl:Entity"[$profile-name eq "sec" and $names = ("cik", "tag", "ticker", "sic")],
        "dei:LegalEntityAxis"[$profile-name eq "sec"],
        "dei:LegalEntityAxis::default"[$profile-name eq "sec"],
        "sec:Archive"[$profile-name eq "sec"]))
};

declare function local:cast-sequence($values as atomic*, $type as string) as atomic*
{
  for $value in $values
  return
      switch ($type)
      case "integer" return $value cast as integer
      case "string" return $value cast as string
      default return error(xs:QName("local:unsupported-type"), $type || ": unsupported type")
};

declare function local:hypercube() as object
{
    let $hypercube-spec :=
    {|
        for $parameter in local:param-names()
        group by $dimension-name :=
            switch(true)
            case ends-with(lower-case($parameter), "::default")
                return substring-before($parameter, "::default")
            case ends-with(lower-case($parameter), ":default")
                return substring-before($parameter, ":default")
            case ends-with(lower-case($parameter), "::type")
                return substring-before($parameter, "::type")
            default
                return $parameter
        let $all as boolean :=
            (local:param-values($dimension-name) ! upper-case($$)) = "ALL"
        let $type as string? :=
            (local:param-values($dimension-name || "::type"), local:param-values($dimension-name || ":type"))[1]

        let $values := local:param-values($dimension-name)
        let $typed-values := if (exists($type)) then local:cast-sequence($values, $type) else $values

        let $has-default := ($parameter = $dimension-name || "::default") or ($parameter = $dimension-name || ":default")
        let $default-value := (local:param-values($dimension-name || "::default"), local:param-values($dimension-name || ":default"))[1]
        let $typed-default-value := if (exists($type)) then local:cast-sequence($default-value, $type) else $default-value

        return
        {
            $dimension-name : {|
                { "Type" : $type }[exists($type)],
                { "Domain" : [ $typed-values ] }[(exists($typed-values) and not($all))],
                { "Default" : $typed-default-value }[$has-default]
            |}
        }
    |}
    return hypercubes:user-defined-hypercube($hypercube-spec)
};

(: Query parameters :)
declare  %rest:case-insensitive                 variable $token             as string? external;
declare  %rest:case-insensitive                 variable $profile-name      as string  external := "sec";
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 $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 := "LATEST";
declare  %rest:case-insensitive %rest:distinct  variable $fiscalPeriod      as string* external := "FY";
declare  %rest:case-insensitive %rest:distinct  variable $aid               as string* external;
declare  %rest:case-insensitive                 variable $map               as string? external;
declare  %rest:case-insensitive                 variable $rule              as string? external;
declare  %rest:case-insensitive                 variable $report            as string? external;
declare  %rest:case-insensitive                 variable $validate          as boolean external := false;
declare  %rest:case-insensitive                 variable $labels            as boolean external := false;
declare  %rest:case-insensitive                 variable $additional-rules  as string? external;

session:audit-call($token);

(: Post-processing :)
let $format as string? := api:preprocess-format($format, $request-uri)
let $tag as string* := api:preprocess-tags($tag)

(: Object resolution :)
let $entities as object* :=
    companies:companies(
        $cik,
        $tag,
        $ticker,
        $sic)
let $report as object? := reports:reports($report)
let $map as item* :=
    if(exists($report))
    then reports:concept-map($report)
    else $map
let $rule as item* :=
    (
        if(exists($report))
        then reports:rules($report)
        else rules:rules($rule),
        if(exists($additional-rules))
        then rules:rules($additional-rules)
        else ()
    )

let $hypercube := local:hypercube()

let $facts :=
    let $options := {|
      {
        Hypercube : $hypercube,
        Validate: $validate
      },
      { "ConceptMaps" : $map }[exists($map)],
      { "Rules" : [ $rule ] }[exists($rule)]
    |}
    return if($profile-name eq "sec")
    then sec:facts-for($options)
    else facts:facts-for($options)

let $facts :=
  let $archives as string* := distinct-values($facts.Aspects."sec:Archive")
  let $concept-names as string* := distinct-values($facts.Aspects."xbrl:Concept")
  let $concepts as object* :=
      (
          concepts:concepts($concept-names, $archives, $concepts:ANY_COMPONENT_LINK_ROLE),
          (reports:concepts($report))[$$.Name = $concept-names]
      )
  let $language as string := ( $report.$components:DEFAULT-LANGUAGE , $concepts:AMERICAN_ENGLISH )[1]
  let $roles as string* := ( $report.Role, $concepts:ANY_COMPONENT_LINK_ROLE )
  for $fact as object in $facts
  let $entityName as string := $entities[$$._id eq $fact.Aspects."xbrl:Entity"].Profiles.SEC.CompanyName
  return
    if(not $labels)
    then $fact
    else {|
      $fact,
      let $concept-labels as object? := facts:labels($fact, $roles, $concepts:STANDARD_LABEL_ROLE, $language, $concepts, ())
      let $standard-labels as object := conversion:get-standard-labels($fact, $entityName)
      return { Labels : {| $concept-labels, $standard-labels |} }
    |}

let $facts := if($profile-name eq "sec") then api:normalize-facts($facts) else $facts

let $result := {
    NetworkIdentifier : "http://bizql.io/facts",
    TableName : "xbrl:Facts",
    FactTable : [ $facts ]
}
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()* {
           <FactTable NetworkIdentifier="http://bizql.io/facts"
            TableName="xbrl:Facts">{
                conversion:facts-to-xml($res.FactTable[], { Caller: "Report"})
        }</FactTable>
    },
    to-csv : function($res as object) as string {
        (conversion:facts-to-csv($res.FactTable[], { Caller: "Report" }), "")[1]
    }
}

let $results := api:serialize($result, $comment, $serializers, $format, "facts")
return api:check-and-return-results($token, $results, $format)

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

import module namespace conversion = "http://28.io/modules/xbrl/conversion";
import module namespace hypercubes = "http://28.io/modules/xbrl/hypercubes";
import module namespace entities = "http://28.io/modules/xbrl/entities";
import module namespace reports = "http://28.io/modules/xbrl/reports";
import module namespace concepts = "http://28.io/modules/xbrl/concepts";
import module namespace facts = "http://28.io/modules/xbrl/facts";
import module namespace rules = "http://28.io/modules/xbrl/rules";
import module namespace components = "http://28.io/modules/xbrl/components";

import module namespace sec = "http://28.io/modules/xbrl/profiles/sec/core";
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 request = "http://www.28msec.com/modules/http-request";

declare function local:param-values($name as string) as string*
{
    switch(true)
     case $name eq "xbrl:Concept"
        return (request:param-values("concept"), request:param-values("xbrl:Concept"))

     case $name eq "sec:FiscalYear" and $profile-name eq "sec"
        return ($fiscalYear, request:param-values("sec:FiscalYear"))[$$ ne "LATEST"]
     case $name eq "sec:FiscalYear::type" and $profile-name eq "sec"
        return "integer"
     case $name eq "sec:FiscalPeriod" and $profile-name eq "sec"
        return ($fiscalPeriod, request:param-values("sec:FiscalPeriod"))
     case $name eq "dei:LegalEntityAxis" and $profile-name eq "sec"
        return
         if(empty((request:param-values("sec:LegalEntityAxis"), request:param-values("sec:LegalEntityAxis::default"))))
         then "sec:DefaultLegalEntity"
         else request:param-values("sec:LegalEntityAxis")
     case $name eq "dei:LegalEntityAxis::default" return
         if(empty((request:param-values("sec:LegalEntityAxis"), request:param-values("sec:LegalEntityAxis::default"))))
         then "sec:DefaultLegalEntity"
         else request:param-values("sec:LegalEntityAxis::default")
     case $name eq "xbrl:Entity" and $profile-name eq "sec" return (
         let $companies := companies:companies(
            $cik,
            api:preprocess-tags($tag),
            $ticker,
            $sic)._id
        return if(empty(($cik,$tag,$ticker,$sic)) or exists($companies))
               then $companies
               else "dummy",
        request:param-values("xbrl:Entity"))
     case $name eq "sec:Archive" and $profile-name eq "sec" return (
            let $fiscalYears := ($fiscalYear, request:param-values("sec:FiscalYear"))
            let $fiscalPeriods := local:param-values("sec:FiscalPeriod")
            let $entities := entities:entities(local:param-values("xbrl:Entity"))
            return
                if($fiscalYears = "LATEST")
                then fiscal-core:latest-filings($entities, $fiscalPeriods)._id
                else (),
            $aid,
            request:param-values("sec:Archive")
        )
     default return request:param-values($name)
};

declare function local:param-names() as string*
{
    let $names := request:param-names()
    return distinct-values((
        $names[contains($$, ":")],
        "xbrl:Concept"[$names = "concept"],

        "sec:Accepted"[$profile-name eq "sec"],
        "sec:FiscalPeriod"[$profile-name eq "sec"],
        "sec:FiscalYear"[$profile-name eq "sec"],
        "xbrl:Entity"[$profile-name eq "sec" and $names = ("cik", "tag", "ticker", "sic")],
        "dei:LegalEntityAxis"[$profile-name eq "sec"],
        "dei:LegalEntityAxis::default"[$profile-name eq "sec"],
        "sec:Archive"[$profile-name eq "sec"]))
};

declare function local:cast-sequence($values as atomic*, $type as string) as atomic*
{
  for $value in $values
  return
      switch ($type)
      case "integer" return $value cast as integer
      case "string" return $value cast as string
      default return error(xs:QName("local:unsupported-type"), $type || ": unsupported type")
};

declare function local:hypercube() as object
{
    let $hypercube-spec :=
    {|
        for $parameter in local:param-names()
        group by $dimension-name :=
            switch(true)
            case ends-with(lower-case($parameter), "::default")
                return substring-before($parameter, "::default")
            case ends-with(lower-case($parameter), ":default")
                return substring-before($parameter, ":default")
            case ends-with(lower-case($parameter), "::type")
                return substring-before($parameter, "::type")
            default
                return $parameter
        let $all as boolean :=
            (local:param-values($dimension-name) ! upper-case($$)) = "ALL"
        let $type as string? :=
            (local:param-values($dimension-name || "::type"), local:param-values($dimension-name || ":type"))[1]

        let $values := local:param-values($dimension-name)
        let $typed-values := if (exists($type)) then local:cast-sequence($values, $type) else $values

        let $has-default := ($parameter = $dimension-name || "::default") or ($parameter = $dimension-name || ":default")
        let $default-value := (local:param-values($dimension-name || "::default"), local:param-values($dimension-name || ":default"))[1]
        let $typed-default-value := if (exists($type)) then local:cast-sequence($default-value, $type) else $default-value

        return
        {
            $dimension-name : {|
                { "Type" : $type }[exists($type)],
                { "Domain" : [ $typed-values ] }[(exists($typed-values) and not($all))],
                { "Default" : $typed-default-value }[$has-default]
            |}
        }
    |}
    return hypercubes:user-defined-hypercube($hypercube-spec)
};

(: Query parameters :)
declare  %rest:case-insensitive                 variable $token             as string? external;
declare  %rest:case-insensitive                 variable $profile-name      as string  external := "sec";
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 $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 := "LATEST";
declare  %rest:case-insensitive %rest:distinct  variable $fiscalPeriod      as string* external := "FY";
declare  %rest:case-insensitive %rest:distinct  variable $aid               as string* external;
declare  %rest:case-insensitive                 variable $map               as string? external;
declare  %rest:case-insensitive                 variable $rule              as string? external;
declare  %rest:case-insensitive                 variable $report            as string? external;
declare  %rest:case-insensitive                 variable $validate          as boolean external := false;
declare  %rest:case-insensitive                 variable $labels            as boolean external := false;
declare  %rest:case-insensitive                 variable $additional-rules  as string? external;

session:audit-call($token);

(: Post-processing :)
let $format as string? := api:preprocess-format($format, $request-uri)
let $tag as string* := api:preprocess-tags($tag)

(: Object resolution :)
let $entities as object* :=
    companies:companies(
        $cik,
        $tag,
        $ticker,
        $sic)
let $report as object? := reports:reports($report)
let $map as item* :=
    if(exists($report))
    then reports:concept-map($report)
    else $map
let $rule as item* :=
    (
        if(exists($report))
        then reports:rules($report)
        else rules:rules($rule),
        if(exists($additional-rules))
        then rules:rules($additional-rules)
        else ()
    )

let $hypercube := local:hypercube()

let $facts :=
    let $options := {|
      {
        Hypercube : $hypercube,
        Validate: $validate
      },
      { "ConceptMaps" : $map }[exists($map)],
      { "Rules" : [ $rule ] }[exists($rule)]
    |}
    return if($profile-name eq "sec")
    then sec:facts-for($options)
    else facts:facts-for($options)

let $facts :=
  let $archives as string* := distinct-values($facts.Aspects."sec:Archive")
  let $concept-names as string* := distinct-values($facts.Aspects."xbrl:Concept")
  let $concepts as object* :=
      (
          concepts:concepts($concept-names, $archives, $concepts:ANY_COMPONENT_LINK_ROLE),
          (reports:concepts($report))[$$.Name = $concept-names]
      )
  let $language as string := ( $report.$components:DEFAULT-LANGUAGE , $concepts:AMERICAN_ENGLISH )[1]
  let $roles as string* := ( $report.Role, $concepts:ANY_COMPONENT_LINK_ROLE )
  for $fact as object in $facts
  let $entityName as string := $entities[$$._id eq $fact.Aspects."xbrl:Entity"].Profiles.SEC.CompanyName
  return
    if(not $labels)
    then $fact
    else {|
      $fact,
      let $concept-labels as object? := facts:labels($fact, $roles, $concepts:STANDARD_LABEL_ROLE, $language, $concepts, ())
      let $standard-labels as object := conversion:get-standard-labels($fact, $entityName)
      return { Labels : {| $concept-labels, $standard-labels |} }
    |}

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

let $result := {
    NetworkIdentifier : "http://bizql.io/facts",
    TableName : "xbrl:Facts",
    FactTable : [ $facts ]
}
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()* {
           <FactTable NetworkIdentifier="http://bizql.io/facts"
            TableName="xbrl:Facts">{
                conversion:facts-to-xml($res.FactTable[], { Caller: "Report"})
        }</FactTable>
    },
    to-csv : function($res as object) as string {
        (conversion:facts-to-csv($res.FactTable[], { Caller: "Report" }), "")[1]
    }
}

let $results := api:serialize($result, $comment, $serializers, $format, "facts")
return api:check-and-return-results($token, $results, $format)