#!/usr/bin/env python3 """Convert a superpermutation to the format of the superpermutation records site: symbols 0-9 then A, B, C (the first n of "0123456789ABC"), all on one line, followed by one newline. The symbols are relabelled so the word begins with the first n symbols in order, as the site requires. Usage: python3 convert_word.py INPUT OUTPUT [--n N] INPUT may be raw, .gz or .xz, and may use - letters 1..n written as single digits (n <= 9), e.g. 123121321, - numbers separated by commas, semicolons, spaces or newlines, optionally in brackets, counting from 0 or from 1, - the site's own format, which is copied unchanged apart from the relabelling. The whole word is held in memory. """ from __future__ import annotations import argparse import gzip import lzma import re import sys import zlib SYMBOLS = "0123456789ABC" def read_text(path: str) -> str: try: with open(path, "rb") as f: head = f.read(6) if head.startswith(b"\x1f\x8b"): opener = gzip.open elif head.startswith(b"\xfd7zXZ\x00"): opener = lzma.open else: opener = open with opener(path, "rb") as f: return f.read().decode("ascii").strip() except UnicodeDecodeError: sys.exit(f"{path} is not plain ASCII text (a byte-order mark or a non-ASCII character such as an en dash?)") except (lzma.LZMAError, zlib.error, EOFError): sys.exit(f"{path} is a corrupt or truncated compressed file") except OSError as e: sys.exit(f"cannot read {path}: {e.strerror or e}") def parse(text: str) -> list[int]: text = text.strip("[]() \t\r\n") if re.fullmatch(r"[0-9A-C]+", text): return [SYMBOLS.index(c) for c in text] values = [] for token in re.split(r"[\s,;\[\]()]+", text): if not token: continue try: values.append(int(token)) except ValueError: sys.exit(f"cannot read {token!r}; expected digits, or numbers separated by commas or spaces") return values def to_site_format(values: list[int], n: int | None) -> str: if not values: sys.exit("the file contains no symbols") low, high = min(values), max(values) shift = 0 if low == 1: shift = 1 values = [v - 1 for v in values] high -= 1 elif low != 0: sys.exit(f"symbols run from {low} to {high}; expected 0..n-1 or 1..n") found_n = high + 1 if found_n > len(SYMBOLS): sys.exit(f"n = {found_n} is larger than 13: this converter holds the word in memory, so at n = 14 write " "the word as a Detour file directly (see the submit page)") if n is not None and n != found_n: sys.exit(f"the word uses n = {found_n} symbols, not {n}") missing = sorted(set(range(found_n)) - set(values)) if missing: names = ", ".join(str(m + shift) for m in missing) phrase = f"symbol {names} never appears" if len(missing) == 1 else f"symbols {names} never appear" sys.exit(f"{phrase}; a superpermutation on {found_n} symbols uses all of them") return "".join(SYMBOLS[v] for v in values) def relabel(word: str, n: int) -> str: """Relabel the symbols so the word begins with the first n symbols in order.""" first = word[:n] if len(set(first)) != n: sys.exit( f"the first {n} letters ({first}) are not a permutation; a superpermutation's first n letters " "should be, so the first letter can probably be deleted" ) return word.translate(str.maketrans(first, SYMBOLS[:n])) def main() -> None: parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("input") parser.add_argument("output") parser.add_argument("--n", type=int, help="expected number of symbols (checked)") args = parser.parse_args() word = to_site_format(parse(read_text(args.input)), args.n) n = SYMBOLS.index(max(word)) + 1 relabelled = relabel(word, n) if relabelled != word: print(f"relabelled so the word begins with {SYMBOLS[:n]}") with open(args.output, "w", newline="\n") as f: f.write(relabelled + "\n") print(f"wrote {args.output}: n = {n}, length {len(relabelled):,}") if __name__ == "__main__": main()