slimjson 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +8 -0
- package/README.md +77 -26
- package/README_EN.md +74 -33
- package/compress-test.js +51 -25
- package/compress.js +423 -410
- package/data/searchGroup.json +96365 -0
- package/package.json +1 -1
- package/test.js +473 -384
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ npm install slimjson
|
|
|
21
21
|
|
|
22
22
|
## API
|
|
23
23
|
|
|
24
|
-
### `compress(source)`
|
|
24
|
+
### `compress(source, opts?)`
|
|
25
25
|
|
|
26
26
|
将对象数组压缩为 `{ keys, rows }` 结构:
|
|
27
27
|
|
|
@@ -43,6 +43,14 @@ const compressed = compress(users);
|
|
|
43
43
|
// }
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
**参数:**
|
|
47
|
+
|
|
48
|
+
| 参数 | 类型 | 默认值 | 说明 |
|
|
49
|
+
|------|------|--------|------|
|
|
50
|
+
| `source` | `Object[]` 或 `Object` | — | 待压缩的对象数组(单个对象会自动包裹为数组) |
|
|
51
|
+
| `opts` | `Object` | — | 可选配置 |
|
|
52
|
+
| `opts.trimTrailingNulls` | `boolean` | `false` | 是否去除行尾的 `null` 值 |
|
|
53
|
+
|
|
46
54
|
**特点:**
|
|
47
55
|
- `keys` 取所有对象的 key 并集,按首次出现顺序排列
|
|
48
56
|
- 某对象缺失某字段 → 对应 row 位置填充 `null`
|
|
@@ -55,21 +63,46 @@ const compressed = compress(users);
|
|
|
55
63
|
```js
|
|
56
64
|
const data = [
|
|
57
65
|
{ name: '张三', age: 28, profile: { avatar: 'a.jpg', bio: 'Hello' } },
|
|
58
|
-
{ name: '李四', age: 35, profile: { avatar: 'b.jpg' } },
|
|
66
|
+
{ name: '李四', age: 35, profile: { avatar: 'b.jpg', file: null } },
|
|
67
|
+
{ name: '王五' },
|
|
59
68
|
];
|
|
60
69
|
|
|
61
70
|
compress(data);
|
|
62
71
|
// {
|
|
63
|
-
// keys: ['name', 'age', { profile: ['avatar', 'bio'] }],
|
|
72
|
+
// keys: ['name', 'age', { profile: ['avatar', 'bio', 'file'] }],
|
|
73
|
+
// rows: [
|
|
74
|
+
// ['张三', 28, ['a.jpg', 'Hello', null]],
|
|
75
|
+
// ['李四', 35, ['b.jpg', null, null]],
|
|
76
|
+
// ['王五', null, null]
|
|
77
|
+
// ]
|
|
78
|
+
// }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
#### `trimTrailingNulls`:去除尾部 null
|
|
82
|
+
|
|
83
|
+
启用后,每行及嵌套子行尾部的 `null` 会被去除,进一步压缩体积:
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
compress(data, { trimTrailingNulls: true });
|
|
87
|
+
// {
|
|
88
|
+
// keys: ['name', 'age', { profile: ['avatar', 'bio', 'file'] }],
|
|
64
89
|
// rows: [
|
|
65
90
|
// ['张三', 28, ['a.jpg', 'Hello']],
|
|
66
|
-
// ['李四', 35, ['b.jpg'
|
|
91
|
+
// ['李四', 35, ['b.jpg']],
|
|
92
|
+
// ['王五']
|
|
67
93
|
// ]
|
|
68
94
|
// }
|
|
95
|
+
```
|
|
69
96
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
97
|
+
`decompress` 会自动将缺失的尾部值补回 `null`,roundtrip 还原结果一致:
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
decompress(compress(data, { trimTrailingNulls: true }));
|
|
101
|
+
// [
|
|
102
|
+
// { name: '张三', age: 28, profile: { avatar: 'a.jpg', bio: 'Hello', file: null } },
|
|
103
|
+
// { name: '李四', age: 35, profile: { avatar: 'b.jpg', bio: null, file: null } },
|
|
104
|
+
// { name: '王五', age: null, profile: null }
|
|
105
|
+
// ]
|
|
73
106
|
```
|
|
74
107
|
|
|
75
108
|
#### 对象数组示例(订单场景)
|
|
@@ -134,10 +167,21 @@ compress(orders);
|
|
|
134
167
|
// }
|
|
135
168
|
// specs 的 key 取并集:第一单有 layout,第二单有 size → 都保留,缺失的填 null
|
|
136
169
|
|
|
137
|
-
|
|
170
|
+
compress(orders, { trimTrailingNulls: true });
|
|
171
|
+
// rows 变为:
|
|
172
|
+
// [
|
|
173
|
+
// ['A001', '张三', [
|
|
174
|
+
// ['键盘', 299, ['黑色', '104键']],
|
|
175
|
+
// ['鼠标', 99, ['白色', null, '4000']]
|
|
176
|
+
// ]],
|
|
177
|
+
// ['A002', '李四', [
|
|
178
|
+
// ['显示器', 1999, ['银色']]
|
|
179
|
+
// ]]
|
|
180
|
+
// ]
|
|
181
|
+
|
|
182
|
+
stringify(compress(orders, { trimTrailingNulls: true }));
|
|
138
183
|
// {keys:[orderId,customer,{items:[name,price,{specs:[color,layout,dpi,size]}]}],rows:[[
|
|
139
|
-
// A001,张三,[[键盘,299,[黑色,104
|
|
140
|
-
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^ specs 中缺失字段用空槽省略 null ^^^^^^^^^^^^^^^^^^^^^^^^
|
|
184
|
+
// A001,张三,[[键盘,299,[黑色,104键]],[鼠标,99,[白色,,4000]]]],[A002,李四,[[显示器,1999,[银色]]]]]}
|
|
141
185
|
```
|
|
142
186
|
|
|
143
187
|
#### 单对象示例
|
|
@@ -149,7 +193,7 @@ compress({ name: 'Alice', age: 25 });
|
|
|
149
193
|
|
|
150
194
|
### `decompress(compressed)`
|
|
151
195
|
|
|
152
|
-
将 `{ keys, rows }`
|
|
196
|
+
将 `{ keys, rows }` 还原为原始对象数组。缺失的尾部值会自动补回 `null`:
|
|
153
197
|
|
|
154
198
|
```js
|
|
155
199
|
const restored = decompress(compressed);
|
|
@@ -258,6 +302,11 @@ const parsed = parse(text);
|
|
|
258
302
|
const restored = decompress(parsed);
|
|
259
303
|
|
|
260
304
|
// restored 与 data 深度相等
|
|
305
|
+
|
|
306
|
+
// 启用 trimTrailingNulls 进一步压缩
|
|
307
|
+
const compressedTrim = compress(data, { trimTrailingNulls: true });
|
|
308
|
+
const textTrim = stringify(compressedTrim);
|
|
309
|
+
// textTrim 比 text 更短
|
|
261
310
|
```
|
|
262
311
|
|
|
263
312
|
### 压缩率计算
|
|
@@ -271,33 +320,35 @@ console.log(`压缩率: ${ratio}%`);
|
|
|
271
320
|
|
|
272
321
|
## 压缩效果
|
|
273
322
|
|
|
274
|
-
基于 `compress-test.js` 基准测试的实际数据(18
|
|
323
|
+
基于 `compress-test.js` 基准测试的实际数据(18 组测试,所有 roundtrip 解压正确):
|
|
275
324
|
|
|
276
|
-
| 数据类型 | 对象数 | 原始大小 |
|
|
277
|
-
|
|
278
|
-
| 简单用户 | 1,000 | 147.
|
|
279
|
-
| 简单用户 | 10,000 | 1.45 MB | 882.
|
|
280
|
-
| 嵌套用户(含 profile.social) | 1,000 | 235.
|
|
281
|
-
| 订单(每单1-5商品) | 500 | 166.
|
|
282
|
-
| 学校数据(6年级×4班×30生) | 24 |
|
|
283
|
-
| 稀疏字段(500条×30字段) | 500 | 144.
|
|
284
|
-
| 稀疏字段(2000条×50字段) | 2,000 |
|
|
285
|
-
| 深层嵌套(5层组织结构) | 5 | 634.
|
|
325
|
+
| 数据类型 | 对象数 | 原始大小 | 不 trim | 压缩率 | trim | 压缩率 | 差值 |
|
|
326
|
+
|---------|-------|---------|---------|-------|------|-------|------|
|
|
327
|
+
| 简单用户 | 1,000 | 147.61 KB | 87.12 KB | 40.98% | 87.12 KB | 40.98% | — |
|
|
328
|
+
| 简单用户 | 10,000 | 1.45 MB | 882.51 KB | 40.69% | 882.51 KB | 40.69% | — |
|
|
329
|
+
| 嵌套用户(含 profile.social) | 1,000 | 235.70 KB | 153.56 KB | 34.85% | 153.27 KB | 34.97% | -294 B |
|
|
330
|
+
| 订单(每单1-5商品) | 500 | 166.95 KB | 72.30 KB | 56.69% | 72.30 KB | 56.69% | — |
|
|
331
|
+
| 学校数据(6年级×4班×30生) | 24 | 214.86 KB | 88.88 KB | 58.63% | 88.53 KB | 58.80% | -365 B |
|
|
332
|
+
| 稀疏字段(500条×30字段) | 500 | 144.61 KB | 45.40 KB | 68.60% | 45.13 KB | 68.79% | -276 B |
|
|
333
|
+
| 稀疏字段(2000条×50字段) | 2,000 | 951.94 KB | 293.62 KB | 69.16% | 292.49 KB | 69.27% | -1.13 KB |
|
|
334
|
+
| 深层嵌套(5层组织结构) | 5 | 634.60 KB | 289.02 KB | 54.46% | 289.02 KB | 54.46% | — |
|
|
286
335
|
|
|
287
336
|
**结论:**
|
|
288
337
|
1. 字段名越长、数量越多,压缩效果越好
|
|
289
338
|
2. 对象数组(订单条目、学生列表)压缩效果显著(55–59%)
|
|
290
339
|
3. 稀疏字段压缩率最高 — 缺失字段的 null 通过空槽省略(67–69%)
|
|
291
|
-
4.
|
|
292
|
-
5.
|
|
340
|
+
4. `trimTrailingNulls` 在数据有缺失尾部字段时额外节省体积(最高 1.48 KB / 5000 条)
|
|
341
|
+
5. 数据完整无缺失字段时,trim 无额外收益
|
|
342
|
+
6. 深层嵌套结构能获得更好的压缩效果
|
|
343
|
+
7. `stringify` 省略引号进一步减少文本体积
|
|
293
344
|
|
|
294
345
|
## 开发
|
|
295
346
|
|
|
296
347
|
```bash
|
|
297
|
-
#
|
|
348
|
+
# 运行测试(192 个用例,100% 覆盖率)
|
|
298
349
|
npm test
|
|
299
350
|
|
|
300
|
-
#
|
|
351
|
+
# 运行压缩率基准测试(含 trim 对比)
|
|
301
352
|
node compress-test.js
|
|
302
353
|
```
|
|
303
354
|
|
package/README_EN.md
CHANGED
|
@@ -21,7 +21,7 @@ npm install slimjson
|
|
|
21
21
|
|
|
22
22
|
## API
|
|
23
23
|
|
|
24
|
-
### `compress(source)`
|
|
24
|
+
### `compress(source, opts?)`
|
|
25
25
|
|
|
26
26
|
Compresses an object array into a `{ keys, rows }` structure:
|
|
27
27
|
|
|
@@ -43,6 +43,14 @@ const compressed = compress(users);
|
|
|
43
43
|
// }
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
**Parameters:**
|
|
47
|
+
|
|
48
|
+
| Parameter | Type | Default | Description |
|
|
49
|
+
|-----------|------|---------|-------------|
|
|
50
|
+
| `source` | `Object[]` or `Object` | — | Object array to compress (single object is auto-wrapped) |
|
|
51
|
+
| `opts` | `Object` | — | Optional configuration |
|
|
52
|
+
| `opts.trimTrailingNulls` | `boolean` | `false` | Remove trailing `null` values from each row |
|
|
53
|
+
|
|
46
54
|
**Features:**
|
|
47
55
|
- `keys` takes the union of all object keys, ordered by first appearance
|
|
48
56
|
- Missing fields in an object → fill `null` at the corresponding row position
|
|
@@ -50,32 +58,51 @@ const compressed = compress(users);
|
|
|
50
58
|
- Object arrays (e.g. order items) are recursively compressed the same way
|
|
51
59
|
- When a plain object is passed (not an array), it is treated as a single-element array
|
|
52
60
|
|
|
53
|
-
```js
|
|
54
|
-
compress({ name: 'Alice', age: 25 });
|
|
55
|
-
// Equivalent to compress([{ name: 'Alice', age: 25 }])
|
|
56
|
-
// { keys: ['name', 'age'], rows: [['Alice', 25]] }
|
|
57
|
-
```
|
|
58
|
-
|
|
59
61
|
#### Nested Object Example
|
|
60
62
|
|
|
61
63
|
```js
|
|
62
64
|
const data = [
|
|
63
65
|
{ name: 'Alice', age: 28, profile: { avatar: 'a.jpg', bio: 'Hello' } },
|
|
64
|
-
{ name: 'Bob', age: 35, profile: { avatar: 'b.jpg' } },
|
|
66
|
+
{ name: 'Bob', age: 35, profile: { avatar: 'b.jpg', file: null } },
|
|
67
|
+
{ name: 'Carol' },
|
|
65
68
|
];
|
|
66
69
|
|
|
67
70
|
compress(data);
|
|
68
71
|
// {
|
|
69
|
-
// keys: ['name', 'age', { profile: ['avatar', 'bio'] }],
|
|
72
|
+
// keys: ['name', 'age', { profile: ['avatar', 'bio', 'file'] }],
|
|
73
|
+
// rows: [
|
|
74
|
+
// ['Alice', 28, ['a.jpg', 'Hello', null]],
|
|
75
|
+
// ['Bob', 35, ['b.jpg', null, null]],
|
|
76
|
+
// ['Carol', null, null]
|
|
77
|
+
// ]
|
|
78
|
+
// }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
#### `trimTrailingNulls`: Remove Trailing nulls
|
|
82
|
+
|
|
83
|
+
When enabled, trailing `null` values in each row (and nested sub-rows) are removed for further compression:
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
compress(data, { trimTrailingNulls: true });
|
|
87
|
+
// {
|
|
88
|
+
// keys: ['name', 'age', { profile: ['avatar', 'bio', 'file'] }],
|
|
70
89
|
// rows: [
|
|
71
90
|
// ['Alice', 28, ['a.jpg', 'Hello']],
|
|
72
|
-
// ['Bob', 35, ['b.jpg'
|
|
91
|
+
// ['Bob', 35, ['b.jpg']],
|
|
92
|
+
// ['Carol']
|
|
73
93
|
// ]
|
|
74
94
|
// }
|
|
95
|
+
```
|
|
75
96
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
97
|
+
`decompress` automatically fills missing trailing values with `null`, so the roundtrip result is identical:
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
decompress(compress(data, { trimTrailingNulls: true }));
|
|
101
|
+
// [
|
|
102
|
+
// { name: 'Alice', age: 28, profile: { avatar: 'a.jpg', bio: 'Hello', file: null } },
|
|
103
|
+
// { name: 'Bob', age: 35, profile: { avatar: 'b.jpg', bio: null, file: null } },
|
|
104
|
+
// { name: 'Carol', age: null, profile: null }
|
|
105
|
+
// ]
|
|
79
106
|
```
|
|
80
107
|
|
|
81
108
|
#### Object Array Example (Order Scenario)
|
|
@@ -140,15 +167,22 @@ compress(orders);
|
|
|
140
167
|
// }
|
|
141
168
|
// specs keys take the union: order 1 has layout, order 2 has size → both kept, missing fields filled with null
|
|
142
169
|
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
170
|
+
compress(orders, { trimTrailingNulls: true });
|
|
171
|
+
// rows become:
|
|
172
|
+
// [
|
|
173
|
+
// ['A001', 'Alice', [
|
|
174
|
+
// ['Keyboard', 299, ['Black', '104-key']],
|
|
175
|
+
// ['Mouse', 99, ['White', null, '4000']]
|
|
176
|
+
// ]],
|
|
177
|
+
// ['A002', 'Bob', [
|
|
178
|
+
// ['Monitor', 1999, ['Silver']]
|
|
179
|
+
// ]]
|
|
180
|
+
// ]
|
|
147
181
|
```
|
|
148
182
|
|
|
149
183
|
### `decompress(compressed)`
|
|
150
184
|
|
|
151
|
-
Restores `{ keys, rows }` back to the original object array
|
|
185
|
+
Restores `{ keys, rows }` back to the original object array. Missing trailing values are automatically filled with `null`:
|
|
152
186
|
|
|
153
187
|
```js
|
|
154
188
|
const restored = decompress(compressed);
|
|
@@ -257,6 +291,11 @@ const parsed = parse(text);
|
|
|
257
291
|
const restored = decompress(parsed);
|
|
258
292
|
|
|
259
293
|
// restored is deep-equal to data
|
|
294
|
+
|
|
295
|
+
// Enable trimTrailingNulls for further compression
|
|
296
|
+
const compressedTrim = compress(data, { trimTrailingNulls: true });
|
|
297
|
+
const textTrim = stringify(compressedTrim);
|
|
298
|
+
// textTrim is shorter than text
|
|
260
299
|
```
|
|
261
300
|
|
|
262
301
|
### Compression Ratio Calculation
|
|
@@ -270,33 +309,35 @@ console.log(`Compression ratio: ${ratio}%`);
|
|
|
270
309
|
|
|
271
310
|
## Compression Results
|
|
272
311
|
|
|
273
|
-
Based on actual data from `compress-test.js` benchmarks (18 test cases,
|
|
312
|
+
Based on actual data from `compress-test.js` benchmarks (18 test cases, all roundtrip decompressions verified):
|
|
274
313
|
|
|
275
|
-
| Data Type |
|
|
276
|
-
|
|
277
|
-
| Simple users | 1,000 | 147.
|
|
278
|
-
| Simple users | 10,000 | 1.45 MB | 882.
|
|
279
|
-
| Nested users (with profile.social) | 1,000 | 235.
|
|
280
|
-
| Orders (1-5 items per order) | 500 | 166.
|
|
281
|
-
| School data (6 grades x 4 classes x 30 students) | 24 |
|
|
282
|
-
| Sparse fields (500 records x 30 fields) | 500 | 144.
|
|
283
|
-
| Sparse fields (2000 records x 50 fields) | 2,000 |
|
|
284
|
-
| Deep nesting (5-level org structure) | 5 | 634.
|
|
314
|
+
| Data Type | Count | Original | No trim | Ratio | Trim | Ratio | Diff |
|
|
315
|
+
|-----------|-------|----------|---------|-------|------|-------|------|
|
|
316
|
+
| Simple users | 1,000 | 147.61 KB | 87.12 KB | 40.98% | 87.12 KB | 40.98% | — |
|
|
317
|
+
| Simple users | 10,000 | 1.45 MB | 882.51 KB | 40.69% | 882.51 KB | 40.69% | — |
|
|
318
|
+
| Nested users (with profile.social) | 1,000 | 235.70 KB | 153.56 KB | 34.85% | 153.27 KB | 34.97% | -294 B |
|
|
319
|
+
| Orders (1-5 items per order) | 500 | 166.95 KB | 72.30 KB | 56.69% | 72.30 KB | 56.69% | — |
|
|
320
|
+
| School data (6 grades x 4 classes x 30 students) | 24 | 214.86 KB | 88.88 KB | 58.63% | 88.53 KB | 58.80% | -365 B |
|
|
321
|
+
| Sparse fields (500 records x 30 fields) | 500 | 144.61 KB | 45.40 KB | 68.60% | 45.13 KB | 68.79% | -276 B |
|
|
322
|
+
| Sparse fields (2000 records x 50 fields) | 2,000 | 951.94 KB | 293.62 KB | 69.16% | 292.49 KB | 69.27% | -1.13 KB |
|
|
323
|
+
| Deep nesting (5-level org structure) | 5 | 634.60 KB | 289.02 KB | 54.46% | 289.02 KB | 54.46% | — |
|
|
285
324
|
|
|
286
325
|
**Conclusions:**
|
|
287
326
|
1. Longer field names and more fields yield better compression
|
|
288
327
|
2. Object arrays (order items, student lists) show significant compression (55–59%)
|
|
289
328
|
3. Sparse fields achieve the highest compression — missing field nulls omitted as empty slots (67–69%)
|
|
290
|
-
4.
|
|
291
|
-
5.
|
|
329
|
+
4. `trimTrailingNulls` saves additional space when data has missing trailing fields (up to 1.48 KB / 5000 records)
|
|
330
|
+
5. When data has no missing fields, trim provides no extra benefit
|
|
331
|
+
6. Deeper nested structures achieve better compression
|
|
332
|
+
7. `stringify` quote omission further reduces text size
|
|
292
333
|
|
|
293
334
|
## Development
|
|
294
335
|
|
|
295
336
|
```bash
|
|
296
|
-
# Run tests
|
|
337
|
+
# Run tests (192 cases, 100% coverage)
|
|
297
338
|
npm test
|
|
298
339
|
|
|
299
|
-
# Run compression ratio benchmarks
|
|
340
|
+
# Run compression ratio benchmarks (with trim comparison)
|
|
300
341
|
node compress-test.js
|
|
301
342
|
```
|
|
302
343
|
|
package/compress-test.js
CHANGED
|
@@ -294,31 +294,39 @@ function generateDeepNested(orgCount = 2, deptPerOrg = 3, teamPerDept = 4, membe
|
|
|
294
294
|
* - compress → decompress → compress 应该得到相同结果(roundtrip)
|
|
295
295
|
* - 缺失字段会被填充为 null,这是预期的规范化行为
|
|
296
296
|
*/
|
|
297
|
-
function verifyRoundtrip(original, compressed) {
|
|
297
|
+
function verifyRoundtrip(original, compressed, opts) {
|
|
298
298
|
const decompressed = decompress(compressed);
|
|
299
|
-
const recompressed = compress(decompressed);
|
|
299
|
+
const recompressed = compress(decompressed, opts);
|
|
300
300
|
// 二次压缩后结构应该完全一致
|
|
301
301
|
return JSON.stringify(compressed) === JSON.stringify(recompressed);
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
function runTest(name, data) {
|
|
305
305
|
const originalSize = getByteSize(data);
|
|
306
|
+
|
|
307
|
+
// 默认(不 trim)
|
|
306
308
|
const compressed = compress(data);
|
|
307
309
|
const compressedSize = Buffer.byteLength(stringify(compressed), 'utf8');
|
|
308
|
-
|
|
309
|
-
// 验证解压正确性(roundtrip: compress→decompress→compress 结果一致)
|
|
310
310
|
const isCorrect = verifyRoundtrip(data, compressed);
|
|
311
|
-
|
|
312
311
|
const ratio = ((originalSize - compressedSize) / originalSize * 100).toFixed(2);
|
|
313
|
-
const savings = originalSize - compressedSize;
|
|
314
312
|
|
|
315
|
-
|
|
313
|
+
// trimTrailingNulls
|
|
314
|
+
const compressedTrim = compress(data, { trimTrailingNulls: true });
|
|
315
|
+
const compressedTrimSize = Buffer.byteLength(stringify(compressedTrim), 'utf8');
|
|
316
|
+
const isCorrectTrim = verifyRoundtrip(data, compressedTrim, { trimTrailingNulls: true });
|
|
317
|
+
const ratioTrim = ((originalSize - compressedTrimSize) / originalSize * 100).toFixed(2);
|
|
318
|
+
|
|
319
|
+
const diff = compressedSize - compressedTrimSize;
|
|
320
|
+
const diffStr = diff > 0 ? `-${formatBytes(diff)}` : diff === 0 ? '—' : `+${formatBytes(-diff)}`;
|
|
321
|
+
|
|
322
|
+
console.log(`\n${'='.repeat(72)}`);
|
|
316
323
|
console.log(`测试: ${name}`);
|
|
317
|
-
console.log('-'.repeat(
|
|
324
|
+
console.log('-'.repeat(72));
|
|
318
325
|
console.log(`对象数量: ${data.length}`);
|
|
319
326
|
console.log(`原始大小: ${formatBytes(originalSize)}`);
|
|
320
|
-
console.log(
|
|
321
|
-
console.log(
|
|
327
|
+
console.log(`不 trim: ${formatBytes(compressedSize).padStart(10)} (${ratio}%) ${isCorrect ? '✓' : '✗'}`);
|
|
328
|
+
console.log(`trim: ${formatBytes(compressedTrimSize).padStart(10)} (${ratioTrim}%) ${isCorrectTrim ? '✓' : '✗'}`);
|
|
329
|
+
console.log(`差值: ${diffStr}`);
|
|
322
330
|
|
|
323
331
|
return {
|
|
324
332
|
name,
|
|
@@ -326,6 +334,9 @@ function runTest(name, data) {
|
|
|
326
334
|
originalSize,
|
|
327
335
|
compressedSize,
|
|
328
336
|
ratio: parseFloat(ratio),
|
|
337
|
+
compressedTrimSize,
|
|
338
|
+
ratioTrim: parseFloat(ratioTrim),
|
|
339
|
+
diff,
|
|
329
340
|
isCorrect
|
|
330
341
|
};
|
|
331
342
|
}
|
|
@@ -375,26 +386,41 @@ function main() {
|
|
|
375
386
|
|
|
376
387
|
// 汇总
|
|
377
388
|
console.log('\n\n');
|
|
378
|
-
console.log('
|
|
379
|
-
console.log('║
|
|
380
|
-
console.log('
|
|
389
|
+
console.log('╔══════════════════════════════════════════════════════════════════════════╗');
|
|
390
|
+
console.log('║ 测试结果汇总 ║');
|
|
391
|
+
console.log('╚══════════════════════════════════════════════════════════════════════════╝');
|
|
381
392
|
console.log('\n');
|
|
382
|
-
|
|
393
|
+
|
|
383
394
|
const avgRatio = results.reduce((sum, r) => sum + r.ratio, 0) / results.length;
|
|
384
|
-
const
|
|
385
|
-
const
|
|
386
|
-
|
|
395
|
+
const avgRatioTrim = results.reduce((sum, r) => sum + r.ratioTrim, 0) / results.length;
|
|
396
|
+
const totalDiff = results.reduce((sum, r) => sum + r.diff, 0);
|
|
397
|
+
const bestCase = results.reduce((best, r) => r.ratioTrim > best.ratioTrim ? r : best);
|
|
398
|
+
const worstCase = results.reduce((worst, r) => r.ratioTrim < worst.ratioTrim ? r : worst);
|
|
399
|
+
|
|
387
400
|
console.log(`总测试数: ${results.length}`);
|
|
388
|
-
console.log(
|
|
389
|
-
console.log(
|
|
390
|
-
console.log(
|
|
391
|
-
|
|
401
|
+
console.log(`平均压缩率(不 trim): ${avgRatio.toFixed(2)}%`);
|
|
402
|
+
console.log(`平均压缩率(trim): ${avgRatioTrim.toFixed(2)}%`);
|
|
403
|
+
console.log(`总节省: ${formatBytes(totalDiff)}`);
|
|
404
|
+
console.log(`最佳压缩: ${bestCase.name} (trim ${bestCase.ratioTrim}%)`);
|
|
405
|
+
console.log(`最差压缩: ${worstCase.name} (trim ${worstCase.ratioTrim}%)`);
|
|
406
|
+
|
|
392
407
|
console.log('\n详细结果:');
|
|
393
|
-
console.log('-'.repeat(
|
|
394
|
-
console.log(
|
|
395
|
-
|
|
408
|
+
console.log('-'.repeat(100));
|
|
409
|
+
console.log(
|
|
410
|
+
`${'测试名称'.padEnd(34)} ${'数量'.padStart(6)}` +
|
|
411
|
+
`${'原始'.padStart(12)} ${'不 trim'.padStart(12)} ${'压缩率'.padStart(7)}` +
|
|
412
|
+
`${'trim'.padStart(12)} ${'压缩率'.padStart(7)} ${'差值'.padStart(12)}`
|
|
413
|
+
);
|
|
414
|
+
console.log('-'.repeat(100));
|
|
396
415
|
for (const r of results) {
|
|
397
|
-
|
|
416
|
+
const diffStr = r.diff > 0 ? `-${formatBytes(r.diff)}` : r.diff === 0 ? '—' : `+${formatBytes(-r.diff)}`;
|
|
417
|
+
console.log(
|
|
418
|
+
`${r.name.padEnd(34)} ${r.count.toString().padStart(6)}` +
|
|
419
|
+
`${formatBytes(r.originalSize).padStart(12)}` +
|
|
420
|
+
`${formatBytes(r.compressedSize).padStart(12)} ${r.ratio.toString().padStart(6)}%` +
|
|
421
|
+
`${formatBytes(r.compressedTrimSize).padStart(12)} ${r.ratioTrim.toString().padStart(6)}%` +
|
|
422
|
+
`${diffStr.padStart(12)}`
|
|
423
|
+
);
|
|
398
424
|
}
|
|
399
425
|
|
|
400
426
|
console.log('\n结论:');
|