Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clean CSA constants #1127

Draft
wants to merge 29 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c3cee7f
Import TapPositionAction from TapChanger
bqth29 Sep 6, 2024
bf7d37f
Author + copyright
bqth29 Sep 6, 2024
ef7689e
Use queries
bqth29 Sep 6, 2024
906e5cc
Unify contingency queries
bqth29 Sep 6, 2024
3d6367c
Override query more elegantly
bqth29 Sep 6, 2024
2c81ca6
Use generic property bag parser for native objects
bqth29 Sep 9, 2024
88720d9
Introduce overridable attributes
bqth29 Sep 9, 2024
6ff6151
CsaInstants enum
bqth29 Sep 9, 2024
db05da9
Simplify LimitType enum
bqth29 Sep 9, 2024
6a0bce4
Lint
bqth29 Sep 9, 2024
c0b5aae
Merge branch 'main' into experiment/csa-tap-changers
bqth29 Sep 9, 2024
2504254
Merge branch 'experiment/csa-tap-changers' into feature/csa/reorganiz…
bqth29 Sep 9, 2024
de0e4f2
Merge tap changer branch
bqth29 Sep 9, 2024
a2e9bd2
Merge branch 'main' into feature/csa/reorganize-constants
bqth29 Sep 23, 2024
33f51df
Merge main
bqth29 Sep 23, 2024
a2fecbf
Remove LimitType enum
bqth29 Sep 23, 2024
e0f9f19
Remove NCAggregator
bqth29 Sep 23, 2024
eb05d7a
Linter and sonar
bqth29 Sep 23, 2024
2c6d9a0
Sonar + refactoring
bqth29 Sep 23, 2024
17882d6
Refactored Network Action Adder
bqth29 Sep 23, 2024
c429dd7
Merge branch 'main' into feature/csa/reorganize-constants
bqth29 Oct 7, 2024
1c05409
Merge main
bqth29 Oct 9, 2024
4c36524
Merge branch 'main' into feature/csa/reorganize-constants
bqth29 Oct 10, 2024
a54a2cb
Merge branch 'main' into feature/csa/reorganize-constants
bqth29 Oct 14, 2024
32b056b
Merge branch 'main' into feature/csa/reorganize-constants
bqth29 Oct 15, 2024
7373fbf
Merge branch 'main' into feature/csa/reorganize-constants
bqth29 Nov 7, 2024
4327b93
merge main
bqth29 Nov 7, 2024
6bf2981
resolve some comments
bqth29 Nov 7, 2024
e4f5fa3
Merge branch 'main' into feature/csa/reorganize-constants
bqth29 Dec 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,22 @@

package com.powsybl.openrao.data.cracio.csaprofiles;

import com.powsybl.openrao.commons.OpenRaoException;
import com.powsybl.openrao.commons.logs.OpenRaoLoggerProvider;
import com.powsybl.openrao.data.cracio.csaprofiles.craccreator.CsaProfileCracUtils;
import com.powsybl.openrao.data.cracio.csaprofiles.craccreator.NcPropertyBagsConverter;
import com.powsybl.openrao.data.cracio.csaprofiles.craccreator.constants.CsaProfileConstants;
import com.powsybl.openrao.data.cracio.csaprofiles.craccreator.constants.CsaProfileKeyword;
import com.powsybl.openrao.data.cracio.csaprofiles.craccreator.constants.HeaderType;
import com.powsybl.openrao.data.cracio.csaprofiles.craccreator.constants.OverridingObjectsFields;
import com.powsybl.openrao.data.cracio.csaprofiles.craccreator.Query;
import com.powsybl.openrao.data.cracio.csaprofiles.nc.*;
import com.powsybl.triplestore.api.PropertyBag;
import com.powsybl.triplestore.api.PropertyBags;
import com.powsybl.triplestore.api.QueryCatalog;
import com.powsybl.triplestore.api.TripleStore;

import java.lang.reflect.InvocationTargetException;
import java.time.OffsetDateTime;
import java.util.*;
import java.util.stream.Collectors;

