import { IParserResult } from "../IParserResult"; import { IParserLine } from "../IParserLine"; import { ExamResultSummary } from "./ExamResultSummary"; import { mySqlConnection } from "@rolesvillesoftware/tiberhealthmodel"; import { ExamQuestionFact, QuestionFact } from "@rolesvillesoftware/tiberhealthmodel/dto"; import * as dto from "@rolesvillesoftware/tiberhealthmodel/dto"; import { StudentEmail } from "@rolesvillesoftware/tiberhealthmodel/dto/StudentEmail"; export interface IMetaDataColumns { col: number; type: string; headerText: string; headerSubText: string; detailsCol: number; columnDimObj: dto.Question | dto.Category; } export class ExamResultSummaryParser implements IParserResult { public columns: IMetaDataColumns[] = new Array(0); private parsedLines: IParserLine[]; private examResultSummary: ExamResultSummary; get result(): ExamResultSummary { return this.examResultSummary; } get dataLines(): IParserLine[] { return this .parsedLines .slice(2) .filter(line => line != null && line.elementCount > 0 && line.items.some(cell => cell != null && cell.length > 0)); } constructor(private dbConnection: mySqlConnection, private exam: dto.Exam, private school: dto.School) { } private verifyArrays(): { result: boolean; messages: string[] } { const messages = new Array(0); var index = 0; this.parsedLines.forEach(line => { if (index > 0) { if (line.elementCount !== this.parsedLines[index - 1].elementCount) { messages.push(`Line ${index} contains ${line.elementCount} elements - expecting to contain ${this.parsedLines[index - 1].elementCount} elements`); } index++; } }); return { result: messages.length === 0, messages: messages }; } /** * Parse out the meta data from the export */ private parseMeta() { if (this.parsedLines == null || this.parsedLines.length < 2) { return; } const isMatch = (val: string, regEx: any): boolean => { if (val == null || typeof val != "string") { return false; } let result = val.trim().match(regEx); if (result != null) { return result.length > 0; } return false; } this.columns = new Array(0); const columnCount = this.parsedLines[0].elementCount; let headerLine = this.parsedLines[0].toArray() as string[]; let headerSubLine = this.parsedLines[1].toArray() as string[]; let col = 0; let row = 0; while (col < columnCount) { let newColumn = { col: col, type: 'none', headerText: headerLine[col], headerSubText: headerSubLine[col], } as IMetaDataColumns; switch (true) { case isMatch(newColumn.headerText, /^StudentID$/): newColumn.type = "studentId"; break; case isMatch(newColumn.headerText, /^Email$/): newColumn.type = "studentEmail"; break; case isMatch(newColumn.headerText, /^Course$/): newColumn.type = "course"; break; case isMatch(newColumn.headerText, /^Q[0-9]*\s?$/): newColumn.type = "question"; newColumn.detailsCol = (col = col + 1); break; case isMatch(newColumn.headerText, /^CATEGORIES\//): newColumn.type = "category"; if ((col + 1) < columnCount && isMatch(headerLine[col + 1], /% Pts Earned$/)) { col = col + 1; newColumn.detailsCol = col; } break; case isMatch(newColumn.headerText, /^Assessment Name$/): newColumn.type = "assessment"; if ((col + 1) < columnCount && isMatch(headerLine[col + 1], /^Assessment ID$/)) { col = col + 1; newColumn.detailsCol = col; } else if ((col - 1) < columnCount && isMatch(headerLine[col - 1], /^Assessment ID$/)) { newColumn.detailsCol = (col - 1); } break; case isMatch(newColumn.headerText, /^Assessment ID$/): newColumn.type = "assessmentId"; newColumn.detailsCol = col; break; default: newColumn.type = "factField"; break; } this.columns.push(newColumn); col = col + 1; } } private parseExam(): Promise { return new Promise( (response, reject) => { const examSummary = new ExamResultSummary(); examSummary.exam = this.exam; examSummary.students = this.parseStudents(); examSummary.studentEmails = this.parseStudentEmail(); examSummary.questions = this.parseQuestions(); examSummary.categories = this.parseCategories(); examSummary.courses = this.parseCourses(); examSummary.assessments = this.parseAssessments(); this.parseFactTables(examSummary); const validationResults = examSummary.validateSave(); if (validationResults == null || validationResults.length > 0) { reject(`Unable to process file: ${validationResults.join("; ")}`); } else { this.exam.importMessage = "Importing"; examSummary.saveToDb(this.dbConnection) .then(data => { response(examSummary); }) .catch(error => { reject(error); }); } }); } private parseStudents(): dto.Student[] { const col = this.columns.find(item => item.type === "studentId"); if (col == null) { return []; } const studentArray = new Array(0); this.dataLines.forEach(line => { if (line.items[col.col] != null) { if (studentArray.every(s => s.studentId.toLowerCase() !== line.items[col.col].trim().toLowerCase())) { studentArray.push(new dto.Student(line.items[col.col].trim())); } } }); return studentArray; } private parseStudentEmail(): dto.StudentEmail[] { const emailCol = this.columns.find(item => item.type === "studentEmail"); const studentIdCol = this.columns.find(item => item.type === "studentId"); if (emailCol == null || studentIdCol == null) { return []; } const emailArray = new Array(0); this.dataLines.forEach(line => { const email = line.items[emailCol.col].trim().toLowerCase(); const student = line.items[studentIdCol.col]; if (email != null && student != null) { if (emailArray.every(s => s.studentEmail.toLowerCase() !== email)) { emailArray.push(Object.assign(new StudentEmail(student), { studentEmail: email })); } } }); return emailArray; } private parseQuestions(): dto.Question[] { const cols = this.columns.filter(item => item.type === "question"); if (cols == null) { return []; } return cols.map(col => { var _dto = new dto.Question(col.headerText, col.headerSubText); col.columnDimObj = _dto; return _dto; }); } private parseCategories(): dto.Category[] { const cols = this.columns.filter(item => item.type === "category"); if (cols == null) { return []; } const categoryArray = new Array(0); cols.forEach(col => { let category = dto.Category.parseCategory(null, col.headerText.split("/"), categoryArray); col.columnDimObj = category; }); return categoryArray; } private parseCourses(): dto.Course[] { const cols = this.columns.find(item => item.type === "course"); if (cols == null) { return []; } const courseArray = new Array(0); this.dataLines.forEach(line => { if (line.items[cols.col] != null) { if (courseArray.every(c => c.courseName.toLowerCase() !== line.items[cols.col].trim().toLowerCase())) { courseArray.push(new dto.Course(line.items[cols.col].trim())); } } }); return courseArray; } private parseAssessments(): dto.Assessment[] { let col = this.columns.find(item => item.type === "assessment") || this.columns.find(item => item.type === "assessmentId") if (col == null) { return []; } const assessmentArray = new Array(0); this.dataLines.forEach(line => { if (line.items[col.col] != null) { if (assessmentArray.every(a => a.assessmentName.toLowerCase() != line.items[col.col].trim().toLowerCase())) { const assessment = new dto.Assessment(line.items[col.col].trim()); assessment.assessmentId = parseInt(line.items[col.detailsCol]); if (!isNaN(assessment.assessmentId)) { assessmentArray.push(assessment); } } } }); return assessmentArray; } private parseFactTables(examSummary: ExamResultSummary) { examSummary.categoryFact = new Array(0); examSummary.questionFact = new Array(0); examSummary.examFact = new Array(0); examSummary.examQuestionFact = new Array(0); let questionColumns = this.columns.filter(item => item.type === "question"); let categoryColumns = this.columns.filter(item => item.type === "category"); examSummary.examQuestionFact = questionColumns .map(question => new ExamQuestionFact(this.school, this.exam.academicYear, this.exam, question.columnDimObj as dto.Question)); this.dataLines.forEach(data => { let student = examSummary.students.find(item => item.studentId === this.findString("StudentID", data)); examSummary.examFact.push(this.parseExamFact(student, data)); /*** Parse Question Related items */ examSummary.questionFact = examSummary.questionFact.concat(questionColumns .map(question => this.parseQuestionFact(student, question.columnDimObj as dto.Question, question, data))); /** Category Facts */ examSummary.categoryFact = examSummary.categoryFact.concat(categoryColumns .map(category => this.parseCategoryFact(student, category, data))); }); } private parseCategoryFact(student: dto.Student, category: IMetaDataColumns, dataLine: IParserLine): dto.CategoryFact { let factRecord = new dto.CategoryFact(this.school, this.exam.academicYear, this.exam, student, category.columnDimObj as dto.Category); factRecord.correctCount = this.findNumberIndex(category.col, dataLine); factRecord.percentagePoints = 0; if (category.detailsCol != null) { factRecord.percentagePoints = this.findNumberIndex(category.detailsCol, dataLine); } return factRecord; } private parseExamFact(student: dto.Student, dataLine: IParserLine): dto.ExamFact { let factRecord = new dto.ExamFact(this.school, this.exam.academicYear, this.exam, student); factRecord.percentCorrect = this.findNumber("%", dataLine); factRecord.examRank = this.findNumber("Rank", dataLine); factRecord.examPoints = this.findNumber("Pts", dataLine); factRecord.assessmentId = this.findNumber("Assessment ID", dataLine); factRecord.assessmentName = this.findString("Assessment Name", dataLine); return factRecord; } private parseQuestionFact(student: dto.Student, question: dto.Question, column: IMetaDataColumns, dataLine: IParserLine): dto.QuestionFact { let factRecord = new dto.QuestionFact(this.school, this.exam.academicYear, this.exam, student, question); let answer = this.findStringPos(column, dataLine).trim(); factRecord.answer = answer.toLowerCase(); factRecord.isCorrect = answer != answer.toLowerCase(); factRecord.questionPoints = this.findNumberIndex(column.detailsCol, dataLine); if (isNaN(factRecord.questionPoints)) { factRecord.questionPoints = 0; } return factRecord; } private findColType(colType: string): IMetaDataColumns[] { return this.columns.filter(col => col.type === colType); } private findCol(columnName: string) { return this.columns.find(col => col.headerText.trim().toLowerCase() === columnName.trim().toLowerCase()); } private findStringIndex(index: number, dataLine: IParserLine): string | null { if (index == null || index > (dataLine.elementCount - 1)) { return null; } return dataLine.items[index]; } private findStringPos(col: IMetaDataColumns, dataLine: IParserLine): string | null { if (col == null) { return null; } return this.findStringIndex(col.col, dataLine); } private parseNumber(stringVal: string | null): number | null { if (stringVal == null) { return null; } const numberVal = parseFloat(stringVal) if (isNaN(numberVal)) { return null; } return numberVal; } private findString(columnName: string, dataLine: IParserLine): string | null { const col = this.findCol(columnName); return this.findStringPos(col, dataLine); } private findNumberIndex(index: number, dataLine: IParserLine): number | null { return this.parseNumber(this.findStringIndex(index, dataLine)); } private findNumberPos(col: IMetaDataColumns, dataLine: IParserLine): number | null { return this.parseNumber(this.findStringIndex(col.col, dataLine)); } private findNumber(columnName: string, dataLine: IParserLine): number | null { return this.parseNumber(this.findString(columnName, dataLine)); } /** * Initialize the class for the beginning of the import */ public initializeImport = (): IParserResult => { this.parsedLines = new Array(0); return this; } /** * * @param line Result line from the delimited parser */ public parse = (line: IParserLine): void => { this.parsedLines.push(line); } /** * Instructions for what to do after all the lines have been imported */ public onImportComplete = (): Promise | null> => { return new Promise | null>( (response, reject) => { if (this.parsedLines == null || this.parsedLines.length === 0) { response(null); return; } const validationResults = this.verifyArrays(); if (validationResults.result) { this.parseMeta(); this.parseExam() .then(data => { this.examResultSummary = data; response(this); }) .catch(error => { reject(error); }) } else { reject(validationResults.messages.join("\r\n")); }; }); } public onImportFailed(error: any): Promise { return new Promise ( (response, reject) => { if (this.exam != null) { this.exam.effStatus = "E"; if (typeof error === "string") { this.exam.importMessage = error; } else { this.exam.importMessage = "General unexpected import error" } this.exam.updateOrCreate(this.dbConnection) .then(_ => { response(error); }) .catch(_ => { response(error); }); } } ) } public dispose() { } }