Преобразовать двоичный/шестнадцатеричный код в Base91/Ascii85/Base64
Мне нужен кодировщик JS для base91 или Ascii85 для двоичного числа. У меня есть лист Google, который выглядит следующим образом:
Код:
function columnToLetter(column)
{
//https://stackoverflow.com/a/21231012/14226613
var temp, letter = '';
while (column > 0)
{
temp = (column - 1) % 26;
letter = String.fromCharCode(temp + 65) + letter;
column = (column - temp - 1) / 26;
}
return letter;
}
function onEdit(e){
var activeSheet = e.source.getActiveSheet();
if (activeSheet.getActiveCell().getRow() == 3) {
var spreadsheet = SpreadsheetApp.getActive();
var str = ''
for (i=0; i < 32; i++) {
var colName = columnToLetter(i+1)
if (spreadsheet.getRange(colName+ '3').isChecked()) {
str = str + '1'
} else {
str = str + '0'
}
}
let number = parseInt(str,2)
let hexx = ((+number).toString(16))
spreadsheet.getRange('R7').setValue(hexx);
}
}
В настоящее время кодирование в Hex работает хорошо, но мне нужен более эффективный способ кодирования этих двоичных флагов.
Цель: этот закодированный шаблон может быть в будущем частью названия продукта/запчасти, где у меня максимум 5-6 символов (как в 80-х :D).
Ascii85 было бы здорово, поэтому представление 'ffffffff' будет 's8W-!', где я бы сохранил 3 символа. Для тестирования/кодирования я использовал cryptii.
Решение должно быть чистым JS без внешних зависимостей/требований и/или должно работать в среде Google. Знаете ли вы какие-либо библиотеки, которые я мог бы использовать для этой цели? Base91 тоже подойдет — если у нас есть печатные символы. Идеальным решением был бы настраиваемый кодировщик/декодер JS, в котором шаблоны и символы для кодирования могут быть предварительно выбраны.
Обновлять:
Выяснилось, что Ascii85 или Base91 или не очень хорошо подходят для объявления кодов на телефоне, поэтому вы не хотите легко находить все символы на клавиатуре. Это правда, что base64 менее эффективен, но с адаптацией требований я смог найти решение с макс. 4-5 персонажей после нескольких дней экспериментов. Я попытаюсь ответить на свой вопрос - см. ниже. Обновленные требования:
- 16 бит для полезной нагрузки
- 4 бита (число 1..15) для выбора семейства/рецепта/типа
- 4 бита для CRC4
- кодировка base64 без внешних зависимостей и с регулируемым алфавитом
2 ответа
Это решение работает для меня. Целые 3 байта были закодированы в 4 символа. Я адаптировал алфавит для замены некоторых вводящих в заблуждение символов (0,O,i,l,1 и т. д.). Заполнение base64 ('=') удалено после кодирования и будет добавлено перед декодированием внутри функций. CRC4 не идеален - лучше, чем отсутствие CRC :)
Буду рад любым отзывам, предложениям по дальнейшей оптимизации. Спасибо.
Еще одна вкладка в этом документе, где вы можете определить варианты/семейства/рецепты:
Вот код для скрипта приложения (особая благодарность @Kamil Kiełczewski за его фрагмент base64):
// Convert a hex string to a byte array
function hexToBytes(hex) {
// https://stackoverflow.com/a/34356351/14226613
for (var bytes = [], c = 0; c < hex.length; c += 2)
bytes.push(parseInt(hex.substr(c, 2), 16));
return bytes;
}
function hexToBytes2(hexString) {
//https://stackoverflow.com/a/62365404/14226613
return hexString.match(/.{1,2}/g).map(x=> +('0x'+x));
}
// Convert a byte array to a hex string
function bytesToHex(bytes) {
// https://stackoverflow.com/a/34356351/14226613
for (var hex = [], i = 0; i < bytes.length; i++) {
var current = bytes[i] < 0 ? bytes[i] + 256 : bytes[i];
hex.push((current >>> 4).toString(16));
hex.push((current & 0xF).toString(16));
}
return hex.join("");
}
function hex2bin(hex, fill = false){
//https://stackoverflow.com/a/45054052/14226613
let bin = (parseInt(hex, 16).toString(2)).padStart(8, '0');
if (fill) {
//https://stackoverflow.com/a/27641835/14226613
bin = "000000000000000000000000".substr(bin.length) + bin;
}
return bin
}
function b64(){
const abc = "ABCDEFGH!JKLMN=PQRSTUVWXYZabcdefghijk:mnopqrstuvwxyz+-$%23456789"; // base64 alphabet
function encode (byteArray) {
//https://stackoverflow.com/a/62365404/14226613
const bin = n => n.toString(2).padStart(8,0); // convert num to 8-bit binary string
const l = byteArray.length
let result = '';
for(let i=0; i<=(l-1)/3; i++) {
let c1 = i*3+1>=l; // case when "=" is on end
let c2 = i*3+2>=l; // case when "=" is on end
let chunk = bin(byteArray[3*i]) + bin(c1? 0:byteArray[3*i+1]) + bin(c2? 0:byteArray[3*i+2]);
let r = chunk.match(/.{1,6}/g).map((x,j)=> j==3&&c2 ? '=' :(j==2&&c1 ? '=':abc[+('0b'+x)]));
result += r.join('');
}
//remove padding - http://jsfiddle.net/bicycle/c49fgz8x/
result = result.replace(/={1,2}$/, '');
return result;
}
function decode (str) {
//https://stackoverflow.com/a/62364519/14226613
let result = [];
//add padding http://jsfiddle.net/bicycle/c49fgz8x/
str = str + Array((4 - str.length % 4) % 4 + 1).join('=');
for(let i=0; i<str.length/4; i++) {
let chunk = [...str.slice(4*i,4*i+4)]
let bin = chunk.map(x=> abc.indexOf(x).toString(2).padStart(6,0)).join('');
let bytes = bin.match(/.{1,8}/g).map(x=> +('0b'+x));
result.push(...bytes.slice(0,3 - (str[4*i+2]=="=") - (str[4*i+3]=="=")));
}
return result;
}
return {
encode: encode,
decode: decode
}
}
var crc4 = function(data) {
//https://gist.github.com/bryc/5916452ad0d1ef5c39f1a3f19566d315
var POLY = 0xC, INIT = 0, XOROUT = 0;
for(var crc = INIT, i = 0; i < data.length; i++) {
crc = crc ^ data[i];
for(var j = 0; j < 8; j++) {
crc = crc & 1 ? crc >>> 1 ^ POLY : crc >>> 1;
}
}
return (crc ^ XOROUT) & 0xF;
};
function get_family(input, sheet, range) {
//get array with family names
let familiesValNames =sheet.getRange(range).getValues();
let obj = {};
for (i=0; i < familiesValNames.length; i++) {
if ( //if input is string - loop through array and find the corresponding number
(typeof input === 'string' && input == familiesValNames[i]) ||
//if it is a number between 1..15
(typeof input === 'number' && input == (i+1))) {
obj.number = i + 1;
//first values are in row 4 (=offset)
let sRange = 'A' + (4 + i) + ':P' + (4 + i)
obj.names = sheet.getRange(sRange).getValues();
break;
}
}
return obj
}
function set_family_object (sheet, range, fixed = false){
let famtypes={1:{family:["Spares ACME 1"],types:[["spare","Alpha v2","Alpha v1","Alpha v0","spare","Beta v2","Beta v1","Beta v0","spare","Gamma v2","Gamma v1","Gamma v0","spare","Delta v2","Delta v1","Delta v0"]]},2:{family:["Spares ACME 2"],types:[["spare","spare","spare","spare","spare","spare","spare","spare","spare","spare","spare","spare","Omega v2","Omega v2","Omega v1","Omega v0"]]},3:{family:["ACME Tools"],types:[["p612 lcp2","p612 lcp1","p612 lcp0","p512 lcp2","p512 lcp1","p512 lcp0","p671 lcp2","p671 lcp1","p671 lcp0","p471 lcp2","p471 lcp1","p471 lcp0","blm v2","blm v1","blm v0","common"]]},4:{family:["Animals"],types:[["Sea Otter","Cat","Panda","Fox","Dog","Lemur","Penguin","Mulgara","Degu","Kiwi","Koala","Monkey","Whale","Oribi","Chimpanzee","Deer"]]},5:{family:["Male Names"],types:[["James","John","Robert","Michael","Willian","David","Richard","Thomas","Charles","Daniel","Matthew","Donald","Mark","Brian","Edward","George"]]},6:{family:["Female Names"],types:[["Olivia","Emma","Ava","Sophia","Isabella","Charlotte","Amelia","Mia","Harper","Evelyn","Emily","Ella","Camila","Luna","Mila","Aria"]]},7:{family:["Car Brands"],types:[["Skoda","Renault","Citroen","Dacia","BMW","Audi","VW","Ford","Peugeot","Honda","Hyundai","Toyota","Kia","Opel","Porsche","Tata"]]},8:{family:["Horror Movies"],types:[["Wrong Turn","Descent","Hostel","Vacancy","Joy ride","Joy Ride","Midsommar","Cave","Eden Lake","Frozen","Ruins","Turistas","Hills have Eyes","Cabin of Woods","Soon the Darkness","Deep"]]},9:{family:["Things for Salad"],types:[["Lettuce","Cucumber","Cheese","Carrot","Tomato","Spinach","Crouton","Bacon","Egg","Onion","Chicken","Avocado","Pepper","Cabbage","Olive Oil","Feta"]]},10:{family:["TV Scientist"],types:[["Spock","Gaius Baltar","Leonard McCoy","Dr Bruce Banner","MacGyver","Dr Krieger","Walter White","Dexter","Lex Luthor","Walter Bishop","Data","The Doctor","Dr Quest","Dana Scully","Jimmy Neutron","Howard Wolowitz"]]},11:{family:["Funny Movies"],types:[["Spaceballs","Airplane!","Monty Python","Blazin Saddles","Lego Movie 2","Shrek","Naked gun","Austin Powers","Shaun of the Dead","Finding Nemo","The Big Lebowski","Dumb and Dumber","Simpsons","Toy Story","South Park","Life of Brian"]]},12:{family:["Actors"],types:[["Jamie Foxx","Val Kilmer","Christian Bale","Cate Blanchett","Jim Carrey","Meryl Streep","Joaquin Phoenix","Sam Rockwell","Leonardi DiCaprio","Heath Ledger","Robin Williams","Bill Murray","Will Ferrel","Jack Nicholson","Kevin spacey","Tom Cruise"]]},13:{family:["Tech Blogs"],types:[["TechCrunch","Wired","TechnoByte","Golem","Heise","Gizmodo","Engadget","Ars Technica","Techaeris","Top10Tech","10xDS","The Verge","TECHi","Urbangeekz","Techsppoks","Mashable"]]},14:{family:["PC Brands"],types:[["Asus","Intel","Dell","HP","Alienware","Microsoft","Apple","ACer","Sony","MSI","Razer","Toshiba","Gateway","LG","Compaq","Panasonic"]]},15:{family:["Evil Companies"],types:[["Facebook","Comcast","Time Warner","Google","Apple","Twitter","Go Daddy","Verizon","Yahoo","Microsoft","Zynga","BuzzFeed","McAfee","Amazon","Youtube","Hulu"]]}};
if (fixed) {
return famtypes
} else {
let familiesValNames =sheet.getRange(range).getValues();
let sRange
let obj = {};
for (i=0; i < 15; i++) {
sRange = 'A' + (4 + i) + ':P' + (4 + i)
Object.assign(obj, {[i +1]: {family: familiesValNames[i]}});
obj[i +1].types = sheet.getRange(sRange).getValues();
}
}
return JSON.stringify(obj);
}
function encoder (sheet, currentRow, colOffset, typeColNo, famSheet, famSheetTypesRange) {
let ret = {}
var str = ''
//loop through columns 3 to 18 of the corresponding row and iterate if its checked -> result binary string
for (i=0; i < 16; i++) {
str = (sheet.getRange(currentRow, (i+colOffset)).isChecked()) ? str + '1' : str + '0';
}
//shift one byte to left for easier calculation
//result are the first 2 byte (0xnnnnFF) of 3 (0xFFFFFF)
let var_bin = ((+parseInt(str,2)) << 8).toString(2)
//format binary as 3 byte
var_bin = ("000000000000000000000000".substr(var_bin.length) + var_bin);
//convert to hex
ret.variant_hex = parseInt(var_bin, 2).toString(16)
//get selected family type
ret.family_str = sheet.getRange(currentRow, typeColNo).getValue()
//get number of this selected type and convert it to hex
ret.family_hex = (get_family(ret.family_str, famSheet, famSheetTypesRange).number >>> 0).toString(16).toUpperCase();
//calculate intermedia hex value; add a trailing '0' to move the family to the upper 4bits of the last byte
ret.joined_hex = (parseInt(ret.variant_hex, 16) + parseInt((ret.family_hex + '0'), 16)).toString(16)
//convert to binary - better to make CRC with binary (1111 1111) than with HEX (ff)
let joined_bin = (parseInt(ret.joined_hex, 16)).toString(2)
//get crc and convert it to hex
ret.crc_hex = crc4(joined_bin).toString(16).toUpperCase()
//final hex code
ret.final_hex = (parseInt(ret.joined_hex, 16) + parseInt(ret.crc_hex, 16)).toString(16).toLocaleUpperCase()
//prepare and encode into base64
/** @type {number[]} */
ret.finalByteArr = [];
ret.finalByteArr[0] = ((parseInt(ret.final_hex, 16) & (parseInt('ff0000', 16))) >>> 16)
ret.finalByteArr[1] = ((parseInt(ret.final_hex, 16) & (parseInt('00ff00', 16))) >>> 8)
ret.finalByteArr[2] = (parseInt(ret.final_hex, 16) & (parseInt('0000ff', 16)))
//ret.finalByteArr = hexToBytes(ret.final_hex)
ret.b64 = b64().encode(ret.finalByteArr)
return ret
}
function decoder(str) {
var ret = {}
//will find on the end of the string the code; trailing whitespaces will be ignored
const regEx = /^(?:.*)#(.{4})(?:\s*)$/
//check if string is valid (checksum will be checked later)
ret.strValid = regEx.test(str);
if (ret.strValid) {
let result = str.match(regEx)
//first capturing group
ret.b64 = result[1];
//convert to a byte array
ret.byteArray = b64().decode(ret.b64)
//convert to hex string
ret.hex = bytesToHex(ret.byteArray).toString(16).toUpperCase();
//convert the hex string to binary
ret.binary = hex2bin(ret.hex, true)
//get crc
ret.crc = (parseInt(ret.binary, 2) & parseInt('00000f', 16)).toString(16).toUpperCase();
//get last 2 bytes (mask the 1st out) and check if its suits to the checksum
let toCheck = (parseInt(ret.binary, 2) & parseInt('fffff0', 16)).toString(2);
ret.crcValid = (crc4(toCheck).toString(16).toUpperCase() == ret.crc) ? true: false;
//get only variant
ret.variant_bin = ((parseInt(ret.binary, 2) & parseInt('ffff00', 16)) >>> 8).toString(2);
//fill with zero's & make a mirror bit set for easier handling later -> [0] = lowest bit
let variant_bin_ext = (("0000000000000000".substr(ret.variant_bin.length)) + ret.variant_bin.toString())
ret.variant_bin_mirr = variant_bin_ext.split("").reverse().join("");
//convert to hex
ret.variant_hex = parseInt(ret.variant_bin, 2).toString(16).toUpperCase()
//get type - mask everythin irrelevant out and move 4 bits to right
ret.type = parseInt(((parseInt(ret.binary, 2) & parseInt('0000f0', 16)) >>> 4), 10)
}
else {
ret.strValid = false;
}
return ret
}
function onEdit(event){
var sheet = event.source.getActiveSheet();
var actualSheetName = sheet.getName();
var lastColumnRow = sheet.getLastColumn();
var editRange = sheet.getActiveRange();
var editCol = editRange.getColumn();
var editRow = editRange.getRow();
var value = sheet.getActiveCell().getValue();
var adress = sheet.getActiveCell().getA1Notation();
var familiesSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Families');
//evaluate if something has been changed in 'Familees'
if (actualSheetName == 'Families') {
let all_types = set_family_object(familiesSheet, 'Q4:Q18', true)
Logger.log(all_types)
Logger.log(all_types[1].family)
Logger.log(all_types[1].types)
Logger.log(all_types[15].family)
Logger.log(all_types[15].types)
}
//evaluate and fill out legend on the top of sheet
if (adress == 'S2' && actualSheetName == 'Spares') {
const str = sheet.getActiveCell().getValue();
var famObj = get_family(str, familiesSheet, 'Q4:Q18')
sheet.getRange('C2:R2').setValues(famObj.names)
}
//getRange(row, column, [numRows], [numColumns]) || getRange(a1Notation)
if (sheet.getActiveCell().getRow() >= 6 && sheet.getActiveCell().getRow() <= 21
&& sheet.getActiveCell().getColumn() >= 3 && sheet.getActiveCell().getColumn() <= 19
&& actualSheetName == 'Spares') {
let currentRow = sheet.getActiveCell().getRow();
let currentCol = sheet.getActiveCell().getColumn();
//calculate code
let codeEnc = encoder(sheet, currentRow, 3, 19, familiesSheet, 'Q4:Q18')
if (sheet.getRange(currentRow, 1).getValue().length > 3) {
sheet.getRange(currentRow, 20).setValue(codeEnc.crc_hex);
sheet.getRange(currentRow, 20).setComment('')
sheet.getRange(currentRow, 21).setValue(codeEnc.b64);
sheet.getRange(currentRow, 21).setComment('')
} else {
sheet.getRange(currentRow, 20).setValue('n/a');
sheet.getRange(currentRow, 20).setComment('Please enter a product name in the 1st column.')
sheet.getRange(currentRow, 21).setValue('n/a');
sheet.getRange(currentRow, 21).setComment('Please enter a product name in the 1st column.')
}
let decoded = decoder('grgrgr #' + codeEnc.b64)
Logger.log(codeEnc)
Logger.log(decoded)
var famObj2 = get_family(decoded.type, familiesSheet, 'Q4:Q18')
Logger.log(famObj2)
}
Результат можно попробовать/декодировать с помощью следующего фрагмента кода HTML/JS. Это быстро и грязно. Просто введите, например:
- Пневматический соединитель #Xs2a
- Камера #!$cg
- Блок питания 24В #p%qz
Вы можете попробовать вручную изменить код base64 - для проверки, работает ли CRC4.
function columnToLetter(col) {
const ss=SpreadsheetApp.getActive();
const sh=ss.getSheetByName('Sheet1');
return sh.getRange(1,col).getA1Notation().slice(0,-1);
}
Поскольку вы собираетесь использовать его несколько раз, создайте объект со всеми столбцами, которые вам нужны в качестве свойств, и он сделает для вас очень быстрый преобразователь без вызовов функций и любого времени переключения контекста.