Ctrl K
ComunidadGuías…

shared/utils/csv.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/* oxlint-disable no-control-regex */

/**
 * Helper class for CSV operations.
 */
export class CSVHelper {
  /**
   * Sanitizes a value for CSV output.
   *
   * @param value The value to sanitize.
   * @returns The sanitized value.
   */
  public static sanitizeValue(value: string): string {
    if (!value) {
      return "";
    }

    return (
      value
        .toString()
        // Control characters (excluding tab, newline, and carriage return)
        .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/gu, "")
        // Zero-width spaces
        .replace(/[\u200B-\u200D\uFEFF]/g, "")
        // Bidirectional control
        .replace(/[\u202A-\u202E\u2066-\u2069]/g, "")
        // Formula triggers, last so that removed characters cannot hide one
        .replace(/^([+\-=@∑√∏<><>≤≥=≠±÷×])/u, "'$1")
    );
  }

  /**
   * Escapes a CSV field value by wrapping it in quotes if necessary.
   *
   * @param value The value to escape.
   * @returns The escaped value.
   */
  public static escapeCSVField(value: unknown): string {
    if (value === null || value === undefined) {
      return "";
    }

    const stringValue =
      typeof value === "object"
        ? JSON.stringify(value)
        : String(value as string | number | boolean);

    // If the value contains comma, quote, or line break, wrap it in quotes and escape internal quotes
    if (
      stringValue.includes(",") ||
      stringValue.includes('"') ||
      stringValue.includes("\n") ||
      stringValue.includes("\r")
    ) {
      return `"${stringValue.replace(/"/g, '""')}"`;
    }

    return stringValue;
  }

  /**
   * Converts an array of objects to CSV format.
   *
   * @param data Array of objects to convert.
   * @param headers Array of header names in the desired order.
   * @returns CSV string.
   */
  public static convertToCSV<T extends Record<string, unknown>>(
    data: T[],
    headers: (keyof T)[]
  ): string {
    if (data.length === 0) {
      return (
        headers
          .map((h) => String(h))
          .map((h) => this.escapeCSVField(this.sanitizeValue(h)))
          .join(",") + "\n"
      );
    }

    const headerRow = headers
      .map((h) => String(h))
      .map((h) => this.escapeCSVField(this.sanitizeValue(h)))
      .join(",");

    const dataRows = data.map((row) =>
      headers
        .map((header) => {
          const value = row[header];
          const stringValue =
            value === null || value === undefined ? "" : String(value);
          return this.escapeCSVField(this.sanitizeValue(stringValue));
        })
        .join(",")
    );

    return [headerRow, ...dataRows].join("\n");
  }
}