Skip to content

Commit

Permalink
crypto-square: sync
Browse files Browse the repository at this point in the history
part of #1824
  • Loading branch information
senekor committed Aug 15, 2024
1 parent bbd6958 commit b46b444
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 164 deletions.
7 changes: 0 additions & 7 deletions exercises/practice/crypto-square/.meta/Cargo-example.toml

This file was deleted.

131 changes: 27 additions & 104 deletions exercises/practice/crypto-square/.meta/example.rs
Original file line number Diff line number Diff line change
@@ -1,117 +1,40 @@
extern crate itertools;
use itertools::Itertools;

/// Encrypt the input string using square cryptography
pub fn encrypt(input: &str) -> String {
let prepared = prepare(input);
if prepared.is_empty() {
let mut input: Vec<_> = input
.to_ascii_lowercase()
.chars()
.filter(char::is_ascii_alphanumeric)
.collect();
if input.is_empty() {
return String::new();
}

let (cols, rows) = dimensions(prepared.len());

let mut output = String::with_capacity(input.len());
for chunk_iterator in SquareIndexer::new(rows, cols).chunks(cols).into_iter() {
for ch_idx in chunk_iterator {
if ch_idx < prepared.len() {
output.push(prepared[ch_idx]);
}
}
output.push(' ');
}

// we know there's one extra space at the end
output.pop();

output
}

/// Construct a vector of characters from the given input.
///
/// Constrain it to the allowed chars: lowercase ascii letters.
/// We construct a vector here because the length of the input
/// matters when constructing the output, so we need to know
/// how many input chars there are. We could treat it as a stream
/// and just stream twice, but collecting it into a vector works
/// equally well and might be a bit faster.
fn prepare(input: &str) -> Vec<char> {
let mut output = Vec::with_capacity(input.len());

output.extend(
input
.chars()
.filter(|&c| c.is_ascii() && !c.is_whitespace() && !c.is_ascii_punctuation())
.map(|c| c.to_ascii_lowercase()),
);

// add space padding to the end such that the actual string returned
// forms a perfect rectangle
let (r, c) = dimensions(output.len());
output.resize(r * c, ' ');
let width = (input.len() as f64).sqrt().ceil() as usize;
let size = width * width;

output.shrink_to_fit();

output
}

/// Get the dimensions of the appropriate bounding rectangle for this encryption
///
/// To find `(rows, cols)` such that `cols >= rows && cols - rows <= 1`, we find
/// the least square greater than or equal to the message length. Its square root
/// is the cols. If the message length is a perfect square, `rows` is the same.
/// Otherwise, it is one less.
fn dimensions(length: usize) -> (usize, usize) {
let cols = (length as f64).sqrt().ceil() as usize;
let rows = if cols * cols == length {
cols
// skip last row if already empty
let last_row = if input.len() + width > size {
width
} else {
cols - 1
width - 1
};
(rows, cols)
}

/// Iterator over the indices of the appropriate chars of the output.
///
/// For a (2, 3) (r, c) grid, yields (0, 3, 1, 4, 2, 5).
/// Does no bounds checking or space insertion: that's handled elsewhere.
#[derive(Debug)]
struct SquareIndexer {
rows: usize,
cols: usize,
cur_row: usize,
cur_col: usize,
max_value: usize,
}
// padding
input.resize(size, ' ');

impl SquareIndexer {
fn new(rows: usize, cols: usize) -> SquareIndexer {
SquareIndexer {
rows,
cols,
cur_row: 0,
cur_col: 0,
max_value: rows * cols,
}
}
}
// prevent input from being moved into closure below
let input = &input;

impl Iterator for SquareIndexer {
type Item = usize;
fn next(&mut self) -> Option<usize> {
let value = self.cur_row + (self.cur_col * self.rows);
let output = if value < self.max_value && self.cur_row < self.rows {
Some(value)
} else {
None
};
// transpose
let mut res: String = (0..width)
.flat_map(|col| {
(0..last_row)
.map(move |row| input[row * width + col])
.chain(std::iter::once(' '))
})
.collect();

// now increment internal state to next value
self.cur_col += 1;
if self.cur_col >= self.cols {
self.cur_col = 0;
self.cur_row += 1;
}
// trailing space separator
res.pop();

output
}
res
}
11 changes: 11 additions & 0 deletions exercises/practice/crypto-square/.meta/test_template.tera
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use crypto_square::*;

{% for test in cases %}
#[test]
#[ignore]
fn {{ test.description | make_ident }}() {
let actual = encrypt({{ test.input.plaintext | json_encode() }});
let expected = {{ test.expected | json_encode() }};
assert_eq!(&actual, expected);
}
{% endfor -%}
37 changes: 34 additions & 3 deletions exercises/practice/crypto-square/.meta/tests.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,34 @@
# This is an auto-generated file. Regular comments will be removed when this
# file is regenerated. Regenerating will not touch any manually added keys,
# so comments can be added in a "comment" key.
# 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.

[407c3837-9aa7-4111-ab63-ec54b58e8e9f]
description = "empty plaintext results in an empty ciphertext"

[aad04a25-b8bb-4304-888b-581bea8e0040]
description = "normalization results in empty plaintext"

[64131d65-6fd9-4f58-bdd8-4a2370fb481d]
description = "Lowercase"

[63a4b0ed-1e3c-41ea-a999-f6f26ba447d6]
description = "Remove spaces"

[1b5348a1-7893-44c1-8197-42d48d18756c]
description = "Remove punctuation"

[8574a1d3-4a08-4cec-a7c7-de93a164f41a]
description = "9 character plaintext results in 3 chunks of 3 characters"

[a65d3fa1-9e09-43f9-bcec-7a672aec3eae]
description = "8 character plaintext results in 3 chunks, the last one with a trailing space"

[fbcb0c6d-4c39-4a31-83f6-c473baa6af80]
description = "54 character plaintext results in 7 chunks, the last two with trailing spaces"
85 changes: 35 additions & 50 deletions exercises/practice/crypto-square/tests/crypto-square.rs
Original file line number Diff line number Diff line change
@@ -1,79 +1,64 @@
use crypto_square::encrypt;
use crypto_square::*;

fn test(input: &str, output: &str) {
assert_eq!(&encrypt(input), output);
#[test]
fn empty_plaintext_results_in_an_empty_ciphertext() {
let actual = encrypt("");
let expected = "";
assert_eq!(&actual, expected);
}

#[test]
fn empty_input() {
test("", "")
#[ignore]
fn normalization_results_in_empty_plaintext() {
let actual = encrypt("... --- ...");
let expected = "";
assert_eq!(&actual, expected);
}

#[test]
#[ignore]
fn encrypt_also_decrypts_square() {
// note that you only get the exact input back if:
// 1. no punctuation
// 2. even spacing
// 3. all lowercase
// 4. square input
let example = "lime anda coco anut";
assert_eq!(example, &encrypt(&encrypt(example)));
fn lowercase() {
let actual = encrypt("A");
let expected = "a";
assert_eq!(&actual, expected);
}

#[test]
#[ignore]
fn example() {
test(
"If man was meant to stay on the ground, god would have given us roots.",
"imtgdvs fearwer mayoogo anouuio ntnnlvt wttddes aohghn sseoau ",
)
fn remove_spaces() {
let actual = encrypt(" b ");
let expected = "b";
assert_eq!(&actual, expected);
}

#[test]
#[ignore]
fn empty_last_line() {
test("congratulate", "crl oaa ntt gue")
fn remove_punctuation() {
let actual = encrypt("@1,%!");
let expected = "1";
assert_eq!(&actual, expected);
}

#[test]
#[ignore]
fn spaces_are_reorganized() {
test("abet", "ae bt");
test("a bet", "ae bt");
test(" a b e t ", "ae bt");
fn test_9_character_plaintext_results_in_3_chunks_of_3_characters() {
let actual = encrypt("This is fun!");
let expected = "tsf hiu isn";
assert_eq!(&actual, expected);
}

#[test]
#[ignore]
fn everything_becomes_lowercase() {
test("caSe", "cs ae");
test("cAsE", "cs ae");
test("CASE", "cs ae");
fn test_8_character_plaintext_results_in_3_chunks_the_last_one_with_a_trailing_space() {
let actual = encrypt("Chill out.");
let expected = "clu hlt io ";
assert_eq!(&actual, expected);
}

#[test]
#[ignore]
fn long() {
test(
r#"
We choose to go to the moon.
We choose to go to the moon in this decade and do the other things,
not because they are easy, but because they are hard, because that
goal will serve to organize and measure the best of our energies and
skills, because that challenge is one that we are willing to accept,
one we are unwilling to postpone, and one which we intend to win,
and the others, too.
-- John F. Kennedy, 12 September 1962
"#,
&(String::from("womdbudlmecsgwdwob enooetbsenaotioihe ")
+ "cwotcbeeaeunolnnnr henhaecrsrsealeaf1 ocieucavugetciwnk9 "
+ "ohnosauerithcnhde6 sotteusteehaegitn2 eohhtseotsatptchn "
+ "tsiehetohatwtohee oesrethrenceopwod gtdtyhagbdhanoety "
+ "ooehaetaesaresih1 tgcirygnsklewtne2 ooaneaoitilweptrs "
+ "ttdgerazoleiaoese hoesaeleflnlrnntp etanshwaosgleedot "
+ "mhnoyainubeiuatoe oedtbrldreinnnojm "),
)
fn test_54_character_plaintext_results_in_7_chunks_the_last_two_with_trailing_spaces() {
let actual = encrypt("If man was meant to stay on the ground, god would have given us roots.");
let expected = "imtgdvs fearwer mayoogo anouuio ntnnlvt wttddes aohghn sseoau ";
assert_eq!(&actual, expected);
}

0 comments on commit b46b444

Please sign in to comment.