diff --git a/jubeatools/formats/guess.py b/jubeatools/formats/guess.py index b145cca..0df2415 100644 --- a/jubeatools/formats/guess.py +++ b/jubeatools/formats/guess.py @@ -37,7 +37,10 @@ def guess_memon_version(obj: Any) -> Format: except KeyError: return Format.MEMON_LEGACY except TypeError: - raise ValueError("Invalid JSON structure for memon file") + raise ValueError( + "This JSON file is not a correct memon file : the top-level " + "value is not an object" + ) if version == "0.1.0": return Format.MEMON_0_1_0 diff --git a/jubeatools/formats/jubeat_analyser/command.py b/jubeatools/formats/jubeat_analyser/command.py index b0a49d4..4da5c20 100644 --- a/jubeatools/formats/jubeat_analyser/command.py +++ b/jubeatools/formats/jubeat_analyser/command.py @@ -99,7 +99,9 @@ def parse_command(line: str) -> Tuple[str, Optional[str]]: return CommandVisitor().visit(command_grammar.parse(line)) # type: ignore except ParseError: if line.strip()[0] == "#": - raise ParseError(f"Invalid command syntax : {line}") from None + raise ParseError( + "Line starts with '#' but it couldn't be parsed as a valid command" + ) from None else: raise diff --git a/jubeatools/formats/jubeat_analyser/load_tools.py b/jubeatools/formats/jubeat_analyser/load_tools.py index 7149a5f..f05545d 100644 --- a/jubeatools/formats/jubeat_analyser/load_tools.py +++ b/jubeatools/formats/jubeat_analyser/load_tools.py @@ -93,6 +93,32 @@ class DoubleColumnChartLine: def __str__(self) -> str: return f"{self.position} |{self.timing}|" + def raise_if_unfit(self, bytes_per_panel: int) -> None: + self.raise_if_position_unfit(bytes_per_panel) + self.raise_if_timing_unfit(bytes_per_panel) + + def raise_if_position_unfit(self, bytes_per_panel: int) -> None: + expected_length = 4 * bytes_per_panel + actual_length = len(self.position.encode("shift-jis-2004")) + if expected_length != actual_length: + raise SyntaxError( + f"Invalid position part. Since #bpp={bytes_per_panel}, the \ + position part of a line should be {expected_length} bytes long, \ + but {self.position!r} is {actual_length} bytes long" + ) + + def raise_if_timing_unfit(self, bytes_per_panel: int) -> None: + if self.timing is None: + return + + length = len(self.timing.encode("shift-jis-2004")) + if length % bytes_per_panel != 0: + raise SyntaxError( + f"Invalid timing part. Since #bpp={bytes_per_panel}, the timing \ + part of a line should be divisible by {bytes_per_panel}, but \ + {self.timing!r} is {length} bytes long so it's not" + ) + class DoubleColumnChartLineVisitor(NodeVisitor): def __init__(self) -> None: @@ -145,7 +171,10 @@ def split_double_byte_line(line: str) -> List[str]: """ encoded_line = line.encode("shift-jis-2004") if len(encoded_line) % 2 != 0: - raise ValueError(f"Invalid chart line : {line}") + raise ValueError( + "Line of odd length encountered while trying to split a double-byte " + f"line : {line!r}" + ) symbols = [] for i in range(0, len(encoded_line), 2): symbols.append(encoded_line[i : i + 2].decode("shift-jis-2004")) @@ -166,7 +195,8 @@ class UnfinishedLongNote: def ends_at(self, end: BeatsTime) -> LongNote: if end < self.time: raise ValueError( - f"Invalid end time ({end}) for long note starting at {self.time}" + "Invalid end time. A long note starting at " + f"{self.time} cannot end at {end} (which is earlier)" ) return LongNote( time=self.time, @@ -228,7 +258,7 @@ def pick_correct_long_note_candidates( solutions: List[Solution] = problem.getSolutions() if not solutions: raise SyntaxError( - "Invalid long note arrow pattern in bloc :\n" + "Impossible arrow pattern found in block :\n" + "\n".join("".join(line) for line in bloc) ) solution = min(solutions, key=long_note_solution_heuristic) @@ -283,7 +313,7 @@ class JubeatAnalyserParser: try: method = getattr(self, f"do_{command}") except AttributeError: - raise SyntaxError(f"Unknown analyser command : {command}") from None + raise SyntaxError(f"Unknown jubeat analyser command : {command}") from None if value is not None: method(value) @@ -341,7 +371,7 @@ class JubeatAnalyserParser: if bpp not in (1, 2): raise ValueError(f"Unexcpected bpp value : {value}") elif self.circle_free and bpp == 1: - raise ValueError("#bpp can only be 2 when #circlefree is activated") + raise ValueError("Can't set #bpp to 1 when #circlefree is on") else: self.bytes_per_panel = int(value) @@ -349,18 +379,17 @@ class JubeatAnalyserParser: self.hold_by_arrow = int(value) == 1 def do_holdbytilde(self, value: str) -> None: - if int(value): - raise ValueError("jubeatools does not support #holdbytilde") + raise NotImplementedError("jubeatools does not support #holdbytilde") def do_circlefree(self, raw_value: str) -> None: activate = bool(int(raw_value)) if activate and self.bytes_per_panel != 2: - raise ValueError("#circlefree can only be activated when #bpp=2") + raise ValueError("#circlefree can only be on when #bpp=2") self.circle_free = activate def _wrong_format(self, f: str) -> None: raise ValueError( - f"{f} command indicates this file uses another jubeat analyser " + f"{f} command means that this file uses another jubeat analyser " "format than the one the currently selected parser is designed for" ) @@ -381,15 +410,15 @@ class JubeatAnalyserParser: length_as_shift_jis = len(symbol.encode("shift-jis-2004")) if length_as_shift_jis != bpp: raise ValueError( - f"Invalid symbol definition. Since #bpp={bpp}, timing symbols " - f"should be {bpp} bytes long but '{symbol}' is {length_as_shift_jis}" + f"Invalid symbol definition. Since #bpp={bpp}, timing symbols \ + should be {bpp} bytes long but '{symbol}' is {length_as_shift_jis}" ) if timing > self.beats_per_section: - message = ( - "Invalid symbol definition conscidering the number of beats per section :\n" - f"*{symbol}:{timing}" + raise ValueError( + f"Invalid symbol definition. Since sections only last \ + {self.beats_per_section} beats, a symbol cannot happen \ + afterwards at {timing}" ) - raise ValueError(message) self.symbols[symbol] = timing def is_short_line(self, line: str) -> bool: diff --git a/jubeatools/formats/jubeat_analyser/memo/dump.py b/jubeatools/formats/jubeat_analyser/memo/dump.py index 9329ac3..8b9cdcc 100644 --- a/jubeatools/formats/jubeat_analyser/memo/dump.py +++ b/jubeatools/formats/jubeat_analyser/memo/dump.py @@ -55,19 +55,28 @@ class Frame: bars: Dict[int, Dict[int, str]] = field(default_factory=dict) def dump(self, length: Decimal) -> Iterator[str]: - # Check that bars are contiguous - for a, b in windowed(sorted(self.bars), 2): - if b is not None and a is not None and b - a != 1: - raise ValueError("Frame has discontinuous bars") - # Check all bars are in the same 4-bar group - if self.bars.keys() != set(bar % 4 for bar in self.bars): - raise ValueError("Frame contains bars from different 4-bar groups") - + self.raise_if_unfit() for pos, bar in zip_longest(self.dump_positions(), self.dump_bars(length)): if bar is None: bar = "" yield f"{pos} {bar}" + def raise_if_unfit(self) -> None: + if not self.bars_are_contiguous(): + raise ValueError("Frame has discontinuous bars") + if not self.all_bars_are_from_the_same_group(): + raise ValueError("Frame contains bars from different 4-bar groups") + + def bars_are_contiguous(self) -> bool: + return all( + b - a == 1 + for a, b in windowed(sorted(self.bars), 2) + if b is not None and a is not None + ) + + def all_bars_are_from_the_same_group(self) -> bool: + return self.bars.keys() == set(bar % 4 for bar in self.bars) + def dump_positions(self) -> Iterator[str]: for y in range(4): yield "".join( @@ -121,9 +130,7 @@ class MemoDumpedSection(JubeatAnalyserDumpedSection): chosen_symbols[time_in_section] = symbol bars[bar_index][time_index] = symbol elif time_in_section not in self.symbols: - raise ValueError( - f"No symbol defined for time in section : {time_in_section}" - ) + raise ValueError(f"No symbol defined for time : {time_in_section}") # Create frame by bar section_symbols = ChainMap(chosen_symbols, self.symbols) diff --git a/jubeatools/formats/jubeat_analyser/memo/load.py b/jubeatools/formats/jubeat_analyser/memo/load.py index 347b25e..b9ffa35 100644 --- a/jubeatools/formats/jubeat_analyser/memo/load.py +++ b/jubeatools/formats/jubeat_analyser/memo/load.py @@ -98,13 +98,7 @@ class MemoParser(JubeatAnalyserParser): self._do_bpp(value) def append_chart_line(self, line: DoubleColumnChartLine) -> None: - if len(line.position.encode("shift-jis-2004")) != 4 * self.bytes_per_panel: - raise SyntaxError( - f"Invalid chart line for #bpp={self.bytes_per_panel} : {line}" - ) - if line.timing is not None and self.bytes_per_panel == 2: - if len(line.timing.encode("shift-jis-2004")) % 2 != 0: - raise SyntaxError(f"Invalid chart line for #bpp=2 : {line}") + line.raise_if_unfit(self.bytes_per_panel) self.current_chart_lines.append(line) if len(self.current_chart_lines) == 4: self._push_frame() diff --git a/jubeatools/formats/jubeat_analyser/memo1/load.py b/jubeatools/formats/jubeat_analyser/memo1/load.py index e52b14b..22495c6 100644 --- a/jubeatools/formats/jubeat_analyser/memo1/load.py +++ b/jubeatools/formats/jubeat_analyser/memo1/load.py @@ -92,13 +92,7 @@ class Memo1Parser(JubeatAnalyserParser): self._do_bpp(value) def append_chart_line(self, line: DoubleColumnChartLine) -> None: - if len(line.position.encode("shift-jis-2004")) != 4 * self.bytes_per_panel: - raise SyntaxError( - f"Invalid chart line for #bpp={self.bytes_per_panel} : {line}" - ) - if line.timing is not None and self.bytes_per_panel == 2: - if len(line.timing.encode("shift-jis-2004")) % 2 != 0: - raise SyntaxError(f"Invalid chart line for #bpp=2 : {line}") + line.raise_if_unfit(self.bytes_per_panel) self.current_chart_lines.append(line) if len(self.current_chart_lines) == 4: self._push_frame()