@onesy/lz77
Version:
276 lines (225 loc) • 10.8 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import is from '@onesy/utils/is';
import isEnvironment from '@onesy/utils/isEnvironment';
import to from '@onesy/utils/to';
import castParam from '@onesy/utils/castParam';
import OnesyDate from '@onesy/date/OnesyDate';
import duration from '@onesy/date/duration';
class OnesyLZ77Response {
constructor() {
let value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
let original_byte_size = arguments.length > 1 ? arguments[1] : undefined;
let value_byte_size = arguments.length > 2 ? arguments[2] : undefined;
let compression_ratio = arguments.length > 3 ? arguments[3] : undefined;
let compression_percentage = arguments.length > 4 ? arguments[4] : undefined;
let positive = arguments.length > 5 ? arguments[5] : undefined;
let performance_milliseconds = arguments.length > 6 ? arguments[6] : undefined;
let performance = arguments.length > 7 ? arguments[7] : undefined;
this.value = value;
this.original_byte_size = original_byte_size;
this.value_byte_size = value_byte_size;
this.compression_ratio = compression_ratio;
this.compression_percentage = compression_percentage;
this.positive = positive;
this.performance_milliseconds = performance_milliseconds;
this.performance = performance;
}
}
class OnesyLZ77 {
static get OnesyLZ77Response() {
return OnesyLZ77Response;
}
static decode(value) {
return new OnesyLZ77().decode(value);
}
static prefix(value) {
if (!is('string', value)) return '`';
const special = ['`', "'", "\"", '(', ')', '|', ',', '+', '*', '-', '.', '^', ';', ':', '=', '?', '[', ']', '_'];
let index = 0;
let amount = 1;
let prefix = '';
while (true) {
prefix = special[index].repeat(amount);
const regexp = new RegExp("\\".concat(prefix, "[^\\").concat(prefix, "]{1,4},[^\\").concat(prefix, "]{1,4}\\").concat(prefix));
const match = value.match(regexp);
if (!match) break; // Increment
if (index === special.length - 1) {
index = 0;
amount++;
} else index++;
}
return prefix;
}
get encoded() {
return this.response;
}
constructor(value) {
this.value = value;
_defineProperty(this, "variant", void 0);
_defineProperty(this, "response", new OnesyLZ77Response());
_defineProperty(this, "valueString", void 0);
_defineProperty(this, "valueEncoded", void 0);
if (this.value !== undefined) this.init();
}
init() {
if (['uint8array', 'buffer', 'string'].some(item => is(item, this.value))) {
this.getVariant();
if (is('string', this.value)) {
this.valueString = this.value;
this.valueEncoded = new TextEncoder().encode(this.value);
} else this.valueEncoded = this.value;
if (is('buffer', this.value)) this.valueString = this.value.toString('utf-8');
if (is('uint8array', this.value)) this.valueString = new TextDecoder().decode(this.value); // Encode
this.encode();
}
}
encode() {
const startTime = OnesyDate.milliseconds;
const prefix = OnesyLZ77.prefix(this.valueString);
const windowLength = 32e4;
const searchIndexes = {
start: -1,
end: 0
};
let search = this.valueEncoded.subarray(searchIndexes.start, searchIndexes.end);
const lookaheadIndexes = {
start: 0,
end: this.valueEncoded.length > windowLength ? windowLength : this.valueEncoded.length
};
let lookahead = this.valueEncoded.subarray(lookaheadIndexes.start, lookaheadIndexes.end);
let output = [];
const getItem = () => {
const allFirstMatches = [];
for (let i = searchIndexes.end - 1; i > -1; i--) {
if (this.valueEncoded[i] === lookahead[0]) allFirstMatches.push(i);
}
if (!allFirstMatches.length) return [0, 0, lookaheadIndexes.start, lookaheadIndexes.start];
let matchLongest = [];
allFirstMatches.sort();
for (let i = 0; i < allFirstMatches.length; i++) {
const start = allFirstMatches[i];
let offset = 2;
let equal = this.equal(this.valueEncoded.subarray(start, start + offset), this.valueEncoded.subarray(lookaheadIndexes.start, lookaheadIndexes.start + offset));
while (equal) {
offset += 1;
equal = this.equal(this.valueEncoded.subarray(start, start + offset), this.valueEncoded.subarray(lookaheadIndexes.start, lookaheadIndexes.start + offset));
}
if (!matchLongest.length || offset >= matchLongest[1]) matchLongest = [start, offset];
}
return [lookaheadIndexes.start - matchLongest[0], matchLongest[1] - 1, lookaheadIndexes.start, lookaheadIndexes.start + matchLongest[1] - 1];
};
while (lookahead.length) {
const [start, offset, lookaheadStartIndex, lookaheadEndIndex] = getItem();
const diff = Math.abs(start - offset);
let value = '';
const valueToAppend = this.valueEncoded[lookaheadEndIndex];
const actualValue = this.valueEncoded.subarray(lookaheadStartIndex - start, lookaheadStartIndex - start + offset);
if (!(start === 0 && offset === 0)) value = "".concat(prefix).concat(start === offset ? this.valueEncode(start) : "".concat(this.valueEncode(start), ",").concat(this.valueEncode(offset))).concat(prefix);
output.push(...(to(actualValue.reduce((result, item) => result += String.fromCharCode(item), ''), 'byte-size') <= to(value, 'byte-size') ? actualValue : [value]), valueToAppend);
const move = lookaheadEndIndex - lookaheadStartIndex + diff;
searchIndexes.start = search.length + lookahead.length >= windowLength ? searchIndexes.start + move : 0;
searchIndexes.end = lookaheadEndIndex + 1;
lookaheadIndexes.start = searchIndexes.end;
lookaheadIndexes.end += move;
search = this.valueEncoded.subarray(searchIndexes.start, searchIndexes.end);
lookahead = this.valueEncoded.subarray(lookaheadIndexes.start, lookaheadIndexes.end);
}
output = output.map(item => is('number', item) ? String.fromCharCode(item) : item).join('');
output = " ".concat(output);
const variantCodeValue = this.variantToValue();
if (variantCodeValue > 0 || prefix !== '`') {
if (prefix !== '`') output = "".concat(prefix).concat(output);
if (variantCodeValue > 0) output = "".concat(variantCodeValue).concat(output);
}
const response = new OnesyLZ77Response(output);
response.performance_milliseconds = OnesyDate.milliseconds - startTime;
response.performance = duration(response.performance_milliseconds) || '0 milliseconds';
response.original_byte_size = to(this.valueEncoded, 'byte-size');
response.value_byte_size = to(response.value, 'byte-size');
response.compression_ratio = Number(((response.value_byte_size + response.original_byte_size) / response.value_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.value_byte_size) / response.original_byte_size * 100).toFixed(2));
response.positive = response.compression_ratio > 1;
this.response = response;
return response;
}
decode(value_) {
const response = new OnesyLZ77Response();
const startTime = OnesyDate.milliseconds;
if (is('string', value_)) {
let meta = '';
let value = value_;
const indexSeparator = value_.indexOf(' ');
meta = value_.substring(0, indexSeparator);
value = value_.substring(indexSeparator + 1);
let variant = '0';
let prefix = '`';
if (['1', '2'].indexOf(meta[0]) > -1) {
variant = meta[0];
meta = meta.substring(1);
}
if (meta.length) prefix = meta;
const regexp = new RegExp("\\".concat(prefix, "[^\\").concat(prefix, "]{1,4}(,[^\\").concat(prefix, "]{1,4})?\\").concat(prefix), 'g');
const abbrs = value.match(regexp) || [];
for (const abbr of abbrs) {
let valueAbbr = abbr;
if (!valueAbbr.includes(',')) valueAbbr = "".concat(prefix).concat(valueAbbr.replace(new RegExp("".concat(prefix), 'g'), ''), ",").concat(valueAbbr.replace(new RegExp("".concat(prefix), 'g'), '')).concat(prefix);
let [position, offset] = valueAbbr.slice(1, -1).split(',');
[position, offset] = [position, offset].map(item => this.valueDecode(item));
const index = value.indexOf(abbr);
const addStartIndex = index - position;
const addEndIndex = addStartIndex + offset;
const array = [];
if (offset > position) {
let left = offset;
while (left > 0) {
const add = value.substring(addStartIndex, left < position ? addStartIndex + left : addStartIndex + position);
array.push(add);
left -= position;
}
} else {
const add = value.substring(addStartIndex, addEndIndex);
array.push(add);
}
const itemValue = array.join('');
value = value.replace(abbr, itemValue);
}
value = this.valueToVariant(value, variant); // Convert value back from UTF-16 -> Utf8Array -> Text decoded
// ie. back to Japanese characters
if (is('string', value)) {
const array = [];
value.split('').forEach(item => array.push(item.charCodeAt(0)));
value = new TextDecoder().decode(new Uint8Array(array));
}
response.value = value;
response.performance_milliseconds = OnesyDate.milliseconds - startTime;
response.performance = duration(response.performance_milliseconds) || '0 milliseconds';
response.original_byte_size = to(value, 'byte-size');
response.value_byte_size = to(value_, 'byte-size');
}
return response;
}
valueToVariant(value, variant_) {
const variant = castParam(variant_);
if (isEnvironment('nodejs') && variant === 1) return Buffer.from(value);
if (isEnvironment('browser') && variant === 1 || variant === 2) return new TextEncoder().encode(value);
return value;
}
variantToValue() {
let value = 0;
if (isEnvironment('nodejs') && this.variant === Buffer) value = 1;else if (this.variant === Uint8Array) value = 2;
return value;
}
valueEncode(value) {
return Number(value).toString(36);
}
valueDecode(value) {
return parseInt(value, 36);
}
getVariant() {
if (isEnvironment('nodejs') && Buffer.isBuffer(this.value)) this.variant = Buffer;else if (is('uint8array', this.value)) this.variant = Uint8Array;else this.variant = String;
}
equal(value, value1) {
return value.join('') === value1.join('');
}
}
export default OnesyLZ77;