diff --git a/src/components/usage/format.ts b/src/components/usage/format.ts index b4a396135..f6a4fbeda 100644 --- a/src/components/usage/format.ts +++ b/src/components/usage/format.ts @@ -80,9 +80,21 @@ const CURRENCY_SYMBOL: Record = { TRY: "₺", }; -/** 日元面额大(1 美元约 158 日元),同一处少显示两位小数:$0.0012 → ≈¥0.19 */ +/** 日元面额大(1 美元约 158 日元),同一处少显示两位小数:$0.0012 → ¥0.19 */ const FEWER_DIGITS: Partial> = { JPY: 2 }; +/** + * 换算出来的金额是按手填汇率估的,再多位数也不更准:最多两位小数(日元再少两位)。 + * 美元按调用方给的位数。 + */ +function shownDigits(display: MoneyDisplay, digits: number): number { + if (display.currency === "USD") return digits; + return Math.max( + 0, + Math.min(digits - (FEWER_DIGITS[display.currency] ?? 0), 2), + ); +} + /** 整数部分加千位分隔:换成日元、里拉后动辄七位数 */ function groupThousands(fixed: string): string { const [whole, frac] = fixed.split("."); @@ -108,13 +120,7 @@ export function resolveMoneyDisplay( }; } -/** 换算出来的数只是按手填汇率估的,前面加「≈」;0 不加。 */ -function moneyPrefix(display: MoneyDisplay, value: number): string { - const approx = display.currency !== "USD" && value !== 0 ? "≈" : ""; - return `${approx}${CURRENCY_SYMBOL[display.currency]}`; -} - -/** 固定小数位:`$1.23` / `≈¥8.36`。组件里用 `useMoneyFormat()`,别直接传 display。 */ +/** 固定小数位:`$1.23` / `¥8.36`。组件里用 `useMoneyFormat()`,别直接传 display。 */ export function formatMoney( usd: unknown, digits: number, @@ -124,20 +130,26 @@ export function formatMoney( const num = parseFiniteNumber(usd); if (num == null) return fallback; const value = num * display.rate; - const shown = Math.max(0, digits - (FEWER_DIGITS[display.currency] ?? 0)); - return `${moneyPrefix(display, value)}${groupThousands(value.toFixed(shown))}`; + const symbol = CURRENCY_SYMBOL[display.currency]; + const shown = shownDigits(display, digits); + const fixed = value.toFixed(shown); + // 换算后位数少了,很小的一笔会四舍五入成 0:改写两位有效数字(¥0.0034),别像是免费 + if (shown < digits && value !== 0 && Number(fixed) === 0) { + return `${symbol}${Number(value.toPrecision(2))}`; + } + return `${symbol}${groupThousands(fixed)}`; } /** 按大小取位数并去掉末尾的 0:`$0.0081` / `$0.012` / `$1.25` */ export function formatMoneyCompact(usd: number, display: MoneyDisplay): string { const value = usd * display.rate; - const prefix = moneyPrefix(display, value); + const prefix = CURRENCY_SYMBOL[display.currency]; if (value < 0.01) return `${prefix}${Number(value.toFixed(4))}`; if (value < 1) return `${prefix}${Number(value.toFixed(3))}`; return `${prefix}${groupThousands(value.toFixed(2))}`; } -/** 侧栏这类窄处:100 以上不带小数(`≈¥280`),免得把旁边的标签挤成省略号。 */ +/** 侧栏这类窄处:100 以上不带小数(`¥280`),免得把旁边的标签挤成省略号。 */ export function formatMoneyShort(usd: unknown, display: MoneyDisplay): string { const num = parseFiniteNumber(usd); if (num == null) return "--"; @@ -146,7 +158,7 @@ export function formatMoneyShort(usd: unknown, display: MoneyDisplay): string { /** * 坐标轴刻度:画图前就把数据换算成显示币种(刻度才会是 ¥50 这种整数), - * 所以这里拿到的已是显示币种的值,只加符号;不定小数位,也不加「≈」。 + * 所以这里拿到的已是显示币种的值,只加符号,不定小数位。 */ export function formatMoneyTick(value: unknown, display: MoneyDisplay): string { const num = parseFiniteNumber(value) ?? 0; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 07d824017..90ce15785 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -2380,7 +2380,7 @@ "rate": "Exchange rate", "rateHint": "Entered by hand and used only for display; costs are still recorded in USD.", "rateInvalid": "Enter a number greater than 0", - "approxNote": "Converted amounts are marked with ≈. Hover to see the USD value.", + "approxNote": "Hover a converted amount to see the USD value.", "sidebarToday": "Sidebar \"Today\"", "sidebarTodayModes": { "cost": "Cost", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 7679f1325..50ccd5f94 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -2380,7 +2380,7 @@ "rate": "為替レート", "rateHint": "手動で入力し、表示の換算にのみ使います。費用は引き続き米ドルで記録されます。", "rateInvalid": "0 より大きい数値を入力してください", - "approxNote": "換算した金額には「≈」が付きます。ホバーで米ドルの値を確認できます。", + "approxNote": "換算した金額はホバーで米ドルの値を確認できます。", "sidebarToday": "サイドバーの「今日」", "sidebarTodayModes": { "cost": "金額", diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index a43c1f2f7..10a267553 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -2381,7 +2381,7 @@ "rate": "匯率", "rateHint": "手動填寫,只用於換算顯示;費用仍以美元記錄。", "rateInvalid": "請輸入大於 0 的數字", - "approxNote": "換算後的金額前面帶「≈」,滑鼠移上去可看美元原值。", + "approxNote": "換算後的金額,滑鼠移上去可看美元原值。", "sidebarToday": "側欄「今日」", "sidebarTodayModes": { "cost": "金額", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 1d1df8185..4a3ea112f 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -2380,7 +2380,7 @@ "rate": "汇率", "rateHint": "手动填写,只用于换算显示;费用仍按美元记录。", "rateInvalid": "请输入大于 0 的数字", - "approxNote": "换算后的金额前面带「≈」,悬停可看美元原值。", + "approxNote": "换算后的金额悬停可看美元原值。", "sidebarToday": "侧栏「今日」", "sidebarTodayModes": { "cost": "金额", diff --git a/tests/components/usageFormat.test.ts b/tests/components/usageFormat.test.ts index c297a8188..2739d9d18 100644 --- a/tests/components/usageFormat.test.ts +++ b/tests/components/usageFormat.test.ts @@ -281,13 +281,13 @@ describe("display currency", () => { ).toEqual({ currency: "CNY", rate: 7.25 }); }); - it("marks converted amounts with ≈ but leaves zero and axis ticks plain", () => { + it("writes converted amounts with the plain currency symbol", () => { const cny = { currency: "CNY" as const, rate: 7 }; - expect(formatMoney(1.5, 2, cny)).toBe("≈¥10.50"); + expect(formatMoney(1.5, 2, cny)).toBe("¥10.50"); expect(formatMoney(0, 2, cny)).toBe("¥0.00"); expect(formatMoney(null, 2, cny)).toBe("--"); - expect(formatMoneyCompact(0.001, cny)).toBe("≈¥0.007"); - expect(formatMoneyCompact(0.0015, cny)).toBe("≈¥0.011"); + expect(formatMoneyCompact(0.001, cny)).toBe("¥0.007"); + expect(formatMoneyCompact(0.0015, cny)).toBe("¥0.011"); // 刻度值画图前已换算,只加符号 expect(formatMoneyTick(50, cny)).toBe("¥50"); }); @@ -295,30 +295,44 @@ describe("display currency", () => { it("writes each currency with its own symbol and groups thousands", () => { expect(formatMoney(30470.55, 2, USD_DISPLAY)).toBe("$30,470.55"); expect(formatMoney(-1234, 0, USD_DISPLAY)).toBe("$-1,234"); - expect(formatMoney(2, 2, { currency: "EUR", rate: 0.87 })).toBe("≈€1.74"); - expect(formatMoney(2, 2, { currency: "TWD", rate: 32 })).toBe("≈NT$64.00"); + expect(formatMoney(2, 2, { currency: "EUR", rate: 0.87 })).toBe("€1.74"); + expect(formatMoney(2, 2, { currency: "TWD", rate: 32 })).toBe("NT$64.00"); expect(formatMoney(100, 2, { currency: "TRY", rate: 48 })).toBe( - "≈₺4,800.00", + "₺4,800.00", ); expect(formatMoneyTick(20000, { currency: "JPY", rate: 158 })).toBe( "¥20,000", ); }); + it("keeps converted amounts to two decimals at most", () => { + const twd = { currency: "TWD" as const, rate: 32 }; + // 悬停用的 6 位、日志用的 4 位,换算后都只留两位 + expect(formatMoney(64.912461, 6, twd)).toBe("NT$2,077.20"); + expect(formatMoney(0.0505, 4, twd)).toBe("NT$1.62"); + expect(formatMoney(1.5, 1, twd)).toBe("NT$48.0"); + // 小到四舍五入成 0 的一笔写两位有效数字,不写成 0.00 + expect(formatMoney(0.0001, 4, twd)).toBe("NT$0.0032"); + expect(formatMoney(0, 4, twd)).toBe("NT$0.00"); + // 美元照调用方给的位数 + expect(formatMoney(0.0001, 4, USD_DISPLAY)).toBe("$0.0001"); + expect(formatMoney(64.912461, 6, USD_DISPLAY)).toBe("$64.912461"); + }); + it("shows yen with two fewer decimals", () => { const jpy = { currency: "JPY" as const, rate: 158 }; - expect(formatMoney(0.0012, 4, jpy)).toBe("≈¥0.19"); - expect(formatMoney(30470.55, 2, jpy)).toBe("≈¥4,814,347"); - expect(formatMoney(1.5, 1, jpy)).toBe("≈¥237"); - expect(formatMoneyShort(0.5, jpy)).toBe("≈¥79"); + expect(formatMoney(0.0012, 4, jpy)).toBe("¥0.19"); + expect(formatMoney(30470.55, 2, jpy)).toBe("¥4,814,347"); + expect(formatMoney(1.5, 1, jpy)).toBe("¥237"); + expect(formatMoneyShort(0.5, jpy)).toBe("¥79"); }); it("drops the decimals in narrow spots once the amount reaches 100", () => { const cny = { currency: "CNY" as const, rate: 6.8 }; expect(formatMoneyShort(41.2, USD_DISPLAY)).toBe("$41.20"); expect(formatMoneyShort(123.456, USD_DISPLAY)).toBe("$123"); - expect(formatMoneyShort(41.2, cny)).toBe("≈¥280"); - expect(formatMoneyShort(14.5, cny)).toBe("≈¥98.60"); + expect(formatMoneyShort(41.2, cny)).toBe("¥280"); + expect(formatMoneyShort(14.5, cny)).toBe("¥98.60"); expect(formatMoneyShort(undefined, cny)).toBe("--"); }); }); diff --git a/tests/hooks/useMoneyFormat.test.tsx b/tests/hooks/useMoneyFormat.test.tsx index e5014ea9a..6aac0cf04 100644 --- a/tests/hooks/useMoneyFormat.test.tsx +++ b/tests/hooks/useMoneyFormat.test.tsx @@ -31,9 +31,9 @@ describe("useMoneyFormat", () => { }); expect(fmt.currency).toBe("CNY"); expect(fmt.rate).toBe(7); - expect(fmt.money(1.5, 2)).toBe("≈¥10.50"); - expect(fmt.moneyShort(41.2)).toBe("≈¥288"); + expect(fmt.money(1.5, 2)).toBe("¥10.50"); + expect(fmt.moneyShort(41.2)).toBe("¥288"); expect(fmt.usdHint(1.5)).toBe("$1.500000"); - expect(fmt.moneyTitle(1.5, 6)).toBe("≈¥10.500000 ($1.500000)"); + expect(fmt.moneyTitle(1.5, 6)).toBe("¥10.50 ($1.500000)"); }); });