Database-like join this DataFrame with the other DataFrame.

        Currently assumes the `on` columns are sorted and the on-column(s) values are unique!
        Next work handles the other cases.

        Note there's no automatic cast if the type of the on columns differs.

        Check DataFrame.merge() for more details.

        Parameters
        ----------
        other : DataFrame
            With which to merge.
        on : str or list or None, optional
            The columns from both DataFrames on which to join.
            If None, will join on the index if it has the same name.
        how : {'inner', 'left', 'right', 'outer'}, optional
            Which kind of join to do.
        lsuffix : str, optional
            Suffix to use on columns that overlap from self.
        rsuffix : str, optional
            Suffix to use on columns that overlap from other.
        algorithm : {'merge', 'hash'}, optional
            Which algorithm to use. Note that for 'hash', the `other` DataFrame is the one hashed.
        is_on_sorted : bool, optional
            If we know that the on columns are already sorted, can employ faster algorithm.
        is_on_unique : bool, optional
            If we know that the values are unique, can employ faster algorithm.

        Returns
        -------
        DataFrame
            DataFrame containing the merge result, with the `on` columns as index.

def join(self, other, on=None, how='left', lsuffix=None, rsuffix=None,
             algorithm='merge', is_on_sorted=True, is_on_unique=True):
        """Database-like join this DataFrame with the other DataFrame.

        Currently assumes the `on` columns are sorted and the on-column(s) values are unique!
        Next work handles the other cases.

        Note there's no automatic cast if the type of the on columns differs.

        Check DataFrame.merge() for more details.

        Parameters
        ----------
        other : DataFrame
            With which to merge.
        on : str or list or None, optional
            The columns from both DataFrames on which to join.
            If None, will join on the index if it has the same name.
        how : {'inner', 'left', 'right', 'outer'}, optional
            Which kind of join to do.
        lsuffix : str, optional
            Suffix to use on columns that overlap from self.
        rsuffix : str, optional
            Suffix to use on columns that overlap from other.
        algorithm : {'merge', 'hash'}, optional
            Which algorithm to use. Note that for 'hash', the `other` DataFrame is the one hashed.
        is_on_sorted : bool, optional
            If we know that the on columns are already sorted, can employ faster algorithm.
        is_on_unique : bool, optional
            If we know that the values are unique, can employ faster algorithm.

        Returns
        -------
        DataFrame
            DataFrame containing the merge result, with the `on` columns as index.

        """
        check_type(lsuffix, str)
        check_type(rsuffix, str)

        self_names = self._gather_column_names()
        other_names = other._gather_column_names()
        common_names = set(self_names).intersection(set(other_names))
        if len(common_names) > 0 and lsuffix is None and rsuffix is None:
            raise ValueError('Columns overlap but no suffixes supplied')

        # need to ensure that some str suffixes are passed to merge
        lsuffix = '' if lsuffix is None else lsuffix
        rsuffix = '' if rsuffix is None else rsuffix

        # TODO: pandas is more flexible, e.g. allows the index names to be different when joining on index
        # TODO i.e. df(ind + a, b) join df(ind2 + b, c) does work and the index is now called ind

        return self.merge(other, how, on, (lsuffix, rsuffix), algorithm, is_on_sorted, is_on_unique)