Make _mask public

Make _mask public

module {
    name: "math/bitwise",
    description: "Bitwise operations on numbers",
    namespace: "fadado.github.io",
    author: {
        name: "Joan Josep Ordinas Rosa",
        email: "jordinas@gmail.com"
    }
};

########################################################################
# Bitwise operations

# BitField: 0..(2^53-1) (53 bits unsigned integer)
# Bit:      2^0, 2^1, 2^2, ...2^52
# Position: 0..52
# New empty bitset: 0e0

# def _bit($position): $position|exp2;
# def _pos($bit): $bit|log2;

# Build mask with $size bits (mask(53) == -1 in signed integers)
def mask($size): #:: (number) => BitField
    $size|exp2-1
;

# Get bit at $position as 0 or 1
def getbit($position; $n): #:: (Position;BitField) => 0^1
    fmod($n/($position|exp2)|floor;2) | fabs
;

# Set bit at $position
def setbit($position; $n): #:: (Position;BitField) => BitField
    if fmod($n/($position|exp2)|floor;2) != 0
    then $n
    else $n + ($position|exp2)
    end
;

# Clear bit at $position
def clrbit($position; $n): #:: (Position;BitField) => BitField
    if fmod($n/($position|exp2)|floor;2) != 0
    then $n - ($position|exp2)
    else $n
    end
;

# Toggle bit at $position
def tglbit($position; $n): #:: (Position;BitField) => BitField
    if fmod($n/($position|exp2)|floor;2) != 0
    then $n - ($position|exp2)
    else $n + ($position|exp2)
    end
;

########################################################################
# Common LISP borrowed functions
#
# CLTL: 12.7. Logical Operations on Numbers
#       http://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node131.html
########################################################################

# See: http://clhs.lisp.se/Body/f_logand.htm
def lognot($n): #:: (BitField) => BitField
    9007199254740991 - $n # use precalculated constant 53|exp2-1
;

# See: http://clhs.lisp.se/Body/f_logand.htm
def logand($m; $n): #:: (BitField;BitField) => BitField
    def r($x; $y; $result; $pow2):
        if $x < 1 or $y < 1
        then $result
        elif fmod($x;2)!=0 and fmod($y;2)!=0
        then r($x/2|floor; $y/2|floor; $result+$pow2; $pow2*2)
        else r($x/2|floor; $y/2|floor; $result; $pow2*2)
        end
    ;
    r($m; $n; 0e0; 1)
;

# See: http://clhs.lisp.se/Body/f_logand.htm
def logior($m; $n): #:: (BitField;BitField) => BitField
    def r($x; $y; $result; $pow2):
        if $x < 1 and $y < 1
        then $result
        elif fmod($x;2)!=0 or fmod($y;2)!=0
        then r($x/2|floor; $y/2|floor; $result+$pow2; $pow2*2)
        else r($x/2|floor; $y/2|floor; $result; $pow2*2)
        end
    ;
    r($m; $n; 0e0; 1)
;

# See: http://clhs.lisp.se/Body/f_logand.htm
def logxor($m; $n): #:: (BitField;BitField) => BitField
    def r($x; $y; $result; $pow2):
        if $x < 1 and $y < 1
        then $result
        elif (fmod($x;2)!=0) != (fmod($y;2)!=0)
        then r($x/2|floor; $y/2|floor; $result+$pow2; $pow2*2)
        else r($x/2|floor; $y/2|floor; $result; $pow2*2)
        end
    ;
    r($m; $n; 0e0; 1)
;

# See: http://clhs.lisp.se/Body/f_logand.htm
def logeqv($m; $n): #:: (BitField;BitField) => BitField
    lognot(logxor($m; $n))
;

# See: http://clhs.lisp.se/Body/f_logand.htm
def lognand($m; $n): #:: (BitField;BitField) => BitField
    lognot(logand($m; $n))
;

# See: http://clhs.lisp.se/Body/f_logand.htm
def lognor($m; $n): #:: (BitField;BitField) => BitField
    lognot(logior($m; $n))
;

# See: http://clhs.lisp.se/Body/f_logand.htm
def logandc1($m; $n): #:: (BitField;BitField) => BitField
    logand(lognot($m); $n)
;

# See: http://clhs.lisp.se/Body/f_logand.htm
def logandc2($m; $n): #:: (BitField;BitField) => BitField
    logand($m; lognot($n))
;

# See: http://clhs.lisp.se/Body/f_logand.htm
def logorc1($m; $n): #:: (BitField;BitField) => BitField
    logior(lognot($m); $n)
;

# See: http://clhs.lisp.se/Body/f_logand.htm
def logorc2($m; $n): #:: (BitField;BitField) => BitField
    logior($m; lognot($n))
;

# See: http://clhs.lisp.se/Body/f_logtes.htm
def logtest($m; $n): #:: (BitField;BitField) => boolean
    logand($m; $n) != 0e0
;

# See: http://clhs.lisp.se/Body/f_logbtp.htm
def logbitp($position; $n): #:: (Position;BitField) => boolean
    fmod($n/($position|exp2)|floor;2) != 0
;

# See: http://clhs.lisp.se/Body/f_ash.htm
def ash($count; $n): #:: (number;BitField) => BitField
    $n*($count|exp2)|floor
;

# See: http://clhs.lisp.se/Body/f_intege.htm
def integer_length($n): #:: (BitField) => number
    if $n < 0 then -$n else $n+1 end
    | log2 | ceil
;

# See: http://clhs.lisp.se/Body/f_logcou.htm
def logcount($n): #:: (BitField) => number
    def r($x; $result):
        if $x < 1
        then $result
        elif fmod($x;2) != 0
        then r($x/2|floor; $result+1)
        else r($x/2|floor; $result)
        end
    ;
    r($n; 0)
;

########################################################################
# Common LISP borrowed functions
#
# CLTL 12.8. Byte Manipulation Functions
#       http://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node132.html
########################################################################

# See: http://clhs.lisp.se/Body/f_by_by.htm
def byte($size; $position):
    {
        siz: $size,
        pos: $position,
        msk: ash($position; $size|exp2-1), # precomputed mask
        neg: null # precomputed negated mask
    } | .neg = lognot(.msk)
;

# See: http://clhs.lisp.se/Body/f_by_by.htm
def byte_size($bytespec):
    $bytespec.siz
;

# See: http://clhs.lisp.se/Body/f_by_by.htm
def byte_position($bytespec):
    $bytespec.pos
;

# See: http://clhs.lisp.se/Body/f_ldb.htm
def ldb($bytespec; $n):
    ash(-$bytespec.pos; logand($bytespec.msk; $n))
;

# See: http://clhs.lisp.se/Body/f_ldb_te.htm
def ldb_test($bytespec; $n):
    logand($bytespec.msk; $n) != 0
;

# See: http://clhs.lisp.se/Body/f_mask_f.htm
def mask_field($bytespec; $n):
    logand($bytespec.msk; $n)
;

# See: http://clhs.lisp.se/Body/f_dpb.htm
def dpb($newbyte; $bytespec; $n):
    logior(logand($bytespec.neg; $n);
           logand($bytespec.msk; ash($bytespec.pos; $newbyte)))
;

# See: http://clhs.lisp.se/Body/f_deposi.htm
def deposit_field($newbyte; $bytespec; $n):
    logior(logand($bytespec.neg; $n);
           logand($bytespec.msk; $newbyte))
;

# vim:ai:sw=4:ts=4:et:syntax=jq

module {
    name: "math/bitwise",
    description: "Bitwise operations on numbers",
    namespace: "fadado.github.io",
    author: {
        name: "Joan Josep Ordinas Rosa",
        email: "jordinas@gmail.com"
    }
};

########################################################################
# Bitwise operations

# BitField: 0..(2^53-1) (53 bits unsigned integer)
# Bit:      2^0, 2^1, 2^2, ...2^52
# Position: 0..52
# New empty bitset: 0e0

# def _bit($position): $position|exp2;
# def _pos($bit): $bit|log2;

# Build mask with $size bits (mask(53) == -1 in signed integers)
def mask($size): #:: (number) => BitField
    $size|exp2-1
;

# Set bit at $position
def setbit($position; $n): #:: (Position;BitField) => BitField
    if fmod($n/($position|exp2)|floor;2) != 0
    then $n
    else $n + ($position|exp2)
    end
;

# Clear bit at $position
def clrbit($position; $n): #:: (Position;BitField) => BitField
    if fmod($n/($position|exp2)|floor;2) != 0
    then $n - ($position|exp2)
    else $n
    end
;

# Toggle bit at $position
def tglbit($position; $n): #:: (Position;BitField) => BitField
    if fmod($n/($position|exp2)|floor;2) != 0
    then $n - ($position|exp2)
    else $n + ($position|exp2)
    end
;

# Get bit at $position as 0 or 1
def getbit($position; $n): #:: (Position;BitField) => 0^1
    fmod($n/($position|exp2)|floor;2) | fabs
;

########################################################################
# Common LISP borrowed functions
#
# CLTL: 12.7. Logical Operations on Numbers
#       http://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node131.html
########################################################################

# See: http://clhs.lisp.se/Body/f_logand.htm
def lognot($n): #:: (BitField) => BitField
    9007199254740991 - $n # use precalculated constant 53|exp2-1
