Remove 8 inline formatPrice/formatVND/formatPriceM2 functions scattered across components and pages, replacing them with imports from @/lib/currency. Add formatVNDFull (full locale, no compact notation) for chuyen-nhuong pages. Fix price-history-chart off-by-1000 bug caused by double-dividing through priceToMillions then formatMillions. Add k/m² branch to formatPricePerM2 for sub-million values. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
144 lines
4.5 KiB
TypeScript
144 lines
4.5 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ỷ`;
|
|
}
|
|
|
|
if (num >= 1_000_000) {
|
|
const millions = num / 1_000_000;
|
|
return `${stripTrailingZero(millions.toFixed(1))} triệu`;
|
|
}
|
|
|
|
return num.toLocaleString('vi-VN');
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Variant: with currency suffix
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Format a VND amount with a " đ" currency suffix.
|
|
* Returns "Miễn phí" 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 đ';
|
|
if (num === 0) return 'Miễn phí';
|
|
|
|
if (num >= 1_000_000_000) {
|
|
const billions = num / 1_000_000_000;
|
|
return `${stripTrailingZero(billions.toFixed(1))} tỷ đ`;
|
|
}
|
|
|
|
if (num >= 1_000_000) {
|
|
const millions = num / 1_000_000;
|
|
return `${stripTrailingZero(millions.toFixed(1))} triệu đ`;
|
|
}
|
|
|
|
return num.toLocaleString('vi-VN') + ' đ';
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Variant: price per square metre
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Format a VND/m² value.
|
|
*
|
|
* @example
|
|
* formatPricePerM2(50_500_000) // "50.5 tr/m²"
|
|
* formatPricePerM2(500_000) // "500k/m²"
|
|
*/
|
|
export function formatPricePerM2(price: string | number): string {
|
|
const num = typeof price === 'string' ? Number(price) : price;
|
|
if (!Number.isFinite(num) || num < 0) return '0 đ/m²';
|
|
|
|
if (num >= 1_000_000_000) {
|
|
const billions = num / 1_000_000_000;
|
|
return `${stripTrailingZero(billions.toFixed(1))} tỷ/m²`;
|
|
}
|
|
|
|
if (num >= 1_000_000) {
|
|
const millions = num / 1_000_000;
|
|
return `${stripTrailingZero(millions.toFixed(1))} tr/m²`;
|
|
}
|
|
|
|
if (num >= 1_000) {
|
|
return `${Math.round(num / 1_000)}k/m²`;
|
|
}
|
|
|
|
return `${num.toLocaleString('vi-VN')} đ/m²`;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Variant: full locale format (no compact notation)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Format a VND amount as a full locale number with " đ" suffix.
|
|
* Unlike formatVND, this never uses compact notation.
|
|
*
|
|
* @example
|
|
* formatVNDFull(4_990_000) // "4.990.000 đ"
|
|
* formatVNDFull(0) // "0 đ"
|
|
*/
|
|
export function formatVNDFull(amount: string | number): string {
|
|
const num = typeof amount === 'string' ? Number(amount) : amount;
|
|
if (!Number.isFinite(num) || num < 0) return '0 đ';
|
|
return new Intl.NumberFormat('vi-VN').format(num) + ' đ';
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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$/, '');
|
|
}
|