Add automatic B/T compact unit formatting

This commit is contained in:
fawney19
2026-05-22 17:13:16 +08:00
parent ecf6019ccb
commit ef04f4b0fb
16 changed files with 161 additions and 125 deletions
@@ -0,0 +1,19 @@
import { describe, expect, it } from 'vitest'
import { formatCompactNumber, formatTokens, formatUsageCount } from '../format'
describe('format utils', () => {
it('formats compact numbers beyond millions', () => {
expect(formatCompactNumber(999)).toBe('999')
expect(formatCompactNumber(1_250)).toBe('1.25K')
expect(formatCompactNumber(12_500_000)).toBe('12.5M')
expect(formatCompactNumber(1_250_000_000)).toBe('1.25B')
expect(formatCompactNumber(12_500_000_000_000)).toBe('12.5T')
})
it('uses the same B/T compact units for tokens and usage counts', () => {
expect(formatTokens(1_000_000_000)).toBe('1B')
expect(formatTokens(1_500_000_000_000)).toBe('1.5T')
expect(formatUsageCount(1_000_000_000)).toBe('1B')
})
})
+70 -36
View File
@@ -1,35 +1,74 @@
const COMPACT_NUMBER_UNITS = [
{ value: 1_000_000_000_000, suffix: 'T' },
{ value: 1_000_000_000, suffix: 'B' },
{ value: 1_000_000, suffix: 'M' },
{ value: 1_000, suffix: 'K' },
] as const
interface CompactNumberOptions {
fractionDigits?: number
nullLabel?: string
}
function trimTrailingDecimalZeros(value: string): string {
return value.replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1')
}
function compactFractionDigits(scaled: number, fixedFractionDigits?: number): number {
if (fixedFractionDigits !== undefined) return fixedFractionDigits
if (scaled >= 100) return 0
if (scaled >= 10) return 1
return 2
}
function formatCompactScaledValue(
absValue: number,
unitIndex: number,
fixedFractionDigits?: number,
): string {
const unit = COMPACT_NUMBER_UNITS[unitIndex]
const scaled = absValue / unit.value
const fractionDigits = compactFractionDigits(scaled, fixedFractionDigits)
const rounded = Number(scaled.toFixed(fractionDigits))
if (rounded >= 1000 && unitIndex > 0) {
return formatCompactScaledValue(absValue, unitIndex - 1, fixedFractionDigits)
}
return `${trimTrailingDecimalZeros(scaled.toFixed(fractionDigits))}${unit.suffix}`
}
export function formatCompactNumber(
num: number | undefined | null,
options: CompactNumberOptions = {},
): string {
if (num === undefined || num === null) {
return options.nullLabel ?? '0'
}
const value = Number(num)
if (!Number.isFinite(value)) {
return options.nullLabel ?? '0'
}
const sign = value < 0 ? '-' : ''
const absValue = Math.abs(value)
if (absValue < 1_000) {
return `${sign}${Number.isInteger(absValue) ? absValue.toString() : trimTrailingDecimalZeros(absValue.toFixed(1))}`
}
const unitIndex = COMPACT_NUMBER_UNITS.findIndex(unit => absValue >= unit.value)
if (unitIndex === -1) {
return `${sign}${Math.round(absValue)}`
}
return `${sign}${formatCompactScaledValue(absValue, unitIndex, options.fractionDigits)}`
}
// Token formatting - intelligent display based on value size
export function formatTokens(num: number | undefined | null): string {
if (num === undefined || num === null || num === 0) {
return '0'
}
// For very small values (< 1000), show as is without unit
if (num < 1000) {
return num.toString()
}
// For values 1K-999K, show in thousands
if (num < 1000000) {
const thousands = num / 1000
if (thousands >= 100) {
return `${Math.round(thousands) }K`
} else if (thousands >= 10) {
return `${thousands.toFixed(1) }K`
} else {
return `${thousands.toFixed(2) }K`
}
}
// For values >= 1M, show in millions
const millions = num / 1000000
if (millions >= 100) {
return `${Math.round(millions) }M`
} else if (millions >= 10) {
return `${millions.toFixed(1) }M`
} else {
return `${millions.toFixed(2) }M`
}
return formatCompactNumber(num)
}
// Currency formatting with high precision for small values
@@ -135,12 +174,7 @@ export function formatCost(cost: number | null | undefined): string {
// Usage count formatting (compact display for large numbers)
export function formatUsageCount(count: number): string {
if (count >= 1000000) {
return `${(count / 1000000).toFixed(1)}M`
} else if (count >= 1000) {
return `${(count / 1000).toFixed(1)}K`
}
return count.toString()
return formatCompactNumber(count, { fractionDigits: 1 })
}
// Format remaining time from unix timestamp
+4 -7
View File
@@ -1,3 +1,5 @@
import { formatCompactNumber } from '@/utils/format'
export function walletStatusLabel(status: string | null | undefined): string {
const labels: Record<string, string> = {
active: '正常',
@@ -41,13 +43,8 @@ export function dailyUsageCategoryLabel(isToday = false): string {
export function formatTokenCount(value: number | null | undefined): string {
const amount = Number(value ?? 0)
if (amount >= 1_000_000) {
return `${(amount / 1_000_000).toFixed(amount >= 10_000_000 ? 0 : 1)}M`
}
if (amount >= 1_000) {
return `${(amount / 1_000).toFixed(amount >= 10_000 ? 0 : 1)}K`
}
return `${Math.round(amount)}`
if (!Number.isFinite(amount) || amount <= 0) return '0'
return formatCompactNumber(Math.round(amount), { fractionDigits: 1 })
}
export function walletTransactionReasonLabel(reasonCode: string | null | undefined): string {