/**
* @author Jean-Pierre Arnould {@literal <jean-pierre.arnould at rte-france.com>}
Expand All @@ -34,12 +35,14 @@ public class CsaProfileCrac {

private final Map<String, Set<String>> keywordMap;
private Map<String, String> overridingData;
private Map<Class<? extends NCObject>, Set<? extends NCObject>> queriedNativeObjects;

public CsaProfileCrac(TripleStore tripleStoreCsaProfileCrac, Map<String, Set<String>> keywordMap) {
this.tripleStoreCsaProfileCrac = tripleStoreCsaProfileCrac;
this.queryCatalogCsaProfileCrac = new QueryCatalog(CsaProfileConstants.SPARQL_FILE_CSA_PROFILE);
this.keywordMap = keywordMap;
this.overridingData = new HashMap<>();
this.queriedNativeObjects = new HashMap<>();
}

public void clearContext(String context) {
Expand All @@ -64,138 +67,77 @@ private Set<String> getContextNamesToRequest(CsaProfileKeyword keyword) {

public Map<String, PropertyBags> getHeaders() {
Map<String, PropertyBags> returnMap = new HashMap<>();
tripleStoreCsaProfileCrac.contextNames().forEach(context -> returnMap.put(context, queryTripleStore(CsaProfileConstants.REQUEST_HEADER, Set.of(context))));
tripleStoreCsaProfileCrac.contextNames().forEach(context -> returnMap.put(context, queryTripleStore("header", Set.of(context))));
return returnMap;
}

public PropertyBags getPropertyBags(CsaProfileKeyword keyword, String... queries) {
Set<String> namesToRequest = getContextNamesToRequest(keyword);
public PropertyBags getPropertyBags(Query query) {
Set<String> namesToRequest = getContextNamesToRequest(query.getTargetProfilesKeyword());
if (namesToRequest.isEmpty()) {
return new PropertyBags();
}
return this.queryTripleStore(List.of(queries), namesToRequest);
return CsaProfileCracUtils.overrideQuery(this.queryTripleStore(List.of(query.getTitle()), namesToRequest), query, overridingData);
}

public PropertyBags getPropertyBags(CsaProfileKeyword keyword, OverridingObjectsFields withOverride, String... queries) {
return withOverride == null ? getPropertyBags(keyword, queries) : CsaProfileCracUtils.overrideData(getPropertyBags(keyword, queries), overridingData, withOverride);
}

public Set<Contingency> getContingencies() {
return new NcPropertyBagsConverter<>(Contingency::fromPropertyBag).convert(getPropertyBags(CsaProfileKeyword.CONTINGENCY, OverridingObjectsFields.CONTINGENCY, CsaProfileConstants.REQUEST_ORDINARY_CONTINGENCY, CsaProfileConstants.REQUEST_EXCEPTIONAL_CONTINGENCY, CsaProfileConstants.REQUEST_OUT_OF_RANGE_CONTINGENCY));
}

public Set<ContingencyEquipment> getContingencyEquipments() {
return new NcPropertyBagsConverter<>(ContingencyEquipment::fromPropertyBag).convert(getPropertyBags(CsaProfileKeyword.CONTINGENCY, CsaProfileConstants.REQUEST_CONTINGENCY_EQUIPMENT));
}

public Set<AssessedElement> getAssessedElements() {
return new NcPropertyBagsConverter<>(AssessedElement::fromPropertyBag).convert(getPropertyBags(CsaProfileKeyword.ASSESSED_ELEMENT, OverridingObjectsFields.ASSESSED_ELEMENT, CsaProfileConstants.REQUEST_ASSESSED_ELEMENT));
}

public Set<AssessedElementWithContingency> getAssessedElementWithContingencies() {
return new NcPropertyBagsConverter<>(AssessedElementWithContingency::fromPropertyBag).convert(getPropertyBags(CsaProfileKeyword.ASSESSED_ELEMENT, OverridingObjectsFields.ASSESSED_ELEMENT_WITH_CONTINGENCY, CsaProfileConstants.REQUEST_ASSESSED_ELEMENT_WITH_CONTINGENCY));
}

public Set<AssessedElementWithRemedialAction> getAssessedElementWithRemedialActions() {
return new NcPropertyBagsConverter<>(AssessedElementWithRemedialAction::fromPropertyBag).convert(getPropertyBags(CsaProfileKeyword.ASSESSED_ELEMENT, OverridingObjectsFields.ASSESSED_ELEMENT_WITH_REMEDIAL_ACTION, CsaProfileConstants.REQUEST_ASSESSED_ELEMENT_WITH_REMEDIAL_ACTION));
}

public Set<CurrentLimit> getCurrentLimits() {
return new NcPropertyBagsConverter<>(CurrentLimit::fromPropertyBag).convert(getPropertyBags(CsaProfileKeyword.CGMES, OverridingObjectsFields.CURRENT_LIMIT, CsaProfileConstants.REQUEST_CURRENT_LIMIT));
}

public Set<VoltageLimit> getVoltageLimits() {
return new NcPropertyBagsConverter<>(VoltageLimit::fromPropertyBag).convert(getPropertyBags(CsaProfileKeyword.CGMES, OverridingObjectsFields.VOLTAGE_LIMIT, CsaProfileConstants.REQUEST_VOLTAGE_LIMIT));
}

public Set<VoltageAngleLimit> getVoltageAngleLimits() {
return new NcPropertyBagsConverter<>(VoltageAngleLimit::fromPropertyBag).convert(getPropertyBags(CsaProfileKeyword.EQUIPMENT_RELIABILITY, OverridingObjectsFields.VOLTAGE_ANGLE_LIMIT, CsaProfileConstants.REQUEST_VOLTAGE_ANGLE_LIMIT));
}

public Set<GridStateAlterationRemedialAction> getGridStateAlterationRemedialActions() {
return new NcPropertyBagsConverter<>(GridStateAlterationRemedialAction::fromPropertyBag).convert(getPropertyBags(CsaProfileKeyword.REMEDIAL_ACTION, OverridingObjectsFields.GRID_STATE_ALTERATION_REMEDIAL_ACTION, CsaProfileConstants.GRID_STATE_ALTERATION_REMEDIAL_ACTION));
}

public Set<TopologyAction> getTopologyActions() {
return new NcPropertyBagsConverter<>(TopologyAction::fromPropertyBag).convert(getPropertyBags(CsaProfileKeyword.REMEDIAL_ACTION, OverridingObjectsFields.TOPOLOGY_ACTION, CsaProfileConstants.TOPOLOGY_ACTION));
}

public Set<RotatingMachineAction> getRotatingMachineActions() {
return new NcPropertyBagsConverter<>(RotatingMachineAction::fromPropertyBag).convert(getPropertyBags(CsaProfileKeyword.REMEDIAL_ACTION, OverridingObjectsFields.ROTATING_MACHINE_ACTION, CsaProfileConstants.ROTATING_MACHINE_ACTION));
}

public Set<ShuntCompensatorModification> getShuntCompensatorModifications() {
return new NcPropertyBagsConverter<>(ShuntCompensatorModification::fromPropertyBag).convert(getPropertyBags(CsaProfileKeyword.REMEDIAL_ACTION, OverridingObjectsFields.SHUNT_COMPENSATOR_MODIFICATION, CsaProfileConstants.SHUNT_COMPENSATOR_MODIFICATION));
}

public Set<TapPositionAction> getTapPositionActions() {
return new NcPropertyBagsConverter<>(TapPositionAction::fromPropertyBag).convert(getPropertyBags(CsaProfileKeyword.REMEDIAL_ACTION, OverridingObjectsFields.TAP_POSITION_ACTION, CsaProfileConstants.TAP_POSITION_ACTION));
}

public Set<StaticPropertyRange> getStaticPropertyRanges() {
return new NcPropertyBagsConverter<>(StaticPropertyRange::fromPropertyBag).convert(getPropertyBags(CsaProfileKeyword.REMEDIAL_ACTION, OverridingObjectsFields.STATIC_PROPERTY_RANGE, CsaProfileConstants.STATIC_PROPERTY_RANGE));
}

public Set<ContingencyWithRemedialAction> getContingencyWithRemedialActions() {
return new NcPropertyBagsConverter<>(ContingencyWithRemedialAction::fromPropertyBag).convert(getPropertyBags(CsaProfileKeyword.REMEDIAL_ACTION, OverridingObjectsFields.CONTINGENCY_WITH_REMEDIAL_ACTION, CsaProfileConstants.REQUEST_CONTINGENCY_WITH_REMEDIAL_ACTION));
}

public Set<Stage> getStages() {
return new NcPropertyBagsConverter<>(Stage::fromPropertyBag).convert(getPropertyBags(CsaProfileKeyword.REMEDIAL_ACTION, CsaProfileConstants.STAGE));
}

public Set<GridStateAlterationCollection> getGridStateAlterationCollections() {
return new NcPropertyBagsConverter<>(GridStateAlterationCollection::fromPropertyBag).convert(getPropertyBags(CsaProfileKeyword.REMEDIAL_ACTION, CsaProfileConstants.GRID_STATE_ALTERATION_COLLECTION));
}

public Set<RemedialActionScheme> getRemedialActionSchemes() {
return new NcPropertyBagsConverter<>(RemedialActionScheme::fromPropertyBag).convert(getPropertyBags(CsaProfileKeyword.REMEDIAL_ACTION, OverridingObjectsFields.REMEDIAL_ACTION_SCHEME, CsaProfileConstants.REMEDIAL_ACTION_SCHEME));
}

public Set<SchemeRemedialAction> getSchemeRemedialActions() {
return new NcPropertyBagsConverter<>(SchemeRemedialAction::fromPropertyBag).convert(getPropertyBags(CsaProfileKeyword.REMEDIAL_ACTION, OverridingObjectsFields.SCHEME_REMEDIAL_ACTION, CsaProfileConstants.REQUEST_SCHEME_REMEDIAL_ACTION));
}

public Set<RemedialActionGroup> getRemedialActionGroups() {
return new NcPropertyBagsConverter<>(RemedialActionGroup::fromPropertyBag).convert(getPropertyBags(CsaProfileKeyword.REMEDIAL_ACTION, CsaProfileConstants.REQUEST_REMEDIAL_ACTION_GROUP));

}

public Set<RemedialActionDependency> getRemedialActionDependencies() {
return new NcPropertyBagsConverter<>(RemedialActionDependency::fromPropertyBag).convert(getPropertyBags(CsaProfileKeyword.REMEDIAL_ACTION, OverridingObjectsFields.SCHEME_REMEDIAL_ACTION_DEPENDENCY, CsaProfileConstants.REQUEST_REMEDIAL_ACTION_DEPENDENCY));
}

public Set<TapChanger> getTapChangers() {
return new NcPropertyBagsConverter<>(TapChanger::fromPropertyBag).convert(getPropertyBags(CsaProfileKeyword.CGMES, CsaProfileConstants.REQUEST_TAP_CHANGER));
/**
* Returns the set of all the native NC objects of the specified type from the NC profiles
*
* @param nativeType NC type of objects to retrieve
* @param <T> native NC class type
* @return set of native objects
*/
public <T extends NCObject> Set<T> getNativeObjects(Class<T> nativeType) {
if (queriedNativeObjects.containsKey(nativeType)) {
return (Set<T>) queriedNativeObjects.get(nativeType);
}
Query query = Arrays.stream(Query.values()).filter(q -> nativeType.equals(q.getNativeClass())).findFirst().orElseThrow();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you can put this in a static method of query (that would return an optional)

Set<T> nativeObjects = getPropertyBags(query).stream().map(pb -> {
try {
return NativeParser.fromPropertyBag(pb, nativeType, query.getDefaultValues());
} catch (NoSuchMethodException | IllegalAccessException | InstantiationException |
InvocationTargetException e) {
throw new OpenRaoException(e);
}
}).collect(Collectors.toSet());
queriedNativeObjects.put(nativeType, nativeObjects);
return nativeObjects;
}

