v2 2024.6.4
增加了一些有用的函数,支持dataview api的函数
使用方法:
-
新建calcTable.js,比如js_code/calcTable.js
-
calcTable.js中输入以下代码:
展开查看代码
// 计算表格通用函数
// 第一个参数,块名字或表格序号(序号从0开始)
// 第二个参数,处理表格回调函数,回调函数第一个参数是表格数组,第二个是表格字符串
// 返回格式化后的markdown格式的表格字符串,返回值会替换原有表格
function calcTable(tableName, callback) {
if(!tableName || typeof callback !== 'function') return;
// 定时器防止与输入时冲突
setTimeout(async () => {
//获取编辑器
const editor =app.workspace.getActiveFileView()?.editor
if(!editor) return;
// 获取文件块信息
const absPath = app.vault.getAbstractFileByPath(dv.currentFilePath);
if(!absPath) return;
const file = app.metadataCache.getFileCache(absPath)
let table;
if(file.blocks[tableName]) {
//通过block name获取表
table = file.blocks[tableName]
} else {
//通过索引获取表
const tables = file.sections.filter(s=>s.type === 'table')
table = tables[parseInt(tableName)]
}
if(!table) {
error(`表格 ${tableName} 未找到`)
return;
}
//获取文本内容
let tableContent = await app.vault.cachedRead(absPath);
//获取表格内容
tableContent = tableContent.substring(table.position.start.offset, table.position.end.offset)
//获取表格数组
const tableArr = markdownTableToArray(tableContent);
//检查表格是否为空
if(tableContent === "" || tableArr.length === 0) {
error(`表格 ${tableName} 内容为空`)
return;
}
// 获取格式化后的表格数据
tableContent = callback(tableArr, tableContent);
if(tableContent?.startsWith("[stop]")) {
error(`表格 ${tableName} ${tableContent.replace('[stop]', '')}`)
return;
}
if(!tableContent) {
error(`表格 ${tableName} 内容为空`)
return;
}
//插入文本到行
const from = { line: table.position.start.line, ch: table.position.start.col }
const to = { line: table.position.end.line, ch: table.position.end.col }
editor.replaceRange(tableContent, from, to)
dv.paragraph(`表格 ${tableName} 已统计完成 (${new Date().toLocaleString()}) ✔︎`)
// 解析表格数据
function markdownTableToArray(markdownTable) {
// 分割成多行
const rows = markdownTable.split('\n');
// 初始化二维数组
const tableArray = [];
let headerProcessed = false; // 用于标记表头是否已经处理过
// 遍历每一行
rows.forEach(row => {
// 检查是否为表头或数据行
if (row.startsWith('|') && row.endsWith('|')) {
// 移除首尾的|以及两侧的空格,并按|分割
const cells = row.slice(1, -1).trim().split(/\s*\|\s*/);
if (!headerProcessed) { // 处理表头
tableArray.push(cells);
headerProcessed = true;
} else { // 处理数据行
tableArray.push(cells);
}
}
});
return tableArray;
}
function error(str) {
dv.paragraph(`<span style="color:var(--text-error);">${str} (${new Date().toLocaleString()}) ✘</span>`)
}
}, 500);
}
function stop(tips) {
return `[stop]${tips||'暂停统计'}`
}
// 常用函数
function getRowData(tableArr, map, filter) {
let arr = dv.clone(tableArr);
if(typeof map === 'string') {
if(map.includes("stripTitle") && hasTitle(arr)) {
arr = arr.slice(2);
//arr.splice(1, 1);
}
if(map.includes('stripHtml')){
arr = arr.map(item => item.map(cell => cell.replace(/<[^>]*?\/?>/g, "")));
}
}
if(typeof map === 'function'){
arr = arr.map(map);
}
if(typeof filter === 'function'){
arr = arr.filter(filter);
}
return arr;
}
function getColData(tableArr, map, filter) {
arr = transposeRowAndCol(arr);
return getRowData(arr, map, filter);
}
// 转置行列
function transposeRowAndCol(tableArr) {
let arr = dv.clone(tableArr);
// 移除并获取表头
let headers = arr.shift();
let transposedArr = [];
// 遍历标题和数据进行转置
for (let i = 0; i < headers.length; i++) {
let newRow = [headers[i]];
for (let j = 0; j < arr.length; j++) {
newRow.push(arr[j][i]);
}
transposedArr.push(newRow);
}
return transposedArr;
}
function hasTitle(tableArr) {
return /^[-:]+$/.test(tableArr[1][0]);
}
function stripHtml(tableArr) {
return tableArr.map(item => item.map(cell => cell.replace(/<[^>]*?\/?>/g, "")));
}
function getTitle(tableArr) {
if(hasTitle()) {
return tableArr.slice(0, 1).pop();
}
return [];
}
function skipRow(tableArr, cond){
// row index
if(/^\d+$/.test(cond)) {
const itemIndex = Number(cond)
cond = (item, index) => index !== itemIndex;
}
// last row
else if(typeof cond === 'string' && cond === 'last') {
const itemIndex = tableArr.length - 1;
cond = (item, index) => index !== itemIndex;
}
// row name
else if(typeof cond === 'string') {
const colName = cond;
cond = (item) => item[0] !== colName;
}
if(typeof cond === 'function') {
return tableArr.filter(cond);
}
return tableArr;
}
function skipCol(tableArr, cond) {
// 创建一个新的数组来存放处理后的子数组
let newArr = [];
// 获取表头,用于后续的字符串匹配
const header = tableArr[0];
// 定义一个函数来判断是否跳过某一列
const shouldSkip = (index) => {
if (/^\d+$/.test(cond)) {
return index === cond;
} else if (cond === 'last') {
return index === header.length - 1;
} else if (typeof cond === 'string') {
return header[index] === cond;
}
return false; // 默认不跳过任何列
};
// 遍历每一行进行处理
tableArr.forEach(row => {
// 使用filter过滤不需要的列
const newRow = row.filter((_, index) => !shouldSkip(index));
newArr.push(newRow);
});
return newArr;
}
function skipRowHas(tableArr, cond) {
if(/^\d+$/.test(cond)) {
const itemIndex = Number(cond)
cond = (item, index) => index !== itemIndex;
}
else if(typeof cond === 'string') {
const itemVal = cond;
cond = (item, index) => !item.includes(itemVal)
}
if(typeof cond === 'function') {
return tableArr.filter(cond);
}
return tableArr;
}
function rowHas(value, tableArr) {
if(typeof value === 'function'){
return tableArr.findIndex(value);
}
return tableArr.findIndex(item=>item.includes(value));
}
function rowNotHas(value) {
if(typeof value === 'function'){
return value;
}
return row => !row.includes(value);
}
// 需要先用transposeRowAndCol交换后才能使用
function colHas(value, tableArr) {
return rowHas(value, tableArr);
}
// 需要先用transposeRowAndCol交换后才能使用
function colNotHas(value) {
return rowNotHas(value);
}
function onTheLeft(row, length) {
if(dv.isArray(length)) length = length[0]?.length;
const newRow = new Array(length).fill('')
newRow.splice(0, row.length, ...row);
return newRow;
}
function onTheRight(row, length) {
if(dv.isArray(length)) length = length[0]?.length;
let newRow = new Array(length).fill('')
// 计算开始替换的索引
let startIndex = newRow.length - row.length;
// 如果startIndex小于0,则说明row比newRow组长或相等,直接替换整个数组
if (startIndex < 0) {
newRow = row;
} else {
// 否则,使用splice方法替换数组的末尾部分
newRow.splice(startIndex, newRow.length, ...row);
}
return newRow;
}
Array.prototype.replaceOrPush = function(newItem, index = -1) {
if(index < 0) {
this.push(newItem);
return this;
}
this.splice(index, 1, newItem);
return this;
}
function getRowNum(tableArr) {
return tableArr.length;
}
function getColNum(tableArr) {
return tableArr[0].length;
}
function renderToMarkdownTable(tableArr){
if(!hasTitle(tableArr)){
const titleLen = tableArr[0].length;
// 创建一个新数组,包含insertLength个'---'
const insertRow = new Array(titleLen).fill('---');
// 使用splice方法在索引1的位置插入新数组
tableArr.splice(1, 0, insertRow);
}
return tableArr.map(row => `| ${row.join(' | ')} |`).join('\n');
}
function setCss(tableArr, rowNum, colNum, css) {
tableArr[rowNum][colNum] = `<span style="${css}">${tableArr[rowNum][colNum]}</span>`;
return tableArr;
}
function setBgColor(tableArr, rowNum, colNum, color) {
return setCss(tableArr, rowNum, colNum, `background-color:${color}`);
}
function setColor(tableArr, rowNum, colNum, color) {
return setCss(tableArr, rowNum, colNum, `color:${color}`);
}
function sum(row, digits = 0) {
const ret = row.reduce((sum, item) => sum + (parseFloat(item)||0), 0);
if(digits > 0) {
ret = dv.func.round(ret, digits);
}
return ret;
}
function avg(row, digits = 0) {
const ret = sum(row)/count(row);
if(digits > 0) {
ret = dv.func.round(ret, digits);
}
return ret;
}
function count(row, stripTitle = false) {
if(stripTitle) return row.length - 2;
return row.length;
//return row.reduce((count, item) => count + 1, 0);
}
function sortTable(tableArr, field, order = 'asc', num = false) {
if(/^\d+$/.test(field)) {
field = Number(field)
} else {
field = tableArr[0].findIndex(item=>item === field);
}
// 检查field是否为有效索引
if (typeof field !== 'number' || field < 0 || field >= tableArr[0].length) {
console.error('Invalid field index.');
return;
}
// 定义比较函数
const compareFn = (a, b) => {
let aValue = a[field];
let bValue = b[field];
// 如果按数字排序
if (num) {
aValue = isNaN(aValue) ? Number.NEGATIVE_INFINITY : parseFloat(aValue);
bValue = isNaN(bValue) ? Number.NEGATIVE_INFINITY : parseFloat(bValue);
return order === 'asc' ? aValue - bValue : bValue - aValue;
} else {
// 使用localeCompare进行本地化字符串比较
const compareResult = aValue.localeCompare(bValue, undefined, {numeric: true, sensitivity: 'base'});
return compareResult * (order === 'asc' ? 1 : -1);
}
};
// title不参与排序
const title = tableArr.splice(0, hasTitle(tableArr) ? 2 : 1);
// 执行排序
tableArr.sort(compareFn);
tableArr = [...title, ...tableArr];
return tableArr;
}
function getRowIndexByColName(tableArr, colName) {
return tableArr[0].findIndex(item=>item === colName);
}
function getRow(tableArr, where, toNum = false){
let newArr = [];
if(/^\d+$/.test(where)) {
const index = Number(where)
newArr = tableArr[index];
}
else {
const index = tableArr[0].findIndex(item=>item === where);
newArr = tableArr[index];
}
if(toNum) {
newArr = newArr.map(item => parseFloat(item)||0);
}
return newArr;
}
function getCol(tableArr, where, toNum = false) {
where = getRowIndexByColName(tableArr, where);
const newTableArr = transposeRowAndCol(tableArr);
return getRow(newTableArr, where, toNum);
}
// 导出函数列表
const exports = {
calcTable,
stop,
getRowData,
getColData,
transposeRowAndCol,
hasTitle,
stripHtml,
getTitle,
skipRow,
skipCol,
skipRowHas,
rowHas,
rowNotHas,
colHas,
colNotHas,
onTheLeft,
onTheRight,
Array,
getRowNum,
getColNum,
renderToMarkdownTable,
setCss,
setBgColor,
setColor,
sum,
avg,
count,
sortTable,
getRowIndexByColName,
getRow,
getCol,
}
// 导出函数
for(func in exports){
this[func] = exports[func];
dv[func] = exports[func];
}
// 统计表格数据使用示例,请根据自己的表格修改
// 第一个参数,块名字或表格序号(序号从0开始)
// 第二个参数,处理表格回调函数,回调函数第一个参数是表格数组,第二个是表格字符串
// 返回格式化后的markdown格式的表格字符串,返回值会替换原有表格
// 也可调用dataview的统计函数比如sum, average等
// 比如:dv.func.sum(row)
// calcTable('demo', (tableArr, tableContent) => {
// //暂停统计
// //return stop();
// // 深拷贝数据(建议深拷贝处理,以免影响原数组)
// let newArr = getRowData(tableArr, 'stripHtml', rowNotHas('合计'));
// // 按cc列asc排序
// //newArr = sortTable(newArr, 'cc', 'asc', 'num');
// // 计算bb总和
// const bb = sum(getCol(newArr, 'bb', 'toNum'));
// // 计算aa总和
// const cc = sum(getCol(newArr, 'cc', 'toNum'));
// // 生成统计数组数据
// const newRow = onTheLeft(['合计', bb, cc], newArr);
// // 替换或插入统计数据(注意这里replaceOrPush函数,如果whereRowHas返回小于0则追加数据,否则替换)
// newArr.replaceOrPush(newRow, rowHas('合计', newArr));
// // 渲染成markdown表格
// const markdownTable = renderToMarkdownTable(newArr);
// //console.log(markdownTable);return;
// // 把表格输出到编辑器
// return markdownTable;
// });
// 统计列数据示例
// 也可调用dataview的统计函数比如sum, average等
// 比如:dv.func.sum(row)
// calcTable('demo', (tableArr, tableContent) => {
// // 转换行和列的位置,把列当做行来处理
// let newArr = transposeRowAndCol(tableArr);
// // 获取数据,并去除html代码和汇总行
// newArr = getRowData(newArr, 'stripHtml', rowNotHas('合计'));
// // 按cc列asc排序
// //newArr = sortTable(newArr, 'demo1', 'asc', true);
// // 计算demo1总和
// const demo1 = sum(getCol(newArr, 'demo1', 'toNum'));
// // 计算demo1总和
// const demo2 = sum(getCol(newArr, 'demo2', 'toNum'));
// // 生成统计数组数据
// const newRow = onTheLeft(['合计', '---', demo1, demo2], newArr);
// // 替换或插入统计数据(注意这里replaceOrPush函数,如果whereRowHas返回小于0则追加数据,否则替换)
// newArr.replaceOrPush(newRow, rowHas('合计', newArr));
// // 处理完毕,恢复行和列的位置
// newArr = transposeRowAndCol(newArr);
// // 渲染成markdown表格
// const markdownTable = renderToMarkdownTable(newArr);
// //console.log(markdownTable, demo1, demo2);return;
// // 把表格输出到编辑器
// return markdownTable;
// });
// 同时统计行和列数据示例
/*
calcTable('demo', (tableArr, tableContent) => {
// 深拷贝数据(建议深拷贝处理,以免影响原数组)
let newArr = getRowData(tableArr, 'stripHtml');
newArr = skipRow(tableArr, '合计')
newArr = skipCol(newArr, '合计')
// 计算行统计
// 计算bb总和
const bb = sum(getCol(newArr, 'bb', 'toNum'));
// 计算aa总和
const cc = sum(getCol(newArr, 'cc', 'toNum'));
// 生成统计数组数据
const newRow = onTheLeft(['合计', bb, cc], newArr);
// 替换或插入统计数据(注意这里replaceOrPush函数,如果whereRowHas返回小于0则追加数据,否则替换)
newArr.replaceOrPush(newRow, rowHas('合计', newArr));
// 计算列统计
// 反转行和列的位置
newArr = transposeRowAndCol(newArr);
// // 计算demo1总和
const demo1 = sum(getCol(newArr, 'demo1', 'toNum'));
// 计算demo1总和
const demo2 = sum(getCol(newArr, 'demo2', 'toNum'));
// 替换原数组数据(注意这里replaceOrPush函数,如果whereRowHas返回小于0则追加数据,否则替换)
const newColRow = onTheLeft(['合计', '---', demo1, demo2], newArr);
// 替换或插入统计数据
newArr.replaceOrPush(newColRow);
// 处理完毕,恢复行和列的位置
newArr = transposeRowAndCol(newArr);
// 渲染成markdown表格
const markdownTable = renderToMarkdownTable(newArr);
//console.log(markdownTable);return;
// 把表格输出到编辑器
return markdownTable;
});
// */
- 使用示例,比如,在需要统计的表格下输入以下代码:
```dataviewjs
// 加载统计函数
await dv.view("js_code/calcTable");
// 统计表格数据使用示例,请根据自己的表格修改
// 第一个参数,块名字或表格序号(序号从0开始)
// 第二个参数,处理表格回调函数,回调函数第一个参数是表格数组,第二个是表格字符串
// 返回格式化后的markdown格式的表格字符串,返回值会替换原有表格
// 也可调用dataview的统计函数比如sum, average等
// 比如:dv.func.sum(row)
calcTable('demo', (tableArr, tableContent) => {
// 暂停统计
//return stop();
// 深拷贝数据(建议深拷贝处理,以免影响原数组)
let newArr = getRowData(tableArr, 'stripHtml', rowNotHas('合计'));
// 按cc列asc排序
//newArr = sortTable(newArr, 'cc', 'asc', 'num');
// 计算bb总和
const bb = sum(getCol(newArr, 'bb', 'toNum'));
// 计算aa总和
const cc = sum(getCol(newArr, 'cc', 'toNum'));
// 生成统计数组数据
const newRow = onTheLeft(['合计', bb, cc], newArr);
// 替换或插入统计数据(注意这里replaceOrPush函数,如果whereRowHas返回小于0则追加数据,否则替换)
newArr.replaceOrPush(newRow, rowHas('合计', newArr));
// 渲染成markdown表格
const markdownTable = renderToMarkdownTable(newArr);
//console.log(markdownTable);return;
// 把表格输出到编辑器
return markdownTable;
});
```
注意,以上只是示例,具体代码需根据自己情况进行修改。
【免责声明】由于涉及到文档更新,请在副本中操作,测试无误后再使用,使用前做好备份,一切后果均与本脚本及作者无关。
【v3计划】
-
支持单元格公式,比如:
:=sum(0,1, 3,1)
,:=sum(thisCol)
等 -
支持SQL汇总查询,比如:
const query = `select * from demo`;
return renderNewMarkdownTable(query);
不过v3只是方便上手和易用罢了,v2编程方式,其实已经够用了,不知道有没有人喜欢v3这样的方式。