;

# See: http://clhs.lisp.se/Body/f_logand.htm
def logand($m; $n): #:: (BitField;BitField) => BitField
    def r($x; $y; $result; $pow2):
        if $x < 1 or $y < 1
        then $result
        elif fmod($x;2)!=0 and fmod($y;2)!=0
        then r($x/2|floor; $y/2|floor; $result+$pow2; $pow2*2)
        else r($x/2|floor; $y/2|floor; $result; $pow2*2)
        end
    ;
    r($m; $n; 0e0; 1)
;

# See: http://clhs.lisp.se/Body/f_logand.htm
def logior($m; $n): #:: (BitField;BitField) => BitField
    def r($x; $y; $result; $pow2):
        if $x < 1 and $y < 1
        then $result
        elif fmod($x;2)!=0 or fmod($y;2)!=0
        then r($x/2|floor; $y/2|floor; $result+$pow2; $pow2*2)
        else r($x/2|floor; $y/2|floor; $result; $pow2*2)
        end
    ;
    r($m; $n; 0e0; 1)
;

# See: http://clhs.lisp.se/Body/f_logand.htm
def logxor($m; $n): #:: (BitField;BitField) => BitField
    def r($x; $y; $result; $pow2):
        if $x < 1 and $y < 1
        then $result
        elif (fmod($x;2)!=0) != (fmod($y;2)!=0)
        then r($x/2|floor; $y/2|floor; $result+$pow2; $pow2*2)
        else r($x/2|floor; $y/2|floor; $result; $pow2*2)
        end
    ;
    r($m; $n; 0e0; 1)
;

# See: http://clhs.lisp.se/Body/f_logand.htm
def logeqv($m; $n): #:: (BitField;BitField) => BitField
    lognot(logxor($m; $n))
;

# See: http://clhs.lisp.se/Body/f_logand.htm
def lognand($m; $n): #:: (BitField;BitField) => BitField
    lognot(logand($m; $n))
;

# See: http://clhs.lisp.se/Body/f_logand.htm
def lognor($m; $n): #:: (BitField;BitField) => BitField
    lognot(logior($m; $n))
;

# See: http://clhs.lisp.se/Body/f_logand.htm
def logandc1($m; $n): #:: (BitField;BitField) => BitField
    logand(lognot($m); $n)
;

# See: http://clhs.lisp.se/Body/f_logand.htm
def logandc2($m; $n): #:: (BitField;BitField) => BitField
    logand($m; lognot($n))
;

# See: http://clhs.lisp.se/Body/f_logand.htm
def logorc1($m; $n): #:: (BitField;BitField) => BitField
    logior(lognot($m); $n)
;

# See: http://clhs.lisp.se/Body/f_logand.htm
def logorc2($m; $n): #:: (BitField;BitField) => BitField
    logior($m; lognot($n))
;

# See: http://clhs.lisp.se/Body/f_logtes.htm
def logtest($m; $n): #:: (BitField;BitField) => boolean
    logand($m; $n) != 0e0
;

# See: http://clhs.lisp.se/Body/f_logbtp.htm
def logbitp($position; $n): #:: (Position;BitField) => boolean
    fmod($n/($position|exp2)|floor;2) != 0
;

# See: http://clhs.lisp.se/Body/f_ash.htm
def ash($count; $n): #:: (number;BitField) => BitField
    $n*($count|exp2)|floor
;

# See: http://clhs.lisp.se/Body/f_intege.htm
def integer_length($n): #:: (BitField) => number
    if $n < 0 then -$n else $n+1 end
    | log2 | ceil
;

# See: http://clhs.lisp.se/Body/f_logcou.htm
def logcount($n): #:: (BitField) => number
    def r($x; $result):
        if $x < 1
        then $result
        elif fmod($x;2) != 0
        then r($x/2|floor; $result+1)
        else r($x/2|floor; $result)
        end
    ;
    r($n; 0)
;

########################################################################
# Common LISP borrowed functions
#
# CLTL 12.8. Byte Manipulation Functions
#       http://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node132.html
########################################################################

# See: http://clhs.lisp.se/Body/f_by_by.htm
def byte($size; $position):
    {
        siz: $size,
        pos: $position,
        msk: ash($position; $size|exp2-1), # precomputed mask
        neg: null # precomputed negated mask
    } | .neg = lognot(.msk)
;

# See: http://clhs.lisp.se/Body/f_by_by.htm
def byte_size($bytespec):
    $bytespec.siz
;

# See: http://clhs.lisp.se/Body/f_by_by.htm
def byte_position($bytespec):
    $bytespec.pos
;

# See: http://clhs.lisp.se/Body/f_ldb.htm
def ldb($bytespec; $n):
    ash(-$bytespec.pos; logand($bytespec.msk; $n))
;

# See: http://clhs.lisp.se/Body/f_ldb_te.htm
def ldb_test($bytespec; $n):
    logand($bytespec.msk; $n) != 0
;

# See: http://clhs.lisp.se/Body/f_mask_f.htm
def mask_field($bytespec; $n):
    logand($bytespec.msk; $n)
;

# See: http://clhs.lisp.se/Body/f_dpb.htm
def dpb($newbyte; $bytespec; $n):
    logior(logand($bytespec.neg; $n);
           logand($bytespec.msk; ash($bytespec.pos; $newbyte)))
;

# See: http://clhs.lisp.se/Body/f_deposi.htm
def deposit_field($newbyte; $bytespec; $n):
    logior(logand($bytespec.neg; $n);
           logand($bytespec.msk; $newbyte))
;

# vim:ai:sw=4:ts=4:et:syntax=jq


:books: Fix documentation.

:books: Fix documentation.

jsoniq version "1.0";
(:
 : Copyright 2015 28msec Inc.
 :)

(:~
 :
This module provides functionality for retrieving labels.

 :
 : @author Federico Cavalieri, Ghislain Fourny
 :)
module namespace labels = "http://28.io/modules/xbrl/labels";

import module namespace concepts = "http://28.io/modules/xbrl/concepts";
import module namespace entities = "http://28.io/modules/xbrl/entities";

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

(:~
 : A helper variable holding the default language ("en-US")
 :)
declare variable $labels:AMERICAN_ENGLISH as xs:string := "en-US";

(:~
 : The standard label role.
 :)
declare variable $labels:STANDARD_LABEL_ROLE as xs:string :=
    "http://www.xbrl.org/2003/role/label";

(:~
 : The verbose label role.
 :)
declare variable $labels:VERBOSE_LABEL_ROLE as xs:string :=
    "http://www.xbrl.org/2003/role/verboseLabel";

(:~
 :
Retrieves all the labels with the given label role and language in the
 : matching concepts, one label for each concept.

 :
 :
Matching concepts are those which:
 :  - concept name and archive number match a given one;
 :  - component role matches a given one or is the default
 :    component role.
 :

 :
 :
The set of concepts to search in is specified as a parameter.

 :
 :
Language matching can either be exact, if no options are given,
 : or approximated, if at least one of the following options is given:

 :

 :
MatchDown: whether to match a more specific language, e.g.:
 :       "en" will match labels which language is "en" or "en-US".

 :
MatchUp: whether to match a less specific language, e.g.:
 :       "en-US" will match labels which language is "en-US" or "en".

 :
MatchAnyVariant: whether to match a different variant of the same
 :       language, e.g.: "en-US" will match labels which language is "en-US"
 :       or "en-UK".

 :

 :
 : @param $concept-names the concepts names.
 : @param $label-roles the label role.
 : @param $languages the label language.
 : @param $concepts the concepts in which the labels will be
 :                  searched.
 : @param $entities the entities in which entity labels will be searched.
 : @param $options Optional parameters to control language matching.
 :
 : @return the matching labels (one associated with each input concept, as
 : an object).
 :)
declare function labels:labels(
    $concept-names as string*,
    $label-roles as string*,
    $languages as string*,
    $concepts as object*,
    $entities as object*,
    $options as object?
  ) as object
{
  {|
    let $normalized-languages as string* := $languages ! labels:normalize-language($$)
    return (
      for $concept as object in $concepts
      where ($concepts:ALL_CONCEPT_NAMES, $concept.$concepts:NAME) = $concept-names
      group by $concept.$concepts:NAME
      let $concept-labels as object* :=
        $concept.$concepts:LABELS[][$$.Role = $label-roles]
      let $available-languages as string* :=
        distinct-values($concept-labels.Language)
      let $approximate-languages as string* := labels:approximate-languages(
        $available-languages,
        $normalized-languages,
        $options
      )
      let $perfect-matches as object* :=
        $concept-labels[$$.Language = $normalized-languages]
      let $approximate-matches as object* :=
        $concept-labels[$$.Language = $approximate-languages]
      let $label as string? :=
        ($perfect-matches.Value,
         $approximate-matches.Value,
         $concept-labels.Value,
         $concept.Label)[1]
      where exists($label)
      return {
        $concept.Name : $label
      },

      for $concept in $concept-names
      where not (($concepts:ALL_CONCEPT_NAMES, $concepts.$concepts:NAME) = $concept)
      let $label as string* :=
          switch(true)
            case starts-with($concept, "iso4217:")
            return substring($concept, 9)
            case $concept castable as date
            return format-date(date($concept), "[MNn] [D], [Y]", "en", (), ())
            case contains($concept, " ")
            return
              switch(true)
              case $concept = entities:eid($entities) and $normalized-languages = ("ja")
              return ($entities[entities:eid($$) = $concept].Profiles.SEC.EntityRegistrantName,
                       $entities[entities:eid($$) = $concept].Profiles.FSA.SubmitterName)[1]
              case $concept = entities:eid($entities)
              return ($entities[entities:eid($$) = $concept].Profiles.SEC.CompanyName,
                       $entities[entities:eid($$) = $concept].Profiles.FSA.SubmitterNameAlphabetic)[1]
              default return ()
           default return ()
      where exists($label)
      return {
        $concept : $label
      }
    )
  |}
};

