Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Affin cipher fix #125

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,19 @@
"control_flow_conditionals",
"text_formatting"
]
},
{
"slug": "affine-cipher",
"uuid": "9bc3f040-9e4b-4ed0-b23c-3ae565e83a59",
"core": false,
"unlocked_by": "rotational-cipher",
"difficulty": 4,
"topics": [
"control_flow_conditionals",
"math",
"recursion",
"algorithms"
]
}
]
}
124 changes: 124 additions & 0 deletions exercises/affine-cipher/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Affine Cipher

Create an implementation of the affine cipher,
an ancient encryption system created in the Middle East.

The affine cipher is a type of monoalphabetic substitution cipher.
Each character is mapped to its numeric equivalent, encrypted with
a mathematical function and then converted to the letter relating to
its new numeric value.

To make an affine cipher, you must create two main functions:

1. `encrypt(plaintext, a, b)`: A function to encrpt a plaintext/message with keys `a` and `b`
2. `decrypt(encryption, a, b)`: A function to decrypt an encrypted plaintext/message with keys `a` and `b`

In order to help to break down the problem into smaller pieces, you must create 4 utility functions.

1. `normalise(text)`: A function to remove whitepsace in text and change all letter cases to lower case.
2. `lookupindex(normalisedtext)`: A function to return an index of a letter from a lookup table.
3. `gcd(x,y)`: A function to compute the greatest common divisor of two numbers.
4. `mmi(a,m)`: A function to compute the modulo multiplicative inverse `a mod m`.

Note: These utility functions are not required to pass the test.

## Main Algorithm

The algorithm is as below:

1. the encryption function is:

`E(x) = (ax + b) mod m`
* where `x` is the letter's index from 0 to (length of alphabet - 1)
* `m` is the length of the alphabet. For the Roman alphabet `m == 26`.
* and `a` and `b` make the key

Alphabet | x
a | 0 |
b | 1 |
c | 2 |
d | 3 |
e | 4 |
f | 5 |
etc. | etc.

2. the decryption function is:

`D(y) = a^-1(y - b) mod m`
* where `y` is the encrypted letter's index from 0 to (length of alphabet - 1)
* it is important to note that `a^-1` is the modular multiplicative inverse
of `a mod m`
* the modular multiplicative inverse of `a` only exists if `a` and `m` are
coprime.

Alphabet | y
a | 0 |
b | 1 |
c | 2 |
d | 3 |
e | 4 |
f | 5 |
etc. | etc.

3. To find the GCD of two numbers:

The easiest way to execute this is to compute the Euclidean Algorithm. The benefits of using the Euclidean algorithm is that it is commutative, i.e. `gcd(x,y) = gcd(y,x)`.

4. To find the MMI of `a`:

`an mod m = 1`, where `n` is the modular multiplicative inverse of `a mod m`

## Caveats

You must also check for the following:
* `a` is coprime with `m`, otherwise, an error is returned. The statement `a` and `m` are coprime means that the greatest common divisor of `a` and `m` equals 1.
* all messages are normalised (white space is removed and all letters converted to lowercase) before applying the encrypt algorithm
* all encryptions are normalised (white space is removed and all letters converted to lowercase) before applying the decrypt algorithm

## Examples of Main Algorithm

* Encoding `test` gives `ybty` with the key a=5 b=7
* Decoding `ybty` gives `test` with the key a=5 b=7
* Decoding `ybty` gives `lqul` with the wrong key a=11 b=7
* Decoding `kqlfd jzvgy tpaet icdhm rtwly kqlon ubstx`
- gives `thequickbrownfoxjumpsoverthelazydog` with the key a=19 b=13
* Encoding `test` with the key a=18 b=13
- gives `Error: a and m must be coprime.`

### Examples of finding a Greatest Common Divisor (GCD)

* The greatest common divisor of two prime numbers is 1
* The greatest common divisor of 8 and 24 is 8
* The greatest common divisor of 60 and 9 is 3

### Examples of finding a Modular Multiplicative Inverse (MMI)

1. simple example:
- `9 mod 26 = 9`
- `9 * 3 mod 26 = 27 mod 26 = 1`
- `3` is the MMI of `9 mod 26`
2. a more complicated example:
- `15 mod 26 = 15`
- `15 * 7 mod 26 = 105 mod 26 = 1`
- `7` is the MMI of `15 mod 26`


