Simplify `all_children`.

Simplify `all_children`.

#!/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:
    (. == {} or . == [] or . == false or . == null or . == 0) | not
;

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

# Format a date into its wordy equivalent.
def format_date:
    try
    (
        (   try (. | strptime("%Y-%m-%d"))
            catch (
                try (. | strptime("%Y/%m/%d"))
                catch (
                    try (. | strptime("%Y-%m"))
                )
            )
        ) | mktime | strftime("%B %-d, %Y")
    )
;

# Group an array of objects into an object of arrays, such that the key-value
# pairs of the new object are an accumulation of those of the old object. For
# example, `[{"a":1, "b":2}, {"a":3}]` turns into `{"a":[1, 3], "b":[3]}`. This
# can be used to partition an array.
# Try: `[1,"2",3] | map ({(.|type):.}) | group`
def group:
    reduce ([.[] | to_entries[]] | group_by(.key))[] as $group
    ( {}
    ; .[ $group[0].key ] |= (. // []) + [ $group[].value ]
    )
;

# 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):
    def tree_aux($future):
        { "name": $future[0] } +
        if ($future | length) > 1 then
            { "contents": [ tree_aux($future[1:]) ] }
        else
            .
        end
    ;
    tree_aux($path)
;

# Merge two values. If the values are both objects, the values at equal indices
# will be recursively merged; if the values are both arrays, the arrays are
# concatenated; if the values are some other type, we only check if the values
# do not conflict.
def merge(other):
    if (. | type) == "object" and (other | type) == "object" then
        (. | to_entries) + (other | to_entries)
        | group_by(.key)
        | map({ "key": (.[0].key), "value" : (reduce .[1:][].value as $x (.[0].value; merge($x)))})
        | from_entries
    elif (. | type) == "array" and (other | type) == "array" then
        . + other
    elif . == other then
        .
    else
        error("trying to merge incompatible objects " + (.|type) + " and " + (other|type))
    end
;

# Merge an array of content by grouping it by name and then merging all the
# pages with the same name. This is necessary because simply merging content
# will only concatenate pages, even if they are the same page.
def merge_content:
    reduce .[] as $x ({}; merge($x)) |
    if has("contents") then
        .contents |= ( group_by(.name) | map(merge_content) )
    else
        .
    end
;

# 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:
    .marked_for_removal = true
;

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

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

# Any page gets a link to its target.
def link(target_extension):
    (all_children | select(has("directory") and (.name | endswith(".md")))) |= (
        .link = ((.directory + [.name]) | join("/") | rtrimstr(".md") | . + "." + target_extension)
    )
;

# 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(.meta.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
# "publish" in the metadata.
def explicit_publish:
    def publish: has("contents") or .external or .meta.publish | bool;
    (( all_children | select(publish | not)) |= mark) | remove_marked
;

# Sort content first according to sort order in metadata.
def sort_content:
    (all_children | select(has("contents")) | .contents) |= (
        sort_by(.meta.sort // .meta.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(target_extension):
    merge_content
    | directory
    | link(target_extension)
    | explicit_publish
    | frontmatter
    | sort_content
    | annotate_leaves
;

# Get publishable targets from an index
def targets:
    recurse(.contents[]?)
    | ., (.frontmatter // empty)
    | select(has("link") and (has("external") | not))
    | .directory as $dir
    | [.link] + [.targets?[] | $dir + [.] | 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:
    (. == {} or . == [] or . == false or . == null or . == 0) | not
;

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

# Format a date into its wordy equivalent.
def format_date:
    try
    (
        (   try (. | strptime("%Y-%m-%d"))
            catch (
                try (. | strptime("%Y/%m/%d"))
                catch (
                    try (. | strptime("%Y-%m"))
                )
            )
        ) | mktime | strftime("%B %-d, %Y")
    )
;

# Group an array of objects into an object of arrays, such that the key-value
# pairs of the new object are an accumulation of those of the old object. For
# example, `[{"a":1, "b":2}, {"a":3}]` turns into `{"a":[1, 3], "b":[3]}`. This
# can be used to partition an array.
# Try: `[1,"2",3] | map ({(.|type):.}) | group`
def group:
    reduce ([.[] | to_entries[]] | group_by(.key))[] as $group
    ( {}
    ; .[ $group[0].key ] |= (. // []) + [ $group[].value ]
    )
;

# 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):
    def tree_aux($future):
        { "name": $future[0] } +
        if ($future | length) > 1 then
            { "contents": [ tree_aux($future[1:]) ] }
        else
            .
        end
    ;
    tree_aux($path)
;

# Merge two values. If the values are both objects, the values at equal indices
# will be recursively merged; if the values are both arrays, the arrays are
# concatenated; if the values are some other type, we only check if the values
# do not conflict.
def merge(other):
    if (. | type) == "object" and (other | type) == "object" then
        (. | to_entries) + (other | to_entries)
        | group_by(.key)
        | map({ "key": (.[0].key), "value" : (reduce .[1:][].value as $x (.[0].value; merge($x)))})
        | from_entries
    elif (. | type) == "array" and (other | type) == "array" then
        . + other
    elif . == other then
        .
    else
        error("trying to merge incompatible objects " + (.|type) + " and " + (other|type))
    end
;

# Merge an array of content by grouping it by name and then merging all the
# pages with the same name. This is necessary because simply merging content
# will only concatenate pages, even if they are the same page.
def merge_content:
    reduce .[] as $x ({}; merge($x)) |
    if has("contents") then
        .contents |= ( group_by(.name) | map(merge_content) )
    else
        .
    end
;

# 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:
    .marked_for_removal = true
;

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

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

# Any page gets a link to its target.
def link(target_extension):
    (all_children | select(has("directory") and (.name | endswith(".md")))) |= (
        .link = ((.directory + [.name]) | join("/") | rtrimstr(".md") | . + "." + target_extension)
    )
;

# 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(.meta.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
# "publish" in the metadata.
def explicit_publish:
    def publish: has("contents") or .external or .meta.publish | bool;
    (( all_children | select(publish | not)) |= mark) | remove_marked
;

# Sort content first according to sort order in metadata.
def sort_content:
    (all_children | select(has("contents")) | .contents) |= (
        sort_by(.meta.sort // .meta.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(target_extension):
    merge_content
    | directory
    | link(target_extension)
    | explicit_publish
    | frontmatter
    | sort_content
    | annotate_leaves
;

# Get publishable targets from an index
def targets:
    recurse(.contents[]?)
    | ., (.frontmatter // empty)
    | select(has("link") and (has("external") | not))
    | .directory as $dir
    | [.link] + [.targets?[] | $dir + [.] | join("/") ]
    | .[]
;


fix filings order again

fix filings order again

import module namespace companies = "http://xbrl.io/modules/bizql/profiles/sec/companies";
import module namespace filings = "http://xbrl.io/modules/bizql/profiles/sec/filings";
import module namespace archives = "http://xbrl.io/modules/bizql/archives";
import module namespace fiscal = "http://xbrl.io/modules/bizql/profiles/sec/fiscal/core";

import module namespace request = "http://www.28msec.com/modules/http-request";
import module namespace response = "http://www.28msec.com/modules/http-response";
import module namespace session = "http://apps.28.io/session";

declare function local:filings(
    $ciks as string*,
    $tags as string*,
    $tickers as string*,
    $sics as string*,
    $fp as string*,
    $fy as string*) as object*
{
    let $entities := if ($tags = "ALL") then companies:companies()
                                        else (
                                            companies:companies($ciks),
                                            companies:companies-for-tags($tags),
                                            companies:companies-for-tickers($tickers),
                                            companies:companies-for-SIC($sics)
                                        )
    for $entity in $entities
    for $fy in distinct-values(
                for $fy in $fy
                return
                    switch ($fy)
                    case "LATEST" return
                        for $p in $fp
                        return
                            if ($p eq "FY")
                            then fiscal:latest-reported-fiscal-period($entity, "10-K").year
                            else fiscal:latest-reported-fiscal-period($entity, "10-Q").year
                        case "ALL" return  $fiscal:ALL_FISCAL_YEARS
                    default return $fy cast as integer
                )
    for $fp in $fp
    return fiscal:filings-for-entities-and-fiscal-periods-and-years($entity, $fp, $fy)
};

session:audit-call();

let $format      := lower-case((request:param-values("format"), substring-after(request:path(), ".jq."))[1])
let $ciks        := distinct-values(companies:eid(request:param-values("cik")))
let $tags        := distinct-values(request:param-values("tag") ! upper-case($$))
let $tickers     := distinct-values(request:param-values("ticker"))
let $sics        := distinct-values(request:param-values("sic"))
let $fiscalYears := distinct-values(
                        for $y in request:param-values("fiscalYear", "LATEST")
                        return
                            if ($y eq "LATEST" or $y eq "ALL")
                            then $y
                            else if ($y castable as integer)
                            then $y
                            else  ()
                    )
let $fiscalPeriods := distinct-values(let $fp := request:param-values("fiscalPeriod", "FY")
                      return
                        if (($fp ! lower-case($$)) = "all")
                        then $fiscal:ALL_FISCAL_PERIODS
                        else $fp)
let $aids        := archives:aid(request:param-values("aid"))
let $archives    := (
                        local:filings($ciks, $tags, $tickers, $sics, $fiscalPeriods, $fiscalYears),
                        archives:archives($aids)
                    )
let $companies   := companies:companies($archives.Entity)
return
    if (session:only-dow30($companies) or session:valid())
    then {
        let $summaries := for $f in filings:summaries($archives)
                          order by $f.Accepted descending
                          return $f
        return
            switch ($format)
            case "xml"  return {
                response:serialization-parameters({"omit-xml-declaration" : false, indent : true });
                ( session:comment("xml", {
                    NumArchives: count($summaries),
                    TotalNumArchives: session:num-archives(),
                    TotalNumEntities: session:num-entities()
                }),
                <Filings>{
                    filings:summaries-to-xml($summaries)
                }</Filings>)
            }
            case "csv" case "text" return {
                response:content-type("text/csv");
                response:header("Content-Disposition", "attachment; filename=filings.csv");
                string-join(filings:summaries-to-csv($summaries), "")
            }
            case "excel" return {
                response:content-type("application/vnd.ms-excel");
                response:header("Content-Disposition", "attachment; filename=filings.csv");
                string-join(filings:summaries-to-csv($summaries))
            }
            default return {
                response:content-type("application/json");
                response:serialization-parameters({"indent" : true});
                {|
                    { "Archives" : [ $summaries ] },
                    session:comment("json", {
                        NumArchives: count($summaries),
                        TotalNumArchives: session:num-archives(),
                        TotalNumEntities: session:num-entities()
                    })
                |}
            }
    } else {
        response:status-code(401);
        session:error("accessing filings of an entity that is not in the DOW30", $format)
    }

import module namespace companies = "http://xbrl.io/modules/bizql/profiles/sec/companies";
import module namespace filings = "http://xbrl.io/modules/bizql/profiles/sec/filings";
import module namespace archives = "http://xbrl.io/modules/bizql/archives";
import module namespace fiscal = "http://xbrl.io/modules/bizql/profiles/sec/fiscal/core";

import module namespace request = "http://www.28msec.com/modules/http-request";
import module namespace response = "http://www.28msec.com/modules/http-response";
import module namespace session = "http://apps.28.io/session";

declare function local:filings(
    $ciks as string*,
    $tags as string*,
    $tickers as string*,
    $sics as string*,
    $fp as string*,
    $fy as string*) as object*
{
    let $entities := if ($tags = "ALL") then companies:companies()
                                        else (
                                            companies:companies($ciks),
                                            companies:companies-for-tags($tags),
                                            companies:companies-for-tickers($tickers),
                                            companies:companies-for-SIC($sics)
                                        )
    for $entity in $entities
    for $fy in distinct-values(
                for $fy in $fy
                return
                    switch ($fy)
                    case "LATEST" return
                        for $p in $fp
                        return
                            if ($p eq "FY")
                            then fiscal:latest-reported-fiscal-period($entity, "10-K").year
                            else fiscal:latest-reported-fiscal-period($entity, "10-Q").year
                        case "ALL" return  $fiscal:ALL_FISCAL_YEARS
                    default return $fy cast as integer
                )
    for $fp in $fp
    return fiscal:filings-for-entities-and-fiscal-periods-and-years($entity, $fp, $fy)
};

session:audit-call();

let $format      := lower-case((request:param-values("format"), substring-after(request:path(), ".jq."))[1])
let $ciks        := distinct-values(companies:eid(request:param-values("cik")))
let $tags        := distinct-values(request:param-values("tag") ! upper-case($$))
let $tickers     := distinct-values(request:param-values("ticker"))
let $sics        := distinct-values(request:param-values("sic"))
let $fiscalYears := distinct-values(
                        for $y in request:param-values("fiscalYear", "LATEST")
                        return
                            if ($y eq "LATEST" or $y eq "ALL")
                            then $y
                            else if ($y castable as integer)
                            then $y
                            else  ()
                    )
let $fiscalPeriods := distinct-values(let $fp := request:param-values("fiscalPeriod", "FY")
                      return
                        if (($fp ! lower-case($$)) = "all")
                        then $fiscal:ALL_FISCAL_PERIODS
                        else $fp)
let $aids        := archives:aid(request:param-values("aid"))
let $archives    := (
                        local:filings($ciks, $tags, $tickers, $sics, $fiscalPeriods, $fiscalYears),
                        archives:archives($aids)
                    )
let $companies   := companies:companies($archives.Entity)
return
    if (session:only-dow30($companies) or session:valid())
    then {
        let $summaries := filings:summaries($archives)
        return
            switch ($format)
            case "xml"  return {
                response:serialization-parameters({"omit-xml-declaration" : false, indent : true });
                ( session:comment("xml", {
                    NumArchives: count($summaries),
                    TotalNumArchives: session:num-archives(),
                    TotalNumEntities: session:num-entities()
                }),
                <Filings>{
                    filings:summaries-to-xml($summaries)
                }</Filings>)
            }
            case "csv" case "text" return {
                response:content-type("text/csv");
                response:header("Content-Disposition", "attachment; filename=filings.csv");
                string-join(filings:summaries-to-csv($summaries), "")
            }
            case "excel" return {
                response:content-type("application/vnd.ms-excel");
                response:header("Content-Disposition", "attachment; filename=filings.csv");
                string-join(filings:summaries-to-csv($summaries))
            }
            default return {
                response:content-type("application/json");
                response:serialization-parameters({"indent" : true});
                {|
                    { "Archives" : [ $summaries ] },
                    session:comment("json", {
                        NumArchives: count($summaries),
                        TotalNumArchives: session:num-archives(),
                        TotalNumEntities: session:num-entities()
                    })
                |}
            }
    } else {
        response:status-code(401);
        session:error("accessing filings of an entity that is not in the DOW30", $format)
    }


fixed modify-hypercube removes domainrestrictions

jsoniq version "1.0";
(:
 : Copyright 2012-2013 28msec Inc.
 :)

(:~
 :
This module provides functionality for manipulating hypercubes.

 :
 :
A hypercube provides a multi-dimensional structure to organize facts.

 :
 :
The dimensions of a hypercube correspond to XBRL aspects (concepts, entities, periods,
 : units, further XBRL dimensions). An XBRL hypercube only is made of XBRL dimensions, however
 : including the other aspects as well is very useful in the context of NOLAP, so that 28.io
 : does so.

 :
 :
28.io introduces the notion of default hypercube. A default hypercube is implicitly added
 : to each component, and only contains the concept, entity, period and unit aspects.
 : In other words, the default hypercube
 : of a component contains all these facts that do not have any XBRL dimensions.

 :
 :
With this module, you can retrieve all hypercubes contained in a component. You can
 : retrieve all facts contained in a hypercube (default dimension values are processed automatically),
 : either in raw form, or organized as a (2D) fact table. You can also populate a network (for example,
 : a presentation network) with the facts contained in a hypercube.

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

import module namespace archives = "http://28.io/modules/xbrl/archives";
import module namespace entities = "http://28.io/modules/xbrl/entities";
import module namespace facts = "http://28.io/modules/xbrl/facts";
import module namespace schema = "http://zorba.io/modules/schema";

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

(:~
 : Joker for all archives.
 :)
declare variable $hypercubes:ALL_ARCHIVES as boolean := true;

(:~
 :
Retrieves all hypercubes in the supplied components.

 :
 : @param $components a sequence of components.
 :
 : @return all hypercubes.
 :)
declare function hypercubes:hypercubes-for-components($components as object*) as object*
{
  $components.Hypercubes ! values($$)
};

(:~
 :
Retrieves all hypercubes in the supplied components and
 : with the given names.

 :
 : @param $components a sequence of components.
 : @param $names a sequence of names.
 :
 : @return all hypercubes.
 :)
declare function hypercubes:hypercubes-for-components(
    $components as object*,
    $names as string*
) as object*
{
  $names ! $components.Hypercubes.($$)
};

(:~
 :
 :
Retrieves all facts in the supplied hypercube, across all archives.

 :
Default values will be populated automatically.

 :
 :
 : @param $hypercube a hypercube.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:facts(
    $hypercube as object
)
as object*
{
  facts:facts-for({Hypercube: $hypercube})
};

(:~
 :
 :
Retrieves all facts in the supplied hypercube, across all archives.

 :
Default values will be populated automatically.

 :
 :
 : @param $hypercube a hypercube.
 : @param $options standard fact retrieving options.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:facts(
    $hypercube as object,
    $options as object?
)
as object*
{
  let $options as object := facts:merge-objects(
    { Hypercube: $hypercube },
    $options,
    true (: parameter has higher priority than option :)
  )
  return facts:facts-for($options)
};

(:~
 :
 :
Retrieves all facts from the supplied archives, that are relevant to the
 : supplied hypercube, and populates them with the default dimension values
 : when missing.

 :
 :
 : @param $hypercube a hypercube.
 : @param $archives a sequence of archives or their AIDs or $hypercubes:ALL_ARCHIVES for no filtering.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:facts-for-hypercube(
    $hypercube as object,
    $archives as item*
)
as object*
{
  hypercubes:facts-for-hypercube($hypercube, $archives, {})
};

(:~
 :
 :
Retrieves all facts from the supplied archives, that are relevant to the
 : supplied hypercube, and populates them with the default dimension values
 : when missing.

 :
 :
 : @param $hypercube a hypercube.
 : @param $archives a sequence of archives or their AIDs or $hypercubes:ALL_ARCHIVES for no filtering.
 : @param $options standard fact retrieving options.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:facts-for-hypercube(
    $hypercube as object,
    $archives as item*,
    $options as object?
)
as item* (: object* :)
{
    if (exists($options.facts-for-archives-and-concepts))
    then
      let $options as object :=
        facts:merge-objects(
          { Hypercube: $hypercube },
          $options,
          true (: parameter has higher priority than option :)
        )
      let $facts-for-archives-and-concepts := $options.facts-for-archives-and-concepts
      let $concepts :=
          keys($hypercube.Aspects.$facts:CONCEPT.Domains."xbrl:ConceptDomain".Members)
      let $concepts := if (exists($concepts))
                   then $concepts
                   else $facts:ALL_OF_THEM
      return
        $facts-for-archives-and-concepts(
        $archives, $concepts, $options)
    else
      let $options as object :=
        facts:merge-objects(
          {
            Hypercube: $hypercube,
            Filter: { Archive: [ archives:aid($archives) ] }
          },
          $options,
          true (: parameter has higher priority than option :)
        )
      return facts:facts-for($options)
};

(:~
 :
 :
Retrieves all facts from the supplied archives, that are relevant to the
 : supplied hypercube, and populates them with the default dimension values
 : when missing.

 :
 :
 : @param $hypercube a hypercube.
 : @param $archives a sequence of archives or their AIDs or $hypercubes:ALL_ARCHIVES for no filtering.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:fact-table-for-hypercube(
  $hypercube as object,
  $archives as item*) as array
{
  hypercubes:fact-table-for-hypercube($hypercube, $archives, {})
};

(:~
 :
 :
Retrieves all facts from the supplied archives, that are relevant to the
 : supplied hypercube, and populates them with the default dimension values
 : when missing.

 :
 :
 : @param $hypercube a hypercube.
 : @param $archives a sequence of archives or their AIDs
 : @param $options standard fact retrieving options.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:fact-table-for-hypercube(
  $hypercube as object,
  $archives as item*,
  $options as object?) as array
{
  hypercubes:fact-table-for-hypercube-and-facts(
    $hypercube,
    hypercubes:facts-for-hypercube($hypercube, $archives, $options)
  )
};

(:~
 :
 :
Utility function to convert a sequence of facts to a fact table
 : given a hypercube. It is not recommended to use this function directly.
 : Use hypercubes:fact-table-for-hypercube instead.

 :
 :
 : @param $hypercube a hypercube.
 : @param $facts a sequence of facts that were retrieved with the hypercube.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:fact-table-for-hypercube-and-facts(
  $hypercube as object,
  $facts as object*) as array
{
  let $dimensions := distinct-values((
      $facts:CONCEPT,
      $facts:ENTITY,
      $facts:PERIOD,
      $facts:UNIT,
      keys($hypercube.Aspects)))
  return [
    [ $dimensions, "Value" ],
    for $fact in $facts
    return [
      $dimensions ! (let $member := $fact.Aspects.($$)
                     return if(exists($member))
                            then $member
                            else null),
      $fact.Value
    ]
  ]
};

(:~
 :
 :
Populates a concept-tree network with all facts from the supplied archives,
 : that are relevant to the
 : supplied hypercube. Default dimension values are added to the facts
 : when missing.

 :
 : @param $networks networks.
 : @param $hypercube a hypercube.
 : @param $archives a sequence of archives or their AIDs or $hypercubes:ALL_ARCHIVES for no filtering.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:populate-networks-with-facts(
  $networks as object*,
  $hypercube as object,
  $archives as item*) as object*
{
  hypercubes:populate-networks-with-facts($networks, $hypercube, $archives, {})
};

(:~
 :
 :
Populates a concept-tree network with all facts from the supplied archives,
 : that are relevant to the
 : supplied hypercube. Default dimension values are added to the facts
 : when missing.

 :
 : @param $networks networks.
 : @param $hypercube a hypercube.
 : @param $archives a sequence of archives or their AIDs.
 : @param $options standard fact retrieving options.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:populate-networks-with-facts(
  $networks as object*,
  $hypercube as object,
  $archives as item*,
  $options as object?) as object*
{
  hypercubes:populate-networks-with-retrieved-facts(
    $networks,
    hypercubes:facts-for-hypercube($hypercube, $archives, $options)
  )
};

(:~
 :
 :
A utility function that populates networks with facts against concepts that belong
 : to them. It is not recommended to use this function directly.
 : Use hypercubes:populate-networks-with-retrieved-facts.

 :
 : @param $networks networks.
 : @param $facts a sequence of facts.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:populate-networks-with-retrieved-facts(
  $networks as object*,
  $facts as object*) as object*
{
  let $fact-table-by-concepts := {|
    for $f in $facts
    group by $concept := $f.Aspects."xbrl:Concept"
    return { $concept : [ $f ] }
  |}
  return
    copy $result := $networks
    modify
      for $concept in descendant-objects(values($result.Trees))[exists($$.Name)]
      let $name := $concept.Name
      return insert json { "Facts" : $fact-table-by-concepts.$name } into $concept
    return $result
};

(:~
 :
Returns an instantiation of a dimensionless Hypercube containing only the basic
 :    characteristics (xbrl:Concept, xbrl:Period, xbrl:Entity, and xbrl:Unit).
 :    For each of those included aspects the value space is not limited.

 :
 : @return dimensionless hypercube instantiation.
 :)
declare function hypercubes:dimensionless-hypercube() as object
{
  hypercubes:dimensionless-hypercube(())
};

(:~
 :
Returns an instantiation of a dimensionless Hypercube containing only the basic
 :    characteristics (xbrl:Concept, xbrl:Period, xbrl:Entity, and xbrl:Unit).
 :    For each of those included aspects the value space is not limited, however
 :    you can restrict it using the options. If you need more
 :    XBRL dimensions, use the user-defined-hypercube function instead.

 :
 : @return dimensionless hypercube instantiation.
 : @param $options: additional options among which:
 : - Concepts: an array of concept names to include in the hypercube.
 : - Periods: an array of periods to include in the hypercube.
 : - Entities: an array of EIDs to include in the hypercube.
 : - Units: an array of units to include in the hypercube.
 :)
declare function hypercubes:dimensionless-hypercube($options as object?) as object
{
  hypercubes:user-defined-hypercube(
    {|
      if(exists($options.Concepts))
      then { "xbrl:Concept" : { Domain: $options.Concepts } }
      else (),
      if(exists($options.Entities))
      then { "xbrl:Entity" : { Domain: [ entities:eid($options.Entities[]) ] } }
      else (),
      if(exists($options.Periods))
      then { "xbrl:Period" : { Domain: $options.Periods } }
      else (),
      if(exists($options.Units))
      then { "xbrl:Unit" : { Domain: $options.Units } }
      else ()
    |}
  )
};

(:~
 :
Returns an instantiation of a Hypercube containing the specified dimensions.
 :    The four basic characteristics (xbrl:Concept, xbrl:Period, xbrl:Entity, and xbrl:Unit)
 :    are always added, but can be overriden. For each of those included aspects the value space is,
 :    by default, not limited, and only xbrl:Unit has a default value.

 :
 : @return the hypercube instantiation.
 : @param $dimensions: an object with pairs of (dimension name, object with dimension information). These
 : dimension information subobjects
 : may have two (optional) fields: Default (a string) and Domain (an array of strings).
 :
 : @error hypercubes:invalid-type if a member of a typed dimension does not match the expected type
 : @error hypercubes:invalid-type if a member of an explicit dimension is not of type string
 :)
declare function hypercubes:user-defined-hypercube($dimensions as object?) as object
{
  {
    "Name" : "xbrl:UserDefinedHypercube",
    "Label" : "User-defined Hypercube",
    "Aspects" : {|
      for $dimension-name as string in distinct-values((
          "xbrl:Concept",
          "xbrl:Entity",
          "xbrl:Period",
          "xbrl:Unit",
          keys($dimensions)
      ))
      let $dimension-spec as object? := $dimensions.$dimension-name
      let $dimension-type as string? := $dimension-spec.Type
      let $dimension-domain as array? := $dimension-spec.Domain
      let $dimension-default as atomic? := $dimension-spec.Default
      let $domain-name := $dimension-name || "Domain"
      return {
        $dimension-name : {|
          (: Name :)
          {
            "Name" : $dimension-name
          },

          (: Label :)
          switch($dimension-name)
          case "xbrl:Concept" return {
            "Label" : "XBRL Concept Dimension"
          }
          case "xbrl:Entity" return {
            "Label" : "XBRL Entity Dimension"
          }
          case "xbrl:Period" return {
            "Label" : "XBRL Period Dimension"
          }
          case "xbrl:Unit" return {
            "Label" : "XBRL Unit Dimension"
          }
          default return (),

          (: Default value :)
          switch(true)
          case exists($dimension-default) return {
            "Default" :
              hypercubes:check-type-and-return($dimension-default,
                                               ($dimension-type, "string")[1])
          }
          case $dimension-name eq "xbrl:Unit" return {
            "Default" : "xbrl:NonNumeric"
          }
          default return (),

          (: Explicit vs. Typed :)
          {
            "Kind" : if(exists($dimension-type))
                     then "TypedDimension"
                     else "ExplicitDimension"
          },

          (: Type :)
          { "Type": $dimension-type }[exists($dimension-type)],

          (: Domain :)
          if (empty($dimension-type))
          then
          {
            "Domains" : {
              $domain-name : {
                "Name" : $domain-name,
                "Label" :
                  switch($dimension-name)
                  case "xbrl:Concept" return "XBRL Concept Domain"
                  case "xbrl:Entity" return "XBRL Entity Domain"
                  case "xbrl:Period" return "XBRL Period Domain"
                  case "xbrl:Unit" return "XBRL Unit Domain"
                  default return $dimension-name || " Domain",
                "Members" : {|
                  for $dimension-member in $dimension-domain[]
                  return {
                    $dimension-member: {
                      Name: hypercubes:check-type-and-return($dimension-member, "string")
                    }
                  }
                |}
              }
            }
          }[exists($dimension-domain)]
          else {
            "DomainRestriction" : {
              "Name" : $domain-name,
              "Label" : $dimension-name || " Domain",
              "Enumeration" : [ hypercubes:check-type-and-return($dimension-domain[], $dimension-type) ]
            }
          }[exists($dimension-domain)]

        |}
      }
    |}
  }
};

declare %private function hypercubes:check-type-and-return(
    $dimension-members as atomic*,
    $dimension-type as string) as atomic*
{
  for $dimension-member as atomic in $dimension-members
  return
  if (not $dimension-member instance of null
      and
      local-name-from-QName(schema:schema-type($dimension-member)) ne
      local-name-from-QName(xs:QName($dimension-type)))
  then error(
        xs:QName("hypercubes:invalid-type"),
        schema:schema-type($dimension-member) ||
          ": type doesn't match expected type (" || $dimension-type || ")")
  else $dimension-member
};

declare function hypercubes:modify-hypercube(
    $hypercube as object,
    $options as object?) as object
{
    let $options :=
    {|
        for $dimension in keys(($hypercube.Aspects, $options))
        let $hypercube-metadata := $hypercube.Aspects.$dimension
        let $new-metadata := $options.$dimension
        return {
            $dimension : if(exists($new-metadata))
            then
                $new-metadata
            else {|
                { Type : $hypercube.Aspects.$dimension.Type }[$hypercube-metadata.Kind eq "TypedDimension"],
                let $hypercube-domain :=
                  (
                     descendant-objects(values($hypercube-metadata.Domains)).Name,
                     jn:flatten($hypercube-metadata.DomainRestriction.Enumeration)
                  )
                where exists($hypercube-domain)
                return { Domain : [ $hypercube-domain ] },
                let $hypercube-default := $hypercube-metadata.Default
                where exists($hypercube-default)
                return { Default : $hypercube-default }
            |}
        }
    |}
    return hypercubes:user-defined-hypercube($options)
};

declare function hypercubes:merge($hypercubes as object*) as object
{
    hypercubes:user-defined-hypercube({|
        for $aspect in keys($hypercubes.Aspects)
        let $default := distinct-values($hypercubes.Aspects.$aspect.Default)
        let $domain := distinct-values((descendant-objects($hypercubes.Aspects.$aspect.Domains).Name))
        return {
            $aspect : {|
                { Default: $default}[exists($default)],
                { Domain: [ distinct-values(($domain, $default)) ]}[exists($domain)]
            |}
        }
    |})
};

jsoniq version "1.0";
(:
 : Copyright 2012-2013 28msec Inc.
 :)

(:~
 :
This module provides functionality for manipulating hypercubes.

 :
 :
A hypercube provides a multi-dimensional structure to organize facts.

 :
 :
The dimensions of a hypercube correspond to XBRL aspects (concepts, entities, periods,
 : units, further XBRL dimensions). An XBRL hypercube only is made of XBRL dimensions, however
 : including the other aspects as well is very useful in the context of NOLAP, so that 28.io
 : does so.

 :
 :
28.io introduces the notion of default hypercube. A default hypercube is implicitly added
 : to each component, and only contains the concept, entity, period and unit aspects.
 : In other words, the default hypercube
 : of a component contains all these facts that do not have any XBRL dimensions.

 :
 :
With this module, you can retrieve all hypercubes contained in a component. You can
 : retrieve all facts contained in a hypercube (default dimension values are processed automatically),
 : either in raw form, or organized as a (2D) fact table. You can also populate a network (for example,
 : a presentation network) with the facts contained in a hypercube.

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

import module namespace archives = "http://28.io/modules/xbrl/archives";
import module namespace entities = "http://28.io/modules/xbrl/entities";
import module namespace facts = "http://28.io/modules/xbrl/facts";
import module namespace schema = "http://zorba.io/modules/schema";

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

(:~
 : Joker for all archives.
 :)
declare variable $hypercubes:ALL_ARCHIVES as boolean := true;

(:~
 :
Retrieves all hypercubes in the supplied components.

 :
 : @param $components a sequence of components.
 :
 : @return all hypercubes.
 :)
declare function hypercubes:hypercubes-for-components($components as object*) as object*
{
  $components.Hypercubes ! values($$)
};

(:~
 :
Retrieves all hypercubes in the supplied components and
 : with the given names.

 :
 : @param $components a sequence of components.
 : @param $names a sequence of names.
 :
 : @return all hypercubes.
 :)
declare function hypercubes:hypercubes-for-components(
    $components as object*,
    $names as string*
) as object*
{
  $names ! $components.Hypercubes.($$)
};

(:~
 :
 :
Retrieves all facts in the supplied hypercube, across all archives.

 :
Default values will be populated automatically.

 :
 :
 : @param $hypercube a hypercube.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:facts(
    $hypercube as object
)
as object*
{
  facts:facts-for({Hypercube: $hypercube})
};

(:~
 :
 :
Retrieves all facts in the supplied hypercube, across all archives.

 :
Default values will be populated automatically.

 :
 :
 : @param $hypercube a hypercube.
 : @param $options standard fact retrieving options.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:facts(
    $hypercube as object,
    $options as object?
)
as object*
{
  let $options as object := facts:merge-objects(
    { Hypercube: $hypercube },
    $options,
    true (: parameter has higher priority than option :)
  )
  return facts:facts-for($options)
};

(:~
 :
 :
Retrieves all facts from the supplied archives, that are relevant to the
 : supplied hypercube, and populates them with the default dimension values
 : when missing.

 :
 :
 : @param $hypercube a hypercube.
 : @param $archives a sequence of archives or their AIDs or $hypercubes:ALL_ARCHIVES for no filtering.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:facts-for-hypercube(
    $hypercube as object,
    $archives as item*
)
as object*
{
  hypercubes:facts-for-hypercube($hypercube, $archives, {})
};

(:~
 :
 :
Retrieves all facts from the supplied archives, that are relevant to the
 : supplied hypercube, and populates them with the default dimension values
 : when missing.

 :
 :
 : @param $hypercube a hypercube.
 : @param $archives a sequence of archives or their AIDs or $hypercubes:ALL_ARCHIVES for no filtering.
 : @param $options standard fact retrieving options.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:facts-for-hypercube(
    $hypercube as object,
    $archives as item*,
    $options as object?
)
as item* (: object* :)
{
    if (exists($options.facts-for-archives-and-concepts))
    then
      let $options as object :=
        facts:merge-objects(
          { Hypercube: $hypercube },
          $options,
          true (: parameter has higher priority than option :)
        )
      let $facts-for-archives-and-concepts := $options.facts-for-archives-and-concepts
      let $concepts :=
          keys($hypercube.Aspects.$facts:CONCEPT.Domains."xbrl:ConceptDomain".Members)
      let $concepts := if (exists($concepts))
                   then $concepts
                   else $facts:ALL_OF_THEM
      return
        $facts-for-archives-and-concepts(
        $archives, $concepts, $options)
    else
      let $options as object :=
        facts:merge-objects(
          {
            Hypercube: $hypercube,
            Filter: { Archive: [ archives:aid($archives) ] }
          },
          $options,
          true (: parameter has higher priority than option :)
        )
      return facts:facts-for($options)
};

(:~
 :
 :
Retrieves all facts from the supplied archives, that are relevant to the
 : supplied hypercube, and populates them with the default dimension values
 : when missing.

 :
 :
 : @param $hypercube a hypercube.
 : @param $archives a sequence of archives or their AIDs or $hypercubes:ALL_ARCHIVES for no filtering.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:fact-table-for-hypercube(
  $hypercube as object,
  $archives as item*) as array
{
  hypercubes:fact-table-for-hypercube($hypercube, $archives, {})
};

(:~
 :
 :
Retrieves all facts from the supplied archives, that are relevant to the
 : supplied hypercube, and populates them with the default dimension values
 : when missing.

 :
 :
 : @param $hypercube a hypercube.
 : @param $archives a sequence of archives or their AIDs
 : @param $options standard fact retrieving options.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:fact-table-for-hypercube(
  $hypercube as object,
  $archives as item*,
  $options as object?) as array
{
  hypercubes:fact-table-for-hypercube-and-facts(
    $hypercube,
    hypercubes:facts-for-hypercube($hypercube, $archives, $options)
  )
};

(:~
 :
 :
Utility function to convert a sequence of facts to a fact table
 : given a hypercube. It is not recommended to use this function directly.
 : Use hypercubes:fact-table-for-hypercube instead.

 :
 :
 : @param $hypercube a hypercube.
 : @param $facts a sequence of facts that were retrieved with the hypercube.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:fact-table-for-hypercube-and-facts(
  $hypercube as object,
  $facts as object*) as array
{
  let $dimensions := distinct-values((
      $facts:CONCEPT,
      $facts:ENTITY,
      $facts:PERIOD,
      $facts:UNIT,
      keys($hypercube.Aspects)))
  return [
    [ $dimensions, "Value" ],
    for $fact in $facts
    return [
      $dimensions ! (let $member := $fact.Aspects.($$)
                     return if(exists($member))
                            then $member
                            else null),
      $fact.Value
    ]
  ]
};

(:~
 :
 :
Populates a concept-tree network with all facts from the supplied archives,
 : that are relevant to the
 : supplied hypercube. Default dimension values are added to the facts
 : when missing.

 :
 : @param $networks networks.
 : @param $hypercube a hypercube.
 : @param $archives a sequence of archives or their AIDs or $hypercubes:ALL_ARCHIVES for no filtering.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:populate-networks-with-facts(
  $networks as object*,
  $hypercube as object,
  $archives as item*) as object*
{
  hypercubes:populate-networks-with-facts($networks, $hypercube, $archives, {})
};

(:~
 :
 :
Populates a concept-tree network with all facts from the supplied archives,
 : that are relevant to the
 : supplied hypercube. Default dimension values are added to the facts
 : when missing.

 :
 : @param $networks networks.
 : @param $hypercube a hypercube.
 : @param $archives a sequence of archives or their AIDs.
 : @param $options standard fact retrieving options.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:populate-networks-with-facts(
  $networks as object*,
  $hypercube as object,
  $archives as item*,
  $options as object?) as object*
{
  hypercubes:populate-networks-with-retrieved-facts(
    $networks,
    hypercubes:facts-for-hypercube($hypercube, $archives, $options)
  )
};

(:~
 :
 :
A utility function that populates networks with facts against concepts that belong
 : to them. It is not recommended to use this function directly.
 : Use hypercubes:populate-networks-with-retrieved-facts.

 :
 : @param $networks networks.
 : @param $facts a sequence of facts.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:populate-networks-with-retrieved-facts(
  $networks as object*,
  $facts as object*) as object*
{
  let $fact-table-by-concepts := {|
    for $f in $facts
    group by $concept := $f.Aspects."xbrl:Concept"
    return { $concept : [ $f ] }
  |}
  return
    copy $result := $networks
    modify
      for $concept in descendant-objects(values($result.Trees))[exists($$.Name)]
      let $name := $concept.Name
      return insert json { "Facts" : $fact-table-by-concepts.$name } into $concept
    return $result
};

(:~
 :
Returns an instantiation of a dimensionless Hypercube containing only the basic
 :    characteristics (xbrl:Concept, xbrl:Period, xbrl:Entity, and xbrl:Unit).
 :    For each of those included aspects the value space is not limited.

 :
 : @return dimensionless hypercube instantiation.
 :)
declare function hypercubes:dimensionless-hypercube() as object
{
  hypercubes:dimensionless-hypercube(())
};

(:~
 :
Returns an instantiation of a dimensionless Hypercube containing only the basic
 :    characteristics (xbrl:Concept, xbrl:Period, xbrl:Entity, and xbrl:Unit).
 :    For each of those included aspects the value space is not limited, however
 :    you can restrict it using the options. If you need more
 :    XBRL dimensions, use the user-defined-hypercube function instead.

 :
 : @return dimensionless hypercube instantiation.
 : @param $options: additional options among which:
 : - Concepts: an array of concept names to include in the hypercube.
 : - Periods: an array of periods to include in the hypercube.
 : - Entities: an array of EIDs to include in the hypercube.
 : - Units: an array of units to include in the hypercube.
 :)
declare function hypercubes:dimensionless-hypercube($options as object?) as object
{
  hypercubes:user-defined-hypercube(
    {|
      if(exists($options.Concepts))
      then { "xbrl:Concept" : { Domain: $options.Concepts } }
      else (),
      if(exists($options.Entities))
      then { "xbrl:Entity" : { Domain: [ entities:eid($options.Entities[]) ] } }
      else (),
      if(exists($options.Periods))
      then { "xbrl:Period" : { Domain: $options.Periods } }
      else (),
      if(exists($options.Units))
      then { "xbrl:Unit" : { Domain: $options.Units } }
      else ()
    |}
  )
};

(:~
 :
Returns an instantiation of a Hypercube containing the specified dimensions.
 :    The four basic characteristics (xbrl:Concept, xbrl:Period, xbrl:Entity, and xbrl:Unit)
 :    are always added, but can be overriden. For each of those included aspects the value space is,
 :    by default, not limited, and only xbrl:Unit has a default value.

 :
 : @return the hypercube instantiation.
 : @param $dimensions: an object with pairs of (dimension name, object with dimension information). These
 : dimension information subobjects
 : may have two (optional) fields: Default (a string) and Domain (an array of strings).
 :
 : @error hypercubes:invalid-type if a member of a typed dimension does not match the expected type
 : @error hypercubes:invalid-type if a member of an explicit dimension is not of type string
 :)
declare function hypercubes:user-defined-hypercube($dimensions as object?) as object
{
  {
    "Name" : "xbrl:UserDefinedHypercube",
    "Label" : "User-defined Hypercube",
    "Aspects" : {|
      for $dimension-name as string in distinct-values((
          "xbrl:Concept",
          "xbrl:Entity",
          "xbrl:Period",
          "xbrl:Unit",
          keys($dimensions)
      ))
      let $dimension-spec as object? := $dimensions.$dimension-name
      let $dimension-type as string? := $dimension-spec.Type
      let $dimension-domain as array? := $dimension-spec.Domain
      let $dimension-default as atomic? := $dimension-spec.Default
      let $domain-name := $dimension-name || "Domain"
      return {
        $dimension-name : {|
          (: Name :)
          {
            "Name" : $dimension-name
          },

          (: Label :)
          switch($dimension-name)
          case "xbrl:Concept" return {
            "Label" : "XBRL Concept Dimension"
          }
          case "xbrl:Entity" return {
            "Label" : "XBRL Entity Dimension"
          }
          case "xbrl:Period" return {
            "Label" : "XBRL Period Dimension"
          }
          case "xbrl:Unit" return {
            "Label" : "XBRL Unit Dimension"
          }
          default return (),

          (: Default value :)
          switch(true)
          case exists($dimension-default) return {
            "Default" :
              hypercubes:check-type-and-return($dimension-default,
                                               ($dimension-type, "string")[1])
          }
          case $dimension-name eq "xbrl:Unit" return {
            "Default" : "xbrl:NonNumeric"
          }
          default return (),

          (: Explicit vs. Typed :)
          {
            "Kind" : if(exists($dimension-type))
                     then "TypedDimension"
                     else "ExplicitDimension"
          },

          (: Type :)
          { "Type": $dimension-type }[exists($dimension-type)],

          (: Domain :)
          if (empty($dimension-type))
          then
          {
            "Domains" : {
              $domain-name : {
                "Name" : $domain-name,
                "Label" :
                  switch($dimension-name)
                  case "xbrl:Concept" return "XBRL Concept Domain"
                  case "xbrl:Entity" return "XBRL Entity Domain"
                  case "xbrl:Period" return "XBRL Period Domain"
                  case "xbrl:Unit" return "XBRL Unit Domain"
                  default return $dimension-name || " Domain",
                "Members" : {|
                  for $dimension-member in $dimension-domain[]
                  return {
                    $dimension-member: {
                      Name: hypercubes:check-type-and-return($dimension-member, "string")
                    }
                  }
                |}
              }
            }
          }[exists($dimension-domain)]
          else {
            "DomainRestriction" : {
              "Name" : $domain-name,
              "Label" : $dimension-name || " Domain",
              "Enumeration" : [ hypercubes:check-type-and-return($dimension-domain[], $dimension-type) ]
            }
          }[exists($dimension-domain)]

        |}
      }
    |}
  }
};

declare %private function hypercubes:check-type-and-return(
    $dimension-members as atomic*,
    $dimension-type as string) as atomic*
{
  for $dimension-member as atomic in $dimension-members
  return
  if (not $dimension-member instance of null
      and
      local-name-from-QName(schema:schema-type($dimension-member)) ne
      local-name-from-QName(xs:QName($dimension-type)))
  then error(
        xs:QName("hypercubes:invalid-type"),
        schema:schema-type($dimension-member) ||
          ": type doesn't match expected type (" || $dimension-type || ")")
  else $dimension-member
};

declare function hypercubes:modify-hypercube(
    $hypercube as object,
    $options as object?) as object
{
    let $options :=
    {|
        for $dimension in keys(($hypercube.Aspects, $options))
        let $hypercube-metadata := $hypercube.Aspects.$dimension
        let $new-metadata := $options.$dimension
        return {
            $dimension : if(exists($new-metadata))
            then
                $new-metadata
            else {|
                { Type : $hypercube.Aspects.$dimension.Type }[$hypercube-metadata.Kind eq "TypedDimension"],
                let $hypercube-domain := descendant-objects(values($hypercube-metadata.Domains)).Name
                where exists($hypercube-domain)
                return { Domain : [ $hypercube-domain ] },
                let $hypercube-default := $hypercube-metadata.Default
                where exists($hypercube-default)
                return { Default : $hypercube-default }
            |}
        }
    |}
    return hypercubes:user-defined-hypercube($options)
};

declare function hypercubes:merge($hypercubes as object*) as object
{
    hypercubes:user-defined-hypercube({|
        for $aspect in keys($hypercubes.Aspects)
        let $default := distinct-values($hypercubes.Aspects.$aspect.Default)
        let $domain := distinct-values((descendant-objects($hypercubes.Aspects.$aspect.Domains).Name))
        return {
            $aspect : {|
                { Default: $default}[exists($default)],
                { Domain: [ distinct-values(($domain, $default)) ]}[exists($domain)]
            |}
        }
    |})
};