(:~
 :
Retrieves all the labels with the given label role and language for
 : all concepts used in the fact and matching a concept in the list of
 : concepts. Concepts used in a fact include not only those from the
 : 'xbrl:Concept' aspect, but also Members of any custom axis.

 :
 :
Matching concepts are those which:
 :  - concept name matches a given one,
 :  - archive number matches that of a given component,
 :  - component role matches that of a given component or is the default
 :    component role.
 :

 :
 :
The set of concepts to search in is specified as a parameter.

 :
 :
Language matching can either be exact, if no options are given,
 : or approximated, if at least one of the following options is given:

 :

 :
MatchDown: whether to match a more specific language, e.g.:
 :       "en" will match labels which language is "en" or "en-US".

 :
MatchUp: whether to match a less specific language, e.g.:
 :       "en-US" will match labels which language is "en-US" or "en".

 :
MatchAnyVariant: whether to match a different variant of the same
 :       language, e.g.: "en-US" will match labels which language is "en-US"
 :       or "en-UK".

 :

 :
 : @param $facts a sequence of facts.
 : @param $label-role the label role.
 : @param $language the label language.
 : @param $concepts the concepts in which the labels will be
 :                  searched (in the version-7 format).
 : @param $options optional parameters to control language matching.
 :
 : @return an object with matching concepts as keys and labels as values.
 :)
declare function labels:labels-for-facts(
    $facts as object*,
    $label-role as string,
    $language as string,
    $concepts as object*,
    $entities as object*,
    $options as object?
  ) as object?
{
    for $facts in $facts
    group by $archive := $facts."Aspects"."xbrl28:Archive"
    let $concept-names as string* :=
        distinct-values((
          values($facts.Aspects),
          keys($facts.Aspects)
    )[$$ instance of string])
    return
        {|
            labels:labels(
                    $concept-names,
                    $label-role,
                    $language,
                    $concepts,
                    $entities,
                    $options)
        |}
};

(:~
 :
Normalizes the language code. This normalized language code can then
 : be used to find the right labels in the concepts collection.

 :
 : @param $language the language identifier.
 :
 : @return the normalized language.
 :)
declare function labels:normalize-language($language as string) as string
{
  replace(lower-case($language), "_", "-")
};

declare %private function labels:approximate-languages(
    $all-languages as string*,
    $normalized-language as string,
    $options as object?
  ) as string*
{
    if(not $options.MatchDown cast as boolean? eq false)
    then $all-languages[starts-with($$, $normalized-language)]
    else (),
    if(not $options.MatchUp cast as boolean? eq false)
    then $all-languages[starts-with($normalized-language, $$)]
    else (),
    if ((not $options.MatchAnyVariant cast as boolean? eq false) and contains($normalized-language, "-"))
    then
      $all-languages[starts-with($$, substring-before($normalized-language, "-") || "-")]
    else ()
};

jsoniq version "1.0";
(:
 : Copyright 2015 28msec Inc.
 :)

(:~
 :
This module provides functionality for retrieving labels.

 :
 : @author Federico Cavalieri, Ghislain Fourny
 :)
module namespace labels = "http://28.io/modules/xbrl/labels";

import module namespace concepts = "http://28.io/modules/xbrl/concepts";
import module namespace entities = "http://28.io/modules/xbrl/entities";

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

(:~
 : A helper variable holding the default language ("en-US")
 :)
declare variable $labels:AMERICAN_ENGLISH as xs:string := "en-US";

(:~
 : The standard label role.
 :)
declare variable $labels:STANDARD_LABEL_ROLE as xs:string :=
    "http://www.xbrl.org/2003/role/label";

(:~
 : The verbose label role.
 :)
declare variable $labels:VERBOSE_LABEL_ROLE as xs:string :=
    "http://www.xbrl.org/2003/role/verboseLabel";

(:~
 :
Retrieves all the labels with the given label role and language in the
 : matching concepts, one label for each concept.

 :
 :
Matching concepts are those which:
 :  - concept name and archive number match a given one;
 :  - component role matches a given one or is the default
 :    component role.
 :

 :
 :
The set of concepts to search in is specified as a parameter.

 :
 :
Language matching can either be exact, if no options are given,
 : or approximated, if at least one of the following options is given:

 :

 :
MatchDown: whether to match a more specific language, e.g.:
 :       "en" will match labels which language is "en" or "en-US".

 :
MatchUp: whether to match a less specific language, e.g.:
 :       "en-US" will match labels which language is "en-US" or "en".

 :
MatchAnyVariant: whether to match a different variant of the same
 :       language, e.g.: "en-US" will match labels which language is "en-US"
 :       or "en-UK".

 :

 :
 : @param $concept-names the concepts names.
 : @param $label-role the label role.
 : @param $language the label language.
 : @param $concepts the concepts in which the labels will be
 :                  searched.
 : @param $entities the entities in which entity labels will be searched.
 : @param $options Optional parameters to control language matching.
 :
 : @return the matching labels (one associated with each input concept, as
 : an object).
 :)
declare function labels:labels(
    $concept-names as string*,
    $label-roles as string*,
    $languages as string*,
    $concepts as object*,
    $entities as object*,
    $options as object?
  ) as object
{
  {|
    let $normalized-languages as string* := $languages ! labels:normalize-language($$)
    return (
      for $concept as object in $concepts
      where ($concepts:ALL_CONCEPT_NAMES, $concept.$concepts:NAME) = $concept-names
      group by $concept.$concepts:NAME
      let $concept-labels as object* :=
        $concept.$concepts:LABELS[][$$.Role = $label-roles]
      let $available-languages as string* :=
        distinct-values($concept-labels.Language)
      let $approximate-languages as string* := labels:approximate-languages(
        $available-languages,
        $normalized-languages,
        $options
      )
      let $perfect-matches as object* :=
        $concept-labels[$$.Language = $normalized-languages]
      let $approximate-matches as object* :=
        $concept-labels[$$.Language = $approximate-languages]
      let $label as string? :=
        ($perfect-matches.Value,
         $approximate-matches.Value,
         $concept-labels.Value,
         $concept.Label)[1]
      where exists($label)
      return {
        $concept.Name : $label
      },

      for $concept in $concept-names
      where not (($concepts:ALL_CONCEPT_NAMES, $concepts.$concepts:NAME) = $concept)
      let $label as string* :=
          switch(true)
            case starts-with($concept, "iso4217:")
            return substring($concept, 9)
            case $concept castable as date
            return format-date(date($concept), "[MNn] [D], [Y]", "en", (), ())
            case contains($concept, " ")
            return
              switch(true)
              case $concept = entities:eid($entities) and $normalized-languages = ("ja")
              return ($entities[entities:eid($$) = $concept].Profiles.SEC.EntityRegistrantName,
                       $entities[entities:eid($$) = $concept].Profiles.FSA.SubmitterName)[1]
              case $concept = entities:eid($entities)
              return ($entities[entities:eid($$) = $concept].Profiles.SEC.CompanyName,
                       $entities[entities:eid($$) = $concept].Profiles.FSA.SubmitterNameAlphabetic)[1]
              default return ()
           default return ()
      where exists($label)
      return {
        $concept : $label
      }
    )
  |}
};

(:~
 :
Retrieves all the labels with the given label role and language for
 : all concepts used in the fact and matching a concept in the list of
 : concepts. Concepts used in a fact include not only those from the
 : 'xbrl:Concept' aspect, but also Members of any custom axis.

 :
 :
Matching concepts are those which:
 :  - concept name matches a given one,
 :  - archive number matches that of a given component,
 :  - component role matches that of a given component or is the default
 :    component role.
 :

 :
 :
The set of concepts to search in is specified as a parameter.

 :
 :
Language matching can either be exact, if no options are given,
 : or approximated, if at least one of the following options is given:

 :

 :
MatchDown: whether to match a more specific language, e.g.:
 :       "en" will match labels which language is "en" or "en-US".

 :
MatchUp: whether to match a less specific language, e.g.:
 :       "en-US" will match labels which language is "en-US" or "en".

 :
MatchAnyVariant: whether to match a different variant of the same
 :       language, e.g.: "en-US" will match labels which language is "en-US"
 :       or "en-UK".

 :

 :
 : @param $facts a sequence of facts.
 : @param $label-role the label role.
 : @param $language the label language.
 : @param $concepts the concepts in which the labels will be
 :                  searched (in the version-7 format).
 : @param $options optional parameters to control language matching.
 :
 : @return an object with matching concepts as keys and labels as values.
 :)
