Skip to content

formatCurrency

ts
/**
 * 将数字格式化为货币字符串。
 *
 * @param {number} num - 要格式化的数字。
 * @param {number} [unit=100] - 将数字除以的单位(例如,100表示从分到元)。
 * @param {string} [symbol='¥'] - 前缀的货币符号。
 * @param {number} [digits=2] - 显示的小数位数。
 * @param {function} [parser] - 可选的解析函数,用于在格式化之前处理数值。
 * @returns {string} 格式化后的货币字符串。
 */
export const formatCurrency = function (
  num: number,
  unit: number = 100,
  symbol: string = '¥',
  digits: number = 2,
  parser?: (value: number) => number
): string {
  // 规范化输入数字,默认为0如果未定义或为null
  const normalizedNum = num ?? 0

  // 按指定单位除以数字
  let value = normalizedNum / unit

  // 如果提供了解析函数,则应用它
  if (isFunction(parser)) {
    value = parser(value)
  }

  // 确定符号位置
  const sign = normalizedNum < 0 ? '-' : ''

  // 将值格式化为带有指定符号和小数位数的货币字符串
  return `${symbol}${sign}${Math.abs(value).toLocaleString('en-US', {
    minimumFractionDigits: digits,
    maximumFractionDigits: digits,
  })}`
}
ts
import { describe, it, expect } from 'vitest'
import { formatCurrency } from '../index'

describe('formatCurrency', () => {
  it('1. 应将整数100格式化为"¥1.00"', () => {
    expect(formatCurrency(100)).toBe('¥1.00')
  })

  it('2. 应将负数-100格式化为"¥-1.00"', () => {
    expect(formatCurrency(-100)).toBe('¥-1.00')
  })

  it('3. 应将0格式化为"¥0.00"', () => {
    expect(formatCurrency(0)).toBe('¥0.00')
  })

  it('4. 应使用自定义单位格式化', () => {
    expect(formatCurrency(10000, 1000)).toBe('¥10.00')
  })

  it('5. 应使用自定义货币符号格式化', () => {
    expect(formatCurrency(100, 100, '$')).toBe('$1.00')
  })

  it('6. 应使用自定义小数位数格式化', () => {
    expect(formatCurrency(100, 100, '¥', 3)).toBe('¥1.000')
  })

  it('7. 应使用自定义解析函数格式化', () => {
    const parser = (value: number) => value * 2
    expect(formatCurrency(100, 100, '¥', 2, parser)).toBe('¥2.00')
  })

  it('8. 应处理未定义的num', () => {
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-expect-error
    expect(formatCurrency(undefined)).toBe('¥0.00')
  })

  it('9. 应处理null值的num', () => {
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-expect-error
    expect(formatCurrency(null)).toBe('¥0.00')
  })

  it('10. 应处理负数和自定义解析函数', () => {
    const parser = (value: number) => value * 2
    expect(formatCurrency(-100, 100, '¥', 2, parser)).toBe('¥-2.00')
  })

  it('11. 应使用自定义单位和小数位数格式化', () => {
    expect(formatCurrency(123456, 100, '€', 0)).toBe('€1,235')
  })
})

digitUppercase

ts
/**
 * 将数字转换为大写金额表示形式的字符串。
 * @param {number} n - 要转换的数字,范围为 -9999999999999.99 到 9999999999999.99。
 * @returns {string} 转换后的大写金额表示形式的字符串。
 */
