Combine two or more numpy ndarray with structured dtypes on common key
    column(s).

    Merge a list (or dictionary) of numpy arrays, given by `L`, on key 
    columns listed in `keycols`.

    The ``strictjoin`` assumes the following restrictions:

    *   each element of `keycol` must be a valid column name in `X` and each 
        array in `L`, and all of the same data-type.

    *   for each column `col`  in `keycols`, and each array `A` in `L`, the 
        values in `A[col]` must be unique, e.g. no repeats of values -- and 
        same for `X[col]`.  (Actually, the uniqueness criterion need not hold
        to the first tabarray in L, but first for all the subsequent ones.)

    *   the *non*-key-column column names in each of the arrays must be 
        disjoint from each other -- or disjoint after a renaming (see below).

    An error will be thrown if these conditions are not met.

    For a wrapper that attempts to meet these restrictions, see 
    :func:`tabular.spreadsheet.join`.

    If you don't provide a value of `keycols`, the algorithm will attempt to 
    infer which columns should be used by trying to find the largest set of 
    common column names that contain unique values in each array and have the 
    same data type.  An error will be thrown if no such inference can be made.

    *Renaming of overlapping columns*

            If the non-keycol column names of the arrays overlap, ``join`` will 
            by default attempt to rename the columns by using a simple 
            convention:

            *   If `L` is a list, it will append the number in the list to the 
                key associated with the array.

            *   If `L` is a dictionary, the algorithm will append the string 
                representation of the key associated with an array to the 
                overlapping columns from that array.

            You can override the default renaming scheme using the `renamer` 
            parameter.

    *Nullvalues for keycolumn differences*

            If there are regions of the keycolumns that are not overlapping 
            between merged arrays, `join` will fill in the relevant entries 
            with null values chosen by default:

            *   '0' for integer columns

            *   '0.0' for float columns

            *   the empty character ('') for string columns.

    **Parameters**

            **L** :  list or dictionary

                    Numpy recarrays to merge.  If `L` is a dictionary, the keys
                    name each numpy recarray, and the corresponding values are 
                    the actual numpy recarrays.

            **keycols** :  list of strings

                    List of the names of the key columns along which to do the 
                    merging.

            **nullvals** :  function, optional

                    A function that returns a null value for a numpy format
                    descriptor string, e.g. ``'<i4'`` or ``'|S5'``.

                    See the default function for further documentation:

                            :func:`tabular.spreadsheet.DEFAULT_NULLVALUEFORMAT`

            **renaming** :  dictionary of dictionaries, optional

                    Dictionary mapping each input numpy recarray to a 
                    dictionary mapping each original column name to its new 
                    name following the convention above.

                    For example, the result returned by:

                            :func:`tabular.spreadsheet.DEFAULT_RENAMER`

    **Returns**

            **result** :  numpy ndarray with structured dtype

                    Result of the join, e.g. the result of merging the input
                    numpy arrays defined in `L` on the key columns listed in 
                    `keycols`.

    **See Also:**

            :func:`tabular.spreadsheet.join`