declare function labels:labels-for-facts(
    $facts as object*,
    $label-role as string,
    $language as string,
    $concepts as object*,
    $entities as object*,
    $options as object?
  ) as object?
{
    for $facts in $facts
    group by $archive := $facts."Aspects"."xbrl28:Archive"
    let $concept-names as string* :=
        distinct-values((
          values($facts.Aspects),
          keys($facts.Aspects)
    )[$$ instance of string])
    return
        {|
            labels:labels(
                    $concept-names,
                    $label-role,
                    $language,
                    $concepts,
                    $entities,
                    $options)
        |}
};

(:~
 :
Normalizes the language code. This normalized language code can then
 : be used to find the right labels in the concepts collection.

 :
 : @param $language the language identifier.
 :
 : @return the normalized language.
 :)
declare function labels:normalize-language($language as string) as string
{
  replace(lower-case($language), "_", "-")
};

declare %private function labels:approximate-languages(
    $all-languages as string*,
    $normalized-language as string,
    $options as object?
  ) as string*
{
    if(not $options.MatchDown cast as boolean? eq false)
    then $all-languages[starts-with($$, $normalized-language)]
    else (),
    if(not $options.MatchUp cast as boolean? eq false)
    then $all-languages[starts-with($normalized-language, $$)]
    else (),
    if ((not $options.MatchAnyVariant cast as boolean? eq false) and contains($normalized-language, "-"))
    then
      $all-languages[starts-with($$, substring-before($normalized-language, "-") || "-")]
    else ()
};


:pencil: Add archive statistics to output.

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

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

(: Query parameters :)
declare  %rest:case-insensitive                 variable $token         as string? external;
declare  %rest:env                              variable $request-uri   as string  external;
declare  %rest:case-insensitive                 variable $format        as string? external;
declare  %rest:case-insensitive %rest:distinct  variable $eid           as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $cik           as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $edinetcode    as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $tag           as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $ticker        as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $sic           as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $fiscalYear    as string* external := "LATEST";
declare  %rest:case-insensitive %rest:distinct  variable $fiscalPeriod  as string* external := "FY";
declare  %rest:case-insensitive %rest:distinct  variable $filingKind    as string* external := ();
declare  %rest:case-insensitive %rest:distinct  variable $aid           as string* external;
declare  %rest:case-insensitive                 variable $profile-name  as string  external := $config:profile-name;

session:audit-call($token);

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

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

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

let $archives as object* := multiplexer:filings(
  $profile-name,
  $entities,
  $fiscalPeriod,
  $fiscalYear,
  $filingKind,
  $aid)

let $summaries :=
    switch($profile-name)
    case "sec" return
        for $f in filings:summaries($archives)
        order by $f.Accepted descending
        return $f
    case "japan" return
      for $a in $archives
      order by $a.Profiles.FSA.SubmissionDate descending
      return project($a, ("_id", "Entity", "Profiles", "Statistics"))
    default return $archives

let $summaries :=
  switch($profile-name)
  case "sec" return
    for $archive in $summaries
    return {|
      project($archive, "AccessionNumber"),
      {
        Components: backend:url("components",
          {
              token: $token,
              aid: encode-for-uri($archive.AccessionNumber),
              format: $format,
              profile-name: $profile-name
          })
      },
      trim($archive, "AccessionNumber")
    |}
  default return
    for $archive in $summaries
    return {|
      project($archive, "_id"),
      {
        Components: backend:url("components",
          {
              token: $token,
              aid: encode-for-uri(archives:aid($archive)),
              format: $format,
              profile-name: $profile-name
          })
       },
      trim($archive, ("_id", "Statistics")),
      {
          NumComponents: archives:num-components($archive),
          NumFacts: archives:num-facts($archive),
          NumFootnotes: archives:num-footnotes($archive),
          NumReportElements: archives:num-report-elements($archive),
          NumHypercubes: archives:num-hypercubes($archive),
          NumDimensions: archives:num-dimensions($archive),
          NumMembers: archives:num-members($archive),
          NumLineItems: archives:num-line-items($archive),
          NumAbstracts: archives:num-abstracts($archive),
          NumConcepts: archives:num-concepts($archive)
      }
    |}

let $result := { "Archives" : [ $summaries ] }
let $comment :=
{
    NumArchives: count($summaries),
    TotalNumArchives: session:num-archives(),
    TotalNumEntities: session:num-entities()
}
let $serializers := {
    to-xml : function($res as object) as node() {
        switch($profile-name)
        case "sec" return
          <Filings>{
            filings:summaries-to-xml($res.Archives[])
          }</Filings>
        default return
          <Archives>{
            api:json-to-xml($res.Archives[], "Archive")
          }</Archives>
    },
    to-csv : function($res as object) as string {
        switch($profile-name)
        case "sec" return
            string-join(filings:summaries-to-csv($res.Archives[]))
        default return api:json-to-csv($res.Archives[])
    }
}

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

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

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

(: Query parameters :)
declare  %rest:case-insensitive                 variable $token         as string? external;
declare  %rest:env                              variable $request-uri   as string  external;
declare  %rest:case-insensitive                 variable $format        as string? external;
declare  %rest:case-insensitive %rest:distinct  variable $eid           as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $cik           as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $edinetcode    as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $tag           as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $ticker        as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $sic           as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $fiscalYear    as string* external := "LATEST";
declare  %rest:case-insensitive %rest:distinct  variable $fiscalPeriod  as string* external := "FY";
declare  %rest:case-insensitive %rest:distinct  variable $filingKind    as string* external := ();
declare  %rest:case-insensitive %rest:distinct  variable $aid           as string* external;
declare  %rest:case-insensitive                 variable $profile-name  as string  external := $config:profile-name;

session:audit-call($token);

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

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

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

let $archives as object* := multiplexer:filings(
  $profile-name,
  $entities,
  $fiscalPeriod,
  $fiscalYear,
  $filingKind,
  $aid)

let $summaries :=
    switch($profile-name)
    case "sec" return
        for $f in filings:summaries($archives)
        order by $f.Accepted descending
        return $f
    case "japan" return
      for $a in $archives
      order by $a.Profiles.FSA.SubmissionDate descending
      return project($a, ("_id", "Entity", "Profiles"))
    default return $archives

let $summaries :=
  switch($profile-name)
  case "sec" return
    for $archive in $summaries
    return {|
      project($archive, "AccessionNumber"),
      {
        Components: backend:url("components",
          {
              token: $token,
              aid: encode-for-uri($archive.AccessionNumber),
              format: $format,
              profile-name: $profile-name
          })
      },
      trim($archive, "AccessionNumber")
    |}
  default return
    for $archive in $summaries
    return {|
      project($archive, "_id"),
      {
        Components: backend:url("components",
          {
              token: $token,
              aid: encode-for-uri(archives:aid($archive)),
              format: $format,
              profile-name: $profile-name
          })
      },
      trim($archive, "_id")
    |}

let $result := { "Archives" : [ $summaries ] }
let $comment :=
{
    NumArchives: count($summaries),
    TotalNumArchives: session:num-archives(),
    TotalNumEntities: session:num-entities()
}
let $serializers := {
    to-xml : function($res as object) as node() {
        switch($profile-name)
        case "sec" return
          <Filings>{
            filings:summaries-to-xml($res.Archives[])
          }</Filings>
        default return
          <Archives>{
            api:json-to-xml($res.Archives[], "Archive")
          }</Archives>
    },
    to-csv : function($res as object) as string {
        switch($profile-name)
        case "sec" return
            string-join(filings:summaries-to-csv($res.Archives[]))
        default return api:json-to-csv($res.Archives[])
    }
}

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


:books: Fix documentation.

:books: Fix documentation.

jsoniq version "1.0";

(:
 : Copyright 2012-2014 28msec Inc.
 :)
(:~
 :
This module provides functionality for automatically fetching entities, etc, from the
 : appropriate profile.

 :
 : @author Ghislain Fourny
 :
 :)
module namespace multiplexer = "http://28.io/modules/xbrl/profiles/multiplexer";

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

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

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

(:~
 :
Return latest filings of entities and fiscal periods.

 :
 : @param $profile-name the name of the profile (e.g., SEC, Japan, Generic).
 : @param $entities a sequence of entity objects.
 : @param $fiscal-periods a set of fiscal periods such as "Q1", "Q2", "Q3", "Q4", "FY"
 :
 : @return the latest archives for given entities and fiscal periods.
 :)
declare function multiplexer:latest-filings(
  $profile-name as string,
  $entities as object*,
  $fiscal-periods as string*) as object*
{
  switch($profile-name)
  case "sec" return fiscal-core:latest-filings($entities, $fiscal-periods)
  case "japan" return
    japan:latest-filings($entities, $fiscal-periods)
  default return
    ()
};

(:~
 :
Retrieves entities depending on the profile.

 :
 : @param $profile-name the name of the profile (e.g., SEC, Japan, Generic).
 : @param $eid a sequence of EIDs.
 : @param $cik a sequence of profile-specific CIKs.
 : @param $tag a sequence of tags (e.g., indices, ALL, ...).
 : @param $ticker a sequence of stock exchange tickers.
 : @param $sic a sequence of industry group SIC codes.
 : @param $aid a sequence of archive IDs.
  :
 : @return the entities retrieved according to the profile specified.
 :)
