Skip to content

千分位格式化增强

a-calc 提供了灵活且强大的千分位格式化功能,支持全球各种数字格式规范。

设计概述

千分位功能采用分层设计,从底层到顶层依次为:

优先级从低到高:
┌─────────────────────────────────────────────┐
│  用户显式指定(格式字符串 !t:预设名)          │  ← 最高优先级
├─────────────────────────────────────────────┤
│  单位关联(输出单位自动触发对应千分位配置)     │
├─────────────────────────────────────────────┤
│  用户自定义预设(_thousands)         │
├─────────────────────────────────────────────┤
│  系统内置预设(THOUSANDS_PRESETS)            │
├─────────────────────────────────────────────┤
│  系统默认兜底预设(default)                  │  ← 最低优先级
└─────────────────────────────────────────────┘

每个预设都是原子配置的集合,系统采用可插拔设计,用户可以创建与系统结构相同的自定义预设。

原子配置

千分位格式化由 6 个原子配置组成:

配置类型默认值说明
sepstring','千分位分隔符
pointstring'.'小数点符号
groupingnumber[][3]分组规则
min_lennumber0最小触发位数,0 表示不限制
point_groupbooleanfalse是否对小数部分分组
fnfunction | nullnull自定义格式化函数(逃生舱)

sep - 千分位分隔符

整数部分的分组分隔符:

javascript
// 不同地区使用不同的分隔符
sep: ','   // 美国/英国/中国: 1,234,567
sep: '.'   // 德国/意大利:    1.234.567
sep: "'"   // 瑞士:          1'234'567
sep: ' '   // 法国/ISO标准:   1 234 567
sep: '_'   // 编程场景:       1_234_567

point - 小数点符号

小数点分隔符:

javascript
// 不同地区使用不同的小数点
point: '.'  // 美国/英国/中国: 1,234.56
point: ','  // 德国/法国:      1.234,56

注意

seppoint 通常是配对使用的。如果千分位用逗号,小数点就要用点;如果千分位用点,小数点就要用逗号。否则会造成解析歧义。

grouping - 分组规则

定义每组包含多少位数字,使用数组表示:

javascript
grouping: [3]      // 每 3 位分组(最常见)
grouping: [3, 2]   // 印度格式
grouping: [4]      // 万进制(中国/日本)

分组规则详解

基础规则:从右向左应用

javascript
grouping: [3]
12345671,234,567
        ←←←←←←←←← 从右向左,每 3 位分组

数组含义:

  • 第一个值:最右边的分组位数
  • 第二个值及之后:后续的分组位数
  • 数组最后一个值会被重复应用
javascript
grouping: [3]      // 等同于 [3, 3, 3, 3...]
grouping: [3, 2]   // 等同于 [3, 2, 2, 2...]
grouping: [4]      // 等同于 [4, 4, 4, 4...]

印度格式示例

印度数字格式是一个经典的不规则分组案例:

javascript
grouping: [3, 2]

// 应用过程(从右向左):
1234567
      ↑ 从最右边开始
   567  ← 第一组取 3
 34     ← 第二组取 2
12      ← 第三组取 2 位(重复最后一个值)

// 结果:12,34,567

万进制示例

中国和日本的万进制格式:

javascript
grouping: [4]

// 应用过程:
123456789
     6789  ← 第一组取 4
 2345      ← 第二组取 4
1          ← 剩余

// 结果:1,2345,6789

min_len - 最小触发位数

控制多少位以上的数字才启用千分位分隔:

javascript
min_len: 0   // 不限制,1000 → 1,000(默认行为)
min_len: 5   // 5 位以上才分隔,1234 → 1234,12345 → 12,345

point_group - 小数部分分组

是否对小数部分也进行分组:

javascript
point_group: false  // 小数部分不分组(默认): 3.141592653
point_group: true   // 小数部分也分组:        3.141 592 653

point_group: true 时,使用与整数部分相同的 grouping 规则,但从左向右应用:

javascript
// 整数部分:从右向左
// 小数部分:从左向右
// 原因:都是从小数点开始向外分组

    1,234,567.123 456 789
        ←←←←←.→→→→→→→→→
         整数  小数
    都从小数点出发

为什么方向相反?

整数部分从个位(最右边)开始计数,每千进一级;小数部分从十分位(最左边)开始计数,每千退一级。因此:

  • 整数:从右向左分组,符合千→百万→十亿的进位逻辑
  • 小数:从左向右分组,符合千分之一→百万分之一的退位逻辑

