RosettaCodeData/Task/Playfair-cipher/Haskell/playfair-cipher.hs

102 lines
3.5 KiB
Haskell
Raw Permalink Normal View History

2023-07-01 11:58:00 -04:00
import Control.Monad (guard)
import Data.Array (Array, assocs, elems, listArray, (!))
import Data.Char (toUpper)
import Data.List (nub, (\\))
import Data.List.Split (chunksOf)
import Data.Maybe (listToMaybe)
import Data.String.Utils (replace)
type Square a = Array (Int, Int) a
-- | Turns a list into an n*m-array.
array2D ::
(Int, Int) -- ^ n * m
-> [e] -> Square e
array2D maxCoord = listArray ((1, 1), maxCoord)
-- | Generates a playfair table starting with the specified string.
--
-- >>> makeTable "hello"
-- "HELOABCDFGIKMNPQRSTUVWXYZ"
makeTable :: String -> String
makeTable k = nub key ++ (alpha \\ key)
where
alpha = ['A' .. 'Z'] \\ "J"
key = map toUpper =<< words k
-- | Turns a playfair table into a 5*5 alphabet square.
makeSquare :: [a] -> Square a
makeSquare = array2D (5, 5)
-- | Displays a playfair square, formatted as a square.
showSquare :: Square Char -> String
showSquare d = unlines $ chunksOf 5 (elems d)
-- | Given a value and an association list of x-coordinate * y-coordinate * value, returns the coordinates
getIndex' :: (Eq a) => a -> [((Int, Int), a)] -> Maybe (Int, Int)
getIndex' el = fmap fst . listToMaybe . filter ((== el) . snd)
encodePair, decodePair :: Eq a => Square a -> (a, a) -> Maybe (a, a)
encodePair = pairHelper (\x -> if x == 5 then 1 else x + 1)
decodePair = pairHelper (\x -> if x == 1 then 5 else x - 1)
pairHelper :: (Eq t)
=> (Int -> Int) -- ^ a function used for wrapping around the square
-> Square t -- ^ a playfair square
-> (t, t) -- ^ two characters
-> Maybe (t, t) -- ^ the two resulting/encoded characters
pairHelper adjust sqr (c1, c2) =
do let ps = assocs sqr
-- assigns an association list of (x-coord * y-coord) * value to ps
(x1, y1) <- getIndex' c1 ps
(x2, y2) <- getIndex' c2 ps
-- returns the coordinates of two values in the square
-- these will later be swapped
guard $ c1 /= c2
-- the characters (and coordinates) cannot be the same
let get x = sqr ! x
-- a small utility function for extracting a value from the square
Just $
-- wrap the coordinates around and find the encrypted characters
case () of
() | y1 == y2 ->
(get (adjust x1, y1), get (adjust x2, y2))
| x1 == x2 ->
(get (x1, adjust y1), get (x2, adjust y2))
| otherwise ->
(get (x1, y2), get (x2, y1))
-- | Turns two characters into a tuple.
parsePair :: String -> [(Char, Char)]
parsePair = fmap (\[x, y] -> (x, y)) . words . fmap toUpper
-- | Turns a tuple of two characters into a string.
unparsePair :: [(Char, Char)] -> String
unparsePair = unwords . fmap (\(x, y) -> [x, y])
codeHelper :: (Square Char -> (Char, Char) -> Maybe (Char, Char))
-> String -> String -> Maybe String
codeHelper subs key =
fmap unparsePair .
mapM (subs (makeSquare $ makeTable key)) .
parsePair
playfair, unplayfair :: String -> String -> Maybe String
playfair key = codeHelper encodePair key . formatEncode
unplayfair = codeHelper decodePair
formatEncode :: String -> String
formatEncode =
map toUpper .
unwords .
map (\[x, y] -> if x == y then [x, 'x'] else [x, y]) .
chunksOf 2 .
replace "j" "i" .
concatMap adjustLength .
words .
filter (\n -> n `elem` (['A'..'Z'] ++ ['a'..'z']))
where
adjustLength str
| odd (length str) = str ++ "x"
| otherwise = str