export const digitUppercase = (n: number): string => {
  const fraction = ['角', '分']
  const digit = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']
  const unit = [
    ['元', '万', '亿'],
    ['', '拾', '佰', '仟'],
  ]
  const head = n < 0 ? '欠' : ''
  n = Math.abs(n)
  let s = ''

  // Handle fraction part
  for (let i = 0; i < fraction.length; i++) {
    s += (digit[Math.floor(n * 10 * 10 ** i) % 10] + fraction[i]).replace(/零./, '')
  }
  s = s || '整'

  // Handle integer part
  n = Math.floor(n)
  for (let k = 0; k < unit[0].length && n > 0; k++) {
    let p = ''
    for (let j = 0; j < unit[1].length && n > 0; j++) {
      p = digit[n % 10] + unit[1][j] + p
      n = Math.floor(n / 10)
    }
    s = `${p.replace(/(零.)*$/, '').replace(/^$/, '零')}${unit[0][k]}${s}`
  }

  return `${head}${s
    .replace(/(零.)*零元/, '元')
    .replace(/(零.)+/g, '零')
    .replace(/^$/, '零元整')}`
}
ts
import { describe, it, expect } from 'vitest'
import { digitUppercase } from '../index'

describe('digitUppercase', () => {
  const testCases: Array<{ input: number; expected: string }> = [
    { input: 123456.78, expected: '壹拾贰万叁仟肆佰伍拾陆元柒角捌分' },
    { input: 100200300, expected: '壹亿零贰拾万零叁佰元整' },
    { input: -987654321.99, expected: '欠玖亿捌仟柒佰陆拾伍万肆仟叁佰贰拾壹元玖角玖分' },
    { input: 0, expected: '零元整' },
    { input: 0.05, expected: '伍分' },
  ]

  testCases.forEach(({ input, expected }, index) => {
    it(`case ${index + 1}: ${input} 应转换为 ${expected}`, () => {
      expect(digitUppercase(input)).toBe(expected)
    })
  })
})

toChineseNumeral

ts
/**
 * 将阿拉伯数字转换为中文数字字符串。
 * @param {number} num - 要转换的数字,支持负数和小数。
 * @returns {string} 中文数字字符串,例如 123 → 一百二十三。
 */
export function toChineseNumeral(num: number): string {
  if (num === 0) return '零'

  const isNegative = num < 0
  const abs = Math.abs(num)
  const str = abs.toString()
  const dotIdx = str.indexOf('.')
  const intStr = dotIdx >= 0 ? str.slice(0, dotIdx) : str
  const decStr = dotIdx >= 0 ? str.slice(dotIdx + 1) : ''
  const intNum = parseInt(intStr, 10)

  let result = ''

  if (intNum > 0) {
    const sections: number[] = []
    let n = intNum
    while (n > 0) {
      sections.unshift(n % 10000)
      n = Math.floor(n / 10000)
    }

    let needZero = false
    for (let i = 0; i < sections.length; i++) {
      const section = sections[i]!
      const unitIdx = sections.length - 1 - i
      if (section === 0) {
        needZero = true
        continue
      }
      if (result && (needZero || section < 1000)) result += '零'
      result += convertNumeralSection(section) + NUMERAL_SECTION_UNITS[unitIdx]
      needZero = false
    }
  }

  if (decStr) {
    if (!result) result = '零'
    result += '点'
    for (const d of decStr) result += NUMERAL_DIGITS[parseInt(d, 10)]
  }

  return isNegative ? '负' + result : result
}
ts
import { describe, it, expect } from 'vitest'
import { toChineseNumeral } from '../index'

describe('toChineseNumeral', () => {
  const testCases: Array<{ input: number; expected: string }> = [
    { input: 0, expected: '零' },
    { input: 1, expected: '一' },
    { input: 10, expected: '一十' },
    { input: 11, expected: '一十一' },
    { input: 100, expected: '一百' },
    { input: 123, expected: '一百二十三' },
    { input: 1000, expected: '一千' },
    { input: 1001, expected: '一千零一' },
    { input: 1100, expected: '一千一百' },
    { input: 10000, expected: '一万' },
    { input: 10001, expected: '一万零一' },
    { input: 11000, expected: '一万一千' },
    { input: 100000, expected: '一十万' },
    { input: 100100, expected: '一十万零一百' },
    { input: 10000000, expected: '一千万' },
    { input: 100000000, expected: '一亿' },
    { input: 100010001, expected: '一亿零一万零一' },
    { input: 1000000000000, expected: '一万亿' },
    { input: -123, expected: '负一百二十三' },
    { input: 1.5, expected: '一点五' },
    { input: 0.5, expected: '零点五' },
  ]

  testCases.forEach(({ input, expected }, index) => {
    it(`case ${index + 1}: ${input} 应转换为 "${expected}"`, () => {
      expect(toChineseNumeral(input)).toBe(expected)
    })
  })
})

