@entestat/formula
Version:
fast excel formula parser
346 lines (290 loc) • 11.1 kB
JavaScript
const FormulaError = require('../error');
const {FormulaHelpers, Types, WildCard} = require('../helpers');
const H = FormulaHelpers;
// Spreadsheet number format
const ssf = require('../../ssf/ssf');
// Change number to Thai pronunciation string
const bahttext = require('bahttext');
// full-width and half-width converter
const charsets = {
latin: {halfRE: /[!-~]/g, fullRE: /[!-~]/g, delta: 0xFEE0},
hangul1: {halfRE: /[ᄀ-ᄒ]/g, fullRE: /[ᆨ-ᇂ]/g, delta: -0xEDF9},
hangul2: {halfRE: /[ᅡ-ᅵ]/g, fullRE: /[ᅡ-ᅵ]/g, delta: -0xEE61},
kana: {
delta: 0,
half: "。「」、・ヲァィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン゙゚",
full: "。「」、・ヲァィゥェォャュョッーアイウエオカキクケコサシ" +
"スセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン゛゜"
},
extras: {
delta: 0,
half: "¢£¬¯¦¥₩\u0020|←↑→↓■°",
full: "¢£¬ ̄¦¥₩\u3000│←↑→↓■○"
}
};
const toFull = set => c => set.delta ?
String.fromCharCode(c.charCodeAt(0) + set.delta) :
[...set.full][[...set.half].indexOf(c)];
const toHalf = set => c => set.delta ?
String.fromCharCode(c.charCodeAt(0) - set.delta) :
[...set.half][[...set.full].indexOf(c)];
const re = (set, way) => set[way + "RE"] || new RegExp("[" + set[way] + "]", "g");
const sets = Object.keys(charsets).map(i => charsets[i]);
const toFullWidth = str0 =>
sets.reduce((str, set) => str.replace(re(set, "half"), toFull(set)), str0);
const toHalfWidth = str0 =>
sets.reduce((str, set) => str.replace(re(set, "full"), toHalf(set)), str0);
const TextFunctions = {
ASC: (text) => {
text = H.accept(text, Types.STRING);
return toHalfWidth(text);
},
BAHTTEXT: (number) => {
number = H.accept(number, Types.NUMBER);
try {
return bahttext(number);
} catch (e) {
throw Error(`Error in https://github.com/jojoee/bahttext \n${e.toString()}`)
}
},
CHAR: (number) => {
number = H.accept(number, Types.NUMBER);
if (number > 255 || number < 1)
throw FormulaError.VALUE;
return String.fromCharCode(number);
},
CLEAN: (text) => {
text = H.accept(text, Types.STRING);
return text.replace(/[\x00-\x1F]/g, '');
},
CODE: (text) => {
text = H.accept(text, Types.STRING);
if (text.length === 0)
throw FormulaError.VALUE;
return text.charCodeAt(0);
},
CONCAT: (...params) => {
let text = '';
// does not allow union
H.flattenParams(params, Types.STRING, false, item => {
item = H.accept(item, Types.STRING);
text += item;
});
return text
},
CONCATENATE: (...params) => {
let text = '';
if (params.length === 0)
throw Error('CONCATENATE need at least one argument.');
params.forEach(param => {
// does not allow range reference, array, union
param = H.accept(param, Types.STRING);
text += param;
});
return text;
},
DBCS: (text) => {
text = H.accept(text, Types.STRING);
return toFullWidth(text);
},
DOLLAR: (number, decimals) => {
number = H.accept(number, Types.NUMBER);
decimals = H.accept(decimals, Types.NUMBER, 2);
const decimalString = Array(decimals).fill('0').join('');
// Note: does not support locales
// TODO: change currency based on user locale or settings from this library
return ssf.format(`$#,##0.${decimalString}_);($#,##0.${decimalString})`, number).trim();
},
EXACT: (text1, text2) => {
text1 = H.accept(text1, [Types.STRING]);
text2 = H.accept(text2, [Types.STRING]);
return text1 === text2;
},
FIND: (findText, withinText, startNum) => {
findText = H.accept(findText, Types.STRING);
withinText = H.accept(withinText, Types.STRING);
startNum = H.accept(startNum, Types.NUMBER, 1);
if (startNum < 1 || startNum > withinText.length)
throw FormulaError.VALUE;
const res = withinText.indexOf(findText, startNum - 1);
if (res === -1)
throw FormulaError.VALUE;
return res + 1;
},
FINDB: (...params) => {
return TextFunctions.FIND(...params);
},
FIXED: (number, decimals, noCommas) => {
number = H.accept(number, Types.NUMBER);
decimals = H.accept(decimals, Types.NUMBER, 2);
noCommas = H.accept(noCommas, Types.BOOLEAN, false);
const decimalString = Array(decimals).fill('0').join('');
const comma = noCommas ? '' : '#,';
return ssf.format(`${comma}##0.${decimalString}_);(${comma}##0.${decimalString})`, number).trim();
},
LEFT: (text, numChars) => {
text = H.accept(text, Types.STRING);
numChars = H.accept(numChars, Types.NUMBER, 1);
if (numChars < 0)
throw FormulaError.VALUE;
if (numChars > text.length)
return text;
return text.slice(0, numChars);
},
LEFTB: (...params) => {
return TextFunctions.LEFT(...params);
},
LEN: (text) => {
text = H.accept(text, Types.STRING);
return text.length;
},
LENB: (...params) => {
return TextFunctions.LEN(...params);
},
LOWER: (text) => {
text = H.accept(text, Types.STRING);
return text.toLowerCase();
},
MID: (text, startNum, numChars) => {
text = H.accept(text, Types.STRING);
startNum = H.accept(startNum, Types.NUMBER);
numChars = H.accept(numChars, Types.NUMBER);
if (startNum > text.length)
return '';
if (startNum < 1 || numChars < 1)
throw FormulaError.VALUE;
return text.slice(startNum - 1, startNum + numChars - 1);
},
MIDB: (...params) => {
return TextFunctions.MID(...params);
},
NUMBERVALUE: (text, decimalSeparator, groupSeparator) => {
text = H.accept(text, Types.STRING);
// TODO: support reading system locale and set separators
decimalSeparator = H.accept(decimalSeparator, Types.STRING, '.');
groupSeparator = H.accept(groupSeparator, Types.STRING, ',');
if (text.length === 0)
return 0;
if (decimalSeparator.length === 0 || groupSeparator.length === 0)
throw FormulaError.VALUE;
decimalSeparator = decimalSeparator[0];
groupSeparator = groupSeparator[0];
if (decimalSeparator === groupSeparator
|| text.indexOf(decimalSeparator) < text.lastIndexOf(groupSeparator))
throw FormulaError.VALUE;
const res = text.replace(groupSeparator, '')
.replace(decimalSeparator, '.')
// remove chars that not related to number
.replace(/[^\-0-9.%()]/g, '')
.match(/([(-]*)([0-9]*[.]*[0-9]+)([)]?)([%]*)/);
if (!res)
throw FormulaError.VALUE;
// ["-123456.78%%", "(-", "123456.78", ")", "%%"]
const leftParenOrMinus = res[1].length, rightParen = res[3].length, percent = res[4].length;
let number = Number(res[2]);
if (leftParenOrMinus > 1 || leftParenOrMinus && !rightParen
|| !leftParenOrMinus && rightParen || isNaN(number))
throw FormulaError.VALUE;
number = number / 100 ** percent;
return leftParenOrMinus ? -number : number;
},
PHONETIC: () => {
},
PROPER: (text) => {
text = H.accept(text, [Types.STRING]);
text = text.toLowerCase();
text = text.charAt(0).toUpperCase() + text.slice(1);
return text.replace(/(?:[^a-zA-Z])([a-zA-Z])/g,
letter => letter.toUpperCase());
},
REPLACE: (old_text, start_num, num_chars, new_text) => {
old_text = H.accept(old_text, [Types.STRING]);
start_num = H.accept(start_num, [Types.NUMBER]);
num_chars = H.accept(num_chars, [Types.NUMBER]);
new_text = H.accept(new_text, [Types.STRING]);
let arr = old_text.split("");
arr.splice(start_num - 1, num_chars, new_text);
return arr.join("");
},
REPLACEB: (...params) => {
return TextFunctions.REPLACE(...params)
},
REPT: (text, number_times) => {
text = H.accept(text, Types.STRING);
number_times = H.accept(number_times, Types.NUMBER);
let str = "";
for (let i = 0; i < number_times; i++) {
str += text;
}
return str;
},
RIGHT: (text, numChars) => {
text = H.accept(text, Types.STRING);
numChars = H.accept(numChars, Types.NUMBER, 1);
if (numChars < 0)
throw FormulaError.VALUE;
const len = text.length;
if (numChars > len)
return text;
return text.slice(len - numChars);
},
RIGHTB: (...params) => {
return TextFunctions.RIGHT(...params);
},
SEARCH: (findText, withinText, startNum) => {
findText = H.accept(findText, Types.STRING);
withinText = H.accept(withinText, Types.STRING);
startNum = H.accept(startNum, Types.NUMBER, 1);
if (startNum < 1 || startNum > withinText.length)
throw FormulaError.VALUE;
// transform to js regex expression
let findTextRegex = WildCard.isWildCard(findText) ? WildCard.toRegex(findText, 'i') : findText;
const res = withinText.slice(startNum - 1).search(findTextRegex);
if (res === -1)
throw FormulaError.VALUE;
return res + startNum;
},
SEARCHB: (...params) => {
return TextFunctions.SEARCH(...params)
},
SUBSTITUTE: (...params) => {
},
T: (value) => {
// extract the real parameter
value = H.accept(value);
if (typeof value === "string")
return value;
return '';
},
TEXT: (value, formatText) => {
value = H.accept(value, Types.NUMBER);
formatText = H.accept(formatText, Types.STRING);
// I know ssf contains bugs...
try {
return ssf.format(formatText, value);
} catch (e) {
console.error(e)
throw FormulaError.VALUE;
}
},
TEXTJOIN: (...params) => {
},
TRIM: (text) => {
text = H.accept(text, [Types.STRING]);
return text.replace(/^\s+|\s+$/g, '')
},
UNICHAR: (number) => {
number = H.accept(number, [Types.NUMBER]);
if (number <= 0)
throw FormulaError.VALUE;
return String.fromCharCode(number);
},
UNICODE: (text) => {
return TextFunctions.CODE(text);
},
UPPER: (text) => {
text = H.accept(text, Types.STRING);
return text.toUpperCase();
},
};
module.exports = TextFunctions;