千分位格式化增强
a-calc 提供了灵活且强大的千分位格式化功能,支持全球各种数字格式规范。
设计概述
千分位功能采用分层设计,从底层到顶层依次为:
优先级从低到高:
┌─────────────────────────────────────────────┐
│ 用户显式指定(格式字符串 !t:预设名) │ ← 最高优先级
├─────────────────────────────────────────────┤
│ 单位关联(输出单位自动触发对应千分位配置) │
├─────────────────────────────────────────────┤
│ 用户自定义预设(_thousands) │
├─────────────────────────────────────────────┤
│ 系统内置预设(THOUSANDS_PRESETS) │
├─────────────────────────────────────────────┤
│ 系统默认兜底预设(default) │ ← 最低优先级
└─────────────────────────────────────────────┘每个预设都是原子配置的集合,系统采用可插拔设计,用户可以创建与系统结构相同的自定义预设。
原子配置
千分位格式化由 6 个原子配置组成:
| 配置 | 类型 | 默认值 | 说明 |
|---|---|---|---|
sep | string | ',' | 千分位分隔符 |
point | string | '.' | 小数点符号 |
grouping | number[] | [3] | 分组规则 |
min_len | number | 0 | 最小触发位数,0 表示不限制 |
point_group | boolean | false | 是否对小数部分分组 |
fn | function | null | null | 自定义格式化函数(逃生舱) |
sep - 千分位分隔符
整数部分的分组分隔符:
// 不同地区使用不同的分隔符
sep: ',' // 美国/英国/中国: 1,234,567
sep: '.' // 德国/意大利: 1.234.567
sep: "'" // 瑞士: 1'234'567
sep: ' ' // 法国/ISO标准: 1 234 567
sep: '_' // 编程场景: 1_234_567point - 小数点符号
小数点分隔符:
// 不同地区使用不同的小数点
point: '.' // 美国/英国/中国: 1,234.56
point: ',' // 德国/法国: 1.234,56注意
sep 和 point 通常是配对使用的。如果千分位用逗号,小数点就要用点;如果千分位用点,小数点就要用逗号。否则会造成解析歧义。
grouping - 分组规则
定义每组包含多少位数字,使用数组表示:
grouping: [3] // 每 3 位分组(最常见)
grouping: [3, 2] // 印度格式
grouping: [4] // 万进制(中国/日本)分组规则详解
基础规则:从右向左应用
grouping: [3]
1234567 → 1,234,567
←←←←←←←←← 从右向左,每 3 位分组数组含义:
- 第一个值:最右边的分组位数
- 第二个值及之后:后续的分组位数
- 数组最后一个值会被重复应用
grouping: [3] // 等同于 [3, 3, 3, 3...]
grouping: [3, 2] // 等同于 [3, 2, 2, 2...]
grouping: [4] // 等同于 [4, 4, 4, 4...]印度格式示例
印度数字格式是一个经典的不规则分组案例:
grouping: [3, 2]
// 应用过程(从右向左):
1234567
↑ 从最右边开始
567 ← 第一组取 3 位
34 ← 第二组取 2 位
12 ← 第三组取 2 位(重复最后一个值)
// 结果:12,34,567万进制示例
中国和日本的万进制格式:
grouping: [4]
// 应用过程:
123456789
6789 ← 第一组取 4 位
2345 ← 第二组取 4 位
1 ← 剩余
// 结果:1,2345,6789min_len - 最小触发位数
控制多少位以上的数字才启用千分位分隔:
min_len: 0 // 不限制,1000 → 1,000(默认行为)
min_len: 5 // 5 位以上才分隔,1234 → 1234,12345 → 12,345point_group - 小数部分分组
是否对小数部分也进行分组:
point_group: false // 小数部分不分组(默认): 3.141592653
point_group: true // 小数部分也分组: 3.141 592 653当 point_group: true 时,使用与整数部分相同的 grouping 规则,但从左向右应用:
// 整数部分:从右向左
// 小数部分:从左向右
// 原因:都是从小数点开始向外分组
1,234,567.123 456 789
←←←←←.→→→→→→→→→
整数 小数
都从小数点出发为什么方向相反?
整数部分从个位(最右边)开始计数,每千进一级;小数部分从十分位(最左边)开始计数,每千退一级。因此:
- 整数:从右向左分组,符合千→百万→十亿的进位逻辑
- 小数:从左向右分组,符合千分之一→百万分之一的退位逻辑
这也是 ISO 31-0 国际标准的规定,例如圆周率写作:3.141 592 653 589 793
fn - 自定义格式化函数
当其他 5 个原子配置无法满足需求时,可以使用 fn 完全自定义格式化逻辑:
fn: (numStr, context) => formattedStr入参说明:
| 参数 | 类型 | 说明 |
|---|---|---|
numStr | string | 原始数字字符串,如 "-1234567.89" |
context | object | 解析后的上下文信息 |
context 结构:
{
intPart: "1234567", // 整数部分(不含符号)
decPart: "89", // 小数部分,无小数则为 null
sign: "-", // 符号: "" | "-" | "+"
options: { ... } // 全局 options 配置对象
}返回值:
格式化后的完整字符串。
使用示例:
// 自定义:整数部分每 4 位用下划线分隔
{
fn: (numStr, { intPart, decPart, sign }) => {
const formatted = intPart.replace(/\B(?=(\d{4})+(?!\d))/g, '_');
return sign + formatted + (decPart ? '.' + decPart : '');
}
}
// 1234567.89 → "123_4567.89"重要规则
当 fn 存在时,其他 5 个原子配置(sep、point、grouping、min_len、point_group)会被完全忽略。fn 拥有完全的格式化控制权。
为什么需要 context.options?
虽然大部分额外信息可以通过闭包获取,但传入 options 可以让 fn 访问到全局配置,实现更灵活的逻辑:
calc('1234567', {
_my_custom_setting: 'special',
_thousands: {
fn: (numStr, { options }) => {
// 可以直接访问全局配置
if (options._my_custom_setting === 'special') {
// 特殊处理
}
}
}
});系统内置预设
系统提供以下内置预设,覆盖全球主要的数字格式:
const THOUSANDS_PRESETS = {
// 英文/国际标准(美国、英国、中国等)
en: {
sep: ',',
point: '.',
grouping: [3]
},
// 欧洲格式(德国、意大利、西班牙等)
eu: {
sep: '.',
point: ',',
grouping: [3]
},
// 瑞士格式
swiss: {
sep: "'",
point: '.',
grouping: [3]
},
// 空格分隔(ISO 31-0 标准)
space: {
sep: ' ',
point: '.',
grouping: [3]
},
// 法国格式(使用不换行空格)
fr: {
sep: '\u00A0', // 不换行空格
point: ',',
grouping: [3]
},
// 印度格式
indian: {
sep: ',',
point: '.',
grouping: [3, 2]
},
// 万进制(中国/日本,每 4 位分组)
wan: {
sep: ',',
point: '.',
grouping: [4]
}
};预设效果对照表:
| 预设 | 数字 1234567.89 的显示 | 使用地区 |
|---|---|---|
en | 1,234,567.89 | 美国、英国、中国 |
eu | 1.234.567,89 | 德国、意大利、西班牙 |
swiss | 1'234'567.89 | 瑞士 |
space | 1 234 567.89 | ISO 标准、北欧、俄罗斯 |
fr | 1 234 567,89 | 法国(不换行空格) |
indian | 12,34,567.89 | 印度 |
wan | 1,2345,678.89 | 中国/日本(万进制) |
用户自定义预设
通过 _thousands 配置自定义预设:
calc('1234567.89', {
_thousands: {
// 自定义预设:BTC 格式
btc: {
sep: ' ',
point: '.',
grouping: [4]
},
// 使用自定义函数
custom: {
fn: (numStr, { intPart, decPart, sign }) => {
// 自定义逻辑
return sign + intPart.split('').reverse().join('');
}
}
}
});预设查找优先级:
用户自定义预设(_thousands) > 系统内置预设(THOUSANDS_PRESETS)如果用户定义了与系统预设同名的预设,用户预设会覆盖系统预设:
calc('1234567.89 | !t:en', {
_thousands: {
en: {
sep: ' ', // 覆盖系统的 en 预设
point: '.',
grouping: [3]
}
}
});
// 结果:'1 234 567.89'(使用用户定义的 en 预设)单位关联
千分位配置可以与单位转换系统联动,实现根据输出单位自动应用对应的千分位格式。
在 _unit_convert_out 中配置
在单位转换配置中添加 _thousands 字段:
calc('100 | !ua:$', {
_unit_convert_out: {
'$': {
CNY: 7.2, // 原有的单位转换
_position: 'before',
_thousands: 'en' // 新增:千分位配置
},
'€': {
CNY: 7.8,
_position: 'before',
_thousands: 'eu' // 欧元使用欧洲格式
}
}
});直接配置 _unit_thousands_map
也可以通过 _unit_thousands_map 单独配置单位与千分位的关联:
calc('1234567 | !ua:$', {
_unit_thousands_map: {
'$': 'en',
'€': 'eu',
'¥': 'wan',
'BTC': 'btc' // 引用自定义预设
},
_thousands: {
btc: {
sep: ' ',
point: '.',
grouping: [4]
}
}
});配置融合
系统会从 _unit_convert_out 中提取千分位配置,然后与 _unit_thousands_map 融合:
// 假设配置如下:
{
_unit_convert_out: {
'$': { _thousands: 'en' },
'€': { _thousands: 'eu' }
},
_unit_thousands_map: {
'$': 'space', // 覆盖从 _unit_convert_out 提取的配置
'¥': 'wan' // 新增
}
}
// 融合后的单位千分位映射:
// {
// '$': 'space', // _unit_thousands_map 优先级更高
// '€': 'eu', // 从 _unit_convert_out 提取
// '¥': 'wan' // 从 _unit_thousands_map 直接配置
// }融合优先级:
_unit_thousands_map(用户直接配置) > 从 _unit_convert_out 提取这与单位位置(_unit_position_map)的设计模式一致。
格式字符串语法
基础语法 !t:预设名
使用 !t: 前缀在格式字符串中指定千分位预设:
calc('1234567.89 | !t:en') // '1,234,567.89'
calc('1234567.89 | !t:eu') // '1.234.567,89'
calc('1234567.89 | !t:swiss') // "1'234'567.89"
calc('1234567.89 | !t:space') // '1 234 567.89'
calc('1234567.89 | !t:indian') // '12,34,567.89'
calc('1234567.89 | !t:wan') // '1,2345,678.89'使用 @ 变量
支持 @ 前缀引用变量值:
calc('1234567.89 | !t:@format', { format: 'eu' })
// 结果:'1.234.567,89'
calc('1234567.89 | !t:@config.thousands', {
config: { thousands: 'swiss' }
})
// 结果:"1'234'567.89"与其他格式化组合
千分位可以与其他格式化规则组合使用:
// 千分位 + 小数位控制
calc('1234567.126 | =2 !t:eu')
// 结果:'1.234.567,12'
// 千分位 + 正号
calc('1234567.89 | + !t:en')
// 结果:'+1,234,567.89'
// 千分位 + 单位
calc('1234567.89 | !t:eu !ua:€')
// 结果:'1.234.567,89€'
// 千分位 + 百分比
calc('0.1234567 | % !t:space')
// 结果:'12.345 67%'优先级汇总
完整的千分位配置优先级(从高到低):
| 优先级 | 来源 | 说明 |
|---|---|---|
| 1(最高) | !t:预设名 | 格式字符串中显式指定 |
| 2 | 单位关联 | 通过输出单位自动触发 |
| 3 | _thousands | 用户自定义预设 |
| 4 | THOUSANDS_PRESETS | 系统内置预设 |
| 5(最低) | default | 系统兜底预设 |
完整示例
多币种金融应用
const options = {
_unit_convert_out: {
'$': { CNY: 7.2, _position: 'before', _thousands: 'en' },
'€': { CNY: 7.8, _position: 'before', _thousands: 'eu' },
'₹': { CNY: 0.086, _position: 'before', _thousands: 'indian' },
'¥': { _position: 'after', _thousands: 'wan' }
}
};
calc('1234567.89 | =2 !ua:$', options); // '$1,234,567.89'
calc('1234567.89 | =2 !ua:€', options); // '€1.234.567,89'
calc('1234567.89 | =2 !ua:₹', options); // '₹12,34,567.89'
calc('1234567.89 | =2 !ua:¥', options); // '123,4567.89¥'加密货币应用
const options = {
_thousands: {
btc: {
sep: ' ',
point: '.',
grouping: [4]
},
satoshi: {
fn: (numStr, { intPart, sign }) => {
// 聪的特殊格式化
const len = intPart.length;
if (len <= 8) return sign + intPart + ' sats';
const btc = intPart.slice(0, len - 8) + '.' + intPart.slice(len - 8);
return sign + btc + ' BTC';
}
}
},
_unit_thousands_map: {
'BTC': 'btc',
'sats': 'satoshi'
}
};
calc('12345678 | !t:btc'); // '1234 5678'
calc('123456789 | !ua:sats', options); // '1.23456789 BTC'科学计算(小数分组)
const options = {
_thousands: {
scientific: {
sep: ' ',
point: '.',
grouping: [3],
point_group: true // 启用小数部分分组
}
}
};
calc('3.141592653589793 | !t:scientific', options);
// 结果:'3.141 592 653 589 793'配置项速查表
| 配置项 | 类型 | 说明 |
|---|---|---|
_thousands | object | 用户自定义千分位预设 |
_unit_thousands_map | object | 单位与千分位预设的映射 |
_unit_convert_out[unit]._thousands | string | 在单位转换中配置千分位 |
| 格式语法 | 说明 | 示例 |
|---|---|---|
!t:预设名 | 使用指定预设 | !t:eu |
!t:@变量 | 使用变量值作为预设名 | !t:@format |