UNPKG

xport-js

Version:

Node.js library to read SAS XPORT v5/v6 data transport files (*.xpt).

274 lines (226 loc) 9.88 kB
import Library from '../src/classes/library'; import Filter, { ColumnMetadata } from 'js-array-filter'; interface DsMetadata { dataset: string label: string length: number name: string type: string } interface AlfalfaRecord { POP: string SAMPLE: string REP: string SEEDWT: string HARV1: string HARV2: string } const path = `${__dirname}/Alfalfa.xpt`; const pathADTTE = `${__dirname}/adtte.xpt`; describe('Library default checks', () => { it('Library type should be a function', () => { expect(typeof Library).toBe('function'); }); }); describe('Can read an xpt file', () => { it('Library should have pathToFile field', () => { const lib = new Library(path); expect(lib.pathToFile).toBe(path); }); it('Library should provide metadata', async () => { const lib = new Library(path); const metadata: DsMetadata[] = await lib.getMetadata() as DsMetadata[]; expect(metadata.length).toBe(6); const firstElement = metadata[0]; expect(firstElement.dataset).toBe('SPEC'); expect(firstElement.name).toBe('POP'); expect(firstElement.label).toBe(''); expect(firstElement.type).toBe('Char'); }); it('Library should provide metadata in dataset-json1.1 format', async () => { const lib = new Library(path); const metadata = await lib.getMetadata('dataset-json1.1'); // Check required dataset-json1.1 properties expect(metadata.name).toBe('SPEC'); expect(metadata.records).toBe(40); expect(Array.isArray(metadata.columns)).toBe(true); expect(metadata.columns.length).toBe(6); // Check first column structure const firstColumn = metadata.columns[0]; expect(firstColumn.itemOID).toBe('POP'); expect(firstColumn.name).toBe('POP'); expect(firstColumn.dataType).toBe('string'); expect(typeof firstColumn.length).toBe('number'); // Check source system info expect(metadata.sourceSystem).toBeDefined(); expect(typeof metadata.sourceSystem?.name).toBe('string'); expect(typeof metadata.sourceSystem?.version).toBe('string'); // Check datetime fields expect(metadata.datasetJSONCreationDateTime).toBeDefined(); expect(metadata.dbLastModifiedDateTime).toBeDefined(); }); }); describe('Can read xpt records using await iterable', () => { it('Records read are valid', async () => { const lib = new Library(path); const records: (string | number | object)[] = []; for await (const obs of lib.read({ rowFormat: 'object' })) { records.push(obs); } const headers = records.shift() as AlfalfaRecord; expect(headers.POP).toBe(''); expect(headers.SAMPLE).toBe(''); expect(headers.REP).toBe(''); expect(headers.SEEDWT).toBe(''); expect(headers.HARV1).toBe(''); expect(headers.HARV2).toBe(''); const firstElement: AlfalfaRecord = records[0] as AlfalfaRecord; expect(firstElement.POP).toBe('min'); expect(firstElement.SAMPLE).toBe(0); expect(firstElement.REP).toBe(1); expect(firstElement.SEEDWT).toBe(64); expect(firstElement.HARV1).toBe(171.7); expect(firstElement.HARV2).toBe(180.3); expect(records.length).toBe(40); }); }); describe('Can read xpt records using await function', () => { it('Records read are valid', async () => { const lib = new Library(path); const records = await lib.getData({ type: 'object', skipHeader: false }); const headers = records.shift() as AlfalfaRecord; expect(headers.POP).toBe(''); expect(headers.SAMPLE).toBe(''); expect(headers.REP).toBe(''); expect(headers.SEEDWT).toBe(''); expect(headers.HARV1).toBe(''); expect(headers.HARV2).toBe(''); const firstElement: AlfalfaRecord = records[0] as AlfalfaRecord; expect(firstElement.POP).toBe('min'); expect(firstElement.SAMPLE).toBe(0); expect(firstElement.REP).toBe(1); expect(firstElement.SEEDWT).toBe(64); expect(firstElement.HARV1).toBe(171.7); expect(firstElement.HARV2).toBe(180.3); expect(records.length).toBe(40); }); it('Should round numeric values according to precision', async () => { const lib = new Library(path); const records = await lib.getData({ type: 'object', roundPrecision: 1 }); const firstElement: AlfalfaRecord = records[1] as AlfalfaRecord; expect(firstElement.HARV1).toBe(138.2); // Original value preserved const roundedRecords = await lib.getData({ type: 'object', roundPrecision: 0 }); const firstRoundedElement: AlfalfaRecord = roundedRecords[1] as AlfalfaRecord; expect(firstRoundedElement.HARV1).toBe(138); // Rounded to nearest integer }); it('Should filter records correctly', async () => { const lib = new Library(path); const columns = await lib.getMetadata() as DsMetadata[]; const updatedColumns: ColumnMetadata[] = columns.map((column) => { return ({ ...column, dataType: column.type } as ColumnMetadata); }); const filter = new Filter("xpt", updatedColumns, { conditions: [ { variable: "POP", operator: "eq", value: 'MAX' }, { variable: "SAMPLE", operator: "ge", value: 3 }, ], connectors: ["and"], }); const records = await lib.getData({ type: 'array', filter }); expect(records.length).toBe(20); }); }); describe('Test parseHeader method', () => { it('Should correctly parse the header', async () => { const lib = new Library(path); const _metadata: DsMetadata[] = await lib.getMetadata() as DsMetadata[]; const header = lib.getHeader(); expect(header).toMatchSnapshot(); }); }); describe('Test missing values', () => { it('Should read missing values as null', async () => { const lib = new Library(pathADTTE); const columns = await lib.getMetadata() as DsMetadata[]; const updatedColumns: ColumnMetadata[] = columns.map((column) => { return ({ ...column, dataType: column.type } as ColumnMetadata); }); const filter = new Filter("xpt", updatedColumns, { conditions: [ { variable: "SRCSEQ", operator: "eq", value: null }, ], connectors: [], }); const records = await lib.getData({ type: 'array', filter }); expect(records.length).toBe(102); }); it('Should read missing values as null when rounding is enabled', async () => { const lib = new Library(pathADTTE); const columns = await lib.getMetadata() as DsMetadata[]; const updatedColumns: ColumnMetadata[] = columns.map((column) => { return ({ ...column, dataType: column.type } as ColumnMetadata); }); const filter = new Filter("xpt", updatedColumns, { conditions: [ { variable: "SRCSEQ", operator: "eq", value: null }, ], connectors: [], }); const records = await lib.getData({ type: 'array', filter, roundPrecision: 10 }); expect(records.length).toBe(102); }); }); describe('Test getUniqueValues method', () => { it('Should return unique values for specified columns (ADTTE)', async () => { const lib = new Library(pathADTTE); const unique = await lib.getUniqueValues({ columns: ['PARAMCD', 'AGE', 'SEX', 'RACE'] }); // Should have keys for each requested column expect(Object.keys(unique)).toEqual(expect.arrayContaining(['PARAMCD', 'AGE', 'SEX', 'RACE'])); expect(Array.isArray(unique.PARAMCD.values)).toBe(true); expect(unique.PARAMCD.values.length).toBeGreaterThan(0); expect(unique.PARAMCD.values).toEqual(['TTDE']); expect(Array.isArray(unique.AGE.values)).toBe(true); expect(unique.AGE.values.length).toEqual(36); expect(typeof unique.AGE.values[0]).toBe('number'); // SEX should have 'M' and 'F' expect(unique.SEX.values.sort()).toEqual(['F', 'M']); // RACE should have the specified values expect(unique.RACE.values).toEqual( expect.arrayContaining([ 'WHITE', 'BLACK OR AFRICAN AMERICAN', 'AMERICAN INDIAN OR ALASKA NATIVE' ]) ); }); it('Should respect the limit parameter (ADTTE)', async () => { const lib = new Library(pathADTTE); const unique = await lib.getUniqueValues({ columns: ['AGE'], limit: 10 }); expect(unique.AGE.values.length).toEqual(10); }); it('Should add counts when addCount is true (ADTTE)', async () => { const lib = new Library(pathADTTE); const unique = await lib.getUniqueValues({ columns: ['RACE'], addCount: true }); expect(unique.RACE.counts).toBeDefined(); // Check that counts are correct expect(unique.RACE.counts).toMatchSnapshot(); }); it('Should sort unique values when sort is true (ADTTE)', async () => { const lib = new Library(pathADTTE); const unique = await lib.getUniqueValues({ columns: ['AGE'], sort: true }); const sorted = [...unique.AGE.values].sort((a, b) => Number(a) - Number(b)); expect(unique.AGE.values).toEqual(sorted); }); it('Should reject if column does not exist (ADTTE)', async () => { const lib = new Library(pathADTTE); await expect(lib.getUniqueValues({ columns: ['NOT_A_COLUMN'] })) .rejects .toThrow(/not found/); }); });