declare function multiplexer:entities(
  $profile-name as string,
  $eid as string*,
  $cik as string*,
  $tag as string*,
  $ticker as string*,
  $sic as string*,
  $aid as string*) as object*
{
  switch($profile-name)
  case "sec" return
    for $entity in companies:companies(
      $cik,
      $tag,
      $ticker,
      $sic,
      $eid,
      $aid)
    order by $entity.Profiles.SEC.CompanyName
    return $entity
  case "japan" return
    for $entity in japan:entities($cik, $ticker, $eid, $tag)
    order by $entity.SubmitterNameAlphabetic
    return $entity
  default return
    entities:entities($eid)
};

(:~
 :
Retrieves archives depending on the profile.

 :
 : @param $profile-name the name of the profile (e.g., SEC, Japan, Generic).
 : @param $entities a sequence of entities or EIDs.
 : @param $fiscalPeriod a sequence of fiscal periods (Q1, Q2, Q3, FY).
 : @param $fiscalYear a sequence of fiscal years.
 : @param $aid a sequence of AIDs.
 :
 : @return the archives retrieved according to the profile specified.
 :)
declare function multiplexer:filings(
  $profile-name as string,
  $entities as item*,
  $fiscalPeriod as string*,
  $fiscalYear as integer*,
  $aid as string*) as object*
{
  switch($profile-name)
  case "sec" return fiscal-core:filings(
      $entities,
      $fiscalPeriod,
      $fiscalYear,
      $aid)
  case "japan" return japan:filings(
      $entities,
      $fiscalYear,
      $fiscalPeriod,
      $aid)
  default return (
      archives:archives($aid),
      archives:archives-for-entities($entities))
};

(:~
  :
Retrieves components depending on the profile.

  :
  : @param $profile-name the name of the profile (e.g., SEC, Japan, Generic).
  : @param $archives a sequence of archive objects.
  : @param $cid a sequence of CIDs.
  : @param $concept a sequence of report element names.
  : @param $disclosure a sequence of disclosure names.
  : @param $role a sequence of network identifiers.
  : @param $exact-label a sequence of labels for exact matches.
  : @param $full-text-label a sequence of labels for full-text search.
  : searched.
  : @param $options additional options. among which

  :
MetadataOnly: to only get Archive and Role information.

  : </lu>
  :
  : @error multiplexer:ARCHIVE-MISSING if a AID is required but not provided.
  :
  : @return the components retrieved according to the profile specified.
:)
declare function multiplexer:components(
  $profile-name as string,
  $archive as object*,
  $cid as string*,
  $concept as string*,
  $disclosure as string*,
  $role as string*,
  $exact-label as string*,
  $full-text-label as string*,
  $options as object?) as object*
{
  switch($profile-name)
  case "sec" return sec-networks:components(
    $archive,
    $cid,
    $concept,
    $disclosure,
    $role,
    $full-text-label)
  default return
    let $role as string* := if(empty($role))
                            then $components:ALL-ROLES
                            else $role
    let $archive as item* := if(empty($archive))
                             then $components:ALL-ARCHIVES
                             else $archive
    let $concept as item* := if(empty($concept))
                             then $components:ALL-CONCEPTS
                             else $concept
    let $exact-label as item* := if(empty($exact-label))
                            then $components:ALL-LABELS
                            else $exact-label
    return components:components-for(
      $archive,
      $role,
      $concept,
      $exact-label,
      $options
    )
};

(:~
  :
Retrieves components depending on the profile.

  :
  : @param $profile-name the name of the profile (e.g., SEC, Japan, Generic).
  : @param $archives a sequence of archive objects.
  : @param $cid a sequence of CIDs.
  : @param $concept a sequence of report element names.
  : @param $disclosure a sequence of disclosure names.
  : @param $role a sequence of network identifiers.
  : @param $exact-label a sequence of labels for exact matches.
  : @param $full-text-label a sequence of labels for full-text search.
  :
  : @error multiplexer:ARCHIVE-MISSING if a AID is required but not provided.
  :
  : @return the components retrieved according to the profile specified.
:)
declare function multiplexer:components(
  $profile-name as string,
  $archive as object*,
  $cid as string*,
  $concept as string*,
  $disclosure as string*,
  $role as string*,
  $exact-label as string*,
  $full-text-label as string*) as object*
{
  multiplexer:components(
    $profile-name,
    $archive,
    $cid,
    $concept,
    $disclosure,
    $role,
    $exact-label,
    $full-text-label,
    ())
};

declare function multiplexer:concepts(
  $profile-name as string,
  $archive as object*,
  $concept as string*,
  $disclosure as string*,
  $role as string*,
  $exact-label as string*,
  $full-text-label as string*,
  $report as object?,
  $onlyNames as boolean) as object*
{
  switch(true)
  case exists($report)
    return
    let $map as object? := reports:concept-map($report)
    let $concepts-computable-by-maps as object* :=
        switch(true)
            case not exists($map) return ()
            case not exists($concept) return $map.Trees[]
            default return
                let $keys as string* := $map.Trees[].Name
                for $c as string in $concept[$$ = $keys]
                return ($map.Trees[])[$$.Name eq $c]
    let $mapped-names as string* := $concepts-computable-by-maps.To[].Name
    let $concepts-not-computable-by-maps as string* :=
        seq:value-except($concept, $mapped-names)
    let $results-not-computed-by-maps as object* :=
        if(exists($concepts-not-computable-by-maps))
        then
          multiplexer:concepts(
            $profile-name,
            $archive,
            $concepts-not-computable-by-maps,
            $disclosure,
            $role,
            $exact-label,
            $full-text-label,
            (),
            $onlyNames
          )
        else ()
    let $results-computed-by-maps as object* :=
        let $all-results as object* := multiplexer:concepts(
          $profile-name,
          $archive,
          $mapped-names,
          $disclosure,
          $role,
          $exact-label,
          $full-text-label,
          (),
          $onlyNames
        )
        for $c as object in $concepts-computable-by-maps
        for $result as object in
            for $candidate-concept in $c.To[].Name
            let $facts := $all-results[$$.Name = $candidate-concept]
            where exists($facts)
            count $n
            where $n eq 1
            return $facts
        let $map-concept := (for $candidate in $concepts-computable-by-maps
                            where $result.Name = (keys($candidate.To), $candidate.To[].Name)
                            return $candidate)[1]
        return
            copy $n := $result
            modify (
                replace value of json $n.Name with $map-concept.Name,
                insert json  { Origin : $result.Name } into $n)
            return $n
    return ($results-not-computed-by-maps, $results-computed-by-maps)

  case empty($disclosure) and empty($report) and empty($full-text-label)
    return
    let $role as string* := if(empty($role))
                            then $concepts:ANY_COMPONENT_LINK_ROLE
                            else $role
    let $concept as item* := if(empty($concept))
                             then $concepts:ALL_CONCEPT_NAMES
                             else $concept
    let $exact-label as item* := if(empty($exact-label))
                            then $concepts:ALL_CONCEPT_LABELS
                            else $exact-label
    return concepts:concepts(
      $concept,
      $archive,
      $role,
      $exact-label,
      { OnlyNames: $onlyNames}
    )
  default return
    let $components as object* :=
      multiplexer:components(
        $profile-name,
        $archive,
        (),
        $concept,
        $disclosure,
        $role,
        $exact-label,
        $full-text-label,
        { MetadataOnly: true })
    return concepts:concepts-for-components(
      $concept,
      $components,
      $exact-label,
      { OnlyNames: $onlyNames})
};

jsoniq version "1.0";

(:
 : Copyright 2012-2014 28msec Inc.
 :)
(:~
 :
This module provides functionality for automatically fetching entities, etc, from the
 : appropriate profile.

 :
 : @author Ghislain Fourny
 :
 :)
module namespace multiplexer = "http://28.io/modules/xbrl/profiles/multiplexer";

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

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

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

(:~
 :
Return latest filings of entities and fiscal periods.

 :
 : @param $profile-name the name of the profile (e.g., SEC, Japan, Generic).
 : @param $entities a sequence of entity objects.
 : @param $fiscal-periods a set of fiscal periods such as "Q1", "Q2", "Q3", "Q4", "FY"
 :
 : @return the latest archives for given entities and fiscal periods.
 :)
declare function multiplexer:latest-filings(
  $profile-name as string,
  $entities as object*,
  $fiscal-periods as string*) as object*
{
  switch($profile-name)
  case "sec" return fiscal-core:latest-filings($entities, $fiscal-periods)
  case "japan" return
    japan:latest-filings($entities, $fiscal-periods)
  default return
    ()
};

(:~
 :
Retrieves entities depending on the profile.

 :
 : @param $profile-name the name of the profile (e.g., SEC, Japan, Generic).
 : @param $eid a sequence of EIDs.
 : @param $cik a sequence of profile-specific CIKs.
 : @param $tag a sequence of tags (e.g., indices, ALL, ...).
 : @param $ticker a sequence of stock exchange tickers.
 : @param $sic a sequence of industry group SIC codes.
 : @param $aid a sequence of archive IDs.
  :
 : @return the entities retrieved according to the profile specified.
 :)
