diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
new file mode 100644
index 0000000..c822caf
--- /dev/null
+++ b/.github/workflows/CI.yml
@@ -0,0 +1,106 @@
+name: CI
+# Run on master, tags, or any pull request
+on:
+ schedule:
+ - cron: '0 2 * * *' # Daily at 2 AM UTC (8 PM CST)
+ push:
+ branches: [master]
+ tags: ["*"]
+ pull_request:
+jobs:
+ test:
+ name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }}
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ version:
+ - "1.0" # LTS
+ - "1.1"
+ - "1.5"
+ - "nightly"
+ os:
+ - ubuntu-latest
+ - macOS-latest
+ - windows-latest
+ arch:
+ - x64
+ - x86
+ exclude:
+ # Test 32-bit only on Linux
+ - os: macOS-latest
+ arch: x86
+ - os: windows-latest
+ arch: x86
+ include:
+ # Add a 1.3 job because that's what Invenia actually uses
+ - os: ubuntu-latest
+ version: 1.3
+ arch: x64
+ steps:
+ - uses: actions/checkout@v2
+ - uses: julia-actions/setup-julia@v1
+ with:
+ version: ${{ matrix.version }}
+ arch: ${{ matrix.arch }}
+ - uses: actions/cache@v1
+ env:
+ cache-name: cache-artifacts
+ with:
+ path: ~/.julia/artifacts
+ key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }}
+ restore-keys: |
+ ${{ runner.os }}-test-${{ env.cache-name }}-
+ ${{ runner.os }}-test-
+ ${{ runner.os }}-
+ - uses: julia-actions/julia-buildpkg@latest
+ - run: |
+ git config --global user.name Tester
+ git config --global user.email te@st.er
+ - uses: julia-actions/julia-runtest@latest
+ - uses: julia-actions/julia-processcoverage@v1
+ - uses: codecov/codecov-action@v1
+ with:
+ file: lcov.info
+
+ slack:
+ name: Notify Slack Failure
+ needs: test
+ runs-on: ubuntu-latest
+ if: github.event == 'schedule'
+ steps:
+ - uses: technote-space/workflow-conclusion-action@v2
+ - uses: voxmedia/github-action-slack-notify-build@v1
+ if: env.WORKFLOW_CONCLUSION == 'failure'
+ with:
+ channel: nightly-dev
+ status: FAILED
+ color: danger
+ env:
+ SLACK_BOT_TOKEN: ${{ secrets.DEV_SLACK_BOT_TOKEN }}
+
+ docs:
+ name: Documentation
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: julia-actions/setup-julia@v1
+ with:
+ version: '1'
+ - run: |
+ git config --global user.name name
+ git config --global user.email email
+ git config --global github.user username
+ - run: |
+ julia --project=docs -e '
+ using Pkg;
+ Pkg.develop(PackageSpec(path=pwd()));
+ Pkg.instantiate();'
+ - run: |
+ julia --project=docs -e '
+ using Documenter: doctest
+ using PkgTemplates
+ doctest(PkgTemplates)'
+ - run: julia --project=docs docs/make.jl
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml
new file mode 100644
index 0000000..cba9134
--- /dev/null
+++ b/.github/workflows/CompatHelper.yml
@@ -0,0 +1,16 @@
+name: CompatHelper
+on:
+ schedule:
+ - cron: 0 0 * * *
+ workflow_dispatch:
+jobs:
+ CompatHelper:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Pkg.add("CompatHelper")
+ run: julia -e 'using Pkg; Pkg.add("CompatHelper")'
+ - name: CompatHelper.main()
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }}
+ run: julia -e 'using CompatHelper; CompatHelper.main()'
diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml
new file mode 100644
index 0000000..e72d645
--- /dev/null
+++ b/.github/workflows/TagBot.yml
@@ -0,0 +1,13 @@
+name: TagBot
+on:
+ schedule:
+ - cron: 0 0 * * *
+ workflow_dispatch:
+jobs:
+ TagBot:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: JuliaRegistries/TagBot@v1
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ ssh: ${{ secrets.DOCUMENTER_KEY }}
diff --git a/.gitignore b/.gitignore
index d0e30c2..2e02d30 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,27 @@
+# Files generated by invoking Julia with --code-coverage
*.jl.cov
*.jl.*.cov
+
+# Files generated by invoking Julia with --track-allocation
*.jl.mem
+
+# System-specific files and directories generated by the BinaryProvider and BinDeps packages
+# They contain absolute paths specific to the host computer, and so should not be committed
+deps/deps.jl
+deps/build.log
+deps/downloads/
+deps/usr/
+deps/src/
+
+# Build artifacts for creating documentation generated by the Documenter package
+docs/build/
+docs/site/
+
+# File generated by Pkg, the package manager, based on a corresponding Project.toml
+# It records a fixed state of all packages used by the project. As such, it should not be
+# committed for packages, but should be committed for applications that require a static
+# environment.
Manifest.toml
-.vscode/*
+
+# Silly macOS stuff
+.DS_Store
diff --git a/README.md b/README.md
index bcbf68c..dd4ed4a 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,17 @@
-[](https://travis-ci.org/Smaug123/ClassicalCiphers.jl) [](https://coveralls.io/github/Smaug123/ClassicalCiphers.jl?branch=master) [](https://github.com/invenia/BlueStyle)
+
+ ClassicalCiphers.jl
+
-# ClassicalCiphers
+
+[](https://username.github.io/MyCoolPackage.jl/dev)
+[](https://github.com/username/MyCoolPackage.jl/actions?query=workflow%3ACI)
+[](https://coveralls.io/github/Smaug123/ClassicalCiphers.jl?branch=master)
+[](https://github.com/invenia/BlueStyle)
## Main Features
-Provides access to encryption and decryption of strings according to a variety of classical algorithms.
+Provides access to encryption and decryption of strings according to a variety of classical algorithms. Classical ciphers were created before computers, and thus work on letters rather than bits.
+
The Solitaire cipher is included for completeness, though it is perhaps not strictly classical.
## Currently Implemented
@@ -20,6 +27,7 @@ The Solitaire cipher is included for completeness, though it is perhaps not stri
* [Solitaire]
* [Rail Fence]
* [Substitution]
+* [Atbash]
## Gotchas
@@ -398,6 +406,12 @@ julia> decrypt_atbash("HLNV GVCG", "abcdefghijklmnopqrstuvwxyz")
"some text"
```
+### Atbash
+
+```julia
+encrypt_atbash("hello this is plaintext", "abcdefghijklmnopqrstuvwxyz") == encrypt_substitution("hello this is plaintext", "abcdefghijklmnopqrstuvwxyz", "zyxwvutsrqponmlkjihgfedcba")
+```
+
[Caesar]: https://en.wikipedia.org/wiki/Caesar_cipher
[Affine]: https://en.wikipedia.org/wiki/Affine_cipher
[Vigenère]: https://en.wikipedia.org/wiki/Vigen%C3%A8re_cipher
@@ -409,3 +423,4 @@ julia> decrypt_atbash("HLNV GVCG", "abcdefghijklmnopqrstuvwxyz")
[Enigma]: https://en.wikipedia.org/wiki/Enigma_machine
[Rail Fence]: https://en.wikipedia.org/wiki/Rail_fence_cipher
[Substitution]: https://en.wikipedia.org/wiki/Substitution_cipher
+[Atbash]: https://en.wikipedia.org/wiki/Atbash
diff --git a/docs/Project.toml b/docs/Project.toml
new file mode 100644
index 0000000..34723a0
--- /dev/null
+++ b/docs/Project.toml
@@ -0,0 +1,3 @@
+[deps]
+ClassicalCiphers = "ecf26e93-935c-5e64-9b21-bc8ac81b4723"
+Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
diff --git a/docs/make.jl b/docs/make.jl
new file mode 100644
index 0000000..fd4ccb5
--- /dev/null
+++ b/docs/make.jl
@@ -0,0 +1,20 @@
+include(joinpath(dirname(@__DIR__), "src", "ClassicalCiphers.jl"))
+using Documenter, .ClassicalCiphers
+
+Documenter.makedocs(
+ clean = true,
+ doctest = true,
+ modules = Module[ClassicalCiphers],
+ repo = "",
+ highlightsig = true,
+ sitename = "ClassicalCiphers Documentation",
+ expandfirst = [],
+ pages = [
+ "Index" => "index.md",
+ "Usage" => "usage.md"
+ ]
+)
+
+deploydocs(;
+ repo = "github.com/Smaug123/ClassicalCiphers.jl.git",
+)
diff --git a/docs/src/index.md b/docs/src/index.md
new file mode 100644
index 0000000..da429ff
--- /dev/null
+++ b/docs/src/index.md
@@ -0,0 +1,41 @@
+# Home
+
+This package provides access to encryption and decryption of strings according to a variety of classical algorithms. Classical ciphers were created before computers, and thus work on letters rather than bits.
+
+The Solitaire cipher is included for completeness, though it is perhaps not strictly classical.
+
+Currently implemented ciphers:
+ - [Caesar](https://en.wikipedia.org/wiki/Caesar_cipher)
+ - [Affine](https://en.wikipedia.org/wiki/Affine_cipher)
+ - [Monoalphabetic substitution](https://en.wikipedia.org/wiki/Substitution_cipher)
+ - [Vigenère](https://en.wikipedia.org/wiki/Vigen%C3%A8re_cipher)
+ - [Portas](http://practicalcryptography.com/ciphers/porta-cipher/)
+ - [Hill](https://en.wikipedia.org/wiki/Hill_cipher)
+ - [Playfair](https://en.wikipedia.org/wiki/Playfair_cipher)
+ - [Enigma (M3 Army)](https://en.wikipedia.org/wiki/Enigma_machine)
+ - [Solitaire](https://en.wikipedia.org/wiki/Solitaire_(cipher))
+ - [Rail Fence](https://en.wikipedia.org/wiki/Rail_fence_cipher)
+ - [Substitution](https://en.wikipedia.org/wiki/Substitution_cipher)
+ - [Atbash](https://en.wikipedia.org/wiki/Atbash)
+
+
+## Contents
+```@contents
+```
+
+```@meta
+CurrentModule = ClassicalCiphers
+DocTestSetup = quote
+ using ClassicalCiphers
+end
+```
+
+## Installing ClassicalCiphers.jl
+```@repl
+using Pkg
+Pkg.add("ClassicalCiphers")
+```
+
+## Index
+```@index
+```
diff --git a/docs/src/usage.md b/docs/src/usage.md
new file mode 100644
index 0000000..401a2c7
--- /dev/null
+++ b/docs/src/usage.md
@@ -0,0 +1,5 @@
+## Usage
+
+```@autodocs
+Modules = [ClassicalCiphers]
+```
diff --git a/src/affine.jl b/src/affine.jl
index 5e6f7b9..cff2c2a 100644
--- a/src/affine.jl
+++ b/src/affine.jl
@@ -1,4 +1,8 @@
"""
+```julia
+encrypt_affine(plaintext, mult::Integer, add::Integer; offset::Integer = 0)
+```
+
Encrypts the given plaintext according to the Affine cipher.
The key is given as a pair of integers: first the multiplier, then
the additive constant.
@@ -7,10 +11,19 @@ The multiplier must be coprime to 26. If it is not, an error is thrown.
Converts the input to uppercase, but retains symbols.
-Optional argument: offset=0, which specifies what number 'a' should be
+Optional argument: `offset=0`, which specifies what number 'a' should be
considered as.
+
+---
+
+### Examples
+
+```julia
+julia> encrypt_affine("Hello, World!", 3, 4)
+"ZQLLU, SUDLN!"
+```
"""
-function encrypt_affine(plaintext, mult::T, add::T; offset::T = 0) where {T <: Integer}
+function encrypt_affine(plaintext, mult::Integer, add::Integer; offset::Integer = 0)
if mult % 2 == 0 || mult % 13 == 0
error("Multiplier must be coprime to 26.")
end
@@ -20,6 +33,10 @@ function encrypt_affine(plaintext, mult::T, add::T; offset::T = 0) where {T <: I
end
"""
+```julia
+decrypt_affine(ciphertext, mult::Integer, add::Integer; offset::Integer=0)
+```
+
Decrypts the given ciphertext according to the Affine cipher.
The key is given as a pair of integers: first the multiplier, then
the additive constant.
@@ -28,10 +45,19 @@ The multiplier must be coprime to 26. If it is not, an error is thrown.
Converts the input to lowercase, but retains symbols.
-Optional argument: offset=0, which specifies what number 'a' should be
+Optional argument: `offset=0`, which specifies what number 'a' should be
considered as.
+
+---
+
+### Examples
+
+```julia
+julia> decrypt_affine("ZQLLU, SUDLN!", 3, 4)
+"hello, world!"
+```
"""
-function decrypt_affine(ciphertext, mult::T, add::T; offset=0) where {T <: Integer}
+function decrypt_affine(ciphertext, mult::Integer, add::Integer; offset::Integer = 0)
if mult % 2 == 0 || mult % 13 == 0
error("Multiplier must be coprime to 26.")
end
@@ -61,15 +87,28 @@ function max_by(arr::AbstractArray, f::Function)
end
"""
+```julia
+crack_affine(ciphertext; mult::Integer = 0, add::Integer = -1)
+```
+
Cracks the given ciphertext according to the Affine cipher.
-Returns ((multiplier, additive constant), decrypted string).
+Returns `((multiplier, additive constant), decrypted string)`.
Converts the input to lowercase, but retains symbols.
-Optional arguments: mult=0, which specifies the multiplier if known;
-add=-1, which specifies the additive constant if known.
+Optional arguments: `mult=0`, which specifies the multiplier if known;
+`add=-1`, which specifies the additive constant if known.
+
+---
+
+### Examples
+
+```julia
+julia> crack_affine("ZQLLU, SUDLN!")
+("hello, world!", (3, 4))
+```
"""
-function crack_affine(ciphertext; mult::T = 0, add::T = -1) where {T <: Integer}
+function crack_affine(ciphertext; mult::Integer = 0, add::Integer = -1)
mults = mult != 0 ? Int[mult] : Int[i for i in filter(x -> (x % 2 != 0 && x % 13 != 0), 1:25)]
adds = add != -1 ? Int[add] : (0:25)
possible_keys = Iterators.product(mults, adds)
diff --git a/src/caesar.jl b/src/caesar.jl
index 59cdb91..86fc230 100644
--- a/src/caesar.jl
+++ b/src/caesar.jl
@@ -1,14 +1,28 @@
"""
+```julia
+encrypt_caesar(plaintext, key::Integer)
+encrypt_caesar(plaintext)
+```
+
Encrypts the given plaintext according to the Caesar cipher.
The key is given as an integer, being the offset of each character;
-so encrypt_caesar("abc", 1) == "BCD".
+so `encrypt_caesar("abc", 1) == "BCD"`.
-Converts the input to uppercase.
+Converts the input to uppercase, but retains symbols.
Traditionally, the Caesar cipher was used with a shift of 3, so this is the method it will
fall back to if only given plaintext.
+
+---
+
+### Examples
+
+```julia
+julia> encrypt_caesar("Hello, World!", 3)
+"KHOOR, ZRUOG!"
+```
"""
-function encrypt_caesar(plaintext, key::T) where {T <: Integer}
+function encrypt_caesar(plaintext, key::Integer)
# plaintext: string; key: integer offset, so k=1 sends "a" to "b"
key = ((key - 1) % 26) + 1
keystr = join(vcat(collect(Char(97 + key):'z'), collect('a':Char(97 + key - 1))))
@@ -17,16 +31,30 @@ end
encrypt_caesar(plaintext) = encrypt_caesar(plaintext, 3)
"""
+```julia
+decrypt_caesar(ciphertext, key::Integer)
+decrypt_caesar(ciphertext)
+```
+
Decrypts the given ciphertext according to the Caesar cipher.
The key is given as an integer, being the offset of each character;
-so decrypt_caesar("abcd", 1) == "zabc".
+so `decrypt_caesar("abcd", 1) == "zabc"`.
-Converts the input to lowercase.
+Converts the input to lowercase, but retains symbols.
Traditionally, the Caesar cipher was used with a shift of 3, so this is the method it will
fall back to if only given plaintext.
+
+---
+
+### Examples
+
+```julia
+julia> decrypt_caesar("Khoor, Zruog!", 3)
+"hello, world!"
+```
"""
-function decrypt_caesar(ciphertext, key::T) where {T <: Integer}
+function decrypt_caesar(ciphertext, key::Integer)
# ciphertext: string; key: integer offset, so k=1 decrypts "B" as "a"
key = ((key - 1) % 26) + 1
return lowercase(encrypt_caesar(ciphertext, 26 - key))
@@ -34,22 +62,36 @@ end
decrypt_caesar(plaintext) = decrypt_caesar(plaintext, 3)
"""
+```julia
+crack_caesar(ciphertext; cleverness::Integer = 1)
+```
+
Cracks the given ciphertext according to the Caesar cipher.
-Returns (plaintext, key::Integer), such that encrypt_caesar(plaintext, key)
+Returns `(plaintext, key::Integer)`, such that `encrypt_caesar(plaintext, key)`
would return ciphertext.
-With cleverness=0, simply does the shift that maximises e's frequency.
-With cleverness=1, maximises the string's total fitness.
+With `cleverness=0`, simply does the shift that maximises e's frequency.
+With `cleverness=1`, maximises the string's total fitness.
Converts the input to lowercase.
+
+---
+
+### Examples
+
+```julia
+julia> crack_caesar("Khoor, Zruog!")
+("hello, world!", 3)
+```
"""
-function crack_caesar(ciphertext; cleverness::T = 1) where {T <: Integer}
+function crack_caesar(ciphertext; cleverness::Integer = 1)
texts = Tuple{String, Int}[(decrypt_caesar(ciphertext,key), key) for key in 0:25]
+
if cleverness == 1
texts = sort(texts, by = (x -> string_fitness(first(x))))
else
texts = sort(texts, by = (x -> length(collect(filter(i -> (i == 'e'), first(x))))))
end
- return texts[end]
+ return last(texts)
end
diff --git a/src/common.jl b/src/common.jl
index 4d3fa8c..e50681f 100644
--- a/src/common.jl
+++ b/src/common.jl
@@ -3,7 +3,7 @@ function letters_only(text::AbstractString)
return filter(x -> ('A' <= x <= 'Z' || 'a' <= x <= 'z'), text)
end
-function rotate_right(arr::AbstractVector, n::T) where {T <: Integer}
+function rotate_right(arr::AbstractVector, n::Integer)
# implementation of the Mathematica function rotate_right - or you could try circshift()?
ans = copy(arr)
for i in 1:length(arr)
@@ -13,7 +13,7 @@ function rotate_right(arr::AbstractVector, n::T) where {T <: Integer}
return ans
end
-function rotate_left(arr::AbstractVector, n::T) where {T <: Integer}
+function rotate_left(arr::AbstractVector, n::Integer)
# implementation of the Mathematica function rotate_left
ans = copy(arr)
for i in 1:length(arr)
@@ -23,9 +23,9 @@ function rotate_left(arr::AbstractVector, n::T) where {T <: Integer}
return ans
end
-rotate_left_str(st::AbstractString, n::T) where {T <: Integer} =
+rotate_left_str(st::AbstractString, n::Integer) =
join(rotate_left(collect(st), n))
-rotate_right_str(st::AbstractString, n::T) where {T <: Integer} =
+rotate_right_str(st::AbstractString, n::Integer) =
join(rotate_right(collect(st), n))
function split_by(arr::AbstractVector, func::Function)
@@ -85,8 +85,12 @@ function string_fitness(input::AbstractString; alreadystripped::Bool = false)
end
"""
-Finds the frequencies of all characters in the input string, returning a Dict
-of 'a' => 4, for instance. Uppercase characters are considered distinct from lowercase.
+```julia
+frequencies(input::AbstractString)
+```
+
+Finds the frequencies of all characters in the input string, returning a `Dict`
+of `'a' => 4`, for instance. Uppercase characters are considered distinct from lowercase.
"""
function frequencies(input::AbstractString)
ans = Dict{Char, Int}()
@@ -102,6 +106,10 @@ function frequencies(input::AbstractString)
end
"""
+```julia
+index_of_coincidence(input::AbstractString)
+```
+
Finds the index of coincidence of the input string. Uppercase characters are considered to be
equal to their lowercase counterparts.
"""
diff --git a/src/enigma.jl b/src/enigma.jl
index 14ea115..c84fb31 100644
--- a/src/enigma.jl
+++ b/src/enigma.jl
@@ -55,30 +55,57 @@ function parse_reflector(reflector::AbstractString)
end
"""
+```julia
+function encrypt_enigma(plaintext,
+ rotors::Array{Integer, 1}, key::AbstractString;
+ reflector_id='B', ring::AbstractString = "AAA",
+ stecker = Tuple{Char, Char}[],
+ skip_stecker_check = false)
+```
+
Encrypts the given plaintext according to the Enigma (M3, army version).
-Arguments are in the order: plaintext, stecker, rotors, ring, key.
+Arguments are in the order: `plaintext, stecker, rotors, ring, key.`
Plaintext is a string; punctuation is stripped out and it is made lowercase.
-Rotors is an array - for example, [1,2,3] - being the order of the rotors.
+Rotors is an array - for example, `[1,2,3]` - being the order of the rotors.
Each entry should be a distinct integer between 1 and 5 inclusive.
Key is a string of three letters, indicating the starting positions of the rotors.
Optional:
-reflector_id='B', which sets whether to use reflector A, B or C.
+ * `reflector_id='B'`, which sets whether to use reflector A, B or C.
Can also be specified as a 26-char string.
-Stecker is either an array - for example, [('A','B'), ('D', 'E')] specifying
+ * Stecker is either an array - for example,` [('A','B'), ('D', 'E')]` specifying
that A, B are swapped and D, E are swapped - or a string ("ABDE" accomplishing
the same thing). No letter may appear more than once.
-Ring is a string - for example, "AAA" - being the offset applied to each rotor.
+ * Ring is a string - for example, "AAA" - being the offset applied to each rotor.
"AAA", for example, signifies no offset. The string must be three letters.
-skip_stecker_check=false, which when `true` skips validation of stecker settings.
+ * `skip_stecker_check=false`, which when `true` skips validation of stecker settings.
+
+---
+
+### Examples
+
+```julia
+julia> encrypt_enigma("AAA", [1,2,3], "ABC")
+"CXT"
+
+julia> encrypt_enigma("AAA", [1,2,3], "ABC", ring="AAA", reflector_id='B', stecker="") # synonymous with above
+"CXT"
+
+julia> encrypt_enigma("AAA", [1,2,3], "ABC", ring="AAA", reflector_id="YRUHQSLDPXNGOKMIEBFZCWVJAT", stecker="") # synonymous with above
+"CXT"
+
+julia> encrypt_enigma("AAA", [1,2,3], "ABC", ring="AAA", reflector_id='B', stecker=Tuple{Char, Char}[]) # synonymous with above
+"CXT"
+```
"""
function encrypt_enigma(plaintext,
rotors::Array{T, 1}, key::AbstractString;
reflector_id='B', ring::AbstractString = "AAA",
stecker = Tuple{Char, Char}[],
- skip_stecker_check = false) where {T <: Integer}
+ skip_stecker_check = false) where T <: Integer
+
parsed_stecker = parse_stecker(stecker)
# validate stecker settings
if !skip_stecker_check
@@ -243,5 +270,8 @@ function encrypt_enigma(plaintext,
return uppercase(String(take!(ans)))
end
+"""
+See `encrypt_enigma` as this function uses identical arguments.
+"""
decrypt_enigma(args1...; args2...) =
lowercase(encrypt_enigma(args1...; args2...))
diff --git a/src/hill.jl b/src/hill.jl
index 8998a8f..49f3b91 100644
--- a/src/hill.jl
+++ b/src/hill.jl
@@ -1,8 +1,12 @@
using LinearAlgebra
"""
+```julia
+encrypt_hill(plaintext::AbstractString, key::AbstractArray{Integer, 2})
+```
+
Encrypts the given plaintext according to the Hill cipher.
-The key may be given as a matrix (that is, two-dimensional Array{Int}) or as a string.
+The key may be given as a matrix (that is, two-dimensional `Array{Int}`) or as a string.
If the key is given as a string, the string is converted to uppercase before use, and
symbols are removed. It is assumed to be of square integer length, and the matrix entries
@@ -11,6 +15,21 @@ to bottom-left to bottom-right. If the string is not of square integer length, a
is thrown.
The matrix must be invertible modulo 26. If it is not, an error is thrown.
+
+---
+
+### Examples
+
+```julia
+julia> encrypt_hill("Hello, World!", [1 2; 5 7]) # Encrypt the text "Hello, World!" with a Hill key of matrix `[1 2; 5 7]`
+"PHHRGUWQRV"
+
+julia> encrypt_hill("Hello, World!", "bcfh")
+"PLHCGQWHRY"
+
+julia> encrypt_hill("Hello", "bcfh") # If the plaintext-length is not a multiple of the dimension of the key matrix, it is padded with X
+"PLHCIX"
+```
"""
function encrypt_hill(plaintext::AbstractString, key::AbstractArray{T, 2}) where {T <: Integer}
if round(Integer, det(key)) % 26 == 0
@@ -60,6 +79,10 @@ function minor(mat::AbstractArray{T, 2}, i::K, j::K) where {T <: Integer, K <: I
end
"""
+```julia
+adjugate(mat::AbstractArray{Integer, 2})
+```
+
Computes the adjugate matrix for given matrix.
"""
function adjugate(mat::AbstractArray{T, 2}) where {T <: Integer}
@@ -68,7 +91,27 @@ function adjugate(mat::AbstractArray{T, 2}) where {T <: Integer}
return Array{Integer, 2}(transpose(ans))
end
-function decrypt_hill(ciphertext, key::AbstractArray{T, 2}) where {T<:Integer}
+"""
+```julia
+decrypt_hill(ciphertext, key::AbstractArray{T, 2}) where {T <: Integer}
+```
+
+---
+
+### Examples
+
+```julia
+julia> decrypt_hill("PLHCGQWHRY", [1 2; 5 7]) # Decrypt the text "PLHCGQWHRY" with key of `[1 2; 5 7]`
+"helloworld"
+
+julia> decrypt_hill("PLHCGQWHRY", "bcfh")
+"helloworld"
+
+julia> decrypt_hill("PLHCIX", "bcfh") # If the plaintext-length is not a multiple of the dimension of the key matrix, it is padded with X
+"hellox"
+```
+"""
+function decrypt_hill(ciphertext, key::AbstractArray{T, 2}) where {T <: Integer}
if ndims(key) != 2
error("Key must be a two-dimensional matrix.")
end
diff --git a/src/monoalphabetic.jl b/src/monoalphabetic.jl
index d774b19..0671198 100644
--- a/src/monoalphabetic.jl
+++ b/src/monoalphabetic.jl
@@ -3,15 +3,31 @@ function keystr_to_dict(keystr::AbstractString)
end
"""
+```julia
+encrypt_monoalphabetic(plaintext, key::Dict{Char, Char})
+```
+
Encrypts the given plaintext according to the monoalphabetic substitution cipher.
-The key may be given as a Dict of replacements {'a' => 'b', 'c' => 'd'}, etc,
+The key may be given as a Dict of replacements `Dict('a' => 'b', 'c' => 'd')`, etc,
or as a 26-length string "keystringbcdfhjlmopqruvwxz", which is shorthand for
-{'a' => 'k', 'e' => 'b', …}
+`Dict('a' => 'k', 'e' => 'b', ...)`
If the key is given as a string, it is assumed that each character occurs only
once, and the string is converted to lowercase.
If the key is given as a Dict, the only substitutions made are those in the Dict;
in particular, the string is not converted to lowercase automatically.
+
+---
+
+### Examples
+
+```julia
+julia> encrypt_monoalphabetic("Hello, World!", "DEFGHIJKLMNOPQRSTUVWXYZABC")
+"KHOOR, ZRUOG!"
+
+julia> encrypt_monoalphabetic("aBcbDd", Dict{Char, Char}('a' => '5', 'B' => '@', 'b' => 'o'))
+"5@coDd"
+```
"""
function encrypt_monoalphabetic(plaintext, key::Dict{Char, Char})
# plaintext: string; key: dictionary of {'a' => 'b'}, etc, for replacing 'a' with 'b'
@@ -19,21 +35,34 @@ function encrypt_monoalphabetic(plaintext, key::Dict{Char, Char})
end
"""
+```julia
+decrypt_monoalphabetic(ciphertext, key::Dict{Char, Char})
+```
+
Decrypts the given ciphertext according to the monoalphabetic substitution cipher.
-The key may be given as a Dict of replacements {'a' => 'b', 'c' => 'd'}, etc,
+The key may be given as a Dict of replacements `Dict('a' => 'b', 'c' => 'd')`, etc,
or as a 26-length string "keystringbcdfhjlmopqruvwxz", which is shorthand for
-{'a' => 'k', 'e' => 'b', …}
+`Dict('a' => 'k', 'e' => 'b', ...)`
If the key is given as a string, it is assumed that each character occurs only
once, and the string is converted to lowercase.
If the key is given as a Dict, the only substitutions made are those in the Dict;
in particular, the string is not converted to lowercase automatically.
+
+---
+
+### Examples
+
+```julia
+julia> decrypt_monoalphabetic("Khoor, Zruog!", "DEFGHIJKLMNOPQRSTUVWXYZABC")
+"hello, world!"
+```
"""
function decrypt_monoalphabetic(ciphertext, key::Dict{Char, Char})
# ciphertext: string; key: dictionary of {'a' => 'b'}, etc, where the plaintext 'a' was
# replaced by ciphertext 'b'. No character should appear more than once
# as a value in {key}.
- return encrypt_monoalphabetic(ciphertext, Dict{Char, Char}([reverse(a) for a in key]))
+ return encrypt_monoalphabetic(ciphertext, Dict{Char, Char}(reverse(a) for a in key))
end
function encrypt_monoalphabetic(plaintext, key::AbstractString)
@@ -47,7 +76,7 @@ function decrypt_monoalphabetic(ciphertext, key::AbstractString)
# working in lowercase; key is assumed only to have each element appearing once
# and to be in lowercase
# so decrypt_monoalphabetic("cb", "cbade…") is "ab"
- dict = Dict{Char, Char}(a => Char(96 + findfirst(i -> i == a, lowercase(key))) for a in lowercase(key))
+ dict = Dict{Char, Char}(a => Char(96 + findfirst(==(a), lowercase(key))) for a in lowercase(key))
return encrypt_monoalphabetic(lowercase(ciphertext), dict)
end
@@ -56,6 +85,10 @@ end
# The method we use for cracking is simulated annealing.
"""
+```julia
+swap_two(str)
+```
+
swap_two(string) swaps two of the characters of the input string, at random.
The characters are guaranteed to be at different positions, though "aa" would be
'swapped' to "aa".
@@ -70,34 +103,55 @@ function swap_two(str)
end
"""
+```julia
+crack_monoalphabetic(
+ ciphertext;
+ starting_key::AbstractString = "",
+ min_temp::AbstractFloat = 0.0001,
+ temp_factor::AbstractFloat = 0.97,
+ acceptance_prob::AbstractFloat = ((e,ep,t) -> ep > e ? 1. : exp(-(e-ep)/t)),
+ chatty::Integer = 0,
+ rounds::Integer = 1
+)
+```
+
crack_monoalphabetic cracks the given ciphertext which was encrypted by the monoalphabetic
substitution cipher.
-Returns (the derived key, decrypted plaintext).
+Returns `(the derived key, decrypted plaintext)`.
-Possible arguments include:
-starting_key="", which when specified (for example, as "ABCDEFGHIJKLMNOPQRSTUVWXYZ"),
-starts the simulation at the given key. The default causes it to start with the most
-common characters being decrypted to the most common English characters.
-min_temp=0.0001, which is the temperature at which we stop the simulation.
-temp_factor=0.97, which is the factor by which the temperature decreases each step.
-chatty=0, which can be set to 1 to print whenever the key is updated, or 2 to print
-whenever any new key is considered.
-rounds=1, which sets the number of repetitions we perform. Each round starts with the
-best key we've found so far.
-acceptance_prob=((e, ep, t) -> ep>e ? 1 : exp(-(e-ep)/t)), which is the probability
-with which we accept new key of fitness ep, given that the current key has fitness e,
-at temperature t.
+The various optional arguments to `crack_monoalphabetic` are:
+* `starting_key=""`, which when specified (for example, as "ABCDEFGHIJKLMNOPQRSTUVWXYZ"),
+ starts the simulation at the given key. The default causes it to start with the most
+ common characters being decrypted to the most common English characters.
+* `min_temp=0.0001`, which is the temperature at which we stop the simulation.
+* `temp_factor=0.97`, which is the factor by which the temperature decreases each step.
+* `chatty=0`, which can be set to 1 to print whenever the key is updated, or 2 to print
+ whenever any new key is considered.
+* `rounds=1`, which sets the number of repetitions we perform. Each round starts with the
+ best key we've found so far.
+* `acceptance_prob=((e, ep, t) -> ep>e ? 1 : exp(-(e-ep)/t))`, which is the probability
+ with which we accept new key of fitness ep, given that the current key has fitness e,
+ at temperature t.
+
+---
+
+### Examples
+
+```julia
+julia> crack_monoalphabetic(str, chatty=0, rounds=10)
+(decrypted_string, key)
+```
"""
function crack_monoalphabetic(
ciphertext;
starting_key::AbstractString = "",
- min_temp::F = 0.0001,
- temp_factor::F = 0.97,
- acceptance_prob::F = ((e,ep,t) -> ep > e ? 1. : exp(-(e-ep)/t)),
- chatty::T = 0,
- rounds::T = 1
-) where {T <: Integer, F <: AbstractFloat}
+ min_temp::AbstractFloat = 0.0001,
+ temp_factor::AbstractFloat = 0.97,
+ acceptance_prob::AbstractFloat = ((e,ep,t) -> ep > e ? 1. : exp(-(e-ep)/t)),
+ chatty::Integer = 0,
+ rounds::Integer = 1
+)
if isempty(starting_key)
# most common letters
diff --git a/src/playfair.jl b/src/playfair.jl
index 94f2903..982442c 100644
--- a/src/playfair.jl
+++ b/src/playfair.jl
@@ -1,11 +1,23 @@
+"""
+```julia
AbstractPair{F, S} = Union{Tuple{F, S}, Pair{F, S}}
+```
+
+A simple wrapper for a `Pair`, or a `Tuple` representing a pair of objects.
+"""
+AbstractPair{F, S} = Union{Tuple{F, S}, Pair{F, S}}
+
parse_abstract_pair(P::AbstractPair) =
P isa Tuple{Char, Char} ? Dict(reverse(Pair(P...))) : Dict(reverse(P))
"""
+```julia
+playfair_key_to_square(key::AbstractString, replacement::AbstractPair{Char, Char})
+```
+
Converts the given key-string to a Playfair key square.
-Parameter `replacement` is a pair, such as ('I', 'J') or 'I' => 'J', containing
+Parameter `replacement` is a pair, such as `('I', 'J')` or `'I' => 'J'`, containing
the two letters which are combined. Only the first of these letters will
be present in the keysquare.
"""
@@ -32,16 +44,40 @@ function encrypt_playfair(plaintext, key::AbstractString; combined::AbstractPair
end
"""
+```julia
+encrypt_playfair(plaintext, key::Array{Char, 2}; stripped::Bool = false, combined::AbstractPair{Char, Char} = ('I', 'J'))
+```
+
Encrypts the given plaintext according to the Playfair cipher.
Throws an error if the second entry in the `combined` tuple is present in the key.
Optional parameters:
-stripped=false. When set to true, encrypt_playfair skips
+`stripped=false`. When set to true, `encrypt_playfair` skips
converting the plaintext to uppercase, removing punctuation, and
combining characters which are to be combined in the key.
-combined=('I', 'J'), marks the characters which are to be combined in the text.
- Only the first of these two may be present in the output of encrypt_playfair.
+`combined=('I', 'J')`, marks the characters which are to be combined in the text.
+ Only the first of these two may be present in the output of `encrypt_playfair`.
+
+---
+
+### Examples
+
+```julia
+julia> encrypt_playfair("Hello, World!", "playfair example")
+"DMYRANVQCRGE"
+
+julia> arr = ['P' 'L' 'A' 'Y' 'F'; 'I' 'R' 'E' 'X' 'M'; 'B' 'C' 'D' 'G' 'H'; 'K' 'N' 'O' 'Q' 'S'; 'T' 'U' 'V' 'W' 'Z'];
+
+julia> encrypt_playfair("Hello, World!", arr) # Encrypt the same text using an explicitly specified keysquare
+"DMYRANVQCRGE"
+
+julia> encrypt_playfair("IJXYZA", "PLAYFIREXM", combined=('I', 'J')) # Optionally specify the two letters which are to be combined (default 'I','J')
+"RMRMFWYE"
+
+julia> encrypt_playfair("IJXYZA", "PLAYFIREXM", combined=('X', 'Z'))
+"BSGXEY"
+```
"""
function encrypt_playfair(plaintext, key::Array{Char, 2}; stripped::Bool = false, combined::AbstractPair{Char, Char} = ('I', 'J'))
if !stripped
@@ -125,9 +161,22 @@ function decrypt_playfair(ciphertext, key::AbstractString; combined::AbstractPai
end
"""
+```julia
+decrypt_playfair(ciphertext, key::Array{Char, 2}; combined::AbstractPair{Char, Char} = ('I', 'J'))
+```
+
Decrypts the given ciphertext according to the Playfair cipher.
Does not attempt to delete X's inserted as padding for double letters.
+
+---
+
+### Examples
+
+```julia
+julia> decrypt_playfair("RMRMFWYE", "playfair example")
+"ixixyzax"
+```
"""
function decrypt_playfair(ciphertext, key::Array{Char, 2}; combined::AbstractPair{Char, Char} = ('I', 'J'))
# to obtain the decrypting keysquare, reverse every row and every column
diff --git a/src/portas.jl b/src/portas.jl
index 45b2086..ceddadc 100644
--- a/src/portas.jl
+++ b/src/portas.jl
@@ -1,9 +1,22 @@
"""
+```julia
+encrypt_portas(plaintext, key_in::AbstractString)
+```
+
Encrypts the given plaintext with the Portas cipher.
The key must be given as a string, whose characters are letters.
Converts the text to uppercase.
+
+---
+
+### Examples
+
+```julia
+julia> encrypt_portas("Hello, World!", "ab")
+"URYYB, JBEYQ!"
+```
"""
function encrypt_portas(plaintext, key_in::AbstractString)
key = uppercase(letters_only(key_in))
@@ -36,11 +49,24 @@ function encrypt_portas(plaintext, key_in::AbstractString)
end
"""
+```julia
+decrypt_portas(ciphertext, key::AbstractString)
+```
+
Decrypts the given ciphertext with the Portas cipher.
The key must be given as a string, whose characters are letters.
Converts the text to lowercase.
+
+---
+
+### Examples
+
+```julia
+julia> decrypt_portas("URYYB, JBEYQ!", "ab")
+"hello, world!"
+```
"""
function decrypt_portas(ciphertext, key::AbstractString)
return lowercase(encrypt_portas(ciphertext, key))
diff --git a/src/railfence.jl b/src/railfence.jl
index aa3713a..79d5c08 100644
--- a/src/railfence.jl
+++ b/src/railfence.jl
@@ -8,13 +8,25 @@ function construct_railfence(input, fence::AbstractArray, n_rails::Integer)
return fence
end
-"""
+@doc raw"""
```julia
construct_railfence(input::AbstractString, n_rails::Integer)
construct_railfence(input::AbstractArray{T}, n_rails::Integer) where {T <: Number}
```
-See https://en.wikipedia.org/wiki/Rail_fence_cipher.
+See [`https://en.wikipedia.org/wiki/Rail_fence_cipher`](https://en.wikipedia.org/wiki/Rail_fence_cipher).
+
+---
+
+### Examples
+
+```julia
+julia> construct_railfence("WE ARE DISCOVERED. FLEE AT ONCE", 3)
+3×26 Array{Char,2}:
+ 'W' '□' '□' '□' 'E' '□' '□' '□' 'C' '□' '□' '□' 'R' … '□' '□' 'F' '□' '□' '□' 'A' '□' '□' '□' 'C' '□'
+ '□' 'E' '□' 'R' '□' 'D' '□' 'S' '□' 'O' '□' 'E' '□' '□' '.' '□' 'L' '□' 'E' '□' 'T' '□' 'N' '□' 'E'
+ '□' '□' 'A' '□' '□' '□' 'I' '□' '□' '□' 'V' '□' '□' 'D' '□' '□' '□' 'E' '□' '□' '□' 'O' '□' '□' '□'
+```
"""
function construct_railfence(input::AbstractString, n_rails::Integer)
input = uppercase(replace(input, " " => ""))
@@ -25,23 +37,41 @@ function construct_railfence(input::AbstractArray{T}, n_rails::Integer) where {T
return construct_railfence(input, zeros(T, n_rails, length(input)), n_rails)
end
-"""
+@doc raw"""
```julia
encrypt_railfence(input::AbstractString, n_rails::Integer)
```
-See https://en.wikipedia.org/wiki/Rail_fence_cipher.
+See [`https://en.wikipedia.org/wiki/Rail_fence_cipher`](https://en.wikipedia.org/wiki/Rail_fence_cipher).
+
+---
+
+### Examples
+
+```julia
+julia> encrypt_railfence("WE ARE DISCOVERED. FLEE AT ONCE", 3) # this reads the above matrix row by row
+"WECRFACERDSOEE.LETNEAIVDEO"
+```
"""
function encrypt_railfence(input::AbstractString, n_rails::Integer)
return join(Char[c for rail in eachrow(construct_railfence(input, n_rails)) for c in rail if c != '□'])
end
-"""
+@doc raw"""
```julia
encrypt_railfence(input::AbstractString, n_rails::Integer)
```
-See https://en.wikipedia.org/wiki/Rail_fence_cipher.
+See [`https://en.wikipedia.org/wiki/Rail_fence_cipher`](https://en.wikipedia.org/wiki/Rail_fence_cipher).
+
+---
+
+### Examples
+
+```julia
+julia> decrypt_railfence("WECRFACERDSOEE.LETNEAIVDEO", 3)
+"wearediscovered.fleeatonce"
+```
"""
function decrypt_railfence(input::AbstractString, n_rails::Integer)
char_positions = Int[n for row in eachrow(construct_railfence(1:length(input), n_rails)) for n in row if n != 0]
diff --git a/src/solitaire.jl b/src/solitaire.jl
index 8c0145a..08fa13d 100644
--- a/src/solitaire.jl
+++ b/src/solitaire.jl
@@ -87,10 +87,23 @@ function SolitaireKeyStream(initialDeck::AbstractVector{T}) where {T <: Integer}
end
"""
+```julia
+encrypt_solitaire(string::AbstractString, initialDeck::AbstractVector{T}) where {T <: Integer}
+```
+
Encrypts the given plaintext according to the Solitaire cipher.
The key may be given either as a vector initial deck, where the cards are
1 through 54 (the two jokers being 53, 54), or as a string.
Schneier's keying algorithm is used to key the deck if the key is a string.
+
+---
+
+### Examples
+
+```julia
+julia> encrypt_solitaire("Hello, World!", "crypto")
+"GRNNQISRYA"
+```
"""
function encrypt_solitaire(string::AbstractString, initialDeck::AbstractVector{T}) where {T <: Integer}
inp = uppercase(letters_only(string))
@@ -103,10 +116,23 @@ function encrypt_solitaire(string::AbstractString, initialDeck::AbstractVector{T
end
"""
+```julia
+decrypt_solitaire(string::AbstractString, initialDeck::AbstractVector{T}) where {T <: Integer}
+```
+
Decrypts the given ciphertext according to the Solitaire cipher.
The key may be given either as a vector initial deck, where the cards are
1 through 54 (the two jokers being 53, 54), or as a string.
Schneier's keying algorithm is used to key the deck if the key is a string.
+
+---
+
+### Examples
+
+```julia
+julia> decrypt_solitaire("EXKYI ZSGEH UNTIQ", collect(1:54)) # as per https://www.schneier.com/code/sol-test.txt
+"aaaaaaaaaaaaaaa"
+```
"""
function decrypt_solitaire(string::AbstractString, initialDeck::AbstractVector{T}) where {T <: Integer}
inp = uppercase(letters_only(string))
diff --git a/src/substitution.jl b/src/substitution.jl
index 3c3ca7e..24e3f64 100644
--- a/src/substitution.jl
+++ b/src/substitution.jl
@@ -26,7 +26,22 @@ encrypt_substitution(plaintext, "zyxwvutsrqponmlkjihgfedcba") # this will create
As per convention, the output will always be uppercase.
-For more information, see https://en.wikipedia.org/wiki/Substitution_cipher.
+For more information, see [`https://en.wikipedia.org/wiki/Substitution_cipher`](https://en.wikipedia.org/wiki/Substitution_cipher).
+
+---
+
+### Examples
+
+```julia
+julia> encrypt_substitution("Hello, this is plaintext", "abcdefghijklmnopqrstuvwxyz", "qwertyuiopasdfghjklzxcvbnm")
+"ITSSG, ZIOL OL HSQOFZTBZ"
+
+julia> encrypt_substitution("Hello, this is plaintext", "qwertyuiopasdfghjklzxcvbnm")
+"ITSSG, ZIOL OL HSQOFZTBZ"
+
+julia> encrypt_substitution("xyz", Dict('x' => 'd', 'y' => 'e', 'z' => 't'))
+"DET"
+```
"""
encrypt_substitution(plaintext, sub_dict::Dict{T, S}) where {T, S} =
uppercase(join(something_crypt_substitution(lowercase(plaintext), sub_dict)))
@@ -58,7 +73,22 @@ decrypt_substitution(ciphertext, "zyxwvutsrqponmlkjihgfedcba"; reverse_dict = tr
As per convention, the output will always be lowercase.
-For more information, see https://en.wikipedia.org/wiki/Substitution_cipher.
+For more information, see [`https://en.wikipedia.org/wiki/Substitution_cipher`](https://en.wikipedia.org/wiki/Substitution_cipher).
+
+---
+
+### Examples
+
+```julia
+julia> decrypt_substitution("ITSSG, ZIOL OL HSQOFZTBZ", "abcdefghijklmnopqrstuvwxyz", "qwertyuiopasdfghjklzxcvbnm", reverse_dict = true)
+"hello, this is plaintext"
+
+julia> encrypt_atbash("some text", "abcdefghijklmnopqrstuvwxyz")
+"HLNV GVCG"
+
+julia> decrypt_atbash("HLNV GVCG", "abcdefghijklmnopqrstuvwxyz")
+"some text"
+```
"""
function decrypt_substitution(ciphertext, sub_dict::Dict{T, S}; reverse_dict::Bool = true) where {T, S}
sub_dict = reverse_dict ? reverse(sub_dict) : sub_dict
diff --git a/src/vigenere.jl b/src/vigenere.jl
index 7ea736d..fbbc7c7 100644
--- a/src/vigenere.jl
+++ b/src/vigenere.jl
@@ -1,51 +1,81 @@
using Statistics
"""
+```julia
+encrypt_vigenere(plaintext, key::Array)
+encrypt_vigenere(ciphertext, key::AbstractString)
+```
+
Encrypts the given string using the Vigenere cipher according to the given vector of offsets.
-For example, encrypt_vigenere("ab", [0, 1]) returns "AC".
+For example, `encrypt_vigenere("ab", [0, 1])` returns `"AC"`.
+
+---
+
+### Examples
+
+```julia
+julia> encrypt_vigenere("Hello, World!", "ab")
+"HFLMOXOSLE"
+```
"""
function encrypt_vigenere(plaintext, key::Array)
# plaintext: string; key: vector of integer offsets, so [0, 1] encrypts "ab" as "ac"
ans = String[encrypt_caesar(chr, key[(i - 1) % length(key) + 1]) for (i, chr) in enumerate(letters_only(plaintext))]
return join(ans)
end
-
-"""
-Decrypts the given string using the Vigenere cipher according to the given vector of offsets.
-For example, decrypt_vigenere("ac", [0, 1]) returns "ab".
-"""
-function decrypt_vigenere(ciphertext, key::Array)
- # ciphertext: string; key: vector of integer offsets, so [0, 1] decrypts "ac" as "ab"
- return lowercase(encrypt_vigenere(ciphertext, map(x -> 26 - x, key)))
-end
-
-"""
-Encrypts the given string using the Vigenere cipher according to the given keystring.
-For example, encrypt_vigenere("ab", "ab") returns "AC".
-"""
function encrypt_vigenere(ciphertext, key::AbstractString)
# ciphertext: string; key: string, so "ab" encrypts "ab" as "AC"
return encrypt_vigenere(ciphertext, Int[Int(i) - 97 for i in lowercase(letters_only(key))])
end
"""
-Decrypts the given string using the Vigenere cipher according to the given keystring.
-For example, decrypt_vigenere("ab", "ac") returns "ab".
+```julia
+decrypt_vigenere(ciphertext, key::Array)
+decrypt_vigenere(plaintext, key::AbstractString)
+```
+
+Decrypts the given string using the Vigenere cipher according to the given vector of offsets.
+For example, `decrypt_vigenere("ac", [0, 1])` returns `"ab"`.
+
+---
+
+### Examples
+
+```julia
+julia> decrypt_vigenere("HFLMOXOSLE", [0, 1]) # Notice that the offset `0` corresponds to the key `a`.
+"helloworld"
+```
"""
+function decrypt_vigenere(ciphertext, key::Array)
+ # ciphertext: string; key: vector of integer offsets, so [0, 1] decrypts "ac" as "ab"
+ return lowercase(encrypt_vigenere(ciphertext, map(x -> 26 - x, key)))
+end
function decrypt_vigenere(plaintext, key::AbstractString)
# plaintext: string; key: string, so "ab" decrypts "ac" as "ab"
return decrypt_vigenere(plaintext, Int[Int(i) - 97 for i in lowercase(letters_only(key))])
end
"""
+```julia
+crack_vigenere(plaintext; keylength::Integer = 0)
+```
+
Cracks the given text encrypted with the Vigenere cipher.
-Returns (derived key, decrypted plaintext).
+Returns `(derived key, decrypted plaintext)`.
Optional parameters:
-keylength=0: if the key length is known, specifying it may help the solver.
+`keylength=0`: if the key length is known, specifying it may help the solver.
If 0, the solver will attempt to derive the key length using the index
of coincidence.
+
+---
+
+### Examples
+
+```julia
+julia> crack_vigenere(str)
+```
"""
function crack_vigenere(plaintext; keylength::Integer = 0)
stripped_text = letters_only(lowercase(plaintext))