RosettaCodeData/Task/Playfair-cipher/JavaScript/playfair-cipher.js

270 lines
9.5 KiB
JavaScript

const readline = require('readline');
// Enum-like object for Playfair options
const playfairOption = {
NO_Q: 0,
I_EQUALS_J: 1,
};
// Represents the Playfair cipher state and methods
class Playfair {
constructor(keyword, pfo) {
this.keyword = keyword;
this.pfo = pfo; // playfairOption
this.table = Array(5).fill(null).map(() => Array(5).fill('')); // 5x5 table
}
// Initializes the 5x5 Playfair table based on the keyword and option
init() {
const used = Array(26).fill(false); // Track used letters
if (this.pfo === playfairOption.NO_Q) {
used['Q'.charCodeAt(0) - 65] = true; // Q is used/omitted
} else {
used['J'.charCodeAt(0) - 65] = true; // J is used/replaced by I
}
const alphabet = this.keyword.toUpperCase() + "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
let row = 0;
let col = 0;
for (const char of alphabet) {
const charCode = char.charCodeAt(0);
// Check if it's a letter A-Z
if (charCode >= 'A'.charCodeAt(0) && charCode <= 'Z'.charCodeAt(0)) {
const charIndex = charCode - 65; // 0-25 index
// Handle J if I_EQUALS_J option is selected
let charToUse = char;
let indexToUse = charIndex;
if (char === 'J' && this.pfo === playfairOption.I_EQUALS_J) {
charToUse = 'I';
indexToUse = 'I'.charCodeAt(0) - 65;
}
// Skip Q if NO_Q option is selected
if (charToUse === 'Q' && this.pfo === playfairOption.NO_Q) {
continue;
}
if (!used[indexToUse]) {
this.table[row][col] = charToUse;
used[indexToUse] = true;
col++;
if (col === 5) {
row++;
if (row === 5) {
break; // Table filled
}
col = 0;
}
}
}
}
}
// Prepares the plaintext: uppercase, removes non-letters,
// handles Q/J based on option, inserts X between duplicate letters,
// pads with X/Z if length is odd.
getCleanText(plainText) {
plainText = plainText.toUpperCase();
const cleanChars = [];
let prevChar = ''; // Use '' to indicate no previous character
for (let i = 0; i < plainText.length; i++) {
let currentChar = plainText[i];
const charCode = currentChar.charCodeAt(0);
// Skip non-letters
if (charCode < 'A'.charCodeAt(0) || charCode > 'Z'.charCodeAt(0)) {
continue;
}
// Handle J if I_EQUALS_J option is specified, replace J with I
if (currentChar === 'J' && this.pfo === playfairOption.I_EQUALS_J) {
currentChar = 'I';
}
// Skip Q if NO_Q option is specified (already handled in init, but good here too)
if (currentChar === 'Q' && this.pfo === playfairOption.NO_Q) {
continue;
}
// Insert 'X' between duplicate consecutive letters
// Only compare if prevChar is set (not the very first character)
if (prevChar !== '' && currentChar === prevChar) {
cleanChars.push('X');
}
cleanChars.push(currentChar);
prevChar = currentChar; // Store the character *after* potential X insertion
}
// If length is odd, add a padding character
if (cleanChars.length % 2 === 1) {
// Add 'X' unless the last letter is 'X', then add 'Z'
if (cleanChars[cleanChars.length - 1] !== 'X') {
cleanChars.push('X');
} else {
cleanChars.push('Z');
}
}
return cleanChars.join('');
}
// Finds the row and column of a character in the table
findChar(char) {
for (let i = 0; i < 5; i++) {
for (let j = 0; j < 5; j++) {
if (this.table[i][j] === char) {
return [i, j]; // Return [row, col]
}
}
}
return [-1, -1]; // Should not happen if getCleanText is correct
}
// Encodes plaintext using the Playfair cipher
encode(plainText) {
const cleanText = this.getCleanText(plainText);
const cipherChars = [];
for (let i = 0; i < cleanText.length; i += 2) {
const char1 = cleanText[i];
const char2 = cleanText[i + 1]; // cleanText length is always even
const [row1, col1] = this.findChar(char1);
const [row2, col2] = this.findChar(char2);
let encodedChar1, encodedChar2;
if (row1 === row2) { // Same row
encodedChar1 = this.table[row1][(col1 + 1) % 5];
encodedChar2 = this.table[row2][(col2 + 1) % 5];
} else if (col1 === col2) { // Same column
encodedChar1 = this.table[(row1 + 1) % 5][col1];
encodedChar2 = this.table[(row2 + 1) % 5][col2];
} else { // Different row and column (rectangle)
encodedChar1 = this.table[row1][col2];
encodedChar2 = this.table[row2][col1];
}
cipherChars.push(encodedChar1);
cipherChars.push(encodedChar2);
// Add space after each digram, matching Go's output format
if (i + 2 < cleanText.length) {
cipherChars.push(' ');
}
}
return cipherChars.join('');
}
// Decodes ciphertext using the Playfair cipher
decode(cipherText) {
// The Go code processes the ciphertext including spaces,
// stepping by 3. We'll do the same by stripping spaces first
// and then processing in pairs. This is conceptually simpler in JS.
// Or, we can replicate the Go loop that steps by 3. Let's replicate for exact match.
const decodedChars = [];
const l = cipherText.length;
for (let i = 0; i < l; i += 3) { // Step by 3 due to spaces in encoded text
const char1 = cipherText[i];
const char2 = cipherText[i + 1];
// cipherText[i+2] will be a space, which findChar will correctly not find (-1, -1)
// But the Go code explicitly finds the *bytes* at i and i+1. Let's stick to that.
// Ensure we only process valid character pairs
if (!char1 || !char2 || char1 === ' ' || char2 === ' ') {
// This shouldn't happen with the i+=3 loop structure if the input
// format from encode is followed, but good to be safe.
continue;
}
const [row1, col1] = this.findChar(char1);
const [row2, col2] = this.findChar(char2);
let decodedChar1, decodedChar2;
// Decoding rules are the reverse of encoding
if (row1 === row2) { // Same row
// Shift left: (col - 1 + 5) % 5
decodedChar1 = this.table[row1][(col1 + 4) % 5];
decodedChar2 = this.table[row2][(col2 + 4) % 5];
} else if (col1 === col2) { // Same column
// Shift up: (row - 1 + 5) % 5
decodedChar1 = this.table[(row1 + 4) % 5][col1];
decodedChar2 = this.table[(row2 + 4) % 5][col2];
} else { // Different row and column (rectangle)
decodedChar1 = this.table[row1][col2];
decodedChar2 = this.table[row2][col1];
}
decodedChars.push(decodedChar1);
decodedChars.push(decodedChar2);
// Add space after each digram in decoded text, matching Go's output format
if (i + 3 < l) { // Check if there's more content after the current digram+space
decodedChars.push(' ');
}
}
return decodedChars.join('');
}
// Prints the 5x5 table to the console
printTable() {
console.log("The table to be used is :\n");
for (let i = 0; i < 5; i++) {
console.log(this.table[i].join(' ')); // Join row elements with space
}
console.log(); // Add a blank line for spacing
}
}
// Main execution function using async/await for readline
async function main() {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const question = (query) => new Promise((resolve) => rl.question(query, resolve));
try {
const keyword = await question("Enter Playfair keyword : ");
let ignoreQ = '';
while (ignoreQ !== "y" && ignoreQ !== "n") {
ignoreQ = (await question("Ignore Q when building table y/n : ")).toLowerCase();
}
const pfo = (ignoreQ === "y") ? playfairOption.NO_Q : playfairOption.I_EQUALS_J;
const pf = new Playfair(keyword, pfo);
pf.init();
pf.printTable();
const plainText = await question("\nEnter plain text : ");
const encodedText = pf.encode(plainText);
console.log("\nEncoded text is :", encodedText);
// The Go code decodes the encoded text *including* the spaces it added.
// Pass the encoded text as is to the decode function.
const decodedText = pf.decode(encodedText);
console.log("Decoded text is :", decodedText);
} finally {
rl.close(); // Close the readline interface
}
}
// Execute the main function
main();