Files
dashboard/utils/formatters.ts
T
Alberto-Audrix 32a36cceff
CI / lint-and-test (push) Canceled after 0s
first commit
2026-07-28 08:55:05 +07:00

204 lines
6.0 KiB
TypeScript

/**
* Centralized number formatting utilities.
*
* All formatters use id-ID locale to provide period (.) as thousands separator
* and comma (,) as decimal separator for consistency across the application.
*/
import { FORMATTING_CONFIG } from '../config/formatting.config.ts';
export type NullableNumber = number | null | undefined;
interface FormatOptions {
/** Maximum number of decimal places */
maxDecimals?: number;
/** Minimum number of decimal places */
minDecimals?: number;
/** Enable thousands grouping (default: true) */
useGrouping?: boolean;
}
/**
* Formats an integer with period as thousands separator (Indonesian format).
*
* @param value - The number to format (can be null/undefined)
* @param options - Formatting options
* @returns Formatted string (e.g., "1.234" or "-" for null)
*
* @example
* formatNumber(1234) // "1.234"
* formatNumber(1234567) // "1.234.567"
* formatNumber(123) // "123"
* formatNumber(null) // "-"
*/
export function formatNumber(value: NullableNumber, options?: FormatOptions): string {
if (value == null || isNaN(value)) {
return FORMATTING_CONFIG.FALLBACK.NULL_VALUE;
}
const { maxDecimals = 0, minDecimals = 0, useGrouping = true } = options || {};
return value.toLocaleString(FORMATTING_CONFIG.NUMBER_LOCALE, {
minimumFractionDigits: minDecimals,
maximumFractionDigits: maxDecimals,
useGrouping,
});
}
/**
* Formats a number with decimal places (Indonesian format).
*
* @param value - The number to format
* @param options - Formatting options
* @returns Formatted string with comma as decimal separator
*
* @example
* formatDecimal(1234.56) // "1.234,56"
* formatDecimal(1.5) // "1,5"
* formatDecimal(1.567, { maxDecimals: 2 }) // "1,57"
* formatDecimal(null) // "-"
*/
export function formatDecimal(value: NullableNumber, options?: FormatOptions): string {
if (value == null || isNaN(value)) {
return FORMATTING_CONFIG.FALLBACK.NULL_VALUE;
}
const { maxDecimals = 2, minDecimals = 0, useGrouping = true } = options || {};
return value.toLocaleString(FORMATTING_CONFIG.NUMBER_LOCALE, {
minimumFractionDigits: minDecimals,
maximumFractionDigits: maxDecimals,
useGrouping,
});
}
/**
* Formats a percentage value (Indonesian format with comma decimal).
*
* @param value - The percentage value (0-100 scale, not 0-1)
* @param decimals - Number of decimal places (default: 1)
* @returns Formatted percentage string with comma as decimal separator
*
* @example
* formatPercentage(85.345) // "85,3%"
* formatPercentage(85.345, 2) // "85,35%"
* formatPercentage(null) // "-"
*/
export function formatPercentage(
value: NullableNumber,
decimals: number = FORMATTING_CONFIG.DECIMALS.PERCENTAGE
): string {
if (value == null || isNaN(value)) {
return FORMATTING_CONFIG.FALLBACK.NULL_VALUE;
}
return `${value.toFixed(decimals).replace('.', ',')}%`;
}
/**
* Formats FCR/EEF ratio values with fixed 3 decimal places (Indonesian format).
*
* @param value - The ratio value
* @returns Formatted ratio string with 3 decimals and comma as decimal separator
*
* @example
* formatRatio(1.567) // "1,567"
* formatRatio(1.5) // "1,500"
* formatRatio(null) // "-"
*/
export function formatRatio(value: NullableNumber): string {
if (value == null || isNaN(value)) {
return FORMATTING_CONFIG.FALLBACK.NULL_VALUE;
}
return value.toFixed(FORMATTING_CONFIG.DECIMALS.FCR).replace('.', ',');
}
/**
* Formats a ratio value with fixed decimals, truncating extra digits instead of rounding.
*
* This is useful for KPI cards where the UI must preserve the first N decimals
* without nudging the displayed value upward.
*/
export function formatTruncatedRatio(value: NullableNumber, decimals = 3): string {
if (value == null || isNaN(value)) {
return FORMATTING_CONFIG.FALLBACK.NULL_VALUE;
}
const factor = 10 ** decimals;
const truncated = Math.trunc(value * factor) / factor;
return truncated.toLocaleString(FORMATTING_CONFIG.NUMBER_LOCALE, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
useGrouping: false,
});
}
/**
* Formats a nullable number with custom fallback text.
*
* @param value - The number to format
* @param formatter - Custom formatter function
* @param fallback - Custom fallback text (default: "-")
* @returns Formatted string or fallback
*
* @example
* formatNullable(1234, (v) => formatNumber(v)) // "1.234"
* formatNullable(null, (v) => formatNumber(v)) // "-"
* formatNullable(null, (v) => formatNumber(v), "N/A") // "N/A"
*/
export function formatNullable(
value: NullableNumber,
formatter: (value: number) => string,
fallback: string = FORMATTING_CONFIG.FALLBACK.NULL_VALUE
): string {
if (value == null || isNaN(value)) {
return fallback;
}
return formatter(value);
}
/**
* Standard tooltip formatters for Recharts components.
* These provide consistent formatting across all charts.
*/
export const tooltipFormatters = {
/** Format as integer with comma separator */
number: (value: number): [string, string] => [formatNumber(value), ''],
/** Format as decimal with specified precision */
decimal: (value: number, decimals = 1): [string, string] => [
formatDecimal(value, { maxDecimals: decimals }),
'',
],
/** Format as percentage */
percentage: (value: number, decimals = 1): [string, string] => [
formatPercentage(value, decimals),
'',
],
/** Format as FCR/EEF ratio */
ratio: (value: number): [string, string] => [formatRatio(value), ''],
};
/**
* Creates a YAxis tick formatter function.
*
* @param decimals - Number of decimal places (default: 0 for integers)
* @returns Formatter function for YAxis ticks
*
* @example
* <YAxis tickFormatter={createYAxisFormatter()} />
* <YAxis tickFormatter={createYAxisFormatter(1)} />
*/
export function createYAxisFormatter(decimals = 0): (value: number) => string {
return (value: number) => {
if (decimals === 0) {
return formatNumber(value);
}
return formatDecimal(value, { maxDecimals: decimals });
};
}