From c773edceb9ed1cb0274724ecef59fba0840d0347 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Wed, 19 Jul 2017 15:29:37 +0000 Subject: [PATCH] Added BCD operations with tests --- src/core/config/Categories.js | 2 + src/core/config/OperationConfig.js | 59 ++++++++ src/core/operations/BCD.js | 214 +++++++++++++++++++++++++++++ src/web/html/index.html | 4 +- test/index.js | 1 + test/tests/operations/BCD.js | 103 ++++++++++++++ 6 files changed, 382 insertions(+), 1 deletion(-) create mode 100755 src/core/operations/BCD.js create mode 100644 test/tests/operations/BCD.js diff --git a/src/core/config/Categories.js b/src/core/config/Categories.js index ef88c524..ce46d221 100755 --- a/src/core/config/Categories.js +++ b/src/core/config/Categories.js @@ -46,6 +46,8 @@ const Categories = [ "From Base58", "To Base", "From Base", + "To BCD", + "From BCD", "To HTML Entity", "From HTML Entity", "URL Encode", diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js index 41e159e3..fe313525 100755 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -2,6 +2,7 @@ import FlowControl from "../FlowControl.js"; import Base from "../operations/Base.js"; import Base58 from "../operations/Base58.js"; import Base64 from "../operations/Base64.js"; +import BCD from "../operations/BCD.js"; import BitwiseOp from "../operations/BitwiseOp.js"; import ByteRepr from "../operations/ByteRepr.js"; import CharEnc from "../operations/CharEnc.js"; @@ -3507,6 +3508,64 @@ const OperationConfig = { } ] }, + "From BCD": { + description: "Binary-Coded Decimal (BCD) is a class of binary encodings of decimal numbers where each decimal digit is represented by a fixed number of bits, usually four or eight. Special bit patterns are sometimes used for a sign.", + run: BCD.runFromBCD, + inputType: "string", + outputType: "number", + args: [ + { + name: "Scheme", + type: "option", + value: BCD.ENCODING_SCHEME + }, + { + name: "Packed", + type: "boolean", + value: true + }, + { + name: "Signed", + type: "boolean", + value: false + }, + { + name: "Input format", + type: "option", + value: BCD.FORMAT + } + ] + + }, + "To BCD": { + description: "Binary-Coded Decimal (BCD) is a class of binary encodings of decimal numbers where each decimal digit is represented by a fixed number of bits, usually four or eight. Special bit patterns are sometimes used for a sign", + run: BCD.runToBCD, + inputType: "number", + outputType: "string", + args: [ + { + name: "Scheme", + type: "option", + value: BCD.ENCODING_SCHEME + }, + { + name: "Packed", + type: "boolean", + value: true + }, + { + name: "Signed", + type: "boolean", + value: false + }, + { + name: "Output format", + type: "option", + value: BCD.FORMAT + } + ] + + }, }; export default OperationConfig; diff --git a/src/core/operations/BCD.js b/src/core/operations/BCD.js new file mode 100755 index 00000000..63c0cda3 --- /dev/null +++ b/src/core/operations/BCD.js @@ -0,0 +1,214 @@ +import Utils from "../Utils.js"; + + +/** + * Binary-Coded Decimal operations. + * + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2017 + * @license Apache-2.0 + * + * @namespace + */ +const BCD = { + + /** + * @constant + * @default + */ + ENCODING_SCHEME: [ + "8 4 2 1", + "7 4 2 1", + "4 2 2 1", + "2 4 2 1", + "8 4 -2 -1", + "Excess-3", + "IBM 8 4 2 1", + ], + + /** + * Lookup table for the binary value of each digit representation. + * + * I wrote a very nice algorithm to generate 8 4 2 1 encoding programatically, + * but unfortunately it's much easier (if less elegant) to use lookup tables + * when supporting multiple encoding schemes. + * + * "Practicality beats purity" - PEP 20 + * + * In some schemes it is possible to represent the same value in multiple ways. + * For instance, in 4 2 2 1 encoding, 0100 and 0010 both represent 2. Support + * has not yet been added for this. + * + * @constant + */ + ENCODING_LOOKUP: { + "8 4 2 1": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + "7 4 2 1": [0, 1, 2, 3, 4, 5, 6, 8, 9, 10], + "4 2 2 1": [0, 1, 4, 5, 8, 9, 12, 13, 14, 15], + "2 4 2 1": [0, 1, 2, 3, 4, 11, 12, 13, 14, 15], + "8 4 -2 -1": [0, 7, 6, 5, 4, 11, 10, 9, 8, 15], + "Excess-3": [3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + "IBM 8 4 2 1": [10, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + + /** + * @default + * @constant + */ + FORMAT: ["Nibbles", "Bytes", "Raw"], + + + /** + * To BCD operation. + * + * @param {number} input + * @param {Object[]} args + * @returns {string} + */ + runToBCD: function(input, args) { + if (isNaN(input)) + return "Invalid input"; + if (Math.floor(input) !== input) + return "Fractional values are not supported by BCD"; + + const encoding = BCD.ENCODING_LOOKUP[args[0]], + packed = args[1], + signed = args[2], + outputFormat = args[3]; + + // Split input number up into separate digits + const digits = input.toString().split(""); + + if (digits[0] === "-" || digits[0] === "+") { + digits.shift(); + } + + let nibbles = []; + + digits.forEach(d => { + const n = parseInt(d, 10); + nibbles.push(encoding[n]); + }); + + if (signed) { + if (packed && digits.length % 2 === 0) { + // If there are an even number of digits, we add a leading 0 so + // that the sign nibble doesn't sit in its own byte, leading to + // ambiguity around whether the number ends with a 0 or not. + nibbles.unshift(encoding[0]); + } + + nibbles.push(input > 0 ? 12 : 13); + // 12 ("C") for + (credit) + // 13 ("D") for - (debit) + } + + let bytes = []; + + if (packed) { + let encoded = 0, + little = false; + + nibbles.forEach(n => { + encoded ^= little ? n : (n << 4); + if (little) { + bytes.push(encoded); + encoded = 0; + } + little = !little; + }); + + if (little) bytes.push(encoded); + } else { + bytes = nibbles; + + // Add null high nibbles + nibbles = nibbles.map(n => { + return [0, n]; + }).reduce((a, b) => { + return a.concat(b); + }); + } + + // Output + switch (outputFormat) { + case "Nibbles": + return nibbles.map(n => { + return Utils.padLeft(n.toString(2), 4); + }).join(" "); + case "Bytes": + return bytes.map(b => { + return Utils.padLeft(b.toString(2), 8); + }).join(" "); + case "Raw": + default: + return Utils.byteArrayToChars(bytes); + } + }, + + + /** + * From BCD operation. + * + * @param {string} input + * @param {Object[]} args + * @returns {number} + */ + runFromBCD: function(input, args) { + const encoding = BCD.ENCODING_LOOKUP[args[0]], + packed = args[1], + signed = args[2], + inputFormat = args[3]; + + let nibbles = [], + output = "", + byteArray; + + // Normalise the input + switch (inputFormat) { + case "Nibbles": + case "Bytes": + input = input.replace(/\s/g, ""); + for (let i = 0; i < input.length; i += 4) { + nibbles.push(parseInt(input.substr(i, 4), 2)); + } + break; + case "Raw": + default: + byteArray = Utils.strToByteArray(input); + byteArray.forEach(b => { + nibbles.push(b >>> 4); + nibbles.push(b & 15); + }); + break; + } + + if (!packed) { + // Discard each high nibble + for (let i = 0; i < nibbles.length; i++) { + nibbles.splice(i, 1); + } + } + + if (signed) { + const sign = nibbles.pop(); + if (sign === 13 || + sign === 11) { + // Negative + output += "-"; + } + } + + nibbles.forEach(n => { + if (isNaN(n)) throw "Invalid input"; + let val = encoding.indexOf(n); + if (val < 0) throw `Value ${Utils.bin(n, 4)} not in encoding scheme`; + output += val.toString(); + }); + + return parseInt(output, 10); + }, + +}; + +export default BCD; diff --git a/src/web/html/index.html b/src/web/html/index.html index aa63b520..315e4c3a 100755 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -35,7 +35,9 @@ "use strict"; // Load theme before the preloader is shown - document.querySelector(":root").className = JSON.parse(localStorage.getItem("options")).theme; + try { + document.querySelector(":root").className = JSON.parse(localStorage.getItem("options")).theme; + } catch (e) {} // Define loading messages const loadingMsgs = [ diff --git a/test/index.js b/test/index.js index adb41f64..25059465 100644 --- a/test/index.js +++ b/test/index.js @@ -12,6 +12,7 @@ import "babel-polyfill"; import TestRegister from "./TestRegister.js"; import "./tests/operations/Base58.js"; +import "./tests/operations/BCD.js"; import "./tests/operations/ByteRepr.js"; import "./tests/operations/CharEnc.js"; import "./tests/operations/Cipher.js"; diff --git a/test/tests/operations/BCD.js b/test/tests/operations/BCD.js new file mode 100644 index 00000000..427f64d2 --- /dev/null +++ b/test/tests/operations/BCD.js @@ -0,0 +1,103 @@ +/** + * BCD tests + * + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2017 + * @license Apache-2.0 + */ +import TestRegister from "../../TestRegister.js"; + +TestRegister.addTests([ + { + name: "To BCD: default 0", + input: "0", + expectedOutput: "0000", + recipeConfig: [ + { + "op": "To BCD", + "args": ["8 4 2 1", true, false, "Nibbles"] + } + ] + }, + { + name: "To BCD: unpacked nibbles", + input: "1234567890", + expectedOutput: "0000 0001 0000 0010 0000 0011 0000 0100 0000 0101 0000 0110 0000 0111 0000 1000 0000 1001 0000 0000", + recipeConfig: [ + { + "op": "To BCD", + "args": ["8 4 2 1", false, false, "Nibbles"] + } + ] + }, + { + name: "To BCD: packed, signed bytes", + input: "1234567890", + expectedOutput: "00000001 00100011 01000101 01100111 10001001 00001100", + recipeConfig: [ + { + "op": "To BCD", + "args": ["8 4 2 1", true, true, "Bytes"] + } + ] + }, + { + name: "To BCD: packed, signed nibbles, 8 4 -2 -1", + input: "-1234567890", + expectedOutput: "0000 0111 0110 0101 0100 1011 1010 1001 1000 1111 0000 1101", + recipeConfig: [ + { + "op": "To BCD", + "args": ["8 4 -2 -1", true, true, "Nibbles"] + } + ] + }, + { + name: "From BCD: default 0", + input: "0000", + expectedOutput: "0", + recipeConfig: [ + { + "op": "From BCD", + "args": ["8 4 2 1", true, false, "Nibbles"] + } + ] + }, + { + name: "From BCD: packed, signed bytes", + input: "00000001 00100011 01000101 01100111 10001001 00001101", + expectedOutput: "-1234567890", + recipeConfig: [ + { + "op": "From BCD", + "args": ["8 4 2 1", true, true, "Bytes"] + } + ] + }, + { + name: "From BCD: Excess-3, unpacked, unsigned", + input: "00000100 00000101 00000110 00000111 00001000 00001001 00001010 00001011 00001100 00000011", + expectedOutput: "1234567890", + recipeConfig: [ + { + "op": "From BCD", + "args": ["Excess-3", false, false, "Nibbles"] + } + ] + }, + { + name: "BCD: raw 4 2 2 1, packed, signed", + input: "1234567890", + expectedOutput: "1234567890", + recipeConfig: [ + { + "op": "To BCD", + "args": ["4 2 2 1", true, true, "Raw"] + }, + { + "op": "From BCD", + "args": ["4 2 2 1", true, true, "Raw"] + } + ] + }, +]);