Text scrambler

Text scrambler

November 14, 2020 5 minutes reading time Development javascript rust

What it does

If you re-arrange the characters in a word, but leave the first and last character in place, your brain can still make sense of this nonsense most of the time. Why? Researchers aren’t entirely sure, but they have some suspicions. They think part of the reason the result is still readable is because our brains are able to use context to make predictions about what’s to come.

This post lets you interactively examine if this works for you. Try it out for yourself.

Test it online!

Type a text into the text area or just copy and paste some text. Hit the scramble button and try to read the result.

Scrambled text will appear here.

What is a word?

In order to scramble a word in the way mentioned above, we first have to agree on what a word is. For the purpose of this post, I’ve decided to define a word as a series of characters from the German alphabet. Lower case and upper case characters are allowed. If you want to define your own alphabet, just change this variable.

// English and German only
const alphabet =
  'abcdefghijklmnopqrstuvwxyzäöüß' +
  'ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÜẞ';

Create a random permutation of letters

This function re-arranges the elements of array a by constructing a new one. It does this by picking a random element from the array and then deleting it, until there are no more elements left.

const getRandomPermutation = (a) => {
  const permutation = [];
  while (a.length > 0) {
    const randomIndex =
      Math.floor(Math.random() * Math.floor(a.length));
    const element = a[randomIndex];
    permutation.push(element);
    a.splice(randomIndex, 1);
  }
  return permutation;
};

Scramble an entire text

This function scrambles a text by iterating over each character. If the character is part of our alphabet, it’s collected in the word variable. Otherwise, the character is put into the result array that will become our result.

Whenever we handle a character that is not part of our alphabet, we see if we have collected enough characters before that we can now re-arrange. If that’s not the case, we just put all characters from the word variable into result. To make sure we do that at the end of our text, we simply append a non-alphabet character at the end of the text that is thrown away afterwards. I’ve chosen the $ characters for this.

If we have enough characters (we need more than 3), we leave the first character word[0] and the last character word[word.length - 1] in place and create a random permutation from the characters between them. The result is transferred to the result variable.

We then have to empty the word variable to be ready for the next word.

const scrambleText = (text) => {
  const result = [];
  let word = [];
  // make sure that text ends with a non-alphabet ($) character,
  // because this activates the else case and clears the word
  (text + '$').split('').forEach((c) => {
    if (alphabet.includes(c)) {
      // store alphabet characters in word
      word.push(c)
    } else {
      // handle word first
      if (word.length > 3) {
        // all word characters except first and last one
        const middle = word.slice(1, word.length - 1);
        result.push(word[0]);
        result.push(...getRandomPermutation(middle));
        result.push(word[word.length - 1]);
      } else {
        result.push(...word);
      }
      // reset word
      word = [];
      // push non-alphabet character to result
      result.push(c)
    }
  });
  // remove trailing $ character
  result.pop();
  return result.join('');
};

Put it together

Put the program together and run it by passing a text to the scrambleText function.

const text =
  'A text is still readable if you re-arrange the letters of words, ...';
console.log(scrambleText(text));

You might get an output similar to this:

A txet is siltl rbaladee if you re-ararnge the ltetres of wodrs, ...

Alternative Rust version

The same code translated into Rust doesn’t look much different. For permutation of the middle of a word I have used the rand crate. Note that I iterate over the characters of the text which will not work for all unicode characters. You can fix this by using the unicode-segmentation crate.

use rand::seq::SliceRandom;
use rand::thread_rng;

// Use type alias to make source code more readable
type Text = Vec::<char>;

// Scrambles a word (reference to vector of char) in place and returns it
fn scramble_word(word: &mut Text) -> &mut Text {
    let len = word.len();
    if len > 3 {
        word[1..len - 1].shuffle(&mut thread_rng());
    }
    word
}

// Takes an alphabet and a string slice and scrambles it, returns a String
fn scramble_text(a: &str, t: &str) -> String {
    let alphabet = a.chars().collect::<Text>();
    let mut text = t.chars().collect::<Text>();

    // Make sure that text ends with a non-alphabet ($) character,
    // because this activates the else case and clears the word
    text.push('$');

    // Tuple of word (component 0) and result (component 1)
    let mut r = text.iter()
        .fold((Text::new(), Text::new()), |mut acc, &c| {
            if alphabet.contains(&c) {
                acc.0.push(c);
            } else {
                acc.1.append(scramble_word(&mut acc.0));
                // Reset word
                acc.0 = Text::new();
                // Push non-alphabet character to result
                acc.1.push(c);
            }
            acc
        });

    // Remove trailing terminal character
    r.1.pop();
    r.1.iter().collect::<String>()
}

fn main() {
    let alphabet =
        "abcdefghijklmnopqrstuvwxyzäöüßABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÜẞ";
    let text =
        "A text is still readable if you re-arrange the letters of words, ...";

    println!("{}", scramble_text(alphabet, text));
}

While translating the source code from JavaScript to Rust, I experimented with the scan function from the iterator trait. It promised more flexibility, but it’s likely not a suitable option.