declare function multiplexer:entities(
  $profile-name as string,
  $eid as string*,
  $cik as string*,
  $tag as string*,
  $ticker as string*,
  $sic as string*,
  $aid as string*) as object*
{
  switch($profile-name)
  case "sec" return
    for $entity in companies:companies(
      $cik,
      $tag,
      $ticker,
      $sic,
      $eid,
      $aid)
    order by $entity.Profiles.SEC.CompanyName
    return $entity
  case "japan" return
    for $entity in japan:entities($cik, $ticker, $eid, $tag)
    order by $entity.SubmitterNameAlphabetic
    return $entity
  default return
    entities:entities($eid)
};

(:~
 :
Retrieves archives depending on the profile.

 :
 : @param $profile-name the name of the profile (e.g., SEC, Japan, Generic).
 : @param $entities a sequence of entities or EIDs.
 : @param $fiscalPeriod a sequence of fiscal periods (Q1, Q2, Q3, FY).
 : @param $fiscalYear a sequence of fiscal years.
 : @param $aid a sequence of AIDs.
 :
 : @return the archives retrieved according to the profile specified.
 :)
declare function multiplexer:filings(
  $profile-name as string,
  $entities as item*,
  $fiscalPeriod as string*,
  $fiscalYear as integer*,
  $aid as string*) as object*
{
  switch($profile-name)
  case "sec" return fiscal-core:filings(
      $entities,
      $fiscalPeriod,
      $fiscalYear,
      $aid)
  case "japan" return japan:filings(
      $entities,
      $fiscalYear,
      $fiscalPeriod,
      $aid)
  default return (
      archives:archives($aid),
      archives:archives-for-entities($entities))
};

(:~
  :
Retrieves components depending on the profile.

  :
  : @param $profile-name the name of the profile (e.g., SEC, Japan, Generic).
  : @param $archives a sequence of archive objects.
  : @param $cid a sequence of CIDs.
  : @param $concept a sequence of report element names.
  : @param $disclosure a sequence of disclosure names.
  : @param $role a sequence of network identifiers.
  : @param $label a sequence of labels.
  : searched.
  : @param $options additional options. among which

  :
LabelsOnly: to only get label information.

  :
ExactLabelMatch: to only get exact label matches.

  : </lu>
  :
  : @error multiplexer:ARCHIVE-MISSING if a AID is required but not provided.
  :
  : @return the components retrieved according to the profile specified.
:)
declare function multiplexer:components(
  $profile-name as string,
  $archive as object*,
  $cid as string*,
  $concept as string*,
  $disclosure as string*,
  $role as string*,
  $exact-label as string*,
  $full-text-label as string*,
  $options as object?) as object*
{
  switch($profile-name)
  case "sec" return sec-networks:components(
    $archive,
    $cid,
    $concept,
    $disclosure,
    $role,
    $full-text-label)
  default return
    let $role as string* := if(empty($role))
                            then $components:ALL-ROLES
                            else $role
    let $archive as item* := if(empty($archive))
                             then $components:ALL-ARCHIVES
                             else $archive
    let $concept as item* := if(empty($concept))
                             then $components:ALL-CONCEPTS
                             else $concept
    let $exact-label as item* := if(empty($exact-label))
                            then $components:ALL-LABELS
                            else $exact-label
    return components:components-for(
      $archive,
      $role,
      $concept,
      $exact-label,
      $options
    )
};

(:~
  :
Retrieves components depending on the profile.

  :
  : @param $profile-name the name of the profile (e.g., SEC, Japan, Generic).
  : @param $archives a sequence of archive objects.
  : @param $cid a sequence of CIDs.
  : @param $concept a sequence of report element names.
  : @param $disclosure a sequence of disclosure names.
  : @param $role a sequence of network identifiers.
  : @param $label a sequence of labels.
  :
  : @error multiplexer:ARCHIVE-MISSING if a AID is required but not provided.
  :
  : @return the components retrieved according to the profile specified.
:)
declare function multiplexer:components(
  $profile-name as string,
  $archive as object*,
  $cid as string*,
  $concept as string*,
  $disclosure as string*,
  $role as string*,
  $exact-label as string*,
  $full-text-label as string*) as object*
{
  multiplexer:components(
    $profile-name,
    $archive,
    $cid,
    $concept,
    $disclosure,
    $role,
    $exact-label,
    $full-text-label,
    ())
};

declare function multiplexer:concepts(
  $profile-name as string,
  $archive as object*,
  $concept as string*,
  $disclosure as string*,
  $role as string*,
  $exact-label as string*,
  $full-text-label as string*,
  $report as object?,
  $onlyNames as boolean) as object*
{
  switch(true)
  case exists($report)
    return
    let $map as object? := reports:concept-map($report)
    let $concepts-computable-by-maps as object* :=
        switch(true)
            case not exists($map) return ()
            case not exists($concept) return $map.Trees[]
            default return
                let $keys as string* := $map.Trees[].Name
                for $c as string in $concept[$$ = $keys]
                return ($map.Trees[])[$$.Name eq $c]
    let $mapped-names as string* := $concepts-computable-by-maps.To[].Name
    let $concepts-not-computable-by-maps as string* :=
        seq:value-except($concept, $mapped-names)
    let $results-not-computed-by-maps as object* :=
        if(exists($concepts-not-computable-by-maps))
        then
          multiplexer:concepts(
            $profile-name,
            $archive,
            $concepts-not-computable-by-maps,
            $disclosure,
            $role,
            $exact-label,
            $full-text-label,
            (),
            $onlyNames
          )
        else ()
    let $results-computed-by-maps as object* :=
        let $all-results as object* := multiplexer:concepts(
          $profile-name,
          $archive,
          $mapped-names,
          $disclosure,
          $role,
          $exact-label,
          $full-text-label,
          (),
          $onlyNames
        )
        for $c as object in $concepts-computable-by-maps
        for $result as object in
            for $candidate-concept in $c.To[].Name
            let $facts := $all-results[$$.Name = $candidate-concept]
            where exists($facts)
            count $n
            where $n eq 1
            return $facts
        let $map-concept := (for $candidate in $concepts-computable-by-maps
                            where $result.Name = (keys($candidate.To), $candidate.To[].Name)
                            return $candidate)[1]
        return
            copy $n := $result
            modify (
                replace value of json $n.Name with $map-concept.Name,
                insert json  { Origin : $result.Name } into $n)
            return $n
    return ($results-not-computed-by-maps, $results-computed-by-maps)

  case empty($disclosure) and empty($report) and empty($full-text-label)
    return
    let $role as string* := if(empty($role))
                            then $concepts:ANY_COMPONENT_LINK_ROLE
                            else $role
    let $concept as item* := if(empty($concept))
                             then $concepts:ALL_CONCEPT_NAMES
                             else $concept
    let $exact-label as item* := if(empty($exact-label))
                            then $concepts:ALL_CONCEPT_LABELS
                            else $exact-label
    return concepts:concepts(
      $concept,
      $archive,
      $role,
      $exact-label,
      { OnlyNames: $onlyNames}
    )
  default return
    let $components as object* :=
      multiplexer:components(
        $profile-name,
        $archive,
        (),
        $concept,
        $disclosure,
        $role,
        $exact-label,
        $full-text-label,
        { MetadataOnly: true })
    return concepts:concepts-for-components(
      $concept,
      $components,
      $exact-label,
      { OnlyNames: $onlyNames})
};


Removed some ()

Removed some ()

module {
    name: "schema",
    description: "JSON schema generation and validation",
    namespace: "fadado.github.io",
    author: {
        name: "Joan Josep Ordinas Rosa",
        email: "jordinas@gmail.com"
    }
};

include "fadado.github.io/prelude";
include "fadado.github.io/types";

# Generates a simple document schema
#
# SCHEMA: a JSON schema document
def generate: #:: α| -> SCHEMA
    { "type": type } +
    if isobject then
        if length == 0 then null
        else
            . as $object |
            { "properties": (
                reduce keys_unsorted[] as $name (
                    {};
                    . + {($name): ($object[$name] | generate)}
                )
              )
            }
        end
    elif isarray then
        if length == 0 then null
        else
            { "items": (
                if every(.[] | isscalar)
                   and ([.[] | type] | unique | length) == 1
                then
                    { "type": (.[0] | type) }
                elif length == 1 then
                   .[0] | generate
                else
                    reduce .[] as $item (
                        [];
                        .[length] = ($item | generate)
                    )
                end
              )
            }
        end
    else null end # scalar
;

