Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 100
RelativeGradingService
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 7
380.00
0.00% covered (danger)
0.00%
0 / 100
 calculateRelativeGrading
0.00% covered (danger)
0.00%
0 / 1
12.00
0.00% covered (danger)
0.00%
0 / 25
 removeZeroMarksStudents
0.00% covered (danger)
0.00%
0 / 1
2.00
0.00% covered (danger)
0.00%
0 / 4
 assignFailGrade
0.00% covered (danger)
0.00%
0 / 1
12.00
0.00% covered (danger)
0.00%
0 / 14
 calculateAndAssignGrades
0.00% covered (danger)
0.00%
0 / 1
20.00
0.00% covered (danger)
0.00%
0 / 30
 getStudentsInThisRange
0.00% covered (danger)
0.00%
0 / 1
12.00
0.00% covered (danger)
0.00%
0 / 14
 getGradePoint
0.00% covered (danger)
0.00%
0 / 1
2.00
0.00% covered (danger)
0.00%
0 / 3
 getGrade
0.00% covered (danger)
0.00%
0 / 1
20.00
0.00% covered (danger)
0.00%
0 / 10
<?php
namespace com\linways\core\ams\professional\service;
use com\linways\base\util\MakeSingletonTrait;
use com\linways\core\ams\professional\service\BaseService;
use com\linways\core\ams\professional\dto\GradeSchemaBasedOnType;
use com\linways\core\ams\professional\service\CurriculumManageService;
use com\linways\core\ams\professional\request\CalculateRelativeGradingRequest;
class RelativeGradingService extends BaseService
{
    use MakeSingletonTrait;
    /**
     * Calculates the relative grading for a given request.
     *
     * @param CalculateRelativeGradingRequest $request The request object containing the necessary data for relative grading calculation.
     * @return Array $students. The array of students with their respective grades.
     */
    public function calculateRelativeGrading(CalculateRelativeGradingRequest $request)
    {
        $request = $this->realEscapeObject($request);
        try{
            //Step 1: Find the grade scheme
            $params = new \stdClass;
            $params->type = "ACADEMIC_PAPER_SUBJECT";
            $params->ids = [$request->paperSubjectId];
            $getGradeSchemeRequest = new GradeSchemaBasedOnType($params);
            $getGradeSchemeRequest->ids = $params->ids;
            $gradingDetails = CurriculumManageService::getInstance()->getPossibleSchemaDetails($getGradeSchemeRequest)[0];
            //Step2: Find the grade scheme rule
            $rule = json_decode(CommonService::getInstance()->getSettings("ACADEMICS","RELATIVE_GRADING_RULE"));
            //Step3: Sort the students based on marks
            $students = $request->students;
            // $students = [
            //     (object) [ "studentId" => "1543", "marks" => 86 ],
            //     (object) [ "studentId" => "2987", "marks" => 83 ],
            //     (object) [ "studentId" => "7612", "marks" => 80 ],
            //     (object) [ "studentId" => "4390", "marks" => 80 ],
            //     (object) [ "studentId" => "5921", "marks" => 79 ],
            //     (object) [ "studentId" => "8345", "marks" => 78 ],
            //     (object) [ "studentId" => "1276", "marks" => 77 ],
            //     (object) [ "studentId" => "3908", "marks" => 76 ],
            //     (object) [ "studentId" => "2093", "marks" => 76 ],
            //     (object) [ "studentId" => "6581", "marks" => 75 ],
            //     (object) [ "studentId" => "9210", "marks" => 75 ],
            //     (object) [ "studentId" => "3429", "marks" => 74 ],
            //     (object) [ "studentId" => "8753", "marks" => 73 ],
            //     (object) [ "studentId" => "1028", "marks" => 72 ],
            //     (object) [ "studentId" => "4593", "marks" => 72 ],
            //     (object) [ "studentId" => "7485", "marks" => 72 ],
            //     (object) [ "studentId" => "2810", "marks" => 71 ],
            //     (object) [ "studentId" => "6234", "marks" => 71 ],
            //     (object) [ "studentId" => "9126", "marks" => 71 ],
            //     (object) [ "studentId" => "1057", "marks" => 68 ],
            //     (object) [ "studentId" => "3846", "marks" => 68 ],
            //     (object) [ "studentId" => "6731", "marks" => 67 ],
            //     (object) [ "studentId" => "8239", "marks" => 66 ],
            //     (object) [ "studentId" => "2903", "marks" => 66 ],
            //     (object) [ "studentId" => "5814", "marks" => 66 ],
            //     (object) [ "studentId" => "9342", "marks" => 65 ],
            //     (object) [ "studentId" => "1739", "marks" => 64 ],
            //     (object) [ "studentId" => "4287", "marks" => 64 ],
            //     (object) [ "studentId" => "7594", "marks" => 63 ],
            //     (object) [ "studentId" => "2183", "marks" => 63 ],
            //     (object) [ "studentId" => "6092", "marks" => 63 ],
            //     (object) [ "studentId" => "8921", "marks" => 63 ],
            //     (object) [ "studentId" => "1765", "marks" => 63 ],
            //     (object) [ "studentId" => "4728", "marks" => 62 ],
            //     (object) [ "studentId" => "7392", "marks" => 60 ],
            //     (object) [ "studentId" => "2916", "marks" => 60 ],
            //     (object) [ "studentId" => "5807", "marks" => 60 ],
            //     (object) [ "studentId" => "8240", "marks" => 59 ],
            //     (object) [ "studentId" => "2031", "marks" => 59 ],
            //     (object) [ "studentId" => "6739", "marks" => 59 ],
            //     (object) [ "studentId" => "8912", "marks" => 59 ],
            //     (object) [ "studentId" => "1358", "marks" => 58 ],
            //     (object) [ "studentId" => "4295", "marks" => 58 ],
            //     (object) [ "studentId" => "7640", "marks" => 57 ],
            //     (object) [ "studentId" => "2871", "marks" => 57 ],
            //     (object) [ "studentId" => "6132", "marks" => 56 ],
            //     (object) [ "studentId" => "9583", "marks" => 55 ],
            //     (object) [ "studentId" => "1467", "marks" => 55 ],
            //     (object) [ "studentId" => "4298", "marks" => 52 ],
            //     (object) [ "studentId" => "7032", "marks" => 52 ],
            //     (object) [ "studentId" => "2145", "marks" => 47 ],
            //     (object) [ "studentId" => "6573", "marks" => 47 ],
            //     (object) [ "studentId" => "8932", "marks" => 46 ],
            //     (object) [ "studentId" => "1329", "marks" => 46 ],
            //     (object) [ "studentId" => "4827", "marks" => 44 ],
            //     (object) [ "studentId" => "7305", "marks" => 44 ],
            //     (object) [ "studentId" => "2941", "marks" => 41 ],
            //     (object) [ "studentId" => "6718", "marks" => 40 ]
            //   ];
            usort($students, function($a, $b) {
                return $b->marks - $a->marks;
            });
            //Step4: Remove the students with 0 marks
            $students = $this->removeZeroMarksStudents($students);
            //Step5: Assign the students having fail marks the fail grade and remove from students array
            $failedStudents = $this->assignFailGrade($students, $rule,$gradingDetails);
            // Removes the failed students from the main students array
            if(!empty($failedStudents)) {
                array_splice($students,array_key_first($failedStudents));
            }
            //Step6: Loop through each rule and assign corresponding grade
            $students = $this->calculateAndAssignGrades($students, $rule,$gradingDetails);
            //Step7: Merge the failed students with the students array
            $students = array_merge($students, $failedStudents);
        }catch(\Exception $e){
            throw new ProfessionalException($e->getCode(),$e->getMessage());
        }
        return $students;
    }
    /**
     * Removes students with zero marks from the given list.
     */
    private function removeZeroMarksStudents($students)
    {
        return array_filter($students, function($student) {
            return $student->marks > 0;
        });
    }
    
