Skip to content

Commit

Permalink
Handle duplicate course codes and alternative course selection ("CHOI…
Browse files Browse the repository at this point in the history
…X" lines) in Cheminot parsing (#42)

* add e2e tests for cheminot service

* exclude dist folder from coverage

* exclude unecessary files from code coverage

* move choix courses into their own object

* Refactor parsing code to avoid duplication of courses

* add one more test for duplication

* remove json

* add non-BAC courses

* reomve trailing commas from horsProgramme course code
  • Loading branch information
mhd-hi authored Oct 10, 2024
1 parent 7f0e9af commit 27ee8f5
Show file tree
Hide file tree
Showing 6 changed files with 8,879 additions and 38 deletions.
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,12 @@
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
"**/*.(t|j)s",
"!dist/**",
"!test/**",
"!coverage/**",
"!src/**/dto/*.ts",
"!./.eslintrc.js"
],
"coverageDirectory": "./coverage",
"testEnvironment": "node"
Expand Down
48 changes: 19 additions & 29 deletions src/common/api-helper/cheminot/Course.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@ export class Course {
public type: string,
public session: number,
public code: string,
public profile: string,
public concentration: string,
public category: string,
public level: string,
public mandatory: boolean,
public prerequisites: string[],
public alternatives?: string[],
public prerequisites: { profile: string; prerequisites: string[] }[] = [],
public alternatives?: string[], // For CHOIX courses
) {}

