# Introduction 

The ragbaby cipher is a substitution cipher that encodes/decodes a text using a keyed alphabet and their position in the plaintext word they are a part of.

To encrypt the text `This is an example.` with the key `cipher`, first construct a keyed alphabet:
```
c       i       p       h       e       r       a       b       d       f       g       j       k       l       m       n       o       q       s       t       u       v       w       x       y       z
```

Then, number the letters in the text as follows:
```
T       h       i       s               i       s               a       n               e       x       a       m       p       l       e       .
1       2       3       4               1       2               1       2               1       2       3       4       5       6       7        
```

To obtain the encoded text, replace each character of the word with the letter in the keyed alphabet the corresponding number of places to the right of it (wrapping if necessary). 
Non-alphabetic characters are preserved to mark word boundaries.

Our ciphertext is then `Urew pu bq rzfsbtj.`

# Task

Wirate functions `encode` and `decode` which accept 2 parameters:
- `text` - string - a text to encode/decode
- `key` -  string - a key

# Notes

- handle lower and upper case in `text` string
- `key` consists of only lowercase characters

Difficulty: introductory

Starter code:

def encode(text, key):
	

Solutions:
from string import ascii_lowercase as aLow
import re

def rotateWord(w, alpha, dct, d):
    lst = []
    for i,c in enumerate(w.lower(), 1):
        transChar = alpha[ (dct[c] + i*d) % 26 ]
        if w[i-1].isupper(): transChar = transChar.upper()
        lst.append(transChar)
    return ''.join(lst)

def encode(text, key, d=1):
    remains, alpha = set(aLow), []
    for c in key+aLow:
        if c in remains:
            remains.remove(c)
            alpha.append(c)
    alpha = ''.join(alpha)
    dct   = {c:i for i,c in enumerate(alpha)}
    return re.sub(r'[a-zA-Z]+', lambda m: rotateWord(m.group(),alpha,dct,d), text)
    
def decode(text, key):
    return encode(text, key, -1)

import re
from collections import OrderedDict
from string import ascii_lowercase

def encode(text, key, mul=1):
    i2c = ''.join(OrderedDict.fromkeys(key)) + ''.join(c for c in ascii_lowercase if c not in key)
    c2i = {c: i for i, c in enumerate(i2c)}
    return re.sub(
        '[a-z]+',
        lambda m: ''.join((str.upper if c.isupper() else str)(i2c[(c2i[c.lower()] + i * mul) % 26])
                          for i, c in enumerate(m.group(), 1)),
        text,
        flags=re.I
    )

def decode(text, key):
    return encode(text, key, -1)

import re
from string import ascii_lowercase as alphabet

def keyed_alphabet(key):
    char2idx, idx2char = {}, {}
    for letter in key + alphabet:
        if letter not in char2idx:
            char2idx[letter] = len(char2idx)
            idx2char[len(idx2char)] = letter
    return char2idx, idx2char

def ragbaby(mode, text, key):
    char2idx, idx2char = keyed_alphabet(key)
    cipher_word = lambda match: ''.join(
    [str.upper, str.lower][letter.islower()](idx2char[(char2idx[letter.lower()] + idx * mode) % len(alphabet)])
            for idx, letter in enumerate(match.group(), 1))
    return re.sub('[{}]+'.format(alphabet), cipher_word, text, flags=re.I)

encode, decode = lambda *args: ragbaby(1, *args), lambda *args: ragbaby(-1, *args)

from string import ascii_lowercase as aLow
import re

def rotateWord(w, alpha, dct, d):
    return ''.join( (str.lower if w[i-1].islower() else str.upper)(alpha[ (dct[c]+i*d) % 26 ]) for i,c in enumerate(w.lower(), 1) )

