CONTRACTS: Clean up helpers, expand documentation, Hamming wording again (#2296)

This commit is contained in:
gmcew
2025-08-26 01:16:26 +01:00
committed by GitHub
parent ef8cbc3e0c
commit 15776e36e3
7 changed files with 368 additions and 365 deletions
-200
View File
@@ -1,200 +0,0 @@
// choose random characters for generating plaintext to compress
export function comprGenChar(): string {
const r = Math.random();
if (r < 0.4) {
return "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[Math.floor(26 * Math.random())];
} else if (r < 0.8) {
return "abcdefghijklmnopqrstuvwxyz"[Math.floor(26 * Math.random())];
} else {
return "01234567689"[Math.floor(10 * Math.random())];
}
}
// generate plaintext which is amenable to LZ encoding
export function comprLZGenerate(): string {
const length = 50 + Math.floor(25 * (Math.random() + Math.random()));
let plain = "";
while (plain.length < length) {
if (Math.random() < 0.8) {
plain += comprGenChar();
} else {
const length = 1 + Math.floor(9 * Math.random());
const offset = 1 + Math.floor(9 * Math.random());
if (offset > plain.length) {
continue;
}
for (let i = 0; i < length; ++i) {
plain += plain[plain.length - offset];
}
}
}
return plain.substring(0, length);
}
// compress plaintext string
export function comprLZEncode(plain: string): string {
// for state[i][j]:
// if i is 0, we're adding a literal of length j
// else, we're adding a backreference of offset i and length j
let cur_state: (string | null)[][] = Array.from(Array(10), () => Array<string | null>(10).fill(null));
let new_state: (string | null)[][] = Array.from(Array(10), () => Array<string | null>(10));
function set(state: (string | null)[][], i: number, j: number, str: string): void {
const current = state[i][j];
if (current == null || str.length < current.length) {
state[i][j] = str;
} else if (str.length === current.length && Math.random() < 0.5) {
// if two strings are the same length, pick randomly so that
// we generate more possible inputs to Compression II
state[i][j] = str;
}
}
// initial state is a literal of length 1
cur_state[0][1] = "";
for (let i = 1; i < plain.length; ++i) {
for (const row of new_state) {
row.fill(null);
}
const c = plain[i];
// handle literals
for (let length = 1; length <= 9; ++length) {
const string = cur_state[0][length];
if (string == null) {
continue;
}
if (length < 9) {
// extend current literal
set(new_state, 0, length + 1, string);
} else {
// start new literal
set(new_state, 0, 1, string + "9" + plain.substring(i - 9, i) + "0");
}
for (let offset = 1; offset <= Math.min(9, i); ++offset) {
if (plain[i - offset] === c) {
// start new backreference
set(new_state, offset, 1, string + String(length) + plain.substring(i - length, i));
}
}
}
// handle backreferences
for (let offset = 1; offset <= 9; ++offset) {
for (let length = 1; length <= 9; ++length) {
const string = cur_state[offset][length];
if (string == null) {
continue;
}
if (plain[i - offset] === c) {
if (length < 9) {
// extend current backreference
set(new_state, offset, length + 1, string);
} else {
// start new backreference
set(new_state, offset, 1, string + "9" + String(offset) + "0");
}
}
// start new literal
set(new_state, 0, 1, string + String(length) + String(offset));
// end current backreference and start new backreference
for (let new_offset = 1; new_offset <= Math.min(9, i); ++new_offset) {
if (plain[i - new_offset] === c) {
set(new_state, new_offset, 1, string + String(length) + String(offset) + "0");
}
}
}
}
const tmp_state = new_state;
new_state = cur_state;
cur_state = tmp_state;
}
let result = null;
for (let len = 1; len <= 9; ++len) {
let string = cur_state[0][len];
if (string == null) {
continue;
}
string += String(len) + plain.substring(plain.length - len, plain.length);
if (result == null || string.length < result.length) {
result = string;
} else if (string.length == result.length && Math.random() < 0.5) {
result = string;
}
}
for (let offset = 1; offset <= 9; ++offset) {
for (let len = 1; len <= 9; ++len) {
let string = cur_state[offset][len];
if (string == null) {
continue;
}
string += String(len) + "" + String(offset);
if (result == null || string.length < result.length) {
result = string;
} else if (string.length == result.length && Math.random() < 0.5) {
result = string;
}
}
}
return result ?? "";
}
// decompress LZ-compressed string, or return null if input is invalid
export function comprLZDecode(compr: string): string | null {
let plain = "";
for (let i = 0; i < compr.length; ) {
const literal_length = compr.charCodeAt(i) - 0x30;
if (literal_length < 0 || literal_length > 9 || i + 1 + literal_length > compr.length) {
return null;
}
plain += compr.substring(i + 1, i + 1 + literal_length);
i += 1 + literal_length;
if (i >= compr.length) {
break;
}
const backref_length = compr.charCodeAt(i) - 0x30;
if (backref_length < 0 || backref_length > 9) {
return null;
} else if (backref_length === 0) {
++i;
} else {
if (i + 1 >= compr.length) {
return null;
}
const backref_offset = compr.charCodeAt(i + 1) - 0x30;
if ((backref_length > 0 && (backref_offset < 1 || backref_offset > 9)) || backref_offset > plain.length) {
return null;
}
for (let j = 0; j < backref_length; ++j) {
plain += plain[plain.length - backref_offset];
}
i += 2;
}
}
return plain;
}
-158
View File
@@ -1,158 +0,0 @@
export function HammingEncode(data: number): string {
const enc: number[] = [0];
const data_bits: number[] = data
.toString(2)
.split("")
.reverse()
.map((value) => parseInt(value));
let k = data_bits.length;
/* NOTE: writing the data like this flips the endianness, this is what the
* original implementation by Hedrauta did so I'm keeping it like it was. */
for (let i = 1; k > 0; i++) {
if ((i & (i - 1)) != 0) {
enc[i] = data_bits[--k];
} else {
enc[i] = 0;
}
}
let parityNumber = 0;
/* Figure out the subsection parities */
for (let i = 0; i < enc.length; i++) {
if (enc[i]) {
parityNumber ^= i;
}
}
const parityArray = parityNumber
.toString(2)
.split("")
.reverse()
.map((value) => parseInt(value));
/* Set the parity bits accordingly */
for (let i = 0; i < parityArray.length; i++) {
enc[2 ** i] = parityArray[i] ? 1 : 0;
}
parityNumber = 0;
/* Figure out the overall parity for the entire block */
for (let i = 0; i < enc.length; i++) {
if (enc[i]) {
parityNumber++;
}
}
/* Finally set the overall parity bit */
enc[0] = parityNumber % 2 == 0 ? 0 : 1;
return enc.join("");
}
export function HammingEncodeProperly(data: number): string {
/* How many bits do we need?
* n = 2^m
* k = 2^m - m - 1
* where k is the number of data bits, m the number
* of parity bits and n the number of total bits. */
let m = 1;
while (2 ** (2 ** m - m - 1) - 1 < data) {
m++;
}
const n: number = 2 ** m;
const k: number = 2 ** m - m - 1;
const enc: number[] = [0];
const data_bits: number[] = data
.toString(2)
.split("")
.reverse()
.map((value) => parseInt(value));
/* Flip endianness as in the original implementation by Hedrauta
* and write the data back to front
* XXX why do we do this? */
for (let i = 1, j = k; i < n; i++) {
if ((i & (i - 1)) != 0) {
enc[i] = data_bits[--j] ? data_bits[j] : 0;
}
}
let parityNumber = 0;
/* Figure out the subsection parities */
for (let i = 0; i < n; i++) {
if (enc[i]) {
parityNumber ^= i;
}
}
const parityArray = parityNumber
.toString(2)
.split("")
.reverse()
.map((value) => parseInt(value));
/* Set the parity bits accordingly */
for (let i = 0; i < m; i++) {
enc[2 ** i] = parityArray[i] ? 1 : 0;
}
parityNumber = 0;
/* Figure out the overall parity for the entire block */
for (let i = 0; i < n; i++) {
if (enc[i]) {
parityNumber++;
}
}
/* Finally set the overall parity bit */
enc[0] = parityNumber % 2 == 0 ? 0 : 1;
return enc.join("");
}
export function HammingDecode(data: string): number {
let err = 0;
const bits: number[] = [];
/* TODO why not just work with an array of digits from the start? */
const bitStringArray = data.split("");
for (let i = 0; i < bitStringArray.length; ++i) {
const bit = parseInt(bitStringArray[i]);
bits[i] = bit;
if (bit) {
err ^= +i;
}
}
/* If err != 0 then it spells out the index of the bit that was flipped */
if (err) {
/* Flip to correct */
bits[err] = bits[err] ? 0 : 1;
}
/* Now we have to read the message, bit 0 is unused (it's the overall parity bit
* which we don't care about). Each bit at an index that is a power of 2 is
* a parity bit and not part of the actual message. */
let ans = "";
for (let i = 1; i < bits.length; i++) {
/* i is not a power of two so it's not a parity bit */
if ((i & (i - 1)) != 0) {
ans += bits[i];
}
}
/* TODO to avoid ambiguity about endianness why not let the player return the extracted (and corrected)
* data bits, rather than guessing at how to convert it to a decimal string? */
return parseInt(ans, 2);
}