Skip to content

Instantly share code, notes, and snippets.

@geovanisouza92
Created June 5, 2021 14:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save geovanisouza92/47331bc73cb4df826d2823491747f707 to your computer and use it in GitHub Desktop.
Save geovanisouza92/47331bc73cb4df826d2823491747f707 to your computer and use it in GitHub Desktop.
/* eslint-disable @typescript-eslint/no-non-null-assertion */
type Job = {
id: number;
code: string;
name: string;
jobType: string;
status: string;
publicationType: string;
recruiterId: number;
recruiterName: string;
recruiterEmail: string;
managerId: number;
managerName: string;
managerEmail: string;
departmentId: number;
departmentName: string;
roleId: number;
roleName: string;
companyBranchId: number;
branchPath: string;
branchLabel: string;
};
type Option = {
label: string;
value: string | null;
}
type FilterName = string;
export type FilterValue = string | number;
export type FilterValues = Record<FilterName, Array<string | number>>;
export type FilterOptions = Record<FilterName, Option[]>;
type FilterFn = (job: Job) => boolean;
interface FilterHandler {
filterFnFactory: (filterValues: FilterValue[]) => FilterFn;
asOption: (job: Job) => Option | Option[];
comparator?: (a: Option, b: Option) => number;
}
type FilterOptionsMap = Record<FilterName, Map<string | number, Option | Option[]>>;
type FilterLabel = Option & {
filterName: FilterName;
parentValue: FilterValue;
};
type FilterLabelsMap = Record<FilterName, Map<FilterValue, FilterLabel>>;
function createSet(filterValues: unknown[]): Set<string> {
return new Set(filterValues.map((value) => `${value}`));
}
function createIntegerSet(filterValues: Array<string | number>): Set<number> {
return new Set(filterValues.map((filterValue) => (typeof filterValue === 'number'
? filterValue
: parseInt(filterValue, 10))));
}
function asMap(list: Option[]): Map<string, Option> {
return Array.isArray(list)
? new Map(list.map((it) => [it.value, it]))
: new Map();
}
// The Design Team asked for a fixed order for better UX
const jobStatusOrder = [
'published',
'closed',
'frozen',
'canceled',
];
type FilterContextCtor = {
userId: number;
ownRecruiterLabel: string;
ownManagerLabel: string;
filterContext: {
jobList: Job[];
branches: Option[];
jobTypesList: Option[];
jobStatusList: Option[];
jobPublicationTypesList: Option[];
};
};
/**
* This class holds all the machinery for producing "filter options" and "filter labels" based
* on "filter values" on a consistent and _fast_ fashion.
*/
export class FilterContext {
private userId: number;
private ownRecruiterLabel: string;
private ownManagerLabel: string;
private jobList: Job[];
// private branchesMap: Map<string, Option>;
private jobTypesMap: Map<string, Option>;
private jobStatusMap: Map<string, Option>;
private jobPublicationTypesMap: Map<string, Option>;
private filterRegistry!: Record<FilterName, FilterHandler>;
private filterNames!: FilterName[];
private filterOptionsIndex!: FilterOptionsMap;
private filterLabelsIndex!: FilterLabelsMap;
constructor({
userId,
ownRecruiterLabel,
ownManagerLabel,
filterContext,
}: FilterContextCtor) {
this.userId = userId;
this.ownRecruiterLabel = ownRecruiterLabel;
this.ownManagerLabel = ownManagerLabel;
this.jobList = filterContext.jobList;
// this.branchesMap = asMap(filterContext.branches);
this.jobTypesMap = asMap(filterContext.jobTypesList);
this.jobStatusMap = asMap(filterContext.jobStatusList);
this.jobPublicationTypesMap = asMap(filterContext.jobPublicationTypesList);
this.initFilterRegistry();
this.initFilterOptionsIndex();
this.initFilterLabelsIndex();
}
private initFilterRegistry(): void {
this.filterRegistry = {
jobs: {
filterFnFactory: (filterValues): FilterFn => {
const values = createIntegerSet(filterValues);
return (job) => values.has(job.id);
},
asOption: (job) => ({
value: job.id ? `${job.id}` : null,
label: [job.code, job.name].filter(Boolean).join(' - '),
}),
},
jobTypes: {
filterFnFactory: (filterValues): FilterFn => {
const values = createSet(filterValues);
return (job) => values.has(job.jobType);
},
asOption: (job) => this.jobTypesMap.get(job.jobType)!,
},
jobStatus: {
filterFnFactory: (filterValues): FilterFn => {
const values = createSet(filterValues);
return (job) => values.has(job.status);
},
asOption: (job) => this.jobStatusMap.get(job.status)!,
comparator: (a, b) => jobStatusOrder.indexOf(a.value!) - jobStatusOrder.indexOf(b.value!),
},
jobPublicationTypes: {
filterFnFactory: (filterValues): FilterFn => {
const values = createSet(filterValues);
return (job) => values.has(job.publicationType);
},
asOption: (job) => this.jobPublicationTypesMap.get(job.publicationType)!,
},
recruiters: {
filterFnFactory: (filterValues): FilterFn => {
const values = createIntegerSet(filterValues);
return (job) => values.has(job.recruiterId);
},
asOption: (job) => ({
value: job.recruiterId ? `${job.recruiterId}` : null,
label: job.recruiterId === this.userId
? this.ownRecruiterLabel
: [job.recruiterName, job.recruiterEmail].filter(Boolean).join(' - '),
}),
},
managers: {
filterFnFactory: (filterValues): FilterFn => {
const values = createIntegerSet(filterValues);
return (job) => values.has(job.managerId);
},
asOption: (job) => ({
value: job.managerId ? `${job.managerId}` : null,
label: job.managerId === this.userId
? this.ownManagerLabel
: [job.managerName, job.managerEmail].filter(Boolean).join(' - '),
}),
},
departments: {
filterFnFactory: (filterValues): FilterFn => {
const values = createIntegerSet(filterValues);
return (job) => values.has(job.departmentId);
},
asOption: (job) => ({
value: job.departmentId ? `${job.departmentId}` : null,
label: job.departmentName,
}),
},
roles: {
filterFnFactory: (filterValues): FilterFn => {
const values = createIntegerSet(filterValues);
return (job) => values.has(job.roleId);
},
asOption: (job) => ({
value: job.roleId ? `${job.roleId}` : null,
label: job.roleName,
}),
},
/*
subsidiaries: {
filterFnFactory: (filterValues): FilterFn => {
const values = createIntegerSet(filterValues);
return (job) => values.has(job.companyBranchId);
},
asOption: (job) => ({
value: job.companyBranchId ? `${job.companyBranchId}` : null,
label: job.branchLabel,
}),
},
branches: {
filterFnFactory: (filterValues): FilterFn => {
const values = [...createSet(filterValues)];
return (job) => (job.branchPath
? values.some((value) => job.branchPath.startsWith(value))
: false);
},
asOption: (job) => {
const branch = this.branchesMap.get(job.branchPath);
if (!branch) return [];
const parents: Option[] = (branch as any).parents
.map((parent) => this.branchesMap.get(parent.value));
return parents.concat(branch);
},
},
*/
};
this.filterNames = Object.keys(this.filterRegistry);
}
private initFilterOptionsIndex(): void {
this.filterOptionsIndex = {};
this.filterNames.forEach((filterName) => {
const { [filterName]: handler } = this.filterRegistry;
const byJob = new Map(this.jobList.map((job) => {
const option = handler.asOption(job);
return [job.id, option];
}));
this.filterOptionsIndex[filterName] = byJob;
});
}
private initFilterLabelsIndex(): void {
this.filterLabelsIndex = {};
this.filterNames.forEach((filterName) => {
this.filterLabelsIndex[filterName] = new Map();
});
}
/**
* Return a list of jobs that matches all filter values. Internally it builds a matching
* functions that uses a list of per-filter matching functions that compare specific job
* properties with corresponding values for each filter.
*
* Complexity: O(n) where n is the number of jobs.
*/
private selectJobsBy(filterValues: FilterValues): Job[] {
const filterFns = this.filterNames
.filter((filterName) => filterName in filterValues)
.map((filterName) => {
const { [filterName]: handler } = this.filterRegistry;
const { [filterName]: values } = filterValues;
return handler.filterFnFactory(values);
});
const doesMatch = (job): boolean => filterFns.every((doesFilterMatch) => doesFilterMatch(job));
return this.jobList.filter(doesMatch);
}
/**
* Find available options considering filterValues as criteria. For each filter value provided,
* it applies all other filter values except the filter being calculated and then uses the
* filter name and job ID as keys to find the option on the pre-calculated mapping.
*
* Complexity: O(n log n) in best scenario, O(n²) in worst scenario
*/
getFilterOptionsBy(filterValues: FilterValues): FilterOptions {
const filterOptions: FilterOptions = {};
return this.filterNames
.reduce((prevOptions, filterName) => {
const { [filterName]: _, ...filterValuesEx } = filterValues;
const { [filterName]: byJob } = this.filterOptionsIndex;
const { [filterName]: handler } = this.filterRegistry;
const unique = new Map();
this.selectJobsBy(filterValuesEx).forEach((job) => {
const options: Option[] = ([] as Option[]).concat(byJob.get(job.id)!);
options.forEach((option) => {
if (option && option.value && !unique.has(option.value)) {
unique.set(option.value, option);
}
});
});
const options = handler.comparator
? [...unique.values()].sort(handler.comparator)
: [...unique.values()];
return {
...prevOptions,
[filterName]: options,
};
}, filterOptions);
}
/**
* Convert applied filter values to human-readable labels. For each applied filter we get
* (or generate) the corresponding label (same as option).
*
* Complexity: O(n log n) in best scenario, O(n²) in worst-scenario
*/
getFilterLabelsBy(filterValues: FilterValues): FilterLabel[] {
return this.filterNames
.filter((filterName) => filterName in filterValues)
.flatMap((filterName) => {
const { [filterName]: byValue } = this.filterLabelsIndex;
const { [filterName]: values } = filterValues;
return values.map((value) => {
if (!byValue.has(value)) {
const { [filterName]: byJob } = this.filterOptionsIndex;
const option: Option = this.selectJobsBy({ [filterName]: [value] })
.flatMap((job): Option[] => ([] as Option[]).concat(byJob.get(job.id)!))
.filter(Boolean)
.find((it) => it.value === `${value}`)!;
byValue.set(value, {
...option,
filterName,
parentValue: value,
});
}
return byValue.get(value)!;
});
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment