Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
io.github.oliviercailloux.grade.format.CsvGrades Maven / Gradle / Ivy
package io.github.oliviercailloux.grade.format;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Verify.verify;
import com.google.common.base.Verify;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.ImmutableTable;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Range;
import com.google.common.collect.Streams;
import com.google.common.math.DoubleMath;
import com.google.common.math.Stats;
import com.univocity.parsers.csv.CsvWriter;
import com.univocity.parsers.csv.CsvWriterSettings;
import io.github.oliviercailloux.git.github.model.GitHubUsername;
import io.github.oliviercailloux.grade.Criterion;
import io.github.oliviercailloux.grade.CriterionGradeWeight;
import io.github.oliviercailloux.grade.Grade;
import io.github.oliviercailloux.grade.GradeAggregator;
import io.github.oliviercailloux.grade.IGrade;
import io.github.oliviercailloux.grade.IGrade.CriteriaPath;
import io.github.oliviercailloux.grade.MarksTree;
import io.github.oliviercailloux.grade.WeightingGrade;
import io.github.oliviercailloux.grade.WeightingGradeAggregator;
import io.github.oliviercailloux.grade.comm.StudentOnGitHub;
import io.github.oliviercailloux.grade.comm.StudentOnGitHubKnown;
import java.io.StringWriter;
import java.text.NumberFormat;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.DoubleStream;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CsvGrades {
@SuppressWarnings("unused")
private static final Logger LOGGER = LoggerFactory.getLogger(CsvGrades.class);
public static final double DEFAULT_DENOMINATOR = 20d;
public static final Function> STUDENT_NAME_FUNCTION =
s -> ImmutableMap.of("Name", s);
public static final Function> STUDENT_USERNAME_FUNCTION =
s -> ImmutableMap.of("Username", s.getUsername());
public static final Function> STUDENT_GENERIC_NAME_FUNCTION =
s -> ImmutableMap.of("Name", s.toString());
public static final Function> STUDENT_IDENTITY_FUNCTION = s -> ImmutableMap.of("Name",
s.hasInstitutionalPart() ? s.toInstitutionalStudent().getLastName() : "unknown",
"GitHub username", s.getGitHubUsername().getUsername());
public static final Function> STUDENT_KNOWN_IDENTITY_FUNCTION = s -> ImmutableMap.of("Name",
s.getLastName(), "GitHub username", s.getGitHubUsername().getUsername());
public static CsvGrades
newInstance(Function> identityFunction, double denominator) {
return new CsvGrades<>(identityFunction, denominator);
}
public static record GenericExam (GradeAggregator aggregator,
ImmutableMap trees) {
public GenericExam(GradeAggregator aggregator, Map trees) {
this(aggregator, ImmutableMap.copyOf(trees));
}
public ImmutableSet getUsernames() {
return trees.keySet();
}
public Grade getGrade(K username) {
return Grade.given(aggregator, trees.get(username));
}
}
public static record PerCriterionWeightingExam (WeightingGradeAggregator aggregator,
ImmutableMap trees) {
private static Stream getSuccessors(MarksTree grade, CriteriaPath prefix) {
return Streams.concat(Stream.of(prefix), grade.getCriteria().stream().map(prefix::withSuffix)
.flatMap(p -> getSuccessors(grade.getTree(p.getTail()), p)));
}
public ImmutableSet getUsernames() {
return trees.keySet();
}
public Grade getGrade(K username) {
return Grade.given(aggregator, trees.get(username));
}
public double weight(CriteriaPath path) {
return aggregator.weight(path);
}
public DoubleStream points(CriteriaPath path) {
return getUsernames().stream().map(this::getGrade).mapToDouble(
g -> g.toMarksTree().hasPath(path) ? g.getGrade(path).mark().getPoints() : 0d);
}
public double points(K key, CriteriaPath path) {
final Grade grade = getGrade(key);
return grade.toMarksTree().hasPath(path) ? grade.mark(path).getPoints() : 0d;
}
public double averagePoints(CriteriaPath path) {
return Stats.of(points(path)).mean();
}
public ImmutableSet allPaths() {
return trees.values().stream().flatMap(g -> getSuccessors(g, CriteriaPath.ROOT))
.collect(ImmutableSet.toImmutableSet());
}
}
private static PerCriterionWeightingExam toPerCriterionWeightingExam(GenericExam exam) {
final PerCriterionWeightingExam newExam = new PerCriterionWeightingExam<>(
Grade.transformToPerCriterionWeighting(exam.aggregator()), Maps.toMap(exam.getUsernames(),
u -> Grade.adaptMarksForPerCriterionWeighting(exam.getGrade(u))));
verify(exam.getUsernames().equals(newExam.getUsernames()));
verify(exam.getUsernames().stream()
.allMatch(u -> DoubleMath.fuzzyEquals(exam.getGrade(u).mark().getPoints(),
newExam.getGrade(u).mark().getPoints(), 1e-6d)));
return newExam;
}
private static String shorten(CriteriaPath gradePath) {
if (gradePath.isRoot()) {
return "POINTS";
}
return gradePath.toSimpleString();
}
private static Stream>
childrenAsStream(Entry parent) {
final Stream> itself = Stream.of(parent);
final IGrade grade = parent.getValue();
final Stream> allSubGrades =
grade.getSubGrades().entrySet().stream().flatMap(CsvGrades::childrenAsStream);
return Stream.concat(itself, allSubGrades);
}
@SuppressWarnings("unused")
private static Stream> childrenAsStream(IGrade parent) {
if (parent.getSubGrades().isEmpty()) {
return Stream.of();
}
return parent.getSubGrades().entrySet().stream().flatMap(CsvGrades::childrenAsStream);
}
private static Stream asContextualizedStream(CriterionGradeWeight parent) {
final Stream itself = Stream.of(parent);
final ImmutableSet subGrades;
if (parent.getGrade() instanceof WeightingGrade) {
WeightingGrade weightingParent = (WeightingGrade) parent.getGrade();
subGrades = weightingParent.getSubGradesAsSet();
} else {
checkArgument(parent.getGrade().getSubGrades().isEmpty());
subGrades = ImmutableSet.of();
}
final Stream mapped = subGrades.stream()
.map((cwg) -> CriterionGradeWeight.from(
Criterion.given(parent.getCriterion().getName() + "/" + cwg.getCriterion().getName()),
cwg.getGrade(), parent.getWeight() * cwg.getWeight()));
final Stream flatmappedChildren =
mapped.flatMap((cwg) -> asContextualizedStream(cwg));
return Stream.concat(itself, flatmappedChildren);
}
private static double getPointsScaled(CriterionGradeWeight cgw, double denominator) {
final double points = cgw.getGrade().getPoints();
final double pointsSigned = cgw.getWeight() >= 0d ? points : 1d - points;
final double pointsScaled = pointsSigned * cgw.getWeight() * denominator;
return pointsScaled;
}
private static String boundsToString(NumberFormat formatter, Range bounds) {
final double lower = bounds.lowerEndpoint();
final double upper = bounds.upperEndpoint();
final String formattedLower = formatter.format(bounds.lowerEndpoint());
final String formattedUpper = formatter.format(bounds.upperEndpoint());
return (lower == upper) ? "{" + formattedLower + "}"
: "[" + formattedLower + ", " + formattedUpper + "]";
}
private Function> identityFunction;
private double denominator;
private CsvGrades(Function> identityFunction,
double denominator) {
this.identityFunction = checkNotNull(identityFunction);
checkArgument(Double.isFinite(denominator));
this.denominator = denominator;
}
public Function> getIdentityFunction() {
return identityFunction;
}
public CsvGrades
setIdentityFunction(Function> identityFunction) {
this.identityFunction = checkNotNull(identityFunction);
return this;
}
public double getDenominator() {
return denominator;
}
public CsvGrades setDenominator(double denominator) {
this.denominator = denominator;
return this;
}
public String toCsv(Map grades) {
final Set keys = grades.keySet();
checkArgument(!keys.isEmpty(), "Can’t determine identity headers with no keys.");
final NumberFormat formatter = NumberFormat.getNumberInstance(Locale.ENGLISH);
final StringWriter stringWriter = new StringWriter();
final CsvWriter writer = new CsvWriter(stringWriter, new CsvWriterSettings());
final ImmutableMap weightingGrades =
grades.entrySet().stream().filter(e -> e.getValue() instanceof WeightingGrade).collect(
ImmutableMap.toImmutableMap(Map.Entry::getKey, e -> (WeightingGrade) e.getValue()));
final ImmutableSetMultimap perKey = weightingGrades.entrySet().stream().collect(
ImmutableSetMultimap.flatteningToImmutableSetMultimap(Entry::getKey, (e) -> e.getValue()
.getSubGradesAsSet().stream().flatMap(CsvGrades::asContextualizedStream)));
final ImmutableTable asTable =
perKey.entries().stream().collect(ImmutableTable.toImmutableTable(Entry::getKey,
(e) -> e.getValue().getCriterion(), Entry::getValue));
LOGGER.debug("From {}, obtained {}, as table {}.", grades, perKey, asTable);
final ImmutableSet allCriteria = asTable.columnKeySet();
final ImmutableSet identityHeadersFromFunction =
keys.stream().flatMap(k -> identityFunction.apply(k).keySet().stream()).distinct()
.collect(ImmutableSet.toImmutableSet());
final ImmutableSet identityHeaders =
identityHeadersFromFunction.isEmpty() ? ImmutableSet.of("") : identityHeadersFromFunction;
final ImmutableList headers =
Streams.concat(identityHeaders.stream(), allCriteria.stream().map(Criterion::getName),
Stream.of("Points")).collect(ImmutableList.toImmutableList());
writer.writeHeaders(headers);
final String firstHeader = identityHeaders.iterator().next();
for (K key : keys) {
final Map identity = identityFunction.apply(key);
identity.entrySet().forEach(e -> writer.addValue(e.getKey(), e.getValue()));
final IGrade grade = grades.get(key);
final ImmutableCollection marks = asTable.row(key).values();
LOGGER.debug("Writing {} and {}.", key, marks);
for (CriterionGradeWeight cgw : marks) {
final Criterion criterion = cgw.getCriterion();
Verify.verify(allCriteria.contains(criterion));
final double pointsScaled = getPointsScaled(cgw, denominator);
writer.addValue(criterion.getName(), formatter.format(pointsScaled));
}
writer.addValue("Points", formatter.format(grade.getPoints() * denominator));
writer.writeValuesToRow();
}
writer.writeEmptyRow();
final ImmutableMap> weightsPerCriterion = asTable.columnKeySet().stream()
.collect(ImmutableMap.toImmutableMap(c -> c, c -> asTable.column(c).values().stream()
.map(CriterionGradeWeight::getWeight).collect(ImmutableSet.toImmutableSet())));
final boolean homogeneousWeights =
weightsPerCriterion.values().stream().allMatch(s -> s.size() == 1);
LOGGER.debug("Writing summary data.");
{
writer.addValue(firstHeader, "Range");
if (homogeneousWeights) {
for (Criterion criterion : allCriteria) {
final double weight = Iterables.getOnlyElement(weightsPerCriterion.get(criterion));
final Range bounds = Range.closed(0d, weight * denominator);
writer.addValue(criterion.getName(), boundsToString(formatter, bounds));
}
}
final Range overallBounds = Range.closed(0d, denominator);
writer.addValue("Points", boundsToString(formatter, overallBounds));
writer.writeValuesToRow();
}
{
writer.addValue(firstHeader, "Upper bound");
if (homogeneousWeights) {
for (Criterion criterion : allCriteria) {
final double weight = Iterables.getOnlyElement(weightsPerCriterion.get(criterion));
writer.addValue(criterion.getName(), formatter.format(weight * denominator));
}
}
writer.addValue("Points", formatter.format(denominator));
writer.writeValuesToRow();
}
{
writer.addValue(firstHeader, "Average");
for (Criterion criterion : allCriteria) {
final double average;
if (homogeneousWeights) {
average = asTable.column(criterion).values().stream()
.collect(Collectors.averagingDouble(c -> getPointsScaled(c, denominator)));
} else {
average = asTable.column(criterion).values().stream()
.collect(Collectors.averagingDouble(c -> c.getGrade().getPoints()));
}
writer.addValue(criterion.getName(), formatter.format(average));
}
final double averageOfTotalScore = weightingGrades.values().stream()
.collect(Collectors.averagingDouble(g -> g.getPoints() * denominator));
writer.addValue("Points", formatter.format(averageOfTotalScore));
writer.writeValuesToRow();
}
{
writer.addValue(firstHeader, "Nb > 0");
for (Criterion criterion : allCriteria) {
final int nb = Math.toIntExact(asTable.column(criterion).values().stream()
.filter(c -> c.getGrade().getPoints() > 0d).count());
writer.addValue(criterion.getName(), formatter.format(nb));
}
final int nb = Math
.toIntExact(weightingGrades.values().stream().filter(g -> g.getPoints() > 0d).count());
writer.addValue("Points", formatter.format(nb));
writer.writeValuesToRow();
}
{
writer.addValue(firstHeader, "Nb MAX");
for (Criterion criterion : allCriteria) {
final int nb = Math.toIntExact(asTable.column(criterion).values().stream()
.filter(c -> c.getGrade().getPoints() == 1d).count());
writer.addValue(criterion.getName(), formatter.format(nb));
}
final int nb = Math
.toIntExact(weightingGrades.values().stream().filter(g -> g.getPoints() == 1d).count());
writer.addValue("Points", formatter.format(nb));
writer.writeValuesToRow();
}
LOGGER.debug("Done writing.");
writer.close();
return stringWriter.toString();
}
public String gradesToCsv(GradeAggregator aggregator,
Map trees) {
final Set keys = trees.keySet();
checkArgument(!keys.isEmpty(), "Can’t determine identity headers with no keys.");
final GenericExam inputExam = new GenericExam<>(aggregator, trees);
final PerCriterionWeightingExam exam = toPerCriterionWeightingExam(inputExam);
final NumberFormat formatter = NumberFormat.getNumberInstance(Locale.ENGLISH);
final StringWriter stringWriter = new StringWriter();
final CsvWriter writer = new CsvWriter(stringWriter, new CsvWriterSettings());
final ImmutableSet identityHeadersFromFunction =
keys.stream().flatMap(k -> identityFunction.apply(k).keySet().stream())
.collect(ImmutableSet.toImmutableSet());
final ImmutableSet identityHeaders =
identityHeadersFromFunction.isEmpty() ? ImmutableSet.of("") : identityHeadersFromFunction;
final ImmutableSet allPaths = exam.allPaths();
final ImmutableList headers =
Streams.concat(identityHeaders.stream(), allPaths.stream().map(CsvGrades::shorten))
.collect(ImmutableList.toImmutableList());
writer.writeHeaders(headers);
final String firstHeader = headers.iterator().next();
for (L key : keys) {
final Map identity = identityFunction.apply(key);
identity.entrySet().forEach(e -> writer.addValue(e.getKey(), e.getValue()));
allPaths.stream().forEach(p -> writer.addValue(CsvGrades.shorten(p),
formatter.format(exam.weight(p) * exam.points(key, p) * denominator)));
writer.writeValuesToRow();
}
writer.writeEmptyRow();
LOGGER.debug("Writing summary data.");
{
writer.addValue(firstHeader, "Upper bound");
allPaths.stream().forEach(p -> writer.addValue(CsvGrades.shorten(p),
formatter.format(exam.weight(p) * denominator)));
writer.writeValuesToRow();
}
{
writer.addValue(firstHeader, "Average");
allPaths.stream().forEach(p -> writer.addValue(CsvGrades.shorten(p),
formatter.format(exam.weight(p) * exam.averagePoints(p) * denominator)));
writer.writeValuesToRow();
}
{
writer.addValue(firstHeader, "Nb ≠ 0");
allPaths.stream().forEach(p -> writer.addValue(CsvGrades.shorten(p),
formatter.format(exam.points(p).filter(v -> v != 0d).count())));
writer.writeValuesToRow();
}
{
writer.addValue(firstHeader, "Nb MAX");
allPaths.stream().forEach(p -> writer.addValue(CsvGrades.shorten(p),
formatter.format(exam.points(p).filter(v -> v > 1d - 1e-6d).count())));
writer.writeValuesToRow();
}
LOGGER.debug("Done writing.");
writer.close();
return stringWriter.toString();
}
}