Skip to content

Commit

Permalink
feat: sort categorical variables with hierarchy being order created
Browse files Browse the repository at this point in the history
  • Loading branch information
buckhalt committed Apr 18, 2024
1 parent be22e8d commit 8548a8c
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 3 deletions.
113 changes: 112 additions & 1 deletion src/utils/__tests__/createSorter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,117 @@ describe('Types', () => {
});
});

describe('Categorical sorting', () => {
it('sorts items based on categorical values', () => {
const mockItems = [
{
category: ['cow'],
name: 'alice',
},
{
category: ['duck'],
name: 'bob',
},
{
category: ['lizard'],
name: 'charlie',
},
{
category: ['cow'],
name: 'david',
},
];

const sorter = createSorter([
{
property: 'category',
type: 'categorical',
hierarchy: ['duck', 'lizard', 'cow'],
},
{
property: 'name',
type: 'string',
direction: 'asc',
},
]);

const result = sorter(mockItems).map((item) => item.name);
expect(result).toEqual(['alice', 'david', 'charlie', 'bob']);
});

it('handles items with multiple categories', () => {
const mockItems = [
{
category: ['duck', 'lizard'],
name: 'alice',
},
{
category: ['cow', 'duck'],
name: 'bob',
},
{
category: ['cow'],
name: 'charlie',
},
{
category: ['lizard'],
name: 'david',
},
];

const sorter = createSorter([
{
property: 'category',
type: 'categorical',
hierarchy: ['cow', 'duck', 'lizard'],
},
{
property: 'name',
type: 'string',
direction: 'asc',
},
]);

const result = sorter(mockItems).map((item) => item.name);
expect(result).toEqual(['david', 'alice', 'bob', 'charlie']);
});

it('handles missing categories', () => {
const mockItems = [
{
name: 'alice',
},
{
category: ['duck'],
name: 'bob',
},
{
category: ['lizard'],
name: 'charlie',
},
{
name: 'david',
},
];

const sorter = createSorter([
{
property: 'category',
type: 'categorical',
hierarchy: ['lizard', 'duck', 'cow'],
},
{
property: 'name',
type: 'string',
direction: 'asc',
},
]);

const result = sorter(mockItems).map((item) => item.name);
expect(result).toEqual(['bob', 'charlie', 'alice', 'david']);
});
});

describe('Order direction', () => {
it('orders ascending with "asc"', () => {
const mockItems = [
Expand Down Expand Up @@ -994,7 +1105,7 @@ describe('processProtocolSortRule', () => {
property: 'category',
direction: 'asc',
};
expect(processProtocolSortRule(codebookVariables)(rule).type).toEqual('string');
expect(processProtocolSortRule(codebookVariables)(rule).type).toEqual('categorical');
});

it('ordinal', () => {
Expand Down
39 changes: 37 additions & 2 deletions src/utils/createSorter.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,36 @@ const stringFunction = ({ property, direction }) => (a, b) => {
return collator.compare(secondValue, firstValue);
};

const categoricalFunction = ({ property, direction, hierarchy = [] }) => (a, b) => {
// hierarchy is whatever order the variables were specified in the variable definition
const firstValues = get(a, property, []);
const secondValues = get(b, property, []);

for (let i = 0; i < Math.max(firstValues.length, secondValues.length); i += 1) {
const firstValue = i < firstValues.length ? firstValues[i] : null;
const secondValue = i < secondValues.length ? secondValues[i] : null;

if (firstValue !== secondValue) {
// If one of the values is not in the hierarchy, it is sorted to the end of the list
const firstIndex = hierarchy.indexOf(firstValue);
const secondIndex = hierarchy.indexOf(secondValue);

if (firstIndex === -1) {
return 1;
}
if (secondIndex === -1) {
return -1;
}

if (direction === 'asc') {
return firstIndex - secondIndex;
} return secondIndex - firstIndex; // desc
}
}

return 0;
};

/**
* Creates a sort function that sorts items according to the index of their
* property value in a hierarchy array.
Expand Down Expand Up @@ -163,8 +193,10 @@ const getSortFunction = (rule) => {

if (type === 'hierarchy') { return hierarchyFunction(rule); }

if (type === 'categorical') { return categoricalFunction(rule); }

// eslint-disable-next-line no-console
console.warn('🤔 Sort rule missing required property \'type\', or type was not recognized. Sorting as a string, which may cause incorrect results. Supported types are: number, boolean, string, date, hierarchy.');
console.warn('🤔 Sort rule missing required property \'type\', or type was not recognized. Sorting as a string, which may cause incorrect results. Supported types are: number, boolean, string, date, hierarchy, categorical');
return stringFunction(rule);
};

Expand Down Expand Up @@ -193,6 +225,7 @@ const createSorter = (sortRules = []) => {
* - hierarchy
* - number
* - date
* - categorical
*
* Network Canvas Variables can be of type:
* - "boolean",
Expand All @@ -208,7 +241,6 @@ const createSorter = (sortRules = []) => {
export const mapNCType = (type) => {
switch (type) {
case 'text':
case 'categorical':
case 'layout':
return 'string';
case 'number':
Expand All @@ -219,6 +251,8 @@ export const mapNCType = (type) => {
return 'date';
case 'ordinal':
return 'hierarchy';
case 'categorical':
return 'categorical';
case 'scalar':
return 'number';
default:
Expand Down Expand Up @@ -269,6 +303,7 @@ export const processProtocolSortRule = (codebookVariables) => (sortRule) => {
type: mapNCType(type),
// Generate a hierarchy if the variable is ordinal based on the ordinal options
...type === 'ordinal' && { hierarchy: variableDefinition.options.map((option) => option.value) },
...type === 'categorical' && { hierarchy: variableDefinition.options.map((option) => option.value) },
};
};

Expand Down

0 comments on commit 8548a8c

Please sign in to comment.