All files / src/app/core/utils/csv csv.import-handler.ts

89.18% Statements 33/37
76.92% Branches 10/13
90% Functions 9/10
88.23% Lines 30/34

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 907x               7x                       4x   4x 1x 1x     3x     3x 3x 3x 3x 3x 4x 2x 1x 1x     2x 2x       3x         3x 3x 3x     3x 3x 3x 3x                         7x               2x     2x     3x      
import { Observable } from 'rxjs';
 
export interface CsvImportData {
  headers: string[];
  data: string[];
}
 
export type CsvImportStatus = 'Default' | 'Valid' | 'InvalidFormat' | 'InvalidHeader' | 'InvalidData';
export class CsvImportHandler {
  /**
   * Processes a CSV file and extracts its content and headers.
   * Validates the file format and optionally validates headers against expected values.
   *
   * @param file            The CSV file to process.
   * @param expectedHeaders Optional array of expected header names for validation.
   * @returns               An observable emitting CsvImportData containing the parsed data lines and headers.
   *                        Emits an error with 'InvalidFormat' for non-CSV files or file read errors,
   *                        or 'InvalidHeader' if header validation fails.
   */
  static processCsvFile(file: File, expectedHeaders: string[] = []): Observable<CsvImportData> {
    return new Observable<CsvImportData>(subscriber => {
      // Reject files that do not match the expected CSV extension.
      if (!file.name.endsWith('.csv')) {
        subscriber.error('InvalidFormat');
        return;
      }
 
      const reader = new FileReader();
 
      // Emit parsed data once the file has been successfully read.
      const handleLoad = () => {
        const fileContent = reader.result as string;
        const lines = CsvImportHandler.parseCsvFileContent(fileContent);
        let headers: string[] = [];
        if (expectedHeaders.length > 0 && lines.length > 0) {
          headers = lines[0].split(',').map(h => h.trim());
          if (!CsvImportHandler.validateHeaders(headers, expectedHeaders)) {
            subscriber.error('InvalidHeader');
            return;
          }
        }
        subscriber.next({ data: lines.slice(expectedHeaders.length > 0 ? 1 : 0), headers });
        subscriber.complete();
      };
 
      // Surface FileReader failures as InvalidFormat errors to consumers.
      const handleError = () => {
        subscriber.error('InvalidFormat');
      };
 
      // Connect FileReader events and kick off the actual read.
      reader.addEventListener('load', handleLoad);
      reader.addEventListener('error', handleError);
      reader.readAsText(file);
 
      // Remove handlers and abort pending reads to prevent leaks on unsubscribe.
      return () => {
        reader.removeEventListener('load', handleLoad);
        reader.removeEventListener('error', handleError);
        Iif (reader.readyState === FileReader.LOADING) {
          reader.abort();
        }
      };
    });
  }
 
  /**
   * Parses CSV file content into an array of lines.
   * Splits the content by newlines and filters out empty lines.
   * Supports Windows (\r\n), Unix (\n), and Mac (\r) line endings.
   */
  private static parseCsvFileContent(fileContent: string): string[] {
    return fileContent.split(/\r\n|\n|\r/).filter(line => line.trim() !== '');
  }
 
  /**
   * Validates that the file headers match the expected headers.
   * Checks for header existence, count matching, and that all expected headers are present.
   */
  private static validateHeaders(fileHeaders: string[], expectedHeaders: string[]): boolean {
    Iif (!fileHeaders || fileHeaders.length === 0) {
      return false;
    }
    Iif (fileHeaders.length !== expectedHeaders.length) {
      return false;
    }
    return expectedHeaders.every(expected => fileHeaders.includes(expected));
  }
}