CourseInsights / src / queryProcessor / QueryCollector.ts
QueryCollector.ts
Raw
import { InsightResult } from "../controller/IInsightFacade";
import {
	APPLYRULE,
	FILTER,
	Key,
	LOGICCOMPARISON,
	MCOMPARISON,
	Mfield,
	NEGATION,
	ORDER,
	SCOMPARISON,
	SORT,
	Sfield,
	TRANSFORMATIONS,
} from "./QueryTree";
import Decimal from "decimal.js";

export default class QueryCollector {
	constructor() {}

	public collectFILTER(filter: FILTER, section: InsightResult): boolean {
		const filterKey = Object.keys(filter)[0];
		switch (filterKey) {
			case "AND":
				return this.collectAND(filter as LOGICCOMPARISON, section);
			case "OR":
				return this.collectOR(filter as LOGICCOMPARISON, section);
			case "LT":
				return this.collectLT(filter as MCOMPARISON, section);
			case "GT":
				return this.collectGT(filter as MCOMPARISON, section);
			case "EQ":
				return this.collectEQ(filter as MCOMPARISON, section);
			case "IS":
				return this.collectIS(filter as SCOMPARISON, section);
			case "NOT":
				return this.collectNOT(filter as NEGATION, section);
			default:
				return false; //can be triggered by unit tests
		}
	}

	public collectAND(filter: LOGICCOMPARISON, section: InsightResult): boolean {
		const AND = filter.AND;
		return AND.every((filterAND) => this.collectFILTER(filterAND, section));
	}

	public collectOR(filter: LOGICCOMPARISON, section: InsightResult): boolean {
		const OR = filter.OR;
		return OR.some((filterOR) => this.collectFILTER(filterOR, section));
	}

	public collectLT(filter: MCOMPARISON, section: InsightResult): boolean {
		const LT = filter.LT;
		const values = this.getValues(LT, section);
		return values[0] < values[1];
	}

	public collectGT(filter: MCOMPARISON, section: InsightResult): boolean {
		const GT = filter.GT;
		const values = this.getValues(GT, section);
		return values[0] > values[1];
	}

	public collectEQ(filter: MCOMPARISON, section: InsightResult): boolean {
		const EQ = filter.EQ;
		const values = this.getValues(EQ, section);
		return values[0] === values[1];
	}

	public collectIS(filter: SCOMPARISON, section: InsightResult): boolean {
		const IS = filter.IS;
		const values = this.getValues(IS, section);
		return this.collectWildcards(values[0] as string, values[1] as string, section);
	}

	public collectNOT(filter: NEGATION, section: InsightResult): boolean {
		const NOT = filter.NOT;
		return !this.collectFILTER(NOT, section);
	}

	public collectWildcards(value: string, inputstring: string, section: InsightResult): boolean {
		if (inputstring === "") {
			value === "";
		}
		if (inputstring === "*" || inputstring === "**") {
			return true;
		}
		if (inputstring.startsWith("*")) {
			if (inputstring.endsWith("*")) {
				//*inputstring*, Contains inputstring
				return value.includes(inputstring.substring(1, inputstring.length - 1));
			} else {
				//*inputstring, Ends with inputstring
				return value.endsWith(inputstring.substring(1));
			}
		} else if (inputstring.endsWith("*")) {
			//inputstring*, Starts with inputstring
			return value.startsWith(inputstring.substring(0, inputstring.length - 1));
		} else {
			//inputstring, Matches inputstring exactly
			return value === inputstring;
		}
	}

	public getValues(filter: any, section: InsightResult): (string | number)[] {
		const mkey = Object.keys(filter)[0];
		const field = this.getField(mkey as Key);
		const fieldValue = section[field] as string | number;
		const filterValue = Object.values(filter)[0] as string | number;
		return [fieldValue, filterValue];
	}

	public getField(key: Key): Mfield | Sfield {
		const index = key.indexOf("_");
		return key.substring(index + 1) as Mfield | Sfield;
	}

	public collectCOLUMNS(columns: Key[], section: InsightResult): InsightResult {
		const result: InsightResult = {};
		columns.forEach((column) => {
			result[column] = section[this.getField(column)];
		});
		return result;
	}

	public collectORDER(order: ORDER, sections: InsightResult[]): InsightResult[] {
		if (typeof order === "object") {
			return sections.sort((first, second) => this.collectSORT(order, first, second));
		} else {
			return sections.sort((first, second) => (first[order] < second[order] ? -1 : 1));
		}
	}