private void setOverridingData(OffsetDateTime importTimestamp) {
overridingData = new HashMap<>();
for (OverridingObjectsFields overridingObject : OverridingObjectsFields.values()) {
addDataFromTripleStoreToMap(overridingData, overridingObject.getRequestName(), overridingObject.getObjectName(), overridingObject.getOverridedFieldName(), overridingObject.getHeaderType(), importTimestamp);
}
Arrays.stream(Query.values()).forEach(query -> addDataFromTripleStoreToMap(overridingData, query, importTimestamp));
}

private void addDataFromTripleStoreToMap(Map<String, String> dataMap, String queryName, String queryObjectName, String queryFieldName, HeaderType headerType, OffsetDateTime importTimestamp) {
PropertyBags propertyBagsResult = queryTripleStore(queryName, tripleStoreCsaProfileCrac.contextNames());
private void addDataFromTripleStoreToMap(Map<String, String> dataMap, Query query, OffsetDateTime importTimestamp) {
if (query.getOverridableAttribute() == null) {
return;
}
PropertyBags propertyBagsResult = queryTripleStore(query.getTitle() + "Overriding", tripleStoreCsaProfileCrac.contextNames());
for (PropertyBag propertyBag : propertyBagsResult) {
if (HeaderType.START_END_DATE.equals(headerType)) {
if (CsaProfileCracUtils.checkProfileKeyword(propertyBag, CsaProfileKeyword.STEADY_STATE_INSTRUCTION) && CsaProfileCracUtils.checkProfileValidityInterval(propertyBag, importTimestamp)) {
String id = propertyBag.getId(queryObjectName);
String overridedValue = propertyBag.get(queryFieldName);
dataMap.put(id, overridedValue);
}
if (!CsaProfileKeyword.CGMES.equals(query.getTargetProfilesKeyword())) {
overrideDataFromSsi(dataMap, query, importTimestamp, propertyBag);
} else {
if (CsaProfileCracUtils.checkProfileKeyword(propertyBag, CsaProfileKeyword.STEADY_STATE_HYPOTHESIS)) {
OffsetDateTime scenarioTime = OffsetDateTime.parse(propertyBag.get(CsaProfileConstants.SCENARIO_TIME));
if (importTimestamp.isEqual(scenarioTime)) {
String id = propertyBag.getId(queryObjectName);
String overridedValue = propertyBag.get(queryFieldName);
dataMap.put(id, overridedValue);
}
overrideDataFromSsh(dataMap, query, importTimestamp, propertyBag);
}
}
}
}

private static void overrideDataFromSsi(Map<String, String> dataMap, Query query, OffsetDateTime importTimestamp, PropertyBag propertyBag) {
if (CsaProfileCracUtils.checkProfileKeyword(propertyBag, CsaProfileKeyword.STEADY_STATE_INSTRUCTION) && CsaProfileCracUtils.checkProfileValidityInterval(propertyBag, importTimestamp)) {
String id = propertyBag.getId(query.getTitle());
String overridingValue = propertyBag.get(query.getOverridableAttribute().getOverridingName());
dataMap.put(id, overridingValue);
}
}

private static void overrideDataFromSsh(Map<String, String> dataMap, Query query, OffsetDateTime importTimestamp, PropertyBag propertyBag) {
OffsetDateTime scenarioTime = OffsetDateTime.parse(propertyBag.get("scenarioTime"));
if (importTimestamp.isEqual(scenarioTime)) {
String id = propertyBag.getId(query.getTitle());
String overridingValue = propertyBag.get(query.getOverridableAttribute().getOverridingName());
dataMap.put(id, overridingValue);
}
}

Expand Down Expand Up @@ -251,8 +193,8 @@ private void clearTimewiseIrrelevantContexts(OffsetDateTime offsetDateTime) {
}

private static boolean checkTimeCoherence(PropertyBag header, OffsetDateTime offsetDateTime) {
String startTime = header.getId(CsaProfileConstants.REQUEST_HEADER_START_DATE);
String endTime = header.getId(CsaProfileConstants.REQUEST_HEADER_END_DATE);
String startTime = header.getId(CsaProfileConstants.START_DATE);
String endTime = header.getId(CsaProfileConstants.END_DATE);
return CsaProfileCracUtils.isValidInterval(offsetDateTime, startTime, endTime);
}
Comment on lines 195 to 199
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you not replace this method with CsaProfileCracUtils.checkProfileValidityInterval directly?

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright (c) 2024, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.powsybl.openrao.data.cracio.csaprofiles;

import com.powsybl.openrao.commons.OpenRaoException;
import com.powsybl.openrao.data.cracio.csaprofiles.nc.NCObject;
import com.powsybl.triplestore.api.PropertyBag;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.RecordComponent;
import java.util.Arrays;
import java.util.Map;

/**
* @author Thomas Bouquet {@literal <thomas.bouquet at rte-france.com>}
*
* Utility class that provides a generic template to query native objects
* in NC profiles and to cast the results to native NC objects.
*/
public final class NativeParser {

private NativeParser() {
}

private static Object parseStringValue(String value, Class<?> targetType) {
if (value == null || String.class.equals(targetType)) {
return value;
} else if (Boolean.class.equals(targetType)) {
if ("true".equalsIgnoreCase(value)) {
return true;
} else if ("false".equalsIgnoreCase(value)) {
return false;
} else {
return null;
}
} else if (Double.class.equals(targetType)) {
return Double.parseDouble(value);
} else if (Integer.class.equals(targetType)) {
return Integer.parseInt(value);
} else {
throw new OpenRaoException("Unsupported type %s".formatted(targetType.getName()));
}
}

private static String getMrid(PropertyBag propertyBag, Class<?> nativeClass) {
String propertyBagName = Character.toLowerCase(nativeClass.getSimpleName().charAt(0)) + nativeClass.getSimpleName().substring(1);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you not simply do getSimpleName().toLowerCase() or do you need any other upper case to stay upper case?

return propertyBag.getId(propertyBagName);
}

public static <T extends NCObject> T fromPropertyBag(PropertyBag propertyBag, Class<T> nativeClass, Map<String, Object> defaultValues) throws IllegalArgumentException, OpenRaoException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to reduce the number of possible thrown exceptions?

// Ensure the class is a record
if (!nativeClass.isRecord()) {
throw new IllegalArgumentException("Provided class is not a record");
}

// Get the record's components (fields)
RecordComponent[] components = nativeClass.getRecordComponents();

// Prepare an array to hold the values for the record's constructor
Object[] constructorArgs = new Object[components.length];

// Loop through the components and extract values from the property bag
for (int i = 0; i < components.length; i++) {
RecordComponent component = components[i];
if ("mrid".equals(component.getName())) {
constructorArgs[i] = getMrid(propertyBag, nativeClass);
} else {
Object parsedValue = parseStringValue(propertyBag.getId(component.getName()), component.getType());
if (parsedValue == null) {
parsedValue = defaultValues.get(component.getName());
}
constructorArgs[i] = parsedValue;
}
}

// Find the canonical constructor of the record class
Constructor<T> constructor = nativeClass.getDeclaredConstructor(
// Get the types of the components (this matches the constructor signature)
Arrays.stream(components)
.map(RecordComponent::getType)
.toArray(Class<?>[]::new)
);

// Create and return a new instance of the record
return constructor.newInstance(constructorArgs);
}
Comment on lines +55 to +91
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class is very complex and difficult to maintain. It also requires the name of the fields of the records to match perfectly the ones in the xsd. It also changes all the booleans into Booleans which makes the rest of the code a bit less nice (I guess this can be solved by changing the fields in the records to booleans?).

}
Loading