number-format-utils.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. /**
  2. * @fileoverview 订单表单数字格式化工具函数
  3. * @description 提供统一的数字格式化功能,确保4位浮点型数据的精确处理
  4. */
  5. /**
  6. * 数字格式化配置选项
  7. * @typedef {Object} NumberFormatOptions
  8. * @property {number} minimumFractionDigits - 最小小数位数
  9. * @property {number} maximumFractionDigits - 最大小数位数
  10. * @property {boolean} useGrouping - 是否使用千分位分隔符
  11. */
  12. /**
  13. * 数字验证结果
  14. * @typedef {Object} NumberValidationResult
  15. * @property {boolean} isValid - 是否为有效数字
  16. * @property {number} value - 转换后的数字值
  17. * @property {string} error - 错误信息(如果有)
  18. */
  19. /**
  20. * 格式化配置常量
  21. * @description 定义不同类型数字的格式化规则
  22. * @readonly
  23. */
  24. export const NUMBER_FORMAT_CONFIG = Object.freeze({
  25. /** 4位浮点型数量格式化选项 */
  26. QUANTITY_FLOAT: {
  27. minimumFractionDigits: 0,
  28. maximumFractionDigits: 4,
  29. useGrouping: false
  30. },
  31. /** 整数数量格式化选项 */
  32. QUANTITY_INTEGER: {
  33. minimumFractionDigits: 0,
  34. maximumFractionDigits: 0,
  35. useGrouping: false
  36. },
  37. /** 金额格式化选项(2位小数) */
  38. AMOUNT: {
  39. minimumFractionDigits: 2,
  40. maximumFractionDigits: 2,
  41. useGrouping: true
  42. },
  43. /** 税率格式化选项(2位小数) */
  44. TAX_RATE: {
  45. minimumFractionDigits: 0,
  46. maximumFractionDigits: 2,
  47. useGrouping: false
  48. },
  49. /** 单价格式化选项(2位小数) */
  50. UNIT_PRICE: {
  51. minimumFractionDigits: 0,
  52. maximumFractionDigits: 2,
  53. useGrouping: false
  54. }
  55. })
  56. /**
  57. * 数字类型枚举
  58. * @description 定义支持的数字类型
  59. * @readonly
  60. */
  61. export const NUMBER_TYPES = Object.freeze({
  62. /** 4位浮点型数量 */
  63. QUANTITY_FLOAT: 'QUANTITY_FLOAT',
  64. /** 整数数量 */
  65. QUANTITY_INTEGER: 'QUANTITY_INTEGER',
  66. /** 金额 */
  67. AMOUNT: 'AMOUNT',
  68. /** 税率 */
  69. TAX_RATE: 'TAX_RATE',
  70. /** 单价 */
  71. UNIT_PRICE: 'UNIT_PRICE'
  72. })
  73. /**
  74. * 验证并转换数字值
  75. * @description 验证输入值是否为有效数字,并进行安全转换
  76. * @param {*} value - 待验证的值
  77. * @returns {NumberValidationResult} 验证结果
  78. */
  79. export function validateNumber(value) {
  80. // 处理null、undefined和空字符串
  81. if (value === null || value === undefined || value === '') {
  82. return {
  83. isValid: true,
  84. value: 0,
  85. error: null
  86. }
  87. }
  88. // 转换为数字
  89. const numValue = Number(value)
  90. // 检查是否为有效数字
  91. if (isNaN(numValue) || !isFinite(numValue)) {
  92. return {
  93. isValid: false,
  94. value: 0,
  95. error: '无效的数字格式'
  96. }
  97. }
  98. return {
  99. isValid: true,
  100. value: numValue,
  101. error: null
  102. }
  103. }
  104. /**
  105. * 格式化4位浮点型数字
  106. * @description 将数字格式化为4位小数的字符串,自动去除尾随零
  107. * @param {number|string|null|undefined} value - 数字值
  108. * @returns {string} 格式化后的数字字符串
  109. * @example
  110. * formatFloatNumber(123.45670000) // 返回 "123.4567"
  111. * formatFloatNumber(100.0000) // 返回 "100"
  112. * formatFloatNumber(null) // 返回 "0"
  113. */
  114. export function formatFloatNumber(value) {
  115. const validation = validateNumber(value)
  116. if (!validation.isValid) {
  117. return '0'
  118. }
  119. const numValue = validation.value
  120. // 使用4位小数精度格式化
  121. const formatted = numValue.toLocaleString('zh-CN', NUMBER_FORMAT_CONFIG.QUANTITY_FLOAT)
  122. // 去除尾随零和不必要的小数点
  123. return formatted.replace(/\.?0+$/, '')
  124. }
  125. /**
  126. * 格式化整数
  127. * @description 将数字格式化为整数字符串
  128. * @param {number|string|null|undefined} value - 数字值
  129. * @returns {string} 格式化后的整数字符串
  130. * @example
  131. * formatIntegerNumber(123.456) // 返回 "123"
  132. * formatIntegerNumber(null) // 返回 "0"
  133. */
  134. export function formatIntegerNumber(value) {
  135. const validation = validateNumber(value)
  136. if (!validation.isValid) {
  137. return '0'
  138. }
  139. console.log(value, Math.round(validation.value).toLocaleString('zh-CN', NUMBER_FORMAT_CONFIG.QUANTITY_INTEGER))
  140. return Math.round(validation.value).toLocaleString('zh-CN', NUMBER_FORMAT_CONFIG.QUANTITY_INTEGER)
  141. }
  142. /**
  143. * 格式化金额
  144. * @description 将数字格式化为金额字符串,保留2位小数并添加千分位分隔符
  145. * @param {number|string|null|undefined} value - 金额值
  146. * @param {boolean} [withSymbol=true] - 是否添加货币符号
  147. * @returns {string} 格式化后的金额字符串
  148. * @example
  149. * formatAmount(1234.567) // 返回 "¥1,234.57"
  150. * formatAmount(1234.567, false) // 返回 "1,234.57"
  151. * formatAmount(null) // 返回 "¥0.00"
  152. */
  153. export function formatAmount(value, withSymbol = true) {
  154. const validation = validateNumber(value)
  155. if (!validation.isValid) {
  156. return withSymbol ? '¥0.00' : '0.00'
  157. }
  158. const formatted = validation.value.toLocaleString('zh-CN', NUMBER_FORMAT_CONFIG.AMOUNT)
  159. return withSymbol ? `¥${formatted}` : formatted
  160. }
  161. /**
  162. * 格式化税率
  163. * @description 将数字格式化为税率字符串,最多保留2位小数
  164. * @param {number|string|null|undefined} value - 税率值(0-100)
  165. * @param {boolean} [withSymbol=true] - 是否添加百分号
  166. * @returns {string} 格式化后的税率字符串
  167. * @example
  168. * formatTaxRate(13.5) // 返回 "13.5%"
  169. * formatTaxRate(13.50, false) // 返回 "13.5"
  170. * formatTaxRate(null) // 返回 "0%"
  171. */
  172. export function formatTaxRate(value, withSymbol = true) {
  173. const validation = validateNumber(value)
  174. if (!validation.isValid) {
  175. return withSymbol ? '0%' : '0'
  176. }
  177. const formatted = validation.value.toLocaleString('zh-CN', NUMBER_FORMAT_CONFIG.TAX_RATE)
  178. return withSymbol ? `${formatted}%` : formatted
  179. }
  180. /**
  181. * 格式化单价
  182. * @description 将数字格式化为单价字符串,最多保留4位小数
  183. * @param {number|string|null|undefined} value - 单价值
  184. * @returns {string} 格式化后的单价字符串
  185. * @example
  186. * formatUnitPrice(123.45670000) // 返回 "123.4567"
  187. * formatUnitPrice(100.0000) // 返回 "100"
  188. * formatUnitPrice(null) // 返回 "0"
  189. */
  190. export function formatUnitPrice(value) {
  191. const validation = validateNumber(value)
  192. if (!validation.isValid) {
  193. return '0.00'
  194. }
  195. const formatted = validation.value.toLocaleString('zh-CN', {
  196. minimumFractionDigits: 2,
  197. maximumFractionDigits: 2,
  198. useGrouping: false
  199. })
  200. return formatted
  201. }
  202. /**
  203. * 通用数字格式化函数
  204. * @description 根据指定类型格式化数字
  205. * @param {number|string|null|undefined} value - 数字值
  206. * @param {keyof typeof NUMBER_TYPES} type - 数字类型
  207. * @param {Object} [options={}] - 额外选项
  208. * @param {boolean} [options.withSymbol] - 是否添加符号(适用于金额和税率)
  209. * @returns {string} 格式化后的数字字符串
  210. * @example
  211. * formatNumber(123.4567, NUMBER_TYPES.QUANTITY_FLOAT) // 返回 "123.4567"
  212. * formatNumber(1234.56, NUMBER_TYPES.AMOUNT) // 返回 "¥1,234.56"
  213. * formatNumber(13.5, NUMBER_TYPES.TAX_RATE) // 返回 "13.5%"
  214. */
  215. export function formatNumber(value, type, options = {}) {
  216. const { withSymbol = true } = options
  217. switch (type) {
  218. case NUMBER_TYPES.QUANTITY_FLOAT:
  219. return formatFloatNumber(value)
  220. case NUMBER_TYPES.QUANTITY_INTEGER:
  221. return formatIntegerNumber(value)
  222. case NUMBER_TYPES.AMOUNT:
  223. return formatAmount(value, withSymbol)
  224. case NUMBER_TYPES.TAX_RATE:
  225. return formatTaxRate(value, withSymbol)
  226. case NUMBER_TYPES.UNIT_PRICE:
  227. return formatUnitPrice(value)
  228. default:
  229. return formatFloatNumber(value)
  230. }
  231. }
  232. /**
  233. * 解析格式化的数字字符串
  234. * @description 将格式化的数字字符串解析为数字值
  235. * @param {string} formattedValue - 格式化的数字字符串
  236. * @returns {NumberValidationResult} 解析结果
  237. * @example
  238. * parseFormattedNumber("1,234.56") // 返回 { isValid: true, value: 1234.56, error: null }
  239. * parseFormattedNumber("¥1,234.56") // 返回 { isValid: true, value: 1234.56, error: null }
  240. * parseFormattedNumber("13.5%") // 返回 { isValid: true, value: 13.5, error: null }
  241. */
  242. export function parseFormattedNumber(formattedValue) {
  243. if (typeof formattedValue !== 'string') {
  244. return validateNumber(formattedValue)
  245. }
  246. // 移除货币符号、百分号、千分位分隔符等
  247. const cleanValue = formattedValue
  248. .replace(/[¥$€£%,\s]/g, '')
  249. .trim()
  250. return validateNumber(cleanValue)
  251. }
  252. /**
  253. * 精确计算两个数字的乘积
  254. * @description 避免浮点数计算精度问题
  255. * @param {number|string} num1 - 第一个数字
  256. * @param {number|string} num2 - 第二个数字
  257. * @returns {number} 计算结果
  258. * @example
  259. * preciseMultiply(0.1, 0.2) // 返回 0.02(而不是 0.020000000000000004)
  260. */
  261. export function preciseMultiply(num1, num2) {
  262. const validation1 = validateNumber(num1)
  263. const validation2 = validateNumber(num2)
  264. if (!validation1.isValid || !validation2.isValid) {
  265. return 0
  266. }
  267. const value1 = validation1.value
  268. const value2 = validation2.value
  269. // 获取小数位数
  270. const decimals1 = (value1.toString().split('.')[1] || '').length
  271. const decimals2 = (value2.toString().split('.')[1] || '').length
  272. const totalDecimals = decimals1 + decimals2
  273. // 转换为整数进行计算,然后除以相应的倍数
  274. const int1 = Math.round(value1 * Math.pow(10, decimals1))
  275. const int2 = Math.round(value2 * Math.pow(10, decimals2))
  276. return (int1 * int2) / Math.pow(10, totalDecimals)
  277. }
  278. /**
  279. * 精确计算两个数字的除法
  280. * @description 避免浮点数计算精度问题
  281. * @param {number|string} dividend - 被除数
  282. * @param {number|string} divisor - 除数
  283. * @returns {number} 计算结果
  284. * @example
  285. * preciseDivide(0.3, 0.1) // 返回 3(而不是 2.9999999999999996)
  286. */
  287. export function preciseDivide(dividend, divisor) {
  288. const validation1 = validateNumber(dividend)
  289. const validation2 = validateNumber(divisor)
  290. if (!validation1.isValid || !validation2.isValid || validation2.value === 0) {
  291. return 0
  292. }
  293. const value1 = validation1.value
  294. const value2 = validation2.value
  295. // 获取小数位数
  296. const decimals1 = (value1.toString().split('.')[1] || '').length
  297. const decimals2 = (value2.toString().split('.')[1] || '').length
  298. const maxDecimals = Math.max(decimals1, decimals2)
  299. // 转换为整数进行计算
  300. const int1 = Math.round(value1 * Math.pow(10, maxDecimals))
  301. const int2 = Math.round(value2 * Math.pow(10, maxDecimals))
  302. return int1 / int2
  303. }
  304. /**
  305. * 四舍五入到指定小数位数
  306. * @description 精确的四舍五入函数
  307. * @param {number|string} value - 数字值
  308. * @param {number} [decimals=4] - 小数位数
  309. * @returns {number} 四舍五入后的结果
  310. * @example
  311. * preciseRound(123.45678, 4) // 返回 123.4568
  312. * preciseRound(123.45678, 2) // 返回 123.46
  313. */
  314. export function preciseRound(value, decimals = 4) {
  315. const validation = validateNumber(value)
  316. if (!validation.isValid) {
  317. return 0
  318. }
  319. const multiplier = Math.pow(10, decimals)
  320. return Math.round(validation.value * multiplier) / multiplier
  321. }