# Generates a document schema (with options)
#
# OPTIONS: an object of runtime options
# SCHEMA: a JSON schema document
def generate($options): #:: α|(OPTIONS) -> SCHEMA
    { "type": type } +
    if isobject then
        if length == 0 then null
        else
            . as $object |
            { "properties": (
                reduce keys_unsorted[] as $name (
                    {};
                    . + {($name): ($object[$name] | generate($options))}
                )
              )
            } +
            if $options.required then
            {
                "required": [keys]
            }
            else null end +
            if $options.object_verbose then
            {
                "additionalProperties": false,
                "minProperties": length,
                "maxProperties": length
            }
            else null end
        end
    elif isarray then
        if length == 0 then null
        else
            { "items": (
                if every(.[] | isscalar)
                   and ([.[] | type] | unique | length) == 1
                then
                    { "type": (.[0] | type) }
                elif length == 1 then
                   .[0] | generate($options)
                else
                    reduce .[] as $item (
                        [];
                        .[length] = ($item | generate($options))
                    )
                end
              )
            } +
            if $options.array_verbose then
            {
                "additionalItems": false,
                "maxItems": length,
                "minItems": 0,
                "uniqueItems": false
            }
            else null end
        end
    elif isnumber then
        if $options.number_verbose then
        {
            "multipleOf": 1,
            "maximum": 10000,
            "minimum": 1,
            "exclusiveMaximum": false,
            "exclusiveMinimum": false
        }
        else null end
    elif isstring then
        if $options.string_verbose then
        {
            "minLength": 1,
            "maxLength": 1000,
            "pattern": "^.*$"
        }
        else null end
    else null end # boolean and null
;

# Validates a document against an schema
#
# SCHEMA: a JSON schema document
def validate($schema; $fatal): #:: α|(SCHEMA;boolean) -> boolean
    def pointer($s):
        if $s | startswith("#") | not
        then error("Only supported pointers in current document")
        elif $s == "#"
        then $schema # root
        elif $s | startswith("#/") | not
        then error("Only supported absolute pointers")
        else
            $schema
            | getpath($s[2:] / "/")
            | when(isnull; error("Cannot dereference pointer"))
        end
    ;
    def _validate($schema; $fatal):
        #
        # Schema keywords
        #
        def k_enum: # keyword enum
            rule($schema | has("enum");
                . as $instance
                | isscalar and ($schema.enum | indices([$instance]) | length) > 0)
        ;
        def k_type: # keyword type
            rule($schema | has("type");
                type as $t
                | if isstring($schema["type"]) # string or array
                  then $t == $schema["type"]
                       or ($t == "number" and $schema["type"] == "integer" and isinteger)
                  else some(($schema["type"][] | type) == $t) end)
        ;
        def k_allOf: # keyword allOf
            rule($schema | has("allOf");
                every(_validate($schema.allOf[]; $fatal)))
        ;
        def k_anyOf: # keyword anyOf
            rule($schema | has("anyOf");
                some(_validate($schema.anyOf[]; false)))
        ;
        def k_oneOf: # keyword oneOf
            rule($schema | has("oneOf");
                [_validate($schema.oneOf[]; false)] == [true])
        ;
        def k_dependencies: # keyword dependencies
            rule(schema | has("dependencies");
                . as $instance
                | every(
                    $schema.dependencies
                    | keys_unsorted[]
                    | rule(in($instance);
                        $schema.dependencies[.] as $d
                        | if isarray($d)
                          then every($d[] | in($instance))
                          else $instance | _validate($d; $fatal) end)))
        ;
        #
        # Type constraints
        #
        def c_number: # number constraints
            rule($schema | has("multipleOf");
                . / $schema.multipleOf
                | . == floor)
            and rule($schema | has("maximum");
                if $schema.exclusiveMaximum
                then . < $schema.maximum
                else . <= $schema.maximum end)
            and rule($schema | has("minimum");
                if $schema.exclusiveMinimum
                then . >$schema.minimum
                else . >= $schema.minimum end)
        ;
        def c_string: # string constraints
            rule($schema | has("maxLength");
                length <= $schema.maxLength)
            and rule($schema | has("minLength");
                length >= $schema.minLength)
            and rule($schema | has("pattern");
                test($schema.pattern))
            and rule($schema | has("format");
                {   "date-time": "[0-9]",   # one digit at least
                    "email": "^.+@.+$",     # one @ inside text
                    "hostname": "[A-Za-z]", # one letter at least
                    "ipv4": "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$", # 4 digits triples
                    "ipv6": "[0-9]",        # one digit at least
                    "uri": "^[a-zA-Z]+:",   # required schema prefix
                    "uriref": "^."          # non empty string
                }[$schema.format] // "^." as $re # any non empty string as default
                | test($re))
        ;
        def c_array: # array constraints
            def valid_array:
                ($schema | has("items") | not)
                or ($schema.items | isobject)
                or ($schema | has("additionalItems") | not)
                or ($schema.additionalItems == true)
                or ($schema.additionalItems | isobject)
                or ($schema.additionalItems == false and ($schema.items | isarray))
                and length <= ($schema.items | length)
            ;
            def valid_elements:
                def additionalItems:
                    if ($schema | has("additionalItems") | not)
                        or $schema.additionalItems == true
                    then {}
                    elif $schema.additionalItems | isobject
                    then $schema.additionalItems
                    else null end
                ;
                if ($schema.items | isobject) then
                    every(.[] | _validate($schema.items; $fatal))
                else # isarray
                    additionalItems as $additional
                    | every(
                        range(length) as $i
                        | if $i | in($schema.items) # $i < ($schema.items|length)
                          then .[$i] | _validate($schema.items[$i]; $fatal)
                          else $additional != null
                               and (.[$i] | _validate($additional; $fatal)) end
                      )
                end
            ;
            valid_array
            and rule($schema | has("maxItems");
                length <= $schema.maxItems)
            and rule($schema | has("minItems");
                length >= $schema.minItems)
            and rule($schema | has("uniqueItems");
                ($schema.uniqueItems | not) or length == (unique | length))
            and valid_elements
        ;
        def c_object: # object constraints
            def valid_object:
                ($schema | has("additionalProperties") | not)
                or $schema.additionalProperties == true
                or ($schema.additionalProperties | isobject)
                or $schema.additionalProperties == false
                and $schema.properties // {} as $p
                    | $schema.patternProperties // {} as $pp
                    | [ keys_unsorted[]
                        | select(in($p) | not)
                        | select(every(test($pp | keys_unsorted[]) | not)) ]
                    | length == 0
            ;
            def valid_members:
                def additionalProperties:
                    if ($schema | has("additionalProperties") | not)
                        or $schema.additionalProperties == true
                    then {}
                    elif $schema.additionalProperties | isobject
                    then $schema.additionalProperties
                    else null end
                ;
                additionalProperties as $additional
                | $schema.properties // {} as $p
                | $schema.patternProperties // {} as $pp
                | every(
                    keys_unsorted[] as $m
                    | [ keep($m | in($p); $p[$m]),
                        (($pp | keys_unsorted[]) as $re
                         | keep($m | test($re); $pp[$re])) ] as $s
                    | if ($s | length) > 0
                      then .[$m] | every(_validate($s[]; $fatal))
                      else $additional != null
                           and (.[$m] | _validate($additional; $fatal)) end
                  )
            ;
            valid_object
            and rule($schema | has("maxProperties");
                length <= $schema.maxProperties)
            and rule($schema | has("minProperties");
                length >= $schema.minProperties)
            and rule($schema | has("required");
                every(has($schema.required[])))
            and valid_members
        ;
        #
        # Validate
        #
        def check(constraint):
            constraint
            or $fatal and ({ instance: ., schema: $schema } | error)
        ;
        $schema == null
        or $schema == {}
        or if isobject and has("$ref") # when validating schemas!
        then .["$ref"] | isstring
        elif $schema | has("$ref")
        then _validate(pointer($schema["$ref"]); $fatal)
        elif $schema | has("not")
        then
            _validate($schema | del(.not); $fatal)
            and (_validate($schema.not; $fatal) | not)
        else
            check(k_type)
            and check(k_enum)
            and check(k_allOf)
            and check(k_anyOf)
            and check(k_oneOf)
            and check(k_dependencies)
            and if isnumber then check(c_number)
            elif isstring   then check(c_string)
            elif isarray    then check(c_array)
            elif isobject   then check(c_object)
            else isnull or isboolean end
        end
    ;
    #
    _validate($schema; $fatal)
;

# Validates a document against an schema
#
# SCHEMA: a JSON schema document
def validate($schema): #:: α|(SCHEMA) -> boolean
    validate($schema; true)
;

# Validates a document without signaling errors
#
# SCHEMA: a JSON schema document
def valid($schema): #:: α|(SCHEMA) -> boolean
    validate($schema; false)
;

# vim:ai:sw=4:ts=4:et:syntax=jq

module {
    name: "schema",
    description: "JSON schema generation and validation",
    namespace: "fadado.github.io",
    author: {
        name: "Joan Josep Ordinas Rosa",
        email: "jordinas@gmail.com"
    }
};

include "fadado.github.io/prelude";
include "fadado.github.io/types";

# Generates a simple document schema
#
# SCHEMA: a JSON schema document
def generate: #:: α| -> SCHEMA
    { "type": type } +
    if isobject then
        if length == 0 then null
        else
            . as $object |
            { "properties": (
                reduce keys_unsorted[] as $name (
                    {};
                    . + {($name): ($object[$name] | generate)}
                )
              )
            }
        end
    elif isarray then
        if length == 0 then null
        else
            { "items": (
                if every(.[] | isscalar)
                   and ([.[] | type] | unique | length) == 1
                then
                    { "type": (.[0] | type) }
                elif length == 1 then
                   .[0] | generate
                else
                    reduce .[] as $item (
                        [];
                        .[length] = ($item | generate)
                    )
                end
              )
            }
        end
    else null end # scalar