这也是 ISO 31-0 国际标准的规定,例如圆周率写作:3.141 592 653 589 793

fn - 自定义格式化函数

当其他 5 个原子配置无法满足需求时,可以使用 fn 完全自定义格式化逻辑:

javascript
fn: (numStr, context) => formattedStr

入参说明:

参数类型说明
numStrstring原始数字字符串,如 "-1234567.89"
contextobject解析后的上下文信息

context 结构:

javascript
{
  intPart: "1234567",     // 整数部分(不含符号)
  decPart: "89",          // 小数部分,无小数则为 null
  sign: "-",              // 符号: "" | "-" | "+"
  options: { ... }        // 全局 options 配置对象
}

返回值:

格式化后的完整字符串。

使用示例:

javascript
// 自定义:整数部分每 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 个原子配置(seppointgroupingmin_lenpoint_group)会被完全忽略fn 拥有完全的格式化控制权。

为什么需要 context.options?

虽然大部分额外信息可以通过闭包获取,但传入 options 可以让 fn 访问到全局配置,实现更灵活的逻辑:

javascript
calc('1234567', {
  _my_custom_setting: 'special',
  _thousands: {
    fn: (numStr, { options }) => {
      // 可以直接访问全局配置
      if (options._my_custom_setting === 'special') {
        // 特殊处理
      }
    }
  }
});

系统内置预设

系统提供以下内置预设,覆盖全球主要的数字格式:

javascript
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 的显示使用地区
en1,234,567.89美国、英国、中国
eu1.234.567,89德国、意大利、西班牙
swiss1'234'567.89瑞士
space1 234 567.89ISO 标准、北欧、俄罗斯
fr1 234 567,89法国(不换行空格)
indian12,34,567.89印度
wan1,2345,678.89中国/日本(万进制)

用户自定义预设

通过 _thousands 配置自定义预设:

javascript
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)

如果用户定义了与系统预设同名的预设,用户预设会覆盖系统预设:

javascript
calc('1234567.89 | !t:en', {
  _thousands: {
    en: {
      sep: ' ',  // 覆盖系统的 en 预设
      point: '.',
      grouping: [3]
    }
  }
});
// 结果:'1 234 567.89'(使用用户定义的 en 预设)

单位关联

千分位配置可以与单位转换系统联动,实现根据输出单位自动应用对应的千分位格式。

在 _unit_convert_out 中配置

在单位转换配置中添加 _thousands 字段:

javascript
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 单独配置单位与千分位的关联:

javascript
calc('1234567 | !ua:$', {
  _unit_thousands_map: {
    '$': 'en',
    '€': 'eu',
    '¥': 'wan',
    'BTC': 'btc'  // 引用自定义预设
  },
  _thousands: {
    btc: {
      sep: ' ',
      point: '.',
      grouping: [4]
    }
  }
});

配置融合

系统会从 _unit_convert_out 中提取千分位配置,然后与 _unit_thousands_map 融合:

javascript
// 假设配置如下:
{
  _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: 前缀在格式字符串中指定千分位预设:

javascript
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'

使用 @ 变量

支持 @ 前缀引用变量值:

javascript
calc('1234567.89 | !t:@format', { format: 'eu' })
// 结果:'1.234.567,89'

calc('1234567.89 | !t:@config.thousands', {
  config: { thousands: 'swiss' }
})
// 结果:"1'234'567.89"

与其他格式化组合

千分位可以与其他格式化规则组合使用:

javascript
// 千分位 + 小数位控制
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用户自定义预设
4THOUSANDS_PRESETS系统内置预设
5(最低)default系统兜底预设

完整示例

多币种金融应用

javascript
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¥'

加密货币应用

javascript
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'

科学计算(小数分组)

javascript
const options = {
  _thousands: {
    scientific: {
      sep: ' ',
      point: '.',
      grouping: [3],
      point_group: true  // 启用小数部分分组
    }
  }
};

calc('3.141592653589793 | !t:scientific', options);
// 结果:'3.141 592 653 589 793'

配置项速查表

配置项类型说明
_thousandsobject用户自定义千分位预设
_unit_thousands_mapobject单位与千分位预设的映射
_unit_convert_out[unit]._thousandsstring在单位转换中配置千分位
格式语法说明示例
!t:预设名使用指定预设!t:eu
!t:@变量使用变量值作为预设名!t:@format

基于 MIT 许可发布