public static isCourseLine(line: string): boolean {
Expand All @@ -33,56 +32,47 @@ export class Course {

// Handle "CHOIX" type courses
if (parts[0] === 'CHOIX') {
const mainCourseCode = parts[3].trim().toUpperCase();
const alternatives = parts[10]
.split(' ')
.map((course) => course.trim().toUpperCase());

return new Course(
parts[0],
parseInt(parts[1], 10),
parts[3].trim().toUpperCase(),
parts[4].trim(),
mainCourseCode,
parts[5].trim(),
parts[6].trim(),
parts[7].trim(),
parts[8] === 'B',
Course.parsePrerequisites(parts[9]),
alternatives,
[], // CHOIX courses have no prerequisites
[mainCourseCode, ...alternatives],
);
}

// If the line has 12 parts, it's an internship, so shift the first part
if (parts.length === this.INTERNSHIP_LINE_PARTS_COUNT) {
parts.shift();
}

if (parts.length < this.COURSE_LINE_PARTS_COUNT - 1) {
return null; // Not enough parts to form a valid course
// Handle regular courses (PROFI, TRONC, etc.)
if (parts.length < this.COURSE_LINE_PARTS_COUNT) {
return null;
}

// Trim all the parts
parts.forEach((part, i) => {
parts[i] = part.trim();
});

const type = parts[0];
const session = parseInt(parts[1], 10);
const code = parts[3].toUpperCase();
// Validate the course code using the course code validation pipe
if (this.courseCodeValidationPipe.transform(code) === false) {
console.log('Invalid course code: ', code);
}
const profile = parts[4];
const concentration = parts[5];
const category = parts[6];
const level = parts[7];
const profile = parts[4].trim();
const concentration = parts[5].trim();
const category = parts[6].trim();
const level = parts[7].trim();
const mandatory = parts[8] === 'B';
const prerequisites = Course.parsePrerequisites(parts[9]);
const prereqList = Course.parsePrerequisites(parts[9]);

// Only add non-empty prerequisites for the profile
const prerequisites =
prereqList.length > 0 ? [{ profile, prerequisites: prereqList }] : [];

return new Course(
type,
session,
code,
profile,
concentration,
category,
level,
Expand Down
52 changes: 48 additions & 4 deletions src/common/api-helper/cheminot/Program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ export class Program {

constructor(
public code: string,
public courses: Course[],
public courses: Course[] = [],
public choix: Course[] = [],
) {}

public static isProgramLine(line: string): boolean {
Expand All @@ -21,19 +22,62 @@ export class Program {
}

const code = parts[1];
return new Program(code, []);
return new Program(code);
}

public getCourses(): Course[] {
return this.courses;
}

public getChoix(): Course[] {
return this.choix;
}

public getHorsProgramme(): string[] {
return this.horsProgramme;
}

public addCourse(course: Course) {
this.courses.push(course);
public addCourse(newCourse: Course) {
// Handle "CHOIX" courses separately
if (newCourse.type === 'CHOIX') {
this.choix.push(newCourse);
return;
}

// Handle regular courses (non-CHOIX)
const existingCourseIndex = this.courses.findIndex(
(course) => course.code === newCourse.code,
);

if (existingCourseIndex >= 0) {
// Merge with an existing course if found
this.mergeCourse(this.courses[existingCourseIndex], newCourse);
} else {
// Otherwise, add it as a new course
this.courses.push(newCourse);
}
}

private mergeCourse(existingCourse: Course, newCourse: Course) {
// Merge profiles and prerequisites for duplicate courses
newCourse.prerequisites.forEach((newProfilePrereqs) => {
const existingProfile = existingCourse.prerequisites.find(
(profilePrereqs) =>
profilePrereqs.profile === newProfilePrereqs.profile,
);

if (existingProfile) {
// Merge prerequisites for the same profile, avoiding duplicates
newProfilePrereqs.prerequisites.forEach((prereq) => {
if (!existingProfile.prerequisites.includes(prereq)) {
existingProfile.prerequisites.push(prereq);
}
});
} else if (newProfilePrereqs.prerequisites.length > 0) {
// Add a new profile with its prerequisites
existingCourse.prerequisites.push(newProfilePrereqs);
}
});
}

public addHorsProgrammeCourse(courseCode: string) {
Expand Down
32 changes: 28 additions & 4 deletions src/common/api-helper/cheminot/cheminot.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Injectable } from '@nestjs/common';

import { CourseCodeValidationPipe } from '../../pipes/models/course/course-code-validation-pipe';
import { Course } from './Course';
import { FileExtractionService } from './file-extraction.service';
import { Program } from './Program';

@Injectable()
export class CheminotService {
private readonly programs: Program[] = [];
private readonly courseCodeValidationPipe = new CourseCodeValidationPipe();

constructor(private readonly fileExtractionService: FileExtractionService) {}

Expand Down Expand Up @@ -34,9 +36,16 @@ export class CheminotService {
continue;
}

// Handle the start of a new program
if (Program.isProgramLine(line)) {
if (currentProgram) {
// Push the current program before starting a new one
this.programs.push(currentProgram);
}
currentProgram = this.handleProgramLine(line, currentProgram);
skipSection = false; // Reset skip section when a new program starts
} else if (this.isCourseList(line) && currentProgram) {
this.handleNonBacCourseList(line, currentProgram);
} else if (this.isSectionToSkip(line)) {
skipSection = true; // Start skipping section
} else if (line.startsWith('.HORS-PROGRAMME')) {
Expand All @@ -60,14 +69,29 @@ export class CheminotService {
);
}

private isCourseList(line: string): boolean {
const courseCodes = line.split(',').map((code) => code.trim());

return courseCodes.every((code) =>
this.courseCodeValidationPipe.transform(code),
);
}

// Example of a non-BAC course line: "ATE800, GES815, GES816, GES817"
private handleNonBacCourseList(line: string, currentProgram: Program) {
const courseCodes = line.split(',').map((code) => code.trim());

courseCodes.forEach((code) => {
const newCourse = new Course('', 1, code, '', '', '', true);
currentProgram.addCourse(newCourse);
});
}

private handleProgramLine(
line: string,
currentProgram: Program | null,
): Program | null {
const program = Program.parseProgramLine(line);
if (program && currentProgram) {
this.programs.push(currentProgram);
}
return program || currentProgram;
}

Expand Down Expand Up @@ -97,7 +121,7 @@ export class CheminotService {
continue;
}

currentProgram.addHorsProgrammeCourse(line);
currentProgram.addHorsProgrammeCourse(line.replace(/,$/, '').trim());
}
}

Expand Down
Loading

0 comments on commit 27ee8f5

Please sign in to comment.