# Introduction

The Condi (Consecutive Digraphs) cipher was introduced by G4EGG (Wilfred Higginson) in 2011. The cipher preserves word divisions, and is simple to describe and encode, but it's surprisingly difficult to crack. 

# Encoding Algorithm

The encoding steps are:
- Start with an `initial key`, e.g. `cryptogram`
- Form a `key`, remove the key duplicated letters except for the first occurrence
- Append to it, in alphabetical order all letters which do not occur in the `key`.  
  The example produces: `cryptogambdefhijklnqsuvwxz`
- Number the `key alphabet` starting with 1.
```python
1  2  3  4  5  6  7  8  9  10 11 12 13
c  r  y  p  t  o  g  a  m  b  d  e  f 
14 15 16 17 18 19 20 21 22 23 24 25 26
h  i  j  k  l  n  q  s  u  v  w  x  z
```
- One of the inputs to encoding algorithm is an `initial shift`, say `10`
- Encode the first letter of your `message` by moving 10 places to the right from the letter's position in the key alphabet. If the first letter were say `o` then the letter 10 places to the right in the `key alphabet` is `j`, so `o` would be encoded as `j`. If you move past the end of the key alphabet you wrap back to the beginning. For example if the first letter were `s` then counting 10 places would bring you around to `t`.
- Use the position of the previous plaintext letter as the number of places to move to encode the next plaintext number. If you have just encoded an `o` (position 6) , and you now want to encode say `n`, then you move 6 places to the right from `n` which brings you to `x`.
- Keep repeating the previous step until all letters are encoded.

Decoding is the reverse of encoding - you move to the left instead of to the right.

# Task

Create two functions - `encode`/`Encode` and `decode`/`Decode` which implement Condi cipher encoding and decoding.

# Inputs

- `message` - a string to encode/decode
- `key` - a key consists of only lower case letters
- `initShift` - a non-negative integer representing the initial shift

# Notes
- Don't forget to remove the duplicated letters from the `key` except for the first occurrence 
- Characters which do not exist in the `key alphabet` should be coppied to the output string exactly like they appear in the `message` string
- Check the test cases for samples

Difficulty: introductory

Starter code:

def encode(message, key, shift, encode=True):
	

Solutions:
#from string import ascii_lowercase as LOWER
LOWER = "abcdefghijklmnopqrstuvwxyz"

def encode(message, key, shift, encode=True):
    key = sorted(LOWER, key=f"{key}{LOWER}".index)
    result = []
    for char in message:
        if char in key:
            i = key.index(char)
            char = key[(i + shift) % 26]
            shift = i + 1 if encode else -(key.index(char) + 1)
        result.append(char)
    return "".join(result)
    
def decode(message, key, shift): 
    return encode(message, key, -shift, encode=False)

from string import ascii_lowercase

def encode_decode(message, key, shift, func):
    keys = ''.join(dict.fromkeys(key)) + ''.join(c for c in ascii_lowercase if c not in key)
    for c in message:
        if c.islower():
            c, shift = func(keys, c, shift)
        yield c

def encode(message, key, shift):
    return ''.join(encode_decode(
        message, key, shift,
        lambda k, c, s: (k[(k.find(c) + s) % 26], k.find(c) + 1)),
    )
    
def decode(message, key, shift):
    return ''.join(encode_decode(
        message, key, shift,
        lambda k, c, s: (k[(k.find(c) - s) % 26], k.find(k[(k.find(c) - s) % 26]) + 1)),
    )

from string import ascii_lowercase


def form_key(key):
    working_key = ""
    for letter in key + ascii_lowercase:
        if letter not in working_key:
            working_key += letter
    return working_key


def encode(msg, key, init_shift):
    encode_key = form_key(key)
    shift = init_shift
    encoded_msg = ""
    for letter in msg:
        if letter not in encode_key:
            encoded_msg += letter
        else:
            encoded_msg += encode_key[(encode_key.index(letter) + shift) % 26]
            shift = encode_key.index(letter) + 1
    return encoded_msg


def decode(msg, key, init_shift):
    decode_key = form_key(key)
    shift = init_shift
    decoded_msg = ""
    for letter in msg:
        if letter not in decode_key:
            decoded_msg += letter
        else:
            decoded_msg += decode_key[(decode_key.index(letter) - shift) % 26]
            shift = decode_key.index(decoded_msg[-1]) + 1
    return decoded_msg

def condi_helper(message, init_key, shift, mode):
    key = sorted(set(init_key), key=init_key.index)
    key += sorted(set('abcdefghijklmnopqrstuvwxyz') - set(init_key))
    
    result=[]
    for char in message:
        if char in key:
            idx = key.index(char)
            new = (idx + mode * shift) % 26
            result.append(key[new])
            if mode == 1:
                shift = idx + 1
            else:
                shift = new + 1
        else:
            result.append(char)
    
    return ''.join(result)

encode = lambda msg, key, sh: condi_helper(msg, key, sh, 1)
decode = lambda msg, key, sh: condi_helper(msg, key, sh, -1)