;

# Generates a document schema (with options)
#
# OPTIONS: an object of runtime options
# SCHEMA: a JSON schema document
def generate($options): #:: α|(OPTIONS) -> SCHEMA
    { "type": type } +
    if isobject then
        if length == 0 then null
        else
            . as $object |
            { "properties": (
                reduce keys_unsorted[] as $name (
                    {};
                    . + {($name): ($object[$name] | generate($options))}
                )
              )
            } +
            if $options.required then
            {
                "required": [keys]
            }
            else null end +
            if $options.object_verbose then
            {
                "additionalProperties": false,
                "minProperties": length,
                "maxProperties": length
            }
            else null end
        end
    elif isarray then
        if length == 0 then null
        else
            { "items": (
                if every(.[] | isscalar)
                   and ([.[] | type] | unique | length) == 1
                then
                    { "type": (.[0] | type) }
                elif length == 1 then
                   .[0] | generate($options)
                else
                    reduce .[] as $item (
                        [];
                        .[length] = ($item | generate($options))
                    )
                end
              )
            } +
            if $options.array_verbose then
            {
                "additionalItems": false,
                "maxItems": length,
                "minItems": 0,
                "uniqueItems": false
            }
            else null end
        end
    elif isnumber then
        if $options.number_verbose then
        {
            "multipleOf": 1,
            "maximum": 10000,
            "minimum": 1,
            "exclusiveMaximum": false,
            "exclusiveMinimum": false
        }
        else null end
    elif isstring then
        if $options.string_verbose then
        {
            "minLength": 1,
            "maxLength": 1000,
            "pattern": "^.*$"
        }
        else null end
    else null end # boolean and null
;

# Validates a document against an schema
#
# SCHEMA: a JSON schema document
def validate($schema; $fatal): #:: α|(SCHEMA;boolean) -> boolean
    def pointer($s):
        if $s | startswith("#") | not
        then error("Only supported pointers in current document")
        elif $s == "#"
        then $schema # root
        elif $s | startswith("#/") | not
        then error("Only supported absolute pointers")
        else
            $schema
            | getpath($s[2:] / "/")
            | when(isnull; error("Cannot dereference pointer"))
        end
    ;
    def _validate($schema; $fatal):
        #
        # Schema keywords
        #
        def k_enum: # keyword enum
            rule($schema | has("enum");
                . as $instance
                | isscalar and ($schema.enum | indices([$instance]) | length) > 0)
        ;
        def k_type: # keyword type
            rule($schema | has("type");
                type as $t
                | if isstring($schema["type"]) # string or array
                  then $t == $schema["type"]
                       or ($t == "number" and $schema["type"] == "integer" and isinteger)
                  else some(($schema["type"][] | type) == $t) end)
        ;
        def k_allOf: # keyword allOf
            rule($schema | has("allOf");
                every(_validate($schema.allOf[]; $fatal)))
        ;
        def k_anyOf: # keyword anyOf
            rule($schema | has("anyOf");
                some(_validate($schema.anyOf[]; false)))
        ;
        def k_oneOf: # keyword oneOf
            rule($schema | has("oneOf");
                [_validate($schema.oneOf[]; false)] == [true])
        ;
        def k_dependencies: # keyword dependencies
            rule(schema | has("dependencies");
                . as $instance
                | every(
                    $schema.dependencies
                    | keys_unsorted[]
                    | rule(in($instance);
                        $schema.dependencies[.] as $d
                        | if isarray($d)
                          then every($d[] | in($instance))
                          else $instance | _validate($d; $fatal) end)))
        ;
        #
        # Type constraints
        #
        def c_number: # number constraints
            rule($schema | has("multipleOf");
                (. / $schema.multipleOf) | . == floor)
            and rule($schema | has("maximum");
                if $schema.exclusiveMaximum
                then . < $schema.maximum
                else . <= $schema.maximum end)
            and rule($schema | has("minimum");
                if $schema.exclusiveMinimum
                then . >$schema.minimum
                else . >= $schema.minimum end)
        ;
        def c_string: # string constraints
            rule($schema | has("maxLength");
                length <= $schema.maxLength)
            and rule($schema | has("minLength");
                length >= $schema.minLength)
            and rule($schema | has("pattern");
                test($schema.pattern))
            and rule($schema | has("format");
                {   "date-time": "[0-9]",   # one digit at least
                    "email": "^.+@.+$",     # one @ inside text
                    "hostname": "[A-Za-z]", # one letter at least
                    "ipv4": "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$", # 4 digits triples
                    "ipv6": "[0-9]",        # one digit at least
                    "uri": "^[a-zA-Z]+:",   # required schema prefix
                    "uriref": "^."          # non empty string
                }[$schema.format]//"^." as $re # any non empty string as default
                | test($re))
        ;
        def c_array: # array constraints
            def valid_array:
                ($schema | has("items") | not)
                or ($schema.items | isobject)
                or ($schema | has("additionalItems") | not)
                or ($schema.additionalItems == true)
                or ($schema.additionalItems | isobject)
                or ($schema.additionalItems == false and ($schema.items | isarray))
                and length <= ($schema.items | length)
            ;
            def valid_elements:
                def additionalItems:
                    if ($schema | has("additionalItems") | not)
                        or ($schema.additionalItems == true)
                    then {}
                    elif $schema.additionalItems | isobject
                    then $schema.additionalItems
                    else null end
                ;
                if ($schema.items | isobject) then
                    every(.[] | _validate($schema.items; $fatal))
                else # isarray
                    additionalItems as $additional
                    | every(
                        range(length) as $i
                        | if $i | in($schema.items) # $i < ($schema.items|length)
                          then .[$i] | _validate($schema.items[$i]; $fatal)
                          else $additional != null
                               and (.[$i] | _validate($additional; $fatal)) end
                      )
                end
            ;
            valid_array
            and rule($schema | has("maxItems");
                length <= $schema.maxItems)
            and rule($schema | has("minItems");
                length >= $schema.minItems)
            and rule($schema | has("uniqueItems");
                ($schema.uniqueItems | not) or length == (unique | length))
            and valid_elements
        ;
        def c_object: # object constraints
            def valid_object:
                ($schema | has("additionalProperties") | not)
                or ($schema.additionalProperties == true)
                or ($schema.additionalProperties | isobject)
                or $schema.additionalProperties == false
                and ($schema.properties//{}) as $p
                    | ($schema.patternProperties//{}) as $pp
                    | [ keys_unsorted[]
                        | select(in($p) | not)
                        | select(every(test($pp | keys_unsorted[]) | not)) ]
                    | length == 0
            ;
            def valid_members:
                def additionalProperties:
                    if ($schema | has("additionalProperties") | not)
                        or ($schema.additionalProperties == true)
                    then {}
                    elif $schema.additionalProperties | isobject
                    then $schema.additionalProperties
                    else null end
                ;
                additionalProperties as $additional
                | ($schema.properties//{}) as $p
                | ($schema.patternProperties//{}) as $pp
                | every(
                    keys_unsorted[] as $m
                    | [ keep($m | in($p); $p[$m]),
                        (($pp | keys_unsorted[]) as $re
                         | keep($m | test($re); $pp[$re])) ] as $s
                    | if ($s | length) > 0
                      then .[$m] | every(_validate($s[]; $fatal))
                      else $additional != null
                           and (.[$m] | _validate($additional; $fatal)) end
                  )
            ;
            valid_object
            and rule($schema | has("maxProperties");
                length <= $schema.maxProperties)
            and rule($schema | has("minProperties");
                length >= $schema.minProperties)
            and rule($schema | has("required");
                every(has($schema.required[])))
            and valid_members
        ;
        #
        # Validate
        #
        def check(constraint):
            constraint
            or $fatal and ({ instance: ., schema: $schema } | error)
        ;
        $schema == null
        or $schema == {}
        or if isobject and has("$ref") # when validating schemas!
        then .["$ref"] | isstring
        elif $schema | has("$ref")
        then _validate(pointer($schema["$ref"]); $fatal)
        elif $schema | has("not")
        then
            _validate($schema | del(.not); $fatal)
            and (_validate($schema.not; $fatal) | not)
        else
            check(k_type)
            and check(k_enum)
            and check(k_allOf)
            and check(k_anyOf)
            and check(k_oneOf)
            and check(k_dependencies)
            and if isnumber then check(c_number)
            elif isstring   then check(c_string)
            elif isarray    then check(c_array)
            elif isobject   then check(c_object)
            else isnull or isboolean end
        end
    ;
    #
    _validate($schema; $fatal)
;

# Validates a document against an schema
#
# SCHEMA: a JSON schema document
def validate($schema): #:: α|(SCHEMA) -> boolean
    validate($schema; true)
;

# Validates a document without signaling errors
#
# SCHEMA: a JSON schema document
def valid($schema): #:: α|(SCHEMA) -> boolean
    validate($schema; false)
;

# vim:ai:sw=4:ts=4:et:syntax=jq