Files
zszq-trs/YLErpWeb/fe-tests/swapPricePrecisionHelper.test.js
T

179 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* swapPricePrecisionHelper.test.js — 字符串精确十进制核心的特征测试
* ============================================================================
* 目的:swapPricePrecisionHelper.js 的组件事件(swapPriceInput.component.test.js)与
* 配置接线(swapPrecisionConfig.test.js)已有测试,但【字符串精确十进制核心】
* (shiftDecimal / roundDecimal / multiplyDecimal / getRule / format /
* roundForSubmit) 一直只有间接覆盖。精度即资损,这里用 golden 值冻结现状:
* 1. 全部走字符串数位运算,绕开 IEEE754 浮点陷阱(0.1*0.2、1.005.toFixed)
* 2. roundDecimal 是【绝对值上四舍五入】= AwayFromZero,与 unwindBondCalc
* 的 ExitYtm 约定同向;
* 3. roundDecimal 不补零(1.2 → '1.2'),补零是 formatCommon/formatFixed 的职责;
* 4. roundDecimal 对非法输入/负精度【原样返回不归一】,调用方别拿它当校验器;
* 5. getRule 的覆盖优先级:先查 defaults 有无该品种(没有直接 null),再看
* main.swapPricePrecision 覆盖,覆盖非法则回落 defaults。
*
* 做法:直接 require 源文件(jest v8 覆盖率据此插桩,readFileSync+new Function
* 的沙箱加载会让覆盖率统计看不见);配置覆盖用 global.main.swapPricePrecision
* 注入——模块内 global 绑定到 globalThis,与本测试文件的 global 同源,
* beforeEach/afterEach 清理防串扰。
*
* 运行:cd YLErpWeb/fe-tests && npx jest swapPricePrecisionHelper
*/
const helper = require('../wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js');
function withConfig(config, extras) {
if (config !== undefined) global.main = { swapPricePrecision: config };
if (extras && extras.otcformat) global.otcformat = extras.otcformat;
}
beforeEach(() => { delete global.main; delete global.otcformat; });
afterEach(() => { delete global.main; delete global.otcformat; });
describe('shiftDecimal:纯数位平移,不经浮点', () => {
test('右移补零 / 左移进小数', () => {
expect(helper.shiftDecimal('1.5', 1)).toBe('15');
expect(helper.shiftDecimal('1.5', -1)).toBe('0.15');
expect(helper.shiftDecimal('12.34', -3)).toBe('0.01234');
expect(helper.shiftDecimal('0.001', -2)).toBe('0.00001');
expect(helper.shiftDecimal('5', 2)).toBe('500');
});
test('保留符号;places=0 或非整数只做归一化', () => {
expect(helper.shiftDecimal('-1.5', 1)).toBe('-15');
expect(helper.shiftDecimal('1.50', 0)).toBe('1.50'); // 不动尾巴零
expect(helper.shiftDecimal('1.50', 1.5)).toBe('1.50');
});
test('归一化先行:首尾零/符号/科学计数法', () => {
expect(helper.shiftDecimal('007.50', 0)).toBe('7.50');
expect(helper.shiftDecimal('+7', 0)).toBe('7');
expect(helper.shiftDecimal('-0.000', 0)).toBe('0.000'); // 负零坍缩为无符号
expect(helper.shiftDecimal('1e-7', 0)).toBe('0.0000001');
expect(helper.shiftDecimal('1.23e5', 0)).toBe('123000');
});
test('非法输入返回 null,空白串返回空串', () => {
expect(helper.shiftDecimal('abc', 2)).toBeNull();
expect(helper.shiftDecimal('1.2.3', 2)).toBeNull();
expect(helper.shiftDecimal('', 2)).toBe('');
});
});
describe('roundDecimal:绝对值四舍五入(AwayFromZero),字符串路径无浮点陷阱', () => {
test('经典浮点陷阱对照:1.005 与 0.1×0.2 场景字符串路径给正确答案', () => {
expect(Number(1.005).toFixed(2)).toBe('1.00'); // 浮点路径错(冻结对照)
expect(helper.roundDecimal('1.005', 2)).toBe('1.01');
expect(helper.roundDecimal('2.345', 2)).toBe('2.35');
expect(helper.roundDecimal('2.344', 2)).toBe('2.34');
});
test('负数远离零(与 ExitYtm 约定同向)', () => {
expect(helper.roundDecimal('-2.345', 2)).toBe('-2.35');
expect(helper.roundDecimal('-2.5', 0)).toBe('-3');
expect(helper.roundDecimal('-0.00005', 4)).toBe('-0.0001');
});
test('进位链:跨数量级与跨整数位', () => {
expect(helper.roundDecimal('9.99', 1)).toBe('10.0');
expect(helper.roundDecimal('9.99', 0)).toBe('10');
expect(helper.roundDecimal('99.999', 2)).toBe('100.00');
expect(helper.roundDecimal('0.00005', 4)).toBe('0.0001');
});
test('不补零也不主动去零:位数不足原样返回,已有位数保留尾巴零', () => {
expect(helper.roundDecimal('1.2', 4)).toBe('1.2'); // 不足4位不补零(补零是 formatCommon 的职责)
expect(helper.roundDecimal('3.10000', 4)).toBe('3.1000'); // 已到4位:只取整不去零(去零是 format 的职责)
});
test('防御姿态:负精度/非法输入原样返回,不做校验', () => {
expect(helper.roundDecimal('2.345', -1)).toBe('2.345');
expect(helper.roundDecimal('2.345', 1.5)).toBe('2.345');
expect(helper.roundDecimal('abc', 2)).toBe('abc');
});
});
describe('multiplyDecimal:字符串精确乘法', () => {
test('0.1×0.2 精确为 0.02(浮点给 0.020000000000000004)', () => {
expect(helper.multiplyDecimal('0.1', '0.2')).toBe('0.02');
expect(helper.multiplyDecimal('0.1', '0.1')).toBe('0.01');
expect(helper.multiplyDecimal('1.1', '1.1')).toBe('1.21');
});
test('符号组合;保留小数标度(1.5×2=3.0 而非 3)', () => {
expect(helper.multiplyDecimal('1.5', '2')).toBe('3.0');
expect(helper.multiplyDecimal('-1.5', '2')).toBe('-3.0');
expect(helper.multiplyDecimal('-1.5', '-2')).toBe('3.0');
expect(helper.multiplyDecimal('-0.03', '-0.02')).toBe('0.0006');
});
test('大整数精确(超出 Number.MAX_SAFE_INTEGER)', () => {
expect(helper.multiplyDecimal('123456789', '987654321')).toBe('121932631112635269');
});
test('非法输入 null;空串按 0 处理', () => {
expect(helper.multiplyDecimal('abc', '1')).toBeNull();
expect(helper.multiplyDecimal('', '1')).toBe('0');
});
});
describe('getRule / getInputFormat / roundForSubmit:字段规则解析与覆盖优先级', () => {
test('defaults 字段级规则:债券三字段 vs 股票兜底', () => {
expect(helper.getRule('Bond', 'yield')).toEqual({ integerDigits: 2, precision: 4 });
expect(helper.getRule('Bond', 'grossPrice')).toEqual({ integerDigits: 6, precision: 9 });
// 债券顶层没有 integerDigits/precision:字段拼错 → null(后续走 umprice 兜底)
// 而 Stock 顶层有 → 字段拼错回落品种级规则。这个不对称冻结于此。
expect(helper.getRule('Bond', 'unknownField')).toBeNull();
expect(helper.getRule('Stock', 'anything')).toEqual({ integerDigits: 7, precision: 2 });
expect(helper.getRule('NoSuchType', 'yield')).toBeNull();
});
test('main.swapPricePrecision 覆盖优先于 defaults,非法覆盖回落 defaults', () => {
withConfig({ Bond: { yield: { integerDigits: 3, precision: 5 } } });
expect(helper.getRule('Bond', 'yield')).toEqual({ integerDigits: 3, precision: 5 });
withConfig({ Bond: { yield: { integerDigits: 0 } } });
const bad = helper;
expect(bad.getRule('Bond', 'yield')).toEqual({ integerDigits: 2, precision: 4 });
// defaults 里没有的品种,配置了也不认:覆盖只允许白名单内微调
withConfig({ BrandNewType: { yield: { integerDigits: 3, precision: 5 } } });
const unknown = helper;
expect(unknown.getRule('BrandNewType', 'yield')).toBeNull();
});
test('getInputFormat 合并 options', () => {
expect(helper.getInputFormat('Bond', 'yield', { negative: true }))
.toEqual({ negative: true, integerDigits: 2, precision: 4 });
expect(helper.getInputFormat('NoSuchType', 'yield', { negative: true }))
.toEqual({ negative: true });
});
test('roundForSubmit:按规则精度+offset 取整;空值/无规则原样返回', () => {
expect(helper.roundForSubmit('3.14159265', 'Bond', 'yield')).toBe('3.1416');
expect(helper.roundForSubmit('3.14159265', 'Bond', 'yield', 2)).toBe('3.141593');
expect(helper.roundForSubmit('', 'Bond', 'yield')).toBe('');
expect(helper.roundForSubmit('1.2', 'NoSuchType', 'yield')).toBe('1.2');
expect(helper.roundForSubmit('3.14159265', 'Bond', 'yield', 2.5)).toBe('3.1416'); // 非整数offset忽略
});
});
describe('format:字段级展示态(取整+去尾巴零)', () => {
test('按字段规则取整并去尾零', () => {
expect(helper.format('3.14159', 'Bond', 'yield')).toBe('3.1416');
expect(helper.format('3.10000', 'Bond', 'yield')).toBe('3.1');
expect(helper.format('99.5', 'Stock', 'whatever')).toBe('99.5');
expect(helper.format('', 'Bond', 'yield')).toBe('');
});
test('无规则时回落 otcformat.trading.umprice', () => {
withConfig(undefined, {
otcformat: { trading: { umprice: function (v) { return 'UM:' + v; } } }
});
expect(helper.format('1.23', 'NoSuchType', 'yield')).toBe('UM:1.23');
});
});