Programming Lewis Carroll's Memoria Technica (2024) Charles Dodgson, better known as Lewis Carroll, devised a cipher called Memoria Technica to help him remember numbers by encoding digits as consonants within memorable phrases, as described in a 1878 pamphlet. The article presents online encoding and decoding tools and details a TypeScript implementation of the cipher, which maps each digit to two possible consonants and ignores vowels, 'y', and punctuation. The authors note the cipher's relevance to steganography, as it allows hidden numeric data to be embedded in seemingly innocuous text. Programming Lewis Carroll's Memoria Technica Charles Dodgson pen name Lewis Carroll had difficulty remembering numbers, such as dates. He developed a cipher to help him remember numbers by embedding them in couplets or phrases. For example, the couplet “Brass trumpet and brazen bassoon, will speedily mark you a tune” encodes the specific gravity of brass 8.39 in the last four consonants: r k t n y is treated as a vowel . In this article, we describe the cipher, present online tools for encoding and decoding, discuss how we implemented the algorithms in TypeScript, and the cipher’s relevancy to steganography. Memoria Technica The cipher encodes plaintext’s of sequences of digits. Using the cipher, consonants except ‘y’ are mapped to digits. Vowels, punctuation, and ‘y’ are ignored and thus can be freely added to the ciphertext The mapping follows the figure: In a three-page pamphlet published by Carroll in 1878, he describes how the digits and their associated letters came to be: - "b" and "c", the first two consonants in the Alphabet. - "d" from "duo"; "w" from "two". - "t" from "tres"; the other may wait awhile. - "f" from four; "q" from "quatuor". - "l" and "v", because "L" and "V" are the Roman symbols for "fifty" and "five". - "s" and "x", from "six". - "p" and "m", from "septem". - "h" from "huit"; and "k" from the Greek "okto". - "n" from "nine"; and "g" because it is so like "9". - "z" and "r", from "zero". The letter “j” is associated with 3 because it is left-over. Decoding ciphertext involves extracting the consonants from a message and finding the mapped integer. Vowels, y, and punctuation are ignored. Encoding an integer involves choosing one of two consonants for each digit and inserting vowels, y, and punctuation to create valid words. Decoding may also involve “meta” information. In the pamphlet, Carroll provides example couplets for encoding the specific gravities of various metals. The decoder will need to know that only the last four consonants are relevant. Example Given the ciphertext found , we can first filter to the consonants: fnd . f is 4, n is 9, and d is 2, so the plaintext is 492 . What was found in 492? Christopher Columbus discovered America in 1492 and Dodgson believed the leading 1 could be assumed. Given the plaintext 492 , we are interested in words that include the consonants f or q, n or g, and d or w, in that order, with vowels, ‘y’, or punctuation in-between. Using the second edition Scrabble dictionary, we find 15 single words that match these constraints: | fend | fiend | find | fined | foined | | fond | fondu | fondue | found | fugued | | fund | fundi | queened | quinoid | quoined | Using our encoding tool, we found 74,151 possibilities from fend to quey yuga yowie . Decoding and Encoding Tools Note 1: These computations run on your computer. Note 2: The JavaScript code for Encode uses import attributes which, as of December 2024, are not supported on Firefox, but are expected to be available soon. Note 3: Negative limits are treated as no limits i.e. Infinity . Negative offsets are treated as no offset i.e. zero . Implementation Mapping Consonants to Digits Our implementation of the mapping is a fairly straight-forward switch case with fall-through: | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | / For a given letter, return the cipher digit based on Carroll's Memoria Technica scheme, or null if there is no associated digit. This function will return null for vowels including y , punctuation marks, and any other non-consonants. Only single letter English consonants have values; case is ignored. @param chr a single character / export function consonant value chr: string : number | null { const code16: number | undefined = chr.toLowerCase .codePointAt 0 ; switch code16 { case 98 / 'b' /: case 99 / 'c' /: return 1; case 100 / 'd' /: case 119 / 'w' /: return 2; case 116 / 't' /: case 106 / 'j' /: return 3; case 102 / 'f' /: case 113 / 'q' /: return 4; case 108 / 'l' /: case 118 / 'v' /: return 5; case 115 / 's' /: case 120 / 'x' /: return 6; case 112 / 'p' /: case 109 / 'm' /: return 7; case 104 / 'h' /: case 107 / 'k' /: return 8; case 110 / 'n' /: case 103 / 'g' /: return 9; case 122 / 'z' /: case 114 / 'r' /: return 0; default: return null; } } | The original version as suggested by the commented code compared strings against strings JavaScript lacks a character type . Profiling revealed that more than half the time was being spent in StringEqual calls. By switching the comparisons from string equality to integer equality, the new code takes approximately 15% of the time than the older version or a speed-up greater than 6x. Decoding Ciphertext The decoding process is essentially a functional collect : map across all elements and keep those that are non-null. | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | / Return an array of numbers based on decoding s. If there are no valid consonants within s, returns an empty array. @param s word or phrase / export function decode s: string : number { let result: number = ; for const chr of s { const v: number | null = consonant value chr ; if v == null { result.push v ; } } return result; } | Alternatively, TextEncoder could be used to build a Uint8Array of byte values and consonant value could be given a byte value as an argument. However, since a character in UTF-8 may span multiple bytes, we would still need to capture the complexity of Unicode encoding somewhere. Encoding a Specific Plaintext Value Using Moby 2002 , we previously developed a dictionary by mapping each word into a cipher value and then grouping words by a shared cipher value. Thus, generating a list of words that match a given plaintext digit string is a look-up: | 1 2 3 4 5 6 7 8 9 10 11 12 13 | / Return from the dictionary all words that have the plaintext value of n. @param n plaintext value @param dictionary plaintext value to array of words / export function encode n: string, dictionary: Map