from string import ascii_lowercase as low

def encode(message, key, initShift): return shifter(message, key, initShift, 1)
def decode(message, key, initShift): return shifter(message, key, initShift, -1)

def shifter(msg, k, shift, dir):

    def moveChar(c):
        shifted  = alphaStr[(keyAlpha[c] + dir + shift[0]*dir) % 26 ]
        shift[0] = keyAlpha[(c if dir == 1 else shifted)]
        return shifted
    
    def cleanKey(k):
        s, sk = "", set()
        for c in k:
            if c not in sk: s += c
            sk.add(c)
        return s,sk

    shift = [shift-1]
    k, sk = cleanKey(k)
    alphaStr = k + ''.join(c for c in low if c not in sk)
    keyAlpha = {l:i for i,l in enumerate(alphaStr)}
    
    return ''.join(c if c not in keyAlpha else moveChar(c) for c in msg)

import string

def xcode(message, key, initShift, is_encode):
    seen, ret, shift = set(), "", initShift
    alphabet = [c for c in key.lower() + string.ascii_lowercase if c.isalpha() and not (c in seen or seen.add(c))]
    for letter in list(message):
        if letter.isalpha():
            new_index = (alphabet.index(letter) + (shift if is_encode else -shift)) % 26
            shift = (alphabet.index(letter) if is_encode else new_index) + 1
            letter = alphabet[new_index]
        ret += letter
    return ret

def encode(message, key, initShift):
    return xcode(message, key, initShift, True)

def decode(message, key, initShift):
    return xcode(message, key, initShift, False)

from string import ascii_lowercase as AL

def encode(s, key, n):
    a = "".join(dict.fromkeys(key + AL))
    d = {x: i for i, x in enumerate(a, 1)}
    r = []
    for x in s:
        if x in d:
            r.append(a[(d[x] + n - 1) % len(a)])
            n = d[x]
        else:
            r.append(x)
    return "".join(r)
    
def decode(s, key, n): 
    a = "".join(dict.fromkeys(key + AL))
    d = {x: i for i, x in enumerate(a, 1)}
    r = []
    for x in s:
        if x in d:
            r.append(a[(d[x] - n - 1) % len(a)])
            n = d[r[-1]]
        else:
            r.append(x)
    return "".join(r)

def encode(message, key, shift): return cipher(message, key, shift, 1)

def decode(message, key, shift): return cipher(message, key, -shift, 0)

def cipher(message, key, shift, mode):
    key = tuple(dict.fromkeys(key.lower() + 'abcdefghijklmnopqrstuvwxyz'))
    res = ''
    for char in message:
        if char in key:
            i = key.index(char)
            char = key[(i + shift) % 26]
            shift = i + 1 if mode else -key.index(char) - 1
        res += char
    return res

def encode(string, key, initShift):
    key = tuple(dict.fromkeys(key.lower() + 'abcdefghijklmnopqrstuvwxyz'))
    shift = (initShift - 1) % 26
    res = ''
    for c in string:
        if c in key:
            i = key.index(c)
            c = key[(i + shift + 1) % 26]
            shift = i
        res += c
    return res


def decode(string, key, initShift):
    key = tuple(dict.fromkeys(key.lower() + 'abcdefghijklmnopqrstuvwxyz'))
    shift = (initShift - 1) % 26
    res = ''
    for i, c in enumerate(string):
        if c in key:
            res += key[(key.index(c) - shift - 1) % 26]
            shift = key.index(res[i])
        else:
            res += c
    return res

def get_key(d, value):
    for k, v in d.items():
        if v == value:
            return k

def encode(message, key, initShift):
    new_key = ''
    for i in key:
        if i not in new_key:
            new_key += i

    alf = 'abcdefghijklmnopqrstuvwxyz'
    for i in alf:
        if not i in new_key:
            new_key += i

    alphabet = {x: y for x, y in zip(range(1, 27), new_key)}
    result = ''
    for i in message:
        if i.isalpha() == False:
            result += i
            continue
        i = get_key(alphabet, i)
        if (i + initShift) <= 26:
            result += alphabet.get(i + initShift)
            initShift = i
        else:
            result += alphabet.get((i + initShift) - 26)
            initShift = i
    return result


def decode(message, key, initShift):
    new_key = ''
    for i in key:
        if i not in new_key:
            new_key += i

    alf = 'abcdefghijklmnopqrstuvwxyz'
    for i in alf:
        if not i in new_key:
            new_key += i

    alphabet = {x: y for x, y in zip(range(1, 27), new_key)}
    result = ''
    for i in message:
        if i.isalpha() == False:
            result += i
            continue
        i = get_key(alphabet, i)
        if (i - initShift) > 0:
            result += alphabet.get(i - initShift)
            initShift = i - initShift
        elif (i - initShift + 26) == 0:
            result += alphabet.get(26)
            initShift = 26
        else:
            result += alphabet.get((i - initShift) + 26)
            initShift = (i - initShift) + 26
    return result