    /**
     * Assigns a fail grade to students based on the given rule.
     */
    private function assignFailGrade($students, $rule,$gradingDetails)
    {
        $failMark = $rule->failMark;
        $failedStudents = [];
        // Get the letter grade for the fail mark
        $letterGrade = $this->getGrade($gradingDetails,$rule->failedGp,$rule->awardGpRules[0]->highestGp);
        
        foreach ($students as $key => $student) {
            if ($student->marks < $failMark) {
                $student->relativeGradePoint = $rule->failedGp;
                $student->relativeGrade = $letterGrade;
                $student->isFailed = true;
                $failedStudents[$key] = $student;
            }
        }
        return $failedStudents;
    }
    
    /**
     * Calculate and assign grades to students based on the provided grading rules and details.
     *
     * @param array $students An array of student objects to be graded.
     * @param object $rule An object containing the grading rules, including awardGpRules.
     * @param object $gradingDetails An object containing the details for grading.
     * 
     * @return array An array of students who have been assigned grades.
     */
    private function calculateAndAssignGrades($students, $rule,$gradingDetails)
    {
        $totalNoOfStudents = count($students);
        // Initialize an array to keep track of students who have already been assigned grades
        $alreadyAssignedStudents = [];
        $alreadyAssignedStudentIds = [];
        // Get the top grade point from the first grading rule
        $topGp = $rule->awardGpRules[0]->highestGp;
        foreach($rule->awardGpRules as $awardGpRule){
            // Extract the highest grade point, percentage of max top grade, percentage of min top grade, and starting grade point from the current rule
            $highestGp = $awardGpRule->highestGp;
            $percentOfMaxTopGrade = $awardGpRule->percentOfMaxTopGrade;
            $percentOfMinTopGrade = $awardGpRule->percentOfMinTopGrade;
            $startingGp = $awardGpRule->startingGp;
            $highestTotalScore = 0;
            // Get the students whose marks fall within the current grade range
            $studentsInThisRange = $this->getStudentsInThisRange($students,$percentOfMinTopGrade, $percentOfMaxTopGrade);
            // Remove students who have already been assigned grades from the current range
            $studentsInThisRange = array_filter($studentsInThisRange, function($student) use ($alreadyAssignedStudentIds) {
                return !in_array($student->studentId, $alreadyAssignedStudentIds);
            });
            // Add the current range of students to the list of already assigned students
            $alreadyAssignedStudents = array_merge($alreadyAssignedStudents, $studentsInThisRange);
            $alreadyAssignedStudentIds = array_merge($alreadyAssignedStudentIds,array_column($studentsInThisRange,'studentId'));
            $studentsInThisRange = array_values($studentsInThisRange);
            
            $highestTotalScore = $studentsInThisRange[0]->marks;
            $lowestTotalScore = $studentsInThisRange[count($studentsInThisRange) - 1]->marks;
            // Loop through each student in the current range to assign grade points and grades
            foreach($studentsInThisRange as $key => $student) {
            // Assign the highest grade point to the first student in the range
            if($key === 0) {
                $student->relativeGradePoint = $highestGp;
            } else {
                // Calculate the grade point for the student based on their marks
                $student->relativeGradePoint = $this->getGradePoint($startingGp, $highestGp, $highestTotalScore,$lowestTotalScore, $student->marks);
            }
            
            // Assign the grade to the student based on their grade point
            $student->relativeGrade = $this->getGrade($gradingDetails,$student->relativeGradePoint,$topGp);
            }
        }
        return $alreadyAssignedStudents;
    }
    /**
     * Retrieves a list of students whose grades fall within a specified range.
     *
     * @param array $students An array of student objects, each containing a 'marks' property.
     * @param float $percentOfMaxTopGrade The percentage of the maximum top grade to define the upper bound of the range.
     * @return array An array of student objects whose grades fall within the specified range.
     */
    private function getStudentsInThisRange($students,$percentOfMinTopGrade, $percentOfMaxTopGrade)
    {
        $indexOfStartStudent = ceil(count($students) * $percentOfMinTopGrade / 100);
        $indexOfEndStudent = ceil(count($students) * $percentOfMaxTopGrade / 100);
        // Initialize an array to hold the students in the current grade range
        $studentsInThisRange = [];
        $studentAtLastIndex = $students[$indexOfEndStudent-1];
        // Slice the students array to get the students in the current grade range
        $studentsInThisRange = array_slice($students, $indexOfStartStudent, $indexOfEndStudent - $indexOfStartStudent);
        // Loop through the students starting from the calculated start index
        foreach (array_slice($students, $indexOfEndStudent) as $student) {
            // If the student's marks are not equal to the marks of the student at the last index, break the loop
            if ($student->marks !== $studentAtLastIndex->marks) {
            break;
            }
            // Else add the student to the current grade range array
            $studentsInThisRange[] = $student;
        }
        return $studentsInThisRange;
    }
    /**
     * Calculate the grade point for a student based on their marks and the grading scale.
     */
    private function getGradePoint($startingGp, $highestGp, $highestTotalScore,$lowestTotalScore, $studentMarks)
    {
        return $highestGp - (($highestTotalScore - $studentMarks) * (($highestGp - $startingGp)/($highestTotalScore-$lowestTotalScore)));
    }
    /**
     * Calculates and returns the letter grade based on the given grade point and grading details.
     */
    private function getGrade($gradingDetails,$gradePoint,$highestGp)
    {
        $normalisedGradePoint = ($gradePoint/$highestGp) * 100;
        foreach($gradingDetails->grades as $gradingDetail){
            if($normalisedGradePoint >= $gradingDetail->rangeFrom && $normalisedGradePoint <= $gradingDetail->rangeTo){
                $grade = $gradingDetail->letterGrade;
                break;
            }
        }
        return $grade;
    }
    
}