226 lines
8.1 KiB
JavaScript
226 lines
8.1 KiB
JavaScript
/**
|
||
* 将列索引 (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!'
|
||
}
|
||
} |