def encode(text, key, d=1):
    s     = set(aLow)
    alpha = ''.join(s.remove(c) or c for c in key+aLow if c in s)
    dct   = {c:i for i,c in enumerate(alpha)}
    return re.sub(r'[a-zA-Z]+', lambda m: rotateWord(m.group(),alpha,dct,d), text)
    
def decode(text, key):
    return encode(text, key, -1)

def encode(text, key):
    
    string = "abcdefghijklmnopqrstuvwxyz"
    string_me = key+string
    periods = []
    spaces = []
    commas = []
       
    string_me = list(dict.fromkeys(string_me))

    
    for x in range(0, len(text)):
        if text[x] == ".":
            periods.append(x)
        elif text[x] == ",":
            commas.append(x)
        elif text[x] == " ":
            spaces.append(x)
            
    text = text.replace(".", " ").replace(",", " ").split()  
    
    word = 0
    
    new_word = []
    
    while word < len(text):
        
        for x in range(0, len(text[word])):
            
            if text[word][x].isupper():
                
                if string_me.index(text[word][x].lower())+x+1 >= 26:
                    
                    number = string_me.index(text[word][x].lower())+x+1
                    
                    while number >= 26:
                        number -= 26
                    
                    new_word.append(string_me[number].upper())
                else:
                    new_word.append(string_me[string_me.index(text[word][x].lower())+x+1].upper())
                
            else:
                
                if string_me.index(text[word][x])+x+1 >= 26:
                    
                    number = string_me.index(text[word][x].lower())+x+1
                    
                    while number >= 26:
                        number -= 26
                        
                    new_word.append(string_me[number])
                else:
                    new_word.append(string_me[string_me.index(text[word][x].lower())+x+1])
            
        text[word] = "".join(new_word)
        new_word = []
        word += 1
        
                       
    text = list("".join(text))
    
    newwww = periods+spaces+commas
    
    newwww.sort()

    if newwww:
        for x in newwww:
            text.insert(x, " ")
        
    for x in periods:
        text[x] = "."
    for x in commas:
        text[x] = ","
    
    return("".join(text))

    
def decode(text, key):
    
    string = "abcdefghijklmnopqrstuvwxyz"
    string_me = key+string
    periods = []
    spaces = []
    commas = []
       
    string_me = list(dict.fromkeys(string_me))
    
    for x in range(0, len(text)):
        if text[x] == ".":
            periods.append(x)
        elif text[x] == ",":
            commas.append(x)
        elif text[x] == " ":
            spaces.append(x)
            
    text = text.replace(".", " ").replace(",", " ").split()  
    
    word = 0
    
    new_word = []
    
    while word < len(text):
        
        for x in range(0, len(text[word])):
            
            if text[word][x].isupper():
                

                
                if string_me.index(text[word][x].lower())-x-1 >= 26:
                    
                    number = string_me.index(text[word][x].lower())-x-1
                    
                    while number >= 26:
                        number -= 26
                    
                    new_word.append(string_me[number].upper())
                
                elif string_me.index(text[word][x].lower())-x-1 < 0:
                    
                    number = string_me.index(text[word][x].lower())-x-1
                    
                    while number < 0:
                        number += 26
                        
                    new_word.append(string_me[number].upper())
                else:
                    new_word.append(string_me[string_me.index(text[word][x].lower())-x-1].upper())
                
            else:


                
                if string_me.index(text[word][x])-x-1 >= 26:
                    
                    number = string_me.index(text[word][x].lower())-x-1
                    
                    while number >= 26:
                        number -= 26
                    new_word.append(string_me[number])    
                elif string_me.index(text[word][x].lower())-x-1 < 0:
                    
                    number = string_me.index(text[word][x].lower())-x-1
                    
                    while number < 0:
                        number += 26
                        
                    new_word.append(string_me[number])
                else:
                    new_word.append(string_me[string_me.index(text[word][x].lower())-x-1])
            
        text[word] = "".join(new_word)
        new_word = []
        word += 1
        
                       
    text = list("".join(text))
    
    newwww = periods+spaces+commas
    
    newwww.sort()

    if newwww:
        for x in newwww:
            text.insert(x, " ")
        
    for x in periods:
        text[x] = "."
    for x in commas:
        text[x] = ","
    
    return("".join(text))


