Skip to content

Commit

Permalink
fix(dynamic table): resizing for size expression (#1181)
Browse files Browse the repository at this point in the history
  • Loading branch information
nsenave committed Dec 26, 2024
1 parent 10051f1 commit 2484d0a
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 4 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ java {

allprojects {
group = "fr.insee.eno"
version = "3.31.2"
version = "3.31.3-SNAPSHOT"
}

subprojects {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import fr.insee.lunatic.model.flat.*;
import lombok.extern.slf4j.Slf4j;

import java.util.*;
import java.util.Objects;

@Slf4j
public class LunaticAddResizing implements ProcessingStep<Questionnaire> {
Expand All @@ -26,6 +26,8 @@ public void apply(Questionnaire lunaticQuestionnaire) {
//
LunaticLoopResizingLogic loopResizingLogic = new LunaticLoopResizingLogic(
lunaticQuestionnaire, enoQuestionnaire, enoIndex);
LunaticRosterResizingLogic rosterResizingLogic = new LunaticRosterResizingLogic(
lunaticQuestionnaire, enoQuestionnaire);
LunaticPairwiseResizingLogic pairwiseResizingLogic = new LunaticPairwiseResizingLogic(
lunaticQuestionnaire, enoIndex);
// Note: roster for loop component don't generate resizing entries unless there is a loop linked to it
Expand All @@ -36,6 +38,9 @@ public void apply(Questionnaire lunaticQuestionnaire) {
if (Objects.requireNonNull(componentType) == ComponentTypeEnum.LOOP) {
loopResizingLogic.buildResizingEntries((Loop) component, resizingType);
}
if (Objects.requireNonNull(componentType) == ComponentTypeEnum.ROSTER_FOR_LOOP) {
rosterResizingLogic.buildResizingEntries((RosterForLoop) component, resizingType);
}
if (componentType == ComponentTypeEnum.PAIRWISE_LINKS) {
pairwiseResizingLogic.buildPairwiseResizingEntries((PairwiseLinks) component, resizingType);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public void buildResizingEntries(Loop lunaticLoop, ResizingType lunaticResizing)

// Variable names that are the keys of the resizing (using a set to make sure there is no duplicates)
Set<String> resizingVariableNames = new LinkedHashSet<>();
// Expression that resize the concerned variables
// Expression that resizes the concerned variables
String sizeExpression = null;

if (enoLoop instanceof StandaloneLoop enoStandaloneLoop) {
Expand Down Expand Up @@ -115,7 +115,7 @@ private String findResizingVariableForLinkedLoop(LinkedLoop enoLinkedLoop) {
enoLinkedLoop.getReference(), enoLinkedLoop.getId()));
}

private static void insertIterationEntry(ResizingType lunaticResizing,
static void insertIterationEntry(ResizingType lunaticResizing,
String resizingVariableName, String sizeExpression, List<String> resizedVariableNames) {
// If no entry for the resizing variable name given, create it
if (lunaticResizing.getResizingEntry(resizingVariableName) == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package fr.insee.eno.core.processing.out.steps.lunatic.resizing;

import fr.insee.eno.core.exceptions.technical.MappingException;
import fr.insee.eno.core.model.EnoQuestionnaire;
import fr.insee.eno.core.model.calculated.BindingReference;
import fr.insee.eno.core.model.question.DynamicTableQuestion;
import fr.insee.lunatic.model.flat.*;
import fr.insee.lunatic.model.flat.variable.VariableType;
import fr.insee.lunatic.model.flat.variable.VariableTypeEnum;

import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import static fr.insee.eno.core.processing.out.steps.lunatic.resizing.LunaticLoopResizingLogic.insertIterationEntry;

public class LunaticRosterResizingLogic {

private final Questionnaire lunaticQuestionnaire;
private final EnoQuestionnaire enoQuestionnaire;

public LunaticRosterResizingLogic(Questionnaire lunaticQuestionnaire, EnoQuestionnaire enoQuestionnaire) {
this.lunaticQuestionnaire = lunaticQuestionnaire;
this.enoQuestionnaire = enoQuestionnaire;
}

/**
* Insert resizing entries for the given roster component.
* @param lunaticRoster Lunatic RosterForLoop (dynamic table) object.
* @param lunaticResizing Lunatic resizing block object.
*/
public void buildResizingEntries(RosterForLoop lunaticRoster, ResizingType lunaticResizing) {

// Corresponding Eno loop object
DynamicTableQuestion enoDynamicTable = getDynamicTable(enoQuestionnaire, lunaticRoster.getId());

// If the dynamic table size is not defined by a VTL expression, nothing to do here
if (enoDynamicTable.getSizeExpression() == null)
return;

// Variable names that are the keys of the resizing (using a set to make sure there is no duplicates)
Set<String> resizingVariableNames = findResizingVariablesForRoster(enoDynamicTable);

// Expression that resizes the concerned variables
String sizeExpression = enoDynamicTable.getSizeExpression().getValue();

// Concerned variables to be resized: responses of the roster component
List<String> resizedVariableNames = lunaticRoster.getComponents().stream()
.map(BodyCell::getResponse).map(ResponseType::getName).toList();

// Insert resizing entries (the logic is the same as for loops)
resizingVariableNames.forEach(variableName -> insertIterationEntry(
lunaticResizing, variableName, sizeExpression, resizedVariableNames));
}

private DynamicTableQuestion getDynamicTable(EnoQuestionnaire enoQuestionnaire, String id) {
Optional<DynamicTableQuestion> searched = enoQuestionnaire.getMultipleResponseQuestions().stream()
.filter(DynamicTableQuestion.class::isInstance).map(DynamicTableQuestion.class::cast)
.filter(dynamicTableQuestion -> id.equals(dynamicTableQuestion.getId()))
.findAny();
if (searched.isEmpty())
throw new MappingException("Cannot find dynamic table question of id " + id);
return searched.get();
}

private Set<String> findResizingVariablesForRoster(DynamicTableQuestion enoDynamicTable) {
List<String> sizeDependencies = enoDynamicTable.getSizeExpression().getBindingReferences().stream()
.map(BindingReference::getVariableName)
.toList();
// Note: we could simply return this dependencies list,
// but we use the Lunatic questionnaire to filter non-collected variables
return lunaticQuestionnaire.getVariables().stream()
.filter(variable -> VariableTypeEnum.COLLECTED.equals(variable.getVariableType()))
.map(VariableType::getName)
.filter(sizeDependencies::contains)
.collect(Collectors.toCollection(LinkedHashSet::new));
// NB: using a linked hash set to preserve the same order in different generations
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package fr.insee.eno.core.processing.out.steps.lunatic.resizing;

import fr.insee.eno.core.DDIToEno;
import fr.insee.eno.core.exceptions.business.DDIParsingException;
import fr.insee.eno.core.mappers.LunaticMapper;
import fr.insee.eno.core.model.EnoQuestionnaire;
import fr.insee.eno.core.model.calculated.BindingReference;
import fr.insee.eno.core.model.calculated.CalculatedExpression;
import fr.insee.eno.core.model.question.DynamicTableQuestion;
import fr.insee.eno.core.parameter.EnoParameters;
import fr.insee.eno.core.processing.out.steps.lunatic.LunaticLoopResolution;
import fr.insee.eno.core.processing.out.steps.lunatic.LunaticSortComponents;
import fr.insee.eno.core.processing.out.steps.lunatic.table.LunaticTableProcessing;
import fr.insee.lunatic.model.flat.*;
import fr.insee.lunatic.model.flat.variable.CollectedVariableType;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.math.BigInteger;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;

class LunaticRosterResizingLogicTest {

@Test
@DisplayName("Dynamic table with size expression: should have a resizing entry.")
void unitTest1() {
// Given
EnoQuestionnaire enoQuestionnaire = new EnoQuestionnaire();
DynamicTableQuestion enoDynamicTable = new DynamicTableQuestion();
enoDynamicTable.setId("dynamic-table-id");
CalculatedExpression sizeExpression = new CalculatedExpression();
sizeExpression.setValue("<foo expression>");
sizeExpression.getBindingReferences().add(new BindingReference("foo-ref","FOO_COLLECTED_VARIABLE"));
enoDynamicTable.setSizeExpression(sizeExpression);
enoQuestionnaire.getMultipleResponseQuestions().add(enoDynamicTable);

Questionnaire lunaticQuestionnaire = new Questionnaire();
RosterForLoop lunaticRoster = new RosterForLoop();
lunaticRoster.setId("dynamic-table-id");
BodyCell column1Component = new BodyCell();
column1Component.setResponse(new ResponseType());
column1Component.getResponse().setName("TABLE_VAR1");
BodyCell column2Component = new BodyCell();
column2Component.setResponse(new ResponseType());
column2Component.getResponse().setName("TABLE_VAR2");
lunaticRoster.getComponents().add(column1Component);
lunaticRoster.getComponents().add(column2Component);
lunaticQuestionnaire.getComponents().add(lunaticRoster);

CollectedVariableType lunaticVariable = new CollectedVariableType();
lunaticVariable.setName("FOO_COLLECTED_VARIABLE");
lunaticQuestionnaire.getVariables().add(lunaticVariable);

// When
ResizingType lunaticResizing = new ResizingType();
new LunaticRosterResizingLogic(lunaticQuestionnaire, enoQuestionnaire)
.buildResizingEntries(lunaticRoster, lunaticResizing);

// Then
assertEquals(1, lunaticResizing.countResizingEntries());
assertEquals("<foo expression>",
lunaticResizing.getResizingEntry("FOO_COLLECTED_VARIABLE").getSize());
assertEquals(List.of("TABLE_VAR1", "TABLE_VAR2"),
lunaticResizing.getResizingEntry("FOO_COLLECTED_VARIABLE").getVariables());
}

@Test
@DisplayName("Dynamic table with size expression: should have no resizing entries.")
void unitTest2() {
// Given
EnoQuestionnaire enoQuestionnaire = new EnoQuestionnaire();
DynamicTableQuestion enoDynamicTable = new DynamicTableQuestion();
enoDynamicTable.setId("dynamic-table-id");
enoDynamicTable.setMinLines(BigInteger.ONE);
enoDynamicTable.setMaxLines(BigInteger.TEN);
enoQuestionnaire.getMultipleResponseQuestions().add(enoDynamicTable);

Questionnaire lunaticQuestionnaire = new Questionnaire();
RosterForLoop lunaticRoster = new RosterForLoop();
lunaticRoster.setId("dynamic-table-id");
BodyCell column1Component = new BodyCell();
column1Component.setResponse(new ResponseType());
column1Component.getResponse().setName("TABLE_VAR1");
BodyCell column2Component = new BodyCell();
column2Component.setResponse(new ResponseType());
column2Component.getResponse().setName("TABLE_VAR2");
lunaticRoster.getComponents().add(column1Component);
lunaticRoster.getComponents().add(column2Component);
lunaticQuestionnaire.getComponents().add(lunaticRoster);

// When
ResizingType lunaticResizing = new ResizingType();
new LunaticRosterResizingLogic(lunaticQuestionnaire, enoQuestionnaire)
.buildResizingEntries(lunaticRoster, lunaticResizing);

// Then
assertEquals(0, lunaticResizing.countResizingEntries());
}

@Test
void integrationTest() throws DDIParsingException {
// Given
EnoQuestionnaire enoQuestionnaire = DDIToEno.transform(
LunaticAddResizingTest.class.getClassLoader().getResourceAsStream(
"integration/ddi/ddi-dynamic-table-size.xml"),
EnoParameters.of(EnoParameters.Context.BUSINESS, EnoParameters.ModeParameter.CAWI));
Questionnaire lunaticQuestionnaire = new Questionnaire();
LunaticMapper lunaticMapper = new LunaticMapper();
lunaticMapper.mapQuestionnaire(enoQuestionnaire, lunaticQuestionnaire);
new LunaticSortComponents(enoQuestionnaire).apply(lunaticQuestionnaire);
new LunaticLoopResolution(enoQuestionnaire).apply(lunaticQuestionnaire);
new LunaticTableProcessing(enoQuestionnaire).apply(lunaticQuestionnaire);

// When
new LunaticAddResizing(enoQuestionnaire).apply(lunaticQuestionnaire);

// Then
ResizingType lunaticResizing = lunaticQuestionnaire.getResizing();
assertEquals(1, lunaticResizing.countResizingEntries());
assertEquals("cast(HOW_MANY, integer)",
lunaticResizing.getResizingEntry("HOW_MANY").getSize());
assertEquals(List.of("DYNAMIC_TABLE_VTL_SIZE1"),
lunaticResizing.getResizingEntry("HOW_MANY").getVariables());
}

}

0 comments on commit 2484d0a

Please sign in to comment.