@onesy/huffman-code
Version:
441 lines (360 loc) • 15.1 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import is from '@onesy/utils/is';
import merge from '@onesy/utils/merge';
import copy from '@onesy/utils/copy';
import to from '@onesy/utils/to';
import binaryStringToHexadecimal from '@onesy/utils/binaryStringToHexadecimal';
import hexadecimalStringToBinary from '@onesy/utils/hexadecimalStringToBinary';
import OnesyDate from '@onesy/date/OnesyDate';
import duration from '@onesy/date/duration';
export class OnesyHuffmanCodeResponse {
constructor() {
let value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
let values = arguments.length > 1 ? arguments[1] : undefined;
let values_encoded = arguments.length > 2 ? arguments[2] : undefined;
let probabilities = arguments.length > 3 ? arguments[3] : undefined;
let efficiency = arguments.length > 4 ? arguments[4] : undefined;
let redundency = arguments.length > 5 ? arguments[5] : undefined;
let entropy = arguments.length > 6 ? arguments[6] : undefined;
let original_byte_size = arguments.length > 7 ? arguments[7] : undefined;
let values_byte_size = arguments.length > 8 ? arguments[8] : undefined;
let value_byte_size = arguments.length > 9 ? arguments[9] : undefined;
let encoded_byte_size = arguments.length > 10 ? arguments[10] : undefined;
let compression_ratio = arguments.length > 11 ? arguments[11] : undefined;
let compression_percentage = arguments.length > 12 ? arguments[12] : undefined;
let positive = arguments.length > 13 ? arguments[13] : undefined;
let average_code_word_length = arguments.length > 14 ? arguments[14] : undefined;
let performance_milliseconds = arguments.length > 15 ? arguments[15] : undefined;
let performance = arguments.length > 16 ? arguments[16] : undefined;
this.value = value;
this.values = values;
this.values_encoded = values_encoded;
this.probabilities = probabilities;
this.efficiency = efficiency;
this.redundency = redundency;
this.entropy = entropy;
this.original_byte_size = original_byte_size;
this.values_byte_size = values_byte_size;
this.value_byte_size = value_byte_size;
this.encoded_byte_size = encoded_byte_size;
this.compression_ratio = compression_ratio;
this.compression_percentage = compression_percentage;
this.positive = positive;
this.average_code_word_length = average_code_word_length;
this.performance_milliseconds = performance_milliseconds;
this.performance = performance;
}
}
export class OnesyNode {
constructor() {
let value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;
let word = arguments.length > 1 ? arguments[1] : undefined;
this.value = value;
this.word = word;
_defineProperty(this, "left", void 0);
_defineProperty(this, "right", void 0);
this.value = Number(value.toFixed(3));
}
get leaf() {
return !(this.left || this.right);
}
get maxDepth() {
const maxDepthMethod = value => {
if (value === undefined) return 0;
return Math.max(1 + maxDepthMethod(value.left), 1 + maxDepthMethod(value.right));
};
return maxDepthMethod(this);
}
}
export class OnesyHuffmanTree {
constructor() {
_defineProperty(this, "root", void 0);
}
static make(value) {
return new OnesyHuffmanTree().make(value);
}
get array() {
const value = [];
this.preorder(this.root, value_ => {
var _value_$path;
value.push(value_.word ? value_.word : value_ === this.root ? 0 : (_value_$path = value_.path) === null || _value_$path === void 0 ? void 0 : _value_$path.slice(-1));
});
return value;
}
isRoot(value) {
return value === this.root;
}
preorder(value, method) {
if (value !== undefined && is('function', method)) {
method(value, value.left, value.right);
this.preorder(value.left, method);
this.preorder(value.right, method);
}
}
make(value_) {
const items = copy(value_);
const onesyHuffmanTree = new OnesyHuffmanTree();
onesyHuffmanTree.root = new OnesyNode();
onesyHuffmanTree.root.index = 0;
function arrayToOnesyHuffmanTree(value) {
if (items[0] === '0' && !value.left) {
value.left = new OnesyNode();
value.left.index = 2 * value.index + 1;
value.left.path = '0';
items.splice(0, 1);
arrayToOnesyHuffmanTree(value.left);
}
if (items[0] === '1' && !value.right) {
value.right = new OnesyNode();
value.right.index = 2 * value.index + 2;
value.right.path = '1';
items.splice(0, 1);
arrayToOnesyHuffmanTree(value.right);
}
if (is('array', items[0])) {
if (items[0].length) {
if (!value.left) {
value.left = new OnesyNode(1, items[0][0]);
value.left.index = 2 * value.index + 1;
value.left.path = '0';
items[0].splice(0, 1);
}
if (!value.right && items[0].length) {
value.right = new OnesyNode(1, items[0][0]);
value.right.index = 2 * value.index + 2;
value.right.path = '1';
items[0].splice(0, 1);
}
if (!items[0].length) items.splice(0, 1);
} else items.splice(0, 1);
}
if (items[0] === '1' && !value.right) arrayToOnesyHuffmanTree(value);
}
arrayToOnesyHuffmanTree(onesyHuffmanTree.root);
return onesyHuffmanTree;
}
}
export const optionsDefault = {
encode_values: true,
base64: true
};
class OnesyHuffmanCode {
static get OnesyHuffmanCodeResponse() {
return OnesyHuffmanCodeResponse;
}
static get OnesyNode() {
return OnesyNode;
}
static get OnesyHuffmanTree() {
return OnesyHuffmanTree;
}
static encodeValue(value) {
if (!(is('string', value) && value.length)) return ''; // Add 1 at the start of every 3 characters
// it's more data, but there will be no bugs
// with padded 0s, a bug fix for now
return binaryStringToHexadecimal((value.match(/.{1,3}/g) || []).map(item => 1 + item).join('')).match(/.{1,2}/g).flatMap(item => {
if (item[0] === '0') return item.split('').map(item_ => String.fromCharCode(parseInt(item_, 16)));
return String.fromCharCode(parseInt(item, 16));
}).join('');
}
static decodeValue(value_) {
if (!(is('string', value_) && value_.length)) return '';
const value = value_.split('').map(item => item.charCodeAt(0).toString(16)).join('');
return (hexadecimalStringToBinary(value).match(/.{1,4}/g) || []).map(item => item.slice(1)).join('');
}
static encodeValues(values) {
let encodeValues = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
if (values) {
let result = '';
const keys = Object.keys(values);
keys.forEach((item, index) => result += "".concat(item).concat(encodeValues ? this.encodeValue(values[item]) : values[item]).concat(index < keys.length - 1 ? ' ' : ''));
return "".concat(encodeValues ? 1 : 0).concat(result);
}
}
static decodeValues(value) {
const result = {};
const values = [];
if (value) {
const encodeValues = value[0] === '1';
const values_ = value.slice(1).split(' ');
values_.forEach((item, index) => {
if (!item) values_[index + 1] = " ".concat(values_[index + 1]);else values.push(item);
});
values.forEach(item => result[item[0]] = encodeValues ? this.decodeValue(item.slice(1)) : item.slice(1));
}
return result;
}
static getValues(onesyHuffmanTree) {
const values = {};
const leafs = [];
if (onesyHuffmanTree) {
onesyHuffmanTree.preorder(onesyHuffmanTree.root, (value, left, right) => {
if (onesyHuffmanTree.isRoot(value)) {
value.path = value.maxDepth === 1 ? '0' : '';
if (value.leaf) leafs.push(value);
}
if (left) {
left.path = value.path + 0;
if (left.leaf) leafs.push(left);
}
if (right) {
right.path = value.path + 1;
if (right.leaf) leafs.push(right);
}
});
}
leafs.filter(leaf => leaf.word).forEach(leaf => values[leaf.word] = leaf.path);
return values;
}
static decode(value, values) {
const instance = new OnesyHuffmanCode();
instance.values = values;
return instance.decode(value);
}
static encodeBase64(value) {
return to(value, 'base64');
}
static decodeBase64(value) {
return to(value, 'string');
}
get encoded() {
return this.response;
}
get entropy() {
const output = Object.keys(this.probabilities).reduce((result, key) => result += this.probabilities[key] * Math.log2(this.probabilities[key]), 0);
return Math.abs(Number(output.toFixed(3)));
}
get averageCodeWordLength() {
const output = Object.keys(this.probabilities).reduce((result, key) => {
var _this$values$key;
return result += this.probabilities[key] * (((_this$values$key = this.values[key]) === null || _this$values$key === void 0 ? void 0 : _this$values$key.length) || 8);
}, 0);
return Number(output.toFixed(3));
}
get redundency() {
return Number(Math.abs(this.entropy - this.averageCodeWordLength).toFixed(3));
}
get efficiency() {
return Number((this.entropy / this.averageCodeWordLength || 0).toFixed(3));
}
constructor(value) {
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : optionsDefault;
this.value = value;
_defineProperty(this, "options", void 0);
_defineProperty(this, "huffmanTree", void 0);
_defineProperty(this, "probabilities", {});
_defineProperty(this, "values", {});
_defineProperty(this, "response", new OnesyHuffmanCodeResponse());
_defineProperty(this, "startTime", void 0);
this.options = merge(options, optionsDefault);
if (this.value !== undefined) this.init();
}
init() {
this.startTime = OnesyDate.milliseconds;
if (!Object.keys(this.probabilities).length && is('string', this.value)) {
// Frequencies
this.getProbabilities();
}
if (!Object.keys(this.values).length && Object.keys(this.probabilities).length) {
// Normalize probabilities
this.normalizeProbabilities(); // Make huffman tree
this.makeHuffmanTree(); // Values
this.values = OnesyHuffmanCode.getValues(this.huffmanTree);
} // Encode
this.encode();
}
encode() {
const response = new OnesyHuffmanCodeResponse();
if (Object.keys(this.values).length && is('string', this.value)) {
let value = Array.from(this.value).reduce((result, item) => result += this.values[item] || item, '');
value = OnesyHuffmanCode.encodeValue(value);
if (this.options.base64) value = OnesyHuffmanCode.encodeBase64(value);
response.value = value;
response.performance_milliseconds = OnesyDate.milliseconds - this.startTime;
response.performance = duration(response.performance_milliseconds) || '0 milliseconds';
response.values = this.values;
response.values_encoded = OnesyHuffmanCode.encodeValues(this.values, this.options.encode_values);
response.probabilities = this.probabilities;
response.efficiency = this.efficiency;
response.redundency = this.redundency;
response.entropy = this.entropy;
response.average_code_word_length = this.averageCodeWordLength;
response.original_byte_size = to(this.value, 'byte-size');
response.values_byte_size = to(response.values_encoded, 'byte-size');
response.value_byte_size = to(value, 'byte-size');
response.encoded_byte_size = response.values_byte_size + response.value_byte_size;
response.compression_ratio = Number(((response.encoded_byte_size + response.original_byte_size) / response.encoded_byte_size - 1).toFixed(2));
response.compression_percentage = response.original_byte_size === 0 ? response.value_byte_size === 0 ? 0 : response.value_byte_size * -100 : Number(((response.original_byte_size - response.encoded_byte_size) / response.original_byte_size * 100).toFixed(2));
response.positive = response.compression_ratio > 1;
this.response = response;
}
return response;
}
decode(value_) {
if (!value_) return new OnesyHuffmanCodeResponse(value_);
const response = new OnesyHuffmanCodeResponse(value_);
const startTime = OnesyDate.milliseconds;
const value = OnesyHuffmanCode.decodeValue(OnesyHuffmanCode.decodeBase64(value_));
if (is('string', value) && Object.keys(this.values).length) {
let input = value;
let output = '';
while (input.length) {
let valueWord = Object.keys(this.values).find(key => input.indexOf(this.values[key]) === 0);
if (!valueWord) {
// bug
valueWord = Object.keys(this.values).find(key => ('0' + input).indexOf(this.values[key]) === 0) || Object.keys(this.values).find(key => ('00' + input).indexOf(this.values[key]) === 0);
if (!valueWord) break;
}
output += valueWord;
input = input.slice(this.values[valueWord].length);
}
response.value = output;
response.performance_milliseconds = OnesyDate.milliseconds - startTime;
response.performance = duration(response.performance_milliseconds) || '0 milliseconds';
response.original_byte_size = to(output, 'byte-size');
response.value_byte_size = to(value_, 'byte-size');
}
return response;
}
getProbabilities() {
const value = this.value || '';
for (let i = 0; i < value.length; i++) {
this.probabilities[value[i]] = ~~this.probabilities[value[i]] + 1;
}
return this.probabilities;
}
normalizeProbabilities() {
const sum = Object.keys(this.probabilities).reduce((result, item) => result += this.probabilities[item], 0);
Object.keys(this.probabilities).forEach(key => this.probabilities[key] = Number((this.probabilities[key] / sum).toFixed(4)));
return this.probabilities;
}
makeHuffmanTree() {
let trees = [];
Object.keys(this.probabilities).forEach(key => {
const onesyNode = new OnesyNode(this.probabilities[key], key);
trees.push(onesyNode);
});
trees.sort((a, b) => a.value - b.value);
while (trees.length > 1) {
const first = trees[0];
const second = trees[1];
const newNode = new OnesyNode(first.value + second.value);
const children = [first, second].sort((a, b) => {
const aMaxDepth = a.maxDepth;
const bMaxDepth = b.maxDepth;
if (a.leaf && b.leaf || aMaxDepth === b.maxDepth) return b.value - a.value;
if (a.leaf || b.leaf) return a.leaf ? -1 : 1;
return aMaxDepth - bMaxDepth;
});
newNode.left = children[0];
newNode.right = children[1];
trees.push(newNode);
trees = trees.slice(2);
trees.sort((a, b) => a.value - b.value);
}
this.huffmanTree = new OnesyHuffmanTree();
this.huffmanTree.root = trees[0];
return this.huffmanTree;
}
}
export default OnesyHuffmanCode;