20260920173439

This commit is contained in:
oneao committed 2026-09-20 17:34:40 +08:00
1 parent f4bfc1ddfc
commit 6f03d92425
174 files changed
+23490 -157

No files matched your search

@@ -0,0 +1,180 @@
/**
* 聚合函数测试(对应设计文档 §16 阶段 3 的"跨页合计"能力基础)。
*
* 语义约定:
* - 空数组:sum → 0,avg/min/max → null,count → 0
* - "合计为 0" 与 "没有数据" 是两种不同语义,前者显示 0,后者显示空白
*/
import { describe, expect, it } from "vitest"
import { evaluateValue } from "../src/evaluator.js"
import type { EvalScope } from "../src/scope.js"
const scope: EvalScope = {
row: {
amounts: [10, 20, 30],
prices: [1.5, 2.5],
mixed: [1, "abc", 3, null],
empty: [],
names: ["甲", "乙", "丙"],
},
params: {},
}
describe("sum", () => {
it("数组求和", () => {
expect(evaluateValue("sum(row.amounts)", scope)).toBe(60)
})
it("空数组返回 0(合计为零应显示 0)", () => {
expect(evaluateValue("sum(row.empty)", scope)).toBe(0)
})
it("忽略非数字元素", () => {
expect(evaluateValue("sum(row.mixed)", scope)).toBe(4)
})
it("标量视为单元素", () => {
expect(evaluateValue("sum(5)", scope)).toBe(5)
})
it("null 返回 0", () => {
expect(evaluateValue("sum(row.nothing)", scope)).toBe(0)
})
it("小数求和", () => {
expect(evaluateValue("sum(row.prices)", scope)).toBe(4)
})
it("可与其他运算组合", () => {
expect(evaluateValue("sum(row.amounts) * 2", scope)).toBe(120)
})
})
describe("avg", () => {
it("平均值为数字", () => {
expect(evaluateValue("avg(row.amounts)", scope)).toBe(20)
})
it("空数组返回 null(无数据应显示空白)", () => {
expect(evaluateValue("avg(row.empty)", scope)).toBeNull()
})
it("忽略非数字元素后再平均", () => {
// 1 和 3 是有效数字,'abc' 与 null 被忽略
expect(evaluateValue("avg(row.mixed)", scope)).toBe(2)
})
it("小数的平均值", () => {
expect(evaluateValue("avg(row.prices)", scope)).toBe(2)
})
})
describe("count", () => {
it("统计元素个数", () => {
expect(evaluateValue("count(row.amounts)", scope)).toBe(3)
})
it("空数组返回 0", () => {
expect(evaluateValue("count(row.empty)", scope)).toBe(0)
})
it("统计非空元素个数", () => {
// mixed = [1, 'abc', 3, null],null 不计入
expect(evaluateValue("count(row.mixed)", scope)).toBe(3)
})
it("无参数返回 0", () => {
expect(evaluateValue("count()", scope)).toBe(0)
})
it("标量计为 1", () => {
expect(evaluateValue("count(5)", scope)).toBe(1)
})
it("null 计为 0", () => {
expect(evaluateValue("count(row.nothing)", scope)).toBe(0)
})
})
describe("min / max 双形态", () => {
it("多参数形态(标量比较)", () => {
expect(evaluateValue("min(3, 1, 2)", scope)).toBe(1)
expect(evaluateValue("max(3, 1, 2)", scope)).toBe(3)
})
it("单参数数组形态(聚合)", () => {
expect(evaluateValue("min(row.amounts)", scope)).toBe(10)
expect(evaluateValue("max(row.amounts)", scope)).toBe(30)
})
it("空数组返回 null", () => {
expect(evaluateValue("min(row.empty)", scope)).toBeNull()
expect(evaluateValue("max(row.empty)", scope)).toBeNull()
})
it("忽略非数字", () => {
expect(evaluateValue("max(row.mixed)", scope)).toBe(3)
})
it("单元素数组与标量结果一致", () => {
expect(evaluateValue("min(5)", scope)).toBe(5)
expect(evaluateValue("min(row.prices)", scope)).toBe(1.5)
})
})
describe("聚合与分组作用域协作", () => {
it("可通过 group 作用域聚合", () => {
const groupScope: EvalScope = {
row: { name: "甲" },
group: {
key: "华北",
amounts: [100, 200, 300],
count: 3,
},
params: {},
}
expect(evaluateValue("sum(group.amounts)", groupScope)).toBe(600)
expect(evaluateValue("avg(group.amounts)", groupScope)).toBe(200)
expect(evaluateValue("group.count", groupScope)).toBe(3)
})
it("可通过 dataset 作用域做全表合计", () => {
const datasetScope: EvalScope = {
row: {},
dataset: { amounts: [1, 2, 3, 4], count: 4 },
params: {},
}
expect(evaluateValue("sum(dataset.amounts)", datasetScope)).toBe(10)
})
it("聚合结果可被格式化", () => {
const groupScope: EvalScope = {
row: {},
group: { amounts: [1234.5, 2.25] },
params: {},
}
expect(evaluateValue("format(sum(group.amounts), '#,##0.00')", groupScope)).toBe("1,236.75")
})
it("空组不产生 NaN", () => {
const groupScope: EvalScope = { row: {}, group: { amounts: [] }, params: {} }
const sum = evaluateValue("sum(group.amounts)", groupScope)
expect(sum).toBe(0)
expect(Number.isNaN(sum as number)).toBe(false)
})
})
describe("聚合函数仍在安全白名单内", () => {
it("sum/avg/count 可调用", () => {
expect(evaluateValue("sum(1)", scope)).toBe(1)
expect(evaluateValue("avg(1)", scope)).toBe(1)
expect(evaluateValue("count(1)", scope)).toBe(1)
})
it("未登记的聚合名仍被拒绝", () => {
// median 等未实现,应在解析期就被拒绝
const scope2: EvalScope = { row: {} }
const result = evaluateValue("median(row.x)", scope2)
expect(result).toBeNull()
})
})