Files
excel/src/utils/formula.js

226 lines
8.1 KiB
JavaScript
Raw Normal View History

2026-01-22 17:00:24 +08:00
/**
* 将列索引 (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!'
}
}