docstr: Checks an input if it only consists of two or three different letters.
  If this is the case, it attempts to regard those letters as
  0 and 1 (with the third characters as an optional delimiter) and then
  converts it to ASCII text.
docstr_tok: ["Checks","an","input","if","it","only","consists","of","two","or","three","different","letters",".","If","this","is","the","case",",","it","attempts","to","regard","those","letters","as","0","and","1","(","with","the","third","characters","as","an","optional","delimiter",")","and","then","converts","it","to","ASCII","text","."]
code: def attemptCrack(self, ctext: str) -> List[CrackResult]:
      """
      Checks an input if it only consists of two or three different letters.
      If this is the case, it attempts to regard those letters as
      0 and 1 (with the third characters as an optional delimiter) and then
      converts it to ASCII text.
      """
      logging.debug("Attempting X-Y replacement")
      variants = []
      candidates = []
      result = []
  
      # Convert the ctext to all-lowercase and regex-match & replace all whitespace
      ctext = re.sub(r"\s+", "", ctext.lower(), flags=re.UNICODE)
  
      # cset contains every unique value in the ctext
      cset = list(set(list(ctext)))
      cset_len = len(cset)
  
      if not 1 < cset_len < 4:
          # We only consider inputs with two or three unique values
          logging.debug(
              "Failed to crack X-Y due to not containing two or three unique values"
          )
          return None
  
      logging.debug(f"String contains {cset_len} unique values: {cset}")
  
      # In case of three unique values, we regard the least frequent character as the delimiter
      if cset_len == 3:
          # Count each unique character in the set to determine the least frequent one
          counting_list = []
          for char in cset:
              counting_list.append(ctext.count(char))
          val, index = min(
              (val, index) for (index, val) in enumerate(counting_list)
          )
          delimiter = cset[index]
          logging.debug(
              f"{delimiter} occurs {val} times and is the probable delimiter"
          )
          # Remove the delimiter from the ctext and compute new cset
          ctext = ctext.replace(delimiter, "")
          cset = list(set(list(ctext)))
  
      # Form both variants of the substitution
      for i in range(2):
          if i:
              variants.append(
                  ctext.replace(cset[0], "1").replace(cset[1], "0")
              )
          else:
              variants.append(
                  ctext.replace(cset[0], "0").replace(cset[1], "1")
              )
  
      # Apply function to both variants and strip stray NULL characters
      for variant in variants:
          candidates.append(self.binary_to_ascii(variant).strip("\x00"))
      for i, candidate in enumerate(candidates):
          if candidate != "":
              keyinfo = (
                  f"{cset[0]} -> {i} & {cset[1]} -> {str(int(not i))}"
              )
              result.append(
                  CrackResult(value=candidate, key_info=keyinfo)
              )
              logging.debug(
                  f"X-Y cracker - Returning results: {result}"
              )
              return result
  
file_path: Ciphey/ciphey/basemods/Crackers/xandy.py
imports: import re
  from typing import Dict, List, Optional
  import logging
  from rich.logging import RichHandler
  from ciphey.iface import Config, Cracker, CrackInfo, CrackResult, ParamSpec, registry
label_desc: Use image node_id 4 for calling the Xandy obj's underlying member method code with example usage: obj.attemptCrack(ctext) and returns: None, result