from string import ascii_lowercase as AL
import re

def f(op):
    def g(s, key):
        a = "".join(dict.fromkeys(key + AL))
        d = {x: i for i, x in enumerate(a)}
        def f(s):
            s, r = s[0], []
            for i, x in enumerate(s, 1):
                y = a[(op(d[x.lower()], i)) % len(a)]
                if x.isupper():
                    y = y.upper()
                r.append(y)
            return "".join(r)
        return re.sub(r"\w+", f, s)
    return g

encode = f(int.__add__)
decode = f(int.__sub__)

def encode(text, key):
    k=''
    for c in key+'abcdefghijklmnopqrstuvwxyz':
        if c not in k:
            k+=c
    r=''
    i=1
    for c in text:
        if c.isalpha():
            j=(k.index(c.lower())+i)%26
            if c.isupper():
                r+=k[j].upper()
            else:
                r+=k[j]
            i+=1
        else:
            r+=c
            i=1
    return r
    
def decode(text, key):
    k=''
    for c in key+'abcdefghijklmnopqrstuvwxyz':
        if c not in k:
            k+=c
        r=''
    i=1
    for c in text:
        if c.isalpha():
            j=(k.index(c.lower())-i)%26
            if c.isupper():
                r+=k[j].upper()
            else:
                r+=k[j]
            i+=1
        else:
            r+=c
            i=1
    return r
    return text

def encode(text, key): return cipher(text, key, 1)


def decode(text, key): return cipher(text, key, -1)


def cipher(text, key, mode):
    U = tuple(dict.fromkeys(key.upper() + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'))
    L = tuple(dict.fromkeys(key.lower() + 'abcdefghijklmnopqrstuvwxyz'))

    output = ''
    i = mode
    for x in text:
        if x in U:
            output += U[(U.index(x) + i) % 26]
            i += mode
        elif x in L:
            output += L[(L.index(x) + i) % 26]
            i += mode
        else:
            output += x
            i = mode
    return output

from string import ascii_lowercase
from operator import add, sub
from itertools import starmap
from re import sub as rsub

def change(text, key, o):
    tmp = dict.fromkeys(key)
    cipher = ''.join(tmp) + ''.join(c for c in ascii_lowercase if c not in tmp)
    D = {c:i for i,c in enumerate(cipher)}
    a = lambda i,c: cipher[o(D[c], i) % 26]
    b = lambda i,c: cipher[o(D[c.lower()], i) % 26].upper()
    f = lambda i,c: a(i, c) if c.islower() else b(i, c) if c.isupper() else c
    g = lambda w: ''.join(starmap(f, enumerate(w.group(), 1)))
    return rsub(r"[a-zA-z]+", g, text)

def encode(text, key):
    return change(text, key, add)
    
def decode(text, key):
    return change(text, key, sub)

def encode(msg,k):
    k = ''.join(dict.fromkeys(k)) + ''.join(i for i in 'abcdefghijklmnopqrstuvwxyz' if i not in k)
    r,i = '',1
    for s in msg:
        x = s.lower()
        if x in k:
            t = k[(k.find(x)+i)%26]
            r += t.upper() if s.isupper() else t
            i += 1
        else:
            r += s
            i = 1
    return r

            
    
def decode(msg,k): 
    k = ''.join(dict.fromkeys(k)) + ''.join(i for i in 'abcdefghijklmnopqrstuvwxyz' if i not in k)
    r,i = '',1
    for s in msg:
        x = s.lower()
        if x in k:
            t = k[(k.find(x)-i)%26]
            r += t.upper() if s.isupper() else t
            i += 1
        else:
            r += s
            i = 1
    return r