Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions src/lib/format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
//
// Nothing imports it, so the bundle never includes it.
import {
axisBytes, axisTop, bytes, cpuName, cycle, daysUntil, despike, osName, pair, percent, quarters, RATE_FLOOR,
rateAxis, tickClock, timeTicks, uptime, windows, withGaps,
axisBytes, axisTop, bytes, cpuName, cycle, daysUntil, despike, money, osName, pair, percent, quarters,
RATE_FLOOR, rateAxis, tickClock, timeTicks, uptime, windows, withGaps,
} from "./format.ts"

let failed = 0
Expand Down Expand Up @@ -173,6 +173,15 @@ eq(uptime(2 * 86400 + 5 * 3600), "2 天 5 小时", "超过一天不再写分钟"
// 旧 hub 存名称,新 hub 把其余长度存成 `<n>m`;两种写法同一个长度读法一致。
eq(["yearly", "12m", "60m", "18m", "once", "weekly"].map(cycle), ["年付", "年付", "5 年付", "18 个月付", "一次性", "weekly"], "付款周期")

// money: zh-CN whatever the browser's language, so US$ stands apart from HK$
// and JP¥ from ¥; a code Intl refuses is shown rather than thrown.
eq(money(100, "CNY"), "¥100.00", "人民币")
eq(money(100, "USD"), "US$100.00", "美元与港币等其它元区分开")
eq(money(100, "HKD"), "HK$100.00", "港币同样符号在前")
eq(money(1200, "JPY"), "JP¥1,200", "日元不带小数,与人民币分开")
eq(money(1200.5, "JPY"), "JP¥1,200.5", "日元填了小数照实显示,不取整")
eq(money(100, "港币"), "港币 100.00", "旧 hub 存下的非法代码不抛错")

eq(osName("Debian GNU/Linux 12 (bookworm)"), "Debian 12", "发行版名去掉代号")
eq(cpuName("Intel(R) Xeon(R) CPU E5-2680 8-Core Processor"), "Intel Xeon E5-2680", "CPU 名去掉商标和核数")

Expand Down
24 changes: 22 additions & 2 deletions src/lib/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,30 @@ export function daysUntil(date?: string | null): number | null {
*/
export const FOREVER = "∞"

const SYMBOLS: Record<string, string> = { USD: "$", CNY: "¥", EUR: "€", GBP: "£", JPY: "¥" }
const MONEY = new Map<string, Intl.NumberFormat>()

/**
* A price as zh-CN writes it: ¥12.00, US$12.00, HK$12.00, JP¥1,200, and the code
* ahead of the amount where the locale has no symbol, as in SGD 12.00. The
* locale is fixed so the figure does not vary with the browser's language, and
* so JPY reads JP¥, apart from CNY. Fractions stop at two places, the precision
* the price is entered in, not at the currency's minor unit: rounding to whole
* yen would show a price of 0.4 as JP¥0. A formatter costs about 100 µs to
* build, hence one per currency.
*/
export function money(amount: number, currency: string): string {
return `${SYMBOLS[currency] ?? ""}${amount.toFixed(2)}${SYMBOLS[currency] ? "" : ` ${currency}`}`
try {
let format = MONEY.get(currency)
if (!format) {
format = new Intl.NumberFormat("zh-CN", { style: "currency", currency, maximumFractionDigits: 2 })
MONEY.set(currency, format)
}
return format.format(amount)
} catch {
// Intl throws on anything but three letters, which hubs before 1.3.1 stored
// unchecked when written through the API.
return `${currency} ${amount.toFixed(2)}`.trim()
}
}

// Hub 1.3.0 and earlier store only these names; later hubs store any other
Expand Down
Loading