feat: Office viewer 的 Excel 中实现公式解析和执行

This commit is contained in:
wuduoyi 2024-05-07 10:46:06 +08:00 committed by lmaomaoz
parent f079ca1128
commit d1b1cebc89
190 changed files with 1058728 additions and 332 deletions

1
.gitattributes vendored
View File

@ -12,3 +12,4 @@
*.xlsx binary
*.pptx binary
*.TTF binary
*.sketch binary

View File

@ -526,7 +526,6 @@ if (fis.project.currentMedia() === 'publish-sdk') {
'!markdown-it-html5-media/**',
'!punycode/**',
'!office-viewer/**',
'!fflate/**',
'!numfmt/**',
'!amis-formula/lib/doc.js'
],
@ -581,7 +580,7 @@ if (fis.project.currentMedia() === 'publish-sdk') {
'echarts-wordcloud/**'
],
'office-viewer.js': ['office-viewer/**', 'fflate/**', 'numfmt/**'],
'office-viewer.js': ['office-viewer/**', 'numfmt/**'],
'json-view.js': 'react-json-view/**',
'fomula-doc.js': 'amis-formula/lib/doc.js',
@ -610,7 +609,6 @@ if (fis.project.currentMedia() === 'publish-sdk') {
'!markdown-it/**',
'!markdown-it-html5-media/**',
'!office-viewer/**',
'!fflate/**',
'!numfmt/**'
]
}),
@ -846,7 +844,6 @@ if (fis.project.currentMedia() === 'publish-sdk') {
'!markdown-it-html5-media/**',
'!punycode/**',
'!amis-formula/**',
'!fflate/**',
'!numfmt/**',
'!office-viewer/**',
'!amis-core/**',
@ -914,7 +911,7 @@ if (fis.project.currentMedia() === 'publish-sdk') {
'!/examples/components/EChartsEditor/Common.tsx'
],
'pkg/office-viewer.js': ['office-viewer/**', 'fflate/**', 'numfmt/**'],
'pkg/office-viewer.js': ['office-viewer/**', 'numfmt/**'],
'pkg/rest.js': [
'**.{js,jsx,ts,tsx}',
@ -939,7 +936,6 @@ if (fis.project.currentMedia() === 'publish-sdk') {
'!uc.micro/**',
'!markdown-it/**',
'!markdown-it-html5-media/**',
'!fflate/**',
'!numfmt/**'
],

View File

@ -3,3 +3,4 @@ lib
esm
.rollup.cache
~$*
.antlr

View File

@ -1,52 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "bun",
"request": "launch",
"name": "Debug Bun",
// The path to a JavaScript or TypeScript file to run.
"program": "${file}",
// The arguments to pass to the program, if any.
"args": [],
// The working directory of the program.
"cwd": "${workspaceFolder}",
// The environment variables to pass to the program.
"env": {},
// If the environment variables should not be inherited from the parent process.
"strictEnv": false,
// If the program should be run in watch mode.
// This is equivalent to passing `--watch` to the `bun` executable.
// You can also set this to "hot" to enable hot reloading using `--hot`.
"watchMode": false,
// If the debugger should stop on the first line of the program.
"stopOnEntry": false,
// If the debugger should be disabled. (for example, breakpoints will not be hit)
"noDebug": false,
// The path to the `bun` executable, defaults to your `PATH` environment variable.
"runtime": "bun",
// The arguments to pass to the `bun` executable, if any.
// Unlike `args`, these are passed to the executable itself, not the program.
"runtimeArgs": []
},
{
"type": "bun",
"request": "attach",
"name": "Attach to Bun",
// The URL of the WebSocket inspector to attach to.
// This value can be retrieved by using `bun --inspect`.
"url": "ws://localhost:6499/"
}
]
}

View File

@ -1,29 +1,522 @@
{
"cSpell.words": [
"ABS",
"ACCRINT",
"ACCRINTM",
"ACOS",
"ACOSH",
"ACOT",
"ACOTH",
"ADDRESS",
"AGGREGATE",
"AMORDEGRC",
"AMORLINC",
"AND",
"ARABIC",
"AREAS",
"ARRAYTOTEXT",
"ASC",
"ASIN",
"ASINH",
"ATAN",
"ATAN2",
"ATANH",
"AVEDEV",
"AVERAGE",
"AVERAGEA",
"AVERAGEIF",
"AVERAGEIFS",
"BAHTTEXT",
"BASE",
"BESSELI",
"BESSELJ",
"BESSELK",
"BESSELY",
"BETA.DIST",
"BETA.INV",
"BETADIST",
"BETAINV",
"BIN2DEC",
"BIN2HEX",
"BIN2OCT",
"BINOM.DIST",
"BINOM.DIST.RANGE",
"BINOM.INV",
"BINOMDIST",
"BITAND",
"BITLSHIFT",
"BITOR",
"BITRSHIFT",
"BITXOR",
"CALL",
"CDXCIX",
"CEILING",
"CEILING.MATH",
"CEILING.PRECISE",
"CELL",
"cellspacing",
"cfvo",
"CHAR",
"CHIDIST",
"CHIINV",
"CHISQ.DIST",
"CHISQ.DIST.RT",
"CHISQ.INV",
"CHISQ.INV.RT",
"CHISQ.TEST",
"CHITEST",
"CLEAN",
"CODE",
"colspan",
"COLUMN",
"COLUMNS",
"COMBIN",
"COMBINA",
"COMPLEX",
"CONCAT",
"CONCATENATE",
"CONFIDENCE",
"CONFIDENCE.NORM",
"CONFIDENCE.T",
"CONFINT",
"Consts",
"CONVERT",
"CORREL",
"COS",
"COSH",
"COT",
"COTH",
"COUNT",
"COUNTA",
"COUNTBLANK",
"COUNTIF",
"COUNTIFS",
"COUPDAYBS",
"COUPDAYS",
"COUPDAYSNC",
"COUPNCD",
"COUPNUM",
"COUPPCD",
"COVAR",
"COVARIANCE.P",
"COVARIANCE.S",
"CRITBINOM",
"CSC",
"CSCH",
"CUBEKPIMEMBER",
"CUBEMEMBER",
"CUBEMEMBERPROPERTY",
"CUBERANKEDMEMBER",
"CUBESET",
"CUBESETCOUNT",
"CUBEVALUE",
"CUMIPMT",
"CUMPRINC",
"cust",
"DATE",
"DATEDIF",
"DATEVALUE",
"DAVERAGE",
"DAY",
"DAYS",
"DAYS360",
"DB",
"DBCS",
"DCCC",
"DCOUNT",
"DCOUNTA",
"DDB",
"DEC2BIN",
"DEC2HEX",
"DEC2OCT",
"DECIMAL",
"DEGREES",
"DELTA",
"DEVSQ",
"DGET",
"DISC",
"Divs",
"DMAX",
"DMIN",
"DOLLAR",
"DOLLARDE",
"DOLLARFR",
"DPRODUCT",
"DSTDEV",
"DSTDEVP",
"DSUM",
"DURATION",
"DVAR",
"DVARP",
"dxfs",
"echarts",
"EDATE",
"EFFECT",
"ENCODEURL",
"EOMONTH",
"ERF",
"ERF.PRECISE",
"ERFC",
"ERFC.PRECISE",
"ERROR.TYPE",
"EUROCONVERT",
"EVEN",
"EXACT",
"EXP",
"EXPON.DIST",
"EXPONDIST",
"F.DIST",
"F.DIST.RT",
"F.INV",
"F.INV.RT",
"F.TEST",
"FACT",
"FACTDOUBLE",
"FALSE",
"FDIST",
"fflate",
"FILTER",
"FILTERXML",
"FIND",
"FINDB",
"FINV",
"FISHER",
"FISHERINV",
"FIXED",
"FLOOR",
"FLOOR.MATH",
"FLOOR.PRECISE",
"Fmla",
"Fmts",
"FORECAST",
"FORECAST.ETS",
"FORECAST.ETS.CONFINT",
"FORECAST.ETS.SEASONALITY",
"FORECAST.ETS.STAT",
"FORECAST.LINEAR",
"Formattings",
"FORMULATEXT",
"FREQUENCY",
"FTEST",
"FV",
"FVSCHEDULE",
"GAMMA",
"GAMMA.DIST",
"GAMMA.INV",
"GAMMADIST",
"GAMMAINV",
"GAMMALN",
"GAMMALN.PRECISE",
"GAUSS",
"GCD",
"GEOMEAN",
"GESTEP",
"GETPIVOTDATA",
"GOSA",
"GROWTH",
"HARMEAN",
"HEX2BIN",
"HEX2DEC",
"HEX2OCT",
"hlink",
"HLOOKUP",
"HOUR",
"HYPERLINK",
"HYPGEOM.DIST",
"HYPGEOMDIST",
"IFERROR",
"IFNA",
"IFS",
"IMABS",
"IMAGINARY",
"IMARGUMENT",
"IMCONJUGATE",
"IMCOS",
"IMCOSH",
"IMCOT",
"IMCSC",
"IMCSCH",
"IMDIV",
"IMEXP",
"IMLN",
"IMLOG10",
"IMLOG2",
"IMPOWER",
"IMPRODUCT",
"IMREAL",
"IMSEC",
"IMSECH",
"IMSIN",
"IMSINH",
"IMSQRT",
"IMSUB",
"IMSUM",
"IMTAN",
"INFO",
"INT",
"INTERCEPT",
"INTRATE",
"IPMT",
"IRR",
"ISBLANK",
"ISERR",
"ISERROR",
"ISEVEN",
"ISFORMULA",
"ISLOGICAL",
"ISNA",
"ISNONTEXT",
"ISNUMBER",
"ISO.CEILING",
"ISODD",
"ISOWEEKNUM",
"ISPMT",
"ISREF",
"ISTEXT",
"JIS",
"jstat",
"KURT",
"LARGE",
"Lbls",
"LCM",
"LEFT",
"LEFTB",
"LEN",
"LENB",
"LET",
"Levithan",
"LINEST",
"LN",
"LOG",
"LOG10",
"LOGEST",
"LOGINV",
"LOGNORM.DIST",
"LOGNORM.INV",
"LOGNORMDIST",
"LOOKUP",
"LOWER",
"LVIIA",
"LXXX",
"MATCH",
"MAX",
"MAXA",
"MAXIFS",
"MDETERM",
"MDLV",
"MDURATION",
"MEDIAN",
"MID",
"MIDB",
"MIN",
"MINA",
"MINIFS",
"MINUTE",
"MINVERSE",
"MIRR",
"MMULT",
"MOD",
"MODE",
"MODE.MULT",
"MODE.SNGL",
"moize",
"MONTH",
"MROUND",
"MULTINOMIAL",
"MUNIT",
"N",
"NA",
"NEGBINOM.DIST",
"NEGBINOMDIST",
"NETWORKDAYS",
"NETWORKDAYS.INTL",
"NOMINAL",
"NORM.DIST",
"NORM.INV",
"NORM.S.DIST",
"NORM.S.INV",
"NORMDIST",
"NORMINV",
"NORMSDIST",
"NORMSINV",
"NOT",
"NOW",
"NPER",
"NPV",
"NUMBERVALUE",
"numfmt",
"OCT2BIN",
"OCT2DEC",
"OCT2HEX",
"ODD",
"ODDFPRICE",
"ODDFYIELD",
"ODDLPRICE",
"ODDLYIELD",
"openxml",
"OR",
"papaparse",
"PDURATION",
"PEARSON",
"PERCENTILE",
"PERCENTILE.EXC",
"PERCENTILE.INC",
"PERCENTRANK",
"PERCENTRANK.EXC",
"PERCENTRANK.INC",
"PERMUT",
"PERMUTATIONA",
"PHI",
"PHONETIC",
"PI",
"PMT",
"POISSON",
"POISSON.DIST",
"POWER",
"PPMT",
"PRICE",
"PRICEDISC",
"PRICEMAT",
"PROB",
"PRODUCT",
"PROPER",
"prst",
"PV",
"QUARTILE",
"QUARTILE.EXC",
"QUARTILE.INC",
"QUOTIENT",
"RADIANS",
"RAND",
"RANDARRAY",
"RANDBETWEEN",
"RANK",
"RANK.AVG",
"RANK.EQ",
"RATE",
"RECEIVED",
"REGISTER.ID",
"rels",
"relt",
"REPLACE",
"REPLACEB",
"REPT",
"RIGHT",
"RIGHTB",
"ROMAN",
"ROUND",
"ROUNDDOWN",
"ROUNDUP",
"ROW",
"ROWS",
"rowspan",
"RRI",
"RSQ",
"RTD",
"SEARCH",
"SEARCHB",
"SEC",
"SECH",
"SECOND",
"SEQUENCE",
"SERIESSUM",
"SHEET",
"SHEETS",
"SIGN",
"SIN",
"SINH",
"SKEW",
"SKEW.P",
"SLN",
"SLOPE",
"SMALL",
"SORT",
"SORTBY",
"Sparkline",
"sparklines",
"sqref",
"xfrm"
"SQRT",
"SQRTPI",
"STANDARDIZE",
"STDEV",
"STDEV.P",
"STDEV.S",
"STDEVA",
"STDEVP",
"STDEVPA",
"STEYX",
"SUBSTITUTE",
"SUBTOTAL",
"SUM",
"SUMIF",
"SUMIFS",
"SUMPRODUCT",
"SUMSQ",
"SUMX2MY2",
"SUMX2PY2",
"SUMXMY2",
"SWITCH",
"SYD",
"T",
"T.DIST",
"T.DIST.2T",
"T.DIST.RT",
"T.INV",
"T.INV.2T",
"T.TEST",
"TAN",
"TANH",
"TBILLEQ",
"TBILLPRICE",
"TBILLYIELD",
"TDIST",
"TEXT",
"TEXTJOIN",
"TIME",
"TIMEVALUE",
"TINV",
"TODAY",
"TRANSPOSE",
"TREND",
"TRIM",
"TRIMMEAN",
"TRUE",
"TRUNC",
"TTEST",
"TYPE",
"UNICHAR",
"UNICODE",
"UNIQUE",
"UPPER",
"VALUE",
"VALUETOTEXT",
"VAR",
"VAR.P",
"VAR.S",
"VARA",
"VARP",
"VARPA",
"VDB",
"VLOOKUP",
"WEBSERVICE",
"WEEKDAY",
"WEEKNUM",
"WEIBULL",
"WEIBULL.DIST",
"WORKDAY",
"WORKDAY.INTL",
"xfrm",
"XIRR",
"XLOOKUP",
"XMATCH",
"XNPV",
"XOR",
"YEAR",
"YEARFRAC",
"YIELD",
"YIELDDISC",
"YIELDMAT",
"Z.TEST",
"ZTEST"
]
}

View File

@ -0,0 +1,27 @@
{
"$schema": "https://biomejs.dev/schemas/1.7.2/schema.json",
"formatter": {
"enabled": true,
"formatWithErrors": false,
"indentStyle": "space",
"indentWidth": 2,
"lineEnding": "lf",
"lineWidth": 80,
"attributePosition": "auto"
},
"organizeImports": { "enabled": true },
"linter": { "enabled": true, "rules": { "recommended": true } },
"javascript": {
"formatter": {
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
"trailingComma": "none",
"semicolons": "always",
"arrowParentheses": "asNeeded",
"bracketSpacing": false,
"bracketSameLine": false,
"quoteStyle": "single",
"attributePosition": "auto"
}
}
}

View File

@ -3,7 +3,7 @@
<title>filterDown</title>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="filterDown" transform="translate(2, 2)">
<rect id="Rectangle" stroke="#717171" stroke-width="4" x="0" y="0" width="213" height="213"></rect>
<rect id="Rectangle" stroke="#717171" stroke-width="4" fill="#FFFFFF" x="0" y="0" width="213" height="213"></rect>
<path d="M192.487604,49.3238637 C193.452114,51.6534092 193.054963,53.6420455 191.29615,55.2897728 L149.339943,97.3068182 L149.339943,160.545454 C149.339943,162.931818 148.233592,164.607954 146.020892,165.573864 C145.283325,165.857955 144.574126,166 143.893295,166 C142.361426,166 141.084867,165.460227 140.063621,164.380682 L118.277031,142.5625 C117.199048,141.482955 116.660057,140.204545 116.660057,138.727273 L116.660057,97.3068182 L74.7038504,55.2897728 C72.9450371,53.6420455 72.5478857,51.6534092 73.5123962,49.3238637 C74.4769068,47.1079547 76.1506161,46 78.5335243,46 L187.466476,46 C189.849384,46 191.523093,47.1079546 192.487604,49.3238637 Z" id="Path" fill="#717171" fill-rule="nonzero"></path>
<polygon id="down_arrow" fill="#717171" fill-rule="nonzero" transform="translate(50, 113) rotate(180) translate(-50, -113)" points="42.4971879 82.9240649 42.4971879 173 57.5028121 173 57.5028121 82.9240649 80 82.9240649 50 53 20 82.9240649"></polygon>
</g>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -3,6 +3,8 @@ import {OfficeViewer} from '../src/OfficeViewer';
import {createOfficeViewer} from '../src/createOfficeViewer';
import XMLPackageParser from '../src/package/XMLPackageParser';
let office: OfficeViewer;
export class App {
dir: string;
fileLists: Record<string, string[]>;
@ -48,6 +50,15 @@ export class App {
this.initFile = initFile || '';
this.renderFileList();
const uploadFile = document.getElementById('uploadFile')!;
uploadFile.addEventListener('change', e => {
const files = (e.target as HTMLInputElement).files;
if (files && files.length > 0) {
this.renderDrop(files[0]);
}
e.stopPropagation();
});
}
renderFileList() {
@ -100,7 +111,9 @@ export class App {
*
*/
async renderOffice(data: ArrayBuffer, fileName: string) {
let office: OfficeViewer;
if (office) {
office.destroy();
}
if (fileName.endsWith('.xml')) {
office = await createOfficeViewer(
data,

View File

@ -27,6 +27,14 @@
<button type="button" onclick="downloadDocx()">dl</button>
<button type="button" onclick="printDocx()">print</button>
<div id="files"></div>
<div id="upload">
<input
type="file"
id="uploadFile"
name="uploadFile"
accept=".docx, .xlsx"
/>
</div>
<div id="mousePosition"></div>
<div id="hitTestResult"></div>
</div>

View File

@ -24,6 +24,14 @@
</label>
</p>
<div id="files"></div>
<div id="upload">
<input
type="file"
id="uploadFile"
name="uploadFile"
accept=".docx, .xlsx"
/>
</div>
</div>
<div class="column">
<div id="viewer"></div>

View File

@ -28,6 +28,7 @@
"genIcons": "ts-node --transpileOnly tools/genIcons.ts",
"emptyExcel": "ts-node --transpileOnly tools/convertFileToBase64.ts src/excel/io/csv/empty.xlsx src/excel/io/csv/emptyXLSX.ts",
"genExampleFileList": "ts-node --transpileOnly tools/genExampleFileList.ts",
"functionCount": "bun tools/functionCount.ts",
"typecheck": "tsc --noEmit"
},
"exports": {
@ -58,10 +59,9 @@
},
"homepage": "https://github.com/baidu/amis#readme",
"dependencies": {
"echarts": "^5.4.0",
"fflate": "^0.8.1",
"numfmt": "^2.5.2",
"papaparse": "^5.4.1",
"puppeteer": "^22.6.3",
"tslib": "^2.3.1"
},
"devDependencies": {
@ -86,6 +86,9 @@
"typescript": "^4.3.5",
"xml-formatter": "^3.3.2"
},
"peerDependencies": {
"echarts": "^5.4.0"
},
"jest": {
"testEnvironment": "jsdom",
"coverageReporters": [
@ -116,4 +119,4 @@
"printBasicPrototype": false
}
}
}
}

View File

@ -208,6 +208,12 @@ export default class Excel implements OfficeViewer {
throw new Error('must implement this method');
}
destroy() {
if (this.workbook) {
this.workbook.destroy();
}
}
async print(): Promise<void> {
if (!this.workbook) {
return;

View File

@ -12,4 +12,6 @@ export interface OfficeViewer {
print(): void;
updateVariable(): void;
destroy(): void;
}

View File

@ -23,4 +23,7 @@ export default class UnSupport implements OfficeViewer {
print(): void {
throw new Error('Method not implemented.');
}
destroy(): void {
throw new Error('Method not implemented.');
}
}

View File

@ -710,7 +710,7 @@ export default class Word implements OfficeViewer {
}
}
const blob = this.parser.generateZip(buildXML(documentData));
const blob = this.parser.generateZipBlob(buildXML(documentData));
downloadBlob(blob, fileName);
}
@ -799,4 +799,6 @@ export default class Word implements OfficeViewer {
appendChild(root, renderNotes(this));
}
destroy(): void {}
}

View File

@ -16,7 +16,7 @@ import {FontSize} from '../types/FontSize';
import {FontStyle} from '../types/FontStyle';
import {CellInfo} from '../types/CellInfo';
import {IWorkbook} from '../types/IWorkbook';
import {CellData, hasValue} from '../types/worksheet/CellData';
import {CellData, FormulaData, hasValue} from '../types/worksheet/CellData';
import defaultFont from './defaultFont';
// @ts-ignore 这个没类型定义
import numfmt from 'numfmt';
@ -34,12 +34,8 @@ import {IDrawing} from '../types/IDrawing';
import {RangeRef} from '../types/RangeRef';
import {CellValue} from '../types/CellValue';
import {applyAutoFilter} from './applyAutoFilter';
import {fromExcelDate} from '../io/excel/util/fromExcelDate';
import {getChineseDay} from '../../util/getChineseDay';
import {numfmtExtend} from './numfmtExtend';
import {getThemeColor} from './getThemeColor';
import {emuToPx} from '../../util/emuToPx';
import {getAbsoluteAnchorPosition} from '../render/drawing/getAbsoluteAnchorPosition';
import {sortByRange} from './autoFilter/sortByRange';
import {px2pt} from '../../util/px2pt';
@ -512,7 +508,7 @@ export class LocalDataProvider implements IDataProvider {
const cell = sheet.worksheet?.cellData[row]?.[col];
if (cell) {
text = cellValue.text;
value = cellValue.value;
value = cellValue.value + '';
cellData = cell;
if (typeof cell === 'object' && 's' in cell) {
const cellXfxIndex = cell.s || 0;
@ -738,6 +734,10 @@ export class LocalDataProvider implements IDataProvider {
return this.defaultFontSize;
}
clearDefaultFontSizeCache() {
this.defaultFontSize = undefined;
}
getDefaultFontStyle(): FontStyle {
return this.defaultFontStyle;
}

View File

@ -6,6 +6,7 @@ import {customFilter} from './autoFilter/customFilter';
import {filters} from './autoFilter/filters';
import {IWorkbook} from '../types/IWorkbook';
import {applySortState} from './autoFilter/applySortState';
import {toNumber} from './toNumber';
/**
* autoFilter
@ -60,9 +61,12 @@ export function applyAutoFilter(
const cellValuesBigNumber = cellValues.map(cellValue => {
let num;
try {
num = parseFloat(cellValue.value);
} catch (e) {}
return {row: cellValue.row, num, value: cellValue.value};
num = toNumber(cellValue.value);
} catch (e) {
console.error('toNumber error', cellValue.value);
num = 0;
}
return {row: cellValue.row, num, value: cellValue.value + ''};
});
const customFiltersHiddenRows = customFilter(

View File

@ -0,0 +1,12 @@
export function toNumber(num: string | number | boolean | undefined): number {
if (num === '' || num === undefined) {
return 0;
}
if (typeof num === 'boolean') {
return num ? 1 : 0;
}
if (typeof num === 'string') {
return parseFloat(num);
}
return num;
}

View File

@ -20,8 +20,6 @@
- 支持多选区
- 表格操作
- 按日期年月日过滤
- sheetTab
- 超长水平滚动
- 公式栏
- 名称列表
- 嵌入单元格的图片,目前看 wps 等都不支持

View File

@ -0,0 +1,48 @@
import {CellValue} from '../types/CellValue';
import {RangeRef} from '../types/RangeRef';
import {CellData} from '../types/worksheet/CellData';
import {CellReference, NameReference, Reference} from './ast/Reference';
import {EvalResult} from './eval/EvalResult';
/**
*
*/
export interface FormulaEnv {
/**
*
* @param name
*/
getDefinedName(name: string): string;
/**
*
* @param range
* @param sheetName
*/
getByRange(range: RangeRef, sheetName?: string): CellValue[];
/**
*
* @param range
* @param sheetName
*/
getByRangeIgnoreHidden(range: RangeRef, sheetName?: string): CellValue[];
/**
*
*/
formulaCell(): RangeRef;
}
export function getRange(env: FormulaEnv, ref: Reference): RangeRef {
// name 的情况没法确定
if ('name' in ref) {
return {
startRow: 0,
startCol: 0,
endRow: 0,
endCol: 0
};
}
return ref.range;
}

View File

@ -0,0 +1,85 @@
/**
* Formula Error.
*/
export class FormulaError extends Error {
private _error: string;
private details?: object;
/**
* @param {string} error - error code, i.e. #NUM!
* @param {string} [msg] - detailed error message
* @param {object|Error} [details]
* @returns {FormulaError}
*/
constructor(error: string, msg?: string, details?: object) {
super(msg);
if (msg == null && details == null && FormulaError.errorMap.has(error))
return FormulaError.errorMap.get(error);
else if (msg == null && details == null) {
this._error = error;
FormulaError.errorMap.set(error, this);
} else {
this._error = error;
}
this.details = details;
}
/**
* Get the error name.
* @returns {string} formula error
*/
get error() {
return this._error;
}
get name() {
return this._error;
}
/**
* Return true if two errors are same.
* @param {FormulaError} err
* @returns {boolean} if two errors are same.
*/
equals(err: FormulaError) {
return err instanceof FormulaError && err._error === this._error;
}
/**
* Return the formula error in string representation.
* @returns {string} the formula error in string representation.
*/
toString() {
return this._error;
}
static errorMap: Map<any, any> = new Map();
static DIV0: FormulaError = new FormulaError('#DIV/0!');
static NA: FormulaError = new FormulaError('#N/A');
static NAME: FormulaError = new FormulaError('#NAME?');
static NULL: FormulaError = new FormulaError('#NULL!');
static NUM: FormulaError = new FormulaError('#NUM!');
static REF: FormulaError = new FormulaError('#REF!');
static VALUE: FormulaError = new FormulaError('#VALUE!');
static NOT_IMPLEMENTED = (functionName: string) => {
return new FormulaError(
'#NAME?',
`Function ${functionName} is not implemented.`
);
};
static TOO_MANY_ARGS = (functionName: string) => {
return new FormulaError(
'#N/A',
`Function ${functionName} has too many arguments.`
);
};
static ARG_MISSING = () => {
return new FormulaError('#N/A', `Argument type is missing.`);
};
static ERROR = (msg: string, details?: object) => {
return new FormulaError('#ERROR!', msg, details);
};
}
export default FormulaError;

View File

@ -0,0 +1,149 @@
import {ASTNode} from './ast/ASTNode';
import {Token, TokenName} from './tokenizer';
import {Precedence} from './parser/tokenPriorityMap';
import {InfixParse} from './parser/InfixParse';
import {BinaryOperator} from './parser/BinaryOperator';
import {PrefixOpParse} from './parser/PrefixOpParse';
import {ConstantParse} from './parser/ConstantParse';
import {RefParse} from './parser/RefParse';
import {GroupParse} from './parser/GroupParse';
import {ArrayParse} from './parser/ArrayParse';
import {FunctionParse} from './parser/FunctionParse';
import {PrefixParse} from './parser/PrefixParse';
import {PercentParse} from './parser/PercentParse';
// 优先级参考 https://support.microsoft.com/en-us/office/calculation-operators-and-precedence-in-excel-48be406d-4975-4d31-b2b8-7af9e0e2878a
export const prefixParserMap: Map<TokenName, PrefixParse> = new Map([
['PLUS', new PrefixOpParse(Precedence.PREFIX)],
['MINUS', new PrefixOpParse(Precedence.PREFIX)],
['STRING', new ConstantParse()],
['SINGLE_QUOTE_STRING', new ConstantParse()],
['NUMBER', new ConstantParse()],
['BOOLEAN', new ConstantParse()],
['ERROR', new ConstantParse()],
['ERROR_REF', new ConstantParse()],
['SHEET', new RefParse()],
['SHEET_QUOTE', new RefParse()],
['CELL', new RefParse()],
['NAME', new RefParse()],
['OPEN_BRACKET', new RefParse()],
['OPEN_PAREN', new GroupParse()],
['OPEN_CURLY', new ArrayParse()],
['FUNCTION', new FunctionParse()]
]);
export const infixParserMap: Map<TokenName, InfixParse> = new Map([
['PLUS', new BinaryOperator(Precedence.PLUS)],
['MINUS', new BinaryOperator(Precedence.PLUS)],
['MUL', new BinaryOperator(Precedence.MUL)],
['DIV', new BinaryOperator(Precedence.MUL)],
['CONCAT', new BinaryOperator(Precedence.MUL)],
['CARET', new BinaryOperator(Precedence.CARET)],
['EQ', new BinaryOperator(Precedence.COMPARE)],
['GT', new BinaryOperator(Precedence.COMPARE)],
['LT', new BinaryOperator(Precedence.COMPARE)],
['GE', new BinaryOperator(Precedence.COMPARE)],
['LE', new BinaryOperator(Precedence.COMPARE)],
['NE', new BinaryOperator(Precedence.COMPARE)],
['PERCENT', new PercentParse(Precedence.POSTFIX)]
]);
export class Parser {
tokens: Token[];
currentIndex: number = -1;
constructor(tokens: Token[]) {
this.tokens = tokens;
}
parse() {
this.currentIndex = -1;
const rootNode = this.parseFormula();
return rootNode;
}
parseFormula(precedence: number = 0): ASTNode {
const token = this.next();
const prefixParser = prefixParserMap.get(token.name);
if (!prefixParser) {
throw new Error(`No prefix parser found for ${token.name}`);
}
let left = prefixParser.parse(this, token);
while (precedence < this.getPrecedence()) {
const nexToken = this.next();
const infixParserConfig = infixParserMap.get(nexToken.name);
if (!infixParserConfig) {
throw new Error(`No infix parser found for ${nexToken.name}`);
}
left = infixParserConfig.parse(this, left, nexToken);
}
return left;
}
getPrecedence() {
const nextToken = this.peakNext();
if (!nextToken) {
return 0;
}
const infixParserConfig = infixParserMap.get(nextToken.name);
if (!infixParserConfig) {
return 0;
}
return infixParserConfig.getPrecedence();
}
/**
* token
*/
match(...tokenNames: TokenName[]) {
const currentToken = this.peakNext();
if (!currentToken) {
return false;
}
if (tokenNames.length > 0) {
if (tokenNames.every(name => name !== currentToken.name)) {
return false;
}
}
this.next();
return true;
}
/**
* token
*/
next(expectTokenName?: TokenName) {
if (expectTokenName) {
const nextToken = this.peakNext();
if (nextToken && nextToken.name !== expectTokenName) {
throw new Error(
`Expect token name ${expectTokenName}, but got ${nextToken.name}`
);
}
}
this.currentIndex++;
return this.tokens[this.currentIndex];
}
peak(): Token | undefined {
return this.tokens[this.currentIndex];
}
/**
* token
*/
peakNext(): Token | undefined {
return this.tokens[this.currentIndex + 1];
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
SUM(#REF)
+ˉ
IF($A4="N/A"," ",IF(=1,AG4,AF4))
#REF+C134
+#REF+C134
IF(A71="N/A"," ",IF(ISERROR(#REF),K59*Include1,#REF))
IF(A71="N/A"," ",IF(ISERROR(#REF),L59*Include1,IF(#REF=0,L59*Include1,#REF)))
F162+F152+F150+F148+#REF+F154+#REF
IF(A11<,B10,"")
IF(="BOLLINGER",$J755+$AH755,0)
LN(O1424/#REF)
Z285+(#REF-#REF)
IF(=7,AVERAGE(C3:C9),0)
IF(=7,AVERAGE(C4:C11),0)
IF(=7,AVERAGE(C13:C19),IF(=14,AVERAGE(C6:C19),0))
IF(=7,AVERAGE(C29:C35),IF(=14,AVERAGE(C22:C35),IF(=30,AVERAGE(C6:C35),0)))
IF(=7,AVERAGE(C37:C43),IF(=14,AVERAGE(C30:C43),IF(=30,AVERAGE(C14:C43),IF(=40,AVERAGE(C4:C43),0))))
IF(A39="N/A"," ",IF(=TRUE,(IF(MONTH(A39)>=4,IF(MONTH(A39)<=10,VLOOKUP(A39,Gas Curves!B17:O377,13),VLOOKUP(A39,Gas Curves!B17:O377,14)),VLOOKUP(A39,Gas Curves!B17:O377,14))),0))
IF(A39="N/A"," ",IF(=TRUE,(IF(MONTH(A39)>=4,IF(MONTH(A39)<=10,VLOOKUP(A39,Gas Curves!B17:O377,13),VLOOKUP(A39,Gas Curves!B17:O377,14)),VLOOKUP(A39,Gas Curves!B17:O377,14))),0))
IF(=7,AVERAGE(C38:C44),IF(=14,AVERAGE(C31:C44),IF(=30,AVERAGE(C15:C44),IF(=40,AVERAGE(C4:C44),0))))
IF(="BOLLINGER",$J755-$AH755,0)
+#REF-#REF
+#REF
VLOOKUP(F2,PJM Monthly Summary 2000 08 V.1!$R$6:$S$1411,2,FALSE)
VLOOKUP(F18,PJM Monthly Summary 2000 08 V.1!$R$20:$S$1359,2,FALSE)
-NOX, Regi
-_SO2, Regi

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
SUM((Exercises 4, 5 and 6!$H$2:$H$11-Exercise 7!B2:B11)/Exercise 7!B2:B11)

View File

@ -0,0 +1,73 @@
=SUM(DeptSales[SaleAmt])
=DeptSales[[SalesPers]:[Region]]
=(DeptSales[SaleAmt],DeptSales[ComAmt])
=DeptSales[[SalesPers]:[SaleAmt]] DeptSales[[Region]:[ComPct]]
=DeptSales[#All]
=DeptSales[#Data]
=DeptSales[#Headers]
=DeptSales[#Totals]
=DeptSales[#This Row]
=[SaleAmt]*[ComPct]
=DeptSales[SaleAmt]*DeptSales[ComPct]
=DeptSales[[#All],[SaleAmt]]
=DeptSales[[#Headers],[ComPct]]
=DeptSales[[#Totals],[Region]]
=DeptSales[[#All],[SaleAmt]:[ComPct]]
=DeptSales[[#Data],[ComPct]:[ComAmt]]
=DeptSales[[#Headers],[Region]:[ComAmt]]
=DeptSales[[#Totals],[SaleAmt]:[ComAmt]]
=DeptSales[[#Headers],[#Data],[ComPct]]
=DeptSales[[#This Row], [ComAmt]]
=DeptSales[[SalesPers]:[Region]]
=DeptSalesFYSummary[[2004]:[2002]]
=DeptSalesFYSummary[[Total$Amount]]
=DeptSales[Total Amount]
=DeptSalesFYSummary['#OfItems]
=DeptSales[ [SalesPers]:[Region] ]
=DeptSales[[#Headers], [#Data], [ComPct]]
=DeptSales[@Column]
=DeptSales[@[SaleAmt]:[ComAmt]]
=DeptSales[@['@]]
=Tabel25[[#This Row],[I/HV]]
VLOOKUP(GroupVertices[[#This Row],[Vertex]], Vertices[], MATCH("ID", Vertices[#Headers], 0), FALSE)
IFERROR(VLOOKUP($A4,TABLA_AHORRO[],6,FALSE),"")
SUM(Table[1])
=SUM(Sales[@[Jan]:[Mar]])
=SUM(Regions[@South], Regions[@West])
=SUM(Regions[@[South sales]], Regions[@[West sales]])
=SUM(Regions[[South]:[East]])
=SUM(Regions[South], Regions[West])
=Regions[#Totals] Regions[[#All],[West]]
=Regions[[South]:[East]]
=Regions[[#Headers],[South]]
=AVERAGE(Regions[South],Regions[West],Regions[North])
=SUBTOTAL(103,Sales[Jan])
=SUBTOTAL(109,[Jan])
=SUM(Sales[Jan])
=SUMIF(Sales[Item],$F$2,Sales[Jan])
=SUMIF(Sales[[Item]:[Item]],$F$2,Sales[Jan])
=SUM(Sales[@[Jan]:[Feb]])
=SUM(Sales[@Jan]:Sales[@Feb])
=Sales
=Sales[#All]
=Sales[#Headers]
=Sales[#Data]
=Sales[#Totals]
=Sales[[#Headers],[#Data]]
=Sales[[#Data],[#Totals]]
=Sales[@Region]
=Sales[Region]
=Sales[[#Headers],[Region]]
=Sales[[#Data],[Region]]
=Sales[[#Totals],[Region]]
=Sales[@[Jan]:[Mar]]
=Sales[[Jan]:[Mar]]
=Sales[[#Headers],[#Data],[Jan]:[Mar]]
=Sales[[#Data],[#Totals],[Jan]:[Mar]]
=SUBTOTAL(109,[Jan])
=SUBTOTAL(109,[Feb])
=SUBTOTAL(109,[Mar])
=XLOOKUP($G7,Sales_1[[Region]:[Region]],Sales_1[Jan])
=SUM(Sales_2[Jan]:Sales_2[Feb])
=[@Jan]+[@Feb]
=COUNTA(Sales_4[[#Headers],[Jan]:[Mar]])

View File

@ -0,0 +1 @@
"IF(A8>0,12*$B$2-D8,""0\"")"

View File

@ -0,0 +1,82 @@
import {CellValue} from '../../types/CellValue';
import {RangeRef} from '../../types/RangeRef';
import {FormulaEnv} from '../FormulaEnv';
import {evalFormula} from '../eval/evalFormula';
const vars: Map<string, string> = new Map([
['aaaa', 'A1:A1'],
['bbbb', 'B1:B1']
]);
const env = {
getDefinedName: (name: string, sheetName?: string) => {
return vars.get(name)!;
},
getByRange: (range: RangeRef, sheetName?: string) => {
if (range.startCol === 0) {
return [{row: 1, col: 0, text: '1', value: 1}];
}
if (range.startCol === 1) {
return [{row: 1, col: 1, text: '2', value: 2}];
}
return [];
},
getByRangeIgnoreHidden: (range: RangeRef, sheetName?: string) => {
return env.getByRange(range, sheetName);
},
formulaCell: () => {
return {startRow: 1, startCol: 1, endRow: 1, endCol: 1};
}
} as FormulaEnv;
test('fourOp', () => {
expect(evalFormula('1 + 2', env)).toEqual(3);
expect(evalFormula('1 - 2', env)).toEqual(-1);
expect(evalFormula('1 * 2', env)).toEqual(2);
expect(evalFormula('1 / 2', env)).toEqual(0.5);
expect(evalFormula('2 ^ 2', env)).toEqual(4);
expect(evalFormula('1 + 2 * 2', env)).toEqual(5);
expect(evalFormula('(1 + 2) * 2', env)).toEqual(6);
});
test('evalRef', () => {
expect(evalFormula('aaaa + bbbb', env)).toEqual(3);
});
test('evalFunction', () => {
expect(evalFormula('SUM(1, 2)', env)).toEqual(3);
expect(evalFormula('SUM(aaaa, bbbb)', env)).toEqual(3);
});
test('string', () => {
expect(evalFormula('"a"', env)).toEqual('a');
expect(evalFormula('"a" & "b"', env)).toEqual('ab');
expect(evalFormula('"A"&TRUE', env)).toEqual('ATRUE');
});
test('num add string', () => {
expect(evalFormula('"1" + "2"', env)).toEqual(3);
expect(evalFormula('1+"$4.00"', env)).toEqual(5);
});
test('date', () => {
expect(evalFormula(' "6/1/2001"-"5/1/2001"', env)).toEqual(31);
});
test('implicit convert', () => {
expect(evalFormula('1+"2"', env)).toEqual(3);
expect(evalFormula('"1"+2', env)).toEqual(3);
expect(evalFormula('1+TRUE', env)).toEqual(2);
});
test('array op', () => {
expect(evalFormula('{1,2}+{3,4}', env)).toEqual([4, 6]);
expect(evalFormula('{1,2}+3', env)).toEqual([4, 5]);
expect(evalFormula('1+{3;4}', env)).toEqual([[4], [5]]);
expect(evalFormula('{1;2}+{3;4}', env)).toEqual([[4], [6]]);
expect(evalFormula('{1,2;3,4}+{1,2;3,4}', env)).toEqual([
[2, 4],
[6, 8]
]);
expect(evalFormula('2*{1,2}', env)).toEqual([2, 4]);
});

View File

@ -0,0 +1,106 @@
import {tokenize} from '../tokenizer';
test('SUM(A1:A2)', () => {
expect(tokenize('SUM(A1:A2)')).toEqual([
{name: 'FUNCTION', value: 'SUM(', start: 0, end: 4},
{name: 'CELL', value: 'A1', start: 4, end: 6},
{name: 'COLON', value: ':', start: 6, end: 7},
{name: 'CELL', value: 'A2', start: 7, end: 9},
{name: 'CLOSE_PAREN', value: ')', start: 9, end: 10}
]);
});
test('SUM(Economics!D28:D45)', () => {
expect(tokenize('SUM(Economics!D28:D45)')).toEqual([
{
end: 4,
name: 'FUNCTION',
start: 0,
value: 'SUM('
},
{
end: 14,
name: 'SHEET',
start: 4,
value: 'Economics!'
},
{
end: 17,
name: 'CELL',
start: 14,
value: 'D28'
},
{
end: 18,
name: 'COLON',
start: 17,
value: ':'
},
{
end: 21,
name: 'CELL',
start: 18,
value: 'D45'
},
{
end: 22,
name: 'CLOSE_PAREN',
start: 21,
value: ')'
}
]);
});
test('G3*G4', () => {
expect(tokenize('G3*G4')).toEqual([
{name: 'CELL', value: 'G3', start: 0, end: 2},
{name: 'MUL', value: '*', start: 2, end: 3},
{name: 'CELL', value: 'G4', start: 3, end: 5}
]);
});
test('COUNTIF(J10:J65536,"MWhs")', () => {
expect(tokenize('COUNTIF(J10:J65536,"MWhs")')).toEqual([
{name: 'FUNCTION', value: 'COUNTIF(', start: 0, end: 8},
{name: 'CELL', value: 'J10', start: 8, end: 11},
{name: 'COLON', value: ':', start: 11, end: 12},
{name: 'CELL', value: 'J65536', start: 12, end: 18},
{name: 'COMMA', value: ',', start: 18, end: 19},
{name: 'STRING', value: '"MWhs"', start: 19, end: 25},
{name: 'CLOSE_PAREN', value: ')', start: 25, end: 26}
]);
});
test('VLOOKUP(G10,USERS,2,FALSE)', () => {
expect(tokenize('VLOOKUP(G10,USERS,2,FALSE)')).toEqual([
{name: 'FUNCTION', value: 'VLOOKUP(', start: 0, end: 8},
{name: 'CELL', value: 'G10', start: 8, end: 11},
{name: 'COMMA', value: ',', start: 11, end: 12},
{name: 'NAME', value: 'USERS', start: 12, end: 17},
{name: 'COMMA', value: ',', start: 17, end: 18},
{name: 'NUMBER', value: '2', start: 18, end: 19},
{name: 'COMMA', value: ',', start: 19, end: 20},
{name: 'BOOLEAN', value: 'FALSE', start: 20, end: 25},
{name: 'CLOSE_PAREN', value: ')', start: 25, end: 26}
]);
});
test('1>=2', () => {
expect(tokenize('1>=2')).toEqual([
{name: 'NUMBER', value: '1', start: 0, end: 1},
{name: 'GE', value: '>=', start: 1, end: 3},
{name: 'NUMBER', value: '2', start: 3, end: 4}
]);
});
test("'[2]Exhibit Data'!B50/1000", () => {
tokenize("'[2]Exhibit Data'!B50/1000");
});
test('COUNTIF($A:$A,$B17)', () => {
tokenize('COUNTIF($A)');
});
test('[1]!Hub_Consolidation', () => {
tokenize('[1]!Hub_Consolidation');
});

View File

@ -0,0 +1,129 @@
import {Parser, infixParserMap, prefixParserMap} from '../Parser';
import {Reference} from '../ast/Reference';
import {tokenDefines, tokenize} from '../tokenizer';
import {printAST} from '../ast/ASTNode';
function testPrintMatch(input: string, output: string) {
const parser = new Parser(tokenize(input));
const ast = parser.parse();
expect(printAST(ast)).toBe(output);
}
test('not in parserMap', () => {
const keys = new Set();
for (const [key] of prefixParserMap.entries()) {
keys.add(key);
}
for (const [key] of infixParserMap.entries()) {
keys.add(key);
}
const whiteList = new Set(['SPACE', 'COMMA', 'COLON', 'SEMICOLON']);
for (const key in tokenDefines) {
if (!keys.has(key) && !key.startsWith('CLOSE_') && !whiteList.has(key)) {
// TODO目前这些解析有问题
// console.log(key);
}
}
});
test('col', () => {
testPrintMatch('SUM(C:C)', 'SUM(C:C)');
});
test('COUNTIF($A:$A,$B17)', () => {
testPrintMatch('COUNTIF($A:$A,$B17)', 'COUNTIF($A:$A,$B17)');
});
test('[2]Section3A', () => {
// TODO: 目前不支持 name 和 cell 混合的情况
// testPrintMatch('[2]Section3A:A3', 'Section3A:A3');
});
test('SUM(A1:INDEX(A2))', () => {
// 目前不支持这种混合情况
});
test('half', () => {
testPrintMatch('SUM(2,)', 'SUM(2)');
});
test('funcArg', () => {
testPrintMatch('SUM(2,,)', 'SUM(2)');
});
test('union', () => {
testPrintMatch('SUM((A1,A2))', 'SUM((A1,A2))');
});
test('range', () => {
testPrintMatch('SUM(A1:A2)', 'SUM(A1:A2)');
});
test('Intersection', () => {
testPrintMatch('SUM(B7:D7 C6:C8)', 'SUM(B7:D7 C6:C8)');
});
test('2dArr', () => {
testPrintMatch('SUM({1,2;3,4})', 'SUM({1,2;3,4})');
});
test('fourOp', () => {
testPrintMatch('a ^ b', '(a^b)');
testPrintMatch('-a * b', '((-a)*b)');
testPrintMatch('a ^ (b + c)', '(a^(b+c))');
testPrintMatch('1 + 2 * 3', '(1+(2*3))');
testPrintMatch('(1 + 2) * 3', '((1+2)*3)');
testPrintMatch('a * b / c', '((a*b)/c)');
});
test('function', () => {
testPrintMatch('SUM()', 'SUM()');
testPrintMatch('SUM(1, 2, 3)', 'SUM(1,2,3)');
});
test('mix', () => {
testPrintMatch('A1 + SUM(A2)', '(A1+SUM(A2))');
});
function testRef(input: string, result: Reference) {
const parser = new Parser(tokenize(input));
const ast = parser.parse();
expect(ast.ref).toEqual(result);
}
test('refParse', () => {
testRef('A1', {
start: 'A1',
end: 'A1',
range: {
startCol: 0,
startRow: 0,
endCol: 0,
endRow: 0
}
});
testRef('A1:B$3', {
start: 'A1',
end: 'B$3',
range: {
startCol: 0,
startRow: 0,
endCol: 1,
endRow: 2
}
});
testRef('sheet1!A1:B$3', {
sheetName: 'sheet1',
start: 'A1',
end: 'B$3',
range: {
startCol: 0,
startRow: 0,
endCol: 1,
endRow: 2
}
});
});

View File

@ -0,0 +1,119 @@
import {Token} from '../tokenizer';
import {Reference} from './Reference';
export type ASTNodeType =
| 'Array'
| 'Function'
| 'Percent'
| 'Constant'
| 'UnaryExpr'
| 'BinaryExpr'
| 'Union'
| 'Intersection'
| 'Reference';
export type ASTNode = {
type: ASTNodeType;
token: Token;
children: ASTNode[] | ASTNode[][];
// ref 特有字段
ref?: Reference;
// intersection 特有字段
refs?: Reference[];
};
export function printAST(ast: ASTNode): string {
const result: string[] = [];
print(ast, result);
return result.join('');
}
export function printArray(
children: ASTNode[] | ASTNode[][],
result: string[]
) {
result.push('{');
children.forEach((child, index) => {
if (Array.isArray(child)) {
child.forEach((c, i) => {
print(c, result);
if (i < child.length - 1) {
result.push(',');
}
});
if (index < children.length - 1) {
result.push(';');
}
} else {
print(child, result);
if (index < children.length - 1) {
result.push(',');
}
}
});
result.push('}');
}
export function print(node: ASTNode | ASTNode[], result: string[]) {
if (Array.isArray(node)) {
printArray(node, result);
return;
}
switch (node.type) {
case 'Array':
printArray(node.children, result);
break;
case 'BinaryExpr':
result.push('(');
print(node.children[0], result);
result.push(node.token.value);
print(node.children[1], result);
result.push(')');
break;
case 'Function':
result.push(node.token.value);
node.children.forEach((child, index) => {
print(child, result);
if (index < node.children.length - 1) {
result.push(',');
}
});
result.push(')');
break;
case 'Percent':
result.push('(');
print(node.children[0], result);
result.push(node.token.value);
result.push(')');
break;
case 'UnaryExpr':
result.push('(');
result.push(node.token.value);
print(node.children[0], result);
result.push(')');
break;
case 'Union':
result.push('(');
node.children.forEach((child, index) => {
print(child, result);
if (index < node.children.length - 1) {
result.push(',');
}
});
result.push(')');
break;
default:
result.push(node.token.value);
}
}

View File

@ -0,0 +1,4 @@
export interface Constant {
type: 'error' | 'number' | 'string' | 'boolean';
value: string;
}

View File

@ -0,0 +1,16 @@
import {RangeRef} from '../../types/RangeRef';
export type Reference = NameReference | CellReference;
export type NameReference = {
sheetName?: string;
name: string;
};
export type CellReference = {
sheetName?: string;
// 类似 A1 或 A$1
start: string;
end: string;
range: RangeRef;
};

View File

@ -0,0 +1,494 @@
/**
*
*/
export const builtinFunctions = [
'ABS',
'ACCRINT',
'ACCRINTM',
'ACOS',
'ACOSH',
'ACOT',
'ACOTH',
'ADDRESS',
'AGGREGATE',
'AMORDEGRC',
'AMORLINC',
'AND',
'ARABIC',
'AREAS',
'ARRAYTOTEXT',
'ASC',
'ASIN',
'ASINH',
'ATAN',
'ATAN2',
'ATANH',
'AVEDEV',
'AVERAGE',
'AVERAGEA',
'AVERAGEIF',
'AVERAGEIFS',
'BAHTTEXT',
'BASE',
'BESSELI',
'BESSELJ',
'BESSELK',
'BESSELY',
'BETA.DIST',
'BETA.INV',
'BETADIST',
'BETAINV',
'BIN2DEC',
'BIN2HEX',
'BIN2OCT',
'BINOM.DIST',
'BINOM.DIST.RANGE',
'BINOM.INV',
'BINOMDIST',
'BITAND',
'BITLSHIFT',
'BITOR',
'BITRSHIFT',
'BITXOR',
'CALL',
'CEILING',
'CEILING.MATH',
'CEILING.PRECISE',
'CELL',
'CHAR',
'CHIDIST',
'CHIINV',
'CHISQ.DIST',
'CHISQ.DIST.RT',
'CHISQ.INV',
'CHISQ.INV.RT',
'CHISQ.TEST',
'CHITEST',
'CLEAN',
'CODE',
'COLUMN',
'COLUMNS',
'COMBIN',
'COMBINA',
'COMPLEX',
'CONCAT',
'CONCATENATE',
'CONFIDENCE',
'CONFIDENCE.NORM',
'CONFIDENCE.T',
'CONVERT',
'CORREL',
'COS',
'COSH',
'COT',
'COTH',
'COUNT',
'COUNTA',
'COUNTBLANK',
'COUNTIF',
'COUNTIFS',
'COUPDAYBS',
'COUPDAYS',
'COUPDAYSNC',
'COUPNCD',
'COUPNUM',
'COUPPCD',
'COVAR',
'COVARIANCE.P',
'COVARIANCE.S',
'CRITBINOM',
'CSC',
'CSCH',
'CUBEKPIMEMBER',
'CUBEMEMBER',
'CUBEMEMBERPROPERTY',
'CUBERANKEDMEMBER',
'CUBESET',
'CUBESETCOUNT',
'CUBEVALUE',
'CUMIPMT',
'CUMPRINC',
'DATE',
'DATEDIF',
'DATEVALUE',
'DAVERAGE',
'DAY',
'DAYS',
'DAYS360',
'DB',
'DBCS',
'DCOUNT',
'DCOUNTA',
'DDB',
'DEC2BIN',
'DEC2HEX',
'DEC2OCT',
'DECIMAL',
'DEGREES',
'DELTA',
'DEVSQ',
'DGET',
'DISC',
'DMAX',
'DMIN',
'DOLLAR',
'DOLLARDE',
'DOLLARFR',
'DPRODUCT',
'DSTDEV',
'DSTDEVP',
'DSUM',
'DURATION',
'DVAR',
'DVARP',
'EDATE',
'EFFECT',
'ENCODEURL',
'EOMONTH',
'ERF',
'ERF.PRECISE',
'ERFC',
'ERFC.PRECISE',
'ERROR.TYPE',
'EUROCONVERT',
'EVEN',
'EXACT',
'EXP',
'EXPON.DIST',
'EXPONDIST',
'F.DIST',
'F.DIST.RT',
'F.INV',
'F.INV.RT',
'F.TEST',
'FACT',
'FACTDOUBLE',
'FALSE',
'FDIST',
'FILTER',
'FILTERXML',
'FIND',
'FINDB',
'FINV',
'FISHER',
'FISHERINV',
'FIXED',
'FLOOR',
'FLOOR.MATH',
'FLOOR.PRECISE',
'FORECAST',
'FORECAST.ETS',
'FORECAST.ETS.CONFINT',
'FORECAST.ETS.SEASONALITY',
'FORECAST.ETS.STAT',
'FORECAST.LINEAR',
'FORMULATEXT',
'FREQUENCY',
'FTEST',
'FV',
'FVSCHEDULE',
'GAMMA',
'GAMMA.DIST',
'GAMMA.INV',
'GAMMADIST',
'GAMMAINV',
'GAMMALN',
'GAMMALN.PRECISE',
'GAUSS',
'GCD',
'GEOMEAN',
'GESTEP',
'GETPIVOTDATA',
'GROWTH',
'HARMEAN',
'HEX2BIN',
'HEX2DEC',
'HEX2OCT',
'HLOOKUP',
'HOUR',
'HYPERLINK',
'HYPGEOM.DIST',
'HYPGEOMDIST',
'IF',
'IFERROR',
'IFNA',
'IFS',
'IMABS',
'IMAGINARY',
'IMARGUMENT',
'IMCONJUGATE',
'IMCOS',
'IMCOSH',
'IMCOT',
'IMCSC',
'IMCSCH',
'IMDIV',
'IMEXP',
'IMLN',
'IMLOG10',
'IMLOG2',
'IMPOWER',
'IMPRODUCT',
'IMREAL',
'IMSEC',
'IMSECH',
'IMSIN',
'IMSINH',
'IMSQRT',
'IMSUB',
'IMSUM',
'IMTAN',
'INFO',
'INT',
'INTERCEPT',
'INTRATE',
'IPMT',
'IRR',
'ISBLANK',
'ISERR',
'ISERROR',
'ISEVEN',
'ISFORMULA',
'ISLOGICAL',
'ISNA',
'ISNONTEXT',
'ISNUMBER',
'ISO.CEILING',
'ISODD',
'ISOWEEKNUM',
'ISPMT',
'ISREF',
'ISTEXT',
'JIS',
'KURT',
'LARGE',
'LCM',
'LEFT',
'LEFTB',
'LEN',
'LENB',
'LET',
'LINEST',
'LN',
'LOG',
'LOG10',
'LOGEST',
'LOGINV',
'LOGNORM.DIST',
'LOGNORM.INV',
'LOGNORMDIST',
'LOOKUP',
'LOWER',
'MATCH',
'MAX',
'MAXA',
'MAXIFS',
'MDETERM',
'MDURATION',
'MEDIAN',
'MID',
'MIDB',
'MIN',
'MINA',
'MINIFS',
'MINUTE',
'MINVERSE',
'MIRR',
'MMULT',
'MOD',
'MODE',
'MODE.MULT',
'MODE.SNGL',
'MONTH',
'MROUND',
'MULTINOMIAL',
'MUNIT',
'N',
'NA',
'NEGBINOM.DIST',
'NEGBINOMDIST',
'NETWORKDAYS',
'NETWORKDAYS.INTL',
'NOMINAL',
'NORM.DIST',
'NORM.INV',
'NORM.S.DIST',
'NORM.S.INV',
'NORMDIST',
'NORMINV',
'NORMSDIST',
'NORMSINV',
'NOT',
'NOW',
'NPER',
'NPV',
'NUMBERVALUE',
'OCT2BIN',
'OCT2DEC',
'OCT2HEX',
'ODD',
'ODDFPRICE',
'ODDFYIELD',
'ODDLPRICE',
'ODDLYIELD',
'OR',
'PDURATION',
'PEARSON',
'PERCENTILE',
'PERCENTILE.EXC',
'PERCENTILE.INC',
'PERCENTRANK',
'PERCENTRANK.EXC',
'PERCENTRANK.INC',
'PERMUT',
'PERMUTATIONA',
'PHI',
'PHONETIC',
'PI',
'PMT',
'POISSON',
'POISSON.DIST',
'POWER',
'PPMT',
'PRICE',
'PRICEDISC',
'PRICEMAT',
'PROB',
'PRODUCT',
'PROPER',
'PV',
'QUARTILE',
'QUARTILE.EXC',
'QUARTILE.INC',
'QUOTIENT',
'RADIANS',
'RAND',
'RANDARRAY',
'RANDBETWEEN',
'RANK',
'RANK.AVG',
'RANK.EQ',
'RATE',
'RECEIVED',
'REGISTER.ID',
'REPLACE',
'REPLACEB',
'REPT',
'RIGHT',
'RIGHTB',
'ROMAN',
'ROUND',
'ROUNDDOWN',
'ROUNDUP',
'ROW',
'ROWS',
'RRI',
'RSQ',
'RTD',
'SEARCH',
'SEARCHB',
'SEC',
'SECH',
'SECOND',
'SEQUENCE',
'SERIESSUM',
'SHEET',
'SHEETS',
'SIGN',
'SIN',
'SINH',
'SKEW',
'SKEW.P',
'SLN',
'SLOPE',
'SMALL',
'SORT',
'SORTBY',
'SQRT',
'SQRTPI',
'STANDARDIZE',
'STDEV',
'STDEV.P',
'STDEV.S',
'STDEVA',
'STDEVP',
'STDEVPA',
'STEYX',
'SUBSTITUTE',
'SUBTOTAL',
'SUM',
'SUMIF',
'SUMIFS',
'SUMPRODUCT',
'SUMSQ',
'SUMX2MY2',
'SUMX2PY2',
'SUMXMY2',
'SWITCH',
'SYD',
'T',
'T.DIST',
'T.DIST.2T',
'T.DIST.RT',
'T.INV',
'T.INV.2T',
'T.TEST',
'TAN',
'TANH',
'TBILLEQ',
'TBILLPRICE',
'TBILLYIELD',
'TDIST',
'TEXT',
'TEXTJOIN',
'TIME',
'TIMEVALUE',
'TINV',
'TODAY',
'TRANSPOSE',
'TREND',
'TRIM',
'TRIMMEAN',
'TRUE',
'TRUNC',
'TTEST',
'TYPE',
'UNICHAR',
'UNICODE',
'UNIQUE',
'UPPER',
'VALUE',
'VALUETOTEXT',
'VAR',
'VAR.P',
'VAR.S',
'VARA',
'VARP',
'VARPA',
'VDB',
'VLOOKUP',
'WEBSERVICE',
'WEEKDAY',
'WEEKNUM',
'WEIBULL',
'WEIBULL.DIST',
'WORKDAY',
'WORKDAY.INTL',
'XIRR',
'XLOOKUP',
'XMATCH',
'XNPV',
'XOR',
'YEAR',
'YEARFRAC',
'YIELD',
'YIELDDISC',
'YIELDMAT',
'Z.TEST',
'ZTEST'
] as const;
export const builtinFunctionSet = new Set(builtinFunctions);
export type FunctionName = (typeof builtinFunctions)[number];

View File

@ -0,0 +1,39 @@
import Graph, {GraphInstance} from '../../../util/graph';
import {rangeRefToString} from '../../io/excel/util/Range';
import {FormulaEnv} from '../FormulaEnv';
import {ASTNode} from '../ast/ASTNode';
export class DependencyVisitor {
formulaEnv: FormulaEnv;
fromCell: string;
graph: GraphInstance;
constructor(formulaEnv: FormulaEnv, graph: GraphInstance, fromCell: string) {
this.formulaEnv = formulaEnv;
this.graph = graph;
this.fromCell = fromCell;
}
visit(node: ASTNode | ASTNode[]) {
if (Array.isArray(node)) {
node.map(n => this.visit(n));
return;
}
const type = node.type;
const ref = node.ref;
switch (type) {
case 'Reference':
if (!ref) {
break;
}
if ('name' in ref) {
} else {
const range = ref.range;
this.graph.addEdge(this.fromCell, rangeRefToString(range));
}
case 'Intersection':
default:
break;
}
}
}

View File

@ -0,0 +1,19 @@
import Graph from '../../../util/graph';
import {rangeRefToString} from '../../io/excel/util/Range';
import {RangeRef} from '../../types/RangeRef';
import {FormulaEnv} from '../FormulaEnv';
import FormulaError from '../FormulaError';
import {Parser} from '../Parser';
import {tokenize} from '../tokenizer';
export function buildFormulaDependencyGraph(formula: string, env: FormulaEnv) {
const visitedFormula = new Set<string>();
const cell = env.formulaCell();
const cellId = rangeRefToString(cell);
const graph = Graph();
}
function getDependentCells(formula: string, env: FormulaEnv) {
const parser = new Parser(tokenize(formula));
const ast = parser.parse();
}

View File

@ -0,0 +1,44 @@
export type EvalResult =
| undefined
| number
| string
| boolean
| Date
| ErrorResult
| EvalResult[]
| UnionValue
| HyperlinkResult;
export type ErrorResult = {
type: 'Error';
value: string;
};
export type UnionValue = {
type: 'Union';
children: EvalResult[];
};
export type HyperlinkResult = {
type: 'Hyperlink';
text: string;
link: string;
};
export function isUnionValue(value: EvalResult): boolean {
return (
value !== null &&
typeof value === 'object' &&
'type' in value &&
value.type === 'Union'
);
}
export function isErrorValue(value: EvalResult): boolean {
return (
value !== null &&
typeof value === 'object' &&
'type' in value &&
value.type === 'Error'
);
}

View File

@ -0,0 +1,64 @@
import {FormulaEnv} from '../FormulaEnv';
import {ASTNode} from '../ast/ASTNode';
import {EvalResult} from './EvalResult';
import {visitArray} from './visitArray';
import {visitBinaryExpr} from './visitBinaryExpr';
import {visitConstant} from './visitConstant';
import {visitFunction} from './visitFunction';
import {visitPercent} from './visitPercent';
import {visitReference} from './visitReference';
import {visitUnaryExpr} from './visitUnaryExpr';
import '../functions/math';
import '../functions/trigonometry';
import '../functions/text';
import '../functions/statistical';
import '../functions/distribution';
import '../functions/engineering';
import '../functions/date';
import '../functions/financial';
import '../functions/information';
import '../functions/logical';
import '../functions/reference';
import '../functions/functionAlias';
import '../functions/database';
import {visitIntersection} from './visitIntersection';
export class FormulaVisitor {
formulaEnv: FormulaEnv;
constructor(formulaEnv: FormulaEnv) {
this.formulaEnv = formulaEnv;
}
visit(node: ASTNode | ASTNode[]): EvalResult {
if (Array.isArray(node)) {
return node.map(n => this.visit(n));
}
const type = node.type;
switch (type) {
case 'BinaryExpr':
return visitBinaryExpr(this, node);
case 'UnaryExpr':
return visitUnaryExpr(this, node);
case 'Constant':
return visitConstant(this, node);
case 'Percent':
return visitPercent(this, node);
case 'Reference':
return visitReference(this.formulaEnv, node);
case 'Intersection':
return visitIntersection(this, this.formulaEnv, node);
case 'Function':
return visitFunction(this, this.formulaEnv, node);
case 'Array':
return visitArray(this, node);
case 'Union':
return {
type: 'Union',
children: visitArray(this, node)
};
default:
throw new Error('Not implemented type' + type);
}
}
}

View File

@ -0,0 +1,72 @@
import {Criteria} from '../parser/parseCriteria';
const type2Number = {boolean: 3, string: 2, number: 1};
type typeName = keyof typeof type2Number;
type Value = string | number | boolean | undefined;
function compareOp(
value1: Value,
infix: Criteria['op'],
value2: Value
): boolean {
if (!value1) {
value1 = 0;
}
if (!value2) {
value2 = 0;
}
if (Array.isArray(value2)) {
return compareOp(value1, infix, value2[0]);
}
const type1 = typeof value1 as typeName,
type2 = typeof value2 as typeName;
if (type1 === type2) {
// same type comparison
switch (infix) {
case '=':
return value1 === value2;
case '>':
return value1 > value2;
case '<':
return value1 < value2;
case '<>':
return value1 !== value2;
case '<=':
return value1 <= value2;
case '>=':
return value1 >= value2;
}
} else {
switch (infix) {
case '=':
return false;
case '>':
return type2Number[type1] > type2Number[type2];
case '<':
return type2Number[type1] < type2Number[type2];
case '<>':
return true;
case '<=':
return type2Number[type1] <= type2Number[type2];
case '>=':
return type2Number[type1] >= type2Number[type2];
}
}
throw Error('Infix.compareOp: Should not reach here.');
}
export function evalCriterial(
criteria: Criteria,
value: string | number | boolean
): boolean {
if (criteria.op === 'wc') {
return criteria.match === (criteria.value as RegExp).test('' + value);
}
return compareOp(value, criteria.op, criteria.value as Value);
}

View File

@ -0,0 +1,17 @@
import {FormulaEnv} from '../FormulaEnv';
import FormulaError from '../FormulaError';
import {Parser} from '../Parser';
import {tokenize} from '../tokenizer';
import {EvalResult} from './EvalResult';
import {FormulaVisitor} from './FormulaVisitor';
export function evalFormula(formula: string, env: FormulaEnv): EvalResult {
const parser = new Parser(tokenize(formula));
const ast = parser.parse();
const formulaVisitor = new FormulaVisitor(env);
const result = formulaVisitor.visit(ast);
if (result === Infinity) {
throw FormulaError.NUM;
}
return result;
}

View File

@ -0,0 +1,14 @@
import {toExcelDate} from '../../io/excel/util/fromExcelDate';
import {getNumber} from '../functions/util/getNumber';
import {EvalResult} from './EvalResult';
export function toNum(evalResult: EvalResult) {
if (evalResult instanceof Date) {
return toExcelDate(evalResult);
}
const num = getNumber(evalResult, undefined, true);
if (num !== undefined) {
return num;
}
return 0;
}

View File

@ -0,0 +1,8 @@
import {EvalResult} from './EvalResult';
export function toString(evalResult: EvalResult) {
if (typeof evalResult === 'boolean') {
return evalResult ? 'TRUE' : 'FALSE';
}
return '' + evalResult;
}

View File

@ -0,0 +1,11 @@
import {ASTNode} from '../ast/ASTNode';
import {EvalResult} from './EvalResult';
import {FormulaVisitor} from './FormulaVisitor';
export function visitArray(
visitor: FormulaVisitor,
node: ASTNode
): EvalResult[] {
const arr = node.children.map(child => visitor.visit(child));
return arr;
}

View File

@ -0,0 +1,158 @@
import FormulaError from '../FormulaError';
import {ASTNode} from '../ast/ASTNode';
import {EvalResult} from './EvalResult';
import {FormulaVisitor} from './FormulaVisitor';
import {toNum} from './toNum';
import {toString} from './toStr';
/**
*
*/
export function visitBinaryExpr(
visitor: FormulaVisitor,
node: ASTNode
): EvalResult {
const op = node.token.value;
const left = visitor.visit(node.children[0]);
const right = visitor.visit(node.children[1]);
switch (op) {
case '+':
return plus(left, right);
case '-':
return minus(left, right);
case '*':
return mul(left, right);
case '/':
return div(left, right);
case '^':
return pow(left, right);
case '&':
return toString(left) + toString(right);
case '=':
return JSON.stringify(left) === JSON.stringify(right);
case '<':
return toNum(left) < toNum(right);
case '>':
return toNum(left) > toNum(right);
case '<=':
return toNum(left) <= toNum(right);
case '>=':
return toNum(left) >= toNum(right);
case '<>':
return JSON.stringify(left) !== JSON.stringify(right);
default:
throw new Error('Not implemented ' + op);
}
}
/**
*
*/
function plus(a: EvalResult, b: EvalResult) {
if (!Array.isArray(a) && !Array.isArray(b)) {
const aNumber = toNum(a);
const bNumber = toNum(b);
return aNumber + bNumber;
} else if (Array.isArray(a) && !Array.isArray(b)) {
return arrayConstOp(a, b, plus);
} else if (!Array.isArray(a) && Array.isArray(b)) {
return arrayConstOp(b, a, plus);
} else {
return arrayArrayOp(a as EvalResult[], b as EvalResult[], plus);
}
}
function minus(a: EvalResult, b: EvalResult) {
if (!Array.isArray(a) && !Array.isArray(b)) {
return toNum(a) - toNum(b);
} else if (Array.isArray(a) && !Array.isArray(b)) {
return arrayConstOp(a, b, minus);
} else if (!Array.isArray(a) && Array.isArray(b)) {
return arrayConstOp(b, a, minus);
} else {
return arrayArrayOp(a as EvalResult[], b as EvalResult[], minus);
}
}
function mul(a: EvalResult, b: EvalResult) {
if (!Array.isArray(a) && !Array.isArray(b)) {
return toNum(a) * toNum(b);
} else if (Array.isArray(a) && !Array.isArray(b)) {
return arrayConstOp(a, b, mul);
} else if (!Array.isArray(a) && Array.isArray(b)) {
return arrayConstOp(b, a, mul);
} else {
return arrayArrayOp(a as EvalResult[], b as EvalResult[], mul);
}
}
function div(a: EvalResult, b: EvalResult): EvalResult {
if (!Array.isArray(a) && !Array.isArray(b)) {
const rightValue = toNum(b);
if (rightValue === 0) {
return {
type: 'Error',
value: FormulaError.DIV0.name
};
}
return toNum(a) / rightValue;
} else if (Array.isArray(a) && !Array.isArray(b)) {
return arrayConstOp(a, b, div);
} else if (!Array.isArray(a) && Array.isArray(b)) {
return arrayConstOp(b, a, div);
} else {
return arrayArrayOp(a as EvalResult[], b as EvalResult[], div);
}
}
function pow(a: EvalResult, b: EvalResult) {
if (!Array.isArray(a) && !Array.isArray(b)) {
return Math.pow(toNum(a), toNum(b));
} else if (Array.isArray(a) && !Array.isArray(b)) {
return arrayConstOp(a, b, pow);
} else if (!Array.isArray(a) && Array.isArray(b)) {
return arrayConstOp(b, a, pow);
} else {
return arrayArrayOp(a as EvalResult[], b as EvalResult[], pow);
}
}
function arrayConstOp(
a: EvalResult[],
b: EvalResult,
op: (a: EvalResult, b: EvalResult) => EvalResult
): EvalResult[] {
return a.map(item => {
if (Array.isArray(item)) {
return item.map(subItem => op(subItem, b));
}
return op(item, b);
});
}
function arrayArrayOp(
a: EvalResult[],
b: EvalResult[],
op: (a: EvalResult, b: EvalResult) => EvalResult
): EvalResult[] {
return a.map((item, index) => {
if (Array.isArray(item) && Array.isArray(b[index])) {
return arrayArrayOp(item, b[index] as EvalResult[], op);
}
return op(item, b[index]);
});
}

View File

@ -0,0 +1,52 @@
import {ASTNode} from '../ast/ASTNode';
import {parseDateWithExtra} from '../functions/date';
import {removeQuote} from '../parser/remoteQuote';
import {EvalResult} from './EvalResult';
import {FormulaVisitor} from './FormulaVisitor';
function convertString(str: string) {
if (str.match(/^\$\d+\.\d+$/)) {
return parseFloat(str.slice(1));
}
// 将 mm/dd/yyyy 转成 Date 对象
if (str.match(/^\d{1,2}\/\d{1,2}\/\d{4}$/)) {
const {date, isDateGiven} = parseDateWithExtra(str);
if (isDateGiven) {
return date;
}
}
// 自动解析日期
return str;
}
export function visitConstant(
visitor: FormulaVisitor,
node: ASTNode
): EvalResult {
const nodeName = node.token.name;
switch (nodeName) {
case 'NUMBER':
return parseFloat(node.token.value);
case 'STRING':
return convertString(removeQuote(node.token.value));
case 'SINGLE_QUOTE_STRING':
return convertString(removeQuote(node.token.value));
case 'BOOLEAN':
return node.token.value === 'TRUE';
case 'ERROR':
case 'ERROR_REF':
return {
type: 'Error',
value: node.token.value
};
default:
throw new Error('Not implemented constant ' + nodeName);
}
}

View File

@ -0,0 +1,39 @@
import {FormulaEnv} from '../FormulaEnv';
import FormulaError from '../FormulaError';
import {ASTNode} from '../ast/ASTNode';
import {FunctionName} from '../builtinFunctions';
import {functions} from '../functions/functions';
import {
AREAS,
COLUMN,
COLUMNS,
ISREF,
ROW,
ROWS,
SUBTOTAL,
specialFunctions
} from '../functions/special';
import {EvalResult} from './EvalResult';
import {FormulaVisitor} from './FormulaVisitor';
export function visitFunction(
visitor: FormulaVisitor,
env: FormulaEnv,
node: ASTNode
): EvalResult {
const funcName = node.token.value.replace(/\($/, '');
if (specialFunctions.has(funcName)) {
return specialFunctions.get(funcName)!(env, node);
}
const args = node.children.map(child => visitor.visit(child));
const func = functions.get(funcName as FunctionName);
if (!func) {
console.error(`Function ${funcName} not found`);
throw FormulaError.NAME;
}
return func(...args);
}

View File

@ -0,0 +1,48 @@
import {getIntersectRanges} from '../../io/excel/util/Range';
import {FormulaEnv} from '../FormulaEnv';
import FormulaError from '../FormulaError';
import {ASTNode} from '../ast/ASTNode';
import {EvalResult} from './EvalResult';
import {FormulaVisitor} from './FormulaVisitor';
/**
*
* @param visitor
* @param env
* @param node
* @returns
*/
export function visitIntersection(
visitor: FormulaVisitor,
env: FormulaEnv,
node: ASTNode
): EvalResult {
const refs = node.refs;
if (!refs) {
throw new Error("Intersection node doesn't have refs");
}
const ranges = refs.map(ref => {
if ('name' in ref) {
throw 'only range ref is supported in intersection';
}
return ref.range;
});
const range = getIntersectRanges(ranges);
if (range === null) {
throw FormulaError.VALUE;
}
const cellData = env.getByRange(range, refs[0].sheetName);
if (cellData.length === 1) {
return cellData[0].value;
}
const arrayResult: EvalResult[] = [];
cellData.forEach(cellValue => {
arrayResult.push(cellValue.value);
});
return arrayResult;
}

View File

@ -0,0 +1,15 @@
import {ASTNode} from '../ast/ASTNode';
import {EvalResult} from './EvalResult';
import {FormulaVisitor} from './FormulaVisitor';
export function visitPercent(
visitor: FormulaVisitor,
node: ASTNode
): EvalResult {
const child = visitor.visit(node.children[0]);
if (typeof child === 'number') {
return child / 100;
} else {
throw new Error('Not a number');
}
}

View File

@ -0,0 +1,76 @@
import {isSingleCell} from '../../io/excel/util/Range';
import {CellValue} from '../../types/CellValue';
import {FormulaEnv} from '../FormulaEnv';
import {ASTNode} from '../ast/ASTNode';
import {CellReference, NameReference} from '../ast/Reference';
import {EvalResult} from './EvalResult';
import {evalFormula} from './evalFormula';
export function visitReference(env: FormulaEnv, node: ASTNode): EvalResult {
const ref = node.ref;
if (!ref) {
throw new Error("Reference node doesn't have ref");
}
if ('name' in ref) {
return getResultByName(env, ref);
} else {
return getResultByRange(env, ref, env.getByRange(ref.range, ref.sheetName));
}
}
function getResultByName(env: FormulaEnv, ref: NameReference) {
const nameValue = env.getDefinedName(ref.name);
return evalFormula(nameValue, env);
}
function getResultByRange(
env: FormulaEnv,
ref: CellReference,
cellData: CellValue[]
): EvalResult {
const range = ref.range;
// 单个值的情况直接返回,避免后面还得解二维数组
if (isSingleCell(range)) {
if (cellData.length === 0) {
return undefined;
}
return cellData[0].value;
}
// 构建二维数组
const arrayResult: EvalResult[][] = [];
const valueMap = new Map<string, CellValue['value']>();
cellData.forEach(cellValue => {
valueMap.set(cellValue.row + '-' + cellValue.col, cellValue.value);
});
for (let i = range.startRow; i <= range.endRow; i++) {
const row: EvalResult[] = [];
for (let j = range.startCol; j <= range.endCol; j++) {
row.push(valueMap.get(i + '-' + j));
}
arrayResult.push(row);
}
return arrayResult;
}
export function visitReferenceIgnoreHidden(
env: FormulaEnv,
node: ASTNode
): EvalResult {
const ref = node.ref;
if (!ref) {
throw new Error("Reference node doesn't have ref");
}
if ('name' in ref) {
return getResultByName(env, ref);
} else {
return getResultByRange(
env,
ref,
env.getByRangeIgnoreHidden(ref.range, ref.sheetName)
);
}
}

View File

@ -0,0 +1,23 @@
import {ASTNode} from '../ast/ASTNode';
import {EvalResult} from './EvalResult';
import {FormulaVisitor} from './FormulaVisitor';
export function visitUnaryExpr(
visitor: FormulaVisitor,
node: ASTNode
): EvalResult {
if (node.token.value === '-') {
const child = visitor.visit(node.children[0]);
if (typeof child === 'number') {
return -child;
} else {
throw new Error('UnaryExpr child is not a number');
}
} else {
if (node.token.value === '+') {
return visitor.visit(node.children[0]);
}
}
throw new Error('Not implemented ' + node.token.value);
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,84 @@
import {parseRange} from '../../../io/excel/util/Range';
import {CellValue} from '../../../types/CellValue';
import {RangeRef} from '../../../types/RangeRef';
import {FormulaEnv} from '../../FormulaEnv';
import FormulaError from '../../FormulaError';
import {EvalResult} from '../../eval/EvalResult';
import {evalFormula} from '../../eval/evalFormula';
export type CellData =
| string
| boolean
| undefined
| number
| {
date: number;
};
export type Data = CellData[][];
export type TestCase = Record<
string,
EvalResult | FormulaError | string | number
>;
export function testEvalCases(testCase: TestCase, env: FormulaEnv) {
for (const [formula, expected] of Object.entries(testCase)) {
console.log(formula, expected);
if (expected instanceof FormulaError) {
expect(() => evalFormula(formula, env)).toThrowError(expected);
} else {
if (typeof expected === 'number') {
expect(evalFormula(formula, env)).toBeCloseTo(expected);
} else {
expect(evalFormula(formula, env)).toEqual(expected);
}
}
}
}
/**
*
* @param data
* @param vars
* @returns
*/
export function buildEnv(data: Data, vars: Map<string, string> = new Map()) {
function getDefinedName(name: string, sheetName?: string) {
if (vars.has(name)) {
const value = vars.get(name)!;
return value;
}
throw new Error('未找到变量');
}
function getByRange(range: RangeRef, sheetName?: string) {
const result: CellValue[] = [];
for (let i = range.startRow; i <= range.endRow; i++) {
for (let j = range.startCol; j <= range.endCol; j++) {
let isDate = false;
let value = data[i][j];
if (value !== null && typeof value === 'object' && 'date' in value) {
isDate = true;
value = value.date;
}
result.push({
row: i,
col: j,
text: data[i][j] + '',
value: value,
isDate
});
}
}
return result;
}
return {
getDefinedName,
getByRange,
getByRangeIgnoreHidden: getByRange,
formulaCell: () => {
return {startRow: 1, startCol: 1, endRow: 1, endCol: 1};
}
} as FormulaEnv;
}

View File

@ -0,0 +1,52 @@
import FormulaError from '../../FormulaError';
import {getDatabaseResult} from '../database';
import {regFunc} from '../functions';
import {TestCase, buildEnv, testEvalCases} from './buildEnv';
const data = [
['Tree', 'Height', 'Age', 'Yield', 'Profit', 'Height'],
['=Apple', '>10', , , , '<16'],
['=Pear', , , ,],
['Tree', 'Height', 'Age', 'Yield', 'Profit'],
['Apple', 18, 20, 14, 105],
['Pear', 12, 12, 10, 96],
['Cherry', 13, 14, 9, 105],
['Apple', 14, 15, 10, 75],
['Pear', 9, 8, 8, 76.8],
['Apple', 8, 9, 6, 45]
];
const env = buildEnv(data);
function runTest(testCase: TestCase) {
testEvalCases(testCase, env);
}
test('getDataBaseResult', () => {
const data = [
['Tree', 'Height', 'Age', 'Yield', 'Profit'],
['Apple', 18, 20, 14, 105],
['Pear', 12, 12, 10, 96],
['Cherry', 13, 14, 9, 105],
['Apple', 14, 15, 10, 75],
['Pear', 9, 8, 8, 76.8],
['Apple', 8, 9, 6, 45]
];
const filter = [
['Tree', 'Height', 'Age', 'Yield', 'Profit', 'Height'],
['=Apple', '>10', , , , '<16'],
['=Pear', , , ,]
];
const filterApple = [
['Tree', 'Height'],
['=Apple', '>10']
];
expect(getDatabaseResult(data, filterApple, 'Yield')).toEqual([14, 10]);
});
test('DAVERAGE', () => {
runTest({'DAVERAGE(A4:E10, "Yield", A1:B2)': 12});
});

View File

@ -0,0 +1,310 @@
import FormulaError from '../../FormulaError';
import {TestCase, buildEnv, testEvalCases} from './buildEnv';
const data = [
['fruit', 'price', 'count', 4, 5],
['Apples', 0.69, 40, 5, 6],
['Bananas', 0.34, 38, 5, 6],
[41235, 0.55, 15, 5, 6],
[41247, 0.25, 25, 5, 6],
[41295, 0.59, 40, 5, 6],
['Almonds', 2.8, 10, 5, 6], // row 7
['Cashews', 3.55, 16, 5, 6], // row 8
['Peanuts', 1.25, 20, 5, 6], // row 9
['Walnuts', 1.75, 12, 5, 6], // row 10
['Apples', 'Lemons', 0, 0, 0], // row 11
['Bananas', 'Pears', 0, 0, 0] // row 12
];
const env = buildEnv(data);
function runTest(testCase: TestCase) {
testEvalCases(testCase, env);
}
test('DATE', () => {
runTest({
'DATE(108,1,2)': 39449,
'DATE(1,1,2)': 368,
'DATE(2008,1,2)': 39449,
'DATE(2008,14,2)': 39846,
'DATE(2008,-3,2)': 39327,
'DATE(2008,1,35)': 39482,
'DATE(2008,1,-15)': 39432,
'DATE(-1,1,2)': FormulaError.NUM,
'DATE(10000,1,2)': FormulaError.NUM
});
});
test('DATEDIF', () => {
runTest({
'DATEDIF("1/1/2001","1/1/2003","Y")': 2,
'DATEDIF("1/2/2001","1/1/2003","Y")': 1,
'DATEDIF("6/1/2001","8/15/2002","M")': 14,
'DATEDIF("6/16/2001","8/15/2002","M")': 13,
'DATEDIF("9/15/2001","8/15/2003","M")': 23,
'DATEDIF("6/1/2001","8/15/2002","D")': 440,
'DATEDIF("6/1/2001","6/1/2002","D")': 365,
'DATEDIF("6/1/2001","8/15/2002","MD")': 14,
'DATEDIF("8/16/2001","8/15/2002","MD")': 30,
'DATEDIF("5/16/2001","7/15/2002","MD")': 29,
'DATEDIF("5/15/2001","7/15/2002","MD")': 0,
'DATEDIF("6/1/2001","8/15/2003","YM")': 2,
'DATEDIF("6/16/2001","8/15/2003","YM")': 1,
'DATEDIF("9/15/2001","8/15/2003","YM")': 11,
'DATEDIF("9/15/2001","9/15/2003","YM")': 0,
'DATEDIF("6/1/2001","8/15/2002","YD")': 75,
'DATEDIF("9/15/2001","8/15/2003","YD")': 334,
'DATEDIF("8/15/2001","8/15/2003","YD")': 0,
'DATEDIF("8/14/2001","8/15/2003","YD")': 1,
'DATEDIF("8/14/2005","8/15/2003","YD")': FormulaError.NUM
});
});
test('DATEVALUE', () => {
runTest({
// 目前这个用例是可以正常解析的
// 'DATEVALUE("4:48:18 PM")': 0,
'DATEVALUE("January 1, 2008")': 39448,
'DATEVALUE("1/1/2008")': 39448,
'DATEVALUE("1-Jan-2008")': 39448,
'DATEVALUE("8/22/2011")': 40777,
'DATEVALUE("22-MAY-2011")': 40685,
'DATEVALUE("2011/02/23")': 40597,
'DATEVALUE("December 31, 9999")': 2958465,
'DATEVALUE("11" & "/" & "3" & "/" & "2011")': 40850,
// all formats
'DATEVALUE("12/3/2014")': 41976,
'DATEVALUE("Wednesday, December 3, 2014")': 41976,
'DATEVALUE("2014-12-3")': 41976,
'DATEVALUE("12/3/14")': 41976,
'DATEVALUE("12/03/14")': 41976,
'DATEVALUE("3-Dec-14")': 41976,
'DATEVALUE("03-Dec-14")': 41976,
'DATEVALUE("December 3, 2014")': 41976,
'DATEVALUE("12/3/14 12:00 AM")': 41976,
'DATEVALUE("12/3/14 0:00")': 41976,
'DATEVALUE("12/03/2014")': 41976,
'DATEVALUE("3-Dec-2014")': 41976,
// 这些例子每年都会变,所以不测试
// 'DATEVALUE("Dec-3")': 44533, // *special
// 'DATEVALUE("December-3")': 44533, // *special
// 'DATEVALUE("3-Dec")': 44533, // *special
// 'DATEVALUE("12/3")': 44533, // *special
// 'DATEVALUE("Dec-3 11:11")': 44533, // *special
'DATEVALUE("1900/1/1")': 1,
'DATEVALUE("10000/12/1")': FormulaError.VALUE
});
});
test('DAY', () => {
runTest({
'DAY(DATEVALUE("15-Apr-11"))': 15,
'DAY("15-Apr-11")': 15,
'DAY(-12)': FormulaError.VALUE
});
});
test('DAYS', () => {
runTest({
'DAYS("2020/3/1", "2020/2/1")': 29,
'DAYS("3/15/11","2/1/11")': 42,
'DAYS(DATEVALUE("3/15/11"),DATEVALUE("2/1/11"))': 42,
'DAYS("12/31/2011","1/1/2011")': 364,
'DAYS(DATEVALUE("12/31/2011"),DATEVALUE("1/1/2011"))': 364
});
});
test('DAYS360', () => {
runTest({
'DAYS360("2/1/2019", "3/1/2019")': 30,
'DAYS360("2/1/2019", "2/28/2019")': 30,
'DAYS360("1/31/2019", "3/31/2019")': 60,
'DAYS360("2/1/2019", "3/31/2019")': 60,
'DAYS360("2/1/2019", "3/31/2019", TRUE)': 59,
'DAYS360("3/31/2019", "3/31/2019")': 0,
'DAYS360("3/31/2019", "3/31/2019", TRUE)': 0,
'DAYS360("1/31/2019", 3/31/2019)': -42870,
'DAYS360("3/15/2019", "3/31/2019")': 16,
'DAYS360("3/15/2019", "3/31/2020")': 376,
'DAYS360("12/31/2019", "1/9/2020")': 9
});
});
test('EDATE', () => {
runTest({'EDATE("15-Jan-11",1)': 40589});
});
test('EOMONTH', () => {
runTest({'EOMONTH("1-Jan-11",1)': 40602, 'EOMONTH("1-Jan-11",-3)': 40482});
});
test('HOUR', () => {
runTest({
'HOUR(0.75)': 18,
'HOUR("7/18/2011 7:45")': 7,
'HOUR("4/21/2012")': 0,
'HOUR("4 PM")': 16,
'HOUR("4")': 0,
'HOUR("16:00")': 16
});
});
test('ISOWEEKNUM', () => {
runTest({'ISOWEEKNUM("3/9/2012")': 10});
});
test('MINUTE', () => {
runTest({'MINUTE("12:45:00 PM")': 45});
});
test('MONTH', () => {
runTest({'MONTH("15-Apr-11")': 4});
});
test('NETWORKDAYS', () => {
runTest({
'NETWORKDAYS("10/1/2012","3/1/2013")': 110,
'NETWORKDAYS("10/1/2012","3/1/2013", 41235)': 109,
'NETWORKDAYS("10/1/2012","3/1/2013", {41235})': 109,
'NETWORKDAYS("10/1/2012","3/1/2013", A4)': 109,
'NETWORKDAYS("10/1/2012","3/1/2013", A4:A6)': 107,
'NETWORKDAYS(DATE(2006,1,1),DATE(2006,1,31))': 22,
'NETWORKDAYS(DATE(2006,2,28),DATE(2006,1,31))': -21
});
});
test('NETWORKDAYS.INTL', () => {
runTest({
'NETWORKDAYS.INTL(DATE(2006,1,1),DATE(2006,1,31))': 22,
'NETWORKDAYS.INTL(DATE(2006,2,28),DATE(2006,1,31))': -21,
'NETWORKDAYS.INTL(DATE(2006,2,28),DATE(2006,1,31), "1111111")': 0,
'NETWORKDAYS.INTL(DATE(2006,1,1),DATE(2006,2,1),7,{"2006/1/2","2006/1/16"})': 22,
'NETWORKDAYS.INTL(DATE(2006,1,1),DATE(2006,2,1),"0010001",{"2006/1/2","2006/1/16"})': 20,
'NETWORKDAYS.INTL(DATE(2006,1,1),DATE(2006,1,31), "1")': FormulaError.VALUE,
'NETWORKDAYS.INTL(DATE(2006,2,28),DATE(2006,1,31), "01111111")':
FormulaError.VALUE
});
});
test('NOW', () => {
runTest({'YEAR(NOW())': new Date().getFullYear()});
});
test('SECOND', () => {
runTest({
'SECOND("4:48:18 PM")': 18,
'SECOND("4:48 PM")': 0
});
});
test('TIME', () => {
runTest({
'TIME(12,0,0)': 0.5,
'TIME(16,48,10)': 0.7001157407407408,
'TIME(-12,0,0)': FormulaError.NUM
});
});
test('TIMEVALUE', () => {
runTest({
'TIMEVALUE("2:24 AM")': 0.1,
'TIMEVALUE("22-Aug-2011 6:35 AM")': 0.2743055555555556
});
});
test('TODAY', () => {
runTest({'YEAR(TODAY())': new Date().getFullYear()});
});
test('WEEKDAY', () => {
runTest({
'WEEKDAY("2/14/2008")': 5,
'WEEKDAY("2/14/2008", 2)': 4,
'WEEKDAY("2/14/2008", "2")': 4,
'WEEKDAY("2/14/2008", 3)': 3,
'WEEKDAY("2/14/2008", 5)': FormulaError.NUM
});
});
test('WEEKNUM', () => {
runTest({
'WEEKNUM("3/9/2012")': 10,
'WEEKNUM("3/9/2012",2)': 11,
'WEEKNUM("3/9/2012",21)': 10,
'ISOWEEKNUM("3/9/2012")': 10
});
});
test('WORKDAY', () => {
runTest({
'WORKDAY(DATE(2008,10,1),1)': 39723,
'WORKDAY(DATE(2008,10,1),5)': 39729,
'WORKDAY(DATE(2008,10,9),1)': 39731,
'WORKDAY(DATE(2008,10,9),2)': 39734,
'WORKDAY(DATE(2008,10,1),151)': 39933,
'WORKDAY(DATE(2008,10,1),399)': 40281,
'WORKDAY(DATE(2008,10,1),151, {"2008/11/26","2008/12/4","2008/1/21"})': 39937
});
});
test('WORKDAY.INTL', () => {
runTest({
// 目前没报错
// 'WORKDAY.INTL(DATE(2012,1,1),30,0)': FormulaError.NUM,
'WORKDAY.INTL(DATE(2012,1,1),30,"1")': FormulaError.VALUE,
'WORKDAY.INTL(DATE(2012,1,1),1,11)': 40910,
'WORKDAY.INTL(DATE(2012,1,1),1,"0000011")': 40910,
'WORKDAY.INTL(DATE(2012,1,1),1,"1111111")': FormulaError.VALUE,
'WORKDAY.INTL(DATE(2012,1,1),1,"011111")': FormulaError.VALUE,
'WORKDAY.INTL(DATE(2012,1,1),90,11)': 41013,
'WORKDAY.INTL(DATE(2012,1,1),30,17)': 40944,
'TEXT(WORKDAY.INTL(DATE(2012,1,1),30,17),"m/dd/yyyy")': '2/05/2012'
});
});
test('YEAR', () => {
runTest({
'YEAR("7/5/2008")': 2008,
'YEAR("7/5/2010")': 2010
});
});
test('YEARFRAC', () => {
runTest({
'YEARFRAC("2012/1/1","2012/7/30")': 0.58055556,
'YEARFRAC("1/1/2012","7/30/2012")': 0.58055556,
'YEARFRAC("2006/1/31","2006/3/31")': 0.1666666667,
'YEARFRAC("2006/1/31","2006/3/29")': 0.163888889,
'YEARFRAC("2006/1/30","2006/3/31")': 0.1666666667,
'YEARFRAC("1900/1/30","1900/3/31", 1)': 0.167123288,
'YEARFRAC("1900/3/31","1900/1/30", 1)': 0.167123288,
'YEARFRAC("2020/2/5","2021/2/1", 1)': 0.989071038,
'YEARFRAC("2020/2/1","2021/1/1", 1)': 0.915300546,
'YEARFRAC("2020/2/1","2022/1/1", 1)': 1.916058394,
'YEARFRAC("1/1/2006","3/1/2006", 1)': 0.161643836,
'YEARFRAC("1/1/2012","7/30/2012", 1)': 0.57650273,
'YEARFRAC("1/1/2012","7/30/2019", 1)': 7.575633128,
'YEARFRAC("1/1/2012","7/30/2012", 2)': 0.58611111,
'YEARFRAC("2012/1/1","2014/7/30", 2)': 2.613888889,
'YEARFRAC("1/1/2012","7/30/2012", 3)': 0.57808219,
'YEARFRAC("1/1/2012","7/30/2012", 4)': 0.58055556,
'YEARFRAC("2012/1/1","2013/7/30",4)': 1.580555556,
'YEARFRAC("2012/1/1","2012/7/30", 5)': FormulaError.VALUE
});
});

View File

@ -0,0 +1,657 @@
import FormulaError from '../../FormulaError';
import {TestCase, buildEnv, testEvalCases} from './buildEnv';
const data = [
['fruit', 'price', 'count', 4, 5],
['Apples', 0.69, 40, 5, 6],
['Bananas', 0.34, 38, 5, 6],
[41235, 0.55, 15, 5, 6],
[41247, 0.25, 25, 5, 6],
[41295, 0.59, 40, 5, 6],
['Almonds', 2.8, 10, 5, 6], // row 7
['Cashews', 3.55, 16, 5, 6], // row 8
['Peanuts', 1.25, 20, 5, 6], // row 9
['Walnuts', 1.75, 12, 5, 6], // row 10
['Apples', 'Lemons', 0, 0, 0], // row 11
['Bananas', 'Pears', 0, 0, 0] // row 12
];
const env = buildEnv(data);
function runTest(testCase: TestCase) {
testEvalCases(testCase, env);
}
test('BESSELI', () => {
runTest({
'BESSELI("1.5", 1)': 0.981666428,
'BESSELI(1.5, 1)': 0.981666428,
'BESSELI(1.5, 1.9)': 0.981666428,
'BESSELI(1.5, -1)': FormulaError.NUM,
'BESSELI(TRUE, 1)': FormulaError.VALUE,
'BESSELI("a", 1)': FormulaError.VALUE,
'BESSELI(1.5, {1,2})': 0.981666428
});
});
test('BESSELJ', () => {
runTest({
'BESSELJ(1.9, 2)': 0.329925829,
'BESSELJ(1.9, 2.9)': 0.329925829,
'BESSELJ(1.9, -2.9)': FormulaError.NUM,
'BESSELJ(TRUE, 2)': FormulaError.VALUE,
'BESSELJ("a", 2)': FormulaError.VALUE,
'BESSELJ(1.9, {2,3})': 0.329925829
});
});
test('BESSELK', () => {
runTest({
'BESSELK(1.5, 1)': 0.277387804,
'BESSELK(1.5, 1.9)': 0.277387804,
'BESSELK(1.5, -1)': FormulaError.NUM,
'BESSELK(TRUE, 1)': FormulaError.VALUE,
'BESSELK("a", 1)': FormulaError.VALUE,
'BESSELK(1.5, {1,2})': 0.277387804
});
});
test('BESSELY', () => {
runTest({
'BESSELY("2.5", 1)': 0.145918138,
'BESSELY(2.5, 1)': 0.145918138,
'BESSELY(2.5, 1.9)': 0.145918138,
'BESSELY(2.5, -1)': FormulaError.NUM,
'BESSELY(TRUE, 1)': FormulaError.VALUE,
'BESSELY("a", 1)': FormulaError.VALUE,
'BESSELY(2.5, {1,2})': 0.145918138
});
});
test('BIN2DEC', () => {
runTest({
'BIN2DEC(1100100)': 100,
'BIN2DEC(1111111111)': -1,
'BIN2DEC(11001001001101)': FormulaError.NUM, // contain more than 10 characters
'BIN2DEC(110010023145)': FormulaError.NUM, // not a valid binary number
'BIN2DEC(TRUE)': FormulaError.VALUE,
'BIN2DEC(FALSE)': FormulaError.VALUE,
'BIN2DEC({1100100})': 100,
'BIN2DEC(1010101010)': -342
});
});
test('BIN2HEX', () => {
runTest({
'BIN2HEX(11111011)': 'FB',
'BIN2HEX(11111011,4)': '00FB',
'BIN2HEX("11111011",4)': '00FB',
'BIN2HEX("11111011",-4)': FormulaError.NUM,
'BIN2HEX(11111011,"4")': '00FB',
'BIN2HEX(11111011,1)': FormulaError.NUM,
'BIN2HEX(11111011,10)': '00000000FB',
'BIN2HEX(11111011,3)': '0FB',
'BIN2HEX(11111011,2)': 'FB',
'BIN2HEX(11111011011,2)': FormulaError.NUM,
'BIN2HEX(1110)': 'E',
'BIN2HEX(1111111111)': 'FFFFFFFFFF',
'BIN2HEX(1111111111,7)': 'FFFFFFFFFF',
'BIN2HEX(1111111111,{1})': 'FFFFFFFFFF',
'BIN2HEX(TRUE,2)': FormulaError.VALUE,
'BIN2HEX(TRUE,3)': FormulaError.VALUE
});
});
test('BIN2OCT', () => {
runTest({
'BIN2OCT(1001,3)': '011',
'BIN2OCT("1001",3)': '011',
'BIN2OCT(1001,"3")': '011',
'BIN2OCT(10010101011,"3")': FormulaError.NUM,
'BIN2OCT(1001,"-7")': FormulaError.NUM,
'BIN2OCT(1001,"1")': FormulaError.NUM,
'BIN2OCT(1100100)': '144',
'BIN2OCT({1100100})': '144',
'BIN2OCT(TRUE)': FormulaError.VALUE,
'BIN2OCT(FALSE)': FormulaError.VALUE,
'BIN2OCT(1111111111)': '7777777777'
});
});
test('BITAND', () => {
runTest({
'BITAND(1,5)': 1,
'BITAND(13,25)': 9,
'BITAND({13},25)': 9,
'BITAND(13,{25})': 9,
'BITAND("13","25")': 9,
'BITAND(-11,5)': FormulaError.NUM,
'BITAND(1,-5)': FormulaError.NUM,
'BITAND(-1,-5)': FormulaError.NUM,
'BITAND(19,281474976710659)': FormulaError.NUM,
'BITAND(281484976720655,5)': FormulaError.NUM,
'BITAND(TRUE,5)': 1,
'BITAND(1,FALSE)': 0,
'BITAND(1.7,FALSE)': FormulaError.NUM,
'BITAND(1, 5.2)': FormulaError.NUM,
'BITAND(TRUE,FALSE)': 0,
'BITAND("TRUE",7)': FormulaError.VALUE
});
});
test('BITLSHIFT', () => {
runTest({
'BITLSHIFT(4,2)': 16,
'BITLSHIFT(13,-2)': 3,
'BITLSHIFT(-4,2)': FormulaError.NUM,
'BITLSHIFT(4,-2)': 1,
'BITLSHIFT(9,-3)': 1,
'BITLSHIFT(4.1,2)': FormulaError.NUM,
'BITLSHIFT(4,2.4)': 16,
'BITLSHIFT(281494976710655,2)': FormulaError.NUM,
'BITLSHIFT(7,-54)': FormulaError.NUM,
'BITLSHIFT(3,58)': FormulaError.NUM
});
});
test('BITOR', () => {
runTest({
'BITOR(23,10)': 31,
'BITOR(5,3)': 7,
'BITOR(-5,10)': FormulaError.NUM,
'BITOR(3,-1)': FormulaError.NUM,
'BITOR(2.2,3)': FormulaError.NUM,
'BITOR(3, 5.1)': FormulaError.NUM,
'BITOR(281674976710655,10)': FormulaError.NUM,
'BITOR(17,281474976710656)': FormulaError.NUM,
'BITOR(TRUE,4)': 5,
'BITOR(60,FALSE)': 60
});
});
test('BITRSHIFT', () => {
runTest({
'BITRSHIFT(13,2)': 3,
'BITRSHIFT(13.9,2)': FormulaError.NUM,
'BITRSHIFT(13,2.7)': 3,
'BITRSHIFT(281474976720655,2)': FormulaError.NUM,
'BITRSHIFT(13,-42)': 57174604644352,
'BITRSHIFT(13,-62)': FormulaError.NUM,
'BITRSHIFT(24,3)': 3,
'BITRSHIFT(9999999999,31)': 4,
'BITRSHIFT(9999999999,22.5)': 2384,
'BITRSHIFT(999999999,31)': 0,
'BITRSHIFT(9999999999,-50)': FormulaError.NUM,
'BITRSHIFT(13,-44)': 228698418577408
});
});
test('BITXOR', () => {
runTest({
'BITXOR(5,3)': 6,
'BITXOR(24,3)': 27,
'BITXOR(5.5,3)': FormulaError.NUM,
'BITXOR(5,3.2)': FormulaError.NUM,
'BITXOR(-5,3)': FormulaError.NUM,
'BITXOR(5,-3)': FormulaError.NUM,
'BITXOR(281474976710658,3)': FormulaError.NUM,
'BITXOR(5,281474976710656)': FormulaError.NUM
});
});
test('COMPLEX', () => {
runTest({
'COMPLEX(0,0)': 0,
'COMPLEX(3,4)': '3+4i',
'COMPLEX(52,1)': '52+i',
'COMPLEX(3,-4)': '3-4i',
'COMPLEX(3,4,"j")': '3+4j',
'COMPLEX(0,1)': 'i',
'COMPLEX(0,-1)': '-i',
'COMPLEX(52,-1)': '52-i',
'COMPLEX(1,0)': '1',
'COMPLEX(3,4,"z")': FormulaError.VALUE,
'COMPLEX(0,4,"i")': '4i',
'COMPLEX(70,0,"j")': '70',
'COMPLEX(TRUE,4,"j")': FormulaError.VALUE,
'COMPLEX(3,FALSE,"j")': FormulaError.VALUE
});
});
test('CONVERT', () => {
runTest({
'CONVERT(1, "lbm", "kg")': 0.45359237
});
});
test('DEC2BIN', () => {
runTest({
'DEC2BIN(9,4)': '1001',
'DEC2BIN(512,4)': FormulaError.NUM,
'DEC2BIN(-523,4)': FormulaError.NUM,
'DEC2BIN(9)': '1001',
'DEC2BIN(19,-1)': FormulaError.NUM,
'DEC2BIN(9,3)': FormulaError.NUM,
'DEC2BIN(-100)': '1110011100'
});
});
test('DEC2HEX', () => {
runTest({
'DEC2HEX(100,4)': '0064',
'DEC2HEX(100000000000,5)': FormulaError.NUM,
'DEC2HEX(-549755813988 ,7)': FormulaError.NUM,
'DEC2HEX(100,-2)': FormulaError.NUM,
'DEC2HEX(-54)': 'FFFFFFFFCA',
'DEC2HEX(28)': '1C',
'DEC2HEX(64,1)': FormulaError.NUM
});
});
test('DEC2OCT', () => {
runTest({
'DEC2OCT(58,3)': '072',
'DEC2OCT(58,-3)': FormulaError.NUM,
'DEC2OCT(72,1)': FormulaError.NUM,
'DEC2OCT(-536874912,3)': FormulaError.NUM,
'DEC2OCT(536870932,3)': FormulaError.NUM,
'DEC2OCT(-100)': '7777777634',
'DEC2OCT(-256)': '7777777400'
});
});
test('DELTA', () => {
runTest({
'DELTA(5,4)': 0,
'DELTA(5,5)': 1,
'DELTA(0.5,0)': 0,
'DELTA(5)': 0,
'DELTA(0)': 1,
'DELTA(TRUE,5)': FormulaError.VALUE,
'DELTA(TRUE,)': FormulaError.VALUE,
// 'DELTA(5,FALSE)': FormulaError.VALUE,
'DELTA(5,"5")': 1,
'DELTA("1",1)': 1
});
});
test('ERF', () => {
runTest({
'ERF(0.745)': 0.70792892,
'ERF(1)': 0.84270079,
'ERF(TRUE)': FormulaError.VALUE
});
});
test('ERFC', () => {
runTest({'ERFC(1)': 0.15729921, 'ERFC(FALSE)': FormulaError.VALUE});
});
test('GESTEP', () => {
runTest({
'GESTEP(5,4)': 1,
'GESTEP(5,5)': 1,
'GESTEP(-4,-5)': 1,
'GESTEP(-1)': 0,
'GESTEP(0)': 1,
// 'GESTEP(1,TRUE)': FormulaError.VALUE,
'GESTEP(5,"3")': 1
});
});
test('HEX2BIN', () => {
runTest({
'HEX2BIN("F",8)': '00001111',
'HEX2BIN("F",3)': FormulaError.NUM,
'HEX2BIN("F",-8)': FormulaError.NUM,
'HEX2BIN("B7")': '10110111',
'HEX2BIN("FFFFFFFFFF")': '1111111111',
'HEX2BIN("F0FFFFFFFF")': FormulaError.NUM
});
});
test('HEX2DEC', () => {
runTest({
'HEX2DEC("A5")': 165,
'HEX2DEC("FFFFFFFF5B")': -165,
'HEX2DEC("FFDFFFFFFF")': -536870913,
'HEX2DEC("8FFFFFFF")': 2415919103,
'HEX2DEC("FFFFFFFF00")': -256,
'HEX2DEC("F000000000")': -68719476736,
'HEX2DEC("A000000000")': -412316860416,
'HEX2DEC("7fffffffff")': 549755813887, // max positive number
'HEX2DEC("3DA408B9")': 1034160313,
'HEX2DEC("G")': FormulaError.NUM
});
});
test('IMABS', () => {
runTest({
'IMABS("5+12i")': 13,
'IMABS("52+60i")': 79.397732965,
'IMABS("-24-72i")': 75.89466384,
'IMABS("3+4z")': FormulaError.NUM,
'IMABS("3+4j")': 5
});
});
test('IMAGINARY', () => {
runTest({
'IMAGINARY("")': FormulaError.NUM,
'IMAGINARY("3+4i")': 4,
'IMAGINARY("+4i")': 4,
'IMAGINARY("0")': 0,
'IMAGINARY(0)': 0,
'IMAGINARY("3+4z")': FormulaError.NUM,
'IMAGINARY("0-j")': -1,
'IMAGINARY("4")': 0,
'IMAGINARY("3+i")': 1,
'IMAGINARY("j")': 1,
'IMAGINARY("-i")': -1
});
});
test('IMARGUMENT', () => {
runTest({
'IMARGUMENT("3+4i")': 0.92729522,
'IMARGUMENT("-3-4i")': -2.2142974355882,
'IMARGUMENT("-15+5i")': 2.819842099,
'IMARGUMENT("0")': FormulaError.DIV0,
'IMARGUMENT("-14i")': -1.570796327,
'IMARGUMENT(TRUE)': FormulaError.VALUE,
'IMARGUMENT("-52")': 3.141592654,
'IMARGUMENT("24i")': 1.570796327,
'IMARGUMENT("5i")': 1.570796327,
'IMARGUMENT("52")': 0,
'IMARGUMENT("13+14z")': FormulaError.NUM
});
});
test('IMCONJUGATE', () => {
runTest({
'IMCONJUGATE("3+4i")': '3-4i',
'IMCONJUGATE("3-4i")': '3+4i',
'IMCONJUGATE("3+4z")': FormulaError.NUM,
'IMCONJUGATE("i")': '-i',
'IMCONJUGATE("-j")': 'j',
'IMCONJUGATE("3")': '3'
});
});
test('IMCOS', () => {
runTest({
'IMCOS(1)': '0.5403023058681398',
'IMCOS("1+i")': '0.8337300251311491-0.9888977057628651i',
'IMCOS("4+3i")': '-6.580663040551157+7.581552742746545i',
'IMCOS(TRUE)': FormulaError.VALUE,
'IMCOS("TRUE")': FormulaError.NUM
});
});
test('IMCOSH', () => {
runTest({
'IMCOSH(5)': '74.20994852478785',
'IMCOSH("4+3i")': '-27.034945603074224+3.851153334811777i',
'IMCOSH(FALSE)': FormulaError.VALUE,
'IMCOSH("4+3j")': '-27.034945603074224+3.851153334811777j',
'IMCOSH("4+3J")': FormulaError.NUM
});
});
test('IMCOT', () => {
runTest({
'IMCOT("4+3i")': '0.0049011823943044056-0.9992669278059017i',
'IMCOT(TRUE)': FormulaError.VALUE,
'IMCOT(FALSE)': FormulaError.VALUE
});
});
test('IMCSC', () => {
runTest({
'IMCSC("4+3i")': '-0.0754898329158637+0.0648774713706355i',
'IMCSC(TRUE)': FormulaError.VALUE,
'IMCSC(FALSE)': FormulaError.VALUE
});
});
test('IMCSCH', () => {
runTest({
'IMCSCH("4+3i")': '-0.03627588962862602-0.005174473184019398i',
'IMCSCH(TRUE)': FormulaError.VALUE,
'IMCSCH(FALSE)': FormulaError.VALUE
});
});
test('IMDIV', () => {
runTest({
'IMDIV("-238+240i","10+24i")': '5+12i',
'IMDIV(0,0)': FormulaError.NUM,
'IMDIV(TRUE,"52")': FormulaError.VALUE,
'IMDIV("100+3j",FALSE)': FormulaError.VALUE,
'IMDIV("52i+10",0)': FormulaError.NUM,
'IMDIV("-238+240i","10+24j")': FormulaError.NUM,
'IMDIV("52i+10",1)': FormulaError.NUM,
'IMDIV(10,10)': '1'
});
});
test('IMEXP', () => {
runTest({
'IMEXP("1+i")': '1.4686939399158851+2.2873552871788423i',
'IMEXP("1+j")': '1.4686939399158851+2.2873552871788423j',
'IMEXP(FALSE)': FormulaError.VALUE
});
});
test('IMLN', () => {
runTest({
'IMLN("3+4i")': '1.6094379124341003+0.9272952180016122i',
'IMLN("3+4j")': '1.6094379124341003+0.9272952180016122j',
'IMLN(TRUE)': FormulaError.VALUE
});
});
test('IMLOG10', () => {
runTest({
'IMLOG10("3+4i")': '0.6989700043360187+0.4027191962733731i',
'IMLOG10("3+4j")': '0.6989700043360187+0.4027191962733731j',
'IMLOG10(TRUE)': FormulaError.VALUE
});
});
test('IMLOG2', () => {
runTest({
'IMLOG2("3+4i")': '2.321928094887362+1.3378042124509761i',
'IMLOG2(TRUE)': FormulaError.VALUE,
'IMLOG2("3+4j")': '2.321928094887362+1.3378042124509761j'
});
});
test('IMPOWER', () => {
runTest({
'IMPOWER("2+3i", 3)': '-45.99999999999999+9.000000000000007i',
'IMPOWER(TRUE, 3)': FormulaError.VALUE,
'IMPOWER("2+3j", TRUE)': FormulaError.VALUE
});
});
test('IMPRODUCT', () => {
runTest({
'IMPRODUCT("3+4i","5-3i")': '27+11i',
'IMPRODUCT("3+4i","5-3j", "5-3j")': FormulaError.VALUE,
'IMPRODUCT("1+2i",30)': '30+60i',
'IMPRODUCT(" ")': FormulaError.NUM,
'IMPRODUCT("52+7i",12,"2+i")': '1164+792i',
'IMPRODUCT(TRUE,12)': FormulaError.VALUE,
'IMPRODUCT("2+3i",TRUE)': FormulaError.VALUE
});
});
test('IMREAL', () => {
runTest({
'IMREAL("")': FormulaError.NUM,
'IMREAL(0)': 0,
'IMREAL("0")': 0,
'IMREAL("6-9i")': 6,
'IMREAL("+1i")': 0,
'IMREAL("+52i")': 0,
'IMREAL("+10+2y")': FormulaError.NUM,
'IMREAL("+10+i")': 10,
'IMREAL("-j")': 0,
'IMREAL("+i")': 0,
'IMREAL("6+9k")': FormulaError.NUM,
'IMREAL("52")': 52,
'IMREAL("j")': 0,
'IMREAL("J")': FormulaError.NUM,
'IMREAL("5.2-9i")': 5.2,
'IMREAL("5.222-10i")': 5.222,
'IMREAL("{5-9j}")': FormulaError.NUM
});
});
test('IMSEC', () => {
runTest({
'IMSEC("4+3i")': '-0.06529402785794704-0.07522496030277322i',
'IMSEC(TRUE)': FormulaError.VALUE
});
});
test('IMSECH', () => {
runTest({
'IMSECH("4+3i")': '-0.03625349691586887-0.005164344607753179i',
'IMSECH(TRUE)': FormulaError.VALUE
});
});
test('IMSIN', () => {
runTest({
'IMSIN("4+3i")': '-7.61923172032141-6.5481200409110025i',
'IMSIN(TRUE)': FormulaError.VALUE,
'IMSIN("4+3j")': '-7.61923172032141-6.5481200409110025j'
});
});
test('IMSINH', () => {
runTest({
'IMSINH("4+3i")': '-27.016813258003932+3.853738037919377i',
'IMSINH(TRUE)': FormulaError.VALUE,
'IMSINH("4+3j")': '-27.016813258003932+3.853738037919377j'
});
});
test('IMSQRT', () => {
runTest({
'IMSQRT("1+i")': '1.0986841134678098+0.45508986056222733i',
'IMSQRT(FALSE)': FormulaError.VALUE,
'IMSQRT("1+j")': '1.0986841134678098+0.45508986056222733j'
});
});
test('IMSUB', () => {
runTest({
'IMSUB("13+4i","5+3i")': '8+i',
'IMSUB("13+4j","5+3j")': '8+j',
'IMSUB("13+4j","5+3i")': FormulaError.VALUE,
'IMSUB(TRUE,"5+3i")': FormulaError.VALUE,
'IMSUB("13+4i",FALSE)': FormulaError.VALUE
});
});
test('IMSUM', () => {
runTest({
'IMSUM("3+4i","5-3i")': '8+i',
'IMSUM("13+4j","5+3i")': FormulaError.VALUE,
'IMSUM("")': FormulaError.NUM
});
});
test('IMTAN', () => {
runTest({
'IMTAN("4+3i")': '0.004908258067495992+1.000709536067233i',
'IMTAN(TRUE)': FormulaError.VALUE,
'IMTAN(FALSE)': FormulaError.VALUE,
'IMTAN("24+72y")': FormulaError.NUM
});
});
test('OCT2BIN', () => {
runTest({
'OCT2BIN(7777777000)': '1000000000',
'OCT2BIN(3)': '11',
'OCT2BIN(34565423412, 3)': FormulaError.NUM,
// David
'OCT2BIN("string", 11)': FormulaError.NUM,
// 目前的实现不会报错
// 'OCT2BIN(3, "string")': FormulaError.VALUE,
// 1. If number's length larger than 10, returns #NUM!
'OCT2BIN(123456789012, 11)': FormulaError.NUM,
// 2. office: If places is negative, OCT2BIN returns the #NUM! error value.
'OCT2BIN(3, -3)': FormulaError.NUM,
'OCT2BIN(3, 3.1)': '011',
'OCT2BIN(3, 3.9)': '011',
// In microsoft Excel, if places is larger than 10, it will return #NUM!
'OCT2BIN(3, 11)': FormulaError.NUM,
'OCT2BIN(376, 10)': '0011111110',
'OCT2BIN(377, 10)': '0011111111',
'OCT2BIN(400, 10)': '0100000000',
'OCT2BIN(401, 10)': '0100000001',
'OCT2BIN(776, 10)': '0111111110',
'OCT2BIN(777, 10)': '0111111111',
'OCT2BIN(777, 8)': FormulaError.NUM,
'OCT2BIN(7771)': FormulaError.NUM
});
});
test('OCT2DEC', () => {
runTest({
'OCT2DEC(54)': 44,
'OCT2DEC(51)': 41,
'OCT2DEC(10)': 8,
'OCT2DEC(7777777533)': -165,
'OCT2DEC(3777777777)': 536870911,
'OCT2DEC(4000000000)': -536870912,
'OCT2DEC(4000000001)': -536870911,
'OCT2DEC(7777777777)': -1,
'OCT2DEC(12345671234)': FormulaError.NUM,
// If number is not a valid octal number, OCT2DEC returns the #NUM! error value.
// 'OCT2DEC(TRUE)': FormulaError.VALUE,
'OCT2DEC(TRUE)': FormulaError.NUM,
'OCT2DEC(8)': FormulaError.NUM,
'OCT2DEC("AAA")': FormulaError.NUM,
'OCT2DEC("//")': FormulaError.NUM
});
});
test('OCT2HEX', () => {
runTest({
'OCT2HEX(100, -4)': FormulaError.NUM,
'OCT2HEX(100)': '40',
'OCT2HEX(100, 4)': '0040',
'OCT2HEX(100, 1)': FormulaError.NUM,
'OCT2HEX(10077777775, 4)': FormulaError.NUM,
'OCT2HEX(520, 3)': '150',
'OCT2HEX(520, -3)': FormulaError.NUM,
'OCT2HEX(520, 2)': FormulaError.NUM,
'OCT2HEX(100, -1)': FormulaError.NUM,
'OCT2HEX(7777777533)': 'FFFFFFFF5B',
'OCT2HEX(7777777772,10)': 'FFFFFFFFFA',
'OCT2HEX(4000000001,10)': 'FFE0000001',
// david
'OCT2HEX("string", 10)': FormulaError.NUM,
// 'OCT2HEX(777, "aaa")': FormulaError.VALUE,
'OCT2HEX(12345671231, 10)': FormulaError.NUM,
'OCT2HEX(1234567123, 10)': '000A72EE53',
'OCT2HEX(888, 10)': FormulaError.NUM,
'OCT2HEX("//", 10)': FormulaError.NUM,
'OCT2HEX(100, 4.9)': '0040',
'OCT2HEX(100, 4.1)': '0040',
'OCT2HEX(100, 0)': '40',
'OCT2HEX(7777777777, 10)': 'FFFFFFFFFF',
'OCT2HEX(3777777777, 10)': '001FFFFFFF',
'OCT2HEX(4000000000, 10)': 'FFE0000000',
'OCT2HEX(4000000001, 10)': 'FFE0000001',
'OCT2HEX(4000000001, 11)': FormulaError.NUM
});
});

View File

@ -0,0 +1,206 @@
import FormulaError from '../../FormulaError';
import {TestCase, buildEnv, testEvalCases} from './buildEnv';
const data = [[]];
const env = buildEnv(data);
function runTest(testCase: TestCase) {
testEvalCases(testCase, env);
}
test('COUPDAYS', () => {
// 还有很多用例没从 formulajs 里拷出来
runTest({
'COUPDAYS("01/25/2011", "11/15/2011", 2, 1)': 181
});
});
test('CUMIPMT', () => {
runTest({
'CUMIPMT(0.1 / 12, 30 * 12, 100000, 13, 24, 0)': -9916.77251395708
});
});
test('CUMPRINC', () => {
runTest({
'CUMPRINC(0.1 / 12, 30 * 12, 100000, 13, 24, 0)': -614.0863271085149
});
});
test('DB', () => {
runTest({
'DB(1000000, 100000, 6, 1)': 319000
});
});
test('DDB', () => {
runTest({
'DDB(1000000, 100000, 6, 1)': 333333.3333333333
});
});
test('DISC', () => {
runTest({
'DISC("01/04/2023", "01/31/2023", 99.71275, 100, 0)': 0.0383
});
});
test('DOLLARDE', () => {
runTest({
'DOLLARDE(1.1, 4)': 1.25,
'DOLLARDE(1.1, 1.5)': 1.1
});
});
test('DOLLARFR', () => {
runTest({
'DOLLARFR(1.1, 1)': 1.1,
'DOLLARFR(1.25, 4)': 1.1
});
});
test('EFFECT', () => {
runTest({
'EFFECT(0.1, 4)': 0.10381289062499977
});
});
test('FV', () => {
runTest({
'FV(0.06 / 12, 10, -200, -500, 1)': 2581.4033740601185
});
});
test('FVSCHEDULE', () => {
runTest({
'FVSCHEDULE(100, {0.09, 0.1, 0.11})': 133.08900000000003
});
});
test('IPMT', () => {
runTest({
'IPMT(0.1 / 12, 6, 2 * 12, 100000, 1000000, 0)': 928.8235718400465
});
});
test('IRR', () => {
runTest({
'IRR({-75000, 12000, 15000, 18000, 21000, 24000})': 0.05715142887178467
});
});
test('ISPMT', () => {
runTest({
'ISPMT(0.1 / 12, 6, 2 * 12, 100000)': -625
});
});
test('MIRR', () => {
runTest({
'MIRR({-75000, 12000, 15000, 18000, 21000, 24000}, 0.1, 0.12)': 0.07971710360838036
});
});
test('NOMINAL', () => {
runTest({
'NOMINAL(0.1, 4)': 0.09645475633778045
});
});
test('NPER', () => {
runTest({
'NPER(0, -100, -1000, 10000)': 90
});
});
test('NPV', () => {
runTest({
'NPV(0.1, -10000, 2000, 4000, 8000)': 1031.3503176012546
});
});
test('PDURATION', () => {
runTest({
'PDURATION(0.1, 1000, 2000)': 7.272540897341714
});
});
test('PMT', () => {
runTest({
'PMT(0.06 / 12, 18 * 12, 0, 50000)': -129.0811608679973
});
});
test('PPMT', () => {
runTest({
'PPMT(0.1 / 12, 1, 2 * 12, 2000)': -75.62318600836667
});
});
test('PRICEDISC', () => {
runTest({
'PRICEDISC("01/05/2023", "01/31/2023", 0.038, 100, 0)': 99.72555556
});
});
test('PV', () => {
runTest({
'PV(0.1 / 12, 2 * 12, 1000, 10000, 0)': -29864.950264779152
});
});
test('RATE', () => {
runTest({
'RATE(2 * 12, -1000, -10000, 100000)': 0.06517891177181546
});
});
test('RRI', () => {
runTest({
'RRI(8, 10000, 11000)': 0.011985024140399592
});
});
test('SLN', () => {
runTest({
'SLN(30000, 7500, 10)': 2250
});
});
test('SYD', () => {
runTest({
'SYD(30, 7, 10, 1)': 4.181818181818182
});
});
test('TBILLEQ', () => {
runTest({
'TBILLEQ("03/31/2008", "06/01/2008", 0.0914)': 0.09412721351734614
});
});
test('TBILLPRICE', () => {
runTest({
'TBILLPRICE("03/31/2008", "06/01/2008", 0.0914)': 98.45127777777778
});
});
test('TBILLYIELD', () => {
runTest({
'TBILLYIELD("03/31/2008", "06/01/2008", 98.45127777777778)': 0.09283779963354702
});
});
test('XIRR', () => {
runTest({
'XIRR({-10000, 2750, 4250, 3250, 2750}, {"01/01/2008", "03/01/2008", "10/30/2008", "02/15/2009", "04/01/2009"}, 0.1)': 0.373362535
});
});
test('XNPV', () => {
// 这个结果有偏差,不知道为啥
runTest({
'XNPV(0.09, {-10000, 2750, 4250, 3250, 2750}, {"01/01/2008", "03/01/2008", "10/30/2008", "02/15/2009", "04/01/2009"})': 2086.647602031535
});
});

View File

@ -0,0 +1,135 @@
import FormulaError from '../../FormulaError';
import {TestCase, buildEnv, testEvalCases} from './buildEnv';
const data = [
['', 1, 2, 3, 4],
['string', 3, 4, 5, 6],
[undefined, undefined]
];
const env = buildEnv(data);
function runTest(testCase: TestCase) {
testEvalCases(testCase, env);
}
test('ERROR.TYPE', () => {
runTest({
'ERROR.TYPE(#NULL!)': 1,
'ERROR.TYPE(#DIV/0!)': 2,
'ERROR.TYPE(#N/A)': 7,
'ERROR.TYPE(#VALUE!)': 3,
'ERROR.TYPE(#REF!)': 4,
'ERROR.TYPE(#NUM!)': 6,
'ERROR.TYPE(#NAME?)': 5
});
});
test('ISBLANK', () => {
runTest({
// Excel 里还真是这样的,但和现有实现冲突了,没法区分,所以先注释
// 'ISBLANK("")': false,
'ISBLANK(A1)': true,
'ISBLANK(A2)': false,
'ISBLANK(A3)': true,
'ISBLANK(B3)': true,
'ISBLANK({1})': false
});
});
test('ISERR', () => {
runTest({'ISERR(1/0)': true, 'ISERR(#DIV/0!)': true, 'ISERR(#N/A)': false});
});
test('ISERROR', () => {
runTest({
'ISERROR(1/0)': true,
'ISERROR(#DIV/0!)': true,
'ISERROR(#N/A)': true,
'ISERROR(#VALUE!)': true,
'ISERROR(#REF!)': true,
'ISERROR(#NUM!)': true,
'ISERROR(#NAME?)': true
});
});
test('ISEVEN', () => {
runTest({
'ISEVEN(2)': true,
'ISEVEN(-2)': true,
'ISEVEN(2.5)': true,
'ISEVEN(3)': false
});
});
test('ISODD', () => {
runTest({
'ISODD(3)': true,
'ISODD(-3)': true,
'ISODD(2.5)': false,
'ISODD(2)': false
});
});
test('ISLOGICAL', () => {
runTest({
'ISLOGICAL(TRUE)': true,
'ISLOGICAL(FALSE)': true,
'ISLOGICAL("TRUE")': false
});
});
test('ISNA', () => {
runTest({'ISNA(#N/A)': true, 'ISNA(#NAME?)': false});
});
test('ISNONTEXT', () => {
runTest({'ISNONTEXT(123)': true, 'ISNONTEXT("123")': false});
});
test('ISNUMBER', () => {
runTest({'ISNUMBER(123)': true, 'ISNUMBER(A1)': false, 'ISNUMBER(B1)': true});
});
test('ISREF', () => {
runTest({
'ISREF(B2)': true,
'ISREF(123)': false,
'ISREF("A1")': false,
'ISREF(#REF!)': false
// 'ISREF(XYZ1)': false,
// 'ISREF(A1:XYZ1)': false,
// 'ISREF(XYZ1:A1)': false
});
});
test('ISTEXT', () => {
runTest({'ISTEXT(123)': false, 'ISTEXT("123")': true});
});
test('N', () => {
runTest({
'N(1)': 1,
'N(TRUE)': 1,
'N(FALSE)': 0,
'N(1/0)': FormulaError.VALUE,
'N("123")': 0
});
});
test('NA', () => {
runTest({'NA()': FormulaError.NA});
});
test('TYPE', () => {
runTest({
// 原本用例这里是 1不知道对不对
'TYPE(A1)': 2,
'TYPE(12)': 1,
'TYPE("12")': 2,
'TYPE("")': 2,
'TYPE(TRUE)': 4,
'TYPE(1/0)': 16,
'TYPE({1;2;3})': 64
});
});

View File

@ -0,0 +1,121 @@
import FormulaError from '../../FormulaError';
import {TestCase, buildEnv, testEvalCases} from './buildEnv';
const data = [
['fruit', 'price', 'count', 4, 5],
['Apples', 0.69, 40, 5, 6],
['Bananas', 0.34, 38, 5, 6],
['Lemons', 0.55, 15, 5, 6],
['Oranges', 0.25, 25, 5, 6],
['Pears', 0.59, 40, 5, 6],
['Almonds', 2.8, 10, 5, 6], // row 7
['Cashews', 3.55, 16, 5, 6], // row 8
['Peanuts', 1.25, 20, 5, 6], // row 9
['Walnuts', 1.75, 12, 5, 6], // row 10
['Apples', 'Lemons', 0, 0, 0], // row 11
['Bananas', 'Pears', 0, 0, 0] // row 12
];
const env = buildEnv(data);
function runTest(testCase: TestCase) {
testEvalCases(testCase, env);
}
test('AND', () => {
runTest({
'AND(A1)': FormulaError.VALUE,
'AND(1,1,1)': true,
'AND(1,0,0)': false,
'AND(A2:C2)': true,
'AND("Test", "TRUE")': true,
'AND("Test", "FALSE")': false,
'AND({0,1,0}, FALSE)': false,
'AND((A2:C2, A3))': true,
'AND((A2:C2 C2))': true
});
});
test('IF', () => {
runTest({
'IF(TRUE, A1, A2)': 'fruit',
'IF(TRUE, A1&1, A2)': 'fruit1',
'IF(A1 = "fruit", A1, A2)': 'fruit',
'IF(IF(D1 < D5, A2) = "count", A1, A2)': 'Apples'
});
});
test('IFS', () => {
runTest({
'IFS(1=3,"Not me", 1=2, "Me neither", 1=1, "Yes me")': 'Yes me',
'IFS(D5<60,"F",D5<70,"D",D5<80,"C",D5<90,"B",D5>=90,"A")': 'F',
'IFS(1=3,"Not me", 1=2, "Me neither", 1=4, "Not me")': FormulaError.NA,
'IFS("HELLO","Not me", 1=2, "Me neither", 1=4, "Not me")':
FormulaError.VALUE,
'IFS("HELLO")': FormulaError.NA
});
});
test('IFNA', () => {
runTest({
'IFNA(#N/A, 1, 2)': FormulaError.TOO_MANY_ARGS('IFNA'),
'IFNA(#N/A, 1)': 1,
'IFNA("Good", 1)': 'Good'
});
});
test('OR', () => {
runTest({
'OR(A1)': FormulaError.VALUE,
'OR(1,1,0)': true,
'OR(0,0,0)': false,
'OR(A2:C2)': true,
'OR("Test", "TRUE")': true,
'OR("Test", "FALSE")': false,
'OR({0,1,0}, FALSE)': true,
'OR((A2:C2, A3))': true,
'OR((A2:C2 C2))': true
});
});
test('TRUE', () => {
runTest({
TRUE: true
});
});
test('TEXTJOIN', () => {
runTest({
'TEXTJOIN(" ", TRUE, "The", "", "sun", "will", "come", "up", "tomorrow.")':
'The sun will come up tomorrow.',
'TEXTJOIN({"_", ">"}, TRUE, "The", "sun", "will", "come", "up", "tomorrow.")':
'The_sun>will_come>up_tomorrow.'
});
});
test('FALSE', () => {
runTest({
FALSE: false
});
});
test('SWITCH', () => {
runTest({
'SWITCH(7, 9, "a", 7, "b")': 'b'
});
});
test('XOR', () => {
runTest({
'XOR(A1)': FormulaError.VALUE,
'XOR(1,1,0)': false,
'XOR(1,1,1)': true,
'XOR(A2:C2)': false,
'XOR(A2:C2, "TRUE")': true,
'XOR("Test", "TRUE")': true,
'XOR({1,1,1}, FALSE)': true,
'XOR((A2:C2, A3))': false,
'XOR((A2:C2 C2))': true
});
});

View File

@ -0,0 +1,607 @@
/**
* fast-formula-parser
*/
import FormulaError from '../../FormulaError';
import {TestCase, buildEnv, testEvalCases} from './buildEnv';
const data = [
[1, 2, 3, 4, 5],
[100000, 7000, 250000, 5, 6],
[200000, 14000, 4, 5, 6],
[300000, 21000, 4, 5, 6],
[400000, 28000, 4, 5, 6],
['string', 3, 4, 5, 6],
// for SUMIF ex2
['Vegetables', 'Tomatoes', 2300, 5, 6], // row 7
['Vegetables', 'Celery', 5500, 5, 6], // row 8
['Fruits', 'Oranges', 800, 5, 6], // row 9
['', 'Butter', 400, 5, 6], // row 10
['Vegetables', 'Carrots', 4200, 5, 6], // row 11
['Fruits', 'Apples', 1200, 5, 6], // row 12
['1'],
[2, 3, 9, 1, 8, 7, 5],
[6, 5, 11, 7, 5, 4, 4]
];
const env = buildEnv(data);
function runTest(testCase: TestCase) {
testEvalCases(testCase, env);
}
test('ABS', () => {
runTest({
'ABS(-1)': 1,
'ABS(1)': 1,
'ABS(0)': 0,
'ABS("a")': FormulaError.VALUE
});
});
test('ARABIC', () => {
runTest({
'ARABIC("XIV")': 14,
'ARABIC("LVII")': 57,
'ARABIC("")': 0,
'ARABIC("LVIIA")': FormulaError.VALUE
});
});
test('BASE', () => {
runTest({
'BASE(7,2)': '111',
'BASE(100,16)': '64',
'BASE(15,2,10)': '0000001111',
'BASE(2^53-1,36)': '2GOSA7PA2GV',
'BASE(-1,2)': FormulaError.NUM,
'BASE(2^53-1,2)': '11111111111111111111111111111111111111111111111111111',
'BASE(2^53,2)': FormulaError.NUM,
'BASE(7,1)': FormulaError.NUM,
'BASE(7,37)': FormulaError.NUM,
'BASE(7,2,-1)': FormulaError.NUM,
'BASE(7,2,0)': '111',
'BASE(7,2,2)': '111',
'BASE(7,2,5)': '00111'
});
});
test('CEILING', () => {
runTest({
'CEILING(2.5, 1)': 3,
'CEILING(-2.5, -2)': -4,
'CEILING(-2.5, 2)': -2,
'CEILING(1.5, 0.1)': 1.5,
'CEILING(0.234, 0.01)': 0.24,
'CEILING(1.5, 0)': 0,
'CEILING(2^1024, 1)': FormulaError.NUM
});
});
test('CEILING.MATH', () => {
runTest({
'CEILING.MATH(24.3,5)': 25,
'CEILING.MATH(6.7)': 7,
'CEILING.MATH(-6.7)': -7,
'CEILING.MATH(-8.1,2)': -8,
'CEILING.MATH(-5.5,2,-1)': -6
});
});
test('CEILING.PRECISE', () => {
runTest({
'CEILING.PRECISE(4.3)': 5,
'CEILING.PRECISE(-4.3)': -4,
'CEILING.PRECISE(4.3, 2)': 6,
'CEILING.PRECISE(4.3,-2)': 6,
'CEILING.PRECISE(-4.3,2)': -4,
'CEILING.PRECISE(-4.3,-2)': -4
});
});
test('COMBIN', () => {
runTest({
'COMBIN(8,2)': 28,
'COMBIN(-1,2)': FormulaError.NUM,
'COMBIN(1,2)': FormulaError.NUM,
'COMBIN(1,-2)': FormulaError.NUM
});
});
test('COMBINA', () => {
runTest({
'COMBINA(4,3)': 20,
'COMBINA(0,0)': 1,
'COMBINA(1,0)': 1,
'COMBINA(-1,2)': FormulaError.NUM,
'COMBINA(1,2)': 1,
'COMBINA(1,-2)': FormulaError.NUM
});
});
test('DECIMAL', () => {
runTest({
'DECIMAL("FF",16)': 255,
'DECIMAL("8000000000",16)': 549755813888,
'DECIMAL(111,2)': 7,
'DECIMAL("zap",36)': 45745,
'DECIMAL("zap",2)': FormulaError.NUM,
'DECIMAL("zap",37)': FormulaError.NUM,
'DECIMAL("zap",1)': FormulaError.NUM
});
});
test('DEGREES', () => {
runTest({
'DEGREES(PI())': 180,
'DEGREES(PI()/2)': 90,
'DEGREES(PI()/4)': 45
});
});
test('EVEN', () => {
runTest({
'EVEN(1.5)': 2,
'EVEN(3)': 4,
'EVEN(2)': 2,
'EVEN(-1)': -2
});
});
test('EXP', () => {
runTest({
'EXP(1)': 2.71828183
});
});
test('FACT', () => {
runTest({
'FACT(5)': 120,
'FACT(150)': 5.7133839564458575e262, // more accurate than excel...
'FACT(150) + 1': 5.7133839564458575e262 + 1, // memorization
'FACT(1.9)': 1,
'FACT(0)': 1,
'FACT(-1)': FormulaError.NUM,
'FACT(1)': 1
});
});
test('FACTDOUBLE', () => {
runTest({
'FACTDOUBLE(6)': 48,
'FACTDOUBLE(6) + 1': 49, // memorization
'FACTDOUBLE(7)': 105,
'FACTDOUBLE(0)': 1,
'FACTDOUBLE(-1)': 1,
'FACTDOUBLE(-2)': FormulaError.NUM,
'FACTDOUBLE(1)': 1
});
});
test('FLOOR', () => {
runTest({
'FLOOR(0,)': 0,
'FLOOR(12,0)': 0,
'FLOOR(3.7,2)': 2,
'FLOOR(-2.5,-2)': -2,
'FLOOR(-2.5,2)': -4,
'FLOOR(2.5,-2)': FormulaError.NUM,
'FLOOR(1.58,0.1)': 1.5,
'FLOOR(0.234,0.01)': 0.23,
'FLOOR(-8.1,2)': -10
});
});
test('FLOOR.MATH', () => {
runTest({
'FLOOR.MATH(0)': 0,
'FLOOR.MATH(12, 0)': 0,
'FLOOR.MATH(24.3,5)': 20,
'FLOOR.MATH(6.7)': 6,
'FLOOR.MATH(-8.1,2)': -10,
'FLOOR.MATH(-5.5,2,-1)': -4,
'FLOOR.MATH(-5.5,2,1)': -4,
'FLOOR.MATH(-5.5,2,)': -6,
'FLOOR.MATH(-5.5,2)': -6,
'FLOOR.MATH(-5.5,-2)': -6,
'FLOOR.MATH(5.5,2)': 4,
'FLOOR.MATH(5.5,-2)': 4,
'FLOOR.MATH(24.3,-5)': 20,
'FLOOR.MATH(-8.1,-2)': -10
});
});
test('FLOOR.PRECISE', () => {
runTest({
'FLOOR.PRECISE(-3.2,-1)': -4,
'FLOOR.PRECISE(3.2, 1)': 3,
'FLOOR.PRECISE(-3.2, 1)': -4,
'FLOOR.PRECISE(3.2,-1)': 3,
'FLOOR.PRECISE(3.2)': 3,
'FLOOR.PRECISE(0)': 0,
'FLOOR.PRECISE(3.2, 0)': 0
});
});
test('GCD', () => {
runTest({
'GCD(5, 2)': 1,
'GCD(24, 36)': 12,
'GCD(7, 1)': 1,
'GCD(5, 0)': 5,
'GCD(123, 0)': 123,
'GCD(128, 80, 44)': 4,
'GCD(128, 80, 44,)': 4,
'GCD(128, 80, 44, 2 ^ 53)': FormulaError.NUM, // excel parse this as #NUM!
'GCD("a")': FormulaError.VALUE,
'GCD(5, 2, (A1))': 1,
'GCD(5, 2, A1:E1)': 1,
'GCD(5, 2, (A1:E1))': 1,
// TODO: 目前实现上当成数组所以其实是支持的
// 'GCD(5, 2, (A1, A2))': FormulaError.VALUE, // does not support union
'GCD(5, 2, {3, 7})': 1,
'GCD(5, 2, {3, "7"})': 1,
'GCD(5, 2, {3, "a"})': FormulaError.VALUE,
'GCD(5, 2, {3, "7"}, TRUE)': FormulaError.VALUE
});
});
test('INT', () => {
runTest({'INT(0)': 0, 'INT(8.9)': 8, 'INT(-8.9)': -9});
});
test('ISO.CEILING', () => {
runTest({
'ISO.CEILING(4.3)': 5,
'ISO.CEILING(-4.3)': -4,
'ISO.CEILING(4.3, 2)': 6,
'ISO.CEILING(4.3,-2)': 6,
'ISO.CEILING(-4.3,2)': -4,
'ISO.CEILING(-4.3,-2)': -4
});
});
test('LCM', () => {
runTest({
'LCM("a")': FormulaError.VALUE,
'LCM(5, 2)': 10,
'LCM(24, 36)': 72,
'LCM(50,56,100)': 1400,
'LCM(50,56,100,)': 1400,
'LCM(128, 80, 44, 2 ^ 53)': FormulaError.NUM, // excel parse this as #NUM!
'LCM(5, 2, (A1))': 10,
'LCM(5, 2, A1:E1)': 60,
'LCM(5, 2, (A1:E1))': 60,
// 'LCM(5, 2, (A1, A2))': FormulaError.VALUE, // does not support union
'LCM(5, 2, {3, 7})': 210,
'LCM(5, 2, {3, "7"})': 210,
'LCM(5, 2, {3, "a"})': FormulaError.VALUE,
'LCM(5, 2, {3, "7"}, TRUE)': FormulaError.VALUE
});
});
test('LN', () => {
runTest({'LN(86)': 4.454347296253507, 'LN(EXP(1))': 1, 'LN(EXP(3))': 3});
});
test('LOG', () => {
runTest({'LOG(10)': 1, 'LOG(8, 2)': 3, 'LOG(86, EXP(1))': 4.454347296253507});
});
test('LOG10', () => {
runTest({
'LOG10(86)': 1.9344984512435677,
'LOG10(10)': 1,
'LOG10(100000)': 5,
'LOG10(10^5)': 5
});
});
test('MDETERM', () => {
runTest({
'MDETERM({3,6,1;1,1,0;3,10,2})': 1,
'MDETERM({3,6;1,1})': -3,
'MDETERM({6})': 6,
'MDETERM({1,3,8,5;1,3,6,1})': FormulaError.VALUE
});
});
test('MMULT', () => {
runTest({
'MMULT({1,3;7,2}, {2,0;0,2})': [
[2, 6],
[14, 4]
],
'MMULT({1,3;7,2;1,1}, {2,0;0,2})': [
[2, 6],
[14, 4],
[2, 2]
],
'MMULT({1,3;"r",2}, {2,0;0,2})': FormulaError.VALUE,
'MMULT({1,3;7,2}, {2,0;"b",2})': FormulaError.VALUE
});
});
test('MOD', () => {
runTest({
'MOD(3, 2)': 1,
'MOD(-3, 2)': 1,
'MOD(3, -2)': -1,
'MOD(-3, -2)': -1,
'MOD(-3, 0)': FormulaError.DIV0
});
});
test('MROUND', () => {
runTest({
'MROUND(10, 1)': 10,
'MROUND(10, 3)': 9,
'MROUND(10, 0)': 0,
'MROUND(-10, -3)': -9,
'MROUND(1.3, 0.2)': 1.4,
'MROUND(5, -2)': FormulaError.NUM,
'MROUND(6.05,0.1)': 6.0, // same as excel, differ from google sheets
'MROUND(7.05,0.1)': 7.1
});
});
test('MULTINOMIAL', () => {
runTest({
'MULTINOMIAL({1,2}, E1, A1:D1)': 92626934400,
'MULTINOMIAL(2, 3, 4)': 1260,
'MULTINOMIAL(2, 3, -4)': FormulaError.NUM
});
});
test('MUNIT', () => {
runTest({
'MUNIT(3)': [
[1, 0, 0],
[0, 1, 0],
[0, 0, 1]
],
'MUNIT(2)': [
[1, 0],
[0, 1]
],
'MUNIT(1)': [[1]]
});
});
test('ODD', () => {
runTest({
'ODD(0)': 1,
'ODD(1.5)': 3,
'ODD(3)': 3,
'ODD(2)': 3,
'ODD(-1)': -1,
'ODD(-2)': -3
});
});
test('PI', () => {
runTest({'PI()': Math.PI});
});
test('POWER', () => {
runTest({
'POWER(5,2)': 25,
'POWER(98.6,3.2)': 2401077.22206958,
'POWER(4,5/4)': 5.656854249
});
});
test('PRODUCT', () => {
runTest({
'PRODUCT(1,2,3,4,5)': 120,
'PRODUCT(1,2,3,4,5, "2")': 240,
'PRODUCT(A1:E1)': 120,
'PRODUCT((A1, B1:E1))': 120,
'PRODUCT(1,2,3,4,5, A1, {1,2})': 240
});
});
test('QUOTIENT', () => {
runTest({
'QUOTIENT(5, 2)': 2,
'QUOTIENT(4.5, 3.1)': 1,
'QUOTIENT(-10, 3)': -3,
'QUOTIENT(-10, -3)': 3
});
});
test('RADIANS', () => {
runTest({'RADIANS(270)': 4.71238898, 'RADIANS(0)': 0});
});
test('RAND', () => {
runTest({'RAND() > 0': true});
});
test('RANK', () => {
runTest({
'RANK(7, {7,3.5,3.5,1,2}, 1)': 5,
'RANK(3.5, {7,3.5,3.5,1,2}, 1)': 3
});
runTest({'RANK(3.5, {7,3.5,3.5,1,2}, 1)': 3});
});
test('RANK.EQ', () => {
runTest({
'RANK.EQ(7, {7,3.5,3.5,1,2}, 1)': 5,
'RANK.EQ(3.5, {7,3.5,3.5,1,2}, 1)': 3,
'RANK.EQ(2, {7,3.5,3.5,1,2})': 4
});
});
test('RANK.AVG', () => {
runTest({
'RANK.AVG(94,{89,88,92,101,94,97,95})': 4
});
});
test('RANDBETWEEN', () => {
runTest({'RANDBETWEEN(-1,1) >= -1': true});
});
test('ROMAN', () => {
runTest({'ROMAN(499,0)': 'CDXCIX'});
});
test('ROUND', () => {
runTest({
'ROUND(2.15, 0)': 2,
'ROUND(2.15, 1)': 2.2,
'ROUND(2.149, 1)': 2.1,
'ROUND(-1.475, 2)': -1.48,
'ROUND(21.5, -1)': 20,
'ROUND(626.3,-3)': 1000,
'ROUND(1.98, -1)': 0,
'ROUND(-50.55,-2)': -100
});
});
test('ROUNDDOWN', () => {
runTest({
'ROUNDDOWN(3.2, 0)': 3,
'ROUNDDOWN(76.9,0)': 76,
'ROUNDDOWN(3.14159, 3)': 3.141,
'ROUNDDOWN(-3.14159, 1)': -3.1,
'ROUNDDOWN(31415.92654, -2)': 31400
});
});
test('ROUNDUP', () => {
runTest({
'ROUNDUP(3.2,0)': 4,
'ROUNDUP(76.9,0)': 77,
'ROUNDUP(3.14159, 3)': 3.142,
'ROUNDUP(-3.14159, 1)': -3.2,
'ROUNDUP(31415.92654, -2)': 31500
});
});
test('SERIESSUM', () => {
runTest({
'SERIESSUM(PI()/4,0,2,{1, -0.5, 0.041666667, -0.001388889})': 0.707103215
});
});
test('SIGN', () => {
runTest({
'SIGN(10)': 1,
'SIGN(4-4)': 0,
'SIGN(-0.00001)': -1
});
});
test('SQRT', () => {
runTest({
'SQRT(16)': 4,
'SQRT(-16)': FormulaError.NUM,
'SQRT(ABS(-16))': 4
});
});
test('SQRTPI', () => {
runTest({
'SQRTPI(1)': 1.772453851,
'SQRTPI(2)': 2.506628275,
'SQRTPI(-1)': FormulaError.NUM
});
});
test('SUM', () => {
runTest({
'SUM(1,2,3)': 6,
'SUM(A1:C1, C1:E1)': 18,
'SUM((A1:C1, C1:E1))': 18,
'SUM((A1:C1, C1:E1), A1)': 19,
// TODO: 为什么不一致
// 'SUM((A1:C1, C1:E1), A13)': 18,
'SUM("1", {1})': 2,
'SUM("1", {"1"})': 2,
'SUM("1", {"1"},)': 2,
'SUM("1", {"1"},TRUE)': 2
});
});
// TODO: 补齐其他测试
test('SUBTOTAL', () => {
runTest({
'SUBTOTAL(9, A1:C1, C1:E1)': 18
});
});
test('SUMIF', () => {
runTest({
'SUMIF(A1:E1, ">1")': 14,
'SUMIF(A2:A5,">160000",B2:B5)': 63000,
'SUMIF(A2:A5,">160000")': 900000,
'SUMIF(A2:A5,300000,B2:B5)': 21000,
'SUMIF(A2:A5,">" & C2,B2:B5)': 49000,
'SUMIF(A7:A12,"Fruits",C7:C12)': 2000,
'SUMIF(A7:A12,"Vegetables",C7:C12)': 12000,
'SUMIF(B7:B12,"*es",C7:C12)': 4300,
'SUMIF(A7:A12,"",C7:C12)': 400
});
});
test('SUMIFS', () => {
runTest({
'SUMIFS({1, 2, 3}, {4, 5, 6}, ">4", {7, 8, 9}, "<9")': 2,
'SUMIFS({1, 2, 3}, {4, 5, 6}, ">4", {7, 8, 9}, "*")': 5
});
});
test('SUMPRODUCT', () => {
runTest({
'SUMPRODUCT({1,"12";7,2}, {2,1;5,2})': 53,
'SUMPRODUCT({1,12;7,2}, {2,1;5,2})': 53,
'SUMPRODUCT({1,12;7,2}, {2,1;5,"2"})': 53,
'SUMPRODUCT({1,12;7,2}, {2,1;5,2;1,1})': FormulaError.VALUE
});
});
test('SUMSQ', () => {
runTest({
'SUMSQ(3, 4)': 25,
'SUMSQ(3, 4, A1)': 26
// 'SUMSQ(3, 4, A1, A13)': 26
});
});
test('SUMX2MY2', () => {
runTest({
'SUMX2MY2(A14:G14,A15:G15)': -55,
'SUMX2MY2({2, 3, 9, 1, 8, 7, 5}, {6, 5, 11, 7, 5, 4, 4})': -55,
'SUMX2MY2(A14:G13,A15:G15)': FormulaError.NA
});
});
test('SUMX2PY2', () => {
runTest({
'SUMX2PY2(A14:G14,A15:G15)': 521,
'SUMX2PY2({2,3,9,1,8,7,5}, {6,5,11,7,5,4,4})': 521,
'SUMX2PY2(A14:G13,A15:G15)': FormulaError.NA
});
});
test('SUMXMY2', () => {
runTest({
'SUMXMY2(A14:G14,A15:G15)': 79,
'SUMXMY2({2,3,9,1,8,7,5}, {6,5,11,7,5,4,4})': 79,
'SUMXMY2(A14:G13,A15:G15)': FormulaError.NA
});
});
test('TRUNC', () => {
runTest({
'TRUNC(8.9)': 8,
'TRUNC(-8.9)': -8,
'TRUNC(0.45)': 0
});
});

View File

@ -0,0 +1,222 @@
import FormulaError from '../../FormulaError';
import {TestCase, buildEnv, testEvalCases} from './buildEnv';
const data = [
['fruit', 'price', 'count', 4, 5],
['Apples', 0.69, 40, 5, 6],
['Bananas', 0.34, 38, 5, 6],
['Lemons', 0.55, 15, 5, 6],
['Oranges', 0.25, 25, 5, 6],
['Pears', 0.59, 40, 5, 6],
['Almonds', 2.8, 10, 5, 6], // row 7
['Cashews', 3.55, 16, 5, 6], // row 8
['Peanuts', 1.25, 20, 5, 6], // row 9
['Walnuts', 1.75, 12, 5, 6], // row 10
['Apples', 'Lemons', 0, 0, 0], // row 11
['Bananas', 'Pears', 0, 0, 0] // row 12
];
const env = buildEnv(data);
function runTest(testCase: TestCase) {
testEvalCases(testCase, env);
}
test('RANGE', () => {
runTest({
'A1': 'fruit',
'A1:B1': [['fruit', 'price']],
'A1:A2': [['fruit'], ['Apples']],
'A1:B2': [
['fruit', 'price'],
['Apples', 0.69]
]
});
});
test('ADDRESS', () => {
runTest({
'ADDRESS(2,3,4, 2, "abc")': 'abc!C2',
'ADDRESS(2,3)': '$C$2',
'ADDRESS(2,3, 1)': '$C$2',
'ADDRESS(2,3,2)': 'C$2',
'ADDRESS(2,3,3)': '$C2',
'ADDRESS(2,3,4, TRUE)': 'C2',
'ADDRESS(2,3,2,FALSE)': 'R2C[3]',
'ADDRESS(2,3,1,FALSE,"[Book1]Sheet1")': "'[Book1]Sheet1'!R2C3",
'ADDRESS(2,3,1,FALSE,"EXCEL SHEET")': "'EXCEL SHEET'!R2C3"
});
});
test('AREAS', () => {
runTest({
'AREAS((B2:D4,E5,F6:I9))': 3,
'AREAS(B2:D4)': 1,
'AREAS(B2:D4 B2)': 1
});
});
test('COLUMN', () => {
runTest({
'COLUMN()': 2,
'COLUMN(C3)': 3,
'COLUMN(C3:V6)': 3,
'COLUMN(123)': FormulaError.VALUE,
'COLUMN({1,2,3})': FormulaError.VALUE,
'COLUMN("A1")': FormulaError.VALUE
});
});
test('COLUMNS', () => {
runTest({
'COLUMNS(A1)': 1,
'COLUMNS(A1:C5)': 3,
'COLUMNS(123)': FormulaError.VALUE,
'COLUMNS({1,2,3})': FormulaError.VALUE,
'COLUMNS("A1")': FormulaError.VALUE
});
});
test('HLOOKUP', () => {
runTest({
'HLOOKUP(3, {1,2,3,4,5}, 1)': 3,
'HLOOKUP(3, {3,2,1}, 1)': 1,
'HLOOKUP(3, {1,2,3,4,5}, 2)': FormulaError.REF,
'HLOOKUP("a", {1,2,3,4,5}, 1)': FormulaError.NA,
'HLOOKUP(3, {1.1,2.2,3.3,4.4,5.5}, 1)': 2.2,
// should handle like Excel.
'HLOOKUP(63, {"c",FALSE,"abc",65,63,61,"b","a",FALSE,TRUE}, 1)': 63,
'HLOOKUP(TRUE, {"c",FALSE,"abc",65,63,61,"b","a",FALSE,TRUE}, 1)': true,
'HLOOKUP(FALSE, {"c",FALSE,"abc",65,63,61,"b","a",FALSE,TRUE}, 1)': false,
'HLOOKUP(FALSE, {"c",TRUE,"abc",65,63,61,"b","a",TRUE,FALSE}, 1)':
FormulaError.NA,
'HLOOKUP("c", {"c",TRUE,"abc",65,63,61,"b","a",TRUE,FALSE}, 1)': 'a',
'HLOOKUP("b", {"c",TRUE,"abc",65,63,61,"b","a",TRUE,FALSE}, 1)': 'b',
'HLOOKUP("abc", {"c",TRUE,"abc",65,63,61,"b","a",TRUE,FALSE}, 1)': 'abc',
'HLOOKUP("a", {"c",TRUE,"abc",65,63,61,"b","a",TRUE,FALSE}, 1)':
FormulaError.NA,
'HLOOKUP("a*", {"c",TRUE,"abc",65,63,61,"b","a",TRUE,FALSE}, 1)':
FormulaError.NA,
// with rangeLookup = FALSE
'HLOOKUP(3, 3, 1,FALSE)': FormulaError.NA,
'HLOOKUP(3, {1,2,3}, 1,FALSE)': 3,
'HLOOKUP("a", {1,2,3,"a","b"}, 1,FALSE)': 'a',
'HLOOKUP(3, {1,2,3;"a","b","c"}, 2,FALSE)': 'c',
'HLOOKUP(6, {1,2,3;"a","b","c"}, 2,FALSE)': FormulaError.NA,
// wildcard support
'HLOOKUP("s?", {"abc", "sd", "qwe"}, 1,FALSE)': 'sd',
'HLOOKUP("*e", {"abc", "sd", "qwe"}, 1,FALSE)': 'qwe',
'HLOOKUP("*e?2?", {"abc", "sd", "qwe123"}, 1,FALSE)': 'qwe123',
// case insensitive
'HLOOKUP("a*", {"c",TRUE,"AbC",65,63,61,"b","a",TRUE,FALSE}, 1, FALSE)':
'AbC',
// single row table
'HLOOKUP(614, { 614;"Foobar"}, 2)': 'Foobar'
});
});
test('MATCH', () => {
runTest({'MATCH(41,{25,38,40,41}, 0)': 4, 'MATCH(39,{25,38,40,41}, 1)': 2});
// runTest({'MATCH(40,{25,38,40,41}, -1)': 2});
});
test('ROW', () => {
runTest({
'ROW()': 2,
'ROW(C4)': 4,
'ROW(C4:V6)': 4,
'ROW(123)': FormulaError.VALUE,
'ROW({1,2,3})': FormulaError.VALUE,
'ROW("A1")': FormulaError.VALUE
});
});
test('ROWS', () => {
runTest({
'ROWS(A1)': 1,
'ROWS(A1:C5)': 5,
'ROWS(123)': FormulaError.VALUE,
'ROWS({1,2,3})': FormulaError.VALUE,
'ROWS("A1")': FormulaError.VALUE
});
});
test('TRANSPOSE', () => {
runTest({'SUM(TRANSPOSE({1,2,3;4,5,6}))': 21});
});
test('SORT', () => {
runTest({
'SORT({1,2,3; 4,5,6; 7,8,9}, 1, 1, TRUE)': [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
],
'SORT({3,2,1; 6,5,4; 9,8,7}, 1, 1, TRUE)': [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
],
'SORT({2,3,1; 5,6,4; 8,9,7}, 1, 1, TRUE)': [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
],
'SORT({1,2,3}, 1, 1, FALSE)': [[1, 2, 3]]
});
});
test('LOOKUP', () => {
runTest({
'LOOKUP("Jack", {"Jim", "Jack", "Franck"}, {"blue", "yellow", "red"})':
'yellow',
'LOOKUP("Jack", {"Jim"; "Jack"; "Franck"}, {"blue"; "yellow"})': 'yellow',
'LOOKUP(0.23, {0.1, 0.2, 0.3, 0.4}, {"A", "B", "C", "D"})': 'B'
});
});
test('UNIQUE', () => {
runTest({
'UNIQUE({1, 2, 3, 4, 5, 6, 6, 3})': [1, 2, 3, 4, 5, 6]
});
});
test('VLOOKUP', () => {
runTest({
'VLOOKUP(3, {1;2;3;4;5}, 1)': 3,
'VLOOKUP(3, {3;2;1}, 1)': 1,
'VLOOKUP(3, {1;2;3;4;5}, 2)': FormulaError.REF,
'VLOOKUP("a", {1;2;3;4;5}, 1)': FormulaError.NA,
'VLOOKUP(3, {1.1;2.2;3.3;4.4;5.5}, 1)': 2.2,
// should handle like Excel.
'VLOOKUP(63, {"c";FALSE;"abc";65;63;61;"b";"a";FALSE;TRUE}, 1)': 63,
'VLOOKUP(TRUE, {"c";FALSE;"abc";65;63;61;"b";"a";FALSE;TRUE}, 1)': true,
'VLOOKUP(FALSE, {"c";FALSE;"abc";65;63;61;"b";"a";FALSE;TRUE}, 1)': false,
'VLOOKUP(FALSE, {"c";TRUE;"abc";65;63;61;"b";"a";TRUE;FALSE}, 1)':
FormulaError.NA,
'VLOOKUP("c", {"c";TRUE;"abc";65;63;61;"b";"a";TRUE;FALSE}, 1)': 'a',
'VLOOKUP("b", {"c";TRUE;"abc";65;63;61;"b";"a";TRUE;FALSE}, 1)': 'b',
'VLOOKUP("abc", {"c";TRUE;"abc";65;63;61;"b";"a";TRUE;FALSE}, 1)': 'abc',
'VLOOKUP("a", {"c";TRUE;"abc";65;63;61;"b";"a";TRUE;FALSE}, 1)':
FormulaError.NA,
'VLOOKUP("a*", {"c";TRUE;"abc";65;63;61;"b";"a";TRUE;FALSE}, 1)':
FormulaError.NA,
// with rangeLookup = FALSE
'VLOOKUP(3, 3, 1,FALSE)': FormulaError.NA,
'VLOOKUP(3, {1;2;3}, 1,FALSE)': 3,
'VLOOKUP("a", {1;2;3;"a";"b"}, 1,FALSE)': 'a',
'VLOOKUP(3, {1,"a";2, "b";3, "c"}, 2,FALSE)': 'c',
'VLOOKUP(6, {1,"a";2, "b";3, "c"}, 2,FALSE)': FormulaError.NA,
// wildcard support
'VLOOKUP("s?", {"abc"; "sd"; "qwe"}, 1,FALSE)': 'sd',
'VLOOKUP("*e", {"abc"; "sd"; "qwe"}, 1,FALSE)': 'qwe',
'VLOOKUP("*e?2?", {"abc"; "sd"; "qwe123"}, 1,FALSE)': 'qwe123',
// case insensitive
'VLOOKUP("a*", {"c";TRUE;"AbC";65;63;61;"b";"a";TRUE;FALSE}, 1, FALSE)':
'AbC',
// single row table
'VLOOKUP(614, { 614,"Foobar"}, 2)': 'Foobar'
});
});

View File

@ -0,0 +1,865 @@
import FormulaError from '../../FormulaError';
import {TestCase, buildEnv, testEvalCases} from './buildEnv';
const data = [
['', true, 1, 'TRUE1', true],
['apples', 32, '{1,2}', 5, 6],
['oranges', 54, 4, 5, 6],
['peaches', 75, 4, 5, 6],
['apples', 86, 4, 5, 6],
['string', 3, 4, 5, 6],
[1, 2, 3, 4, 5, 6, 7], // row 7
[100000, 7000], //row 8
[200000, 14000], //row 9
[300000, 21000], //row 10
[400000, 28000], //row 11
['East', 45678], //row 12
['West', 23789], //row 13
['North', -4789], //row 14
['South (New Office)', 0], //row 15
['MidWest', 9678], //row 16
[undefined, true, 1, 2]
];
const env = buildEnv(data);
function runTest(testCase: TestCase) {
testEvalCases(testCase, env);
}
test('AVEDEV', () => {
runTest({
'AVEDEV({4,1,6,7,5,4,3})': 1.469387755,
'AVEDEV({4,1,6,7,5,4,3},(A7,B7),"1")': 1.8,
'AVEDEV({4,1,6,7,5,4,3,TRUE},(A7,B7),"1")': 1.8,
'AVEDEV({4,1,6,7,5,4,3,TRUE},(A7,B7),"1",1)': 1.83471074380165
});
});
test('AVERAGE', () => {
runTest({
'AVERAGE({TRUE,1,2,"12"}, 3)': 2,
// 'AVERAGE((A7, B7), 3)': 2,
'AVERAGE(TRUE, "3")': 2
// 'AVERAGE(TRUE, "a")': FormulaError.VALUE
});
});
test('AVERAGEIF', () => {
runTest({
'AVERAGEIF({2, 4, 8, 16}, ">5")': 12
});
});
test('AVERAGEIFS', () => {
runTest({
'AVERAGEIFS({2, 4, 8, 16}, {1, 2, 3, 4}, ">2")': 12
});
});
test('AVERAGEA', () => {
runTest({
// 'AVERAGEA((A7, B7), 3)': 2,
'AVERAGEA(TRUE, "3")': 2,
'AVERAGEA({1,2,"12"}, 3)': 1.5,
'AVERAGEA({TRUE,1,2,"3",""}, 3, TRUE, "1", 0)': 1
});
});
test('COUNT', () => {
runTest({
'COUNT(A2:A5, 123)': 1,
'COUNT(A2:A5)': 0,
'COUNT(A1:E1)': 1,
'COUNT(A2:E2)': 3,
'COUNT(A2:E2, A1:E1)': 4,
'COUNT((A2:E2, A1:E1))': 4
});
});
test('COUNTA', () => {
runTest({
'COUNTA({39790,19,22.24,TRUE,#DIV/0!})': 5
});
});
test('COUNTIF', () => {
runTest({
'COUNTIF(B1:E1, {1,3,4})': 1,
'COUNTIF(C2:E2, "{1,2}")': 1,
'COUNTIF(C2:E2, "={1,2}")': 1,
'COUNTIF(C2:E2, {1,2})': 0,
'COUNTIF(B1:E1,TRUE)': 2,
'COUNTIF(A2:A5, "apples")': 2,
'COUNTIF(A2:A5,A4)': 1,
'COUNTIF(A2:A5,A2)+COUNTIF(A2:A5,A3)': 3,
'COUNTIF(B2:B5,">55")': 2,
'COUNTIF(B2:B5,"<>"&B4)': 3,
'COUNTIF(B2:B5,">=32")-COUNTIF(B2:B5,">85")': 3,
'COUNTIF(A2:A5,"*")': 4,
'COUNTIF(A2:A5,"?????es")': 2,
'COUNTIF(B1:E1,"=TRUE")': 2,
'COUNTIF(B1:E1,"TRUE")': 2,
'COUNTIF(B1:E1,"TRUE1")': 1,
'COUNTIF(B1:E1,"=TRUE1")': 1
});
});
test('COUNTIFS', () => {
runTest({
'COUNTIFS({1, 3, ""}, ">1")': 1
});
});
test('LARGE', () => {
const testData = '{3,5,3,5,4,4,2,4,6,7}';
runTest({
[`LARGE(${testData}, 3)`]: 5,
[`LARGE(${testData}, 7)`]: 4
});
});
test('SMALL', () => {
// 来自官方的例子
runTest({
'SMALL({3,4,5,2,3,4,6,4,7}, 4)': 4,
'SMALL({1,4,8,3,7,12,54,8,23}, 2)': 3
});
});
test('MAX', () => {
runTest({
'MAX({1,2,3})': 3
});
});
test('MAXA', () => {
runTest({
'MAXA({0.2, TRUE})': 1
});
});
test('MAXIFS', () => {
runTest({
'MAXIFS({2, 4, 8, 16}, {1, 4, 4, 1}, ">2")': 8,
'MAXIFS({2, 4, 8, 16}, {1, 4, 4, 1}, "*")': 16,
'MAXIFS({2, 4, 8, 16}, {1, 4, 4, 1}, ">2", {1, 4, 4, 4}, ">2")': 8
});
});
test('MEDIAN', () => {
runTest({
'MEDIAN({1,2,3,4,5})': 3,
'MEDIAN({1,2,3,4,5,6})': 3.5
});
});
test('MIN', () => {
runTest({
'MIN({1,2,3})': 1
});
});
test('MINIFS', () => {
runTest({
'MINIFS({2, 4, 8, 16}, {1, 4, 4, 1}, ">2")': 4
});
});
test('MINA', () => {
runTest({
'MINA({2, TRUE})': 1
});
});
test('MODE.MULT', () => {
runTest({
'MODE.MULT({1, 2, 3, 4, 3, 2, 1, 2, 3, 5, 6, 1})': [2, 3, 1]
});
});
test('MODE.SNGL', () => {
runTest({
'MODE.SNGL({5.6, 4, 4, 3, 2, 4})': 4
});
});
test('BETA.DIST', () => {
runTest({
'BETA.DIST(2,8,10,TRUE,1,3)': 0.6854705810547,
'BETA.DIST(2,8,10,FALSE,1,3)': 1.4837646484375,
'BETA.DIST(6,8,10,FALSE,1,7)': 0.0008976224783
});
});
test('BETA.INV', () => {
runTest({'BETA.INV(0.6854705810547,8,10,1,3)': 2});
});
test('BINOM.DIST', () => {
runTest({
'BINOM.DIST(6,10,0.5,FALSE)': 0.205078125,
'BINOM.DIST(6,10,0.5,TRUE)': 0.828125
});
});
test('BINOM.DIST.RANGE', () => {
runTest({
'BINOM.DIST.RANGE(60,0.75,48)': 0.083974967429,
'BINOM.DIST.RANGE(60,0.75,45,50)': 0.5236297934719
});
});
test('BINOM.INV', () => {
runTest({'BINOM.INV(6, 0.6, 0.75)': 4});
});
test('CHISQ.DIST', () => {
runTest({
'CHISQ.DIST(0.5,1,TRUE)': 0.520499877813,
'CHISQ.DIST(2,3,FALSE)': 0.2075537487103
});
});
test('CHISQ.DIST.RT', () => {
runTest({
'CHISQ.DIST.RT(18.307,10)': 0.0500005890914
});
});
test('CHISQ.INV', () => {
runTest({
'CHISQ.INV(0.93,1)': 3.2830202867595,
'CHISQ.INV(0.6,2)': 1.8325814637483
});
});
test('CHISQ.INV.RT', () => {
runTest({
'CHISQ.INV.RT(0.050001,10)': 18.3069734569611,
'CHISQ.INV.RT(0.6,2)': 1.021651247532
});
});
test('CHISQ.TEST', () => {
runTest({
'CHISQ.TEST({58,35;11,25;10,23},{43.35,47.65;17.56,18.44;16.09,0})':
FormulaError.DIV0,
'CHISQ.TEST({58,35;11,25;10,23},{43.35,47.65;17.56,18.44;16.09,16.91})': 0.0001513457663,
'CHISQ.TEST({58,35;11,25;10,23},{43.35,47.65;17.56,18.44;16.09,"16.91"})': 0.000453139
});
});
test('CONFIDENCE.NORM', () => {
runTest({
'CONFIDENCE.NORM(0.05,2.5,50)': 0.6929519121748
});
});
test('CONFIDENCE.T', () => {
runTest({
'CONFIDENCE.T(0.05,1,50)': 0.2841968554957
});
});
test('CORREL', () => {
runTest({
'CORREL({3,2,4,5,6},{9,7,12,15,17})': 0.99705448550158
});
});
test('COVARIANCE.P', () => {
runTest({
'COVARIANCE.P({3,2,4,5,6},{9,7,12,15,17})': 5.2
});
});
test('COVARIANCE.S', () => {
runTest({
'COVARIANCE.S(3,9)': FormulaError.DIV0,
'COVARIANCE.S({2,4,8},{5,11,12})': 9.666666666667,
'COVARIANCE.S({3,2,4,5,6},{9,7,12,15,17})': 6.5
});
});
test('DEVSQ', () => {
runTest({
'DEVSQ(1,2,3)': 2,
'DEVSQ(1,"2",{1,2},1)': 1.2
// 不知道啥情况
// 'DEVSQ(1,"2",{1,2,"2"},1,TRUE)': 1.3333333333
});
});
test('EXPON.DIST', () => {
runTest({
'EXPON.DIST(0.2,10,TRUE)': 0.864664716763387,
'EXPON.DIST(0.2,10,FALSE)': 1.35335283236613,
'EXPON.DIST(0.-2,10,FALSE)': FormulaError.NUM,
'EXPON.DIST(0.2,-10,FALSE)': FormulaError.NUM,
'EXPON.DIST(0.2,0.0,FALSE)': FormulaError.NUM
});
});
test('F.DIST', () => {
runTest({
'F.DIST(15,6,4,TRUE)': 0.989741952394019,
'F.DIST(15,6.1,4,TRUE)': 0.989741952394019,
'F.DIST(15,6.9,4.8,TRUE)': 0.989741952394019,
'F.DIST(15,6,4,FALSE)': 0.001271447,
'F.DIST(-0.5,6,4,TRUE)': FormulaError.NUM,
'F.DIST(15.6,0.9,4,TRUE)': FormulaError.NUM,
'F.DIST(15.6,6,0.4,TRUE)': FormulaError.NUM
});
});
test('F.DIST.RT', () => {
runTest({
'F.DIST.RT(15.2068649, 6, 4)': 0.01,
'F.DIST.RT(15.2068649, 6.5, 4)': 0.01,
'F.DIST.RT(15.2068649, 6, 4.4)': 0.01,
'F.DIST.RT(-0.5, 6, 4)': FormulaError.NUM,
'F.DIST.RT(15.6, 0.9, 4)': FormulaError.NUM,
'F.DIST.RT(15.6, 6, 0.4)': FormulaError.NUM
});
});
test('F.INV', () => {
runTest({
'F.INV(0.01, 6, 4)': 0.109309914,
'F.INV(0.01, 6.9, 4.9)': 0.109309914,
'F.INV(-0.01, 6, 4)': FormulaError.NUM,
'F.INV(1.01, 6, 4)': FormulaError.NUM,
'F.INV(1.01, 0.6, 4)': FormulaError.NUM,
'F.INV(0.01, 6, 0.4)': FormulaError.NUM,
'F.INV(1.01, -6, -4)': FormulaError.NUM
});
});
test('F.INV.RT', () => {
runTest({
'F.INV.RT(0.01, 6, 4)': 15.20686486,
'F.INV.RT(0.01, 6.9, 4.8)': 15.20686486,
'F.INV.RT(-0.01, 6.9, 4.8)': FormulaError.NUM,
'F.INV.RT(1.01, 6.9, 4.8)': FormulaError.NUM,
'F.INV.RT(0.01, 0.9, 4)': FormulaError.NUM,
'F.INV.RT(0.01, 6.9, 0.4)': FormulaError.NUM,
'F.INV.RT(0.01, 1000000000000, 4)': FormulaError.NUM,
'F.INV.RT(0.01, 6.9, 1000000000000)': FormulaError.NUM
});
});
test('F.TEST', () => {
runTest({
'F.TEST({6,7,9,15,21}, {20,1})': 0.200507085811744,
'F.TEST({6,7,9,15,21}, {20,28,31,38,40})': 0.648317846786174,
'F.TEST({6}, {20})': FormulaError.DIV0
});
});
test('FISHER', () => {
runTest({
'FISHER(0.75)': 0.972955074527657,
'FISHER(0.5)': 0.549306144334055,
'FISHER(-1.1)': FormulaError.NUM,
'FISHER(1.1)': FormulaError.NUM
});
});
test('FISHERINV', () => {
runTest({
'FISHERINV(0.972955)': 0.749999967,
'FISHERINV("string")': FormulaError.VALUE
});
});
test('FORECAST', () => {
runTest({
'FORECAST(30,{6,7,9,15,21},{20,28,31,38,40})': 10.60725308642,
// 'FORECAST(30,{6,6},{2,"1"})': FormulaError.DIV0,
'FORECAST(30,{6,6,4},{2,1,3})': -22.66666666667,
'FORECAST(30,{6,6,4},{2,1})': FormulaError.NA
});
});
test('FREQUENCY', () => {
runTest({
'FREQUENCY({1,2,3,4,5},{1,5,11})': [[1], [5], [5], [5]],
'FREQUENCY({1,2,3,4,5},{5,1,11})': [[1], [5], [5], [5]],
'FREQUENCY({1,2,3,4,5},{2,3})': [[2], [3], [5]],
'FREQUENCY({1,2,3,4,5},A1)': [[0], [5]]
});
});
test('GAMMA', () => {
runTest({
'GAMMA(2.5)': 1.329340388,
// 'GAMMA(-3.75)': 0.267866128861417, // Error: precise problem
'GAMMA(-2.5)': -0.94530872048,
'GAMMA(0)': FormulaError.NUM,
'GAMMA(-2)': FormulaError.NUM
});
});
test('GAMMA.DIST', () => {
runTest({
'GAMMA.DIST(-10.00001131, 9, 2, FALSE)': FormulaError.NUM,
'GAMMA.DIST(10.00001131, -9, 2, TRUE)': FormulaError.NUM,
'GAMMA.DIST(10.00001131, 9, -2, FALSE)': FormulaError.NUM,
'GAMMA.DIST(10.00001131, 9, 2, TRUE)': 0.068094004,
'GAMMA.DIST(10.00001131, 9, 2, FALSE)': 0.03263913
});
});
test('GAMMA.INV', () => {
runTest({
'GAMMA.INV(0.068094,9,2)': 10.00001119,
'GAMMA.INV(-0.1,9,2)': FormulaError.NUM,
'GAMMA.INV(11.1,9,2)': FormulaError.NUM,
'GAMMA.INV(0.5,-0.9,2)': FormulaError.NUM,
'GAMMA.INV(0.5,9,-0.2)': FormulaError.NUM
});
});
test('GAMMALN', () => {
runTest({
'GAMMALN(4)': 1.791759469,
'GAMMALN("string")': FormulaError.VALUE,
'GAMMALN(-4)': FormulaError.NUM
});
});
test('GAMMALN.PRECISE', () => {
runTest({
'GAMMALN.PRECISE(4)': 1.791759469,
'GAMMALN.PRECISE("string")': FormulaError.VALUE,
'GAMMALN.PRECISE(-4)': FormulaError.NUM
});
});
test('GAUSS', () => {
runTest({'GAUSS(2)': 0.477249868, 'GAUSS("string")': FormulaError.VALUE});
});
test('GEOMEAN', () => {
runTest({
'GEOMEAN({2, 2, "string"})': 2.0,
'GEOMEAN({2, 2})': 2.0,
'GEOMEAN({4,5,8,7,11,4,3})': 5.47698696965696
});
});
test('GROWTH', () => {
runTest({
'GROWTH({33100,47300,69000,102000,150000,220000})': [
[
32618.203773539713, 47729.42261474775, 69841.30085621744,
102197.07337883241, 149542.4867400457, 218821.87621459528
]
],
'GROWTH({33100,47300,69000,102000,150000,220000}, {11,12,13,14,15})':
FormulaError.REF,
'GROWTH({33100,47300,69000,102000,150000,220000}, {11,12,13,14,15,16})': [
[
32618.203773538437, 47729.422614746654, 69841.30085621694,
102197.07337883314, 149542.4867400494, 218821.87621460424
]
],
'SUM(GROWTH({33100,47300,69000,102000,150000,220000}, {11,12,13,14,15,16}))': 620750.363577979,
'GROWTH({33100,47300,69000,102000,150000,220000}, {11,12,13,14,15,16},{1;2})':
[[724.7673986628065, 1060.5344705164614]],
'SUM(GROWTH({33100,47300,69000,102000,150000,220000}, {11,12,13,14,15,16},{1;2}))': 1785.3018691796,
'SUM(GROWTH({1,2,3,4},{4,6,8,11},{8,9,10,11}))': 13.9796233554563
});
});
test('HARMEAN', () => {
runTest({
'HARMEAN(4,5,8,7,11,4,3)': 5.02837596206,
'HARMEAN(4,"5",{8,7,11,4,3})': 5.02837596206
});
});
test('HYPGEOM.DIST', () => {
runTest({
// 'HYPGEOM.DIST': (sample_s, number_sample, population_s, number_pop, cumulative)
'HYPGEOM.DIST(1,4,8,20,TRUE)': 0.465428276574,
'HYPGEOM.DIST(1,4,8,20,FALSE)': 0.363261093911,
// if ( sample_s < 0 || number_sample <= 0 || population_s <= 0 || number_pop <= 0 )
'HYPGEOM.DIST(0,4,8,20,TRUE)': 0.102167183,
'HYPGEOM.DIST(-1,4,8,20,TRUE)': FormulaError.NUM,
'HYPGEOM.DIST(1,0.0,8,20,TRUE)': FormulaError.NUM,
'HYPGEOM.DIST(1,-4,8,20,TRUE)': FormulaError.NUM,
'HYPGEOM.DIST(1,4,0,20,TRUE)': FormulaError.NUM,
'HYPGEOM.DIST(1,4,-8,20,TRUE)': FormulaError.NUM,
'HYPGEOM.DIST(1,4,8,0.0,TRUE)': FormulaError.NUM,
'HYPGEOM.DIST(1,4,8,-20,TRUE)': FormulaError.NUM,
// if (number_sample > number_pop)
'HYPGEOM.DIST(1,21,8,20,TRUE)': FormulaError.NUM,
// if (population_s > number_pop)
'HYPGEOM.DIST(1,4,8,7,TRUE)': FormulaError.NUM,
// If sample_s is less than the larger of 0 or (number_sample - number_population + population_s), HYPGEOM.DIST returns the #NUM! error value.
'HYPGEOM.DIST(1,4,8,10,FALSE)': FormulaError.NUM,
// if (number_sample < sample_s || population_s < sample_s)
'HYPGEOM.DIST(5,4,8,20,TRUE)': FormulaError.NUM,
'HYPGEOM.DIST(9,15,8,20,TRUE)': FormulaError.NUM,
'HYPGEOM.DIST(8,15,8,14,TRUE)': FormulaError.NUM,
'HYPGEOM.DIST(1,8,8,7,FALSE)': FormulaError.NUM,
'HYPGEOM.DIST(1,4,21,20,FALSE)': FormulaError.NUM
});
});
test('INTERCEPT', () => {
runTest({'INTERCEPT({2,3,9,1,8},{6,5,11,7,5})': 0.04838709677});
});
test('KURT', () => {
runTest({
'KURT(3,4,5,2,3,4,5,6,4,7)': -0.151799637208,
'KURT(3,{4,5},"2",3,4,5,6,4,7)': -0.151799637208
});
});
test('LOGNORM.DIST', () => {
runTest({
'LOGNORM.DIST(4,3.5,1.2,TRUE)': 0.0390835557068,
'LOGNORM.DIST(4,3.5,1.2,FALSE)': 0.0176175966818,
'LOGNORM.DIST(-4,3.5,1.2,TRUE)': FormulaError.NUM,
'LOGNORM.DIST(4,3.5,-1.2,TRUE)': FormulaError.NUM
});
});
test('LOGNORM.INV', () => {
runTest({
'LOGNORM.INV(0.039084, 3.5, 1.2)': 4.0000252186806,
'LOGNORM.INV(-0.039084, 3.5, 1.2)': FormulaError.NUM,
'LOGNORM.INV( 1.039084, 3.5, 1.2)': FormulaError.NUM,
'LOGNORM.INV(0.039084, 3.5, -1.2)': FormulaError.NUM
});
});
test('NEGBINOM.DIST', () => {
runTest({
'NEGBINOM.DIST(10, 5, 0.25,TRUE)': 0.3135140584782,
'NEGBINOM.DIST(10, 5, 0.25,FALSE)': 0.0550486603752,
'NEGBINOM.DIST(10, 5, -0.25,FALSE)': FormulaError.NUM,
'NEGBINOM.DIST(10, 5, 1.25,FALSE)': FormulaError.NUM,
'NEGBINOM.DIST(-10, 5, 0.25,FALSE)': FormulaError.NUM,
'NEGBINOM.DIST(10, 0.5, 0.25,FALSE)': FormulaError.NUM
});
});
test('NORM.DIST', () => {
runTest({
'NORM.DIST(42, 40, 1.5, TRUE)': 0.90878878,
'NORM.DIST(42, 40, 1.5, FALSE)': 0.10934005,
'NORM.DIST(1.333333, 0, 1, TRUE)': 0.908788726, // the same to NORM.S.DIST
'NORM.DIST(42, 40, 0, TRUE)': FormulaError.NUM,
'NORM.DIST(42, 40, -1.1, TRUE)': FormulaError.NUM
});
});
test('NORM.INV', () => {
runTest({
'NORM.INV(0.908789, 40, 1.5)': 42.00000201,
'NORM.INV(0.908789, 0, 1)': 1.333334673, // the same to NORM.S.INV
'NORM.INV(-1.0, 40, 1.5)': FormulaError.NUM,
'NORM.INV(0.0, 40, 1.5)': FormulaError.NUM,
'NORM.INV(1.1, 40, 1.5)': FormulaError.NUM,
'NORM.INV(1.0, 40, 1.5)': FormulaError.NUM,
'NORM.INV(0.9, 40, -1.5)': FormulaError.NUM,
'NORM.INV(0.9, 40, 0.0)': FormulaError.NUM
});
});
test('NORM.S.DIST', () => {
runTest({
'NORM.S.DIST(1.333333,TRUE)': 0.908788726,
'NORM.S.DIST(1.333333,FALSE)': 0.164010148
});
});
test('NORM.S.INV', () => {
runTest({
'NORM.S.INV(0.908789)': 1.333334673,
'NORM.S.INV(-1)': FormulaError.NUM,
'NORM.S.INV(0)': FormulaError.NUM,
'NORM.S.INV(1)': FormulaError.NUM,
'NORM.S.INV(1.1)': FormulaError.NUM
});
});
test('PHI', () => {
runTest({
'PHI(0.5)': 0.3520653268,
'PHI(0.75)': 0.301137432,
'PHI(1.00)': 0.2419707245
});
});
test('POISSON.DIST', () => {
runTest({
'POISSON.DIST(-0.5,5,FALSE)': FormulaError.NUM,
'POISSON.DIST(2,-0.5,FALSE)': FormulaError.NUM,
'POISSON.DIST(2,5,TRUE)': 0.124652019,
'POISSON.DIST(2,5,FALSE)': 0.084224337
});
});
test('PROB', () => {
runTest({
'PROB({0, 1, 2, 3},{0.2, 0.3, 0.1, 0.4}, 2)': 0.1,
'PROB({0, 1, 2, 3},{0.2, 0.3, 0.1, 0.4}, 1, 3)': 0.8
});
});
test('QUARTILE', () => {
runTest({
'QUARTILE({1,2,4,7,8,9,10,12},1)': 3.5
});
});
test('QUARTILE.INC', () => {
runTest({
'QUARTILE.INC({1,2,4,7,8,9,10,12},1)': 3.5
});
});
test('QUARTILE.EXC', () => {
runTest({
'QUARTILE.EXC({6,7,15,36,49,40,41,42,43,47,49},1)': 15,
'QUARTILE.EXC({6,7,15,36,49,40,41,42,43,47,49},3)': 47
});
});
test('STANDARDIZE', () => {
runTest({
'STANDARDIZE(42, 40,1.5)': 1.333333333333,
'STANDARDIZE(42, 40, 0.0)': FormulaError.NUM,
'STANDARDIZE(42, 40, -0.5)': FormulaError.NUM
});
});
test('STDEV', () => {
runTest({
'STDEV({1345,1301,1368,1322,1310,1370,1318,1350,1303,1299})': 27.46392,
'STDEV.S({1345,1301,1368,1322,1310,1370,1318,1350,1303,1299})': 27.46392
});
});
test('STDEV.P', () => {
runTest({
'STDEV.P({1345,1301,1368,1322,1310,1370,1318,1350,1303,1299})': 26.05455814
});
});
test('STDEVA', () => {
runTest({
'STDEVA({1345, 1301, 1368, 1322, 1310, 1370, 1318, 1350, 1303, 1299})': 27.46391571984349
});
});
test('STDEVPA', () => {
runTest({
'STDEVPA({1345, 1301, 1368, 1322, 1310, 1370, 1318, 1350, 1303, 1299})': 26.054558142482477
});
});
test('STEYX', () => {
runTest({
'STEYX({2, 3, 9, 1, 8, 7, 5}, {6, 5, 11, 7, 5, 4, 4})': 3.305718950210041
});
});
test('T.DIST', () => {
runTest({
'T.DIST(60,1,TRUE)': 0.994695326367,
'T.DIST(8,3,TRUE)': 0.9979617112,
'T.DIST(8,3,FALSE)': 0.000736906521,
'T.DIST(60,0.9,TRUE)': FormulaError.NUM,
'T.DIST(60,0,TRUE)': FormulaError.NUM
});
});
test('T.DIST.2T', () => {
runTest({
'T.DIST.2T(1.959999998,60)': 0.0546449299759,
'T.DIST.2T(-0.959999998,60)': FormulaError.NUM,
'T.DIST.2T(1.959999998,0.6)': FormulaError.NUM
});
});
test('T.DIST.RT', () => {
runTest({
'T.DIST.RT(1.959999998, 60)': 0.027322464988,
'T.DIST.RT(1.959999998, 0.6)': FormulaError.NUM
});
});
test('T.INV', () => {
runTest({
'T.INV(0.75, 2)': 0.816496581,
'T.INV(0.75, 2.1)': 0.816496581,
'T.INV(0.75, 2.9)': 0.816496581,
'T.INV(0.0, 2)': FormulaError.NUM,
'T.INV(-0.75, 2)': FormulaError.NUM,
'T.INV(1.75, 2)': FormulaError.NUM,
'T.INV(0.75, 0.92)': FormulaError.NUM
});
});
test('T.INV.2T', () => {
runTest({
'T.INV.2T(0.546449, 60)': 0.606533076,
'T.INV.2T(0.546449, 60.1)': 0.606533076,
'T.INV.2T(0.546449, 60.9)': 0.606533076,
'T.INV.2T(0.0, 60)': FormulaError.NUM,
'T.INV.2T(-0.546449, 60)': FormulaError.NUM,
'T.INV.2T(1.546449, 60)': FormulaError.NUM,
'T.INV.2T(0.546449, 0.6)': FormulaError.NUM
});
});
test('VAR.S', () => {
runTest({
'VAR.S({1345,1301,1368,1322,1310,1370,1318,1350,1303,1299})': 754.2667
});
});
test('VAR.P', () => {
runTest({
'VAR.P({1345,1301,1368,1322,1310,1370,1318,1350,1303,1299})': 678.84
});
});
test('WEIBULL.DIST', () => {
runTest({
'WEIBULL.DIST(105, 20, 100,TRUE)': 0.92958139,
'WEIBULL.DIST(105, 20, 100,FALSE)': 0.035588864,
'WEIBULL.DIST(-105, 20, 100,FALSE)': FormulaError.NUM,
'WEIBULL.DIST(105, 0.0, 100,FALSE)': FormulaError.NUM,
'WEIBULL.DIST(105, -20, 100,FALSE)': FormulaError.NUM,
'WEIBULL.DIST(105, 20, 0.0,FALSE)': FormulaError.NUM,
'WEIBULL.DIST(105, 20, -100,FALSE)': FormulaError.NUM
});
});
test('Z.TEST', () => {
runTest({
'Z.TEST({3,6,7,8,6,5,4,3,2,1,9}, 4)': 0.118314897
});
});
test('PEARSON', () => {
runTest({
'PEARSON({9, 7, 5, 3, 1},{10, 6, 1, 5, 3})': 0.6993786061802354
});
});
test('PERMUT', () => {
runTest({
'PERMUT(100, 3)': 970200
});
});
test('PERMUTATIONA', () => {
runTest({
'PERMUTATIONA(3,2)': 9
});
});
test('RSQ', () => {
runTest({
'RSQ({2, 3, 9, 1, 8, 7, 5},{6, 5, 11, 7, 5, 4, 4})': 0.05795019157088122
});
});
test('SLOPE', () => {
runTest({
'SLOPE({2, 3, 9, 1, 8, 7, 5},{6, 5, 11, 7, 5, 4, 4})': 0.3055555555555556
});
});
test('SKEW', () => {
runTest({
'SKEW({3, 4, 5, 2, 3, 4, 5, 6, 4, 7})': 0.3595430714067974
});
});
test('SKEW.P', () => {
runTest({
'SKEW.P({3, 4, 5, 2, 3, 4, 5, 6, 4, 7})': 0.303193339354144
});
});
test('COUNTBLANK', () => {
runTest({
'COUNTBLANK({1,2,3,"",5,"",7})': 2
});
});
test('PERCENTILE.EXC', () => {
runTest({
'PERCENTILE.EXC({1, 2, 3, 4},0.2)': 1
});
});
test('PERCENTILE.INC', () => {
runTest({
'PERCENTILE.INC({1, 2, 3, 4},0.2)': 1.6
});
});
test('PERCENTRANK.EXC', () => {
runTest({
'PERCENTRANK.EXC({1, 2, 3, 4},1)': 0.2
});
});
test('PERCENTRANK.INC', () => {
runTest({
'PERCENTRANK.INC({1, 2, 3, 4},1)': 0,
'PERCENTRANK.INC({1, 2, 3, 4},2)': 0.333
});
});
test('VARA', () => {
runTest({
'VARA({1, 2, 3, 4, 10, 10})': 16
});
});
test('VARPA', () => {
runTest({
'VARPA({1, 2, 3, 4, 10, 10})': 13.333333333333334
});
});
test('LINEST', () => {
runTest({
'LINEST({1,9,5,7},{0,4,2,3})': [2, 1]
});
});
test('LOGEST', () => {
runTest({
'LOGEST({1,9,5,7},{0,4,2,3})': [1.751116, 1.194316]
});
});
test('TREND', () => {
runTest({
'TREND({1,9,5,7},{0,4,2,3},{5,8})': [11, 17]
});
});
test('TRIMMEAN', () => {
runTest({
'TRIMMEAN({4, 5, 6, 7, 2, 3, 4, 5, 1, 2, 3}, 0.2)': 3.7777777777777777
});
});

View File

@ -0,0 +1,243 @@
/**
* fast-formula-parser
*/
import FormulaError from '../../FormulaError';
import {EvalResult} from '../../eval/EvalResult';
import {TestCase, buildEnv, testEvalCases} from './buildEnv';
const data: [] = [];
const env = buildEnv(data);
function runTest(testCase: TestCase) {
testEvalCases(testCase, env);
}
test('ASC', () => {
runTest({
'ASC("")': 'ABC'
// 'ASC("ヲァィゥ")': 'ヲァィゥ',
// 'ASC(",。")': ',。'
});
});
test('BAHTTEXT', () => {
runTest({
'BAHTTEXT(1234)': 'หนึ่งพันสองร้อยสามสิบสี่บาทถ้วน'
});
});
test('CHAR', () => {
runTest({
'CHAR(65)': 'A',
'CHAR(33)': '!'
});
});
test('CLEAN', () => {
runTest({
'CLEAN("äÄçÇéÉêPHP-MySQLöÖÐþúÚ")': 'äÄçÇéÉêPHP-MySQLöÖÐþúÚ',
'CLEAN(CHAR(9)&"Monthly report"&CHAR(10))': 'Monthly report'
});
});
test('CODE', () => {
runTest({
'CODE("C")': 67,
'CODE("")': FormulaError.VALUE
});
});
test('CONCAT', () => {
runTest({
'CONCAT(0, {1,2,3;5,6,7})': '0123567',
'CONCAT(TRUE, 0, {1,2,3;5,6,7})': 'TRUE0123567',
'CONCAT(0, {1,2,3;5,6,7},)': '0123567',
'CONCAT("The"," ","sun"," ","will"," ","come"," ","up"," ","tomorrow.")':
'The sun will come up tomorrow.',
'CONCAT({1,2,3}, "aaa", TRUE, 0, FALSE)': '123aaaTRUE0FALSE'
});
});
test('CONCATENATE', () => {
runTest({
'CONCATENATE({9,8,7})': '9',
'CONCATENATE({9,8,7},{8,7,6})': '98',
'CONCATENATE({9,8,7},"hello")': '9hello',
'CONCATENATE({0,2,3}, 1, "A", TRUE, -12)': '01ATRUE-12'
});
});
test('DBCS', () => {
runTest({
'DBCS("ABC")': ''
// 'DBCS("ヲァィゥ")': 'ヲァィゥ',
// 'DBCS(",。")': ',。',
});
});
test('DOLLAR', () => {
runTest({
'DOLLAR(1234567)': '$1,234,567.00',
'DOLLAR(12345.67)': '$12,345.67'
});
});
test('EXACT', () => {
runTest({
'EXACT("hello", "hElLo")': false,
'EXACT("HELLO","HELLO")': true
});
});
test('FIND', () => {
runTest({'FIND("h","Hello")': FormulaError.VALUE, 'FIND("o", "hello")': 5});
});
test('FIXED', () => {
runTest({
'FIXED(1234.567, 1)': '1,234.6',
'FIXED(12345.64123213)': '12,345.64',
'FIXED(12345.64123213, 5)': '12,345.64123',
'FIXED(12345.64123213, 5, TRUE)': '12345.64123',
'FIXED(123456789.64, 5, FALSE)': '123,456,789.64000'
});
});
test('LEFT', () => {
runTest({
'LEFT("Salesman")': 'S',
'LEFT("Salesman",4)': 'Sale'
});
});
test('LEN', () => {
runTest({'LEN("Phoenix, AZ")': 11});
});
test('LOWER', () => {
runTest({'LOWER("E. E. Cummings")': 'e. e. cummings'});
});
test('MID', () => {
runTest({
'MID("Fluid Flow",1,5)': 'Fluid',
'MID("Foo",5,1)': '',
'MID("Foo",1,5)': 'Foo',
'MID("Foo",-1,5)': FormulaError.VALUE,
'MID("Foo",1,-5)': FormulaError.VALUE
});
});
test('NUMBERVALUE', () => {
runTest({
'NUMBERVALUE("3.5%")': 0.035,
'NUMBERVALUE("2.500,27",",",".")': 2500.27,
// group separator occurs before the decimal separator
'NUMBERVALUE("2500.,27",",",".")': 2500.27,
'NUMBERVALUE("3 50")': 350,
'NUMBERVALUE("$3 50")': 350,
'NUMBERVALUE("($3 50)")': -350,
'NUMBERVALUE("-($3 50)")': FormulaError.VALUE,
'NUMBERVALUE("($-3 50)")': FormulaError.VALUE,
'NUMBERVALUE("2500,.27",",",".")': FormulaError.VALUE,
// group separator occurs after the decimal separator
'NUMBERVALUE("3.5%",".",".")': FormulaError.VALUE,
// 'NUMBERVALUE("3.5%",,)': FormulaError.VALUE,
// decimal separator is used more than once
'NUMBERVALUE("3..5")': FormulaError.VALUE
});
});
test('PROPER', () => {
runTest({
'PROPER("this is a tiTle")': 'This Is A Title',
'PROPER("2-way street")': '2-Way Street',
'PROPER("76BudGet")': '76Budget'
});
});
test('REPLACE', () => {
runTest({
'REPLACE("abcdefghijk",6,5,"*")': 'abcde*k',
'REPLACE("abcdefghijk",6,0,"*")': 'abcde*fghijk'
});
});
test('REPT', () => {
runTest({'REPT("*_",4)': '*_*_*_*_'});
});
test('RIGHT', () => {
runTest({
'RIGHT("Salesman")': 'n',
'RIGHT("Salesman",4)': 'sman'
});
});
test('SEARCH', () => {
runTest({
'SEARCH(",", "abcdef")': FormulaError.VALUE,
'SEARCH("b", "abcdef")': 2,
'SEARCH("c*f", "abcdef")': 3,
'SEARCH("c?f", "abcdef")': FormulaError.VALUE,
'SEARCH("c?e", "abcdef")': 3,
'SEARCH("c\\b", "abcabcac\\bacb", 6)': 8
});
});
test('SUBSTITUTE', () => {
runTest({
'SUBSTITUTE("Jim Alateras", "Jim", "James")': 'James Alateras'
});
});
test('T', () => {
runTest({'T("*_")': '*_', 'T(19)': ''});
});
test('TEXT', () => {
runTest({'TEXT(1234.567,"$#,##0.00")': '$1,234.57'});
});
test('TRIM', () => {
runTest({
'TRIM(" First Quarter Earnings ")': 'First Quarter Earnings'
});
});
test('UNICHAR', () => {
runTest({
'UNICHAR(32)': ' ',
'UNICHAR(66)': 'B',
'UNICHAR(0)': FormulaError.VALUE,
'UNICHAR(3333)': 'അ'
});
});
test('UNICODE', () => {
runTest({
'UNICODE(" ")': 32,
'UNICODE("B")': 66,
'UNICODE("")': FormulaError.VALUE
});
});
test('UPPER', () => {
runTest({'UPPER("E. E. Cummings")': 'E. E. CUMMINGS'});
});
test('ENCODEURL', () => {
runTest({
'ENCODEURL("http://www.google.com")': 'http%3A%2F%2Fwww.google.com',
'ENCODEURL("http://www.google.com/this is a test")':
'http%3A%2F%2Fwww.google.com%2Fthis%20is%20a%20test'
});
});
test('VALUE', () => {
runTest({
'VALUE("12%")': 0.12
});
});

View File

@ -0,0 +1,222 @@
import FormulaError from '../../FormulaError';
import {TestCase, buildEnv, testEvalCases} from './buildEnv';
const data = [
['', 1, 2, 3, 4],
['string', 3, 4, 5, 6]
];
const env = buildEnv(data);
function runTest(testCase: TestCase) {
testEvalCases(testCase, env);
}
test('AGGREGATE', () => {
runTest({
'AGGREGATE(1, 4, {1, 2, 3})': 2,
'AGGREGATE(2, 4, {1, 2, 3, "does not count"})': 3,
'AGGREGATE(3, 4, {1, 2, 3, "counts"})': 4,
'AGGREGATE(4, 4, {1, 2, 3})': 3,
'AGGREGATE(5, 4, {1, 2, 3})': 1,
'AGGREGATE(6, 4, {1, 2, 3})': 6,
'AGGREGATE(7, 4, {1, 2, 3})': 1,
'AGGREGATE(8, 4, {1, 2, 3})': 0.816496580927726,
'AGGREGATE(9, 4, {1, 2, 3})': 6,
'AGGREGATE(10, 4, {1, 2, 3})': 1,
'AGGREGATE(11, 4, {1, 2, 3})': 0.6666666666666666,
'AGGREGATE(12, 4, {1, 2, 3})': 2,
'AGGREGATE(13, 4, {1, 2, 3})': 1,
'AGGREGATE(14, 4, {1, 2, 3}, 2)': 2,
'AGGREGATE(15, 4, {1, 2, 3}, 2)': 2,
'AGGREGATE(16, 4, {1, 2, 3}, 0.4)': 1.8,
'AGGREGATE(17, 4, {1, 2, 3}, 2)': 2,
'AGGREGATE(18, 4, {1, 2, 3}, 0.4)': 1.6,
'AGGREGATE(19, 4, {1, 2, 3}, 2)': 2,
'AGGREGATE("invalid", 4, {1, 2, 3}, 2)': FormulaError.VALUE
});
});
test('ACOS', () => {
runTest({
'ACOS(-0.5)': 2.094395102,
'ACOS(-0.5)*180/PI()': 120,
'DEGREES(ACOS(-0.5))': 120,
'ACOS(-1.5)': FormulaError.NUM
});
});
test('ACOSH', () => {
runTest({
'ACOSH(1)': 0,
'ACOSH(10)': 2.993222846,
'ACOSH(0.99)': FormulaError.NUM
});
});
test('ACOT', () => {
runTest({'ACOT(2)': 0.463647609, 'ACOT(-2)': 2.677945045});
});
test('ACOTH', () => {
runTest({
'ACOTH(-5)': -0.202732554,
'ACOTH(6)': 0.168236118,
'ACOTH(0.99)': FormulaError.NUM
});
});
test('ASIN', () => {
runTest({
'ASIN(-0.5)': -0.523598776,
'ASIN(-0.5)*180/PI()': -30,
'DEGREES(ASIN(-0.5))': -30,
'ASIN(-1.5)': FormulaError.NUM
});
});
test('ASINH', () => {
runTest({
'ASINH(-2.5)': -1.647231146,
'ASINH(10)': 2.99822295
});
});
test('ATAN', () => {
runTest({
'ATAN(0)': 0,
'ATAN(1)': 0.785398163,
'ATAN(1)*180/PI()': 45,
'DEGREES(ATAN(1))': 45
});
});
test('ATAN2', () => {
runTest({
'ATAN2(1, 1)': 0.785398163,
'ATAN2(-1, -1)': -2.35619449,
'ATAN2(-1, -1)*180/PI()': -135,
'DEGREES(ATAN2(-1, -1))': -135,
'ATAN2(0,0)': FormulaError.DIV0
});
});
test('ATANH', () => {
runTest({
'ATANH(0.76159416)': 1.00000001,
'ATANH(-0.1)': -0.100335348,
'ATANH(-1.1)': FormulaError.NUM
});
});
test('COS', () => {
runTest({
'COS(1.047)': 0.500171075,
'COS(60*PI()/180)': 0.5,
'COS(RADIANS(60))': 0.5,
'COS(2^27-1)': -0.293388404,
'COS(2^27)': FormulaError.NUM
});
});
test('COSH', () => {
runTest({
'COSH(4)': 27.30823284,
'COSH(EXP(1))': 7.610125139,
'COSH(0)': 1,
'COSH(800)': FormulaError.NUM
});
});
test('COT', () => {
runTest({
'COT(30)': -0.156119952,
'COT(45)': 0.617369624,
'COT(2^27-1)': 0.306893777,
'COT(2^27)': FormulaError.NUM,
'COT(0)': FormulaError.DIV0
});
});
test('COTH', () => {
runTest({
'COTH(2)': 1.037314721,
'COTH(0)': FormulaError.DIV0,
'COTH(2^100)': 1, // no value error here
'COTH(-2^100)': 1
});
});
test('CSC', () => {
runTest({
'CSC(15)': 1.537780562,
'CSC(2^27-1)': -1.046032404,
'CSC(2^27)': FormulaError.NUM
});
});
test('CSCH', () => {
runTest({
'CSCH(1.5)': 0.469642441,
'CSCH(2^100)': 0,
'CSCH(-2^100)': 0,
'CSCH(0)': FormulaError.DIV0
});
});
test('SEC', () => {
runTest({
'SEC(45)': 1.903594407,
'SEC(2^27-1)': -3.408451009,
'SEC(2^27)': FormulaError.NUM
});
});
test('SECH', () => {
runTest({
'SECH(45)': 5.7250371611e-20,
'SECH(2^100)': 0,
'SECH(-2^100)': 0,
'SECH(0)': 1
});
});
test('SIN', () => {
runTest({
'SIN(PI())': 0,
'SIN(PI()/2)': 1,
'SIN(30*PI()/180)': 0.5,
'SIN(RADIANS(30))': 0.5,
'SIN(2^27-1)': -0.955993329,
'SIN(2^27)': FormulaError.NUM
});
});
test('SINH', () => {
runTest({
'2.868*SINH(0.0342*1.03)': 0.101049063,
'SINH(800)': FormulaError.NUM
});
});
test('TAN', () => {
runTest({
'TAN(0.785)': 0.99920399,
'TAN(45*PI()/180)': 1,
'TAN(RADIANS(45))': 1,
'TAN(0)': 0,
'TAN(PI())': -1.22515e-16,
'TAN(2^27-1)': 3.258456426,
'TAN(2^27)': FormulaError.NUM
});
});
test('TANH', () => {
runTest({
'TANH(-2)': -0.96402758,
'TANH(0)': 0,
'TANH(0.5)': 0.462117157,
'TANH(2^100)': 1,
'TANH(-2^100)': 1
});
});

View File

@ -0,0 +1,357 @@
/**
*
*/
import FormulaError from '../FormulaError';
import {EvalResult} from '../eval/EvalResult';
import {Factorials, factorial, factorialDouble} from './util/Factorials';
import {functions, regFunc} from './functions';
import {
getNumber,
getNumberOrThrow,
getNumberWithDefault
} from './util/getNumber';
import {getNumbers} from './util/getNumbers';
import {getNumber2DArray} from './util/getNumbersWithUndefined';
import {getString, getStringOrThrow} from './util/getString';
import {Criteria, parseCriteria} from '../parser/parseCriteria';
import {evalCriterial} from '../eval/evalCriterial';
import {getArray} from './util/getArray';
import {evalIFS} from './util/evalIFS';
import {flattenArgs} from './util/flattenArgs';
import {VAR, VARP, standardDeviation} from './distribution';
/**
*
* @param arr1
* @param arr2
*/
function arrayIsEqual(arr1: EvalResult[][], arr2: EvalResult[][]) {
if (arr1.length !== arr2.length) {
return false;
}
for (let i = 0; i < arr1.length; i++) {
if (arr1[i].length !== arr2[i].length) {
return false;
}
for (let j = 0; j < arr1[i].length; j++) {
if (arr1[i][j] !== arr2[i][j]) {
return false;
}
}
}
return true;
}
function getFilteredData(
fieldNameIndex: Map<string, number>,
db: EvalResult[][],
criteria: EvalResult[][]
) {
// 数据库过滤后的内容
const filtered: EvalResult[][] = [];
// 构建解析后的条件,其中的 number 是字段索引Criteria[] 是条件
const parsedCriteria: Array<Map<number, Criteria[]>> = [];
const firstCriteriaRow = criteria[0];
for (let rowIndex = 1; rowIndex < criteria.length; rowIndex++) {
for (let colIndex = 0; colIndex < criteria[rowIndex].length; colIndex++) {
const criteriaStr = criteria[rowIndex][colIndex];
if (typeof criteriaStr === 'string') {
const fileName = firstCriteriaRow[colIndex] as string;
const fieldIndex = fieldNameIndex.get(fileName);
if (fieldIndex === undefined) {
throw FormulaError.VALUE;
}
if (!parsedCriteria[rowIndex]) {
parsedCriteria[rowIndex] = new Map();
parsedCriteria[rowIndex].set(fieldIndex, [
parseCriteria(criteriaStr)
]);
} else {
if (!parsedCriteria[rowIndex].has(fieldIndex)) {
parsedCriteria[rowIndex].set(fieldIndex, [
parseCriteria(criteriaStr)
]);
} else {
parsedCriteria[rowIndex]
.get(fieldIndex)!
.push(parseCriteria(criteriaStr));
}
}
}
}
}
for (let rowIndex = 1; rowIndex < db.length; rowIndex++) {
const row = db[rowIndex];
// 一行内是且关系,多行是或关系
let match = false;
for (const criteriaRow of parsedCriteria) {
if (!criteriaRow) {
continue;
}
let rowMatch = true;
for (const fieldIndex of criteriaRow.keys()) {
const criteriaParsed = criteriaRow.get(fieldIndex);
if (criteriaParsed) {
for (const criteria of criteriaParsed) {
if (!evalCriterial(criteria, row[fieldIndex] as string)) {
rowMatch = false;
break;
}
}
}
}
if (rowMatch) {
match = true;
}
}
if (match) {
filtered.push(row);
}
}
return filtered;
}
/**
*
* @param db
* @param criteria
* @param field
*/
export function getDatabaseResult(
db: EvalResult[][],
criteria: EvalResult[][],
field?: number | string
) {
// 数据库过滤后的内容
const firstRow = db[0];
// 字段名对应的索引
const fieldNameIndex: Map<string, number> = new Map();
for (let i = 0; i < firstRow.length; i++) {
fieldNameIndex.set(firstRow[i] as string, i);
}
let filtered: EvalResult[][] = [];
if (arrayIsEqual(db, criteria)) {
db.shift();
filtered = db;
} else {
filtered = getFilteredData(fieldNameIndex, db, criteria);
}
let result: EvalResult[] = [];
if (field) {
let fieldIndex = 0;
if (typeof field === 'string') {
fieldIndex = fieldNameIndex.get(field) || 0;
} else {
fieldIndex = field - 1;
}
for (const row of filtered) {
result.push(row[fieldIndex]);
}
return result;
}
return flattenArgs(filtered);
}
regFunc(
'DAVERAGE',
(database: EvalResult, field: EvalResult, criteria: EvalResult) => {
const filtered = getDatabaseResult(
database as EvalResult[][],
criteria as EvalResult[][],
field as number | string
);
const numbers = getNumbers(filtered);
if (numbers.length === 0) {
throw FormulaError.DIV0;
}
return numbers.reduce((prev, cur) => prev + cur, 0) / numbers.length;
}
);
regFunc(
'DCOUNT',
(database: EvalResult, field: EvalResult, criteria: EvalResult) => {
const filtered = getDatabaseResult(
database as EvalResult[][],
criteria as EvalResult[][],
field as number | string
);
return getNumbers(filtered).length;
}
);
regFunc(
'DCOUNTA',
(database: EvalResult, field: EvalResult, criteria: EvalResult) => {
const filtered = getDatabaseResult(
database as EvalResult[][],
criteria as EvalResult[][],
field as number | string
);
return filtered.length;
}
);
regFunc(
'DGET',
(database: EvalResult, field: EvalResult, criteria: EvalResult) => {
const filtered = getDatabaseResult(
database as EvalResult[][],
criteria as EvalResult[][],
field as number | string
);
if (filtered.length === 0) {
throw FormulaError.VALUE;
}
return filtered[0];
}
);
regFunc(
'DMAX',
(database: EvalResult, field: EvalResult, criteria: EvalResult) => {
const filtered = getDatabaseResult(
database as EvalResult[][],
criteria as EvalResult[][],
field as number | string
);
const numbers = getNumbers(filtered);
if (numbers.length === 0) {
throw FormulaError.DIV0;
}
return Math.max(...numbers);
}
);
regFunc(
'DMIN',
(database: EvalResult, field: EvalResult, criteria: EvalResult) => {
const filtered = getDatabaseResult(
database as EvalResult[][],
criteria as EvalResult[][],
field as number | string
);
const numbers = getNumbers(filtered);
if (numbers.length === 0) {
throw FormulaError.DIV0;
}
return Math.min(...numbers);
}
);
regFunc(
'DPRODUCT',
(database: EvalResult, field: EvalResult, criteria: EvalResult) => {
const filtered = getDatabaseResult(
database as EvalResult[][],
criteria as EvalResult[][],
field as number | string
);
const numbers = getNumbers(filtered);
if (numbers.length === 0) {
return 0;
}
return numbers.reduce((prev, cur) => prev * cur, 1);
}
);
regFunc(
'DSTDEV',
(database: EvalResult, field: EvalResult, criteria: EvalResult) => {
const filtered = getDatabaseResult(
database as EvalResult[][],
criteria as EvalResult[][],
field as number | string
);
const numbers = getNumbers(filtered);
if (numbers.length === 0) {
throw FormulaError.DIV0;
}
return standardDeviation(numbers);
}
);
regFunc(
'DSTDEVP',
(database: EvalResult, field: EvalResult, criteria: EvalResult) => {
const filtered = getDatabaseResult(
database as EvalResult[][],
criteria as EvalResult[][],
field as number | string
);
const numbers = getNumbers(filtered);
if (numbers.length === 0) {
throw FormulaError.DIV0;
}
return standardDeviation(numbers, true);
}
);
regFunc(
'DSUM',
(database: EvalResult, field: EvalResult, criteria: EvalResult) => {
const filtered = getDatabaseResult(
database as EvalResult[][],
criteria as EvalResult[][],
field as number | string
);
return getNumbers(filtered).reduce((prev, cur) => prev + cur, 0);
}
);
regFunc(
'DVAR',
(database: EvalResult, field: EvalResult, criteria: EvalResult) => {
const filtered = getDatabaseResult(
database as EvalResult[][],
criteria as EvalResult[][],
field as number | string
);
const numbers = getNumbers(filtered);
if (numbers.length === 0) {
throw FormulaError.DIV0;
}
return VAR(...numbers);
}
);
regFunc(
'DVARP',
(database: EvalResult, field: EvalResult, criteria: EvalResult) => {
const filtered = getDatabaseResult(
database as EvalResult[][],
criteria as EvalResult[][],
field as number | string
);
const numbers = getNumbers(filtered);
if (numbers.length === 0) {
throw FormulaError.DIV0;
}
return VARP(numbers);
}
);

View File

@ -0,0 +1,745 @@
/**
* fast-formula-parser
*/
import FormulaError from '../FormulaError';
import {EvalResult} from '../eval/EvalResult';
import {regFunc} from './functions';
import {getNumberOrThrow, getNumberWithDefault} from './util/getNumber';
import {getBoolean} from './util/getBoolean';
import {getStringOrThrow} from './util/getString';
import {loopArgs} from './util/loopArgs';
import {flattenArgs} from './util/flattenArgs';
const MS_PER_DAY = 1000 * 60 * 60 * 24;
const d1900 = new Date(Date.UTC(1900, 0, 1));
const WEEK_STARTS = [
undefined,
0,
1,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
1,
2,
3,
4,
5,
6,
0
];
const WEEK_TYPES = [
undefined,
[1, 2, 3, 4, 5, 6, 7],
[7, 1, 2, 3, 4, 5, 6],
[6, 0, 1, 2, 3, 4, 5],
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
[7, 1, 2, 3, 4, 5, 6],
[6, 7, 1, 2, 3, 4, 5],
[5, 6, 7, 1, 2, 3, 4],
[4, 5, 6, 7, 1, 2, 3],
[3, 4, 5, 6, 7, 1, 2],
[2, 3, 4, 5, 6, 7, 1],
[1, 2, 3, 4, 5, 6, 7]
];
const WEEKEND_TYPES = [
undefined,
[6, 0],
[0, 1],
[1, 2],
[2, 3],
[3, 4],
[4, 5],
[5, 6],
undefined,
undefined,
undefined,
[0],
[1],
[2],
[3],
[4],
[5],
[6]
];
// Formats: h:mm:ss A, h:mm A, H:mm, H:mm:ss, H A
const timeRegex = /^\s*(\d\d?)\s*(:\s*\d\d?)?\s*(:\s*\d\d?)?\s*(pm|am)?\s*$/i;
// 12-3, 12/3
const dateRegex1 = /^\s*((\d\d?)\s*([-\/])\s*(\d\d?))([\d:.apm\s]*)$/i;
// 3-Dec, 3/Dec
const dateRegex2 =
/^\s*((\d\d?)\s*([-/])\s*(jan\w*|feb\w*|mar\w*|apr\w*|may\w*|jun\w*|jul\w*|aug\w*|sep\w*|oct\w*|nov\w*|dec\w*))([\d:.apm\s]*)$/i;
// Dec-3, Dec/3
const dateRegex3 =
/^\s*((jan\w*|feb\w*|mar\w*|apr\w*|may\w*|jun\w*|jul\w*|aug\w*|sep\w*|oct\w*|nov\w*|dec\w*)\s*([-/])\s*(\d\d?))([\d:.apm\s]*)$/i;
function parseSimplifiedDate(text: string): Date {
const fmt1 = text.match(dateRegex1);
const fmt2 = text.match(dateRegex2);
const fmt3 = text.match(dateRegex3);
if (fmt1) {
text = fmt1[1] + fmt1[3] + new Date().getFullYear() + fmt1[5];
} else if (fmt2) {
text = fmt2[1] + fmt2[3] + new Date().getFullYear() + fmt2[5];
} else if (fmt3) {
text = fmt3[1] + fmt3[3] + new Date().getFullYear() + fmt3[5];
}
return new Date(Date.parse(`${text} UTC`));
}
/**
* Parse time string to date in UTC.
* @param {string} text
*/
function parseTime(text: string) {
const res = text.match(timeRegex);
if (!res) return;
//  ["4:50:55 pm", "4", ":50", ":55", "pm", ...]
const minutes = res[2] ? res[2] : ':00';
const seconds = res[3] ? res[3] : ':00';
const ampm = res[4] ? ' ' + res[4] : '';
const date = new Date(
Date.parse(`1/1/1900 ${res[1] + minutes + seconds + ampm} UTC`)
);
let now = new Date();
now = new Date(
Date.UTC(
now.getFullYear(),
now.getMonth(),
now.getDate(),
now.getHours(),
now.getMinutes(),
now.getSeconds(),
now.getMilliseconds()
)
);
return new Date(
Date.UTC(
now.getUTCFullYear(),
now.getUTCMonth(),
now.getUTCDate(),
date.getUTCHours(),
date.getUTCMinutes(),
date.getUTCSeconds(),
date.getUTCMilliseconds()
)
);
}
/**
* Parse a UTC date to excel serial number.
* @param {Date|number} date - A UTC date.
*/
function toSerial(date: number | Date): number {
if (date instanceof Date) {
date = date.getTime();
}
const addOn = date > -2203891200000 ? 2 : 1;
return Math.floor((date - d1900.getTime()) / 86400000) + addOn;
}
/**
* Parse an excel serial number to UTC date.
* @param serial
*/
function toDate(serial: number) {
if (serial < 0) {
throw FormulaError.VALUE;
}
if (serial <= 60) {
return new Date(d1900.getTime() + (serial - 1) * 86400000);
}
return new Date(d1900.getTime() + (serial - 2) * 86400000);
}
export function parseDateWithExtra(serialOrString: string | Date) {
if (serialOrString instanceof Date) {
return {date: serialOrString, isDateGiven: true};
}
let isDateGiven = true,
date;
if (!isNaN(Number(serialOrString))) {
const serial = Number(serialOrString);
date = toDate(serial);
} else {
// support time without date
date = parseTime(serialOrString);
if (!date) {
date = parseSimplifiedDate(serialOrString);
} else {
isDateGiven = false;
}
}
return {date, isDateGiven};
}
export function parseDate(serialOrString: string | Date) {
return parseDateWithExtra(serialOrString).date;
}
export function parseDates(...dates: EvalResult[]) {
return flattenArgs(dates).map(date => parseDate(date as string));
}
function compareDateIgnoreTime(date1: Date, date2: Date) {
return (
date1.getUTCFullYear() === date2.getUTCFullYear() &&
date1.getUTCMonth() === date2.getUTCMonth() &&
date1.getUTCDate() === date2.getUTCDate()
);
}
function isLeapYear(year: number) {
if (year === 1900) {
return true;
}
return new Date(year, 1, 29).getMonth() === 1;
}
regFunc('DATE', (...arg: EvalResult[]) => {
let year = getNumberOrThrow(arg[0]);
const month = getNumberOrThrow(arg[1]);
const day = getNumberOrThrow(arg[2]);
if (year < 0 || year >= 10000) {
throw FormulaError.NUM;
}
// If year is between 0 (zero) and 1899 (inclusive), Excel adds that value to 1900 to calculate the year.
if (year < 1900) {
year += 1900;
}
return toSerial(Date.UTC(year, month - 1, day));
});
export function DATEDIF(startDate: Date, endDate: Date, unit: string) {
unit = unit.toLowerCase();
if (startDate > endDate) {
throw FormulaError.NUM;
}
const yearDiff = endDate.getUTCFullYear() - startDate.getUTCFullYear();
const monthDiff = endDate.getUTCMonth() - startDate.getUTCMonth();
const dayDiff = endDate.getUTCDate() - startDate.getUTCDate();
let offset;
switch (unit) {
case 'y':
offset = monthDiff < 0 || (monthDiff === 0 && dayDiff < 0) ? -1 : 0;
return offset + yearDiff;
case 'm':
offset = dayDiff < 0 ? -1 : 0;
return yearDiff * 12 + monthDiff + offset;
case 'd':
return Math.floor(endDate.getTime() - startDate.getTime()) / MS_PER_DAY;
case 'md':
// The months and years of the dates are ignored.
startDate.setUTCFullYear(endDate.getUTCFullYear());
if (dayDiff < 0) {
startDate.setUTCMonth(endDate.getUTCMonth() - 1);
} else {
startDate.setUTCMonth(endDate.getUTCMonth());
}
return Math.floor(endDate.getTime() - startDate.getTime()) / MS_PER_DAY;
case 'ym':
// The days and years of the dates are ignored
offset = dayDiff < 0 ? -1 : 0;
return (offset + yearDiff * 12 + monthDiff) % 12;
case 'yd':
// The years of the dates are ignored.
if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) {
startDate.setUTCFullYear(endDate.getUTCFullYear() - 1);
} else {
startDate.setUTCFullYear(endDate.getUTCFullYear());
}
return Math.floor(endDate.getTime() - startDate.getTime()) / MS_PER_DAY;
}
throw FormulaError.VALUE;
}
regFunc('DATEDIF', (...arg: EvalResult[]) => {
const startDate = parseDate(arg[0] as string);
const endDate = parseDate(arg[1] as string);
const unit = getStringOrThrow(arg[2]).toLowerCase();
return DATEDIF(startDate, endDate, unit);
});
regFunc('DATEVALUE', (dateText: EvalResult) => {
dateText = parseDate(dateText as string);
const {date, isDateGiven} = parseDateWithExtra(dateText);
if (!isDateGiven) {
return 0;
}
const serial = toSerial(date);
if (serial < 0 || serial > 2958465) {
throw FormulaError.VALUE;
}
return serial;
});
regFunc('DAY', (dateText: EvalResult) => {
const date = parseDate(dateText as string);
return date.getUTCDate();
});
export function DAYS(endDate: EvalResult, startDate: EvalResult) {
endDate = parseDate(endDate as string).getTime();
startDate = parseDate(startDate as string).getTime();
let offset = 0;
if (startDate < -2203891200000 && -2203891200000 < endDate) {
offset = 1;
}
return Math.floor(endDate - startDate) / MS_PER_DAY + offset;
}
regFunc('DAYS', DAYS);
export function DAYS360(start_date: Date, end_date: Date, method: boolean) {
const sm = start_date.getMonth();
let em = end_date.getMonth();
let sd, ed;
if (method) {
sd = start_date.getDate() === 31 ? 30 : start_date.getDate();
ed = end_date.getDate() === 31 ? 30 : end_date.getDate();
} else {
const smd = new Date(start_date.getFullYear(), sm + 1, 0).getDate();
const emd = new Date(end_date.getFullYear(), em + 1, 0).getDate();
sd = start_date.getDate() === smd ? 30 : start_date.getDate();
if (end_date.getDate() === emd) {
if (sd < 30) {
em++;
ed = 1;
} else {
ed = 30;
}
} else {
ed = end_date.getDate();
}
}
return (
360 * (end_date.getFullYear() - start_date.getFullYear()) +
30 * (em - sm) +
(ed - sd)
);
}
regFunc(
'DAYS360',
(startDate: EvalResult, endDate: EvalResult, method: EvalResult) => {
startDate = parseDate(startDate as string);
endDate = parseDate(endDate as string);
// default is US method
method = getBoolean(method, false)!;
return DAYS360(startDate, endDate, method);
}
);
regFunc('EDATE', (startDate: EvalResult, months: EvalResult) => {
startDate = parseDate(startDate as string);
months = getNumberOrThrow(months);
startDate.setUTCMonth(startDate.getUTCMonth() + months);
return toSerial(startDate);
});
regFunc('EOMONTH', (startDate: EvalResult, months: EvalResult) => {
startDate = parseDate(startDate as string);
months = getNumberOrThrow(months);
startDate.setUTCMonth(startDate.getUTCMonth() + months + 1, 0);
return toSerial(startDate);
});
regFunc('HOUR', (dateText: EvalResult) => {
const date = parseDate(dateText as string);
return date.getUTCHours();
});
function ISOWEEKNUM(dateText: EvalResult) {
const date = parseDate(dateText as string);
// https://stackoverflow.com/questions/6117814/get-week-of-year-in-javascript-like-in-php
const d = new Date(
Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())
);
const dayNum = d.getUTCDay();
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
}
regFunc('ISOWEEKNUM', ISOWEEKNUM);
regFunc('MINUTE', (dateText: EvalResult) => {
const date = parseDate(dateText as string);
return date.getUTCMinutes();
});
regFunc('MONTH', (dateText: EvalResult) => {
const date = parseDate(dateText as string);
return date.getUTCMonth() + 1;
});
regFunc(
'NETWORKDAYS',
(startDate: EvalResult, endDate: EvalResult, holidays: EvalResult) => {
startDate = parseDate(startDate as string);
endDate = parseDate(endDate as string);
let sign = 1;
if (startDate > endDate) {
sign = -1;
const temp = startDate;
startDate = endDate;
endDate = temp;
}
const holidaysArr: Date[] = [];
if (holidays !== undefined) {
loopArgs([holidays], item => {
holidaysArr.push(parseDate(item as string));
});
}
let numWorkDays = 0;
while (startDate <= endDate) {
// Skips Sunday and Saturday
if (startDate.getUTCDay() !== 0 && startDate.getUTCDay() !== 6) {
let found = false;
for (let i = 0; i < holidaysArr.length; i++) {
if (compareDateIgnoreTime(startDate, holidaysArr[i])) {
found = true;
break;
}
}
if (!found) numWorkDays++;
}
startDate.setUTCDate(startDate.getUTCDate() + 1);
}
return sign * numWorkDays;
}
);
regFunc(
'NETWORKDAYS.INTL',
(
startDate: EvalResult,
endDate: EvalResult,
weekend: EvalResult,
holidays: EvalResult
) => {
startDate = parseDate(startDate as string);
endDate = parseDate(endDate as string);
let sign = 1;
if (startDate > endDate) {
sign = -1;
const temp = startDate;
startDate = endDate;
endDate = temp;
}
if (weekend === '1111111') {
return 0;
}
weekend = weekend || 1;
// Using 1111111 will always return 0.
// using weekend string, i.e, 0000011
if (typeof weekend === 'string' && Number(weekend).toString() !== weekend) {
if (weekend.length !== 7) throw FormulaError.VALUE;
weekend = weekend.charAt(6) + weekend.slice(0, 6);
const weekendArr = [];
for (let i = 0; i < weekend.length; i++) {
if (weekend.charAt(i) === '1') weekendArr.push(i);
}
weekend = weekendArr;
} else {
// using weekend number
if (typeof weekend !== 'number') throw FormulaError.VALUE;
weekend = WEEKEND_TYPES[weekend];
}
const holidaysArr: Date[] = [];
if (holidays !== undefined) {
loopArgs([holidays], item => {
holidaysArr.push(parseDate(item as string));
});
}
let numWorkDays = 0;
while (startDate <= endDate) {
let skip = false;
if (Array.isArray(weekend)) {
for (let i = 0; i < weekend.length; i++) {
if (weekend[i] === startDate.getUTCDay()) {
skip = true;
break;
}
}
}
if (!skip) {
let found = false;
for (let i = 0; i < holidaysArr.length; i++) {
if (compareDateIgnoreTime(startDate, holidaysArr[i])) {
found = true;
break;
}
}
if (!found) numWorkDays++;
}
startDate.setUTCDate(startDate.getUTCDate() + 1);
}
return sign * numWorkDays;
}
);
regFunc('NOW', () => {
const now = new Date();
return (
toSerial(
Date.UTC(
now.getFullYear(),
now.getMonth(),
now.getDate(),
now.getHours(),
now.getMinutes(),
now.getSeconds(),
now.getMilliseconds()
)
) +
(3600 * now.getHours() + 60 * now.getMinutes() + now.getSeconds()) / 86400
);
});
regFunc('SECOND', (dateText: EvalResult) => {
const date = parseDate(dateText as string);
return date.getUTCSeconds();
});
regFunc('TIME', (hour: EvalResult, minute: EvalResult, second: EvalResult) => {
hour = getNumberOrThrow(hour);
minute = getNumberOrThrow(minute);
second = getNumberOrThrow(second);
if (
hour < 0 ||
hour > 32767 ||
minute < 0 ||
minute > 32767 ||
second < 0 ||
second > 32767
) {
throw FormulaError.NUM;
}
return (3600 * hour + 60 * minute + second) / 86400;
});
regFunc('TIMEVALUE', (timeText: EvalResult) => {
timeText = parseDate(timeText as string);
return (
(3600 * timeText.getUTCHours() +
60 * timeText.getUTCMinutes() +
timeText.getUTCSeconds()) /
86400
);
});
regFunc('TODAY', () => {
const now = new Date();
return toSerial(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()));
});
regFunc('WEEKDAY', (serialOrString: EvalResult, returnType: EvalResult) => {
const date = parseDate(serialOrString as string);
returnType = getNumberWithDefault(returnType, 1);
const day = date.getUTCDay();
const weekTypes = WEEK_TYPES[returnType];
if (!weekTypes) {
throw FormulaError.NUM;
}
return weekTypes[day];
});
regFunc('WEEKNUM', (serialOrString: EvalResult, returnType: EvalResult) => {
const date = parseDate(serialOrString as string);
returnType = getNumberWithDefault(returnType, 1);
if (returnType === 21) {
return ISOWEEKNUM(serialOrString);
}
const weekStart = WEEK_STARTS[returnType];
if (weekStart === undefined) {
throw FormulaError.NUM;
}
const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
const offset = yearStart.getUTCDay() < weekStart ? 1 : 0;
return (
Math.ceil(((date.getTime() - yearStart.getTime()) / 86400000 + 1) / 7) +
offset
);
});
regFunc(
'WORKDAY',
(startDate: EvalResult, days: EvalResult, holidays: EvalResult) => {
return WORKDAY_INTL(startDate, days, 1, holidays);
}
);
function WORKDAY_INTL(
startDate: EvalResult,
days: EvalResult,
weekend: EvalResult,
holidays: EvalResult
) {
startDate = parseDate(startDate as string);
days = getNumberOrThrow(days);
weekend = weekend || 1;
// Using 1111111 will always return value error.
if (weekend === '1111111') {
throw FormulaError.VALUE;
}
// using weekend string, i.e, 0000011
if (typeof weekend === 'string' && Number(weekend).toString() !== weekend) {
if (weekend.length !== 7) throw FormulaError.VALUE;
weekend = weekend.charAt(6) + weekend.slice(0, 6);
const weekendArr = [];
for (let i = 0; i < weekend.length; i++) {
if (weekend.charAt(i) === '1') weekendArr.push(i);
}
weekend = weekendArr;
} else {
// using weekend number
if (typeof weekend !== 'number') throw FormulaError.VALUE;
weekend = WEEKEND_TYPES[weekend];
if (weekend == null) throw FormulaError.NUM;
}
const holidaysArr: Date[] = [];
if (holidays !== undefined) {
loopArgs([holidays], item => {
holidaysArr.push(parseDate(item as string));
});
}
startDate.setUTCDate(startDate.getUTCDate() + 1);
let cnt = 0;
while (cnt < days) {
let skip = false;
for (let i = 0; i < weekend.length; i++) {
if (weekend[i] === startDate.getUTCDay()) {
skip = true;
break;
}
}
if (!skip) {
let found = false;
for (let i = 0; i < holidaysArr.length; i++) {
if (compareDateIgnoreTime(startDate, holidaysArr[i])) {
found = true;
break;
}
}
if (!found) cnt++;
}
startDate.setUTCDate(startDate.getUTCDate() + 1);
}
return toSerial(startDate) - 1;
}
regFunc('WORKDAY.INTL', WORKDAY_INTL);
regFunc('YEAR', (dateText: EvalResult) => {
const date = parseDate(dateText as string);
return date.getUTCFullYear();
});
export function YEARFRAC(startDate: Date, endDate: Date, basis: number) {
if (startDate > endDate) {
const temp = startDate;
startDate = endDate;
endDate = temp;
}
if (basis < 0 || basis > 4) throw FormulaError.VALUE;
// https://github.com/LesterLyu/formula.js/blob/develop/lib/date-time.js#L508
let sd = startDate.getUTCDate();
const sm = startDate.getUTCMonth() + 1;
const sy = startDate.getUTCFullYear();
let ed = endDate.getUTCDate();
const em = endDate.getUTCMonth() + 1;
const ey = endDate.getUTCFullYear();
switch (basis) {
case 0:
// US (NASD) 30/360
if (sd === 31 && ed === 31) {
sd = 30;
ed = 30;
} else if (sd === 31) {
sd = 30;
} else if (sd === 30 && ed === 31) {
ed = 30;
}
return (
Math.abs(ed + em * 30 + ey * 360 - (sd + sm * 30 + sy * 360)) / 360
);
case 1:
// Actual/actual
if (ey - sy < 2) {
const yLength = isLeapYear(sy) && sy !== 1900 ? 366 : 365;
const days = DAYS(endDate, startDate);
return days / yLength;
} else {
const years = ey - sy + 1;
const days =
(new Date(ey + 1, 0, 1).getTime() - new Date(sy, 0, 1).getTime()) /
1000 /
60 /
60 /
24;
const average = days / years;
return DAYS(endDate, startDate) / average;
}
case 2:
// Actual/360
return Math.abs(DAYS(endDate, startDate) / 360);
case 3:
// Actual/365
return Math.abs(DAYS(endDate, startDate) / 365);
case 4:
// European 30/360
return (
Math.abs(ed + em * 30 + ey * 360 - (sd + sm * 30 + sy * 360)) / 360
);
}
throw FormulaError.VALUE;
}
regFunc(
'YEARFRAC',
(startDate: EvalResult, endDate: EvalResult, basis: EvalResult) => {
startDate = parseDate(startDate as string);
endDate = parseDate(endDate as string);
basis = Math.trunc(getNumberWithDefault(basis, 0));
return YEARFRAC(startDate, endDate, basis);
}
);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,43 @@
import {FunctionName} from '../builtinFunctions';
import {functions, regFunc} from './functions';
const functionAlias: Map<FunctionName, FunctionName> = new Map([
['BETADIST', 'BETA.DIST'],
['BETAINV', 'BETA.INV'],
['BINOMDIST', 'BINOM.DIST'],
['CHIDIST', 'CHISQ.DIST'],
['CHIINV', 'CHISQ.INV'],
['CHITEST', 'CHISQ.TEST'],
['COVAR', 'COVARIANCE.P'],
['CRITBINOM', 'BINOM.INV'],
['EXPONDIST', 'EXPON.DIST'],
['FDIST', 'F.DIST'],
['FINV', 'F.INV'],
['FTEST', 'F.TEST'],
['GAMMADIST', 'GAMMA.DIST'],
['GAMMAINV', 'GAMMA.INV'],
['HYPGEOMDIST', 'HYPGEOM.DIST'],
['LOGINV', 'LOGNORM.INV'],
['LOGNORMDIST', 'LOGNORM.DIST'],
['NEGBINOMDIST', 'NEGBINOM.DIST'],
['NORMDIST', 'NORM.DIST'],
['NORMINV', 'NORM.INV'],
['NORMSDIST', 'NORM.S.DIST'],
['NORMSINV', 'NORM.S.INV'],
['MODE', 'MODE.SNGL'],
['STDEVP', 'STDEV.P'],
['TDIST', 'T.DIST'],
['TINV', 'T.INV'],
// ['TTEST', 'T.TEST'],
['VARP', 'VAR.P'],
['ZTEST', 'Z.TEST']
]);
for (const [alias, name] of functionAlias) {
const func = functions.get(name);
if (!func) {
throw new Error(`Function ${name} not found`);
} else {
functions.set(alias, func);
}
}

View File

@ -0,0 +1,22 @@
import {FunctionName} from '../builtinFunctions';
import {EvalResult} from '../eval/EvalResult';
// https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb?ui=en-us&rs=en-us&ad=us
type Func = (...args: EvalResult[]) => EvalResult;
export const functions: Map<FunctionName, Func> = new Map();
export function regFunc(name: FunctionName, func: Func) {
if (functions.has(name)) {
throw new Error(`function ${name} has been registered`);
}
functions.set(name, func);
}
// 这些函数每次计算结果都不一样,不能缓存
const volatileFunctions = new Set(['RAND', 'RANDBETWEEN', 'NOW', 'TODAY']);
export function isVolatileFunction(functionName: FunctionName) {
return volatileFunctions.has(functionName);
}

View File

@ -0,0 +1,117 @@
/**
* fast-formula-parser
*/
import FormulaError from '../FormulaError';
import {EvalResult, isErrorValue} from '../eval/EvalResult';
import {regFunc} from './functions';
import {getNumberOrThrow} from './util/getNumber';
import {getSingleValue} from './util/getSingleValue';
const error2Number = {
'#NULL!': 1,
'#DIV/0!': 2,
'#VALUE!': 3,
'#REF!': 4,
'#NAME?': 5,
'#NUM!': 6,
'#N/A': 7
};
type error2NumberKey = keyof typeof error2Number;
regFunc('ERROR.TYPE', (value: EvalResult) => {
if (typeof value === 'object' && 'type' in value && value.type === 'Error') {
return error2Number[value.value as error2NumberKey];
}
throw FormulaError.NA;
});
regFunc('ISBLANK', (value: EvalResult) => {
return value === undefined || value === null || value === '';
});
regFunc('ISERR', (value: EvalResult) => {
return (
value &&
typeof value === 'object' &&
'type' in value &&
value.type === 'Error' &&
value.value !== '#N/A'
);
});
regFunc('ISERROR', (value: EvalResult) => {
return isErrorValue(value);
});
regFunc('ISEVEN', (value: EvalResult) => {
return !(Math.floor(Math.abs(getNumberOrThrow(value))) & 1);
});
regFunc('ISODD', (value: EvalResult) => {
return !!(Math.floor(Math.abs(getNumberOrThrow(value))) & 1);
});
regFunc('ISLOGICAL', (value: EvalResult) => {
return typeof value === 'boolean';
});
export function ISNA(value: EvalResult) {
return (
value &&
typeof value === 'object' &&
'type' in value &&
value.type === 'Error' &&
value.value === '#N/A'
);
}
regFunc('ISNA', ISNA);
regFunc('ISNONTEXT', (value: EvalResult) => {
return typeof getSingleValue(value) !== 'string';
});
regFunc('ISNUMBER', (value: EvalResult) => {
return typeof getSingleValue(value) === 'number';
});
regFunc('ISTEXT', (value: EvalResult) => {
return typeof getSingleValue(value) === 'string';
});
regFunc('N', (value: EvalResult) => {
value = getSingleValue(value);
if (typeof value === 'number') {
return value;
} else if (typeof value === 'boolean') {
return value ? 1 : 0;
} else if (isErrorValue(value)) {
throw FormulaError.VALUE;
} else {
return 0;
}
});
regFunc('NA', () => {
throw FormulaError.NA;
});
regFunc('TYPE', (value: EvalResult) => {
if (typeof value === 'number') {
return 1;
} else if (typeof value === 'string') {
return 2;
} else if (typeof value === 'boolean') {
return 4;
} else if (value === undefined || value === null) {
return 16;
} else if (isErrorValue(value)) {
return 16;
} else if (Array.isArray(value)) {
return 64;
} else {
return 0;
}
});

View File

@ -0,0 +1,154 @@
/**
* fast-formula-parser
*/
import FormulaError from '../FormulaError';
import {EvalResult, isErrorValue} from '../eval/EvalResult';
import {regFunc} from './functions';
import {getBoolean} from './util/getBoolean';
import {loopArgs} from './util/loopArgs';
import {ISNA} from './information';
import {getSingleValue} from './util/getSingleValue';
const error2Number = {
'#NULL!': 1,
'#DIV/0!': 2,
'#VALUE!': 3,
'#REF!': 4,
'#NAME?': 5,
'#NUM!': 6,
'#N/A': 7
};
function getNumLogicalValue(params: EvalResult[]) {
let numTrue = 0,
numFalse = 0;
loopArgs(params, val => {
const type = typeof val;
if (type === 'string') {
if (val === 'TRUE') val = true;
else if (val === 'FALSE') val = false;
} else if (type === 'number') val = Boolean(val);
if (typeof val === 'boolean') {
if (val === true) numTrue++;
else numFalse++;
}
});
return [numTrue, numFalse];
}
regFunc('AND', (...params: EvalResult[]) => {
const [numTrue, numFalse] = getNumLogicalValue(params);
// OR returns #VALUE! if no logical values are found.
if (numTrue === 0 && numFalse === 0) {
throw FormulaError.VALUE;
}
return numTrue > 0 && numFalse === 0;
});
regFunc('FALSE', () => false);
regFunc(
'IF',
(
logicalTest: EvalResult,
valueIfTrue: EvalResult,
valueIfFalse: EvalResult = false
) => {
logicalTest = getBoolean(logicalTest);
return logicalTest ? valueIfTrue : valueIfFalse;
}
);
regFunc('IFERROR', (value: EvalResult, valueIfError: EvalResult) => {
return isErrorValue(value) ? valueIfError : value;
});
regFunc('IFS', (...params: EvalResult[]) => {
if (params.length % 2 !== 0) {
throw FormulaError.VALUE;
}
for (let i = 0; i < params.length / 2; i += 2) {
const logicalTest = getBoolean(params[i * 2]);
const valueIfTrue = params[i * 2 + 1];
if (logicalTest) {
return valueIfTrue;
}
}
throw FormulaError.NA;
});
regFunc(
'IFNA',
(value: EvalResult, valueIfNa: EvalResult, other: EvalResult) => {
if (other !== undefined) {
throw FormulaError.TOO_MANY_ARGS('IFNA');
}
return ISNA(value) ? valueIfNa : value;
}
);
regFunc('NOT', (logical: EvalResult) => {
return !getBoolean(logical);
});
regFunc('OR', (...params: EvalResult[]) => {
const [numTrue, numFalse] = getNumLogicalValue(params);
// OR returns #VALUE! if no logical values are found.
if (numTrue === 0 && numFalse === 0) {
throw FormulaError.VALUE;
}
return numTrue > 0;
});
regFunc('TRUE', () => true);
regFunc('SWITCH', function SWITCH() {
let result;
if (arguments.length > 0) {
const targetValue = getSingleValue(arguments[0]);
const argc = arguments.length - 1;
const switchCount = Math.floor(argc / 2);
let switchSatisfied = false;
const hasDefaultClause = argc % 2 !== 0;
const defaultClause =
argc % 2 === 0 ? null : arguments[arguments.length - 1];
if (switchCount) {
for (let index = 0; index < switchCount; index++) {
if (targetValue === arguments[index * 2 + 1]) {
result = arguments[index * 2 + 2];
switchSatisfied = true;
break;
}
}
}
if (!switchSatisfied) {
if (hasDefaultClause) {
return result;
}
throw FormulaError.NA;
}
} else {
throw FormulaError.VALUE;
}
return result;
});
regFunc('XOR', (...params: EvalResult[]) => {
const [numTrue, numFalse] = getNumLogicalValue(params);
// XOR returns #VALUE! if no logical values are found.
if (numTrue === 0 && numFalse === 0) {
throw FormulaError.VALUE;
}
return numTrue % 2 === 1;
});

View File

@ -0,0 +1,838 @@
/**
* fast-formula-parser
* https://github.com/LesterLyu/fast-formula-parser
*/
import FormulaError from '../FormulaError';
import {EvalResult} from '../eval/EvalResult';
import {Factorials, factorial, factorialDouble} from './util/Factorials';
import {functions, regFunc} from './functions';
import {
getNumber,
getNumberOrThrow,
getNumberWithDefault
} from './util/getNumber';
import {getNumbers} from './util/getNumbers';
import {getNumber2DArray} from './util/getNumbersWithUndefined';
import {getString, getStringOrThrow} from './util/getString';
import {parseCriteria} from '../parser/parseCriteria';
import {evalCriterial} from '../eval/evalCriterial';
import {getArray} from './util/getArray';
import {evalIFS} from './util/evalIFS';
// https://support.microsoft.com/en-us/office/abs-function-3420200f-5628-4e8c-99da-c99d7c87713c
regFunc('ABS', (arg: EvalResult) => {
const num = getNumberOrThrow(arg);
return Math.abs(num);
});
// https://support.microsoft.com/en-us/office/arabic-function-9a8da418-c17b-4ef9-a657-9370a30a674f
regFunc('ARABIC', (...args: EvalResult[]) => {
const str = getStringOrThrow(args[0]);
const roman = str.toUpperCase();
if (
!/^M*(?:D?C{0,3}|C[MD])(?:L?X{0,3}|X[CL])(?:V?I{0,3}|I[XV])$/.test(roman)
) {
throw FormulaError.VALUE;
}
let r = 0;
roman.replace(/[MDLV]|C[MD]?|X[CL]?|I[XV]?/g, function (i) {
r +=
{
M: 1000,
CM: 900,
D: 500,
CD: 400,
C: 100,
XC: 90,
L: 50,
XL: 40,
X: 10,
IX: 9,
V: 5,
IV: 4,
I: 1
}[i] || 0;
return '';
});
return r;
});
// https://support.microsoft.com/en-us/office/base-function-2ef61411-aee9-4f29-a811-1c42456c6342
regFunc('BASE', (...args: EvalResult[]) => {
const numbers = getNumbers(args);
if (numbers.length < 2) {
throw FormulaError.VALUE;
}
const number = numbers[0];
if (number < 0 || number >= 2 ** 53) {
throw FormulaError.NUM;
}
const radix = numbers[1];
if (radix < 2 || radix > 36) {
throw FormulaError.NUM;
}
const minLength = numbers.length > 2 ? numbers[2] : 0;
if (minLength < 0) {
throw FormulaError.NUM;
}
const result = number.toString(radix).toUpperCase();
const res =
new Array(Math.max(minLength + 1 - result.length, 0)).join('0') + result;
return res;
});
// https://support.microsoft.com/en-us/office/ceiling-function-0a5cd7c8-0720-4f0a-bd2c-c943e510899f
function CEILING(...args: EvalResult[]) {
const numbers = getNumbers(args);
if (numbers.length !== 2) {
throw FormulaError.VALUE;
}
const number = numbers[0];
const significance = numbers[1];
if (significance === 0) {
return 0;
}
if ((number / significance) % 1 === 0) {
return number;
}
const absSignificance = Math.abs(significance);
const times = Math.floor(Math.abs(number) / absSignificance);
if (number < 0) {
// round down, away from zero
const roundDown = significance < 0;
return roundDown
? -absSignificance * (times + 1)
: -absSignificance * times;
} else {
return (times + 1) * absSignificance;
}
}
regFunc('CEILING', CEILING);
// https://support.microsoft.com/en-us/office/ceiling-math-function-80f95d2f-b499-4eee-9f16-f795a8e306c8
regFunc('CEILING.MATH', (...args: EvalResult[]) => {
const number = getNumberOrThrow(args[0]);
const significance = getNumber(args[1], number > 0 ? 1 : -1)!;
const mode = getNumber(args[2], 0)!;
if (number >= 0) {
return CEILING(number, significance);
}
// if round down, away from zero, then significance
const offset = mode ? significance : 0;
return CEILING(number, significance) - offset;
});
// https://support.microsoft.com/en-us/office/ceiling-precise-function-f366a774-527a-4c92-ba49-af0a196e66cb
regFunc('CEILING.PRECISE', (...args: EvalResult[]) => {
const number = getNumberOrThrow(args[0]);
const significance = getNumber(args[1], 1)!;
return CEILING(number, Math.abs(significance));
});
function COMBIN(number: number, numberChosen: number) {
if (number < 0 || numberChosen < 0 || number < numberChosen) {
throw FormulaError.NUM;
}
const nFactorial = FACT(number),
kFactorial = FACT(numberChosen);
return nFactorial / kFactorial / FACT(number - numberChosen);
}
// https://support.microsoft.com/en-us/office/combin-function-12a3f276-0a21-423a-8de6-06990aaf638a
regFunc('COMBIN', (...args: EvalResult[]) => {
const numbers = getNumbers(args);
if (numbers.length !== 2) {
throw FormulaError.VALUE;
}
return COMBIN(numbers[0], numbers[1]);
});
// https://support.microsoft.com/en-us/office/combina-function-efb49eaa-4f4c-4cd2-8179-0ddfcf9d035d
regFunc('COMBINA', (...args: EvalResult[]) => {
const number = getNumberOrThrow(args[0]);
const numberChosen = getNumberOrThrow(args[1]);
if ((number === 0 || number === 1) && numberChosen === 0) {
return 1;
}
if (number < 0 || numberChosen < 0) {
throw FormulaError.NUM;
}
return COMBIN(number + numberChosen - 1, number - 1);
});
regFunc('DECIMAL', (...args: EvalResult[]) => {
if (args.length != 2) {
throw FormulaError.VALUE;
}
const text = getStringOrThrow(args[0]);
let radix = getNumberOrThrow(args[1]);
radix = Math.trunc(radix);
if (radix < 2 || radix > 36) {
throw FormulaError.NUM;
}
const res = parseInt(text, radix);
if (isNaN(res)) {
throw FormulaError.NUM;
}
return res;
});
regFunc('DEGREES', (arg: EvalResult) => {
const num = getNumberOrThrow(arg);
return (num * 180) / Math.PI;
});
regFunc('EVEN', (arg: EvalResult) => {
const num = getNumberOrThrow(arg);
return CEILING(num, -2);
});
regFunc('EXP', (arg: EvalResult) => {
const num = getNumberOrThrow(arg);
return Math.exp(num);
});
function FACT(num: number) {
num = Math.trunc(num);
if (num > 170 || num < 0) {
throw FormulaError.NUM;
}
if (num <= 100) {
return Factorials[num];
}
return factorial(num);
}
regFunc('FACT', (arg: EvalResult) => {
const num = getNumberOrThrow(arg);
return FACT(num);
});
regFunc('FACTDOUBLE', (arg: EvalResult) => {
let number = getNumberOrThrow(arg);
number = Math.trunc(number);
if (number < -1) {
throw FormulaError.NUM;
}
if (number === -1) {
return 1;
}
return factorialDouble(number);
});
export function FLOOR(number: number, significance: number) {
if (significance === 0) {
return 0;
}
if (number > 0 && significance < 0) {
throw FormulaError.NUM;
}
if ((number / significance) % 1 === 0) {
return number;
}
const absSignificance = Math.abs(significance);
const times = Math.floor(Math.abs(number) / absSignificance);
if (number < 0) {
// round down, away from zero
const roundDown = significance < 0;
return roundDown
? -absSignificance * times
: -absSignificance * (times + 1);
} else {
// toward zero
return times * absSignificance;
}
}
regFunc('FLOOR', (...args: EvalResult[]) => {
const number = getNumberOrThrow(args[0]);
const significance = getNumber(args[1], 0)!;
return FLOOR(number, significance);
});
regFunc('FLOOR.MATH', (...args: EvalResult[]) => {
const number = getNumberOrThrow(args[0]);
const significance = getNumber(args[1], number > 0 ? 1 : -1)!;
const mode = getNumber(args[2], 0)!;
// The Mode argument does not affect positive numbers.
if (mode === 0 || number >= 0) {
// away from zero
return FLOOR(number, Math.abs(significance));
}
// towards zero, add a significance
return FLOOR(number, significance) + significance;
});
regFunc('FLOOR.PRECISE', (...args: EvalResult[]) => {
const number = getNumberOrThrow(args[0]);
const significance = getNumber(args[1], 1)!;
return FLOOR(number, Math.abs(significance));
});
regFunc('GCD', (...args: EvalResult[]) => {
const numbers = getNumbers(args, arg => {
if (arg === undefined) {
throw FormulaError.VALUE;
}
if (arg < 0 || arg > 9007199254740990) {
throw FormulaError.NUM;
}
return Math.trunc(arg);
});
// http://rosettacode.org/wiki/Greatest_common_divisor#JavaScript
let i,
y,
n = numbers.length,
x = Math.abs(numbers[0]);
for (i = 1; i < n; i++) {
y = Math.abs(numbers[i]);
while (x && y) {
x > y ? (x %= y) : (y %= x);
}
x += y;
}
return x;
});
regFunc('INT', (arg: EvalResult) => {
const num = getNumberOrThrow(arg);
return Math.floor(num);
});
regFunc('ISO.CEILING', (...args: EvalResult[]) => {
return functions.get('CEILING.PRECISE')!(...args);
});
regFunc('LCM', (...args: EvalResult[]) => {
const numbers = getNumbers(args, arg => {
if (arg === undefined) {
throw FormulaError.VALUE;
}
if (arg < 0 || arg > 9007199254740990) {
throw FormulaError.NUM;
}
return Math.trunc(arg);
});
let n = numbers.length,
a = Math.abs(numbers[0]);
for (let i = 1; i < n; i++) {
let b = Math.abs(numbers[i]),
c = a;
while (a && b) {
a > b ? (a %= b) : (b %= a);
}
a = Math.abs(c * numbers[i]) / (a + b);
}
return a;
});
regFunc('LN', (arg: EvalResult) => {
const num = getNumberOrThrow(arg);
return Math.log(num);
});
regFunc('LOG', (...args: EvalResult[]) => {
const number = getNumberOrThrow(args[0]);
const base = getNumber(args[1], 10)!;
return Math.log(number) / Math.log(base);
});
regFunc('LOG10', (arg: EvalResult) => {
const num = getNumberOrThrow(arg);
return Math.log10(num);
});
function getNum(num: number | undefined): number {
if (num === undefined) {
throw FormulaError.VALUE;
}
return num;
}
regFunc('MDETERM', (...args: EvalResult[]) => {
if (args.length !== 1) {
throw FormulaError.VALUE;
}
const array = getNumber2DArray(args[0]);
if (array.length === 0) {
throw FormulaError.VALUE;
}
if (!Array.isArray(array[0])) {
throw FormulaError.VALUE;
}
if (array[0].length !== array.length) {
throw FormulaError.VALUE;
}
const numRow = array.length,
numCol = array[0].length;
let det = 0,
diagLeft,
diagRight;
if (numRow === 1) {
return array[0][0] || 0;
} else if (numRow === 2) {
if (!Array.isArray(array[1])) {
throw FormulaError.VALUE;
}
return (
getNum(array[0][0]) * getNum(array[1][1]) -
getNum(array[0][1]) * getNum(array[1][0])
);
}
for (let col = 0; col < numCol; col++) {
diagLeft = getNum(array[0][col]);
diagRight = getNum(array[0][col]);
for (let row = 1; row < numRow; row++) {
const rowArray = array[row];
if (!Array.isArray(rowArray) || rowArray === undefined) {
throw FormulaError.VALUE;
}
diagRight *= getNum(rowArray[(((col + row) % numCol) + numCol) % numCol]);
diagLeft *= getNum(rowArray[(((col - row) % numCol) + numCol) % numCol]);
}
det += diagRight - diagLeft;
}
return det;
});
regFunc('MMULT', (...args: EvalResult[]) => {
const array1 = getNumber2DArray(args[0]);
const array2 = getNumber2DArray(args[1]);
const aNumRows = array1.length,
aNumCols = array1[0].length,
bNumRows = array2.length,
bNumCols = array2[0].length,
m = new Array(aNumRows); // initialize array of rows
if (aNumCols !== bNumRows) {
throw FormulaError.VALUE;
}
for (let r = 0; r < aNumRows; r++) {
m[r] = new Array(bNumCols); // initialize the current row
for (let c = 0; c < bNumCols; c++) {
m[r][c] = 0; // initialize the current cell
for (let i = 0; i < aNumCols; i++) {
const v1 = array1[r][i],
v2 = array2[i][c];
if (typeof v1 !== 'number' || typeof v2 !== 'number') {
throw FormulaError.VALUE;
}
m[r][c] += array1[r][i]! * array2[i][c]!;
}
}
}
return m;
});
regFunc('MOD', (...args: EvalResult[]) => {
const number = getNumberOrThrow(args[0]);
const divisor = getNumberOrThrow(args[1]);
if (divisor === 0) {
throw FormulaError.DIV0;
}
return number - divisor * Math.floor(number / divisor);
});
regFunc('MROUND', (...args: EvalResult[]) => {
const number = getNumberOrThrow(args[0]);
const multiple = getNumberOrThrow(args[1]);
if (multiple === 0) {
return 0;
}
if ((number > 0 && multiple < 0) || (number < 0 && multiple > 0)) {
throw FormulaError.NUM;
}
if ((number / multiple) % 1 === 0) {
return number;
}
return Math.round(number / multiple) * multiple;
});
regFunc('MULTINOMIAL', (...args: EvalResult[]) => {
const numbers = getNumbers(args);
let numerator = 0,
denominator = 1;
for (const num of numbers) {
if (num < 0) {
throw FormulaError.NUM;
}
numerator += num;
denominator *= factorial(num);
}
return factorial(numerator) / denominator;
});
regFunc('MUNIT', (arg: EvalResult) => {
const dimension = parseInt(getNumberOrThrow(arg) + '', 10);
if (dimension < 0) {
throw FormulaError.NUM;
}
if (!dimension || dimension <= 0) {
throw FormulaError.VALUE;
}
return Array(dimension)
.fill(0)
.map(() => Array(dimension).fill(0))
.map((el, i) => {
el[i] = 1;
return el;
});
});
regFunc('ODD', (arg: EvalResult) => {
const number = getNumberOrThrow(arg);
if (number === 0) {
return 1;
}
let temp = Math.ceil(Math.abs(number));
temp = temp & 1 ? temp : temp + 1;
return number > 0 ? temp : -temp;
});
regFunc('PI', () => Math.PI);
regFunc('POWER', (...args: EvalResult[]) => {
const number = getNumberOrThrow(args[0]);
const power = getNumberOrThrow(args[1]);
return number ** power;
});
regFunc('PRODUCT', (...args: EvalResult[]) => {
const numbers = getNumbers(args);
return numbers.reduce((prev, cur) => prev * cur, 1);
});
regFunc('QUOTIENT', (...args: EvalResult[]) => {
const number = getNumberOrThrow(args[0]);
const divisor = getNumberOrThrow(args[1]);
if (divisor === 0) {
throw FormulaError.DIV0;
}
return Math.trunc(number / divisor);
});
regFunc('RADIANS', (arg: EvalResult) => {
const degrees = getNumberOrThrow(arg);
return (degrees * Math.PI) / 180;
});
regFunc('RAND', () => Math.random());
function RANK(number: EvalResult, ref: EvalResult, order: EvalResult) {
const num = getNumberOrThrow(number);
const array = getNumbers([ref]);
const ord = getNumberWithDefault(order, 0);
const sorted = array.slice().sort((a, b) => a - b);
const index = sorted.indexOf(num);
if (index === -1) {
throw FormulaError.NA;
}
return ord === 0 ? array.length - index : index + 1;
}
regFunc('RANK', RANK);
regFunc('RANK.EQ', RANK);
regFunc(
'RANK.AVG',
(number: EvalResult, ref: EvalResult, order: EvalResult) => {
const num = getNumberOrThrow(number);
const array = getNumbers([ref]);
const ord = getNumberWithDefault(order, 0);
const sorted = array.slice().sort((a, b) => a - b);
const index = sorted.indexOf(num);
if (index === -1) {
throw FormulaError.NA;
}
const count = sorted.filter(v => v === num).length;
return ord === 0
? array.length - index - (count - 1) / 2
: index + 1 + (count - 1) / 2;
}
);
regFunc('RANDBETWEEN', (...args: EvalResult[]) => {
const bottom = getNumberOrThrow(args[0]);
const top = getNumberOrThrow(args[1]);
if (bottom > top) {
throw FormulaError.NUM;
}
return Math.floor(Math.random() * (top - bottom + 1) + bottom);
});
regFunc('ROMAN', (...args: EvalResult[]) => {
const number = getNumberOrThrow(args[0]);
const form = getNumber(args[1], 0);
if (form !== 0) {
throw Error('ROMAN: only allows form=0 (classic form).');
}
// The MIT License
// Copyright (c) 2008 Steven Levithan
const digits = String(number).split('');
if (digits.length === 0) {
throw FormulaError.VALUE;
}
const key = [
'',
'C',
'CC',
'CCC',
'CD',
'D',
'DC',
'DCC',
'DCCC',
'CM',
'',
'X',
'XX',
'XXX',
'XL',
'L',
'LX',
'LXX',
'LXXX',
'XC',
'',
'I',
'II',
'III',
'IV',
'V',
'VI',
'VII',
'VIII',
'IX'
];
let roman = '',
i = 3;
while (i--) {
roman = (key[+digits.pop()! + i * 10] || '') + roman;
}
return new Array(+digits.join('') + 1).join('M') + roman;
});
regFunc('ROUND', (...args: EvalResult[]) => {
const number = getNumberOrThrow(args[0]);
const digits = getNumberOrThrow(args[1]);
const multiplier = Math.pow(10, Math.abs(digits));
const sign = number > 0 ? 1 : -1;
if (digits > 0) {
return (sign * Math.round(Math.abs(number) * multiplier)) / multiplier;
} else if (digits === 0) {
return sign * Math.round(Math.abs(number));
} else {
return sign * Math.round(Math.abs(number) / multiplier) * multiplier;
}
});
regFunc('ROUNDDOWN', (...args: EvalResult[]) => {
const number = getNumberOrThrow(args[0]);
const digits = getNumberOrThrow(args[1]);
const multiplier = Math.pow(10, Math.abs(digits));
const sign = number > 0 ? 1 : -1;
if (digits > 0) {
const offset = (1 / multiplier) * 0.5;
return (
(sign * Math.round((Math.abs(number) - offset) * multiplier)) / multiplier
);
} else if (digits === 0) {
const offset = 0.5;
return sign * Math.round(Math.abs(number) - offset);
} else {
const offset = multiplier * 0.5;
return (
sign * Math.round((Math.abs(number) - offset) / multiplier) * multiplier
);
}
});
regFunc('ROUNDUP', (...args: EvalResult[]) => {
const number = getNumberOrThrow(args[0]);
const digits = getNumberOrThrow(args[1]);
const multiplier = Math.pow(10, Math.abs(digits));
const sign = number > 0 ? 1 : -1;
if (digits > 0) {
const offset = (1 / multiplier) * 0.5;
return (
(sign * Math.round((Math.abs(number) + offset) * multiplier)) / multiplier
);
} else if (digits === 0) {
const offset = 0.5;
return sign * Math.round(Math.abs(number) + offset);
} else {
const offset = multiplier * 0.5;
return (
sign * Math.round((Math.abs(number) + offset) / multiplier) * multiplier
);
}
});
regFunc('SERIESSUM', (argX, argN, argM, ...args: EvalResult[]) => {
const x = getNumberOrThrow(argX);
const n = getNumberOrThrow(argN);
const m = getNumberOrThrow(argM);
let i = 0;
let result = 0;
const coefficients = getNumbers(args);
for (const coefficient of coefficients) {
if (i === 0) {
result = coefficient * Math.pow(x, n);
} else {
result += coefficient * Math.pow(x, n + i * m);
}
i++;
}
return result;
});
regFunc('SIGN', (arg: EvalResult) => {
const num = getNumberOrThrow(arg);
return num > 0 ? 1 : num < 0 ? -1 : 0;
});
regFunc('SQRT', (arg: EvalResult) => {
const num = getNumberOrThrow(arg);
if (num < 0) {
throw FormulaError.NUM;
}
return Math.sqrt(num);
});
regFunc('SQRTPI', (arg: EvalResult) => {
const num = getNumberOrThrow(arg);
if (num < 0) {
throw FormulaError.NUM;
}
return Math.sqrt(num * Math.PI);
});
regFunc('SUM', (...args: EvalResult[]) => {
const sum = getNumbers(args).reduce((prev, cur) => prev + cur, 0);
return sum;
});
regFunc(
'SUMIF',
(range: EvalResult, criteria: EvalResult, sumRange: EvalResult) => {
const parsedCriteria = parseCriteria(criteria as string);
const numbers = getArray([range]) as number[];
const sumNumbers = sumRange ? getNumbers([sumRange]) : numbers;
if (numbers.length !== sumNumbers.length) {
throw FormulaError.VALUE;
}
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
if (evalCriterial(parsedCriteria, numbers[i])) {
sum += sumNumbers[i];
}
}
return sum;
}
);
regFunc('SUMIFS', (...args: EvalResult[]) => {
const numbers = evalIFS(args) as number[];
return numbers.reduce((prev, cur) => prev + cur, 0);
});
regFunc('SUMPRODUCT', (array1Arg, ...args: EvalResult[]) => {
const array1 = getNumber2DArray(array1Arg);
const arrays = args.map(arg => getNumber2DArray(arg));
if (arrays.length === 0) {
throw FormulaError.VALUE;
}
for (const array of arrays) {
if (array1[0].length !== array[0].length || array1.length !== array.length)
throw FormulaError.VALUE;
for (let i = 0; i < array1.length; i++) {
for (let j = 0; j < array1[0].length; j++) {
if (typeof array1[i][j] !== 'number') {
array1[i][j] = 0;
}
if (typeof array[i][j] !== 'number') {
array[i][j] = 0;
}
array1[i][j]! *= array[i][j]!;
}
}
}
let result = 0;
array1.forEach(row => {
row.forEach(value => {
result += value || 0;
});
});
return result;
});
regFunc('SUMSQ', (...args: EvalResult[]) => {
const numbers = getNumbers(args);
return numbers.reduce((prev, cur) => prev + cur ** 2, 0);
});
regFunc('SUMX2MY2', (arrayX: EvalResult, arrayY: EvalResult) => {
const x = getNumbers([arrayX]);
const y = getNumbers([arrayY]);
if (x.length !== y.length) {
throw FormulaError.VALUE;
}
return x.reduce((prev, cur, i) => prev + cur ** 2 - y[i] ** 2, 0);
});
regFunc('SUMX2PY2', (arrayX: EvalResult, arrayY: EvalResult) => {
const x = getNumbers([arrayX]);
const y = getNumbers([arrayY]);
if (x.length !== y.length) {
throw FormulaError.VALUE;
}
return x.reduce((prev, cur, i) => prev + cur ** 2 + y[i] ** 2, 0);
});
regFunc('SUMXMY2', (arrayX: EvalResult, arrayY: EvalResult) => {
const x = getNumbers([arrayX]);
const y = getNumbers([arrayY]);
if (x.length !== y.length) {
throw FormulaError.VALUE;
}
return x.reduce((prev, cur, i) => prev + (cur - y[i]) ** 2, 0);
});
regFunc('TRUNC', (arg: EvalResult) => {
const num = getNumberOrThrow(arg);
return num >= 0 ? Math.floor(num) : Math.ceil(num);
});

View File

@ -0,0 +1,461 @@
/**
* fast-formula-parser
*/
import FormulaError from '../FormulaError';
import {EvalResult, isErrorValue} from '../eval/EvalResult';
import {regFunc} from './functions';
import {flattenArgs} from './util/flattenArgs';
import {get2DArray} from './util/get2DArrayOrThrow';
import {getArray} from './util/getArray';
import {getBoolean, getBooleanWithDefault} from './util/getBoolean';
import {getNumberOrThrow, getNumberWithDefault} from './util/getNumber';
import {getNumbers} from './util/getNumbers';
import {
getNumber2DArray,
getNumber2DArrayOrThrow
} from './util/getNumbersWithUndefined';
import {getStringOrThrow, getStringWithDefault} from './util/getString';
import {loopArgs} from './util/loopArgs';
import {WildCard} from './util/wildCard';
function columnNumberToName(number: number) {
let dividend = number;
let name = '';
let modulo = 0;
while (dividend > 0) {
modulo = (dividend - 1) % 26;
name = String.fromCharCode('A'.charCodeAt(0) + modulo) + name;
dividend = Math.floor((dividend - modulo) / 26);
}
return name;
}
regFunc(
'ADDRESS',
(
rowNumber: EvalResult,
columnNumber: EvalResult,
absNum: EvalResult,
a1: EvalResult,
sheetText: EvalResult
) => {
rowNumber = getNumberOrThrow(rowNumber);
columnNumber = getNumberOrThrow(columnNumber);
absNum = getNumberWithDefault(absNum, 1);
a1 = getBooleanWithDefault(a1, true);
sheetText = getStringWithDefault(sheetText, '');
if (rowNumber < 1 || columnNumber < 1 || absNum < 1 || absNum > 4)
throw FormulaError.VALUE;
let result = '';
if (sheetText.length > 0) {
if (/[^A-Za-z_.\d\u007F-\uFFFF]/.test(sheetText)) {
result += `'${sheetText}'!`;
} else {
result += sheetText + '!';
}
}
if (a1) {
// A1 style
result += absNum === 1 || absNum === 3 ? '$' : '';
result += columnNumberToName(columnNumber);
result += absNum === 1 || absNum === 2 ? '$' : '';
result += rowNumber;
} else {
// R1C1 style
result += 'R';
result += absNum === 4 || absNum === 3 ? `[${rowNumber}]` : rowNumber;
result += 'C';
result +=
absNum === 4 || absNum === 2 ? `[${columnNumber}]` : columnNumber;
}
return result;
}
);
regFunc(
'LOOKUP',
(lookupValue: EvalResult, lookupArray: any, resultArray: any) => {
lookupArray = getArray(lookupArray);
resultArray = resultArray ? getArray(resultArray) : lookupArray;
const isNumberLookup = typeof lookupValue === 'number';
let result = 0;
for (let i = 0; i < lookupArray.length; i++) {
if (lookupArray[i] === lookupValue) {
return resultArray[i];
} else if (
(isNumberLookup && lookupArray[i] <= lookupValue) ||
(typeof lookupArray[i] === 'string' &&
lookupArray[i].localeCompare(lookupValue) < 0)
) {
result = resultArray[i];
} else if (isNumberLookup && lookupArray[i] > lookupValue) {
return result;
}
}
return result;
}
);
regFunc(
'HLOOKUP',
(
lookupValue: any, // 目前这两个参数可能有很多类型,后面再优化
tableArray: any,
rowIndexNum: EvalResult,
rangeLookup: EvalResult
) => {
tableArray = get2DArray(tableArray);
if (!Array.isArray(tableArray)) {
throw FormulaError.NA;
}
rowIndexNum = getNumberOrThrow(rowIndexNum);
rangeLookup = getBooleanWithDefault(rangeLookup, true);
// check if rowIndexNum out of bound
if (rowIndexNum < 1) throw FormulaError.VALUE;
if (tableArray[rowIndexNum - 1] === undefined) {
throw FormulaError.REF;
}
const lookupType = typeof lookupValue; // 'number', 'string', 'boolean'
// approximate lookup (assume the array is sorted)
if (rangeLookup) {
let prevValue =
lookupType === typeof tableArray[0][0] ? tableArray[0][0] : null;
for (let i = 1; i < tableArray[0].length; i++) {
const currValue = tableArray[0][i];
const type = typeof currValue;
// skip the value if type does not match
if (type !== lookupType) continue;
// if the previous two values are greater than lookup value, throw #N/A
if (prevValue > lookupValue && currValue > lookupValue) {
throw FormulaError.NA;
}
if (currValue === lookupValue) return tableArray[rowIndexNum - 1][i];
// if previous value <= lookup value and current value > lookup value
if (
prevValue != null &&
currValue > lookupValue &&
prevValue <= lookupValue
) {
return tableArray[rowIndexNum - 1][i - 1];
}
prevValue = currValue;
}
if (prevValue == null) {
throw FormulaError.NA;
}
if (tableArray[0].length === 1) {
return tableArray[rowIndexNum - 1][0];
}
return prevValue;
}
// exact lookup with wildcard support
else {
let index = -1;
if (WildCard.isWildCard(lookupValue)) {
index = tableArray[0].findIndex((item: any) => {
return WildCard.toRegex(lookupValue, 'i').test(item);
});
} else {
index = tableArray[0].findIndex((item: any) => {
return item === lookupValue;
});
}
// the exact match is not found
if (index === -1) throw FormulaError.NA;
return tableArray[rowIndexNum - 1][index];
}
}
);
// 这个函数实现主要来自 formulajs
regFunc(
'MATCH',
(lookupValue: any, lookupArray: any, matchType: EvalResult) => {
lookupArray = getArray(lookupArray);
if (!Array.isArray(lookupArray)) {
throw FormulaError.NA;
}
matchType = getNumberWithDefault(matchType, 1);
if (matchType !== -1 && matchType !== 0 && matchType !== 1) {
throw FormulaError.NA;
}
let index;
let indexValue;
for (let idx = 0; idx < lookupArray.length; idx++) {
if (matchType === 1) {
if (lookupArray[idx] === lookupValue) {
return idx + 1;
} else if (lookupArray[idx] < lookupValue) {
if (!indexValue) {
index = idx + 1;
indexValue = lookupArray[idx];
} else if (lookupArray[idx] > indexValue) {
index = idx + 1;
indexValue = lookupArray[idx];
}
}
} else if (matchType === 0) {
if (
typeof lookupValue === 'string' &&
typeof lookupArray[idx] === 'string'
) {
const lookupValueStr = lookupValue
.toLowerCase()
.replace(/\?/g, '.')
.replace(/\*/g, '.*')
.replace(/~/g, '\\')
.replace(/\+/g, '\\+')
.replace(/\(/g, '\\(')
.replace(/\)/g, '\\)')
.replace(/\[/g, '\\[')
.replace(/\]/g, '\\]');
const regex = new RegExp('^' + lookupValueStr + '$');
if (regex.test(lookupArray[idx].toLowerCase())) {
return idx + 1;
}
} else {
if (lookupArray[idx] === lookupValue) {
return idx + 1;
}
}
} else if (matchType === -1) {
if (lookupArray[idx] === lookupValue) {
return idx + 1;
} else if (lookupArray[idx] > lookupValue) {
if (!indexValue) {
index = idx + 1;
indexValue = lookupArray[idx];
} else if (lookupArray[idx] < indexValue) {
index = idx + 1;
indexValue = lookupArray[idx];
}
}
}
}
return index;
}
);
regFunc('TRANSPOSE', (arg: EvalResult) => {
const array = getNumber2DArrayOrThrow(arg);
// https://github.com/numbers/numbers.js/blob/master/lib/numbers/matrix.js#L171
const result: number[][] = [];
if (!Array.isArray(array)) {
throw FormulaError.VALUE;
}
if (!array.length) {
return [[]];
}
for (let i = 0; i < array[0].length; i++) {
result[i] = [];
for (let j = 0; j < array.length; j++) {
result[i][j] = array[j][i];
}
}
return result;
});
regFunc(
'VLOOKUP',
(
lookupValue: any,
tableArray: any,
colIndexNum: EvalResult,
rangeLookup: EvalResult
) => {
tableArray = get2DArray(tableArray);
if (!Array.isArray(tableArray)) {
throw FormulaError.NA;
}
colIndexNum = getNumberOrThrow(colIndexNum);
rangeLookup = getBooleanWithDefault(rangeLookup, true);
// check if colIndexNum out of bound
if (colIndexNum < 1) throw FormulaError.VALUE;
if (tableArray[0][colIndexNum - 1] === undefined) throw FormulaError.REF;
const lookupType = typeof lookupValue; // 'number', 'string', 'boolean'
// approximate lookup (assume the array is sorted)
if (rangeLookup) {
let prevValue =
lookupType === typeof tableArray[0][0] ? tableArray[0][0] : null;
for (let i = 1; i < tableArray.length; i++) {
const currRow = tableArray[i];
const currValue = tableArray[i][0];
const type = typeof currValue;
// skip the value if type does not match
if (type !== lookupType) continue;
// if the previous two values are greater than lookup value, throw #N/A
if (prevValue > lookupValue && currValue > lookupValue) {
throw FormulaError.NA;
}
if (currValue === lookupValue) return currRow[colIndexNum - 1];
// if previous value <= lookup value and current value > lookup value
if (
prevValue != null &&
currValue > lookupValue &&
prevValue <= lookupValue
) {
return tableArray[i - 1][colIndexNum - 1];
}
prevValue = currValue;
}
if (prevValue == null) throw FormulaError.NA;
if (tableArray.length === 1) {
return tableArray[0][colIndexNum - 1];
}
return prevValue;
}
// exact lookup with wildcard support
else {
let index = -1;
if (WildCard.isWildCard(lookupValue)) {
index = tableArray.findIndex(currRow => {
return WildCard.toRegex(lookupValue, 'i').test(currRow[0]);
});
} else {
index = tableArray.findIndex(currRow => {
return currRow[0] === lookupValue;
});
}
// the exact match is not found
if (index === -1) throw FormulaError.NA;
return tableArray[index][colIndexNum - 1];
}
}
);
function isOneDimensionalArray(array: any[]) {
return !array.every(el => Array.isArray(el)) || array.length === 0;
}
export function fillMatrix(array: number[][] | number[], fill_value?: number) {
if (!array) {
throw FormulaError.VALUE;
}
let matrix: number[][];
// 兼容一维数组的情况
if (isOneDimensionalArray(array)) {
matrix = [[...(array as number[])]];
} else {
matrix = array as number[][];
}
matrix.map((arr, i) => {
arr.map((a, j) => {
if (!a) {
matrix[i][j] = 0;
}
});
});
const longestArrayIndex = matrix.reduce(
(acc, arr, i) => (arr.length > matrix[acc].length ? i : acc),
0
);
const longestArrayLength = matrix[longestArrayIndex].length;
return matrix.map(el => [
...el,
...Array(longestArrayLength - el.length).fill(fill_value ? fill_value : 0)
]);
}
export function transpose(matrix: number[][]) {
if (!matrix) {
throw FormulaError.VALUE;
}
return matrix[0].map((col, i) => matrix.map(row => row[i]));
}
regFunc('SORT', (...args: EvalResult[]) => {
const array = get2DArray(args[0]) as number[][];
let sort_index = getNumberWithDefault(args[1], 1);
const sort_order = getNumberWithDefault(args[2], 1);
const by_col = getBooleanWithDefault(args[3], false);
if (array.length === 0) {
return 0;
}
if (!sort_index || sort_index < 1) {
throw FormulaError.VALUE;
}
if (sort_order !== 1 && sort_order !== -1) {
throw FormulaError.VALUE;
}
const sortArray = (arr: number[][]) =>
arr.sort((a, b) => {
const a_v = a[sort_index - 1];
const b_v = b[sort_index - 1];
return sort_order === 1
? a_v < b_v
? sort_order * -1
: sort_order
: a_v > b_v
? sort_order
: sort_order * -1;
});
const matrix = fillMatrix(array);
const result = by_col ? transpose(matrix) : matrix;
return sort_index >= 1 && sort_index <= result[0].length
? by_col
? transpose(sortArray(result))
: sortArray(result)
: 0;
});
export function UNIQUE(...args: any[]) {
const result = [];
for (let i = 0; i < args.length; ++i) {
let hasElement = false;
const element = args[i];
// Check if we've already seen this element.
for (let j = 0; j < result.length; ++j) {
hasElement = result[j] === element;
if (hasElement) {
break;
}
}
// If we did not find it, add it to the result.
if (!hasElement) {
result.push(element);
}
}
return result;
}
regFunc('UNIQUE', (...args: EvalResult[]) => {
return UNIQUE(...flattenArgs(args));
});

View File

@ -0,0 +1,178 @@
/**
* visitFunction
*/
import {FormulaEnv, getRange} from '../FormulaEnv';
import FormulaError from '../FormulaError';
import {ASTNode} from '../ast/ASTNode';
import {EvalResult} from '../eval/EvalResult';
import {
visitReference,
visitReferenceIgnoreHidden
} from '../eval/visitReference';
import {tokenIsRef} from '../parser/tokenIsRef';
import {functions} from './functions';
export function ISREF(env: FormulaEnv, node: ASTNode): EvalResult {
if (node.children.length === 1) {
const arg = node.children[0];
if (!Array.isArray(arg)) {
return tokenIsRef(arg.token);
} else {
return false;
}
} else {
return false;
}
}
export function AREAS(env: FormulaEnv, node: ASTNode): EvalResult {
if (node.children.length === 1) {
const arg = node.children[0];
if (!Array.isArray(arg)) {
if (arg.type === 'Union') {
return arg.children.length;
} else {
return 1;
}
} else {
return arg.length;
}
} else {
throw FormulaError.VALUE;
}
}
export function SUBTOTAL(env: FormulaEnv, node: ASTNode): EvalResult {
const args = node.children;
if (args.length < 2) {
throw FormulaError.VALUE;
}
const funcNum = parseInt((args[0] as ASTNode).token.value);
const refs = args.slice(1) as ASTNode[];
let values: EvalResult[] = [];
if (funcNum > 100) {
values = refs.map(ref => {
return visitReferenceIgnoreHidden(env, ref);
});
} else {
values = refs.map(ref => {
return visitReference(env, ref);
});
}
switch (funcNum) {
case 1:
case 101:
return functions.get('AVERAGE')!(...values);
case 2:
case 102:
return functions.get('COUNT')!(...values);
case 3:
case 103:
return functions.get('COUNTA')!(...values);
case 4:
case 104:
return functions.get('MAX')!(...values);
case 5:
case 105:
return functions.get('MIN')!(...values);
case 6:
case 106:
return functions.get('PRODUCT')!(...values);
case 7:
case 107:
return functions.get('STDEV')!(...values);
case 8:
case 108:
return functions.get('STDEVP')!(...values);
case 9:
case 109:
return functions.get('SUM')!(...values);
case 10:
case 110:
return functions.get('VAR')!(...values);
case 11:
case 111:
return functions.get('VARP')!(...values);
default:
throw FormulaError.VALUE;
}
}
export function ROW(env: FormulaEnv, node: ASTNode) {
const args = node.children;
if (args.length === 1) {
const ref = args[0] as ASTNode;
if (tokenIsRef(ref.token) && ref.ref) {
const range = getRange(env, ref.ref);
return range.startRow + 1;
}
throw FormulaError.VALUE;
} else {
return env.formulaCell().startRow + 1;
}
}
export function ROWS(env: FormulaEnv, node: ASTNode) {
const args = node.children;
if (args.length === 1) {
const ref = args[0] as ASTNode;
if (tokenIsRef(ref.token) && ref.ref) {
const range = getRange(env, ref.ref);
return range.endRow - range.startRow + 1;
} else if (Array.isArray(ref)) {
return ref.length;
}
throw FormulaError.VALUE;
} else {
throw FormulaError.VALUE;
}
}
export function COLUMN(env: FormulaEnv, node: ASTNode) {
const args = node.children;
if (args.length === 1) {
const ref = args[0] as ASTNode;
if (tokenIsRef(ref.token) && ref.ref) {
const range = getRange(env, ref.ref);
return range.startCol + 1;
}
throw FormulaError.VALUE;
} else {
return env.formulaCell().startCol + 1;
}
}
export function COLUMNS(env: FormulaEnv, node: ASTNode) {
const args = node.children;
if (args.length === 1) {
const ref = args[0] as ASTNode;
if (tokenIsRef(ref.token) && ref.ref) {
const range = getRange(env, ref.ref);
return range.endCol - range.startCol + 1;
} else if (Array.isArray(ref)) {
if (Array.isArray(ref[0])) {
return ref[0].length;
}
return 0;
}
throw FormulaError.VALUE;
} else {
throw FormulaError.VALUE;
}
}
export const specialFunctions = new Map<
string,
(env: FormulaEnv, node: ASTNode) => EvalResult
>();
specialFunctions.set('ISREF', ISREF);
specialFunctions.set('AREAS', AREAS);
specialFunctions.set('SUBTOTAL', SUBTOTAL);
specialFunctions.set('ROW', ROW);
specialFunctions.set('ROWS', ROWS);
specialFunctions.set('COLUMN', COLUMN);
specialFunctions.set('COLUMNS', COLUMNS);

View File

@ -0,0 +1,481 @@
/**
* fast-formula-parser
* https://github.com/LesterLyu/fast-formula-parser
*/
import FormulaError from '../FormulaError';
import {EvalResult} from '../eval/EvalResult';
import {regFunc} from './functions';
import {getNumberOrThrow} from './util/getNumber';
import {getNumbers} from './util/getNumbers';
import {getFirstNumbers} from './util/getFirstNumbers';
import {loopArgs} from './util/loopArgs';
import {parseCriteria} from '../parser/parseCriteria';
import {evalCriterial} from '../eval/evalCriterial';
import {jStat} from './util/jStat';
import {evalIFS} from './util/evalIFS';
import {evalIF} from './util/evalIF';
import {getBooleanWithDefault} from './util/getBoolean';
import {FLOOR} from './math';
regFunc('AVEDEV', (...arg: EvalResult[]) => {
const numbers = getNumbers(arg);
let sum = numbers.reduce((acc, cur) => acc + cur, 0);
const avg = sum / numbers.length;
sum = 0;
for (let i = 0; i < numbers.length; i++) {
sum += Math.abs(numbers[i] - avg);
}
return sum / numbers.length;
});
regFunc('AVERAGE', (...arg: EvalResult[]) => {
const numbers = getFirstNumbers(arg, true);
return numbers.reduce((acc, cur) => acc + cur, 0) / numbers.length;
});
regFunc('AVERAGEIF', (...arg: EvalResult[]) => {
const numbers = evalIF(arg) as number[];
return numbers.reduce((acc, cur) => acc + cur, 0) / numbers.length;
});
regFunc('AVERAGEIFS', (...arg: EvalResult[]) => {
const numbers = evalIFS(arg) as number[];
return numbers.reduce((acc, cur) => acc + cur, 0) / numbers.length;
});
regFunc('AVERAGEA', (...args: EvalResult[]) => {
let sum = 0;
let cnt = 0;
// 目前是瞎试出来的,不确定是不是对的
for (const arg of args) {
if (Array.isArray(arg)) {
// 如果是数组,只有数组里的数字才会参与计算
for (const item of arg) {
if (typeof item === 'number') {
sum += item;
cnt++;
}
if (typeof item === 'string') {
cnt++;
}
}
} else if (typeof arg === 'string') {
sum += parseFloat(arg);
cnt++;
} else if (typeof arg === 'boolean') {
sum += arg ? 1 : 0;
cnt++;
} else if (typeof arg === 'number') {
sum += arg;
cnt++;
}
}
return sum / cnt;
});
regFunc('COUNT', (...args: EvalResult[]) => {
let cnt = 0;
loopArgs(args, arg => {
if (typeof arg === 'number') {
cnt++;
}
if (typeof arg === 'string') {
arg = arg.replace(/%$/, '');
const num = parseFloat(arg);
if (!isNaN(num)) {
cnt++;
}
}
});
return cnt;
});
regFunc('COUNTA', (...args: EvalResult[]) => {
let cnt = 0;
loopArgs(args, arg => {
if (typeof arg !== undefined) {
cnt++;
}
});
return cnt;
});
regFunc('COUNTIF', (range: EvalResult, criteriaArg: EvalResult) => {
let cnt = 0;
const criteria = parseCriteria(criteriaArg as string | number);
loopArgs([range], arg => {
if (typeof arg === 'number' && evalCriterial(criteria, arg)) {
cnt++;
}
if (typeof arg === 'string') {
if (arg.endsWith('%')) {
arg = arg.replace(/%$/, '');
const num = parseFloat(arg);
if (!isNaN(num) && evalCriterial(criteria, num)) {
cnt++;
}
} else if (evalCriterial(criteria, arg)) {
cnt++;
}
}
if (typeof arg === 'boolean' && evalCriterial(criteria, arg)) {
cnt++;
}
});
return cnt;
});
regFunc('COUNTIFS', (...args: EvalResult[]) => {
const range = evalIFS(args);
return range.length;
});
regFunc('COUNTBLANK', (...args: EvalResult[]) => {
let cnt = 0;
loopArgs(args, arg => {
if (arg === undefined || arg === null || arg === '') {
cnt++;
}
});
return cnt;
});
regFunc('LARGE', (...args: EvalResult[]) => {
const numbers = getNumbers([args[0]]);
const k = getNumberOrThrow(args[1]);
if (k < 1) {
throw FormulaError.VALUE;
}
if (k > numbers.length) {
throw FormulaError.NUM;
}
numbers.sort((a, b) => b - a);
return numbers[k - 1];
});
function LINEST(knownY: number[], knownX: number[]) {
const meanY = jStat.mean(knownY);
const meanX = jStat.mean(knownX);
const n = knownX.length;
let num = 0;
let den = 0;
for (let i = 0; i < n; i++) {
num += (knownX[i] - meanX) * (knownY[i] - meanY);
den += Math.pow(knownX[i] - meanX, 2);
}
const m = num / den;
const b = meanY - m * meanX;
return [m, b];
}
regFunc('LINEST', (...args: EvalResult[]) => {
const knownY = getNumbers([args[0]]);
const knownX = getNumbers([args[1]]);
return LINEST(knownY, knownX);
});
regFunc('LOGEST', (...args: EvalResult[]) => {
const knownY = getNumbers([args[0]]);
const knownX = getNumbers([args[1]]);
if (knownY.length !== knownX.length) {
throw FormulaError.VALUE;
}
for (let i = 0; i < knownY.length; i++) {
knownY[i] = Math.log(knownY[i]);
}
const result = LINEST(knownY, knownX);
result[0] = Math.round(Math.exp(result[0]) * 1000000) / 1000000;
result[1] = Math.round(Math.exp(result[1]) * 1000000) / 1000000;
return result;
});
regFunc('TREND', (...arg: EvalResult[]) => {
const knownY = getNumbers([arg[0]]);
const knownX = getNumbers([arg[1]]);
const newValues = getNumbers([arg[2]]);
const linest = LINEST(knownY, knownX);
const m = linest[0];
const b = linest[1];
const result = [];
for (let i = 0; i < newValues.length; i++) {
result.push(m * newValues[i] + b);
}
return result;
});
export function initial(array: number[], idx?: number) {
idx = idx || 1;
if (!array || typeof array.slice !== 'function') {
return array;
}
return array.slice(0, array.length - idx);
}
export function rest(array: number[], idx?: number) {
idx = idx || 1;
if (!array || typeof array.slice !== 'function') {
return array;
}
return array.slice(idx);
}
regFunc('TRIMMEAN', (...args: EvalResult[]) => {
const range = getNumbers([args[0]]);
const percent = getNumberOrThrow(args[1]);
if (percent < 0 || percent > 1) {
throw FormulaError.NUM;
}
const trim = FLOOR(range.length * percent, 2) / 2;
return jStat.mean(
initial(
rest(
range.sort((a, b) => a - b),
trim
),
trim
)
);
});
regFunc('SMALL', (...args: EvalResult[]) => {
const numbers = getNumbers([args[0]]);
const k = getNumberOrThrow(args[1]);
if (k < 1) {
throw FormulaError.VALUE;
}
if (k > numbers.length) {
throw FormulaError.NUM;
}
numbers.sort((a, b) => a - b);
return numbers[k - 1];
});
regFunc('MAX', (...args: EvalResult[]) => {
const numbers = getNumbers(args);
if (numbers.length === 0) {
return 0;
}
return Math.max(...numbers);
});
regFunc('MAXIFS', (...args: EvalResult[]) => {
const numbers = evalIFS(args) as number[];
if (numbers.length === 0) {
return 0;
}
return Math.max(...numbers);
});
regFunc('MAXA', (...args: EvalResult[]) => {
const numbers = getNumbers(args, undefined, true);
if (numbers.length === 0) {
return 0;
}
return Math.max(...numbers);
});
regFunc('MEDIAN', (...args: EvalResult[]) => {
const numbers = getNumbers(args);
if (numbers.length === 0) {
return 0;
}
numbers.sort((a, b) => a - b);
const mid = Math.floor(numbers.length / 2);
if (numbers.length % 2 === 0) {
return (numbers[mid - 1] + numbers[mid]) / 2;
} else {
return numbers[mid];
}
});
regFunc('MIN', (...args: EvalResult[]) => {
const numbers = getNumbers(args);
if (numbers.length === 0) {
return 0;
}
return Math.min(...numbers);
});
regFunc('MINIFS', (...args: EvalResult[]) => {
const numbers = evalIFS(args) as number[];
if (numbers.length === 0) {
return 0;
}
return Math.min(...numbers);
});
regFunc('MINA', (...args: EvalResult[]) => {
const numbers = getNumbers(args, undefined, true);
if (numbers.length === 0) {
return 0;
}
return Math.min(...numbers);
});
function MODE_MULT(range: number[]) {
const n = range.length;
const count: Record<number, number> = {};
let maxItems = [];
let max = 0;
let currentItem;
for (let i = 0; i < n; i++) {
currentItem = range[i];
count[currentItem] = count[currentItem] ? count[currentItem] + 1 : 1;
if (count[currentItem] > max) {
max = count[currentItem];
maxItems = [];
}
if (count[currentItem] === max) {
maxItems[maxItems.length] = currentItem;
}
}
return maxItems;
}
regFunc('MODE.MULT', (...args: EvalResult[]) => {
const range = getNumbers(args);
return MODE_MULT(range);
});
regFunc('MODE.SNGL', (...args: EvalResult[]) => {
const range = getNumbers(args);
if (range instanceof Error) {
return range;
}
return MODE_MULT(range).sort((a, b) => a - b)[0];
});
export function PEARSON(array1: number[], array2: number[]) {
const meanX = jStat.mean(array2);
const meanY = jStat.mean(array1);
const n = array2.length;
let num = 0;
let den1 = 0;
let den2 = 0;
for (let i = 0; i < n; i++) {
num += (array2[i] - meanX) * (array1[i] - meanY);
den1 += Math.pow(array2[i] - meanX, 2);
den2 += Math.pow(array1[i] - meanY, 2);
}
return num / Math.sqrt(den1 * den2);
}
regFunc('PEARSON', (...args: EvalResult[]) => {
const array1 = getNumbers([args[0]]);
const array2 = getNumbers([args[1]]);
return PEARSON(array1, array2);
});
regFunc('PERMUTATIONA', (...args: EvalResult[]) => {
const number = getNumberOrThrow(args[0]);
const number_chosen = getNumberOrThrow(args[1]);
return Math.pow(number, number_chosen);
});
regFunc('PERMUT', (...args: EvalResult[]) => {
const n = getNumberOrThrow(args[0]);
const k = getNumberOrThrow(args[1]);
if (n < 0 || k < 0) {
throw FormulaError.NUM;
}
if (n < k) {
throw FormulaError.NUM;
}
return jStat.factorial(n) / jStat.factorial(n - k);
});
regFunc('RSQ', (...args: EvalResult[]) => {
const knownY = getNumbers([args[0]]);
const knownX = getNumbers([args[1]]);
return Math.pow(PEARSON(knownY, knownX), 2);
});
regFunc('SLOPE', (...args: EvalResult[]) => {
const knownY = getNumbers([args[0]]);
const knownX = getNumbers([args[1]]);
const meanX = jStat.mean(knownX);
const meanY = jStat.mean(knownY);
const n = knownX.length;
let num = 0;
let den = 0;
for (let i = 0; i < n; i++) {
num += (knownX[i] - meanX) * (knownY[i] - meanY);
den += Math.pow(knownX[i] - meanX, 2);
}
return num / den;
});
regFunc('SKEW', (...args: EvalResult[]) => {
const range = getNumbers(args);
const mean = jStat.mean(range);
const n = range.length;
let sigma = 0;
for (let i = 0; i < n; i++) {
sigma += Math.pow(range[i] - mean, 3);
}
return (
(n * sigma) / ((n - 1) * (n - 2) * Math.pow(jStat.stdev(range, true), 3))
);
});
regFunc('SKEW.P', (...args: EvalResult[]) => {
const range = getNumbers(args);
const mean = jStat.mean(range);
const n = range.length;
let m2 = 0;
let m3 = 0;
for (let i = 0; i < n; i++) {
m3 += Math.pow(range[i] - mean, 3);
m2 += Math.pow(range[i] - mean, 2);
}
m3 = m3 / n;
m2 = m2 / n;
return m3 / Math.pow(m2, 3 / 2);
});
export function VARA(numbers: number[]) {
if (numbers.length === 0) {
return 0;
}
return jStat.variance(numbers, true);
}
regFunc('VARA', (...args: EvalResult[]) => {
const numbers = getNumbers(args, undefined, true);
return VARA(numbers);
});
regFunc('VARPA', (...args: EvalResult[]) => {
const numbers = getNumbers(args, undefined, true);
if (numbers.length === 0) {
return 0;
}
return jStat.variance(numbers, false);
});

View File

@ -0,0 +1,448 @@
/**
* fast-formula-parser
* https://github.com/LesterLyu/fast-formula-parser
*/
// @ts-ignore 这个没类型定义
import numfmt from 'numfmt';
import FormulaError from '../FormulaError';
import {EvalResult} from '../eval/EvalResult';
import {regFunc} from './functions';
import {bahttext} from './util/bahttext';
import {full2half, half2full} from './util/convertWidth';
import {getFirstStrings} from './util/getFirstStrings';
import {getNumber, getNumberOrThrow} from './util/getNumber';
import {getString, getStringOrThrow} from './util/getString';
import {getStrings} from './util/getStrings';
import {getBoolean, getBooleanOrThrow} from './util/getBoolean';
import {WildCard} from './util/wildCard';
import {flattenArgs} from './util/flattenArgs';
regFunc('ASC', (arg: EvalResult) => {
const text = getStringOrThrow(arg);
return full2half(text);
});
regFunc('BAHTTEXT', (arg: EvalResult) => {
const number = getNumberOrThrow(arg);
if (number < 0 || number > 999999999.99) {
throw FormulaError.VALUE;
}
return bahttext(number);
});
regFunc('CHAR', (arg: EvalResult) => {
const number = getNumberOrThrow(arg);
if (number < 1 || number > 255) {
throw FormulaError.VALUE;
}
return String.fromCharCode(number);
});
regFunc('CLEAN', (arg: EvalResult) => {
const text = getStringOrThrow(arg);
return text.replace(/[\x00-\x1F]/g, '');
});
regFunc('CODE', (arg: EvalResult) => {
const text = getStringOrThrow(arg);
if (text.length === 0) {
throw FormulaError.VALUE;
}
return text.charCodeAt(0);
});
regFunc('CONCAT', (...args: EvalResult[]) => {
const strings = getStrings(args);
return strings.join('');
});
regFunc('CONCATENATE', (...args: EvalResult[]) => {
const strings = getFirstStrings(args);
return strings.join('');
});
regFunc('DBCS', (arg: EvalResult) => {
const text = getStringOrThrow(arg);
return half2full(text);
});
regFunc('DOLLAR', (...arg: EvalResult[]) => {
const number = getNumberOrThrow(arg[0]);
const decimals = getNumber(arg[1], 2);
const decimalString = Array(decimals).fill('0').join('');
// 还需要支持 locale
const formatter = numfmt(
`$#,##0.${decimalString}_);($#,##0.${decimalString})`
);
return formatter(number).trim();
});
regFunc('EXACT', (arg1: EvalResult, arg2: EvalResult) => {
const text1 = getStringOrThrow(arg1);
const text2 = getStringOrThrow(arg2);
return text1 === text2;
});
regFunc('ENCODEURL', (arg: EvalResult) => {
const text = getStringOrThrow(arg);
return encodeURIComponent(text);
});
function find(...arg: EvalResult[]) {
const findText = getStringOrThrow(arg[0]);
const withinText = getStringOrThrow(arg[1]);
const startNum = getNumber(arg[2], 1)!;
if (startNum < 1 || startNum > withinText.length) {
throw FormulaError.VALUE;
}
const index = withinText.indexOf(findText, startNum - 1);
if (index === -1) {
throw FormulaError.VALUE;
}
return index + 1;
}
regFunc('FIND', find);
regFunc('FINDB', find);
regFunc('FIXED', (...arg: EvalResult[]) => {
const number = getNumberOrThrow(arg[0]);
const decimals = getNumber(arg[1], 2);
const noCommas = getBoolean(arg[2], false);
const decimalString = Array(decimals).fill('0').join('');
const comma = noCommas ? '' : '#,';
const formatter = numfmt(
`${comma}##0.${decimalString}_);(${comma}##0.${decimalString})`
);
return formatter(number).trim();
});
function left(...arg: EvalResult[]) {
const text = getStringOrThrow(arg[0]);
const numChars = getNumber(arg[1], 1)!;
if (numChars < 0) {
throw FormulaError.VALUE;
}
if (numChars > text.length) {
return text;
}
return text.slice(0, numChars);
}
regFunc('LEFT', left);
regFunc('LEFTB', left);
regFunc('LEN', (arg: EvalResult) => {
const text = getStringOrThrow(arg);
return text.length;
});
regFunc('LENB', (arg: EvalResult) => {
const text = getStringOrThrow(arg);
return text.length;
});
regFunc('LOWER', (arg: EvalResult) => {
const text = getStringOrThrow(arg);
return text.toLowerCase();
});
function mid(...arg: EvalResult[]) {
const text = getStringOrThrow(arg[0]);
const startNum = getNumberOrThrow(arg[1]);
const numChars = getNumberOrThrow(arg[2]);
if (startNum > text.length) {
return '';
}
if (startNum < 1 || numChars < 1) {
throw FormulaError.VALUE;
}
return text.slice(startNum - 1, startNum + numChars - 1);
}
regFunc('MID', mid);
regFunc('MIDB', mid);
regFunc('NUMBERVALUE', (...arg: EvalResult[]) => {
const text = getStringOrThrow(arg[0]);
let decimalSeparator = getString(arg[1], '.')!;
let groupSeparator = getString(arg[2], ',')!;
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;
});
regFunc('PROPER', (arg: EvalResult) => {
let text = getString(arg, '')!;
text = text.toLowerCase();
text = text.charAt(0).toUpperCase() + text.slice(1);
return text.replace(/(?:[^a-zA-Z])([a-zA-Z])/g, (letter: string) =>
letter.toUpperCase()
);
});
function replace(...arg: EvalResult[]) {
const oldText = getStringOrThrow(arg[0]);
const startNum = getNumberOrThrow(arg[1]);
const numChars = getNumberOrThrow(arg[2]);
const newText = getStringOrThrow(arg[3]);
if (startNum < 1 || numChars < 0) {
throw FormulaError.VALUE;
}
let arr = oldText.split('');
arr.splice(startNum - 1, numChars, newText);
return arr.join('');
}
regFunc('REPLACE', replace);
regFunc('REPLACEB', replace);
regFunc('REPT', (arg1: EvalResult, arg2: EvalResult) => {
const text = getStringOrThrow(arg1);
const number = getNumberOrThrow(arg2);
if (number < 0) {
throw FormulaError.VALUE;
}
return Array(number).fill(text).join('');
});
function right(...arg: EvalResult[]) {
const text = getStringOrThrow(arg[0]);
const numChars = getNumber(arg[1], 1)!;
if (numChars < 0) {
throw FormulaError.VALUE;
}
if (numChars > text.length) {
return text;
}
return text.slice(-numChars);
}
regFunc('RIGHT', right);
regFunc('RIGHTB', right);
function search(...arg: EvalResult[]) {
const findText = getStringOrThrow(arg[0]);
const withinText = getStringOrThrow(arg[1]);
const startNum = getNumber(arg[2], 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;
}
regFunc('SEARCH', search);
regFunc('SEARCHB', search);
regFunc(
'SUBSTITUTE',
function SUBSTITUTE(
text: string,
old_text: string,
new_text: string,
instance_num: number
) {
if (arguments.length < 3) {
throw FormulaError.NA;
}
if (!text || !old_text) {
return text;
} else if (instance_num === undefined) {
return getString(text)!.split(old_text).join(new_text);
} else {
text = getStringOrThrow(text);
old_text = getStringOrThrow(old_text);
new_text = getStringOrThrow(new_text);
instance_num = Math.floor(Number(instance_num));
if (Number.isNaN(instance_num) || instance_num <= 0) {
throw FormulaError.VALUE;
}
let index = 0;
let i = 0;
while (index > -1 && text.indexOf(old_text, index) > -1) {
index = text.indexOf(old_text, index + 1);
i++;
if (index > -1 && i === instance_num) {
return (
text.substring(0, index) +
new_text +
text.substring(index + old_text.length)
);
}
}
return text;
}
}
);
// TODO: 目前从 cell 里拿到的肯定都是字符串,后续需要区分一下
regFunc('T', (arg: EvalResult) => {
if (typeof arg === 'string') {
return arg;
}
return '';
});
regFunc('TEXT', (arg1: EvalResult, arg2: EvalResult) => {
const value = getNumberOrThrow(arg1);
const formatText = getStringOrThrow(arg2);
try {
return numfmt(formatText)(value);
} catch (e) {
throw FormulaError.VALUE;
}
});
regFunc('TRIM', (arg: EvalResult) => {
const text = getStringOrThrow(arg);
return text.trim();
});
regFunc(
'TEXTJOIN',
function JOIN(
delimiter: EvalResult,
ignore_empty: boolean,
...args: EvalResult[]
) {
let delimiterStr = getString(delimiter);
ignore_empty = getBooleanOrThrow(ignore_empty);
if (arguments.length < 3) {
throw FormulaError.NA;
}
delimiterStr =
delimiterStr !== null && delimiterStr !== undefined ? delimiterStr : '';
let flatArgs = getStrings(args);
let textToJoin = ignore_empty ? flatArgs.filter(text => text) : flatArgs;
if (Array.isArray(delimiter)) {
const delimiterArray = getStrings(delimiter);
let chunks = textToJoin.map(item => [item]);
let index = 0;
for (let i = 0; i < chunks.length - 1; i++) {
chunks[i].push(delimiterArray[index]);
index++;
if (index === delimiterArray.length) {
index = 0;
}
}
const textToJoinResult = flattenArgs(chunks);
return textToJoinResult.join('');
}
return textToJoin.join(delimiterStr);
}
);
regFunc('UNICHAR', (arg: EvalResult) => {
const number = getNumberOrThrow(arg);
if (number < 1 || number > 1114111) {
throw FormulaError.VALUE;
}
return String.fromCodePoint(number);
});
regFunc('UNICODE', (arg: EvalResult) => {
const text = getStringOrThrow(arg);
if (text.length === 0) {
throw FormulaError.VALUE;
}
return text.charCodeAt(0);
});
regFunc('UPPER', (arg: EvalResult) => {
const text = getStringOrThrow(arg);
return text.toUpperCase();
});
regFunc('HYPERLINK', (arg1: EvalResult, arg2: EvalResult) => {
const text = getStringOrThrow(arg1);
const link = getStringOrThrow(arg2);
return {
type: 'Hyperlink',
text,
link
};
});
regFunc('VALUE', (arg: EvalResult) => {
let text = getStringOrThrow(arg);
const isPercent = /(%)$/.test(text) || /^(%)/.test(text);
text = text.replace(/^[^0-9-]{0,3}/, '');
text = text.replace(/[^0-9]{0,3}$/, '');
text = text.replace(/[ ,]/g, '');
if (text === '') {
return 0;
}
let output = Number(text);
if (isNaN(output)) {
throw FormulaError.VALUE;
}
output = output || 0;
if (isPercent) {
output = output * 0.01;
}
return output;
});

View File

@ -0,0 +1,213 @@
/**
* fast-formula-parser
* https://github.com/LesterLyu/fast-formula-parser
*/
import FormulaError from '../FormulaError';
import {EvalResult} from '../eval/EvalResult';
import {functions, regFunc} from './functions';
import {getNumberOrThrow} from './util/getNumber';
const MAX_NUMBER = 2 ** 27 - 1;
regFunc('AGGREGATE', (...args: EvalResult[]) => {
const function_num = getNumberOrThrow(args[0]);
const options = getNumberOrThrow(args[1]);
const ref1 = args[2];
const ref2 = args[3];
switch (function_num) {
case 1:
return functions.get('AVERAGE')!(ref1);
case 2:
return functions.get('COUNT')!(ref1);
case 3:
return functions.get('COUNTA')!(ref1);
case 4:
return functions.get('MAX')!(ref1);
case 5:
return functions.get('MIN')!(ref1);
case 6:
return functions.get('PRODUCT')!(ref1);
case 7:
return functions.get('STDEV.S')!(ref1);
case 8:
return functions.get('STDEV.P')!(ref1);
case 9:
return functions.get('SUM')!(ref1);
case 10:
return functions.get('VAR.S')!(ref1);
case 11:
return functions.get('VAR.P')!(ref1);
case 12:
return functions.get('MEDIAN')!(ref1);
case 13:
return functions.get('MODE.SNGL')!(ref1);
case 14:
return functions.get('LARGE')!(ref1, ref2);
case 15:
return functions.get('SMALL')!(ref1, ref2);
case 16:
return functions.get('PERCENTILE.INC')!(ref1, ref2);
case 17:
return functions.get('QUARTILE.INC')!(ref1, ref2);
case 18:
return functions.get('PERCENTILE.EXC')!(ref1, ref2);
case 19:
return functions.get('QUARTILE.EXC')!(ref1, ref2);
}
throw FormulaError.VALUE;
});
regFunc('ACOS', (arg: EvalResult) => {
const number = getNumberOrThrow(arg);
if (number > 1 || number < -1) {
throw FormulaError.NUM;
}
return Math.acos(number);
});
regFunc('ACOSH', (arg: EvalResult) => {
const number = getNumberOrThrow(arg);
if (number < 1) {
throw FormulaError.NUM;
}
return Math.acosh(number);
});
regFunc('ACOT', (arg: EvalResult) => {
const number = getNumberOrThrow(arg);
return Math.PI / 2 - Math.atan(number);
});
regFunc('ACOTH', (arg: EvalResult) => {
const number = getNumberOrThrow(arg);
if (Math.abs(number) <= 1) {
throw FormulaError.NUM;
}
return Math.atanh(1 / number);
});
regFunc('ASIN', (arg: EvalResult) => {
const number = getNumberOrThrow(arg);
if (number > 1 || number < -1) {
throw FormulaError.NUM;
}
return Math.asin(number);
});
regFunc('ASINH', (arg: EvalResult) => {
const number = getNumberOrThrow(arg);
return Math.asinh(number);
});
regFunc('ATAN', (arg: EvalResult) => {
const number = getNumberOrThrow(arg);
return Math.atan(number);
});
regFunc('ATAN2', (arg1: EvalResult, arg2: EvalResult) => {
const x = getNumberOrThrow(arg1);
const y = getNumberOrThrow(arg2);
if (y === 0 && x === 0) {
throw FormulaError.DIV0;
}
return Math.atan2(y, x);
});
regFunc('ATANH', (arg: EvalResult) => {
const number = getNumberOrThrow(arg);
if (Math.abs(number) > 1) {
throw FormulaError.NUM;
}
return Math.atanh(number);
});
regFunc('COS', (arg: EvalResult) => {
const number = getNumberOrThrow(arg);
if (Math.abs(number) > MAX_NUMBER) {
throw FormulaError.NUM;
}
return Math.cos(number);
});
regFunc('COSH', (arg: EvalResult) => {
const number = getNumberOrThrow(arg);
return Math.cosh(number);
});
regFunc('COT', (arg: EvalResult) => {
const number = getNumberOrThrow(arg);
if (Math.abs(number) > MAX_NUMBER) {
throw FormulaError.NUM;
}
if (number === 0) {
throw FormulaError.DIV0;
}
return 1 / Math.tan(number);
});
regFunc('COTH', (arg: EvalResult) => {
const number = getNumberOrThrow(arg);
if (number === 0) {
throw FormulaError.DIV0;
}
return 1 / Math.tanh(number);
});
regFunc('CSC', (arg: EvalResult) => {
const number = getNumberOrThrow(arg);
if (Math.abs(number) > MAX_NUMBER) {
throw FormulaError.NUM;
}
return 1 / Math.sin(number);
});
regFunc('CSCH', (arg: EvalResult) => {
const number = getNumberOrThrow(arg);
if (number === 0) {
throw FormulaError.DIV0;
}
return 1 / Math.sinh(number);
});
regFunc('SEC', (arg: EvalResult) => {
const number = getNumberOrThrow(arg);
if (Math.abs(number) > MAX_NUMBER) {
throw FormulaError.NUM;
}
return 1 / Math.cos(number);
});
regFunc('SECH', (arg: EvalResult) => {
const number = getNumberOrThrow(arg);
return 1 / Math.cosh(number);
});
regFunc('SIN', (arg: EvalResult) => {
const number = getNumberOrThrow(arg);
if (Math.abs(number) > MAX_NUMBER) {
throw FormulaError.NUM;
}
return Math.sin(number);
});
regFunc('SINH', (arg: EvalResult) => {
const number = getNumberOrThrow(arg);
return Math.sinh(number);
});
regFunc('TAN', (arg: EvalResult) => {
const number = getNumberOrThrow(arg);
if (Math.abs(number) > MAX_NUMBER) {
throw FormulaError.NUM;
}
return Math.tan(number);
});
regFunc('TANH', (arg: EvalResult) => {
const number = getNumberOrThrow(arg);
return Math.tanh(number);
});

View File

@ -0,0 +1,104 @@
export const Factorials = [
1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800, 39916800, 479001600,
6227020800, 87178291200, 1307674368000, 20922789888000, 355687428096000,
6402373705728000, 121645100408832000, 2432902008176640000,
51090942171709440000, 1124000727777607680000, 25852016738884976640000,
620448401733239439360000, 15511210043330985984000000,
403291461126605635584000000, 10888869450418352160768000000,
304888344611713860501504000000, 8841761993739701954543616000000,
265252859812191058636308480000000, 8222838654177922817725562880000000,
263130836933693530167218012160000000, 8683317618811886495518194401280000000,
295232799039604140847618609643520000000,
10333147966386144929666651337523200000000,
371993326789901217467999448150835200000000,
13763753091226345046315979581580902400000000,
523022617466601111760007224100074291200000000,
20397882081197443358640281739902897356800000000,
815915283247897734345611269596115894272000000000,
33452526613163807108170062053440751665152000000000,
1405006117752879898543142606244511569936384000000000,
60415263063373835637355132068513997507264512000000000,
2658271574788448768043625811014615890319638528000000000,
119622220865480194561963161495657715064383733760000000000,
5502622159812088949850305428800254892961651752960000000000,
258623241511168180642964355153611979969197632389120000000000,
12413915592536072670862289047373375038521486354677760000000000,
608281864034267560872252163321295376887552831379210240000000000,
30414093201713378043612608166064768844377641568960512000000000000,
1551118753287382280224243016469303211063259720016986112000000000000,
80658175170943878571660636856403766975289505440883277824000000000000,
4274883284060025564298013753389399649690343788366813724672000000000000,
230843697339241380472092742683027581083278564571807941132288000000000000,
12696403353658275925965100847566516959580321051449436762275840000000000000,
710998587804863451854045647463724949736497978881168458687447040000000000000,
40526919504877216755680601905432322134980384796226602145184481280000000000000,
2350561331282878571829474910515074683828862318181142924420699914240000000000000,
138683118545689835737939019720389406345902876772687432540821294940160000000000000,
8320987112741390144276341183223364380754172606361245952449277696409600000000000000,
507580213877224798800856812176625227226004528988036003099405939480985600000000000000,
31469973260387937525653122354950764088012280797258232192163168247821107200000000000000,
1982608315404440064116146708361898137544773690227268628106279599612729753600000000000000,
126886932185884164103433389335161480802865516174545192198801894375214704230400000000000000,
8247650592082470666723170306785496252186258551345437492922123134388955774976000000000000000,
544344939077443064003729240247842752644293064388798874532860126869671081148416000000000000000,
36471110918188685288249859096605464427167635314049524593701628500267962436943872000000000000000,
2480035542436830599600990418569171581047399201355367672371710738018221445712183296000000000000000,
171122452428141311372468338881272839092270544893520369393648040923257279754140647424000000000000000,
11978571669969891796072783721689098736458938142546425857555362864628009582789845319680000000000000000,
850478588567862317521167644239926010288584608120796235886430763388588680378079017697280000000000000000,
61234458376886086861524070385274672740778091784697328983823014963978384987221689274204160000000000000000,
4470115461512684340891257138125051110076800700282905015819080092370422104067183317016903680000000000000000,
330788544151938641225953028221253782145683251820934971170611926835411235700971565459250872320000000000000000,
24809140811395398091946477116594033660926243886570122837795894512655842677572867409443815424000000000000000000,
1885494701666050254987932260861146558230394535379329335672487982961844043495537923117729972224000000000000000000,
145183092028285869634070784086308284983740379224208358846781574688061991349156420080065207861248000000000000000000,
11324281178206297831457521158732046228731749579488251990048962825668835325234200766245086213177344000000000000000000,
894618213078297528685144171539831652069808216779571907213868063227837990693501860533361810841010176000000000000000000,
71569457046263802294811533723186532165584657342365752577109445058227039255480148842668944867280814080000000000000000000,
5797126020747367985879734231578109105412357244731625958745865049716390179693892056256184534249745940480000000000000000000,
475364333701284174842138206989404946643813294067993328617160934076743994734899148613007131808479167119360000000000000000000,
39455239697206586511897471180120610571436503407643446275224357528369751562996629334879591940103770870906880000000000000000000,
3314240134565353266999387579130131288000666286242049487118846032383059131291716864129885722968716753156177920000000000000000000,
281710411438055027694947944226061159480056634330574206405101912752560026159795933451040286452340924018275123200000000000000000000,
24227095383672732381765523203441259715284870552429381750838764496720162249742450276789464634901319465571660595200000000000000000000,
2107757298379527717213600518699389595229783738061356212322972511214654115727593174080683423236414793504734471782400000000000000000000,
185482642257398439114796845645546284380220968949399346684421580986889562184028199319100141244804501828416633516851200000000000000000000,
16507955160908461081216919262453619309839666236496541854913520707833171034378509739399912570787600662729080382999756800000000000000000000,
1485715964481761497309522733620825737885569961284688766942216863704985393094065876545992131370884059645617234469978112000000000000000000000,
135200152767840296255166568759495142147586866476906677791741734597153670771559994765685283954750449427751168336768008192000000000000000000000,
12438414054641307255475324325873553077577991715875414356840239582938137710983519518443046123837041347353107486982656753664000000000000000000000,
1156772507081641574759205162306240436214753229576413535186142281213246807121467315215203289516844845303838996289387078090752000000000000000000000,
108736615665674308027365285256786601004186803580182872307497374434045199869417927630229109214583415458560865651202385340530688000000000000000000000,
10329978488239059262599702099394727095397746340117372869212250571234293987594703124871765375385424468563282236864226607350415360000000000000000000000,
991677934870949689209571401541893801158183648651267795444376054838492222809091499987689476037000748982075094738965754305639874560000000000000000000000,
96192759682482119853328425949563698712343813919172976158104477319333745612481875498805879175589072651261284189679678167647067832320000000000000000000000,
9426890448883247745626185743057242473809693764078951663494238777294707070023223798882976159207729119823605850588608460429412647567360000000000000000000000,
933262154439441526816992388562667004907159682643816214685929638952175999932299156089414639761565182862536979208272237582511852109168640000000000000000000000,
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
];
const f: number[] = [],
fd: number[] = [];
export function factorial(n: number): number {
if (n <= 100) {
return Factorials[n];
}
if (f[n] > 0) {
return f[n];
}
return (f[n] = factorial(n - 1) * n);
}
export function factorialDouble(n: number): number {
if (n === 1 || n === 0) {
return 1;
}
if (n === 2) {
return 2;
}
if (fd[n] > 0) {
return fd[n];
}
return (fd[n] = factorialDouble(n - 2) * n);
}

View File

@ -0,0 +1,25 @@
import FormulaError from '../../FormulaError';
import {EvalResult} from '../../eval/EvalResult';
import {Factorials, factorial} from './Factorials';
import {getNumberOrThrow} from './getNumber';
export const MathFunctions = {
COMBIN: (number: EvalResult, numberChosen: EvalResult) => {
number = getNumberOrThrow(number);
numberChosen = getNumberOrThrow(numberChosen);
if (number < 0 || numberChosen < 0 || number < numberChosen)
throw FormulaError.NUM;
const nFactorial = MathFunctions.FACT(number),
kFactorial = MathFunctions.FACT(numberChosen);
return nFactorial / kFactorial / MathFunctions.FACT(number - numberChosen);
},
FACT: (number: EvalResult) => {
number = getNumberOrThrow(number);
number = Math.trunc(number);
// max number = 170
if (number > 170 || number < 0) throw FormulaError.NUM;
if (number <= 100) return Factorials[number];
return factorial(number);
}
};

View File

@ -0,0 +1,10 @@
export const TextFunctions = {
REPT: (text: string, number_times: number) => {
let str = '';
for (let i = 0; i < number_times; i++) {
str += text;
}
return str;
}
};

View File

@ -0,0 +1,139 @@
/**
* from https://github.com/jojoee/bahttext/blob/master/src/index.js
* Nathachai Thongniran
* MIT License
*/
const bahtxtConst = {
defaultResult: 'ศูนย์บาทถ้วน',
singleUnitStrs: [
'',
'หนึ่ง',
'สอง',
'สาม',
'สี่',
'ห้า',
'หก',
'เจ็ด',
'แปด',
'เก้า'
],
placeNameStrs: ['', 'สิบ', 'ร้อย', 'พัน', 'หมื่น', 'แสน', 'ล้าน']
};
const GrammarFixs = [
{pat: /หนึ่งสิบ/g, replace: 'สิบ'},
{pat: /สองสิบ/g, replace: 'ยี่สิบ'},
{pat: /สิบหนึ่ง/g, replace: 'สิบเอ็ด'}
];
/**
* @private
* @param {number[]} nums
* @returns {string}
*/
function bahtxtNum2Word(nums: number[]): string {
let result = '';
const len = nums.length;
const maxLen = 7;
if (len > maxLen) {
// more than million
const overflowIndex = len - maxLen + 1;
const overflowNums = nums.slice(0, overflowIndex);
const remainingNumbs = nums.slice(overflowIndex);
return (
bahtxtNum2Word(overflowNums) + 'ล้าน' + bahtxtNum2Word(remainingNumbs)
);
} else {
for (const num in nums) {
const digit = nums[num];
if (digit > 0) {
result +=
bahtxtConst.singleUnitStrs[digit] +
bahtxtConst.placeNameStrs[len - parseInt(num) - 1];
}
}
}
return result;
}
/**
* @private
* @todo improve performance
* @param {string} str
* @returns {string}
*/
function bahtxtGrammarFix(str: string) {
for (const GrammarFix of GrammarFixs) {
str = str.replace(GrammarFix.pat, GrammarFix.replace);
}
return str;
}
/**
* bahtxtCombine baht and satang
* and also adding unit
*
* @private
* @param {string} baht
* @param {string} satang
* @returns {string}
*/
function bahtxtCombine(baht: string, satang: string) {
if (!baht && !satang) {
return bahtxtConst.defaultResult;
} else if (baht && !satang) {
return baht + 'บาท' + 'ถ้วน';
} else if (!baht && satang) {
return satang + 'สตางค์';
} else {
return baht + 'บาท' + satang + 'สตางค์';
}
}
/**
* Change number to Thai pronunciation string
*
* @public
* @param {number} num
* @returns {string}
*/
export function bahttext(num: number): string {
if (
!num || // no null
typeof num === 'boolean' || // no boolean
isNaN(Number(num)) || // must be number only
num < Number.MIN_SAFE_INTEGER || // not less than Number.MIN_SAFE_INTEGER
num > Number.MAX_SAFE_INTEGER // no more than Number.MAX_SAFE_INTEGER
) {
return bahtxtConst.defaultResult;
}
// set
const positiveNum = Math.abs(num);
// split baht and satang e.g. 432.214567 >> 432, 21
const bahtStr = Math.floor(positiveNum).toString();
/** @type {string} */
const satangStr = ((positiveNum % 1) * 100).toFixed(0);
/** @type {number[]} */
const bahtArr = Array.from(bahtStr).map(Number);
/** @type {number[]} */
const satangArr = Array.from(satangStr).map(Number);
// proceed
let baht = bahtxtNum2Word(bahtArr);
let satang = bahtxtNum2Word(satangArr);
// grammar
baht = bahtxtGrammarFix(baht);
satang = bahtxtGrammarFix(satang);
// combine
const result = bahtxtCombine(baht, satang);
return num >= 0 ? result : 'ลบ' + result;
}

View File

@ -0,0 +1,338 @@
/* bessel.js (C) 2013-present SheetJS -- http://sheetjs.com */
/* vim: set ts=2: */
/*exported BESSEL */
var M = Math;
function _horner(arr: number[], v: number) {
for (var i = 0, z = 0; i < arr.length; ++i) z = v * z + arr[i];
return z;
}
function _bessel_iter(
x: number,
n: number,
f0: number,
f1: number,
sign: number
) {
if (n === 0) return f0;
if (n === 1) return f1;
var tdx = 2 / x,
f2 = f1;
for (var o = 1; o < n; ++o) {
f2 = f1 * o * tdx + sign * f0;
f0 = f1;
f1 = f2;
}
return f2;
}
function _bessel_wrap(
bessel0: (x: number) => number,
bessel1: (x: number) => number,
name: string,
nonzero: number,
sign: number
) {
return function bessel(x: number, n: number) {
if (nonzero) {
if (x === 0) return nonzero == 1 ? -Infinity : Infinity;
else if (x < 0) return NaN;
}
if (n === 0) return bessel0(x);
if (n === 1) return bessel1(x);
if (n < 0) return NaN;
n |= 0;
var b0 = bessel0(x),
b1 = bessel1(x);
return _bessel_iter(x, n, b0, b1, sign);
};
}
var besselj = (function () {
var W = 0.636619772; // 2 / Math.PI
var b0_a1a = [
57568490574.0, -13362590354.0, 651619640.7, -11214424.18, 77392.33017,
-184.9052456
].reverse();
var b0_a2a = [
57568490411.0, 1029532985.0, 9494680.718, 59272.64853, 267.8532712, 1.0
].reverse();
var b0_a1b = [
1.0, -0.1098628627e-2, 0.2734510407e-4, -0.2073370639e-5, 0.2093887211e-6
].reverse();
var b0_a2b = [
-0.1562499995e-1, 0.1430488765e-3, -0.6911147651e-5, 0.7621095161e-6,
-0.934935152e-7
].reverse();
function bessel0(x: number) {
var a = 0,
a1 = 0,
a2 = 0,
y = x * x;
if (x < 8) {
a1 = _horner(b0_a1a, y);
a2 = _horner(b0_a2a, y);
a = a1 / a2;
} else {
var xx = x - 0.785398164;
y = 64 / y;
a1 = _horner(b0_a1b, y);
a2 = _horner(b0_a2b, y);
a = M.sqrt(W / x) * (M.cos(xx) * a1 - (M.sin(xx) * a2 * 8) / x);
}
return a;
}
var b1_a1a = [
72362614232.0, -7895059235.0, 242396853.1, -2972611.439, 15704.4826,
-30.16036606
].reverse();
var b1_a2a = [
144725228442.0, 2300535178.0, 18583304.74, 99447.43394, 376.9991397, 1.0
].reverse();
var b1_a1b = [
1.0, 0.183105e-2, -0.3516396496e-4, 0.2457520174e-5, -0.240337019e-6
].reverse();
var b1_a2b = [
0.04687499995, -0.2002690873e-3, 0.8449199096e-5, -0.88228987e-6,
0.105787412e-6
].reverse();
function bessel1(x: number) {
var a = 0,
a1 = 0,
a2 = 0,
y = x * x,
xx = M.abs(x) - 2.356194491;
if (Math.abs(x) < 8) {
a1 = x * _horner(b1_a1a, y);
a2 = _horner(b1_a2a, y);
a = a1 / a2;
} else {
y = 64 / y;
a1 = _horner(b1_a1b, y);
a2 = _horner(b1_a2b, y);
a =
M.sqrt(W / M.abs(x)) *
(M.cos(xx) * a1 - (M.sin(xx) * a2 * 8) / M.abs(x));
if (x < 0) a = -a;
}
return a;
}
return function besselj(x: number, n: number): number {
n = Math.round(n);
if (!isFinite(x)) return isNaN(x) ? x : 0;
if (n < 0) return (n % 2 ? -1 : 1) * besselj(x, -n);
if (x < 0) return (n % 2 ? -1 : 1) * besselj(-x, n);
if (n === 0) return bessel0(x);
if (n === 1) return bessel1(x);
if (x === 0) return 0;
var ret = 0.0;
if (x > n) {
ret = _bessel_iter(x, n, bessel0(x), bessel1(x), -1);
} else {
var m = 2 * M.floor((n + M.floor(M.sqrt(40 * n))) / 2);
var jsum = false;
var bjp = 0.0,
sum = 0.0;
var bj = 1.0,
bjm = 0.0;
var tox = 2 / x;
for (var j = m; j > 0; j--) {
bjm = j * tox * bj - bjp;
bjp = bj;
bj = bjm;
if (M.abs(bj) > 1e10) {
bj *= 1e-10;
bjp *= 1e-10;
ret *= 1e-10;
sum *= 1e-10;
}
if (jsum) sum += bj;
jsum = !jsum;
if (j == n) ret = bjp;
}
sum = 2.0 * sum - bj;
ret /= sum;
}
return ret;
};
})();
var bessely = (function () {
var W = 0.636619772;
var b0_a1a = [
-2957821389.0, 7062834065.0, -512359803.6, 10879881.29, -86327.92757,
228.4622733
].reverse();
var b0_a2a = [
40076544269.0, 745249964.8, 7189466.438, 47447.2647, 226.1030244, 1.0
].reverse();
var b0_a1b = [
1.0, -0.1098628627e-2, 0.2734510407e-4, -0.2073370639e-5, 0.2093887211e-6
].reverse();
var b0_a2b = [
-0.1562499995e-1, 0.1430488765e-3, -0.6911147651e-5, 0.7621095161e-6,
-0.934945152e-7
].reverse();
function bessel0(x: number) {
var a = 0,
a1 = 0,
a2 = 0,
y = x * x,
xx = x - 0.785398164;
if (x < 8) {
a1 = _horner(b0_a1a, y);
a2 = _horner(b0_a2a, y);
a = a1 / a2 + W * besselj(x, 0) * M.log(x);
} else {
y = 64 / y;
a1 = _horner(b0_a1b, y);
a2 = _horner(b0_a2b, y);
a = M.sqrt(W / x) * (M.sin(xx) * a1 + (M.cos(xx) * a2 * 8) / x);
}
return a;
}
var b1_a1a = [
-0.4900604943e13, 0.127527439e13, -0.5153438139e11, 0.7349264551e9,
-0.4237922726e7, 0.8511937935e4
].reverse();
var b1_a2a = [
0.249958057e14, 0.4244419664e12, 0.3733650367e10, 0.2245904002e8,
0.102042605e6, 0.3549632885e3, 1
].reverse();
var b1_a1b = [
1.0, 0.183105e-2, -0.3516396496e-4, 0.2457520174e-5, -0.240337019e-6
].reverse();
var b1_a2b = [
0.04687499995, -0.2002690873e-3, 0.8449199096e-5, -0.88228987e-6,
0.105787412e-6
].reverse();
function bessel1(x: number) {
var a = 0,
a1 = 0,
a2 = 0,
y = x * x,
xx = x - 2.356194491;
if (x < 8) {
a1 = x * _horner(b1_a1a, y);
a2 = _horner(b1_a2a, y);
a = a1 / a2 + W * (besselj(x, 1) * M.log(x) - 1 / x);
} else {
y = 64 / y;
a1 = _horner(b1_a1b, y);
a2 = _horner(b1_a2b, y);
a = M.sqrt(W / x) * (M.sin(xx) * a1 + (M.cos(xx) * a2 * 8) / x);
}
return a;
}
return _bessel_wrap(bessel0, bessel1, 'BESSELY', 1, -1);
})();
var besseli = (function () {
var b0_a = [
1.0, 3.5156229, 3.0899424, 1.2067492, 0.2659732, 0.360768e-1, 0.45813e-2
].reverse();
var b0_b = [
0.39894228, 0.1328592e-1, 0.225319e-2, -0.157565e-2, 0.916281e-2,
-0.2057706e-1, 0.2635537e-1, -0.1647633e-1, 0.392377e-2
].reverse();
function bessel0(x: number) {
if (x <= 3.75) return _horner(b0_a, (x * x) / (3.75 * 3.75));
return (
(M.exp(M.abs(x)) / M.sqrt(M.abs(x))) * _horner(b0_b, 3.75 / M.abs(x))
);
}
var b1_a = [
0.5, 0.87890594, 0.51498869, 0.15084934, 0.2658733e-1, 0.301532e-2,
0.32411e-3
].reverse();
var b1_b = [
0.39894228, -0.3988024e-1, -0.362018e-2, 0.163801e-2, -0.1031555e-1,
0.2282967e-1, -0.2895312e-1, 0.1787654e-1, -0.420059e-2
].reverse();
function bessel1(x: number) {
if (x < 3.75) return x * _horner(b1_a, (x * x) / (3.75 * 3.75));
return (
(((x < 0 ? -1 : 1) * M.exp(M.abs(x))) / M.sqrt(M.abs(x))) *
_horner(b1_b, 3.75 / M.abs(x))
);
}
return function besseli(x: number, n: number) {
n = Math.round(n);
if (n === 0) return bessel0(x);
if (n === 1) return bessel1(x);
if (n < 0) return NaN;
if (M.abs(x) === 0) return 0;
if (x == Infinity) return Infinity;
var ret = 0.0,
j,
tox = 2 / M.abs(x),
bip = 0.0,
bi = 1.0,
bim = 0.0;
var m = 2 * M.round((n + M.round(M.sqrt(40 * n))) / 2);
for (j = m; j > 0; j--) {
bim = j * tox * bi + bip;
bip = bi;
bi = bim;
if (M.abs(bi) > 1e10) {
bi *= 1e-10;
bip *= 1e-10;
ret *= 1e-10;
}
if (j == n) ret = bip;
}
ret *= besseli(x, 0) / bi;
return x < 0 && n % 2 ? -ret : ret;
};
})();
var besselk = (function () {
var b0_a = [
-0.57721566, 0.4227842, 0.23069756, 0.348859e-1, 0.262698e-2, 0.1075e-3,
0.74e-5
].reverse();
var b0_b = [
1.25331414, -0.7832358e-1, 0.2189568e-1, -0.1062446e-1, 0.587872e-2,
-0.25154e-2, 0.53208e-3
].reverse();
function bessel0(x: number) {
if (x <= 2)
return -M.log(x / 2) * besseli(x, 0) + _horner(b0_a, (x * x) / 4);
return (M.exp(-x) / M.sqrt(x)) * _horner(b0_b, 2 / x);
}
var b1_a = [
1.0, 0.15443144, -0.67278579, -0.18156897, -0.1919402e-1, -0.110404e-2,
-0.4686e-4
].reverse();
var b1_b = [
1.25331414, 0.23498619, -0.365562e-1, 0.1504268e-1, -0.780353e-2,
0.325614e-2, -0.68245e-3
].reverse();
function bessel1(x: number) {
if (x <= 2)
return (
M.log(x / 2) * besseli(x, 1) + (1 / x) * _horner(b1_a, (x * x) / 4)
);
return (M.exp(-x) / M.sqrt(x)) * _horner(b1_b, 2 / x);
}
return _bessel_wrap(bessel0, bessel1, 'BESSELK', 2, 1);
})();
export const bessel = {besselj, bessely, besseli, besselk};

View File

@ -0,0 +1,192 @@
/**
*
* Width Converter
* Copyright 2017 Yanbin Ma under MIT
* https://github.com/myanbin/hwfw-convert
*/
const CODEPOINT_BASE = '\uff10'.codePointAt(0)! - '0'.codePointAt(0)!;
const CJK_PUNCTUATIONS = [
0xff0c /* */, 0x3002 /* 。 */, 0xff01 /* */, 0xff08 /* */,
0xff09 /* */, 0x3001 /* 、 */, 0xff1a /* */, 0xff1b /* */,
0xff1f /* */, 0xff3b /* */, 0xff3d /* */, 0xff5e /* */,
0x2018 /* */, 0x2019 /* */, 0x201c /* “ */, 0x201d /* ” */,
0x300a /* 《 */, 0x300b /* 》 */, 0x3008 /* 〈 */, 0x3009 /* 〉 */,
0x3010 /* 【 */, 0x3011 /* 】 */
];
const LATIN_PUNCTUATIONS = [
0x2c /* , */, 0x2e /* . */, 0x21 /* ! */, 0x28 /* ( */, 0x29 /* ) */,
0x2c /* , */, 0x3a /* : */, 0x3b /* ; */, 0x3f /* ? */, 0x5b /* [ */,
0x5d /* ] */, 0x7e /* ~ */, 0x27 /* ' */, 0x27 /* ' */, 0x22 /* " */,
0x22 /* " */, 0xab /* « */, 0xbb /* » */, 0x2039 /* */, 0x203a /* */,
0x5b /* [ */, 0x5d /* ] */
];
/* Reference: https://en.wikipedia.org/wiki/Halfwidth_and_fullwidth_forms */
const FULL_SYMBOLS = [
0xff02 /* */, 0xff03 /* */, 0xff04 /* */, 0xff05 /* */,
0xff06 /* */, 0xff07 /* */, 0xff0a /* */, 0xff0b /* */,
0xff0d /* */, 0xff0e /* */, 0xff0f /* */, 0xff1c /* */,
0xff1d /* */, 0xff1e /* */, 0xff20 /* */, 0xff3c /* */,
0xff3e /* */, 0xff3f /* _ */, 0xff40 /* */, 0xff5b /* */,
0xff5c /* */, 0xff5d /* */
];
const HALF_SYMBOLS = FULL_SYMBOLS.map(function (codePoint) {
return codePoint - CODEPOINT_BASE;
});
function _mergeOptions(_options: object) {
const defaultOptions = {
digit: true, // 将全角数字转换成半角
alpha: true, // 将全角字母转换成半角
space: true, // 将全角空格转换成半角
symbol: true, // 将全角的 #、$、%、& 等特殊字符转换成半角(不包括中文标点符号)
punctuation: false, // 将中文标点符号转换成对应英文标点符号(在中文环境中不推荐使用)
smartMode: true // 智能排除模式。可以识别出数值、网址等内容并进行精确转换
};
return Object.assign(defaultOptions, _options);
}
/**
* Full width to Half width Tramsformer
* @param {string} source Source text (full width)
* @param {object} options Options
*/
export function full2half(source: string, options: object = {}) {
const sourceSize = source.length;
const _options = _mergeOptions(options);
let output = [];
for (let index = 0; index < sourceSize; index++) {
const codePoint = source.codePointAt(index)!;
if (
/* Digit Flag = */ _options.digit &&
codePoint >= 0xff10 &&
codePoint <= 0xff19
) {
output[index] = String.fromCodePoint(codePoint - CODEPOINT_BASE);
} else if (
/* Alpha Flag = */ _options.alpha &&
((codePoint >= 0xff21 && codePoint <= 0xff3a) ||
(codePoint >= 0xff41 && codePoint <= 0xff5a))
) {
output[index] = String.fromCodePoint(codePoint - CODEPOINT_BASE);
} else if (
/* Symbol Flag */ _options.symbol &&
FULL_SYMBOLS.indexOf(codePoint) !== -1
) {
output[index] = String.fromCodePoint(codePoint - CODEPOINT_BASE);
} else if (
/* Space Flag = */ _options.space &&
codePoint === 0x3000 /* Fullwidth Space */
) {
output[index] = String.fromCodePoint(0x0020);
} else {
output[index] = source[index];
}
if (
/* Punctuation Flag = */ _options.punctuation &&
CJK_PUNCTUATIONS.indexOf(codePoint) !== -1
) {
output[index] = String.fromCodePoint(
LATIN_PUNCTUATIONS[CJK_PUNCTUATIONS.indexOf(codePoint)]
);
}
}
let destination = output.join('');
if (/* Smart Mode = */ _options.smartMode) {
if (/* Digit Flag = */ _options.digit) {
destination = destination.replace(/\d[\uff0c]\d/g, function (match) {
return match.replace(/[\uff0c]/, ',');
});
destination = destination.replace(/\d\d[\uff1a]\d\d/g, function (match) {
return match.replace(/[\uff1a]/, ':');
});
destination = destination.replace(/\d[\uff0e]\d/g, function (match) {
return match.replace(/[\uff0e]/, '.');
});
destination = destination.replace(/\d[\u3002]\d/g, function (match) {
return match.replace(/[\uff0e]/, '.');
});
}
if (/* Symbol Flag */ _options.symbol) {
destination = destination.replace(/https?[\uff1a]/g, function (match) {
return match.replace(/[\uff1a]/, ':');
});
}
}
return destination;
}
/**
* Half width to Full width Tramsformer
* @param {string} source Source text (half width)
* @param {object} options Options
*/
export function half2full(source: string, options: object = {}) {
const sourceSize = source.length;
const _options = _mergeOptions(options);
let output = [];
for (let index = 0; index < sourceSize; index++) {
const codePoint = source.codePointAt(index)!;
if (
/* Digit Flag = */ _options.digit &&
codePoint >= 0x0030 &&
codePoint <= 0x0039
) {
output[index] = String.fromCodePoint(codePoint + CODEPOINT_BASE);
} else if (
/* Alpha Flag = */ _options.alpha &&
((codePoint >= 0x0041 && codePoint <= 0x005a) ||
(codePoint >= 0x0061 && codePoint <= 0x007a))
) {
output[index] = String.fromCodePoint(codePoint + CODEPOINT_BASE);
} else if (
/* Symbol Flag */ _options.symbol &&
HALF_SYMBOLS.indexOf(codePoint) !== -1
) {
output[index] = String.fromCodePoint(codePoint + CODEPOINT_BASE);
} else if (
/* Space Flag = */ _options.space &&
codePoint === 0x0020 /* Halfwidth Space */
) {
output[index] = String.fromCodePoint(0x3000);
} else {
output[index] = source[index];
}
if (
/* Punctuation Flag = */ _options.punctuation &&
LATIN_PUNCTUATIONS.indexOf(codePoint) !== -1
) {
output[index] = String.fromCodePoint(
CJK_PUNCTUATIONS[LATIN_PUNCTUATIONS.indexOf(codePoint)]
);
}
}
let destination = output.join('');
if (/* Smart Mode = */ _options.smartMode) {
if (/* Digit Flag = */ _options.digit) {
destination = destination.replace(/\d[,\uff0c]\d{3}/g, function (match) {
return match.replace(/[,\uff0c]/, String.fromCodePoint(0xff0c));
});
destination = destination.replace(
/\d\d[:\uff1a]]\d\d/g,
function (match) {
return match.replace(/[:\uff1a]/, String.fromCodePoint(0xff1a));
}
);
destination = destination.replace(/\d[.\u3002]]\d/g, function (match) {
return match.replace(/[.\u3002]/, String.fromCodePoint(0xff0e));
});
}
if (/* Symbol Flag */ _options.symbol) {
destination = destination.replace(/https?[:\uff1a]/g, function (match) {
return match.replace(/[:\uff1a]/, String.fromCodePoint(0xff1a));
});
}
}
return destination;
}

View File

@ -0,0 +1,24 @@
import {EvalResult} from '../../eval/EvalResult';
import {evalCriterial} from '../../eval/evalCriterial';
import {parseCriteria} from '../../parser/parseCriteria';
import {getArray} from './getArray';
export function evalIF(args: EvalResult[]): EvalResult[] {
const range = getArray(args.shift());
const criteria = args.shift();
const testRange = args[2] ? getArray(args[2]) : range;
const values: EvalResult[] = [];
for (let i = 0; i < range.length; i++) {
const valueToTest = testRange[i];
const parsedCriteria = parseCriteria(criteria as string);
if (evalCriterial(parsedCriteria, valueToTest as string)) {
values.push(valueToTest);
}
}
return values;
}

View File

@ -0,0 +1,57 @@
import FormulaError from '../../FormulaError';
import {EvalResult} from '../../eval/EvalResult';
import {evalCriterial} from '../../eval/evalCriterial';
import {parseCriteria} from '../../parser/parseCriteria';
import {flattenArgs} from './flattenArgs';
import {getArray} from './getArray';
import {getString} from './getString';
export function evalIFS(args: EvalResult[]): EvalResult[] {
const range = getArray(args.shift());
const criteriaes = args;
const criteriaLength = criteriaes.length / 2;
if (criteriaLength === 0) {
throw FormulaError.VALUE;
}
if (criteriaes.length !== 1 && criteriaes.length % 2 !== 0) {
throw FormulaError.VALUE;
}
if (criteriaLength > 1) {
for (let i = 0; i < criteriaLength; i++) {
criteriaes[i * 2] = flattenArgs([criteriaes[i * 2]]);
}
}
if (criteriaes.length === 1) {
criteriaes[1] = criteriaes[0];
criteriaes[0] = range;
}
let values: EvalResult[] = [];
for (let i = 0; i < range.length; i++) {
let isMetCondition = false;
for (let j = 0; j < criteriaLength; j++) {
const valueToTest = (criteriaes[j * 2] as EvalResult[])[i];
const criteria = criteriaes[j * 2 + 1];
const parsedCriteria = parseCriteria(criteria as string);
if (evalCriterial(parsedCriteria, valueToTest as string)) {
isMetCondition = true;
} else {
isMetCondition = false;
break;
}
}
if (isMetCondition) {
values.push(range[i]);
}
}
return values;
}

View File

@ -0,0 +1,22 @@
import {EvalResult, UnionValue, isUnionValue} from '../../eval/EvalResult';
/**
*
*/
export function flattenArgs(args: EvalResult[]): EvalResult[] {
const result: EvalResult[] = [];
for (const arg of args) {
if (Array.isArray(arg)) {
for (const item of arg) {
result.push(...flattenArgs([item]));
}
} else if (isUnionValue(arg)) {
result.push(...flattenArgs((arg as UnionValue).children));
} else {
result.push(arg);
}
}
return result;
}

View File

@ -0,0 +1,33 @@
import FormulaError from '../../FormulaError';
import {EvalResult} from '../../eval/EvalResult';
/**
*
* @param args
* @returns
*/
export function get2DArrayOrThrow(args: EvalResult): EvalResult[][] {
if (Array.isArray(args)) {
if (Array.isArray(args[0])) {
return args as EvalResult[][];
}
}
throw FormulaError.VALUE;
}
/**
*
* @param args
* @returns
*/
export function get2DArray(args: EvalResult): EvalResult[][] | undefined {
if (Array.isArray(args)) {
if (Array.isArray(args[0])) {
return args as EvalResult[][];
} else {
return [args];
}
} else {
return undefined;
}
}

View File

@ -0,0 +1,10 @@
import {EvalResult} from '../../eval/EvalResult';
import {flattenArgs} from './flattenArgs';
export function getArray(arg: EvalResult): EvalResult[] {
if (Array.isArray(arg)) {
return flattenArgs(arg);
} else {
return [arg];
}
}

View File

@ -0,0 +1,27 @@
import {EvalResult} from '../../eval/EvalResult';
import {getNumber} from './getNumber';
/**
*
*
* @param args
* @param process
* @returns
*/
export function getArrayNumbers(args: EvalResult): number[] {
const numbers: number[] = [];
if (Array.isArray(args)) {
for (const arg of args) {
const num = getNumber(arg);
if (num !== undefined) {
numbers.push(num);
}
}
} else {
let num = getNumber(args);
if (num !== undefined) {
numbers.push(num);
}
}
return numbers;
}

View File

@ -0,0 +1,53 @@
import FormulaError from '../../FormulaError';
import {EvalResult} from '../../eval/EvalResult';
/**
*
*
* @param arg
* @param defaultValue
* @returns
*/
export function getBoolean(
arg: EvalResult,
defaultValue?: boolean
): boolean | undefined {
if (typeof arg === 'number') {
if (arg === 0) {
return false;
}
return true;
}
if (typeof arg === 'string') {
if (arg === 'TRUE') {
return true;
}
if (arg === 'FALSE') {
return false;
}
}
if (typeof arg === 'boolean') {
return arg;
}
if (defaultValue !== undefined) {
return defaultValue;
}
return undefined;
}
export function getBooleanOrThrow(arg: EvalResult): boolean {
const result = getBoolean(arg);
if (result === undefined) {
throw new FormulaError('VALUE');
}
return result;
}
export function getBooleanWithDefault(
arg: EvalResult,
defaultValue: boolean
): boolean {
return getBoolean(arg, defaultValue)!;
}

View File

@ -0,0 +1,12 @@
import FormulaError from '../../FormulaError';
import {EvalResult} from '../../eval/EvalResult';
export function getDateOrThrow(arg: EvalResult): Date {
if (Array.isArray(arg)) {
return getDateOrThrow(arg[0]);
}
if (arg instanceof Date) {
return arg;
}
throw FormulaError.VALUE;
}

View File

@ -0,0 +1,28 @@
import {EvalResult} from '../../eval/EvalResult';
import {getNumber} from './getNumber';
import {getNumbers} from './getNumbers';
/**
*
*/
export function getFirstNumbers(
args: EvalResult[],
booleanToNumber: boolean
): number[] {
if (args.length === 1 && Array.isArray(args[0])) {
return getNumbers(args[0], undefined, booleanToNumber);
}
const numbers: number[] = [];
for (let arg of args) {
if (Array.isArray(arg)) {
if (arg.length > 0) {
arg = arg[0];
}
}
const num = getNumber(arg, undefined, booleanToNumber);
if (num !== undefined) {
numbers.push(num);
}
}
return numbers;
}

View File

@ -0,0 +1,21 @@
import {EvalResult} from '../../eval/EvalResult';
import {getString} from './getString';
/**
*
*/
export function getFirstStrings(args: EvalResult[]): string[] {
const strings: string[] = [];
for (let arg of args) {
if (Array.isArray(arg)) {
if (arg.length > 0) {
arg = arg[0];
}
}
const str = getString(arg);
if (str !== undefined) {
strings.push(str);
}
}
return strings;
}

View File

@ -0,0 +1,79 @@
import FormulaError from '../../FormulaError';
import {EvalResult} from '../../eval/EvalResult';
/**
*
*
* @param arg
* @param defaultValue
* @returns
*/
export function getNumber(
arg: EvalResult,
defaultValue?: number,
booleanToNumber = false
): number | undefined {
if (Array.isArray(arg)) {
return getNumber(arg[0], defaultValue, booleanToNumber);
}
if (typeof arg === 'number') {
if (!isFinite(arg)) {
throw FormulaError.NUM;
}
return arg;
}
if (typeof arg === 'boolean') {
return booleanToNumber ? (arg ? 1 : 0) : undefined;
}
if (typeof arg === 'string') {
if (arg.endsWith('%')) {
const num = Number(arg.slice(0, -1));
if (!isNaN(num)) {
if (!isFinite(num)) {
throw FormulaError.NUM;
}
return num / 100;
} else {
return undefined;
}
} else {
// 这里不能用 parseInt在 Excel 中 1a 会被认为不是数字
const num = Number(arg);
if (!isNaN(num)) {
if (!isFinite(num)) {
throw FormulaError.NUM;
}
return num;
} else {
return undefined;
}
}
}
if (defaultValue !== undefined) {
return defaultValue;
}
return undefined;
}
export function getNumberOrThrow(
arg: EvalResult,
booleanToNumber = false
): number {
if (Array.isArray(arg)) {
arg = arg[0];
}
const number = getNumber(arg, undefined, booleanToNumber);
if (number === undefined) {
throw FormulaError.VALUE;
}
return number;
}
export function getNumberWithDefault(
arg: EvalResult,
defaultValue: number
): number {
return getNumber(arg, defaultValue)!;
}

Some files were not shown because too many files have changed in this diff Show More