- Add `formatCompact` as an exported alias for `formatPrice` in lib/currency.ts - Replace 5 inline copies of the tỷ/triệu compact formatter: - components/map/listing-map.tsx (local `formatPrice` fn) - components/agents/agent-profile-client.tsx (local `fmtVND` fn) - app/(dashboard)/dashboard/saved-searches/page.tsx (local `formatPrice` fn) - app/(public)/page.tsx (local `formatVnd` fn + `vndFmt` Intl instance) - components/listings/price-history-chart.tsx (local `formatMillions` + `priceToMillions`) All call sites now import from the canonical lib/currency module. PriceHistoryChart now stores raw VND in chart data (was: millions) so formatCompact emits correct tỷ/triệu labels using canonical thresholds. Pre-existing test failures in inquiry/lead/AVM specs are unrelated to this change. Co-Authored-By: Paperclip <noreply@paperclip.ing>
134 lines
4.3 KiB
TypeScript
134 lines
4.3 KiB
TypeScript
/**
|
|
* Vietnamese currency formatting utilities.
|
|
*
|
|
* Centralised formatter for all price displays across the platform.
|
|
* Converts raw VND numbers into human-readable Vietnamese format:
|
|
* 3,500,000,000 -> "3.5 ty"
|
|
* 150,000,000 -> "150 trieu"
|
|
* 800,000 -> "800.000"
|
|
*/
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Core formatter
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Format a VND amount into compact Vietnamese notation.
|
|
*
|
|
* @example
|
|
* formatPrice(3_500_000_000) // "3.5 ty"
|
|
* formatPrice(150_000_000) // "150 trieu"
|
|
* formatPrice(1_500_000) // "1.5 trieu"
|
|
* formatPrice(800_000) // "800.000"
|
|
* formatPrice("3500000000") // "3.5 ty" (string input accepted)
|
|
*/
|
|
export function formatPrice(amount: string | number): string {
|
|
const num = typeof amount === 'string' ? Number(amount) : amount;
|
|
if (!Number.isFinite(num) || num < 0) return '0';
|
|
|
|
if (num >= 1_000_000_000) {
|
|
const billions = num / 1_000_000_000;
|
|
return `${stripTrailingZero(billions.toFixed(1))} t\u1ef7`;
|
|
}
|
|
|
|
if (num >= 1_000_000) {
|
|
const millions = num / 1_000_000;
|
|
return `${stripTrailingZero(millions.toFixed(1))} tri\u1ec7u`;
|
|
}
|
|
|
|
return num.toLocaleString('vi-VN');
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Variant: with currency suffix
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Format a VND amount with a " \u0111" currency suffix.
|
|
* Returns "Mi\u1ec5n ph\u00ed" for zero amounts.
|
|
*
|
|
* @example
|
|
* formatVND(4_990_000) // "4.99 trieu d"
|
|
* formatVND(0) // "Mien phi"
|
|
*/
|
|
export function formatVND(amount: string | number): string {
|
|
const num = typeof amount === 'string' ? Number(amount) : amount;
|
|
if (!Number.isFinite(num) || num < 0) return '0 \u0111';
|
|
if (num === 0) return 'Mi\u1ec5n ph\u00ed';
|
|
|
|
if (num >= 1_000_000_000) {
|
|
const billions = num / 1_000_000_000;
|
|
return `${stripTrailingZero(billions.toFixed(1))} t\u1ef7 \u0111`;
|
|
}
|
|
|
|
if (num >= 1_000_000) {
|
|
const millions = num / 1_000_000;
|
|
return `${stripTrailingZero(millions.toFixed(1))} tri\u1ec7u \u0111`;
|
|
}
|
|
|
|
return num.toLocaleString('vi-VN') + ' \u0111';
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Variant: price per square metre
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Format a VND/m\u00b2 value.
|
|
*
|
|
* @example
|
|
* formatPricePerM2(50_500_000) // "50.5 tr/m\u00b2"
|
|
* formatPricePerM2(500_000) // "500.000 \u0111/m\u00b2"
|
|
*/
|
|
export function formatPricePerM2(price: string | number): string {
|
|
const num = typeof price === 'string' ? Number(price) : price;
|
|
if (!Number.isFinite(num) || num < 0) return '0 \u0111/m\u00b2';
|
|
|
|
if (num >= 1_000_000_000) {
|
|
const billions = num / 1_000_000_000;
|
|
return `${stripTrailingZero(billions.toFixed(1))} t\u1ef7/m\u00b2`;
|
|
}
|
|
|
|
if (num >= 1_000_000) {
|
|
const millions = num / 1_000_000;
|
|
return `${stripTrailingZero(millions.toFixed(1))} tr/m\u00b2`;
|
|
}
|
|
|
|
return `${num.toLocaleString('vi-VN')} \u0111/m\u00b2`;
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Alias: formatCompact
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Alias for {@link formatPrice}.
|
|
* Use this name when the call-site intent is compact/abbreviated display
|
|
* rather than a full price string (e.g. chart tick labels, map markers).
|
|
*/
|
|
export const formatCompact = formatPrice;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Parser (reverse direction)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Parse a formatted Vietnamese price string back into a number.
|
|
* Returns `null` if the input cannot be parsed.
|
|
*/
|
|
export function parseVND(formatted: string): number | null {
|
|
const cleaned = formatted.replace(/[^\d]/g, '');
|
|
if (cleaned === '') return null;
|
|
return Number(cleaned);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** Remove a trailing ".0" so "3.0 ty" becomes "3 ty". */
|
|
function stripTrailingZero(str: string): string {
|
|
return str.replace(/\.0$/, '');
|
|
}
|