Source code for juggling.notation.siteswap

import re

from .base import JugglingNotation
from ..pattern import Pattern


__all__ = ['Siteswap', 'siteswap_char_to_int', 'siteswap_int_to_char', 'is_valid_siteswap_syntax',
           'convert_str_to_beat_list', 'convert_char_to_beat']


def siteswap_char_to_int(char):
    # type: str -> int
    """ Converts a siteswap character to a digit """
    if not isinstance(char, str):
        raise ValueError("Invalid SiteSwap character: '{}'".format(char))
    if len(char) != 1:
        raise ValueError('Invalid SiteSwap character, too many characters')
    try:
        return int(char.lower(), 36)
    except ValueError:
        raise ValueError("Invalid SiteSwap character: '{}'".format(char))


def siteswap_int_to_char(digit):
    if not isinstance(digit, int):
        raise ValueError("Digit must be between an integer 0 and 36 [0:Z]")
    if digit < 0 or digit > 35:
        raise ValueError("Digit must be between an integer 0 and 36 [0:Z]")
    if digit < 10:
        return str(digit)
    return chr(ord("a") + (digit-10))


TOSS_RE = r'([0-9a-z]x?)'
PASS_TOSS_RE = r'([0-9a-z]x?(p{num_jugglers})?)'
MULTIPLEX_RE = r'\[{}+\]'.format(TOSS_RE)
PASS_MULTIPLEX_RE = r'\[{}+\]'.format(PASS_TOSS_RE)
SYNC_BASE_RE = r'(\(({toss}|{multi}),({toss}|{multi})\))\*?'
SYNC_RE = SYNC_BASE_RE.format(toss=TOSS_RE, multi=MULTIPLEX_RE)
PASS_SYNC_RE = SYNC_BASE_RE.format(toss=PASS_TOSS_RE, multi=MULTIPLEX_RE)
BEAT_RE = r'({}|{}|{})'.format(TOSS_RE, MULTIPLEX_RE, SYNC_RE)
PASS_BEAT_RE = r'({}|{}|{})'.format(PASS_TOSS_RE, PASS_MULTIPLEX_RE, PASS_SYNC_RE)
SOLO_SITESWAP_RE = re.compile(r'^{}+$'.format(BEAT_RE), re.IGNORECASE | re.VERBOSE)
PASS_RE = r'<({beat})(\|{beat})+>'.format(beat=PASS_BEAT_RE)


def is_valid_siteswap_syntax(pattern, num_jugglers=1, return_match=False):
    # type: (str, int) -> bool or (bool, match)
    """
    Checks whether the given pattern is in valid siteswap syntax.

    >>> is_valid_siteswap_syntax('441') == True
    >>> is_valid_siteswap_syntax('$$92') == False
    >>> matched, match_object = is_valid_siteswap_syntax('(6x,4)*', return_match=True)

    Note: This ignores ALL whitespace in the given `pattern`

    :param pattern: A string of the siteswap to check
    :param num_jugglers:  Number of jugglers involved in the siteswap. This determines whether
        to allow passing notation and also whether the passing notation needs to include the
        juggler being passed to.
    :param return_match: Affects whether or not the match object is returned as well. If True,
        this function will return a tuple, (bool, match). The bool is whether or not the syntax is valid. If
        the syntax is valid, a regex match object is returned. Otherwise None is returned
    :return: A bool whether or not the syntax is valid.
    """
    pattern = str(pattern)
    pattern = re.sub('\s', '', pattern)  # ignore all whitespace by stripping it out

    if num_jugglers < 1:
        raise ValueError("Invalid number of jugglers: {}".format(num_jugglers))
    elif num_jugglers > 1:
        if num_jugglers > 2:
            pass_re = "^({})+$".format(PASS_RE.format(
                num_jugglers="[1-{}]".format(num_jugglers)))
        else:
            pass_re = "^({})+$".format(PASS_RE.format(num_jugglers=""))
        m = re.match(pass_re, pattern, re.IGNORECASE | re.VERBOSE)
    else:
        m = SOLO_SITESWAP_RE.match(pattern)

    if return_match:
        return m is not None, m
    else:
        return m is not None


def convert_char_to_beat(beat_str, is_sync=False):
    # type: (str) -> (int or list)
    """ Converts a single siteswap beat into a :class:`Pattern` beat """
    # print('beat', beat_str)
    if beat_str.startswith('['):
        if is_sync:
            return convert_str_to_beat_list(beat_str[1:-1])
        return [siteswap_char_to_int(_) for _ in beat_str[1:-1]]
    elif beat_str.startswith('('):
        return tuple(convert_str_to_beat_list(_, True)[0] for _ in beat_str[1:-1].split(','))
    else:
        return siteswap_char_to_int(beat_str)


def convert_str_to_beat_list(siteswap, is_sync=False):
    # type: (str) -> list
    """ Converts a siteswap string to a :class:`Pattern` beat list
    :param is_sync: Used internally for recusively parsing SSS
    """
    # print('convert', siteswap, is_sync)
    siteswap = str(siteswap)
    siteswap = re.sub('\s', '', siteswap)  # ignore all whitespace by stripping it out
    raw_beats = list(_.group() for _ in re.finditer(BEAT_RE, siteswap, re.IGNORECASE))
    beats = []
    for beat in raw_beats:
        if beat.lower().endswith(')*'):
            s = beat[1:-2].split(',')
            beats.append(convert_char_to_beat(beat[:-1], is_sync))
            beats.append(convert_char_to_beat('({},{})'.format(s[1], s[0]), is_sync))
        elif len(beat) == 2 and beat[1].lower() == 'x':
            beats.append(convert_char_to_beat(beat[0], is_sync) + 0.5)
        else:
            beats.append(convert_char_to_beat(beat, is_sync))
    return beats


[docs]class Siteswap(JugglingNotation): """ Siteswap notation """ def __init__(self, notation_pattern, raise_invalid=False): notation_pattern = re.sub('\s', '', notation_pattern) # removes all whitespace characters super(Siteswap, self).__init__(notation_pattern=notation_pattern, raise_invalid=raise_invalid) if self.is_valid_syntax: self.pattern = Pattern(convert_str_to_beat_list(self.notation_pattern)) @property def is_valid_syntax(self): # return cached value if we have it return is_valid_siteswap_syntax(self.notation_pattern)