/** * Copyright © 2002 Instituto Superior Técnico * * This file is part of FenixEdu Academic. * * FenixEdu Academic is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * FenixEdu Academic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with FenixEdu Academic. If not, see <http://www.gnu.org/licenses/>. */ package org.fenixedu.academic.domain.student.curriculum; import java.io.Serializable; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.function.Supplier; import org.fenixedu.academic.domain.ExecutionYear; import org.fenixedu.academic.domain.Grade; import org.fenixedu.academic.domain.GradeScale; import org.fenixedu.academic.domain.IEnrolment; import org.fenixedu.academic.domain.StudentCurricularPlan; import org.fenixedu.academic.domain.degreeStructure.CycleType; import org.fenixedu.academic.domain.exceptions.DomainException; import org.fenixedu.academic.domain.studentCurriculum.CurriculumModule; import org.fenixedu.academic.domain.studentCurriculum.CycleCurriculumGroup; import org.fenixedu.academic.domain.studentCurriculum.Dismissal; import org.fenixedu.academic.domain.studentCurriculum.ExternalEnrolment; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Curriculum implements Serializable, ICurriculum { private static Logger logger = LoggerFactory.getLogger(Curriculum.class); static private final long serialVersionUID = -8365985725904139675L; public static interface CurricularYearCalculator { Integer curricularYear(Curriculum curriculum); Integer totalCurricularYears(Curriculum curriculum); BigDecimal approvedCredits(Curriculum curriculum); BigDecimal remainingCredits(Curriculum curriculum); } private static Supplier<CurricularYearCalculator> CURRICULAR_YEAR_CALCULATOR = () -> new CurricularYearCalculator() { private BigDecimal approvedCredits; private BigDecimal remainingCredits; private Integer curricularYear; private Integer totalCurricularYears; @Override public Integer curricularYear(Curriculum curriculum) { if (curricularYear == null) { doCalculus(curriculum); } return curricularYear; } @Override public Integer totalCurricularYears(Curriculum curriculum) { if (totalCurricularYears == null) { doCalculus(curriculum); } return totalCurricularYears; } @Override public BigDecimal approvedCredits(Curriculum curriculum) { if (approvedCredits == null) { doCalculus(curriculum); } return approvedCredits; } @Override public BigDecimal remainingCredits(Curriculum curriculum) { if (remainingCredits == null) { doCalculus(curriculum); } return remainingCredits; } private void doCalculus(Curriculum curriculum) { StudentCurricularPlan scp = curriculum.getStudentCurricularPlan(); totalCurricularYears = scp == null ? 0 : scp.getDegreeCurricularPlan().getDurationInYears(getCycleType(curriculum)); approvedCredits = BigDecimal.ZERO; for (final ICurriculumEntry entry : curriculum.getCurricularYearEntries()) { approvedCredits = approvedCredits.add(entry.getEctsCreditsForCurriculum()); } accountForDirectIngressions(curriculum); if (approvedCredits.compareTo(BigDecimal.ZERO) == 0) { curricularYear = Integer.valueOf(1); } else { final BigDecimal ectsCreditsCurricularYear = curriculum.getSumEctsCredits().add(BigDecimal.valueOf(24)) .divide(BigDecimal.valueOf(60), 2 * 2 + 1, RoundingMode.HALF_EVEN).add(BigDecimal.valueOf(1)); curricularYear = Math.min(ectsCreditsCurricularYear.intValue(), totalCurricularYears.intValue()); } remainingCredits = BigDecimal.ZERO; for (final ICurriculumEntry entry : curriculum.getCurricularYearEntries()) { if (entry instanceof Dismissal) { final Dismissal dismissal = (Dismissal) entry; if (dismissal.getCredits().isCredits() || dismissal.getCredits().isEquivalence() || (dismissal.isCreditsDismissal() && !dismissal.getCredits().isSubstitution())) { remainingCredits = remainingCredits.add(entry.getEctsCreditsForCurriculum()); } } } } private void accountForDirectIngressions(Curriculum curriculum) { if (getCycleType(curriculum) != null) { return; } if (!curriculum.getStudentCurricularPlan().getDegreeCurricularPlan().isBolonhaDegree()) { return; } //this is to prevent some oddly behavior spotted (e.g. student 57276) if (curriculum.getStudentCurricularPlan().getCycleCurriculumGroups().isEmpty()) { return; } CycleCurriculumGroup sgroup = Collections.min(curriculum.getStudentCurricularPlan().getCycleCurriculumGroups(), CycleCurriculumGroup.COMPARATOR_BY_CYCLE_TYPE_AND_ID); CycleType cycleIter = sgroup.getCycleType().getPrevious(); while (cycleIter != null) { if (curriculum.getStudentCurricularPlan().getDegreeCurricularPlan().getCycleCourseGroup(cycleIter) != null) { approvedCredits = approvedCredits.add(new BigDecimal(cycleIter.getEctsCredits())); } cycleIter = cycleIter.getPrevious(); } } private CycleType getCycleType(Curriculum curriculum) { if (!curriculum.hasCurriculumModule() || !curriculum.isBolonha()) { return null; } final CurriculumModule module = curriculum.getCurriculumModule(); final CycleType cycleType = module.isCycleCurriculumGroup() ? ((CycleCurriculumGroup) module).getCycleType() : null; return cycleType; } }; public static void setCurricularYearCalculator(Supplier<CurricularYearCalculator> calculator) { if (calculator != null) { CURRICULAR_YEAR_CALCULATOR = calculator; } else { logger.error("Could not set curriculum year calculator strategy to null"); } } public static interface CurriculumGradeCalculator { Grade rawGrade(Curriculum curriculum); Grade finalGrade(Curriculum curriculum); @Deprecated BigDecimal weigthedGradeSum(Curriculum curriculum); } private static Supplier<CurriculumGradeCalculator> CURRICULUM_GRADE_CALCULATOR = () -> new CurriculumGradeCalculator() { private BigDecimal sumPiCi; private BigDecimal sumPi; private Grade rawGrade; private Grade finalGrade; private void doCalculus(Curriculum curriculum) { sumPiCi = BigDecimal.ZERO; sumPi = BigDecimal.ZERO; countAverage(curriculum.averageEnrolmentRelatedEntries, curriculum.getAverageType()); countAverage(curriculum.averageDismissalRelatedEntries, curriculum.getAverageType()); BigDecimal avg = calculateAverage(); rawGrade = Grade.createGrade(avg.setScale(2, RoundingMode.HALF_UP).toString(), GradeScale.TYPE20); finalGrade = Grade.createGrade(avg.setScale(0, RoundingMode.HALF_UP).toString(), GradeScale.TYPE20); } private void countAverage(final Set<ICurriculumEntry> entries, AverageType averageType) { for (final ICurriculumEntry entry : entries) { if (entry.getGrade().isNumeric()) { final BigDecimal weigth = entry.getWeigthForCurriculum(); if (averageType == AverageType.WEIGHTED) { sumPi = sumPi.add(weigth); sumPiCi = sumPiCi.add(entry.getWeigthForCurriculum().multiply(entry.getGrade().getNumericValue())); } else if (averageType == AverageType.SIMPLE) { sumPi = sumPi.add(BigDecimal.ONE); sumPiCi = sumPiCi.add(entry.getGrade().getNumericValue()); } else { throw new DomainException("Curriculum.average.type.not.supported"); } } } } private BigDecimal calculateAverage() { return sumPi.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : sumPiCi.divide(sumPi, 2 * 2 + 1, RoundingMode.HALF_UP); } @Override public Grade rawGrade(Curriculum curriculum) { if (rawGrade == null) { doCalculus(curriculum); } return rawGrade; } @Override public Grade finalGrade(Curriculum curriculum) { if (finalGrade == null) { doCalculus(curriculum); } return finalGrade; } @Override public BigDecimal weigthedGradeSum(Curriculum curriculum) { if (sumPiCi == null) { doCalculus(curriculum); } return sumPiCi; } }; public static void setCurriculumGradeCalculator(Supplier<CurriculumGradeCalculator> calculator) { if (calculator != null) { CURRICULUM_GRADE_CALCULATOR = calculator; } else { logger.error("Could not set curriculum grade calculator strategy to null"); } } private CurriculumModule curriculumModule; private Boolean bolonhaDegree; private final ExecutionYear executionYear; private final Set<ICurriculumEntry> averageEnrolmentRelatedEntries = new HashSet<ICurriculumEntry>(); private final Set<ICurriculumEntry> averageDismissalRelatedEntries = new HashSet<ICurriculumEntry>(); private final Set<ICurriculumEntry> curricularYearEntries = new HashSet<ICurriculumEntry>(); private AverageType averageType = AverageType.WEIGHTED; private CurricularYearCalculator curricularYearCalculator = CURRICULAR_YEAR_CALCULATOR.get(); private CurriculumGradeCalculator gradeCalculator = CURRICULUM_GRADE_CALCULATOR.get(); static public Curriculum createEmpty(final ExecutionYear executionYear) { return Curriculum.createEmpty(null, executionYear); } static public Curriculum createEmpty(final CurriculumModule curriculumModule, final ExecutionYear executionYear) { return new Curriculum(curriculumModule, executionYear); } private Curriculum(final CurriculumModule curriculumModule, final ExecutionYear executionYear) { this.curriculumModule = curriculumModule; this.bolonhaDegree = curriculumModule == null ? null : curriculumModule.getStudentCurricularPlan().isBolonhaDegree(); this.executionYear = executionYear; } public Curriculum(final CurriculumModule curriculumModule, final ExecutionYear executionYear, final Collection<ICurriculumEntry> averageEnrolmentRelatedEntries, final Collection<ICurriculumEntry> averageDismissalRelatedEntries, final Collection<ICurriculumEntry> curricularYearEntries) { this(curriculumModule, executionYear); addAverageEntries(this.averageEnrolmentRelatedEntries, averageEnrolmentRelatedEntries); addAverageEntries(this.averageDismissalRelatedEntries, averageDismissalRelatedEntries); addCurricularYearEntries(this.curricularYearEntries, curricularYearEntries); } public void add(final Curriculum curriculum) { if (!hasCurriculumModule()) { this.curriculumModule = curriculum.getCurriculumModule(); this.bolonhaDegree = curriculum.isBolonha(); } addAverageEntries(averageEnrolmentRelatedEntries, curriculum.getEnrolmentRelatedEntries()); addAverageEntries(averageDismissalRelatedEntries, curriculum.getDismissalRelatedEntries()); addCurricularYearEntries(curricularYearEntries, curriculum.getCurricularYearEntries()); curricularYearCalculator = CURRICULAR_YEAR_CALCULATOR.get(); gradeCalculator = CURRICULUM_GRADE_CALCULATOR.get(); } private void addAverageEntries(final Set<ICurriculumEntry> entries, final Collection<ICurriculumEntry> newEntries) { for (final ICurriculumEntry newEntry : newEntries) { if (!isAlreadyAverageEntry(newEntry)) { add(entries, newEntry); } } } private boolean isAlreadyAverageEntry(final ICurriculumEntry newEntry) { return averageEnrolmentRelatedEntries.contains(newEntry) || averageDismissalRelatedEntries.contains(newEntry); } private void addCurricularYearEntries(final Set<ICurriculumEntry> entries, final Collection<ICurriculumEntry> newEntries) { for (final ICurriculumEntry newEntry : newEntries) { add(entries, newEntry); } } private void add(final Set<ICurriculumEntry> entries, final ICurriculumEntry newEntry) { if (isBolonha() || !isAlreadyCurricularYearEntry(newEntry)) { entries.add(newEntry); } } private boolean isAlreadyCurricularYearEntry(final ICurriculumEntry newEntry) { if (newEntry instanceof IEnrolment) { return isCurricularYearEntryAsEnrolmentOrAsSourceEnrolment((IEnrolment) newEntry); } else if (newEntry instanceof Dismissal) { return isCurricularYearEntryAsSimilarDismissal((Dismissal) newEntry); } return false; } private boolean isCurricularYearEntryAsEnrolmentOrAsSourceEnrolment(final IEnrolment newIEnrolment) { for (final ICurriculumEntry entry : curricularYearEntries) { if (entry instanceof Dismissal && ((Dismissal) entry).hasSourceIEnrolments(newIEnrolment)) { return true; } else if (entry == newIEnrolment) { return true; } } return false; } private boolean isCurricularYearEntryAsSimilarDismissal(final Dismissal newDismissal) { for (final ICurriculumEntry entry : curricularYearEntries) { if (entry instanceof Dismissal && !newDismissal.isCreditsDismissal() && newDismissal.isSimilar((Dismissal) entry)) { return true; } } return false; } public CurriculumModule getCurriculumModule() { return curriculumModule; } public boolean hasCurriculumModule() { return getCurriculumModule() != null; } public Boolean isBolonha() { return bolonhaDegree; } public ExecutionYear getExecutionYear() { return executionYear; } @Override public StudentCurricularPlan getStudentCurricularPlan() { return hasCurriculumModule() ? getCurriculumModule().getStudentCurricularPlan() : null; } public boolean hasAverageEntry() { return hasCurriculumModule() && !getCurriculumEntries().isEmpty(); } @Override public boolean isEmpty() { return !hasCurriculumModule() || (getCurriculumEntries().isEmpty() && curricularYearEntries.isEmpty()); } @Override public Collection<ICurriculumEntry> getCurriculumEntries() { final Collection<ICurriculumEntry> result = new HashSet<ICurriculumEntry>(); result.addAll(averageEnrolmentRelatedEntries); result.addAll(averageDismissalRelatedEntries); return result; } @Override public boolean hasAnyExternalApprovedEnrolment() { for (final ICurriculumEntry entry : averageDismissalRelatedEntries) { if (entry instanceof ExternalEnrolment) { return true; } } return false; } public Set<ICurriculumEntry> getEnrolmentRelatedEntries() { return averageEnrolmentRelatedEntries; } public Set<ICurriculumEntry> getDismissalRelatedEntries() { return averageDismissalRelatedEntries; } @Override public Set<ICurriculumEntry> getCurricularYearEntries() { return curricularYearEntries; } @Deprecated public BigDecimal getWeigthedGradeSum() { return gradeCalculator.weigthedGradeSum(this); } @Override public Grade getRawGrade() { return gradeCalculator.rawGrade(this); } @Override public Grade getFinalGrade() { return gradeCalculator.finalGrade(this); } @Override public BigDecimal getSumEctsCredits() { return curricularYearCalculator.approvedCredits(this); } @Override public Integer getCurricularYear() { return curricularYearCalculator.curricularYear(this); } @Override public BigDecimal getRemainingCredits() { return curricularYearCalculator.remainingCredits(this); } @Deprecated @Override public void setAverageType(AverageType averageType) { this.averageType = averageType; gradeCalculator = CURRICULUM_GRADE_CALCULATOR.get(); } @Deprecated public AverageType getAverageType() { return averageType; } @Override public Integer getTotalCurricularYears() { return curricularYearCalculator.totalCurricularYears(this); } @Override public String toString() { final StringBuilder result = new StringBuilder(); result.append("\n[CURRICULUM]"); if (hasCurriculumModule()) { result.append("\n[CURRICULUM_MODULE][ID] " + getCurriculumModule().getExternalId() + "\t[NAME]" + getCurriculumModule().getName().getContent()); result.append("\n[BOLONHA] " + isBolonha().toString()); } else { result.append("\n[NO CURRICULUM_MODULE]"); } result.append("\n[SUM ENTRIES] " + (averageEnrolmentRelatedEntries.size() + averageDismissalRelatedEntries.size())); result.append("\n[RAW GRADE] " + getRawGrade().getValue()); result.append("\n[FINAL GRADE] " + getFinalGrade().getValue()); result.append("\n[SUM ECTS CREDITS] " + getSumEctsCredits().toString()); result.append("\n[CURRICULAR YEAR] " + getCurricularYear()); result.append("\n[REMAINING CREDITS] " + getRemainingCredits().toString()); result.append("\n[TOTAL CURRICULAR YEARS] " + getTotalCurricularYears()); result.append("\n[AVERAGE ENROLMENT ENTRIES]"); for (final ICurriculumEntry entry : averageEnrolmentRelatedEntries) { result.append("\n[ENTRY] [NAME]" + entry.getName().getContent() + "\t[CREATION_DATE]" + entry.getCreationDateDateTime() + "\t[GRADE] " + entry.getGrade().toString() + "\t[WEIGHT] " + entry.getWeigthForCurriculum() + "\t[ECTS] " + entry.getEctsCreditsForCurriculum() + "\t[CLASS_NAME] " + entry.getClass().getSimpleName()); } result.append("\n[AVERAGE DISMISSAL RELATED ENTRIES]"); for (final ICurriculumEntry entry : averageDismissalRelatedEntries) { result.append("\n[ENTRY] [NAME]" + entry.getName().getContent() + "\t[CREATION_DATE]" + entry.getCreationDateDateTime() + "\t[GRADE] " + entry.getGrade().toString() + "\t[WEIGHT] " + entry.getWeigthForCurriculum() + "\t[ECTS] " + entry.getEctsCreditsForCurriculum() + "\t[CLASS_NAME] " + entry.getClass().getSimpleName()); } result.append("\n[CURRICULAR YEAR ENTRIES]"); for (final ICurriculumEntry entry : curricularYearEntries) { result.append("\n[ENTRY] [NAME]" + entry.getName().getContent() + "\t[CREATION_DATE]" + entry.getCreationDateDateTime() + "\t[ECTS] " + entry.getEctsCreditsForCurriculum() + "\t[CLASS_NAME] " + entry.getClass().getSimpleName()); } return result.toString(); } /** * This is used to remove an entry from average and curricular year entries * in order to do calculus without it * * @param entryToRemove */ public void removeFromAllCurriculumEntries(final ICurriculumEntry entryToRemove) { averageEnrolmentRelatedEntries.remove(entryToRemove); averageDismissalRelatedEntries.remove(entryToRemove); curricularYearEntries.remove(entryToRemove); } }