feat(export): dimension primitives, date members, status index
This commit is contained in:
@@ -54,6 +54,54 @@ export function listExportableParams(dailyTemplate, statuses) {
|
|||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const EXPORT_DIMENSIONS = [
|
||||||
|
{ dim: 'date', label: 'Date' },
|
||||||
|
{ dim: 'subject', label: 'Subject' },
|
||||||
|
{ dim: 'parameter', label: 'Parameter' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function dimLabel(dim) {
|
||||||
|
return EXPORT_DIMENSIONS.find((d) => d.dim === dim)?.label ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function otherDimension(rowDim, colDim) {
|
||||||
|
return EXPORT_DIMENSIONS.map((d) => d.dim).find((d) => d !== rowDim && d !== colDim) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve a subject's group value for a given group field id.
|
||||||
|
// null -> no grouping (field is falsy or '__none__')
|
||||||
|
// '—' -> grouping is on but this subject has no value
|
||||||
|
export function subjectGroupValue(animal, groupField) {
|
||||||
|
if (!groupField || groupField === '__none__') return null;
|
||||||
|
if (groupField === '__name__') return animal?.animal_name ?? '—';
|
||||||
|
if (groupField === '__id__') return animal?.animal_id_string ?? '—';
|
||||||
|
const v = animal?.subject_info?.[groupField];
|
||||||
|
return v === null || v === undefined || v === '' ? '—' : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listDateMembers(statuses) {
|
||||||
|
const keys = new Set();
|
||||||
|
for (const s of statuses ?? []) {
|
||||||
|
if (s?.date) keys.add(dateKey(s.date));
|
||||||
|
}
|
||||||
|
return [...keys].sort().map((k) => ({ id: k, label: k, dateKey: k }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// dateKey -> Map(animalId -> status); first status per (date, animal) wins,
|
||||||
|
// scanning in date order (matches the legacy buildMatrix tie-break).
|
||||||
|
export function buildStatusIndex(statuses) {
|
||||||
|
const index = new Map();
|
||||||
|
const ordered = [...(statuses ?? [])].sort((a, b) => dateKey(a?.date).localeCompare(dateKey(b?.date)));
|
||||||
|
for (const s of ordered) {
|
||||||
|
if (!s?.date || s.animal_id === null || s.animal_id === undefined) continue;
|
||||||
|
const dk = dateKey(s.date);
|
||||||
|
if (!index.has(dk)) index.set(dk, new Map());
|
||||||
|
const byAnimal = index.get(dk);
|
||||||
|
if (!byAnimal.has(s.animal_id)) byAnimal.set(s.animal_id, s);
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
// A cell has a value unless it is null, undefined, or the empty string.
|
// A cell has a value unless it is null, undefined, or the empty string.
|
||||||
// (0 and false are real values.)
|
// (0 and false are real values.)
|
||||||
function hasValue(v) {
|
function hasValue(v) {
|
||||||
|
|||||||
@@ -155,3 +155,68 @@ describe('csvFilename', () => {
|
|||||||
expect(csvFilename('', '')).toBe('export.csv');
|
expect(csvFilename('', '')).toBe('export.csv');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
import {
|
||||||
|
EXPORT_DIMENSIONS, dimLabel, otherDimension, subjectGroupValue,
|
||||||
|
listDateMembers, buildStatusIndex,
|
||||||
|
} from '../src/lib/dataExport';
|
||||||
|
|
||||||
|
describe('dimension primitives', () => {
|
||||||
|
it('exposes the three dimensions with labels', () => {
|
||||||
|
expect(EXPORT_DIMENSIONS.map((d) => d.dim)).toEqual(['date', 'subject', 'parameter']);
|
||||||
|
expect(dimLabel('date')).toBe('Date');
|
||||||
|
expect(dimLabel('subject')).toBe('Subject');
|
||||||
|
expect(dimLabel('parameter')).toBe('Parameter');
|
||||||
|
});
|
||||||
|
it('otherDimension returns the leftover', () => {
|
||||||
|
expect(otherDimension('date', 'subject')).toBe('parameter');
|
||||||
|
expect(otherDimension('parameter', 'date')).toBe('subject');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('subjectGroupValue', () => {
|
||||||
|
const animal = { animal_name: 'Alpha', animal_id_string: 'R-001', subject_info: { sex: 'M', cohort: '' } };
|
||||||
|
it('returns null when no group field', () => {
|
||||||
|
expect(subjectGroupValue(animal, '__none__')).toBeNull();
|
||||||
|
expect(subjectGroupValue(animal, '')).toBeNull();
|
||||||
|
});
|
||||||
|
it('resolves name / id / subject_info fields', () => {
|
||||||
|
expect(subjectGroupValue(animal, '__name__')).toBe('Alpha');
|
||||||
|
expect(subjectGroupValue(animal, '__id__')).toBe('R-001');
|
||||||
|
expect(subjectGroupValue(animal, 'sex')).toBe('M');
|
||||||
|
});
|
||||||
|
it('renders missing or empty values as an em dash', () => {
|
||||||
|
expect(subjectGroupValue(animal, 'cohort')).toBe('—');
|
||||||
|
expect(subjectGroupValue({}, 'sex')).toBe('—');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('listDateMembers', () => {
|
||||||
|
it('returns sorted unique dateKeys with id/label', () => {
|
||||||
|
const statuses = [
|
||||||
|
{ date: '2026-07-02T00:00:00.000Z' },
|
||||||
|
{ date: '2026-07-01T12:00:00.000Z' },
|
||||||
|
{ date: '2026-07-02T09:00:00.000Z' },
|
||||||
|
];
|
||||||
|
expect(listDateMembers(statuses)).toEqual([
|
||||||
|
{ id: '2026-07-01', label: '2026-07-01', dateKey: '2026-07-01' },
|
||||||
|
{ id: '2026-07-02', label: '2026-07-02', dateKey: '2026-07-02' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('tolerates empty input', () => {
|
||||||
|
expect(listDateMembers(undefined)).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buildStatusIndex', () => {
|
||||||
|
it('indexes by dateKey then animalId, first status wins', () => {
|
||||||
|
const statuses = [
|
||||||
|
{ animal_id: 'a1', date: '2026-07-01T00:00:00.000Z', analysis_summary: { total: 7 } },
|
||||||
|
{ animal_id: 'a1', date: '2026-07-01T06:00:00.000Z', analysis_summary: { total: 99 } },
|
||||||
|
{ animal_id: 'a2', date: '2026-07-01T00:00:00.000Z', analysis_summary: { total: 3 } },
|
||||||
|
];
|
||||||
|
const idx = buildStatusIndex(statuses);
|
||||||
|
expect(idx.get('2026-07-01').get('a1').analysis_summary.total).toBe(7);
|
||||||
|
expect(idx.get('2026-07-01').get('a2').analysis_summary.total).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user