Commit 6000dd53 authored by François-Xavier Lebastard's avatar François-Xavier Lebastard
Browse files

UNOTOPLYS-109 UNOTOPLYS-100 feat(expression)

ajout de la fonction hasAnswer,
renommage de la méthode moyenneQuestions en average
suppression de la méthode moyenne
la méthode moyenneQuestions est gardée pour la rétrocompatibilité
parent 14f7e90c
......@@ -3,6 +3,7 @@ package com.unantes.orientactive.condition;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
......@@ -33,10 +34,16 @@ import com.unantes.orientactive.service.dto.VariableDTO;
*/
public class Expression implements IExpression {
public static final String VARIABLE_PART_SEPARATOR = "_";
private static final Logger LOGGER = LoggerFactory.getLogger(Expression.class);
/**
* Le séparateur entre les différentes parties d'une variable
*/
private static final String VARIABLE_PART_SEPARATOR = "_";
/**
* Lors du calcul des variables {@link #addVariables(List)}, une boucle infi peut se produire. Cette constante en est le seuil de détection.
*/
public static final int INFINITE_LOOP_THRESHOLD = 10000;
/**
......@@ -71,11 +78,21 @@ public class Expression implements IExpression {
*/
private final ExpressionItemGroup<Double> scores;
/**
* Contient les questions avec des réponses.
*/
private final Set<String> questionsWithAnswer;
/**
* Le parser d'expression SpEL.
*/
private final ExpressionParser parser;
/**
* Contient les scores associés à chaque réponse possible d'une question. La clé est calculée par {@link #getQuestionReference(String, String)}.
*/
private final Map<String, Double> answersScores;
private StandardEvaluationContext context;
/**
......@@ -87,6 +104,8 @@ public class Expression implements IExpression {
scores = new ExpressionItemGroup(0D);
parser = new SpelExpressionParser();
context = new StandardEvaluationContext(this);
questionsWithAnswer = new HashSet<>();
answersScores = new HashMap<>();
}
@Override
......@@ -105,8 +124,13 @@ public class Expression implements IExpression {
scores.put(questionRef, questionScore.map(oldScore -> score + oldScore).orElse(score));
}
/**
* Récupère le score d'une question. Comme le score n'est pas un élément obligatoire de la réponse, le retour est optionnel.
* @param questionRef la référence à une question {@link #getQuestionReference(String, String)}
* @return
*/
protected Optional<Double> getQuestionScore(final String questionRef) {
if (!scores.containsKey(questionRef)) {
if (!hasScore(questionRef)) {
return Optional.empty();
}
return Optional.of(scores.get(questionRef));
......@@ -142,23 +166,6 @@ public class Expression implements IExpression {
return scores;
}
/**
* Permet de réaliser une moyenne. Cette méthode peut être appelée depuis une expression avec cette syntaxe :
* {@code moyenne(scores[Q1], scores[Q2])}
*
* @param operandes
* un ensemble d'entier
* @return une moyenne arithmétique
*/
public Double moyenne(Integer... operandes) {
if (ArrayUtils.isEmpty(operandes)) {
return 0D;
}
DescriptiveStatistics stats = new DescriptiveStatistics();
Stream.of(operandes).filter(Objects::nonNull).forEach(stats::addValue);
return stats.getMean();
}
/**
* Permet de réaliser une moyenne. Cette méthode peut être appelée depuis une expression avec cette syntaxe :
* {@code moyenne(scores[Q1], scores[Q2])}.
......@@ -169,13 +176,13 @@ public class Expression implements IExpression {
* un tableau de références de questions
* @return une moyenne arithmétique
*/
public Double moyenneQuestions(String... questionsReferences) {
public Double average(String... questionsReferences) {
if (ArrayUtils.isEmpty(questionsReferences)) {
return 0D;
}
//@formatter:off
final List<Double> questionsScores = Stream.of(questionsReferences)
.filter(this::isQuestionAnswerExists)
.filter(this::hasScore)
.map(this.scores::get)
.filter(Objects::nonNull)
.collect(Collectors.toList());
......@@ -188,10 +195,39 @@ public class Expression implements IExpression {
return stats.getMean();
}
protected boolean isQuestionAnswerExists(String questionReference) {
/**
* Permet de réaliser une moyenne. Cette méthode peut être appelée depuis une expression avec cette syntaxe :
* {@code moyenne(scores[Q1], scores[Q2])}.
* Ne sont compatbilisées que les questions ayant une réponse dans {@link #answers} et ayant un score (même égal à zéro).
* Les autres questions ne sont pas compatbilisées dans le calcul du score.
* @deprecated : utiliser {@link #average(String...)} en lieu et place
* @param questionsReferences
* un tableau de références de questions
* @return une moyenne arithmétique
*/
@Deprecated(forRemoval = true, since = "0.1.1")
public Double moyenneQuestions(String... questionsReferences) {
return average(questionsReferences);
}
/**
* Est ce qu'un score existe pour cette question.
* @param questionReference
* @return
*/
public boolean hasScore(String questionReference) {
return scores.keySet().contains(questionReference);
}
/**
* Est ce qu'une réponse existe pour cette question.
* @param questionReference
* @return
*/
public boolean hasAnswer(String questionReference) {
return questionsWithAnswer.contains(questionReference);
}
/**
* Ou exclusif.
* @param op1 opérande 1
......@@ -208,7 +244,6 @@ public class Expression implements IExpression {
return parser.parseExpression(expression).getValue(context, classz);
} catch (ParseException e) {
LOGGER.error("Erreur à l'analyse syntaxique de l'expression {}", expression, e);
} catch (ExpressionException e) {
LOGGER.error("Erreur à l'évaluation de l'expression {}", expression, e);
LOGGER.info("Variables : {}", variables);
......@@ -227,8 +262,6 @@ public class Expression implements IExpression {
}
protected void addAnswer(final AnswerDTO answer) {
// contient les scores associés à chaque réponses
final Map<String, Double> answersScores = new HashMap<>();
// on met par défaut toutes les réponses comme non sélectionnées
//@formatter:off
final List<MultipleChoiceItem> screenQuestions =
......@@ -239,30 +272,24 @@ public class Expression implements IExpression {
//@formatter:on
final String screenReference = answer.getScreenReference();
for (final MultipleChoiceItem screenItem : screenQuestions) {
answersScores.putAll(initializeScreenAnswers(screenReference, screenItem.getReference(), screenItem.getChoices()));
initializeScreenAnswers(screenReference, screenItem.getReference(), screenItem.getChoices());
}
// puis on vient sélectionner celles qui ont étés selectionnées
// récupération des réponses de chaque question de l'écran
// on peuple la table des scores en fonction de la répnse sélectionnée
final List<AnswerElements> answerElements = answer.getAnswerElements();
for (final AnswerElements answerElement : answerElements) {
registerAnswers(answersScores, screenReference, answerElement);
registerAnswers(screenReference, answerElement);
}
}
/**
* Initialisation des variables relatives à un écran :
* <ul>
* <li>Toutes les réponses sont non sélectionnées. {@link #registerAnswers(Map, String, AnswerElements) viendra ensuite sélectionnées les réponses effectivement sélectionnées}</li>
* <li>On retourne les scores associés à chaque réponse. Cette Map sera utilisée par #registerAnswers(Map, String, AnswerElements) pour peupler {@link #scores}</li>
* </ul>
* Initialisation des variables relatives à un écran : toutes les réponses sont non sélectionnées. {@link #registerAnswers(String, AnswerElements) viendra ensuite sélectionnées les réponses effectivement sélectionnées}</li>
* @param screenReference une réference d'écran
* @param questionReference une référence de question
* @param questionChoices les réponses possibles à la question
* @return les scores de l'écran courant
*/
private Map<String, Double> initializeScreenAnswers(final String screenReference, final String questionReference, final List<Choice> questionChoices) {
final Map<String, Double> answersScores = new HashMap<>();
private void initializeScreenAnswers(final String screenReference, final String questionReference, final List<Choice> questionChoices) {
for (final Choice choice : questionChoices) {
final String answerRef = getAnswerReference(screenReference, questionReference, choice.getValue());
// on stocke le score associé à chaque réponse; utilisé pour calculer le score de la question en fonction de la réponse sélectionnée
......@@ -273,32 +300,64 @@ public class Expression implements IExpression {
// dans un premier temps, on considère toutes les réponses comme non sélectionnées.
addAnswer(answerRef, false);
}
return answersScores;
}
private void registerAnswers(final Map<String, Double> answersScores, final String screenReference, final AnswerElements answerElement) {
/**
* Enregistre les réponses d'une question. Peuple {@link #answers}, {@link #questionsWithAnswer}, {@link #scores}.
* @param screenReference la référence de l'écran courant
* @param answerElement les réponses d'une question
*/
private void registerAnswers(final String screenReference, final AnswerElements answerElement) {
List<String> answer = answerElement.getAnswer();
if (CollectionUtils.isNotEmpty(answer)) {
String questionReference = answerElement.getQuestionReference();
for (String value : answer) {
String questionRef = getQuestionReference(screenReference, questionReference);
String answerRef = getAnswerReference(screenReference, questionReference, value);
if (answersScores.containsKey(answerRef)) {
addQuestionScore(questionRef, answersScores.get(answerRef));
}
addAnswer(answerRef, true);
registerAnswer(screenReference, questionReference, value);
}
}
}
/**
* Enregistre une réponse à une question
* @param screenReference la référence de l'écran
* @param questionReference la référence d'une question
* @param value la réponse
*/
private void registerAnswer(final String screenReference, final String questionReference, final String value) {
String questionRef = getQuestionReference(screenReference, questionReference);
String answerRef = getAnswerReference(screenReference, questionReference, value);
if (answersScores.containsKey(answerRef)) {
addQuestionScore(questionRef, answersScores.get(answerRef));
}
addAnswer(answerRef, true);
questionsWithAnswer.add(questionRef);
}
/**
* Construit la référence à une réponse : referenceEcran_referenceQuestion_valeurReponse
* @param screenReference reference de l'écran contenant la question
* @param questionReference reference d'une question de l'écran
* @param value reference d'une réponse de la question
* @return
*/
protected String getAnswerReference(final String screenReference, final String questionReference, final String value) {
return screenReference + VARIABLE_PART_SEPARATOR + questionReference + VARIABLE_PART_SEPARATOR + value;
return getQuestionReference(screenReference, questionReference) + VARIABLE_PART_SEPARATOR + value;
}
/**
* Construit la référence d'une question : referenceEcran_referenceQuestion
* @param screenReference reference de l'écran contenant la question
* @param questionReference reference d'une question de l'écran
* @return
*/
protected String getQuestionReference(final String screenReference, final String questionReference) {
return screenReference + VARIABLE_PART_SEPARATOR + questionReference;
}
/**
* Evalue une variable. Ne doit être appelée que si les variables dont dépend la variable passée en paramètre ont été évaluées.
* @param variable
*/
private void evaluate(VariableDTO variable) {
try {
final Double variableValue = evaluate(variable.getExpression(), Double.class);
......@@ -343,6 +402,11 @@ public class Expression implements IExpression {
}
}
/**
* Détermine si les variables précédentes de la variable passée en paramètres ont toutes été calculées
* @param variable une variable de l'expression
* @return
*/
private boolean allPreviousVariablesAreComputed(VariableDTO variable) {
final List<String> previousVariables = variable.getPreviousVariables().stream().map(VariableDTO::getReference).collect(Collectors.toList());
final Set<String> computedVariablesReferences = variables.keySet();
......
......@@ -26,58 +26,35 @@ class ExpressionTest extends AbstractTest {
ExpressionTest() {serviceConverter = new ServiceConverter();}
@Test
void moyenneNoInput() {
final Expression expression = new Expression();
assertEquals(0d, expression.moyenne());
}
@Test
void moyenneNullInput() {
final Expression expression = new Expression();
assertEquals(0d, expression.moyenne(null));
}
@Test
void moyenneNullAndValidInput() {
final Expression expression = new Expression();
assertEquals(12, expression.moyenne(null, 10, 14));
}
@Test
void moyenneValidInput() {
final Expression expression = new Expression();
assertEquals(12, expression.moyenne(10, 14));
}
@Test
void moyenQuestionAvecQuestion(){
void averageAvecQuestion(){
final Expression expression = new Expression();
expression.addAnswer("Q1_R1", true);
expression.addQuestionScore("Q1_R1", 10D);
expression.addAnswer("Q2_R2", true);
expression.addQuestionScore("Q2_R2", 20D);
assertEquals(15D, expression.moyenneQuestions("Q1_R1", "Q2_R2"));
assertEquals(15D, expression.average("Q1_R1", "Q2_R2"));
}
@Test
void moyenQuestionAvecQuestionNonRepondue() {
void averageAvecQuestionNonRepondue() {
final Expression expression = new Expression();
expression.addAnswer("Q1_R1", true);
expression.addQuestionScore("Q1_R1", 10D);
assertEquals(10D, expression.moyenneQuestions("Q1_R1", "Q1_R2"));
assertEquals(10D, expression.average("Q1_R1", "Q1_R2"));
}
@Test
void moyenQuestionSansQuestions() {
void averageSansQuestions() {
final Expression expression = new Expression();
assertEquals(0D, expression.moyenneQuestions());
assertEquals(0D, expression.average());
}
@Test
void moyenQuestionAvecQuestionSansScore() {
void averageAvecQuestionSansScore() {
final Expression expression = new Expression();
expression.addAnswer("Q1_R1", false);
assertEquals(0D, expression.moyenneQuestions("Q1_R1"));
assertEquals(0D, expression.average("Q1_R1"));
}
@Test
void evaluate() {
......@@ -102,10 +79,10 @@ class ExpressionTest extends AbstractTest {
expression.addQuestionScore("Q3", 11d);
expression.addVariable("moy_mat_litt_bacG", 20d);
expression.addVariable("moy_spe_scient", 5d);
assertEquals(15.5d, expression.evaluate("moyenne(scores[Q1], scores[Q2])", Double.class));
assertEquals(15.5d, expression.evaluate("average('Q1', 'Q2')", Double.class));
assertEquals(15.5d, expression.evaluate("0.7 * variables[moy_mat_litt_bacG] + 0.3 * variables[moy_spe_scient]", Double.class));
assertEquals(14d, expression.evaluate("answers[Q1_R4] ? moyenne(scores[Q1], scores[Q2]) : moyenne(scores[Q1], scores[Q2], scores[Q3])", Double.class));
assertEquals(15.5d, expression.evaluate("answers[Q1_R3] ? moyenne(scores[Q1], scores[Q2]) : moyenne(scores[Q1], scores[Q2], scores[Q3])", Double.class));
assertEquals(14d, expression.evaluate("answers[Q1_R4] ? average('Q1', 'Q2') : average('Q1', 'Q2', 'Q3')", Double.class));
assertEquals(15.5d, expression.evaluate("answers[Q1_R3] ? average('Q1', 'Q2') : average('Q1', 'Q2', 'Q3')", Double.class));
assertFalse(expression.evaluate("(answers[Q1_R4] || answers[Q2_R4]) && answers[Q3_R4]", Boolean.class));
assertTrue(expression.evaluate("answers[Q2_R1] && variables[moy_spe_scient] >= 0 && variables[moy_spe_scient] < 6", Boolean.class));
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment