@nstc-business/imes 1.0.35 → 1.0.37
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/package.json +1 -1
- package/src/components/measureCalc/MeasureStatCell.vue +6 -5
- package/src/components/measureCalc/helpers.js +4 -9
- package/src/components/measureCalc/ui/InputNumber.vue +253 -14
- package/src/components/measureCalc/ui/numberFormat.js +105 -0
- package/src/components/measureCalc/ui/numberFormat.test.js +65 -0
package/package.json
CHANGED
|
@@ -85,8 +85,9 @@
|
|
|
85
85
|
|
|
86
86
|
<script>
|
|
87
87
|
import { InputNumber } from './ui'
|
|
88
|
-
import {
|
|
88
|
+
import { addThousands } from '../common'
|
|
89
89
|
import { formatScoreText } from './helpers'
|
|
90
|
+
import { formatFixedDecimal } from './ui/numberFormat'
|
|
90
91
|
|
|
91
92
|
export default {
|
|
92
93
|
name: 'MeasureStatCell',
|
|
@@ -120,9 +121,9 @@ export default {
|
|
|
120
121
|
originalValue(cell) {
|
|
121
122
|
const val = cell.indicatorResult
|
|
122
123
|
if (val === '' || val == null) return ''
|
|
123
|
-
let text =
|
|
124
|
+
let text = formatFixedDecimal(val, this.digits)
|
|
124
125
|
if (cell.showStyle === 2) {
|
|
125
|
-
text = addThousands(
|
|
126
|
+
text = addThousands(text)
|
|
126
127
|
}
|
|
127
128
|
const suffix = cell.displayStyle === 1 && (val || val === 0) ? '%' : ''
|
|
128
129
|
return text + suffix
|
|
@@ -144,9 +145,9 @@ export default {
|
|
|
144
145
|
const val = cell.value
|
|
145
146
|
if (val === '' || val == null) return ''
|
|
146
147
|
if (typeof val === 'object' && val.scoreText) return val.scoreText
|
|
147
|
-
let text = val || val === 0 ?
|
|
148
|
+
let text = val || val === 0 ? formatFixedDecimal(val, this.digits) : ''
|
|
148
149
|
if (cell.showStyle === 2 && text) {
|
|
149
|
-
text = addThousands(
|
|
150
|
+
text = addThousands(text)
|
|
150
151
|
}
|
|
151
152
|
const suffix = cell.displayStyle === 1 && (val || val === 0) ? '%' : ''
|
|
152
153
|
return text + suffix
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { N } from 'n20-common-lib'
|
|
12
12
|
import { addThousands } from '../common'
|
|
13
13
|
import { resolveManualFillValue, statItemChildren } from './statModelHelper'
|
|
14
|
+
import { formatFixedDecimal } from './ui/numberFormat'
|
|
14
15
|
|
|
15
16
|
export const ITEM_TYPE = {
|
|
16
17
|
CALC_MODEL: 11,
|
|
@@ -140,8 +141,6 @@ export function sumIndicatorScores(nodes) {
|
|
|
140
141
|
items.forEach(n => {
|
|
141
142
|
const val = nodeScoreValue(n)
|
|
142
143
|
if (val === null) return
|
|
143
|
-
const num = Number(val)
|
|
144
|
-
if (Number.isNaN(num)) return
|
|
145
144
|
has = true
|
|
146
145
|
total += num
|
|
147
146
|
})
|
|
@@ -237,9 +236,7 @@ export function modelResultDigits(modelWrap) {
|
|
|
237
236
|
*/
|
|
238
237
|
export function formatModelResultText(val, modelWrap, options = {}) {
|
|
239
238
|
if (val === '' || val == null) return ''
|
|
240
|
-
|
|
241
|
-
if (Number.isNaN(num)) return String(val)
|
|
242
|
-
let text = num.toFixed(modelResultDigits(modelWrap))
|
|
239
|
+
let text = formatFixedDecimal(val, modelResultDigits(modelWrap))
|
|
243
240
|
if (options.thousands) {
|
|
244
241
|
text = addThousands(text)
|
|
245
242
|
}
|
|
@@ -253,9 +250,7 @@ export function formatModelResultText(val, modelWrap, options = {}) {
|
|
|
253
250
|
*/
|
|
254
251
|
export function formatScoreText(val, options = {}) {
|
|
255
252
|
if (val === '' || val == null) return ''
|
|
256
|
-
|
|
257
|
-
if (Number.isNaN(num)) return String(val)
|
|
258
|
-
let text = num.toFixed(SCORE_DIGITS)
|
|
253
|
+
let text = formatFixedDecimal(val, SCORE_DIGITS)
|
|
259
254
|
if (options.thousands) {
|
|
260
255
|
text = addThousands(text)
|
|
261
256
|
}
|
|
@@ -396,7 +391,7 @@ export function indicatorResultText(row, modelWrap) {
|
|
|
396
391
|
const suffix =
|
|
397
392
|
row.displayStyle === 1 && (raw || raw === 0) ? '%' : ''
|
|
398
393
|
if (raw || raw === 0) {
|
|
399
|
-
value =
|
|
394
|
+
value = formatFixedDecimal(raw, digitsCalc(row, modelWrap))
|
|
400
395
|
if (row.showStyle === 2) {
|
|
401
396
|
value = addThousands(value)
|
|
402
397
|
}
|
|
@@ -1,24 +1,146 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
<div
|
|
3
|
+
class="n20-num-w"
|
|
4
|
+
@mouseenter="isHove = true"
|
|
5
|
+
@mouseleave="isHove = false"
|
|
6
|
+
>
|
|
7
|
+
<el-input
|
|
8
|
+
ref="input"
|
|
9
|
+
v-model="valueStr"
|
|
10
|
+
v-bind="$attrs"
|
|
11
|
+
class="n20-stc"
|
|
12
|
+
:disabled="disabled"
|
|
13
|
+
:placeholder="phd"
|
|
14
|
+
:clearable="isClearable"
|
|
15
|
+
:validate-event="false"
|
|
16
|
+
@focus="focusFn"
|
|
17
|
+
@input="inputFn"
|
|
18
|
+
@blur="blurFn"
|
|
19
|
+
@clear="clearFn"
|
|
20
|
+
@change="commit"
|
|
21
|
+
@keydown.native="stepFn"
|
|
22
|
+
@copy.native="onCopy"
|
|
23
|
+
@cut.native="onCopy"
|
|
24
|
+
>
|
|
25
|
+
<template v-if="type === 'rate'">
|
|
26
|
+
<span v-if="suffixV" slot="suffix" class="el-input__icon">{{
|
|
27
|
+
suffix
|
|
28
|
+
}}</span>
|
|
29
|
+
</template>
|
|
30
|
+
</el-input>
|
|
31
|
+
</div>
|
|
9
32
|
</template>
|
|
10
33
|
|
|
11
34
|
<script>
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
35
|
+
import { formatFixedDecimal } from './numberFormat'
|
|
36
|
+
import { resolveNumberClipboardText, clearInputMaxlength } from './numberCopy'
|
|
37
|
+
import { addDecimalText } from './numberStep'
|
|
38
|
+
import { addThousands } from '../../common'
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 输入过程允许的中间态(去千分位后校验):空、-、.、1、1.、.5、-1.2 等
|
|
42
|
+
*/
|
|
43
|
+
const NUM_RE = /^[+-]?(\d*(\.\d*)?|\.\d*)$/
|
|
17
44
|
|
|
18
45
|
export default {
|
|
19
46
|
name: 'InputNumber',
|
|
20
47
|
inheritAttrs: false,
|
|
21
|
-
|
|
48
|
+
props: {
|
|
49
|
+
value: {
|
|
50
|
+
type: [Number, String],
|
|
51
|
+
default: undefined
|
|
52
|
+
},
|
|
53
|
+
type: {
|
|
54
|
+
type: String,
|
|
55
|
+
default: 'money',
|
|
56
|
+
validator(v) {
|
|
57
|
+
return ['money', 'rate'].includes(v)
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
min: {
|
|
61
|
+
type: Number,
|
|
62
|
+
default: -9999999999999.99
|
|
63
|
+
},
|
|
64
|
+
max: {
|
|
65
|
+
type: Number,
|
|
66
|
+
default: 9999999999999.99
|
|
67
|
+
},
|
|
68
|
+
step: {
|
|
69
|
+
type: Number,
|
|
70
|
+
default: 1
|
|
71
|
+
},
|
|
72
|
+
stepStrictly: {
|
|
73
|
+
type: Boolean,
|
|
74
|
+
default: false
|
|
75
|
+
},
|
|
76
|
+
disabled: {
|
|
77
|
+
type: Boolean,
|
|
78
|
+
default: undefined
|
|
79
|
+
},
|
|
80
|
+
isClearable: {
|
|
81
|
+
type: Boolean,
|
|
82
|
+
default: false
|
|
83
|
+
},
|
|
84
|
+
placeholder: {
|
|
85
|
+
type: String,
|
|
86
|
+
default: undefined
|
|
87
|
+
},
|
|
88
|
+
dNum: {
|
|
89
|
+
type: Number,
|
|
90
|
+
default: undefined
|
|
91
|
+
},
|
|
92
|
+
format: {
|
|
93
|
+
type: String,
|
|
94
|
+
default: undefined
|
|
95
|
+
},
|
|
96
|
+
suffix: {
|
|
97
|
+
type: String,
|
|
98
|
+
default: '%'
|
|
99
|
+
},
|
|
100
|
+
rangeAuto: {
|
|
101
|
+
type: Boolean,
|
|
102
|
+
default: false
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
data() {
|
|
106
|
+
return {
|
|
107
|
+
valueStr: '',
|
|
108
|
+
isHove: false,
|
|
109
|
+
isFocus: false,
|
|
110
|
+
/** 输入过程/上次提交保留的精确数字文本(去千分位),回写不经过 Number() */
|
|
111
|
+
preciseText: '',
|
|
112
|
+
preValue: ''
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
computed: {
|
|
116
|
+
suffixV() {
|
|
117
|
+
if (!this.isClearable) return true
|
|
118
|
+
if (!this.valueStr) return true
|
|
119
|
+
if (!(this.isHove || this.isFocus)) return true
|
|
120
|
+
return false
|
|
121
|
+
},
|
|
122
|
+
phd() {
|
|
123
|
+
return this.placeholder || '请输入'
|
|
124
|
+
},
|
|
125
|
+
fNum() {
|
|
126
|
+
if (this.format) {
|
|
127
|
+
const i = this.format.indexOf('.')
|
|
128
|
+
return i === -1 ? 0 : this.format.length - i - 1
|
|
129
|
+
}
|
|
130
|
+
if (this.dNum || this.dNum === 0) {
|
|
131
|
+
return this.dNum
|
|
132
|
+
}
|
|
133
|
+
return this.type === 'rate' ? 4 : 2
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
watch: {
|
|
137
|
+
value: {
|
|
138
|
+
handler() {
|
|
139
|
+
this.renderFromValue()
|
|
140
|
+
},
|
|
141
|
+
immediate: true
|
|
142
|
+
}
|
|
143
|
+
},
|
|
22
144
|
mounted() {
|
|
23
145
|
this.clearMaxlength()
|
|
24
146
|
},
|
|
@@ -57,10 +179,127 @@ export default {
|
|
|
57
179
|
)
|
|
58
180
|
: ''
|
|
59
181
|
const text = resolveNumberClipboardText(input && input.value, selected)
|
|
60
|
-
const clip =
|
|
182
|
+
const clip =
|
|
183
|
+
(e && e.clipboardData) ||
|
|
184
|
+
(e && e.originalEvent && e.originalEvent.clipboardData)
|
|
61
185
|
if (!text || text === selected || !clip) return
|
|
62
186
|
e.preventDefault()
|
|
63
187
|
clip.setData('text/plain', text)
|
|
188
|
+
},
|
|
189
|
+
/**
|
|
190
|
+
* 内部 input 当前输入(去千分位、去空格)
|
|
191
|
+
*/
|
|
192
|
+
rawText() {
|
|
193
|
+
const input = this.$el && this.$el.querySelector('input')
|
|
194
|
+
return input
|
|
195
|
+
? String(input.value || '')
|
|
196
|
+
.replace(/,/g, '')
|
|
197
|
+
.trim()
|
|
198
|
+
: ''
|
|
199
|
+
},
|
|
200
|
+
/**
|
|
201
|
+
* 外部 value 变化 -> 精确格式化展示
|
|
202
|
+
* 字符串入参直接按十进制处理(formatFixedDecimal 不经过 Number),不丢高精度
|
|
203
|
+
*/
|
|
204
|
+
renderFromValue() {
|
|
205
|
+
const val = this.value
|
|
206
|
+
if (val === '' || val == null) {
|
|
207
|
+
this.valueStr = ''
|
|
208
|
+
this.preciseText = ''
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
const text = formatFixedDecimal(val, this.fNum)
|
|
212
|
+
this.preciseText = text
|
|
213
|
+
this.valueStr = addThousands(text)
|
|
214
|
+
},
|
|
215
|
+
focusFn() {
|
|
216
|
+
this.isFocus = true
|
|
217
|
+
if (!this.disabled && this.valueStr) {
|
|
218
|
+
this.valueStr = this.valueStr.replace(/,/g, '')
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
/**
|
|
222
|
+
* 输入过程只做合法性校验,不格式化(保留完整精度)
|
|
223
|
+
*/
|
|
224
|
+
inputFn(valStr) {
|
|
225
|
+
const raw = String(valStr || '')
|
|
226
|
+
.replace(/,/g, '')
|
|
227
|
+
.trim()
|
|
228
|
+
if (raw !== '-' && raw !== '' && !NUM_RE.test(raw)) {
|
|
229
|
+
this.valueStr = this.preValue
|
|
230
|
+
} else {
|
|
231
|
+
this.preValue = valStr
|
|
232
|
+
this.preciseText = raw
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
blurFn() {
|
|
236
|
+
this.isFocus = false
|
|
237
|
+
this.commit()
|
|
238
|
+
this.$emit('blur')
|
|
239
|
+
},
|
|
240
|
+
/**
|
|
241
|
+
* 失焦提交:按 fNum 精确四舍五入(字符串路径),回写字符串值,不经过 Number()
|
|
242
|
+
*/
|
|
243
|
+
commit() {
|
|
244
|
+
if (this.disabled) return
|
|
245
|
+
const raw = this.preciseText
|
|
246
|
+
if (raw === '' || raw == null) {
|
|
247
|
+
this.valueStr = ''
|
|
248
|
+
const oVal = this.value
|
|
249
|
+
if (oVal || oVal === 0) {
|
|
250
|
+
this.$emit('input', undefined)
|
|
251
|
+
this.$emit('change', undefined)
|
|
252
|
+
}
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
let text = formatFixedDecimal(raw, this.fNum)
|
|
256
|
+
// min/max 钳制(值域内 Number 比较安全;钳制值用精确字符串格式化)
|
|
257
|
+
const num = Number(text)
|
|
258
|
+
if (num < this.min) {
|
|
259
|
+
text = formatFixedDecimal(this.min, this.fNum)
|
|
260
|
+
} else if (num > this.max) {
|
|
261
|
+
text = formatFixedDecimal(this.max, this.fNum)
|
|
262
|
+
}
|
|
263
|
+
this.preciseText = text
|
|
264
|
+
this.valueStr = addThousands(text)
|
|
265
|
+
const oVal = this.value
|
|
266
|
+
if (oVal !== text) {
|
|
267
|
+
this.$emit('input', text)
|
|
268
|
+
this.$emit('change', text)
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
clearFn() {
|
|
272
|
+
this.$emit('clear')
|
|
273
|
+
},
|
|
274
|
+
/**
|
|
275
|
+
* 上下键步进:基于精确字符串做十进制加减,避免 number 累加丢精度
|
|
276
|
+
*/
|
|
277
|
+
stepFn(ev) {
|
|
278
|
+
if (
|
|
279
|
+
(ev.code === 'ArrowUp' || ev.code === 'ArrowDown') &&
|
|
280
|
+
!this.disabled
|
|
281
|
+
) {
|
|
282
|
+
const raw = this.rawText()
|
|
283
|
+
if (raw === '' || raw === '-' || raw === '.') return
|
|
284
|
+
if (!NUM_RE.test(raw)) return
|
|
285
|
+
ev.preventDefault()
|
|
286
|
+
const delta = ev.code === 'ArrowUp' ? this.step : -this.step
|
|
287
|
+
let text = addDecimalText(raw, delta)
|
|
288
|
+
if (this.stepStrictly) {
|
|
289
|
+
const num = Number(text)
|
|
290
|
+
if (num % this.step !== 0) {
|
|
291
|
+
text = String(Math.round(num / this.step) * this.step)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const num = Number(text)
|
|
295
|
+
if (num < this.min) {
|
|
296
|
+
text = formatFixedDecimal(this.min, this.fNum)
|
|
297
|
+
} else if (num > this.max) {
|
|
298
|
+
text = formatFixedDecimal(this.max, this.fNum)
|
|
299
|
+
}
|
|
300
|
+
this.preciseText = text
|
|
301
|
+
this.valueStr = addThousands(text)
|
|
302
|
+
}
|
|
64
303
|
}
|
|
65
304
|
}
|
|
66
305
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 测算数字展示格式化
|
|
3
|
+
* 时间:2026-09-14
|
|
4
|
+
* 人员:AI && 徐红飞
|
|
5
|
+
* 入参:val 原始值;digits 小数位数
|
|
6
|
+
* 出参:指定小数位的十进制字符串
|
|
7
|
+
* 方法内容:不用 Number.toFixed。JS 二进制浮点会把 132898923.63 格式化成 132898923.6299999952
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 按指定小数位四舍五入格式化数字
|
|
12
|
+
* 时间:2026-09-14
|
|
13
|
+
* 人员:AI && 徐红飞
|
|
14
|
+
* 入参:val 数字或数字字符串;digits 小数位数
|
|
15
|
+
* 出参:格式化后的字符串;空值返回 '';非数字字符串原样返回
|
|
16
|
+
* 方法内容:先转最短十进制串再按位四舍五入补零,避开原生 toFixed 的精度噪声;
|
|
17
|
+
* 字符串输入直接按十进制字符串处理,不经过 Number()(双精度只有约 15~17 位有效数字,
|
|
18
|
+
* 字符串一旦转 number 高精度小数即被舍入,例如 '160835267.0579509644' 会变成
|
|
19
|
+
* 160835267.05795097,精度无法恢复)
|
|
20
|
+
*/
|
|
21
|
+
export function formatFixedDecimal(val, digits) {
|
|
22
|
+
if (val === '' || val == null) return ''
|
|
23
|
+
const raw = typeof val === 'string' ? val.replace(/,/g, '').trim() : val
|
|
24
|
+
if (raw === '') return ''
|
|
25
|
+
const d = digits == null || digits === '' ? 2 : Number(digits)
|
|
26
|
+
if (!Number.isFinite(d) || d < 0) return String(val)
|
|
27
|
+
|
|
28
|
+
// 字符串数字:直接按十进制字符串四舍五入,不经过 Number()
|
|
29
|
+
if (typeof raw === 'string') {
|
|
30
|
+
if (/[eE]/.test(raw)) {
|
|
31
|
+
// 科学计数法字符串(如 '1.6e8'):转 number 后展开,保持原行为
|
|
32
|
+
const n = Number(raw)
|
|
33
|
+
if (Number.isNaN(n)) return String(val)
|
|
34
|
+
return roundPlainDecimal(numberToPlainString(n), d)
|
|
35
|
+
}
|
|
36
|
+
if (!/^[+-]?(\d+(\.\d*)?|\.\d+)$/.test(raw)) {
|
|
37
|
+
return String(val) // 非数字字符串原样返回
|
|
38
|
+
}
|
|
39
|
+
return roundPlainDecimal(raw, d)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const n = raw
|
|
43
|
+
if (Number.isNaN(n)) return String(val)
|
|
44
|
+
return roundPlainDecimal(numberToPlainString(n), d)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 把 Number 转成普通十进制字符串(处理科学计数法)
|
|
49
|
+
* 时间:2026-09-14
|
|
50
|
+
* 人员:AI && 徐红飞
|
|
51
|
+
* 入参:n 有限数字
|
|
52
|
+
* 出参:不含 e 的十进制字符串
|
|
53
|
+
* 方法内容:String(n) 遇到科学计数法时按指数展开
|
|
54
|
+
*/
|
|
55
|
+
function numberToPlainString(n) {
|
|
56
|
+
const s = String(n)
|
|
57
|
+
if (!/[eE]/.test(s)) return s
|
|
58
|
+
const sign = s.charAt(0) === '-' ? '-' : ''
|
|
59
|
+
const body = sign ? s.slice(1) : s
|
|
60
|
+
const [coeff, expStr] = body.split(/[eE]/)
|
|
61
|
+
let exp = Number(expStr)
|
|
62
|
+
const digits = coeff.replace('.', '')
|
|
63
|
+
const decLen = (coeff.split('.')[1] || '').length
|
|
64
|
+
exp -= decLen
|
|
65
|
+
if (exp >= 0) {
|
|
66
|
+
return sign + digits + '0'.repeat(exp)
|
|
67
|
+
}
|
|
68
|
+
const zeros = Math.abs(exp) - 1
|
|
69
|
+
return sign + '0.' + '0'.repeat(zeros) + digits
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 对普通十进制字符串按小数位四舍五入并补零
|
|
74
|
+
* 时间:2026-09-14
|
|
75
|
+
* 人员:AI && 徐红飞
|
|
76
|
+
* 入参:str 十进制字符串;digits 小数位数
|
|
77
|
+
* 出参:补齐小数位后的字符串
|
|
78
|
+
* 方法内容:用整数进位避免浮点乘法误差
|
|
79
|
+
*/
|
|
80
|
+
function roundPlainDecimal(str, digits) {
|
|
81
|
+
const neg = str.charAt(0) === '-'
|
|
82
|
+
if (neg) str = str.slice(1)
|
|
83
|
+
let [intPart, frac = ''] = str.split('.')
|
|
84
|
+
intPart = intPart || '0'
|
|
85
|
+
if (digits === 0) {
|
|
86
|
+
const roundUp = (frac.charAt(0) || '0') >= '5'
|
|
87
|
+
let i = BigInt(intPart)
|
|
88
|
+
if (roundUp) i += 1n
|
|
89
|
+
return (neg && i !== 0n ? '-' : '') + i.toString()
|
|
90
|
+
}
|
|
91
|
+
frac = frac.padEnd(digits + 1, '0')
|
|
92
|
+
const keep = frac.slice(0, digits)
|
|
93
|
+
const next = frac.charAt(digits)
|
|
94
|
+
if (next >= '5') {
|
|
95
|
+
const width = intPart.length + digits
|
|
96
|
+
const asInt = BigInt(intPart + keep) + 1n
|
|
97
|
+
let s = asInt.toString()
|
|
98
|
+
if (s.length < width) s = s.padStart(width, '0')
|
|
99
|
+
intPart = s.slice(0, s.length - digits) || '0'
|
|
100
|
+
frac = s.slice(s.length - digits)
|
|
101
|
+
} else {
|
|
102
|
+
frac = keep
|
|
103
|
+
}
|
|
104
|
+
return (neg ? '-' : '') + intPart + '.' + frac
|
|
105
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 数字展示精度:禁止 Number.toFixed 把 132898923.63 显示成 132898923.6299999952
|
|
3
|
+
*/
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
|
|
7
|
+
const src = fs
|
|
8
|
+
.readFileSync(path.join(__dirname, 'numberFormat.js'), 'utf8')
|
|
9
|
+
.replace(/export /g, '')
|
|
10
|
+
const exportsFromSrc = {}
|
|
11
|
+
new Function('exports', src + '\nexports.formatFixedDecimal = formatFixedDecimal')(
|
|
12
|
+
exportsFromSrc
|
|
13
|
+
)
|
|
14
|
+
const { formatFixedDecimal } = exportsFromSrc
|
|
15
|
+
|
|
16
|
+
const nativeBug = (132898923.63).toFixed(10)
|
|
17
|
+
if (nativeBug !== '132898923.6299999952') {
|
|
18
|
+
throw new Error('用例前提变化:原生 toFixed(10) 不再复现 6299999952,实际: ' + nativeBug)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const padded = formatFixedDecimal(132898923.63, 10)
|
|
22
|
+
if (padded !== '132898923.6300000000') {
|
|
23
|
+
throw new Error('10 位应补零为 132898923.6300000000,实际: ' + padded)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const two = formatFixedDecimal(132898923.63, 2)
|
|
27
|
+
if (two !== '132898923.63') {
|
|
28
|
+
throw new Error('2 位应为 132898923.63,实际: ' + two)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const empty = formatFixedDecimal(null, 10)
|
|
32
|
+
if (empty !== '') {
|
|
33
|
+
throw new Error('空值应返回空字符串,实际: ' + empty)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const text = formatFixedDecimal('abc', 2)
|
|
37
|
+
if (text !== 'abc') {
|
|
38
|
+
throw new Error('非数字字符串应原样返回,实际: ' + text)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const rounded = formatFixedDecimal(1.239, 2)
|
|
42
|
+
if (rounded !== '1.24') {
|
|
43
|
+
throw new Error('四舍五入 1.239 保留 2 位应为 1.24,实际: ' + rounded)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 高精度 number 字面量:双精度只有约 15~17 位有效数字,入参在解析时已舍入为最近可表示值,
|
|
47
|
+
// 格式化结果只能基于该近似值(语言限制,非格式化层可恢复)
|
|
48
|
+
const highPrecNumber = formatFixedDecimal(160835267.0579509644, 10)
|
|
49
|
+
if (highPrecNumber !== '160835267.0579509700') {
|
|
50
|
+
throw new Error('number 入参受双精度限制应为 160835267.0579509700,实际: ' + highPrecNumber)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 高精度数字字符串:不经过 Number(),应完整保留每一位小数
|
|
54
|
+
const highPrecString = formatFixedDecimal('160835267.0579509644', 10)
|
|
55
|
+
if (highPrecString !== '160835267.0579509644') {
|
|
56
|
+
throw new Error('字符串入参应完整保留 160835267.0579509644,实际: ' + highPrecString)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 字符串入参四舍五入进位
|
|
60
|
+
const carry = formatFixedDecimal('9.99999', 4)
|
|
61
|
+
if (carry !== '10.0000') {
|
|
62
|
+
throw new Error('字符串进位 9.99999 保留 4 位应为 10.0000,实际: ' + carry)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log('numberFormat 用例全部通过')
|