Merge pull request #3778 from s2ks/hamming

CODINGCONTRACT: HammingCodes description and implementation fixes
This commit is contained in:
hydroflame
2022-07-21 02:16:35 -04:00
committed by GitHub
2 changed files with 167 additions and 106 deletions

View File

@ -2,7 +2,7 @@ import { getRandomInt } from "../utils/helpers/getRandomInt";
import { MinHeap } from "../utils/Heap"; import { MinHeap } from "../utils/Heap";
import { comprGenChar, comprLZGenerate, comprLZEncode, comprLZDecode } from "../utils/CompressionContracts"; import { comprGenChar, comprLZGenerate, comprLZEncode, comprLZDecode } from "../utils/CompressionContracts";
import { HammingEncode, HammingDecode } from "../utils/HammingCodeTools"; import { HammingEncode, HammingDecode, HammingEncodeProperly } from "../utils/HammingCodeTools";
/* tslint:disable:completed-docs no-magic-numbers arrow-return-shorthand */ /* tslint:disable:completed-docs no-magic-numbers arrow-return-shorthand */
/* Function that generates a valid 'data' for a contract type */ /* Function that generates a valid 'data' for a contract type */
@ -1289,16 +1289,17 @@ export const codingContractTypesMetadata: ICodingContractTypeMetadata[] = [
return [ return [
"You are given the following decimal Value: \n", "You are given the following decimal Value: \n",
`${n} \n`, `${n} \n`,
"Convert it into a binary string and encode it as a 'Hamming-Code'. eg:\n ", "Convert it to a binary representation and encode it as an 'extended Hamming code'. Eg:\n ",
"Value 8 will result into binary '1000', which will be encoded", "Value 8 is expressed in binary as '1000', which will be encoded",
"with the pattern 'pppdpddd', where p is a paritybit and d a databit,\n", "with the pattern 'pppdpddd', where p is a parity bit and d a data bit,\n",
"or '10101' (Value 21) will result into (pppdpdddpd) '1001101011'.\n\n", "or '10101' (Value 21) will result into (pppdpdddpd) '1001101011'.\n",
"NOTE: You need an parity Bit on Index 0 as an 'overall'-paritybit. \n", "The answer should be given as a string containing only 1s and 0s.\n",
"NOTE 2: You should watch the HammingCode-video from 3Blue1Brown, which explains the 'rule' of encoding,", "NOTE: the endianness of the data bits is reversed in relation to the endianness of the parity bits.\n",
"including the first Index parity-bit mentioned on the first note.\n\n", "NOTE: The bit at index zero is the overall parity bit, this should be set last.\n",
"Now the only one rule for this encoding:\n", "NOTE 2: You should watch the Hamming Code video from 3Blue1Brown, which explains the 'rule' of encoding,",
" It's not allowed to add additional leading '0's to the binary value\n", "including the first index parity bit mentioned in the previous note.\n\n",
"That means, the binary value has to be encoded as it is", "Extra rule for encoding:\n",
"There should be no leading zeros in the 'data bit' section",
].join(" "); ].join(" ");
}, },
gen: (): number => { gen: (): number => {
@ -1316,19 +1317,21 @@ export const codingContractTypesMetadata: ICodingContractTypeMetadata[] = [
desc: (n: unknown): string => { desc: (n: unknown): string => {
return [ return [
"You are given the following encoded binary string: \n", "You are given the following encoded binary string: \n",
`'${n}' \n`, `'${n}' \n\n`,
"The string is a Hamming code with 1 'possible' error on a random index.\n", "Treat it as an extended Hamming code with 1 'possible' error at a random index.\n",
"If there is an error, find the bit that is an error and fix it.\n", "Find the 'possible' wrong bit, fix it and extract the decimal value, which is hidden inside the string.\n\n",
"Extract the encoded decimal value and return a string with that value.\n\n", "Note: The length of the binary string is dynamic, but it's encoding/decoding follows Hamming's 'rule'\n",
"NOTE: The length of the binary string is dynamic.\n", "Note 2: Index 0 is an 'overall' parity bit. Watch the Hamming code video from 3Blue1Brown for more information\n",
"NOTE 2: Index 0 is an 'overall' parity bit. Watch the Hamming code video from 3Blue1Brown for more information.\n", "Note 3: There's a ~55% chance for an altered Bit. So... MAYBE there is an altered Bit 😉\n",
"NOTE 3: There's approximately a 55% chance for an altered bit. So... MAYBE there is an altered bit 😉\n", "Note: The endianness of the encoded decimal value is reversed in relation to the endianness of the Hamming code. Where",
"NOTE 4: Return the decimal value as a string.", "the Hamming code is expressed as little-endian (LSB at index 0), the decimal value encoded in it is expressed as big-endian",
"(MSB at index 0).\n",
"Extra note for automation: return the decimal value as a string",
].join(" "); ].join(" ");
}, },
gen: (): string => { gen: (): string => {
const _alteredBit = Math.round(Math.random()); const _alteredBit = Math.round(Math.random());
const _buildArray: Array<string> = HammingEncode( const _buildArray: Array<string> = HammingEncodeProperly(
getRandomInt(Math.pow(2, 4), Math.pow(2, getRandomInt(1, 57))), getRandomInt(Math.pow(2, 4), Math.pow(2, getRandomInt(1, 57))),
).split(""); ).split("");
if (_alteredBit) { if (_alteredBit) {

View File

@ -1,97 +1,155 @@
// by Discord: H3draut3r#6722, feel free to ask me any questions. i probably don't know the answer 🤣 export function HammingEncode(data: number): string {
export function HammingEncode(value: number): string { const enc: Array<number> = [0];
// encoding following Hammings rule const data_bits: Array<any> = data.toString(2).split("").reverse();
function HammingSumOfParity(_lengthOfDBits: number): number {
// will calculate the needed amount of parityBits 'without' the "overall"-Parity (that math took me 4 Days to get it working)
return _lengthOfDBits < 3 || _lengthOfDBits == 0 // oh and of course using ternary operators, it's a pretty neat function
? _lengthOfDBits == 0
? 0
: _lengthOfDBits + 1
: // the following math will only work, if the length is greater equal 3, otherwise it's "kind of" broken :D
Math.ceil(Math.log2(_lengthOfDBits * 2)) <=
Math.ceil(Math.log2(1 + _lengthOfDBits + Math.ceil(Math.log2(_lengthOfDBits))))
? Math.ceil(Math.log2(_lengthOfDBits) + 1)
: Math.ceil(Math.log2(_lengthOfDBits));
}
const _data = value.toString(2).split(""); // first, change into binary string, then create array with 1 bit per index
const _sumParity: number = HammingSumOfParity(_data.length); // get the sum of needed parity bits (for later use in encoding)
const count = (arr: Array<string>, val: string): number =>
arr.reduce((a: number, v: string) => (v === val ? a + 1 : a), 0);
// function count for specific entries in the array, for later use
const _build = ["x", "x", ..._data.splice(0, 1)]; // init the "pre-build" data_bits.forEach((e, i, a) => {
for (let i = 2; i < _sumParity; i++) { a[i] = parseInt(e);
// add new paritybits and the corresponding data bits (pre-building array) });
_build.push("x", ..._data.splice(0, Math.pow(2, i) - 1));
} let k = data_bits.length;
// now the "calculation"... get the paritybits ('x') working
for (const index of _build.reduce(function (a: Array<number>, e: string, i: number) { /* NOTE: writing the data like this flips the endianness, this is what the
if (e == "x") a.push(i); * original implementation by Hedrauta did so I'm keeping it like it was. */
return a; for (let i = 1; k > 0; i++) {
}, [])) { if ((i & (i - 1)) != 0) {
// that reduce will result in an array of index numbers where the "x" is placed enc[i] = data_bits[--k];
const _tempcount = index + 1; // set the "stepsize" for the parityBit } else {
const _temparray = []; // temporary array to store the extracted bits enc[i] = 0;
const _tempdata = [..._build]; // only work with a copy of the _build
while (_tempdata[index] !== undefined) {
// as long as there are bits on the starting index, do "cut"
const _temp: Array<string> = _tempdata.splice(index, _tempcount * 2); // cut stepsize*2 bits, then...
_temparray.push(..._temp.splice(0, _tempcount)); // ... cut the result again and keep the first half
} }
_temparray.splice(0, 1); // remove first bit, which is the parity one }
_build[index] = (count(_temparray, "1") % 2).toString(); // count with remainder of 2 and"toString" to store the parityBit
} // parity done, now the "overall"-parity is set let parity: any = 0;
_build.unshift((count(_build, "1") % 2).toString()); // has to be done as last element
return _build.join(""); // return the _build as string /* Figure out the subsection parities */
for (let i = 0; i < enc.length; i++) {
if (enc[i]) {
parity ^= i;
}
}
parity = parity.toString(2).split("").reverse();
parity.forEach((e: any, i: any, a: any) => {
a[i] = parseInt(e);
});
/* Set the parity bits accordingly */
for (let i = 0; i < parity.length; i++) {
enc[2 ** i] = parity[i] ? 1 : 0;
}
parity = 0;
/* Figure out the overall parity for the entire block */
for (let i = 0; i < enc.length; i++) {
if (enc[i]) {
parity++;
}
}
/* Finally set the overall parity bit */
enc[0] = parity % 2 == 0 ? 0 : 1;
return enc.join("");
} }
export function HammingDecode(_data: string): number { export function HammingEncodeProperly(data: number): string {
//check for altered bit and decode /* How many bits do we need?
const _build = _data.split(""); // ye, an array for working, again * n = 2^m
const _testArray = []; //for the "truthtable". if any is false, the data has an altered bit, will check for and fix it * k = 2^m - m - 1
const _sumParity = Math.ceil(Math.log2(_data.length)); // sum of parity for later use * where k is the number of data bits, m the number
const count = (arr: Array<string>, val: string): number => * of parity bits and n the number of total bits. */
arr.reduce((a: number, v: string) => (v === val ? a + 1 : a), 0);
// the count.... again ;)
let _overallParity = _build.splice(0, 1).join(""); // store first index, for checking in next step and fix the _build properly later on let m = 1;
_testArray.push(_overallParity == (count(_build, "1") % 2).toString() ? true : false); // first check with the overall parity bit
for (let i = 0; i < _sumParity; i++) { while (2 ** (2 ** m - m - 1) - 1 < data) {
// for the rest of the remaining parity bits we also "check" m++;
const _tempIndex = Math.pow(2, i) - 1; // get the parityBits Index }
const _tempStep = _tempIndex + 1; // set the stepsize
const _tempData = [..._build]; // get a "copy" of the build-data for working const n: number = 2 ** m;
const _tempArray = []; // init empty array for "testing" const k: number = 2 ** m - m - 1;
while (_tempData[_tempIndex] != undefined) {
// extract from the copied data until the "starting" index is undefined const enc: Array<number> = [0];
const _temp = [..._tempData.splice(_tempIndex, _tempStep * 2)]; // extract 2*stepsize const data_bits: Array<any> = data.toString(2).split("").reverse();
_tempArray.push(..._temp.splice(0, _tempStep)); // and cut again for keeping first half
data_bits.forEach((e, i, a) => {
a[i] = parseInt(e);
});
/* 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;
} }
const _tempParity = _tempArray.shift(); // and again save the first index separated for checking with the rest of the data
_testArray.push(_tempParity == (count(_tempArray, "1") % 2).toString() ? true : false);
// is the _tempParity the calculated data? push answer into the 'truthtable'
} }
let _fixIndex = 0; // init the "fixing" index and start with 0
for (let i = 1; i < _sumParity + 1; i++) { let parity: any = 0;
// simple binary adding for every boolean in the _testArray, starting from 2nd index of it
_fixIndex += _testArray[i] ? 0 : Math.pow(2, i) / 2; /* Figure out the subsection parities */
for (let i = 0; i < n; i++) {
if (enc[i]) {
parity ^= i;
}
} }
_build.unshift(_overallParity); // now we need the "overall" parity back in it's place
// try fix the actual encoded binary string if there is an error parity = parity.toString(2).split("").reverse();
if (_fixIndex > 0 && _testArray[0] == false) { parity.forEach((e: any, i: any, a: any) => {
// if the overall is false and the sum of calculated values is greater equal 0, fix the corresponding hamming-bit a[i] = parseInt(e);
_build[_fixIndex] = _build[_fixIndex] == "0" ? "1" : "0"; });
} else if (_testArray[0] == false) {
// otherwise, if the the overall_parity is the only wrong, fix that one /* Set the parity bits accordingly */
_overallParity = _overallParity == "0" ? "1" : "0"; for (let i = 0; i < m; i++) {
} else if (_testArray[0] == true && _testArray.some((truth) => truth == false)) { enc[2 ** i] = parity[i] ? 1 : 0;
return 0; // uhm, there's some strange going on... 2 bits are altered? How? This should not happen 👀
} }
// oof.. halfway through... we fixed an possible altered bit, now "extract" the parity-bits from the _build
for (let i = _sumParity; i >= 0; i--) { parity = 0;
// start from the last parity down the 2nd index one /* Figure out the overall parity for the entire block */
_build.splice(Math.pow(2, i), 1); for (let i = 0; i < n; i++) {
if (enc[i]) {
parity++;
}
} }
_build.splice(0, 1); // remove the overall parity bit and we have our binary value
return parseInt(_build.join(""), 2); // parse the integer with redux 2 and we're done! /* Finally set the overall parity bit */
enc[0] = parity % 2 == 0 ? 0 : 1;
return enc.join("");
}
export function HammingDecode(data: string): number {
let err = 0;
const bits: Array<number> = [];
/* TODO why not just work with an array of digits from the start? */
for (const i in data.split("")) {
const bit = parseInt(data[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);
} }