	public collectSORT(order: SORT, first: InsightResult, second: InsightResult): number {
		for (const key of order.keys) {
			if (first[key] === second[key]) {
				continue;
			}
			return order.dir === "UP" ? (first[key] < second[key] ? -1 : 1) : first[key] < second[key] ? 1 : -1;
		}
		return 0;
	}

	public collectTRANSFORMATIONS(transformations: TRANSFORMATIONS, results: InsightResult[]): InsightResult[] {
		const groups = this.collectGROUP(transformations.GROUP, results);
		return this.collectAPPLY(groups, transformations.GROUP, transformations.APPLY);
	}

	public collectGROUP(group: Key[], results: InsightResult[]): InsightResult[][] {
		const groupMap: Record<string, InsightResult[]> = {};
		results.forEach((result) => {
			const groupsKey = this.createGroupsKey(group, result);
			if (!groupMap[groupsKey]) {
				groupMap[groupsKey] = [result];
			} else {
				groupMap[groupsKey].push(result);
			}
		});
		return Object.values(groupMap);
	}

	public createGroupsKey(group: Key[], result: InsightResult): string {
		let firstKey = String(result[this.getField(group[0])]);
		for (let i = 1; i < group.length; i++) {
			firstKey += "_" + String(result[this.getField(group[i])]);
		}
		return firstKey;
	}

	public collectAPPLY(groups: InsightResult[][], group: Key[], apply: APPLYRULE[]): InsightResult[] {
		return groups.map((results: InsightResult[]) => {
			const newResult: InsightResult = this.collectAPPLYRULE(results, apply);
			const result = results[0];
			group.forEach((fieldKey) => {
				const field = this.getField(fieldKey);
				newResult[field] = result[field];
			});
			return newResult;
		});
	}

	public collectAPPLYRULE(group: InsightResult[], apply: APPLYRULE[]): InsightResult {
		const newResult: InsightResult = {};
		apply.forEach((applyrule) => {
			const applykey = Object.keys(applyrule)[0];
			const applyobject = Object.values(applyrule)[0];
			newResult[applykey] = this.collectAPPLYTOKEN(group, applyobject);
		});
		return newResult;
	}

	public collectAPPLYTOKEN(group: InsightResult[], applyobject: any): string | number {
		const applytoken = Object.keys(applyobject)[0];
		const applyvalue = Object.values(applyobject)[0];
		switch (applytoken) {
			case "MAX":
				return this.collectMAX(group, applyvalue as Key);
			case "MIN":
				return this.collectMIN(group, applyvalue as Key);
			case "AVG":
				return this.collectAVG(group, applyvalue as Key);
			case "COUNT":
				return this.collectCOUNT(group, applyvalue as Key);
			case "SUM":
				return this.collectSUM(group, applyvalue as Key);
			default:
				return 0;
		}
	}

	public collectMAX(group: InsightResult[], applyvalue: Key): number {
		const field = this.getField(applyvalue);
		let max = group[0][field] as number;
		group.forEach((result) => {
			const value = result[field] as number;
			max = max > value ? max : value;
			//max = Math.max(max, value);
		});
		return max;
	}

	public collectMIN(group: InsightResult[], applyvalue: Key): number {
		const field = this.getField(applyvalue);
		let min = group[0][field] as number;
		group.forEach((result) => {
			const value = result[field] as number;
			min = Math.min(min, value);
		});
		return min;
	}

	public collectAVG(group: InsightResult[], applyvalue: Key): number {
		const field = this.getField(applyvalue);
		let total = new Decimal(0);
		group.forEach((result) => {
			const value = new Decimal(result[field] as number);
			total = total.add(value);
		});
		const avg = total.toNumber() / group.length;
		return Number(avg.toFixed(2));
	}

	public collectCOUNT(group: InsightResult[], applyvalue: Key): number {
		const field = this.getField(applyvalue);
		const set = new Set();
		group.forEach((result) => {
			const value = result[field];
			set.add(value);
		});
		return set.size;
	}

	public collectSUM(group: InsightResult[], applyvalue: Key): number {
		const field = this.getField(applyvalue);
		let sum = 0;
		group.forEach((result) => {
			const value = result[field] as number;
			sum += value;
		});
		return Number(sum.toFixed(2));
	}
}