Skip to content

Added "affine-cipher" exercise #973

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

Merged
merged 10 commits into from
May 13, 2025
Merged
16 changes: 16 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,22 @@
],
"difficulty": 4
},
{
"slug": "affine-cipher",
"name": "Affine Cipher",
"uuid": "61374c16-0a99-4616-9244-a38919eafbe1",
"practices": [
"math",
"exceptions"
],
"prerequisites": [
"functions",
"comparisons",
"loops",
"booleans"
],
"difficulty": 6
},
{
"slug": "alphametics",
"name": "Alphametics",
Expand Down
74 changes: 74 additions & 0 deletions exercises/practice/affine-cipher/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Instructions

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.
Although all monoalphabetic ciphers are weak, the affine cipher is much stronger than the Atbash cipher, because it has many more keys.

[//]: # " monoalphabetic as spelled by Merriam-Webster, compare to polyalphabetic "

## Encryption

The encryption function is:

```text
E(x) = (ai + b) mod m
```

Where:

- `i` is the letter's index from `0` to the length of the alphabet - 1.
- `m` is the length of the alphabet.
For the Latin alphabet `m` is `26`.
- `a` and `b` are integers which make up the encryption key.

Values `a` and `m` must be _coprime_ (or, _relatively prime_) for automatic decryption to succeed, i.e., they have number `1` as their only common factor (more information can be found in the [Wikipedia article about coprime integers][coprime-integers]).
In case `a` is not coprime to `m`, your program should indicate that this is an error.
Otherwise it should encrypt or decrypt with the provided key.

For the purpose of this exercise, digits are valid input but they are not encrypted.
Spaces and punctuation characters are excluded.
Ciphertext is written out in groups of fixed length separated by space, the traditional group size being `5` letters.
This is to make it harder to guess encrypted text based on word boundaries.

## Decryption

The decryption function is:

```text
D(y) = (a^-1)(y - b) mod m
```

Where:

- `y` is the numeric value of an encrypted letter, i.e., `y = E(x)`
- it is important to note that `a^-1` is the modular multiplicative inverse (MMI) of `a mod m`
- the modular multiplicative inverse only exists if `a` and `m` are coprime.

The MMI of `a` is `x` such that the remainder after dividing `ax` by `m` is `1`:

```text
ax mod m = 1
```

More information regarding how to find a Modular Multiplicative Inverse and what it means can be found in the [related Wikipedia article][mmi].

## General Examples

- Encrypting `"test"` gives `"ybty"` with the key `a = 5`, `b = 7`
- Decrypting `"ybty"` gives `"test"` with the key `a = 5`, `b = 7`
- Decrypting `"ybty"` gives `"lqul"` with the wrong key `a = 11`, `b = 7`
- Decrypting `"kqlfd jzvgy tpaet icdhm rtwly kqlon ubstx"` gives `"thequickbrownfoxjumpsoverthelazydog"` with the key `a = 19`, `b = 13`
- Encrypting `"test"` with the key `a = 18`, `b = 13` is an error because `18` and `26` are not coprime

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

Finding MMI for `a = 15`:

- `(15 * x) mod 26 = 1`
- `(15 * 7) mod 26 = 1`, ie. `105 mod 26 = 1`
- `7` is the MMI of `15 mod 26`

[mmi]: https://en.wikipedia.org/wiki/Modular_multiplicative_inverse
[coprime-integers]: https://en.wikipedia.org/wiki/Coprime_integers
21 changes: 21 additions & 0 deletions exercises/practice/affine-cipher/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"authors": [
"marcelweikum"
],
"files": {
"solution": [
"affine_cipher.cpp",
"affine_cipher.h"
],
"test": [
"affine_cipher_test.cpp"
],
"example": [
".meta/example.cpp",
".meta/example.h"
]
},
"blurb": "Create an implementation of the Affine cipher, an ancient encryption algorithm from the Middle East.",
"source": "Wikipedia",
"source_url": "https://en.wikipedia.org/wiki/Affine_cipher"
}
65 changes: 65 additions & 0 deletions exercises/practice/affine-cipher/.meta/example.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#include <cctype>
#include <stdexcept>

#include "affine_cipher.h"

namespace affine_cipher {

static int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); }

static int mod(int x, int m) {
int r = x % m;
return r < 0 ? r + m : r;
}

static int modinv(int a, int m) {
int t = 0, newt = 1;
int r = m, newr = a;
while (newr != 0) {
int q = r / newr;
t -= q * newt;
std::swap(t, newt);
r -= q * newr;
std::swap(r, newr);
}
if (r != 1) throw std::invalid_argument("a and m must be coprime.");
return t < 0 ? t + m : t;
}

std::string encode(const std::string& text, int a, int b) {
const int m = 26;
if (gcd(a, m) != 1) throw std::invalid_argument("a and m must be coprime.");
std::string out;
out.reserve(text.size());
for (char ch : text) {
if (std::isalpha(ch)) {
int x = std::tolower(ch) - 'a';
out += char('a' + mod(a * x + b, m));
} else if (std::isdigit(ch)) {
out += ch;
}
}
for (int i = 5; i < (int)out.size(); i += 6) {
out.insert(out.begin() + i, ' ');
}
return out;
}

std::string decode(const std::string& text, int a, int b) {
const int m = 26;
if (gcd(a, m) != 1) throw std::invalid_argument("a and m must be coprime.");
int inv = modinv(a, m);
std::string out;
out.reserve(text.size());
for (char ch : text) {
if (std::isalpha(ch)) {
int y = std::tolower(ch) - 'a';
out += char('a' + mod(inv * (y - b), m));
} else if (std::isdigit(ch)) {
out += ch;
}
}
return out;
}

} // namespace affine_cipher
10 changes: 10 additions & 0 deletions exercises/practice/affine-cipher/.meta/example.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#pragma once

