Files
excel/src/utils/formula.js
2026-01-22 17:00:24 +08:00

226 lines
8.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 将列索引 (0, 1, 2...) 转换为字母 (A, B, C...)
*/
export const indexToColName = (index) => {
let label = ''
let i = index
while (i >= 0) {
label = String.fromCharCode(65 + (i % 26)) + label
i = Math.floor(i / 26) - 1
}
return label
}
/**
* 将坐标 (A1) 解析为 {r, c}
*/
export const parseCellCoord = (coord) => {
const match = coord.match(/^([A-Z]+)([0-9]+)$/)
if (!match) return null
const colStr = match[1]
const rowStr = match[2]
let c = 0
for (let i = 0; i < colStr.length; i++) {
c = c * 26 + (colStr.charCodeAt(i) - 64)
}
return { r: parseInt(rowStr) - 1, c: c - 1 }
}
/**
* 函数注册表:包含实现逻辑、描述和语法提示
* 用于公式计算和 UI 提示
*/
export const FUNCTION_REGISTRY = {
// --- 数学与统计 ---
SUM: {
description: '计算单元格区域中所有数值的和',
syntax: 'SUM(number1, [number2], ...)',
impl: (...args) => {
return args.flat(Infinity).reduce((a, b) => {
const num = Number(b)
return isNaN(num) ? a : a + num
}, 0)
}
},
AVERAGE: {
description: '返回参数的平均值(算术平均值)',
syntax: 'AVERAGE(number1, [number2], ...)',
impl: (...args) => {
const nums = args.flat(Infinity).map(Number).filter(n => !isNaN(n))
if (nums.length === 0) return 0
return nums.reduce((a, b) => a + b, 0) / nums.length
}
},
MAX: {
description: '返回一组值中的最大值',
syntax: 'MAX(number1, [number2], ...)',
impl: (...args) => {
const nums = args.flat(Infinity).map(Number).filter(n => !isNaN(n))
return nums.length ? Math.max(...nums) : 0
}
},
MIN: {
description: '返回一组值中的最小值',
syntax: 'MIN(number1, [number2], ...)',
impl: (...args) => {
const nums = args.flat(Infinity).map(Number).filter(n => !isNaN(n))
return nums.length ? Math.min(...nums) : 0
}
},
COUNT: {
description: '计算区域中包含数字的单元格个数',
syntax: 'COUNT(value1, [value2], ...)',
impl: (...args) => {
return args.flat(Infinity).filter(v => typeof v === 'number' || (typeof v === 'string' && !isNaN(Number(v)) && v.trim() !== '')).length
}
},
ROUND: {
description: '将数字四舍五入到指定的位数',
syntax: 'ROUND(number, num_digits)',
impl: (number, digits = 0) => {
const n = Number(number)
const d = Number(digits)
if (isNaN(n) || isNaN(d)) return '#VALUE!'
return Math.round(n * (10 ** d)) / (10 ** d)
}
},
ABS: {
description: '返回数字的绝对值',
syntax: 'ABS(number)',
impl: (number) => Math.abs(Number(number))
},
// --- 逻辑 ---
IF: {
description: '判断是否满足某个条件,如果满足返回一个值,如果不满足返回另一个值',
syntax: 'IF(logical_test, value_if_true, [value_if_false])',
impl: (condition, trueVal, falseVal) => {
return condition ? trueVal : (falseVal === undefined ? false : falseVal)
}
},
AND: {
description: '如果所有参数都为 TRUE则返回 TRUE',
syntax: 'AND(logical1, [logical2], ...)',
impl: (...args) => args.flat(Infinity).every(Boolean)
},
OR: {
description: '如果任一参数为 TRUE则返回 TRUE',
syntax: 'OR(logical1, [logical2], ...)',
impl: (...args) => args.flat(Infinity).some(Boolean)
},
// --- 文本 ---
CONCAT: {
description: '将两个或多个文本字符串联接为一个字符串',
syntax: 'CONCAT(text1, [text2], ...)',
impl: (...args) => args.flat(Infinity).join('')
},
LEN: {
description: '返回文本字符串中的字符个数',
syntax: 'LEN(text)',
impl: (text) => String(text || '').length
},
TRIM: {
description: '移除文本中多余的空格',
syntax: 'TRIM(text)',
impl: (text) => String(text || '').trim()
},
UPPER: {
description: '将文本转换为大写',
syntax: 'UPPER(text)',
impl: (text) => String(text || '').toUpperCase()
},
LOWER: {
description: '将文本转换为小写',
syntax: 'LOWER(text)',
impl: (text) => String(text || '').toLowerCase()
},
// --- 日期与时间 ---
NOW: {
description: '返回当前日期和时间',
syntax: 'NOW()',
impl: () => new Date().toLocaleString()
},
TODAY: {
description: '返回当前日期',
syntax: 'TODAY()',
impl: () => new Date().toLocaleDateString()
}
}
/**
* 简易公式求值器
* @param {String} formula 公式字符串 (e.g. "=SUM(A1:B2) + 10")
* @param {Function} getValueCb 回调函数,用于获取指定单元格的值 (r, c) => val
*/
export const evaluateFormula = (formula, getValueCb) => {
if (!formula || typeof formula !== 'string' || !formula.startsWith('=')) return formula
const expression = formula.substring(1).trim()
if (!expression) return ''
// 1. 预处理范围引用:将 A1:B2 转换为数值数组 [1, 2, 3, 4]
// 注意:为了支持文本函数,如果单元格内容是文本,也需要正确提取
const rangeRegex = /([A-Z]+[0-9]+):([A-Z]+[0-9]+)/g
const parsedExpressionWithRanges = expression.replace(rangeRegex, (match, start, end) => {
const s = parseCellCoord(start)
const e = parseCellCoord(end)
if (!s || !e) return match
const values = []
const minR = Math.min(s.r, e.r), maxR = Math.max(s.r, e.r)
const minC = Math.min(s.c, e.c), maxC = Math.max(s.c, e.c)
for(let r=minR; r<=maxR; r++) {
for(let c=minC; c<=maxC; c++) {
let val = getValueCb(r, c)
// 尝试转为数字,如果不仅是数字字符则保留原值(用于文本处理)
const numVal = Number(val)
if (!isNaN(numVal) && val !== '') {
values.push(numVal)
} else {
// 简单的转义处理,防止字符串破坏数组结构
values.push(`"${String(val).replace(/"/g, '\\"')}"`)
}
}
}
return `[${values.join(',')}]`
})
// 2. 预处理单个单元格引用:将 A1 转换为具体值
// 复杂的正则用于避免匹配到已经是字符串中的内容,这里做简化处理
// 我们假设单元格引用不会出现在引号内部(这是一个简易引擎的限制)
const cellRegex = /(?<!["'[a-zA-Z0-9_])([A-Z]+[0-9]+)(?!["'[a-zA-Z0-9_])/g
const finalExpression = parsedExpressionWithRanges.replace(cellRegex, (match) => {
const coords = parseCellCoord(match)
if (!coords) return 0
const val = getValueCb(coords.r, coords.c)
const numVal = Number(val)
// 如果是纯数字,返回数字;否则返回带引号的字符串
return (!isNaN(numVal) && val !== '') ? numVal : `"${String(val).replace(/"/g, '\\"')}"`
})
// 3. 构建执行上下文
// 将 FUNCTION_REGISTRY 中的 impl 提取出来作为参数传递给 Function 构造器
const funcNames = Object.keys(FUNCTION_REGISTRY)
const funcImpls = funcNames.map(name => FUNCTION_REGISTRY[name].impl)
try {
// 注入所有支持的函数到执行作用域
// e.g. new Function('SUM', 'AVERAGE', ..., 'return SUM(1, 2)')
const func = new Function(...funcNames, `return ${finalExpression}`)
const result = func(...funcImpls)
// 如果结果是 NaN 或 Infinity做一些友好的处理
if (typeof result === 'number' && isNaN(result)) return '#VALUE!'
if (typeof result === 'number' && !isFinite(result)) return '#DIV/0!'
return result
} catch (e) {
console.warn(`Formula Eval Error: ${formula}`, e)
return '#ERROR!'
}
}