Files
typescript-katas/1kyu/functional-sql/solution.ts
2025-02-15 00:52:14 +01:00

233 lines
6.3 KiB
TypeScript

type GroupedData = Map<string, any[] | GroupedData>;
class Query {
private data: any[];
private selector: (obj: any) => any = (obj) => obj;
private filterGroups: ((obj: any) => boolean)[][] = [];
private groupBySelectors: ((obj: any) => any)[] = [];
private groupFilterGroups: ((obj: any) => boolean)[][] = [];
private orderFunction: (obj1: any, obj2: any) => number = (obj1, obj2) => 0;
constructor() {
this.data = [];
}
// If you repeat a SQL clause (except where() or having()), an exception will be thrown
private _selected = false;
private _fromed = false;
private _grouped = false;
private _ordered = false;
select(newSelector?: (obj: any) => any) {
if (this._selected) {
throw new Error('Duplicate SELECT');
}
if (newSelector) {
this.selector = newSelector;
};
this._selected = true;
return this;
}
from(...dataRows: any[][]) {
if (this._fromed) {
throw new Error('Duplicate FROM');
}
this._fromed = true;
// cartesian product of all data arrays
dataRows.forEach((dataRow) => {
if (this.data.length === 0) {
this.data = dataRow;
return;
}
const newData: any[] = [];
this.data.forEach((obj) => {
dataRow.forEach((newObj) => {
newData.push([obj, newObj]);
});
})
this.data = newData;
})
return this;
}
where(...newFilters: ((obj: any) => boolean)[]) {
this.filterGroups.push(newFilters);
return this;
}
groupBy(...newGroupBySelectors: ((obj: any) => any)[]) {
if (this._grouped) {
throw new Error('Duplicate GROUPBY');
}
this._grouped = true;
this.groupBySelectors = newGroupBySelectors;
return this;
}
having(...newGroupFilters: ((obj: any) => boolean)[]) {
this.groupFilterGroups.push(newGroupFilters);
return this;
}
orderBy(newOrderFunction: (obj1: any, obj2: any) => number) {
if (this._ordered) {
throw new Error('Duplicate ORDERBY');
}
this._ordered = true;
this.orderFunction = newOrderFunction;
return this;
}
execute() {
// filter
this.data = this.executeFilter();
// group by
const groupedMap = this.executeGroupBy();
// filter groups
this.executeHaving(groupedMap);
const selected = this.executeSelect(groupedMap);
const result = this.executeOrderBy(selected);
return result;
}
private executeFilter() {
// this.filterGroups is a list of groups of filters
// filters in the same group are ORed
// groups are ANDed
let filteredData = this.data;
this.filterGroups.forEach((filterGroup) => {
filteredData = filteredData.filter((obj) => {
return filterGroup.some((filter) => filter(obj));
});
})
return filteredData;
}
private groupOnce(data: any[], groupBySelector: (obj: any) => any) {
const groupedData = new Map<string, any[] | GroupedData>();
data.forEach((obj) => {
const key = groupBySelector(obj);
if (!groupedData.has(key)) {
groupedData.set(key, []);
}
(groupedData.get(key) as any[]).push(obj);
});
return groupedData;
}
private groupRecursively(data: any[], groupBySelectors: ((obj: any) => any)[]) {
const [groupBySelector, ...restGroupBySelectors] = groupBySelectors;
const groupedData = this.groupOnce(data, groupBySelector);
if (restGroupBySelectors.length === 0) {
return groupedData;
}
Array.from(groupedData.keys()).forEach((key) => {
const group = groupedData.get(key) as any[];
groupedData.set(key, this.groupRecursively(group, restGroupBySelectors) as Map<string, any[]>);
});
return groupedData;
}
private executeGroupBy(filteredData: any[] = this.data) {
if (this.groupBySelectors.length === 0) {
return new Map<string, any[]>([['', filteredData]]);
}
const groupedData = this.groupRecursively(filteredData, this.groupBySelectors);
return groupedData;
}
private executeHaving(groupedData: GroupedData) {
if (this.groupFilterGroups.length === 0) return groupedData;
// filter groups
// groupFilterGroups is a list of groups of filters
// filters in the same group are ORed
// groups are ANDed
// first flatten the map to arrays
let flattened: { flatGroup: any[], key: string }[] = [];
Array.from(groupedData.keys()).forEach((key) => {
const group = groupedData.get(key) as GroupedData | any[];
const flattenedGroup = Array.isArray(group) ?
group
: this.flattenMapToArrays(group as GroupedData, this.selector);
flattened.push({ flatGroup: [key, flattenedGroup], key });
});
this.groupFilterGroups.forEach((groupFilters) => {
flattened = flattened.filter((group) => {
const toKeep = groupFilters.some((filter) => filter(group.flatGroup));
if (!toKeep) groupedData.delete(group.key);
return toKeep;
})
});
return groupedData;
}
private flattenMapToArrays(groupedData: GroupedData, selector?: (obj: any) => any) {
// flatten maps to arrays
const result: any[] = [];
Array.from(groupedData.keys()).forEach((key) => {
const group = groupedData.get(key) as GroupedData | any[];
const flattened = Array.isArray(group) ?
selector ? (group as any[]).map(selector)
: group as any[]
: this.flattenMapToArrays(group as GroupedData, selector)
result.push([key, flattened]);
});
return result;
}
private executeSelect(groupedData: GroupedData) {
const keys = Array.from(groupedData.keys());
if (keys.length === 1 && keys[0] === '') {
return (groupedData.get('') as any[])?.map(this.selector);
}
return Array.from(keys).map((key) => {
const group = groupedData.get(key) as GroupedData | any[];
if (this.groupBySelectors.length === 0) {
// no group by was done,
// selector selects from objects
return [key, (group as any[]).map(this.selector)];
}
const flattenedGroup = Array.isArray(group) ?
group
: this.flattenMapToArrays(group as GroupedData, this.selector);
// selector selects from groups
return this.selector([key, flattenedGroup]);
});
}
private executeOrderBy(selected: any[]) {
return selected.sort(this.orderFunction);
}
}
export function query() {
const q = new Query();
return q;
};