yeg
4 天以前 f4d7007b89cf565746af4623623a294b82666ff1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
import assert from 'node:assert/strict'
import { test } from 'node:test'
import XLSX from 'xlsx'
import { parseConsoleImport, consoleImportColumns } from '../src/utils/consoleDataImport.ts'
import * as importUtils from '../src/utils/consoleDataImport.ts'
import { formatTestDuration, parseTestDuration } from '../src/utils/testDataDuration.ts'
 
test('时长保留毫秒、累计小时和空值,非法时长不变成有效值', () => {
  assert.equal(formatTestDuration(90062.125), '25:01:02.125')
  assert.equal(parseTestDuration('25:01:02.125'), 90062.125)
  assert.equal(formatTestDuration(0), '0:00:00.000')
  assert.equal(formatTestDuration(null), '-')
  assert.equal(parseTestDuration('-'), null)
  for (const value of ['-1', '0:60:00.000', '2023-05-08 12:00:00', 'abc']) assert.ok(Number.isNaN(parseTestDuration(value)))
})
 
test('工步单页模板借同电池 record 时间,系统编号不充当来源编号', () => {
  const workbook = importUtils.createConsoleImportWorkbook('STEP')
  assert.deepEqual(workbook.SheetNames, ['工步', 'record'])
  assert.deepEqual(XLSX.utils.sheet_to_json(workbook.Sheets['工步'], { header: 1 })[0], [
    '记录编号', '电池', '循环序号', '工步序号', '工步名称', '工步时间(h:min:s.ms)', '容量(mAh)', '能量(mWh)',
    '起始电压(V)', '终止电压(V)', '充电中值电压(V)', '放电中值电压(V)',
  ])
  XLSX.utils.sheet_add_aoa(workbook.Sheets['工步'], [['step:999', 'BAT-1', 1, 1, '恒流放电', '1:02:32.125']], { origin: 'A2' })
  const records = [{ batteryNo: 'BAT-1', recordedAt: '2023-05-08 11:42:11' }, { batteryNo: 'BAT-2', recordedAt: '2023-05-08 12:42:11' }]
  XLSX.utils.sheet_add_aoa(workbook.Sheets.record, records.map(row => Object.keys(consoleImportColumns.REALTIME).map(key => ({ ...row, cycleNo: 1, stepNo: 1 })[key] ?? '')), { origin: 'A2' })
  const imported = parseConsoleImport(workbook, 'STEP', ['BAT-1', 'BAT-2'])
  assert.equal(imported.length, 1)
  assert.deepEqual(imported[0].errors, [])
  assert.equal(imported[0].data.recordNo, 'step:1:1')
  assert.equal(imported[0].data.recordedAt, 1683517331000)
  assert.equal(imported[0].data.stepDuration, 3752.125)
  workbook.Sheets.record.A2 = { t: 's', v: '' }
  workbook.Sheets.record.B2 = { t: 's', v: 'BAT-2' }
  assert.match(parseConsoleImport(workbook, 'STEP', ['BAT-1', 'BAT-2'])[0].errors.join(), /绝对.*无效/)
})
 
test('一键模板包含三个标准 Sheet,并逐 Sheet 预览与报告缺失表', () => {
  const workbook = importUtils.createConsoleImportWorkbook()
  assert.deepEqual(workbook.SheetNames, ['实时', '工步', '循环'])
  const row = { recordNo: '17', batteryNo: 'BAT-1', recordedAt: '2023-05-08 11:42:11', cycleNo: 1, stepNo: 1, stepName: '恒流放电', sequenceId: 17 }
  for (const [index, type] of ['REALTIME', 'STEP', 'CYCLE'].entries()) {
    const sheet = workbook.Sheets[workbook.SheetNames[index]]
    assert.deepEqual(XLSX.utils.sheet_to_json(sheet, { header: 1 })[0], Object.values(consoleImportColumns[type]))
    XLSX.utils.sheet_add_aoa(sheet, [Object.keys(consoleImportColumns[type]).map(key => row[key] ?? '')], { origin: 'A2' })
  }
  const sheets = importUtils.parseConsoleImportWorkbook(workbook, ['BAT-1'])
  assert.deepEqual(sheets.map(sheet => [sheet.type, sheet.rows.length, sheet.errors]), [['REALTIME', 1, []], ['STEP', 1, []], ['CYCLE', 1, []]])
  delete workbook.Sheets['循环']
  const missing = importUtils.parseConsoleImportWorkbook(workbook, ['BAT-1'])
  assert.match(missing[2].errors.join(), /缺少循环工作表/)
  assert.equal(missing[0].rows.length, 1)
})
 
function partyWorkbook() {
  const workbook = XLSX.utils.book_new()
  XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet([['设备信息']]), 'Unit')
  XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet([
    ['循环序号', '工步序号', '工步名称', '记录序号', '记录时间(h:min:s.ms)', '电压(V)', '电流(mA)', '容量(mAh)', '能量(mWh)', '绝对时间'],
    [1, 1, '恒流放电', 1, '0:00:00.000', 1.3349, -8165.1, 0, 0, '2023-05-08 11:42:11'],
    [1, 1, '恒流放电', 2, '1:02:32.000', 0.8999, -8165.1, 8512.2, 9717.8, '2023-05-08 12:44:43'],
  ]), 'record')
  return workbook
}
 
test('实时子页选择 record 并保留身份、香港绝对时间及系统单位', () => {
  const rows = parseConsoleImport(partyWorkbook(), 'REALTIME', ['BAT-1'], 'BAT-1')
  assert.equal(rows.length, 2)
  assert.deepEqual(rows[0].errors, [])
  assert.equal(rows[0].data.recordNo, '1')
  assert.equal(rows[0].data.sequenceId, 1)
  assert.equal(rows[0].data.cycleNo, 1)
  assert.equal(rows[0].data.stepNo, 1)
  assert.equal(rows[0].data.recordedAt, 1683517331000)
  assert.equal(rows[0].data.relativeTimeSeconds, 0)
  assert.equal(rows[1].data.relativeTimeSeconds, 3752)
  assert.equal(rows[0].data.deviceTimeRaw, '2023-05-08 11:42:11')
  assert.equal(rows[0].data.current, -8.1651)
  assert.equal(rows[1].data.capacity, 8.5122)
  assert.equal(rows[1].data.energy, 9.7178)
  for (const field of ['temperature', 'positivePotential', 'negativePotential']) {
    assert.equal(rows[0].data[field], null)
  }
})
 
test('汇总时间匹配组中存在非法绝对时间时拒绝,不选择更早有效行代替', () => {
  const workbook = partyWorkbook()
  XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet([
    Object.values(consoleImportColumns.CYCLE), ['', 'BAT-1', 1, 0, 0, 0, 0, 0],
  ]), '循环')
  workbook.Sheets.record.J3 = { t: 's', v: '错误时间' }
  assert.match(parseConsoleImport(workbook, 'CYCLE', ['BAT-1'])[0].errors.join(), /绝对.*无效/)
})
 
test('采集器三项测量只读取 Excel 已有值,缺列或空格不生成数值', () => {
  const workbook = partyWorkbook()
  XLSX.utils.sheet_add_aoa(workbook.Sheets.record, [
    ['温度(℃)', '正电位(V)', '负电位(V)'],
    [32.75, 1.8234, -0.2045],
    ['', '', ''],
  ], { origin: 'K1' })
  const rows = parseConsoleImport(workbook, 'REALTIME', ['BAT-1'])
  assert.deepEqual(rows[0].errors, [])
  assert.deepEqual(['temperature', 'positivePotential', 'negativePotential'].map(field => rows[0].data[field]), [32.75, 1.8234, -0.2045])
  assert.deepEqual(['temperature', 'positivePotential', 'negativePotential'].map(field => rows[1].data[field]), [null, null, null])
})
 
test('标准模板兼容文本、毫秒时间戳和 Excel 日期系统,并拒绝非法日期', () => {
  const make = (time, date1904 = false) => {
    const workbook = XLSX.utils.book_new()
    workbook.Workbook = { WBProps: { date1904 } }
    const data = { recordNo: '17', batteryNo: 'BAT-1', recordedAt: time, cycleNo: 1, stepNo: 1, stepName: '恒流放电', sequenceId: 17 }
    XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet([
      Object.values(consoleImportColumns.REALTIME), Object.keys(consoleImportColumns.REALTIME).map(field => data[field] ?? ''),
    ]), '实时模板')
    return parseConsoleImport(workbook, 'REALTIME', ['BAT-1'])[0]
  }
  assert.equal(make('2023/5/8 11:42:11.123').data.recordedAt, 1683517331123)
  assert.equal(make(1683517331123).data.recordedAt, 1683517331123)
  assert.equal(make(45054).data.recordedAt, 1683475200000)
  assert.equal(make(43592, true).data.recordedAt, 1683475200000)
  assert.match(make('2023-02-30 11:00:00').errors.join(), /第2行.*设备时间.*无效/)
  assert.match(make('1:02:32.000').errors.join(), /设备时间.*无效/)
})
 
