/**
* Shared formatting utilities for safe data transformation and display
*/
import { CHARACTER_LIMIT } from "../constants.js";
import { logError } from "../utils/errors.js";
// Formatting constants
const DEFAULT_FALLBACK = "—";
const FORMAT_ERROR_SYMBOL = "✗ Format error";
const TRUNCATION_SUFFIX =
"\n\n... [Output truncated. Use pagination or filters to reduce results.]";
const TRUNCATION_BUFFER = 100;
/**
* Truncate text if it exceeds CHARACTER_LIMIT.
*
* Ensures output stays within bounds while preserving readability by adding
* a clear truncation notice.
*
* @param text - Text to potentially truncate
* @returns Original text if within limit, otherwise truncated text with notice
*
* @example
* ```ts
* truncateIfNeeded("short text"); // returns "short text"
* truncateIfNeeded(veryLongText); // returns truncated with "... [Output truncated...]"
* ```
*/
export function truncateIfNeeded(text: string): string {
if (text.length <= CHARACTER_LIMIT) {
return text;
}
const truncationPoint = CHARACTER_LIMIT - TRUNCATION_BUFFER;
return `${text.slice(0, truncationPoint)}${TRUNCATION_SUFFIX}`;
}
/**
* Safely format data with fallback for null/undefined and error handling.
*
* Wraps formatter functions to gracefully handle null/undefined inputs and
* catch formatting errors, ensuring output never fails catastrophically.
*
* @template T - Type of data being formatted
* @param data - Data to format (may be null/undefined)
* @param formatter - Pure function that transforms data to string
* @param fallback - String to return for null/undefined (default: "—")
* @returns Formatted string, fallback for null/undefined, or error symbol on exception
*
* @example
* ```ts
* // Handle null/undefined
* formatSafely(null, (x) => `Value: ${x}`); // returns "—"
* formatSafely(undefined, (x) => `Value: ${x}`, "N/A"); // returns "N/A"
*
* // Format valid data
* formatSafely("test", (x) => x.toUpperCase()); // returns "TEST"
*
* // Handle formatter errors
* formatSafely({}, (x) => x.nonExistent.property); // returns "✗ Format error"
* ```
*/
export function formatSafely<T>(
data: T | null | undefined,
formatter: (data: T) => string,
fallback: string = DEFAULT_FALLBACK
): string {
if (data == null) {
return fallback;
}
try {
return formatter(data);
} catch (error) {
logError(error, { operation: "safeFormat" });
return FORMAT_ERROR_SYMBOL;
}
}
/**
* Get value or fallback for null/undefined.
*
* Provides consistent null-handling across the codebase using nullish
* coalescing. Preserves falsy values like 0, false, and empty strings.
*
* @template T - Type of value being checked
* @param value - Value to check for null/undefined
* @param fallback - String to return for null/undefined (default: "—")
* @returns Original value if not null/undefined, otherwise fallback
*
* @example
* ```ts
* getValueOrFallback(null); // returns "—"
* getValueOrFallback(undefined); // returns "—"
* getValueOrFallback(0); // returns 0 (preserves falsy)
* getValueOrFallback(false); // returns false (preserves falsy)
* getValueOrFallback(""); // returns "" (preserves empty string)
* getValueOrFallback("test"); // returns "test"
* ```
*/
export function getValueOrFallback<T>(
value: T | null | undefined,
fallback: string = DEFAULT_FALLBACK
): T | string {
return value ?? fallback;
}
/**
* Generate freshness timestamp for volatile data output.
*
* Per STYLE.md Section 3.6, volatile data (logs, stats, resources, uptime,
* discovery scans) must include a freshness timestamp to indicate when the
* data was captured.
*
* @param timezone - IANA timezone identifier (default: "America/New_York")
* @returns Formatted timestamp string: "As of (<TZ>): HH:MM:SS | MM/DD/YYYY"
*
* @example
* ```ts
* getTimestamp(); // returns "As of (EST): 14:42:10 | 02/13/2026" or "As of (EDT): ..."
* ```
*/
export function getTimestamp(timezone = "America/New_York"): string {
const now = new Date();
const formatter = new Intl.DateTimeFormat("en-US", {
timeZone: timezone,
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
month: "2-digit",
day: "2-digit",
year: "numeric",
timeZoneName: "short",
});
const parts = formatter.formatToParts(now);
const getPart = (type: string) => parts.find((p) => p.type === type)?.value || "";
const time = `${getPart("hour")}:${getPart("minute")}:${getPart("second")}`;
const date = `${getPart("month")}/${getPart("day")}/${getPart("year")}`;
const label = getPart("timeZoneName") || timezone;
return `As of (${label}): ${time} | ${date}`;
}