def stedman(stage: int, start_row: Optional[str] = None) -> RowGenerator: """ Generates Stedman on a given stage (even bell Stedman will cause an exception). """ assert stage % 2 == 1 if stage == 5: return PlaceNotationGenerator.stedman_doubles(start_row) stage_bell = convert_to_bell_string(stage) stage_bell_1 = convert_to_bell_string(stage - 1) stage_bell_2 = convert_to_bell_string(stage - 2) notation = f"3.1.{stage_bell}.3.1.3.1.3.{stage_bell}.1.3.1" return PlaceNotationGenerator( stage, notation, bob=CallDef({ 3: stage_bell_2, 9: stage_bell_2 }), single=CallDef({ 3: f"{stage_bell_2}{stage_bell_1}{stage_bell}", 9: f"{stage_bell_2}{stage_bell_1}{stage_bell}" }), start_row=start_row)
def stedman_doubles() -> RowGenerator: """ Generates Stedman on a given stage (even bell Stedman will cause an exception). """ notation = "3.1.5.3.1.3.1.3.5.1.3.1" return PlaceNotationGenerator(5, notation, bob=CallDef({}), single=CallDef({ 6: "345", 12: "145" }))
def grandsire(stage: int) -> RowGenerator: """ Generates Grandsire on a given stage. """ stage_bell = convert_to_bell_string(stage) cross_notation = stage_bell if stage % 2 else '-' main_body = [ "1" if i % 2 else cross_notation for i in range(2 * stage) ] main_body[0] = "3" notation = ".".join(main_body) return PlaceNotationGenerator(stage, notation, bob=CallDef({-1: "3"}), single=CallDef({-1: "3.123"}))
def test_call_parsing(self): test_cases = [ ("14", { 0: "14" }), ("3.123", { 0: "3.123" }), (" 0 \t: \n 16 ", { 0: "16" }), ("20: 70", { 20: "70" }), ("20: 70/ 14", { 20: "70", 0: "14" }), ] for (input_arg, expected_call_dict) in test_cases: with self.subTest(input=input_arg, expected_call_dict=expected_call_dict): self.assertEqual(CallDef(expected_call_dict), parse_call(input_arg))
def parse_call(input_string: str) -> CallDef: """ Parse a call string into a dict of lead locations to place notation strings. """ def exit_with_message(message: str) -> NoReturn: """ Raises a parse error with the given error message. """ raise CallParseError(input_string, message) # A dictionary that will be filled with the parsed calls parsed_calls: Dict[int, str] = {} for segment in input_string.split("/"): # Default the location to 0 and initialise place_notation_str with None location = 0 place_notation_str = None if ":" in segment: # Split the segment into location and place notations try: location_str, place_notation_str = segment.split(":") except ValueError: exit_with_message( f"Call specification '{segment.strip()}' should contain at most one ':'." ) # Strip whitespace from the string segments so that they can be parsed more easily assert place_notation_str is not None location_str = location_str.strip() place_notation_str = place_notation_str.strip() # Parse the first section as an integer try: location = int(location_str) except ValueError: exit_with_message( f"Location '{location_str}' is not an integer.") else: # If there's only one section, it must be the place notation so all we need to do is to # strip it of whitespace (and `location` defaults to 0 so that's also set correctly) place_notation_str = segment.strip() # Produce a nice error message if the pn string is empty if place_notation_str == "": exit_with_message("Place notation strings cannot be empty.") # Insert the new call definition into the dictionary if location in parsed_calls: exit_with_message( f"Location {location} has two conflicting calls: \ '{parsed_calls[location]}' and '{place_notation_str}'.") parsed_calls[location] = place_notation_str return CallDef(parsed_calls)
def json_to_call(name: str) -> Optional[CallDef]: """ Helper function to generate a call with a given name from the json. """ if name not in json: logger.warning(f"No field '{name}' in the row generator JSON") return None call: Dict[int, str] = {} for key, value in json[name].items(): try: index = int(key) except ValueError as e: raise_error(name, f"Call index '{key}' is not a valid integer", e) call[index] = value return CallDef(call)
def parse_call_dict( unparsed_calls: CallDef) -> Dict[int, List[Places]]: """ Parse a dict of type `int => str` to `int => [PlaceNotation]`. """ parsed_calls = {} for i, place_notation_str in unparsed_calls.items(): # Parse the place notation string into a list of place notations, adjust the # call locations by the length of their calls (so that e.g. `0` always refers to # the lead end regardless of how long the calls are). converted_place_notations = convert_pn(place_notation_str) # Add the processed call to the output dictionary parsed_calls[(i - 1) % self.lead_len] = converted_place_notations return parsed_calls
class PlaceNotationGenerator(RowGenerator): """ A row generator to generate rows given a place notation. """ # Dict Lead Index: String PlaceNotation # 0 for end of the lead DefaultBob: ClassVar[CallDef] = CallDef({0: '14'}) DefaultSingle: ClassVar[CallDef] = CallDef({0: '1234'}) def __init__(self, stage: int, method: str, bob: CallDef = None, single: CallDef = None, start_index: int = 0) -> None: super().__init__(stage) if bob is None: bob = PlaceNotationGenerator.DefaultBob if single is None: single = PlaceNotationGenerator.DefaultSingle self.method_pn = convert_pn(method) self.lead_len = len(self.method_pn) # Store the method place notation as a string for the summary string self.method_pn_string = method self.start_index = start_index def parse_call_dict( unparsed_calls: CallDef) -> Dict[int, List[Places]]: """ Parse a dict of type `int => str` to `int => [PlaceNotation]`. """ parsed_calls = {} for i, place_notation_str in unparsed_calls.items(): # Parse the place notation string into a list of place notations, adjust the # call locations by the length of their calls (so that e.g. `0` always refers to # the lead end regardless of how long the calls are). converted_place_notations = convert_pn(place_notation_str) # Add the processed call to the output dictionary parsed_calls[(i - 1) % self.lead_len] = converted_place_notations return parsed_calls self.bobs_pn = parse_call_dict(bob) self.singles_pn = parse_call_dict(single) self._generating_call_pn: List[Places] = [] def summary_string(self) -> str: """ Returns a short string summarising the RowGenerator. """ return f"place notation '{self.method_pn_string}'" def _gen_row(self, previous_row: Row, stroke: Stroke, index: int) -> Row: assert self.lead_len > 0 lead_index = (index + self.start_index) % self.lead_len if self._has_bob and self.bobs_pn.get(lead_index): self._generating_call_pn = list(self.bobs_pn[lead_index]) self.logger.info(f"Bob at index {lead_index}") self.reset_calls() elif self._has_single and self.singles_pn.get(lead_index): self._generating_call_pn = list(self.singles_pn[lead_index]) self.logger.info(f"Single at index {lead_index}") self.reset_calls() if self._generating_call_pn: place_notation = self._generating_call_pn.pop(0) else: place_notation = self.method_pn[lead_index] return self.permute(previous_row, place_notation) def start_stroke(self) -> Stroke: return Stroke.from_index(self.start_index) @staticmethod def grandsire(stage: int) -> RowGenerator: """ Generates Grandsire on a given stage. """ stage_bell = convert_to_bell_string(stage) cross_notation = stage_bell if stage % 2 else '-' main_body = [ "1" if i % 2 else cross_notation for i in range(2 * stage) ] main_body[0] = "3" notation = ".".join(main_body) return PlaceNotationGenerator(stage, notation, bob=CallDef({-1: "3"}), single=CallDef({-1: "3.123"})) @staticmethod def stedman(stage: int) -> RowGenerator: """ Generates Stedman on a given stage (even bell Stedman will cause an exception). """ assert stage % 2 == 1 if stage == 5: return PlaceNotationGenerator.stedman_doubles() stage_bell = convert_to_bell_string(stage) stage_bell_1 = convert_to_bell_string(stage - 1) stage_bell_2 = convert_to_bell_string(stage - 2) notation = f"3.1.{stage_bell}.3.1.3.1.3.{stage_bell}.1.3.1" return PlaceNotationGenerator( stage, notation, bob=CallDef({ 3: stage_bell_2, 9: stage_bell_2 }), single=CallDef({ 3: f"{stage_bell_2}{stage_bell_1}{stage_bell}", 9: f"{stage_bell_2}{stage_bell_1}{stage_bell}" })) @staticmethod def stedman_doubles() -> RowGenerator: """ Generates Stedman on a given stage (even bell Stedman will cause an exception). """ notation = "3.1.5.3.1.3.1.3.5.1.3.1" return PlaceNotationGenerator(5, notation, bob=CallDef({}), single=CallDef({ 6: "345", 12: "145" }))