dataviewjs表格统计之V2

查看版本2

效果:

调用方式:

// 第一个参数,块名字或表格序号(序号从0开始)
// 第二个参数,处理表格回调函数,回调函数第一个参数是表格字符串,第二个是表格数组
// 返回格式化后的markdown格式的表格字符串,返回值会替换原有表格

calcTable('demo', (tableContent, tableArr) => {});

例如:

calcTable('demo', (tableContent, tableArr) => {
	// 如果已统计过则去除最后一条记录
	if(tableContent.contains("合计")){
		tableArr.pop();
		tableContent = tableContent.split("\n")
		tableContent.pop()
		tableContent = tableContent.join("\n")
	}
	
	// 计算bb总和
	const bb = tableArr.reduce((sum, item) => sum + parseInt(item["bb"]), 0);
	// 计算cc总和
	const cc = tableArr.reduce((sum, item) => sum + parseInt(item["cc"]), 0)
	tableContent += `\n| 合计  | ${bb}  | ${cc}  |`
	
	return tableContent;
});

代码:

```dataviewjs
// 统计表格数据使用示例,请根据自己的表格修改
// 第一个参数,块名字或表格序号(序号从0开始)
// 第二个参数,处理表格回调函数,回调函数第一个参数是表格字符串,第二个是表格数组
// 返回格式化后的markdown格式的表格字符串,返回值会替换原有表格
/*calcTable('demo', (tableContent, tableArr) => {
	// 如果已统计过则去除最后一条记录
	if(tableContent.contains("合计")){
		tableArr.pop();
		tableContent = tableContent.split("\n")
		tableContent.pop()
		tableContent = tableContent.join("\n")
	}
	
	// 计算bb总和
	const bb = tableArr.reduce((sum, item) => sum + parseInt(item["bb"]), 0);
	// 计算cc总和
	const cc = tableArr.reduce((sum, item) => sum + parseInt(item["cc"]), 0)
	tableContent += `\n| 合计  | ${bb}  | ${cc}  |`
	
	return tableContent;
});*/

// 计算表格通用函数
// 第一个参数,块名字或表格序号(序号从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 = extractMarkDownTable(tableContent);
		//检查表格是否为空
		if(tableContent === "" || tableArr.length === 0) {
			error(`表格 ${tableName} 内容为空`)
			return;
		}
		// 获取格式化后的表格数据
		tableContent = callback(tableContent, tableArr);
		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 extractMarkDownTable(content) {
		  let rows = content.split("\n");
		  let headRow = rows[0];
		  let headCols = headRow.split("|");
		  let results = [];
		  for (let i = 2; i < rows.length; i++) {
			let row = rows[i];
			let cols = row.split("|");
			let obj = {};
			for (let j = 1; j < headCols.length - 1; j++) {
			  let val = cols[j].trim();
			  let numVal = Number(val);
			  obj[headCols[j].trim()] = isNaN(numVal) ? val : numVal;
			}
			results.push(obj);
		  }
		  return results;
		}
		function error(str) {
			dv.paragraph(`<span style="  
 color:var(--text-error);">${str} (${new Date().toLocaleString()}) ✘</span>`)
		}
	}, 500);
}
```

【免责声明】由于涉及到文档更新,请在副本中操作,测试无误后再使用,使用前做好备份,一切后果均与本脚本及作者无关。

1 个赞

v2 2024.6.4

增加了一些有用的函数,支持dataview api的函数

使用方法:

  1. 新建calcTable.js,比如js_code/calcTable.js

  2. 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;
});
// */
  1. 使用示例,比如,在需要统计的表格下输入以下代码:
```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计划】

  1. 支持单元格公式,比如::=sum(0,1, 3,1):=sum(thisCol)

  2. 支持SQL汇总查询,比如:

const query = `select * from demo`;
return renderNewMarkdownTable(query);

不过v3只是方便上手和易用罢了,v2编程方式,其实已经够用了,不知道有没有人喜欢v3这样的方式。

1 个赞