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

5.4% Statements 2/37
0% Branches 0/13
0% Functions 0/10
5.88% Lines 2/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 906x               6x                                                                                                                                                                  
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.
      Iif (!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[] = [];
        Iif (expectedHeaders.length > 0 && lines.length > 0) {
          headers = lines[0].split(',').map(h => h.trim());
          Iif (!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));
  }
}