parseChineseNumeral

ts
/**
 * 将中文数字字符串解析为阿拉伯数字。
 * @param {string} str - 中文数字字符串,例如 一百二十三 → 123。
 * @returns {number} 对应的阿拉伯数字。
 */
export function parseChineseNumeral(str: string): number {
  if (str === '零') return 0

  let s = str
  const isNegative = s.startsWith('负')
  if (isNegative) s = s.slice(1)

  const dotIdx = s.indexOf('点')
  let dec = 0
  if (dotIdx >= 0) {
    const decChars = [...s.slice(dotIdx + 1)]
    s = s.slice(0, dotIdx)
    if (decChars.length > 0) {
      const decDigits = decChars.map((ch) => NUMERAL_DIGIT_MAP[ch] ?? 0)
      dec = parseInt(decDigits.join(''), 10) / 10 ** decDigits.length
    }
  }

  let result = 0
  const yiIdx = s.indexOf('亿')
  if (yiIdx >= 0) {
    const beforeYi = s.slice(0, yiIdx)
    s = s.slice(yiIdx + 1).replace(/^+/, '')
    const wanInYi = beforeYi.indexOf('万')
    if (wanInYi >= 0) {
      result += parseNumeralSection(beforeYi.slice(0, wanInYi)) * 10000 * 100000000
      result += parseNumeralSection(beforeYi.slice(wanInYi + 1)) * 100000000
    } else {
      result += parseNumeralSection(beforeYi) * 100000000
    }
  }

  const wanIdx = s.indexOf('万')
  if (wanIdx >= 0) {
    result += parseNumeralSection(s.slice(0, wanIdx)) * 10000
    result += parseNumeralSection(s.slice(wanIdx + 1).replace(/^+/, ''))
  } else {
    result += parseNumeralSection(s)
  }

  return isNegative ? -(result + dec) : result + dec
}
ts
import { describe, it, expect } from 'vitest'
import { parseChineseNumeral } from '../index'

describe('parseChineseNumeral', () => {
  const testCases: Array<{ input: string; expected: number }> = [
    { input: '零', expected: 0 },
    { input: '一', expected: 1 },
    { input: '十', expected: 10 },
    { input: '十五', expected: 15 },
    { input: '一十', expected: 10 },
    { input: '一十一', expected: 11 },
    { input: '一百', expected: 100 },
    { input: '一百二十三', expected: 123 },
    { input: '一千', expected: 1000 },
    { input: '一千零一', expected: 1001 },
    { input: '一千一百', expected: 1100 },
    { input: '一万', expected: 10000 },
    { input: '一万零一', expected: 10001 },
    { input: '一万一千', expected: 11000 },
    { input: '一十万', expected: 100000 },
    { input: '一十万零一百', expected: 100100 },
    { input: '一千万', expected: 10000000 },
    { input: '一亿', expected: 100000000 },
    { input: '一亿零一万零一', expected: 100010001 },
    { input: '一万亿', expected: 1000000000000 },
    { input: '负一百二十三', expected: -123 },
    { input: '一点五', expected: 1.5 },
    { input: '零点五', expected: 0.5 },
  ]

  testCases.forEach(({ input, expected }, index) => {
    it(`case ${index + 1}: "${input}" 应解析为 ${expected}`, () => {
      expect(parseChineseNumeral(input)).toBe(expected)
    })
  })
})