#include <string>

namespace affine_cipher {

std::string encode(const std::string& input, int a, int b);
std::string decode(const std::string& input, int a, int b);

} // namespace affine_cipher
58 changes: 58 additions & 0 deletions exercises/practice/affine-cipher/.meta/tests.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# This is an auto-generated file.
#
# Regenerating this file via `configlet sync` will:
# - Recreate every `description` key/value pair
# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications
# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion)
# - Preserve any other key/value pair
#
# As user-added comments (using the # character) will be removed when this file
# is regenerated, comments can be added via a `comment` key.

[2ee1d9af-1c43-416c-b41b-cefd7d4d2b2a]
description = "encode -> encode yes"

[785bade9-e98b-4d4f-a5b0-087ba3d7de4b]
description = "encode -> encode no"

[2854851c-48fb-40d8-9bf6-8f192ed25054]
description = "encode -> encode OMG"

[bc0c1244-b544-49dd-9777-13a770be1bad]
description = "encode -> encode O M G"

[381a1a20-b74a-46ce-9277-3778625c9e27]
description = "encode -> encode mindblowingly"

[6686f4e2-753b-47d4-9715-876fdc59029d]
description = "encode -> encode numbers"

[ae23d5bd-30a8-44b6-afbe-23c8c0c7faa3]
description = "encode -> encode deep thought"

[c93a8a4d-426c-42ef-9610-76ded6f7ef57]
description = "encode -> encode all the letters"

[0673638a-4375-40bd-871c-fb6a2c28effb]
description = "encode -> encode with a not coprime to m"

[3f0ac7e2-ec0e-4a79-949e-95e414953438]
description = "decode -> decode exercism"

[241ee64d-5a47-4092-a5d7-7939d259e077]
description = "decode -> decode a sentence"

[33fb16a1-765a-496f-907f-12e644837f5e]
description = "decode -> decode numbers"

[20bc9dce-c5ec-4db6-a3f1-845c776bcbf7]
description = "decode -> decode all the letters"

[623e78c0-922d-49c5-8702-227a3e8eaf81]
description = "decode -> decode with no spaces in input"

[58fd5c2a-1fd9-4563-a80a-71cff200f26f]
description = "decode -> decode with too many spaces"

[b004626f-c186-4af9-a3f4-58f74cdb86d5]
description = "decode -> decode with a not coprime to m"
67 changes: 67 additions & 0 deletions exercises/practice/affine-cipher/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Get the exercise name from the current directory
get_filename_component(exercise ${CMAKE_CURRENT_SOURCE_DIR} NAME)

# Basic CMake project
cmake_minimum_required(VERSION 3.5.1)

# Name the project after the exercise
project(${exercise} CXX)

# Get a source filename from the exercise name by replacing -'s with _'s
string(REPLACE "-" "_" file ${exercise})

# Implementation could be only a header
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${file}.cpp)
set(exercise_cpp ${file}.cpp)
else()
set(exercise_cpp "")
endif()

# Use the common Catch library?
if(EXERCISM_COMMON_CATCH)
# For Exercism track development only
add_executable(${exercise} ${file}_test.cpp ${exercise_cpp} ${file}.h $<TARGET_OBJECTS:catchlib>)
elseif(EXERCISM_TEST_SUITE)
# The Exercism test suite is being run, the Docker image already
# includes a pre-built version of Catch.
find_package(Catch2 REQUIRED)
add_executable(${exercise} ${file}_test.cpp ${exercise_cpp} ${file}.h)
target_link_libraries(${exercise} PRIVATE Catch2::Catch2WithMain)
# When Catch is installed system wide we need to include a different
# header, we need this define to use the correct one.
target_compile_definitions(${exercise} PRIVATE EXERCISM_TEST_SUITE)
else()
# Build executable from sources and headers
add_executable(${exercise} ${file}_test.cpp ${exercise_cpp} ${file}.h test/tests-main.cpp)
endif()

set_target_properties(${exercise} PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED OFF
CXX_EXTENSIONS OFF
)

set(CMAKE_BUILD_TYPE Debug)

if("${CMAKE_CXX_COMPILER_ID}" MATCHES "(GNU|Clang)")
set_target_properties(${exercise} PROPERTIES
COMPILE_FLAGS "-Wall -Wextra -Wpedantic -Werror"
)
endif()

# Configure to run all the tests?
if(${EXERCISM_RUN_ALL_TESTS})
target_compile_definitions(${exercise} PRIVATE EXERCISM_RUN_ALL_TESTS)
endif()

# Tell MSVC not to warn us about unchecked iterators in debug builds
# Treat warnings as errors
# Treat type conversion warnings C4244 and C4267 as level 4 warnings, i.e. ignore them in level 3
if(${MSVC})
set_target_properties(${exercise} PROPERTIES
COMPILE_DEFINITIONS_DEBUG _SCL_SECURE_NO_WARNINGS
COMPILE_FLAGS "/WX /w44244 /w44267")
endif()

# Run the tests on every build
add_custom_target(test_${exercise} ALL DEPENDS ${exercise} COMMAND ${exercise})
7 changes: 7 additions & 0 deletions exercises/practice/affine-cipher/affine_cipher.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#include "affine_cipher.h"

namespace affine_cipher {

// TODO: add your solution here

} // namespace affine_cipher
10 changes: 10 additions & 0 deletions exercises/practice/affine-cipher/affine_cipher.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#ifndef AFFINE_CIPHER_H
#define AFFINE_CIPHER_H

namespace affine_cipher {

// TODO: add your solution here

} // namespace affine_cipher

#endif // AFFINE_CIPHER_H
Loading