test('多电池文件要求明确归属,保留空白行后的实际错误位置', () => {
  assert.match(parseConsoleImport(partyWorkbook(), 'REALTIME', ['BAT-1', 'BAT-2'])[0].errors.join(), /电池.*不能为空/)
  assert.equal(parseConsoleImport(partyWorkbook(), 'REALTIME', ['BAT-1', 'BAT-2'], 'BAT-2')[0].data.batteryNo, 'BAT-2')
  const workbook = partyWorkbook()
  XLSX.utils.sheet_add_aoa(workbook.Sheets.record, [[1, 1, '恒流放电', 19, '0:01:00.000', '不是数值', 0, 0, 0, '2023-05-08 11:44:11']], { origin: 'A20' })
  const last = parseConsoleImport(workbook, 'REALTIME', ['BAT-1']).at(-1)
  assert.equal(last.sourceRow, 20)
  assert.match(last.errors.join(), /第20行,列「电压\(V\)」/)
})
 
test('工步和循环仅转换对应页,时间取匹配 record 的末次绝对时间', () => {
  const workbook = partyWorkbook()
  XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet([
    ['循环序号', '工步序号', '工步名称', '工步时间(h:min:s.ms)', '容量(mAh)', '能量(mWh)', '起始电压(V)', '终止电压(V)', '充电中值电压(V)', '放电中值电压(V)'],
    [1, 1, '恒流放电', '1:02:32.000', 8512.2, 9717.8, 1.3349, 0.8999, 0, 1.1709],
  ]), 'step')
  XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet([
    ['循环序号', '充电容量(mAh)', '放电容量(mAh)', '充放电效率(%)', '充电能量(mWh)', '放电能量(mWh)'],
    [1, 0, 8512.2, 100, 0, 9717.8],
  ]), 'cycle')
  const step = parseConsoleImport(workbook, 'STEP', ['BAT-1'])[0]
  assert.deepEqual(step.errors, [])
  assert.equal(step.data.recordNo, 'step:1:1')
  assert.equal(step.data.recordedAt, 1683521083000)
  assert.equal(step.data.stepDuration, 3752)
  assert.equal(step.data.chargeMedianVoltage, 0)
  assert.equal(step.data.dischargeMedianVoltage, 1.1709)
  // 中值空格和缺列不变成零,也不回退读取同一文件的峰谷值。
  XLSX.utils.sheet_add_aoa(workbook.Sheets.step, [['', '', '充电峰值电压(V)', '放电谷值电压(V)'], ['', '', 1.8, 0.8]], { origin: 'I1' })
  const absent = parseConsoleImport(workbook, 'STEP', ['BAT-1'])[0]
  assert.deepEqual(absent.errors, [])
  assert.equal(absent.data.chargeMedianVoltage, null)
  assert.equal(absent.data.dischargeMedianVoltage, null)
  XLSX.utils.sheet_add_aoa(workbook.Sheets.step, [['充电中值电压(V)', '放电中值电压(V)'], ['', '不是数字']], { origin: 'I1' })
  const invalid = parseConsoleImport(workbook, 'STEP', ['BAT-1'])[0]
  assert.equal(invalid.data.chargeMedianVoltage, null)
  assert.match(invalid.errors.join(), /放电中值电压.*必须是数字或空值/)
  const cycle = parseConsoleImport(workbook, 'CYCLE', ['BAT-1'])[0]
  assert.deepEqual(cycle.errors, [])
  assert.equal(cycle.data.recordNo, 'cycle:1')
  assert.equal(cycle.data.recordedAt, 1683521083000)
  assert.equal(cycle.data.dischargeCapacity, 8.5122)
  delete workbook.Sheets.record
  assert.match(parseConsoleImport(workbook, 'STEP', ['BAT-1'])[0].errors.join(), /第2行.*绝对时间.*无效/)
})