Skip to content

Commit 8e2bbba

Browse files
hotlongclaude
andauthored
fix(service-analytics): compareTo 把比较桶键平移回当期,让日期维度即网格维度时的同比真正对齐 (#6007) (#6213)
趋势图 + 同比是 compareTo 最常见的形状:日期维度既写进 selection.dimensions (它就是图表的时间轴),又被 compareTo 用作锚点。这个形状下比较趟从来没有对齐过。 比较趟查询的是**平移后**的窗口,所以它的行按平移后的桶键落地;而 mergeByDimensions 按 selection.dimensions 元组建键 —— 2025-01 不等于 2026-01,于是没有一条比较行合并 得进去,全部作为新行追加。两趟各自只报告了自己那一半,fillEmptyGroups 把另一半填成自信 的 0,再加上平移后的桶键坐在网格里,而它们落在调用方筛选窗口之外。一个 2 桶窗口回来是 4 行、每行一个 0、两行在窗口外。 按维护者 2026-08-07 裁决(方向 1)实施:合并之前,把每个比较桶键用当期的说法重述一遍。 - previousYear —— 窗口是按日历年平移的,逆运算就是按日历年往前推一年:对桶自己的首日 平移再重新分桶。刻意是 shiftRange 那套年运算的精确逆运算(含 setUTCFullYear 溢出), 窗口与桶键因此不可能对「一年」有两种理解。 - previousPeriod —— 任意天数窗口没有日历对应物,按桶序(bucket ordinal)对齐:上一窗口 的第 n 个桶对上本窗口的第 n 个桶。序号由日历算出而不是数组下标,所以本期网格里的空档 不会让其后每个桶都错位一格。 响应形状不变(仍是 <measure>__compare 列),objectui#3337 不受影响。 不确定时一律保持改动前的行为而不是猜:空桶(键为 null,两趟本来就互相合并)、未分桶的日期 维度(分组的是原始时间戳)、平移回来落在当期窗口之外的桶。范围限定在锚点既是网格维度又被 分桶的形状;两趟通过同一个 granularityOf 读桶大小,重述的桶大小按构造即分组用的桶大小。 bucketKeyAtOrdinal 是本包唯一自己铸造桶键的地方,它铸出的键要和运行时 GROUP BY 产出的 键逐字节相等,所以对 @objectstack/core 的 bucketKeyToCalendarRange(同一套词汇的规范 逆函数)做了 55 例往返钉,而不是靠眼看。 dataset-window-timedimension-bucketing.test.ts 的「bucketed anchor」用例按裁决有意 翻红并改写为成对的 2×2;新增 dataset-compare-bucket-alignment.test.ts,其 fake 按 filter 真的分桶(旧 pin 的 fake 两趟返回同一固定桶键,合并必然成功,够不到这一层)。 Claude-Session: https://claude.ai/code/session_015a5qkLzpGXhLL2F5gvJ7dD Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 773f80a commit 8e2bbba

4 files changed

Lines changed: 1020 additions & 18 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
"@objectstack/service-analytics": patch
3+
---
4+
5+
fix(service-analytics): `compareTo` 在「日期维度本身就是网格维度」时把比较桶键平移回当期 (#6007)
6+
7+
趋势图 + 同比是 `compareTo` 最常见的形状:日期维度既写进 `selection.dimensions`
8+
(它就是图表的时间轴),又被 `compareTo` 用作锚点。这个形状下比较趟从来没有对齐过。
9+
10+
比较趟查询的是**平移后**的窗口,所以它的行按平移后的桶键落地;而
11+
`mergeByDimensions``selection.dimensions` 元组建键 —— `2025-01` 不等于
12+
`2026-01`,于是**没有一条**比较行合并得进去,全部作为新行追加。两趟各自只报告了自己
13+
那一半,`fillEmptyGroups` 把另一半填成自信的 `0`,再加上平移后的桶键坐在网格里,而它们
14+
落在调用方筛选窗口之外。一个 2 桶窗口的「今年 vs 去年同期」回来是这样的:
15+
16+
```
17+
[{"close_date":"2025-01","opp_count__compare":5,"opp_count":0},
18+
{"close_date":"2025-02","opp_count__compare":7,"opp_count":0},
19+
{"close_date":"2026-01","opp_count":1,"opp_count__compare":0},
20+
{"close_date":"2026-02","opp_count":2,"opp_count__compare":0}]
21+
```
22+
23+
四行、每行一个 0、两行在窗口外;期望是 2 行 × 2 列。
24+
25+
**修法(维护者裁决 2026-08-07,方向 1):合并之前,把每个比较桶键用当期的说法重述一遍。**
26+
上例现在返回 `[{close_date:'2026-01',opp_count:1,opp_count__compare:5},
27+
{close_date:'2026-02',opp_count:2,opp_count__compare:7}]`。
28+
29+
- `previousYear` —— 窗口是按日历年平移的,所以逆运算就是按日历年往前推一年:对桶自己的
30+
首日做平移再重新分桶。`2025-01``2026-01``2025-Q1``2026-Q1`
31+
`2025-W03``2026-W03`。它刻意是 `shiftRange` 那套年运算的精确逆运算(含
32+
`setUTCFullYear` 的溢出行为),窗口与桶键因此不可能对「一年」有两种理解。
33+
- `previousPeriod` —— 任意天数窗口没有日历对应物,所以按**桶序(bucket ordinal)**对齐:
34+
上一窗口的第 n 个桶对上本窗口的第 n 个桶,n 各自从自己窗口的起点数起。序号由**日历**算出
35+
而不是数组下标,所以本期网格里某个桶没有数据(存在空档)不会让其后每个桶都错位一格。
36+
37+
**响应形状不变** —— 仍然是 `<measure>__compare` 列,行仍然是网格维度元组,所以消费端
38+
(objectui#3337 正在收敛的那条契约)不受影响。
39+
40+
不确定时一律**保持原样**(即改动前的行为),而不是猜:空桶(两条聚合路径上键都是 `null`,
41+
两趟本来就互相合并)、未分桶的日期维度(分组的是原始时间戳,不是桶键)、以及平移回来落在
42+
当期窗口之外的桶(两个等长的天数窗口可以切出不同的桶数)。
43+
44+
范围严格限定在坏掉的那个形状:锚点必须是**网格维度**(仅作窗口的锚点两趟都不是列,#5688
45+
之后本来就对齐)且必须**被分桶**。两趟通过同一个 `granularityOf` 读取桶大小,所以这里重述
46+
的桶大小按构造就是查询分组用的桶大小。

0 commit comments

Comments
 (0)