## Installation
See [this guide](https://exercism.io/tracks/r/installation) for instructions on how to setup your local R environment.

## How to implement your solution
In each problem folder, there is a file named `<exercise_name>.R` containing a function that returns a `NULL` value. Place your implementation inside the body of the function.

## How to run tests

Inside of RStudio, simply execute the `test_<exercise_name>.R` script. This can be conveniently done with [testthat's `auto_test` function](https://www.rdocumentation.org/packages/testthat/topics/auto_test). Because Exercism code and tests are in the same folder, use this same path for both `code_path` and `test_path` parameters. On the command-line, you can also run `Rscript test_<exercise_name>.R`.


## Source

1. [Lecture Notes](http://pi.math.cornell.edu/~kozdron/Teaching/Cornell/135Summer06/Handouts/affine.pdf)
2. [Wikipedia](https://en.wikipedia.org/wiki/Modular_multiplicative_inverse)
3. [Modular Multiplicative Inverse](https://en.wikipedia.org/wiki/Modular_multiplicative_inverse)

## Submitting Incomplete Solutions
It's possible to submit an incomplete solution so you can see how others have completed the exercise.
23 changes: 23 additions & 0 deletions exercises/affine-cipher/affine-cipher.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
normalise <- function(text) {

}

lookupindex <- function(normalisedtext) {

}

gcd <- function(x, y) {

}

mmi <- function(a, m) {

}

encrypt <- function(plaintext, a, b) {

}

decrypt <- function(encryption, a, b) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does encryption mean the ciphertext here? If yes, please consider renaming it. message -> plaintext might also be a good idea then, because those are the domain terms.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Agreed.


}
49 changes: 49 additions & 0 deletions exercises/affine-cipher/example.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
normalise <- function(text) {
return(tolower(gsub(" ", "", text)))
}

lookupindex <- function(normalisedtext) {
letterslist <- strsplit(normalisedtext, "")[[1]]
return(match(letterslist, letters) - 1)
}

gcd <- function(x, y) {
r <- x %% y
return(ifelse(r, gcd(y, r), y))
}

mmi <- function(a, m) {
a <- a %% m
for (x in 1:m) {
if ((a * x) %% m == 1) {
return(x)
}
}
return(1)
}

encrypt <- function(plaintext, a, b) {
m <- 26

if (gcd(a, m) != 1) {
stop(paste("a=", a, " and m=", m, "is coprime"))
}

normalisedplaintext <- normalise(plaintext)
x <- lookupindex(normalisedplaintext)

return(paste(letters[ ((a * x + b) %% m) + 1], collapse = ""))
}

decrypt <- function(encryption, a, b) {
m <- 26

if (gcd(a, m) != 1) {
stop("a and 26 must be co-prime")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


normalisedencryption <- normalise(encryption)
y <- lookupindex(normalisedencryption)

return(paste(letters[((mmi(a, m) * (y - b)) %% m) + 1], collapse = ""))
}
40 changes: 40 additions & 0 deletions exercises/affine-cipher/test_affine-cipher.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
source("./affine-cipher.R")
library(testthat)

test_that("encrypt() returns correct string", {
expect_identical(encrypt("test", 5, 7), "ybty")
})

test_that("encrypt() accounts for whitespace", {
expect_identical(encrypt("te st ", 5, 7), "ybty")
})

test_that("encrypt() accounts for case-sensitivity", {
expect_identical(encrypt("TeST", 5, 7), "ybty")
})

test_that("encrypt() checks that a is coprime with m", {
expect_error(encrypt("jknkasd", 18, 13))
})

test_that("decrypt() returns correct string", {
expect_identical(decrypt("ybty", 5, 7), "test")
expect_identical(
decrypt("kqlfd jzvgy tpaet icdhm rtwly kqlon ubstx", 19, 13),
"thequickbrownfoxjumpsoverthelazydog"
)
})

test_that("decrypt() accounts for whitespace", {
expect_identical(decrypt(" ybt y", 5, 7), "test")
expect_identical(
decrypt("kqlfd jzvgy tpaet icdhm rtwly kqlon ubstx", 19, 13),
"thequickbrownfoxjumpsoverthelazydog"
)
})

test_that("decrypt() checks that a is coprime with m", {
expect_error(decrypt("jknkasd", 18, 13))
})

message("All tests passed for exercise: affine-cipher")