diff --git a/specs/CipherSpecs/CaesarCipherSpec.hs b/specs/CipherSpecs/CaesarCipherSpec.hs new file mode 100644 index 0000000..b50fe12 --- /dev/null +++ b/specs/CipherSpecs/CaesarCipherSpec.hs @@ -0,0 +1,25 @@ +{-# LANGUAGE ScopedTypeVariables #-} +module CipherSpecs.CaesarCipherSpec where + +import Test.Hspec +import Test.QuickCheck +import Ciphers.CaesarCipher +import Data.Char (toLower) + +spec :: Spec +spec = do + describe "encrypt" $ do + it "does nothing when alphabet is empty" $ property $ + encrypt "hello" 5 [] == "hello" + + it "does nothing when key is 0" $ property $ + encrypt "hello" 0 ['a'..'z'] == "hello" + + it "does not transform characters not found in the provided alphabet" $ property $ + encrypt "h3llo" 1 ['a'..'z'] == "i3mmp" + + describe "decrypt" $ do + it "returns the original input when given the same key and alphabet as encrypt" $ property $ + forAll arbitrary $ + \(asciiInput :: ASCIIString, key :: Int) -> let input = map toLower $ getASCIIString asciiInput + in decrypt (encrypt input key ['a'..'z']) key ['a'..'z'] == input diff --git a/src/Ciphers/CaesarCipher.hs b/src/Ciphers/CaesarCipher.hs new file mode 100644 index 0000000..e17840f --- /dev/null +++ b/src/Ciphers/CaesarCipher.hs @@ -0,0 +1,22 @@ +module Ciphers.CaesarCipher(encrypt,decrypt) where + +import Data.List (elemIndex) +import Data.Maybe (fromMaybe) + +-- | Encrypting a string maps each character to the character `n` elements +-- after it in the given alphabet, where `n`is the provided key +encrypt :: String -> Int -> String -> String +encrypt = helper (+) + +-- | Decrypting a string maps each character to the character `n` elements +-- before it in the given alphabet, where `n` is the provided key +decrypt :: String -> Int -> String -> String +decrypt = helper (-) + +helper :: (Int -> Int -> Int) -> String -> Int -> String -> String +helper _ input 0 _ = input +helper _ input _ [] = input +helper op input key alphabet = map (\x -> fromMaybe x (mappedChar $ elemIndex x alphabet)) input + where + len = length alphabet + mappedChar = fmap (\c -> alphabet !! (c `op` key `mod` len)) diff --git a/src/Ciphers/Rot13.hs b/src/Ciphers/Rot13.hs new file mode 100644 index 0000000..1c60ebd --- /dev/null +++ b/src/Ciphers/Rot13.hs @@ -0,0 +1,16 @@ +module Ciphers.Rot13 where + +import Data.Char (isAsciiLower, isAsciiUpper) + +-- See https://en.wikipedia.org/wiki/ROT13 + +-- | "Rotates" each character in a string by 13. +-- Note: This only rotates alphabetic characters, any other character is left +-- as-is +dencrypt :: String -> String +dencrypt = map rotate + +rotate :: Char -> Char +rotate c | isAsciiLower c = ([c..'z'] ++ ['a'..'z']) !! 13 + | isAsciiUpper c = ([c..'Z'] ++ ['A'..'Z']) !! 13 + | otherwise = c