def strictjoin(L, keycols, nullvals=None, renaming=None, Names=None):
    """
    Combine two or more numpy ndarray with structured dtypes on common key
    column(s).

    Merge a list (or dictionary) of numpy arrays, given by `L`, on key 
    columns listed in `keycols`.

    The ``strictjoin`` assumes the following restrictions:

    *   each element of `keycol` must be a valid column name in `X` and each 
        array in `L`, and all of the same data-type.

    *   for each column `col`  in `keycols`, and each array `A` in `L`, the 
        values in `A[col]` must be unique, e.g. no repeats of values -- and 
        same for `X[col]`.  (Actually, the uniqueness criterion need not hold
        to the first tabarray in L, but first for all the subsequent ones.)

    *   the *non*-key-column column names in each of the arrays must be 
        disjoint from each other -- or disjoint after a renaming (see below).

    An error will be thrown if these conditions are not met.

    For a wrapper that attempts to meet these restrictions, see 
    :func:`tabular.spreadsheet.join`.

    If you don't provide a value of `keycols`, the algorithm will attempt to 
    infer which columns should be used by trying to find the largest set of 
    common column names that contain unique values in each array and have the 
    same data type.  An error will be thrown if no such inference can be made.

    *Renaming of overlapping columns*

            If the non-keycol column names of the arrays overlap, ``join`` will 
            by default attempt to rename the columns by using a simple 
            convention:

            *   If `L` is a list, it will append the number in the list to the 
                key associated with the array.

            *   If `L` is a dictionary, the algorithm will append the string 
                representation of the key associated with an array to the 
                overlapping columns from that array.

            You can override the default renaming scheme using the `renamer` 
            parameter.

    *Nullvalues for keycolumn differences*

            If there are regions of the keycolumns that are not overlapping 
            between merged arrays, `join` will fill in the relevant entries 
            with null values chosen by default:

            *   '0' for integer columns

            *   '0.0' for float columns

            *   the empty character ('') for string columns.

    **Parameters**

            **L** :  list or dictionary

                    Numpy recarrays to merge.  If `L` is a dictionary, the keys
                    name each numpy recarray, and the corresponding values are 
                    the actual numpy recarrays.

            **keycols** :  list of strings

                    List of the names of the key columns along which to do the 
                    merging.

            **nullvals** :  function, optional

                    A function that returns a null value for a numpy format
                    descriptor string, e.g. ``'<i4'`` or ``'|S5'``.

                    See the default function for further documentation:

                            :func:`tabular.spreadsheet.DEFAULT_NULLVALUEFORMAT`

            **renaming** :  dictionary of dictionaries, optional

                    Dictionary mapping each input numpy recarray to a 
                    dictionary mapping each original column name to its new 
                    name following the convention above.

                    For example, the result returned by:

                            :func:`tabular.spreadsheet.DEFAULT_RENAMER`

    **Returns**

            **result** :  numpy ndarray with structured dtype

                    Result of the join, e.g. the result of merging the input
                    numpy arrays defined in `L` on the key columns listed in 
                    `keycols`.

    **See Also:**

            :func:`tabular.spreadsheet.join`
    """

    if isinstance(L,dict):
        Names = L.keys()
        LL = L.values()
    else:
        if Names == None:
            Names = range(len(L))
        else:
            assert len(Names) == len(L)
        LL = L

    if isinstance(keycols,str):
        keycols = [l.strip() for l in keycols.split(',')]

    assert all([set(keycols) <= set(l.dtype.names) for l in LL]), \
           ('keycols,', str(keycols), 
            ', must be valid column names in all arrays being merged.')
    assert all([isunique(l[keycols]) for l in LL[1:]]), \
           ('values in keycol columns,', str(keycols), 
            ', must be unique in all arrays being merged.')

    if renaming == None:
        renaming = {}
    assert RenamingIsInCorrectFormat(renaming, L, Names=Names), \
           'renaming is not in proper format ... '

    L = dict([(k,ll.copy()) for (k,ll) in zip(Names,LL)])
    LL = L.values()

    for i in Names:
        l = L[i]
        l.dtype = np.dtype(l.dtype.descr)
        if i in renaming.keys():
            for k in renaming[i].keys():
                if k not in keycols:
                    renamecol(L[i], k, renaming[i][k])
        l.sort(order = keycols)


    commons = set(Commons([l.dtype.names for l in LL])).difference(keycols)
    assert len(commons) == 0, ('The following (non-keycol) column names ' 
                    'appear in more than on array being merged:', str(commons))

    Result = colstack([(L[Names[0]][keycols])[0:0]] + 
                      [deletecols(L[k][0:0], keycols) \
                      for k in Names if deletecols(L[k][0:0], keycols) != None])

    PL = powerlist(Names)
    ToGet = utils.listunion([[p for p in PL if len(p) == k] 
                             for k in range(1, len(Names))]) + [PL[-1]]

    for I in ToGet[::-1]:
        Ref = L[I[0]][keycols]

        for j in I[1:]:
            if len(Ref) > 0:
                Ref = Ref[fast.recarrayisin(Ref, L[j][keycols], weak=True)]
            else:
                break

        if len(Ref) > 0:
            D = [fast.recarrayisin(L[j][keycols], Ref, weak=True) for j in I]
            
            Ref0 = L[I[0]][keycols][D[0]]
            Reps0 = np.append(np.append([-1],
                (Ref0[1:] != Ref0[:-1]).nonzero()[0]),[len(Ref0)-1])
            Reps0 = Reps0[1:] - Reps0[:-1]
                        
            NewRows = colstack([Ref0] + 
                  [deletecols(L[j][D[i]], keycols).repeat(Reps0) if i > 0 else 
                   deletecols(L[j][D[i]], keycols) for (i, j) in enumerate(I)  
                   if deletecols(L[j][D[i]], keycols) != None])
            for (i,j) in enumerate(I):
                L[j] = L[j][np.invert(D[i])]
            Result = rowstack([Result, NewRows], mode='nulls', 
                              nullvals=nullvals)

    return Result