first commit
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
using Core.Entities;
|
||||
|
||||
namespace Core.Calculation;
|
||||
|
||||
public class DataProcessing
|
||||
{
|
||||
public static EventStudentPicks[] GetEventStudentPicks(IList<CompetitiveEvent> events, IList<Student> students)
|
||||
{
|
||||
return
|
||||
students.SelectMany(
|
||||
student => student.RankedEventPicks.Select((e, i) => (e, student, i + 1)))
|
||||
.OrderBy(tuple => tuple.Item3)
|
||||
.ThenByDescending(tuple => tuple.student.Grade + tuple.student.TsaYear)
|
||||
.GroupBy(tuple => tuple.e)
|
||||
.OrderBy(tuples => tuples.Key.Name)
|
||||
.Select(tuples =>
|
||||
new EventStudentPicks(tuples.Key, tuples.Select(tuple => Tuple.Create(tuple.student, tuple.Item3)).ToList())
|
||||
).ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
using System.Diagnostics;
|
||||
using Core.Entities;
|
||||
using Google.OrTools.Sat;
|
||||
using IntVar = Google.OrTools.Sat.IntVar;
|
||||
|
||||
namespace Core.Calculation
|
||||
{
|
||||
public class EventAssigner
|
||||
{
|
||||
private readonly IList<CompetitiveEvent> _events;
|
||||
private readonly IList<Student> _students;
|
||||
private readonly AssignmentParameters _parameters;
|
||||
private readonly int[] _allEvents;
|
||||
private readonly int[] _allStudents;
|
||||
// how many students have picked each event?
|
||||
private readonly int[] _eventPickCounts;
|
||||
private IList<EventAssignment> _preAssigned = new List<EventAssignment>();
|
||||
private IList<EventAssignment> _prohibited = new List<EventAssignment>();
|
||||
private IList<CompetitiveEvent> _droppedEvents;
|
||||
private IList<CompetitiveEvent> _includedEvents;
|
||||
|
||||
public EventAssigner(IList<CompetitiveEvent> events, IList<Student> students, AssignmentParameters parameters)
|
||||
{
|
||||
_events = events;
|
||||
_students = students;
|
||||
_parameters = parameters;
|
||||
_allEvents = Enumerable.Range(0, _events.Count).ToArray();
|
||||
_allStudents = Enumerable.Range(0, _students.Count).ToArray();
|
||||
_eventPickCounts = new int[_allEvents.Length];
|
||||
_preAssigned = new List<EventAssignment>();
|
||||
_droppedEvents = new List<CompetitiveEvent>();
|
||||
for (var i = 0; i < _events.Count; i++)
|
||||
{
|
||||
var e = _events[i];
|
||||
_eventPickCounts[i] = _students.Count(s => s.RankedEventPicks.Contains(e));
|
||||
}
|
||||
}
|
||||
|
||||
public void AssignToEvent(EventAssignment preAssigned)
|
||||
{
|
||||
_preAssigned.Add(preAssigned);
|
||||
}
|
||||
|
||||
|
||||
public void ExcludeFromEvent(EventAssignment prohibited)
|
||||
{
|
||||
_prohibited.Add(prohibited);
|
||||
}
|
||||
|
||||
public void RemoveEvent(IList<CompetitiveEvent> events)
|
||||
{
|
||||
_droppedEvents = events;
|
||||
}
|
||||
public void IncludedEvents(IList<CompetitiveEvent> events)
|
||||
{
|
||||
_includedEvents = events;
|
||||
}
|
||||
|
||||
public class SolutionPrinter : CpSolverSolutionCallback
|
||||
{
|
||||
private readonly IList<CompetitiveEvent> _events;
|
||||
private readonly IList<Student> _students;
|
||||
private readonly BoolVar[,] _eventAssignment;
|
||||
|
||||
public SolutionPrinter(IList<CompetitiveEvent> events, IList<Student> students, BoolVar[,] eventAssignment)
|
||||
{
|
||||
_events = events;
|
||||
_students = students;
|
||||
_eventAssignment = eventAssignment;
|
||||
}
|
||||
public override void OnSolutionCallback()
|
||||
{
|
||||
Console.WriteLine($"Solution ");
|
||||
foreach (var evt in Enumerable.Range(0,_events.Count))
|
||||
{
|
||||
foreach (var student in Enumerable.Range(0, _students.Count))
|
||||
{
|
||||
if (Value(_eventAssignment[evt, student]) == 1L)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Team[] Solve()
|
||||
{
|
||||
|
||||
// Model.
|
||||
var model = new CpModel();
|
||||
|
||||
// Variables.
|
||||
var x = new BoolVar[_allEvents.Length, _allStudents.Length];
|
||||
foreach (var e in _allEvents)
|
||||
foreach (var s in _allStudents)
|
||||
x[e, s] = model.NewBoolVar($"eventAssignments[{e},{s}]");
|
||||
|
||||
foreach (var preAssignment in _preAssigned)
|
||||
{
|
||||
var e = _events.IndexOf(preAssignment.Event);
|
||||
var s = _students.IndexOf(preAssignment.Student);
|
||||
model.AddAssumption(x[e, s]);
|
||||
}
|
||||
|
||||
foreach (var prohibit in _prohibited)
|
||||
{
|
||||
var e = _events.IndexOf(prohibit.Event);
|
||||
var s = _students.IndexOf(prohibit.Student);
|
||||
|
||||
var prohibitVar = new List<IntVar> { x[e, s] };
|
||||
|
||||
model.AddLinearConstraint(LinearExpr.Sum(prohibitVar), 0, 0);
|
||||
prohibitVar.Clear();
|
||||
}
|
||||
|
||||
// Limit the capacity of each event
|
||||
foreach (var e in _allEvents)
|
||||
{
|
||||
var evt = _events[e];
|
||||
var eventPickCounts = _eventPickCounts[e];
|
||||
|
||||
var evtMinTeamSize = evt.MinTeamSize;
|
||||
var evtMaxTeamSize = evt.MaxTeamSize;
|
||||
|
||||
var teamDivs = eventPickCounts / (evtMinTeamSize * 1.0);
|
||||
if (_includedEvents.Contains(evt))
|
||||
teamDivs = 1;
|
||||
|
||||
//var teamsCount = (int)Math.Ceiling(teamDivs);
|
||||
var teamCount = (int)Math.Round(teamDivs);
|
||||
if (teamCount > evt.MaxTeamCountState)
|
||||
teamCount = evt.MaxTeamCountState;
|
||||
|
||||
// limit to one team for group events
|
||||
if (_parameters.LimitTeamsToOne && evt.Format is EventFormat.Team && teamCount > 1) teamCount = 1;
|
||||
|
||||
if (evt.Name == "Tech Bowl")
|
||||
teamCount = 1;
|
||||
|
||||
var eventCapacity = new List<IntVar>();
|
||||
foreach (var s in _allStudents)
|
||||
{
|
||||
eventCapacity.Add(x[e, s]);
|
||||
}
|
||||
|
||||
if (_droppedEvents != null && _droppedEvents.Contains(evt))
|
||||
teamCount = 0;
|
||||
|
||||
var lb = evtMinTeamSize * teamCount;
|
||||
var ub = Math.Min(evtMaxTeamSize * teamCount, _parameters.TeamSizeLimit);
|
||||
model.AddLinearConstraint(LinearExpr.Sum(eventCapacity), lb, ub);
|
||||
Debug.WriteLine($"{evt.Name,30}\t{evt.Format,-10}\t{lb} - {ub}");
|
||||
|
||||
model.Minimize(LinearExpr.Sum(eventCapacity));
|
||||
eventCapacity.Clear();
|
||||
}
|
||||
|
||||
// Limit the number of events a student is assigned
|
||||
foreach (var s in _allStudents)
|
||||
{
|
||||
var student = _students[s];
|
||||
|
||||
var studentCapacity = new List<IntVar>();
|
||||
foreach (var e in _allEvents)
|
||||
{
|
||||
studentCapacity.Add(x[e, s]);
|
||||
}
|
||||
|
||||
model.AddLinearConstraint(LinearExpr.Sum(studentCapacity), _parameters.AssignmentLowerBound, _parameters.AssignmentUpperBound);
|
||||
studentCapacity.Clear();
|
||||
}
|
||||
|
||||
var eventEffortCoefficients = GetEventEffortCoefficients(_events);
|
||||
foreach (var s in _allStudents)
|
||||
{
|
||||
var effortVar = new BoolVar[_allEvents.Length];
|
||||
|
||||
foreach (var e in _allEvents)
|
||||
{
|
||||
effortVar[e] = x[e, s];
|
||||
}
|
||||
var student = _students[s];
|
||||
var experienceOffset = 0;
|
||||
switch (student.TsaYear)
|
||||
{
|
||||
case 1: experienceOffset = 1; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
var ub = _parameters.EffortUpperBound - experienceOffset;
|
||||
var lb = _parameters.EffortLowerBound;
|
||||
if (ub <= lb)
|
||||
lb = ub - 1;
|
||||
|
||||
model.Add(LinearExpr.WeightedSum(effortVar, eventEffortCoefficients) >= lb);
|
||||
model.Add(LinearExpr.WeightedSum(effortVar, eventEffortCoefficients) <= ub);
|
||||
}
|
||||
|
||||
// each student should be assigned at least one on site activity event
|
||||
foreach (var s in _allStudents)
|
||||
{
|
||||
var onSiteActivity = new List<ILiteral>();
|
||||
foreach (var e in _allEvents)
|
||||
{
|
||||
if (_events[e].OnSiteActivity)
|
||||
onSiteActivity.Add(x[e, s]);
|
||||
}
|
||||
|
||||
if (_parameters.RequireOnSite)
|
||||
model.AddAtLeastOne(onSiteActivity);
|
||||
onSiteActivity.Clear();
|
||||
}
|
||||
|
||||
// students should have at maximum one individual event
|
||||
foreach (var s in _allStudents)
|
||||
{
|
||||
var individualEvent = new List<ILiteral>();
|
||||
foreach (var e in _allEvents)
|
||||
{
|
||||
if (_events[e].Format == EventFormat.Individual)
|
||||
individualEvent.Add(x[e, s]);
|
||||
}
|
||||
|
||||
model.AddAtMostOne(individualEvent);
|
||||
individualEvent.Clear();
|
||||
}
|
||||
|
||||
// students should have at maximum regional events
|
||||
foreach (var s in _allStudents)
|
||||
{
|
||||
var regionalEvent = new List<ILiteral>();
|
||||
foreach (var e in _allEvents)
|
||||
{
|
||||
if (_events[e].RegionalEvent)
|
||||
regionalEvent.Add(x[e, s]);
|
||||
}
|
||||
|
||||
if (_parameters.RequireRegional)
|
||||
model.AddLinearConstraint(LinearExpr.Sum(regionalEvent), 1, 2);
|
||||
regionalEvent.Clear();
|
||||
}
|
||||
|
||||
var maximizePicks = LinearExpr.NewBuilder();
|
||||
// optimize student selections
|
||||
foreach (var s in _allStudents)
|
||||
{
|
||||
var eventPickCoefficients = GetEventPickCoefficients(_students[s].RankedEventPicks, _events);
|
||||
|
||||
foreach (var e in _allEvents)
|
||||
{
|
||||
maximizePicks.AddTerm(x[e, s], eventPickCoefficients[e]);
|
||||
}
|
||||
}
|
||||
model.Maximize(maximizePicks);
|
||||
|
||||
var solver = new CpSolver();
|
||||
var cpSolverStatus = solver.Solve(model);
|
||||
|
||||
// print solver status
|
||||
Console.WriteLine($"Solver status: {cpSolverStatus}");
|
||||
|
||||
var eventAssignmentsList = new List<Team>();
|
||||
|
||||
if (cpSolverStatus == CpSolverStatus.Optimal || cpSolverStatus == CpSolverStatus.Feasible)
|
||||
{
|
||||
foreach (var e in _allEvents)
|
||||
{
|
||||
var students = new List<Student>();
|
||||
foreach (var s in _allStudents)
|
||||
{
|
||||
if (solver.BooleanValue(x[e, s]))
|
||||
{
|
||||
students.Add(_students[s]);
|
||||
//Console.WriteLine($"{_events[evt].Name} : {_students[s].Name}");
|
||||
}
|
||||
}
|
||||
if (students.Count > 0)
|
||||
eventAssignmentsList.Add(new Team(_events[e].Name, _events[e], students));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Console.WriteLine("No solution found.");
|
||||
}
|
||||
|
||||
return eventAssignmentsList.ToArray();
|
||||
}
|
||||
|
||||
private long[] GetEventPickCoefficients(IList<CompetitiveEvent> rankedEvents, IEnumerable<CompetitiveEvent> events)
|
||||
{
|
||||
var eventPickCount = rankedEvents.Count;
|
||||
|
||||
return
|
||||
events.Select(e =>
|
||||
{
|
||||
var eventPickIndex = rankedEvents.IndexOf(e);
|
||||
return eventPickIndex switch
|
||||
{
|
||||
0 => eventPickCount + 1,
|
||||
1 => eventPickCount + 0,
|
||||
> 1 => eventPickCount ,
|
||||
_ => 0L
|
||||
};
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
|
||||
private long[] GetEventEffortCoefficients(IEnumerable<CompetitiveEvent> events)
|
||||
{
|
||||
return
|
||||
events.Select(e => e.Name == "Tech Bowl" ? 0 : e.LevelOfEffort.HasValue ? e.LevelOfEffort.Value : 10L).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
using Core.Entities;
|
||||
using Google.OrTools.Sat;
|
||||
|
||||
namespace Core.Calculation;
|
||||
public class TeamScheduler
|
||||
{
|
||||
private readonly IList<Student> _studentObjs;
|
||||
private readonly IList<Team> _teamObjs;
|
||||
|
||||
private readonly int[] _students;
|
||||
private readonly int[] _teams;
|
||||
private readonly int[] _timeSlots;
|
||||
|
||||
private readonly List<Tuple<int,int>> _scheduleSeparateTeams = new ();
|
||||
|
||||
public TeamScheduler(IList<Team> teams, int numTimeSlots)
|
||||
{
|
||||
_teamObjs = teams;
|
||||
_studentObjs = teams.SelectMany(t => t.Students).Distinct().ToList();
|
||||
|
||||
_students = Enumerable.Range(0, _studentObjs.Count).ToArray();
|
||||
_teams = Enumerable.Range(0, _teamObjs.Count).ToArray();
|
||||
_timeSlots = Enumerable.Range(0, numTimeSlots).ToArray();
|
||||
}
|
||||
|
||||
public void ScheduleSeparate(Team team1, Team team2)
|
||||
{
|
||||
var one = _teamObjs.IndexOf(team1);
|
||||
var two = _teamObjs.IndexOf(team2);
|
||||
_scheduleSeparateTeams.Add(Tuple.Create(one,two));
|
||||
}
|
||||
|
||||
public static TeamScheduler CreateInstance(IList<Team> teams, int numTimeSlots)
|
||||
{
|
||||
return new TeamScheduler(teams, numTimeSlots);
|
||||
}
|
||||
|
||||
public IList<Team>[] Solve()
|
||||
{
|
||||
// Model.
|
||||
var model = new CpModel();
|
||||
|
||||
// Data
|
||||
var m = new int[_students.Length,_teams.Length];
|
||||
foreach (var i in _students)
|
||||
foreach (var t in _teams)
|
||||
m[i, t] = _studentObjs[i].Teams.Contains(_teamObjs[t]) ? 1 : 0;
|
||||
|
||||
// Variables.
|
||||
// x - 1 if meeting of team t takes place at time slot s, else 0
|
||||
var x = new IntVar[_teams.Length, _timeSlots.Length];
|
||||
foreach (var t in _teams)
|
||||
foreach (var s in _timeSlots)
|
||||
x[t, s] = model.NewIntVar(0, 1,$"team time slots[{t},{s}]");
|
||||
|
||||
// y - 1 if individual i has meetings at time slot s, 0 otherwise
|
||||
var y = new IntVar[_students.Length, _timeSlots.Length];
|
||||
foreach (var i in _students)
|
||||
foreach (var s in _timeSlots)
|
||||
y[i, s] = model.NewIntVar(0, 1, $"individual time slots[{i},{s}]");
|
||||
|
||||
// each team meets exactly one time
|
||||
foreach (var t in _teams)
|
||||
model.AddLinearConstraint(LinearExpr.Sum(_timeSlots.Select(s => x[t, s])), 1L, 1L);
|
||||
|
||||
// individual must have at least one team meeting at the given time slot to attend
|
||||
foreach (var i in _students)
|
||||
foreach (var s in _timeSlots)
|
||||
model.Add(
|
||||
new BoundedLinearExpression(
|
||||
y[i, s],
|
||||
LinearExpr.Sum(_teams.Select(t => m[i, t] * x[t, s])),
|
||||
false));
|
||||
|
||||
// maximize number of times individuals meet
|
||||
var indTimeSlotVars = LinearExpr.NewBuilder();
|
||||
foreach (var i in _students)
|
||||
foreach (var s in _timeSlots)
|
||||
indTimeSlotVars.Add(y[i, s]);
|
||||
model.Minimize(indTimeSlotVars);
|
||||
|
||||
foreach (var ts in _scheduleSeparateTeams)
|
||||
foreach (var s in _timeSlots)
|
||||
model.Add(x[ts.Item1, s] != x[ts.Item2, s]);
|
||||
//model.Add(
|
||||
// new BoundedLinearExpression(
|
||||
// x[ts.Item1, s],
|
||||
// x[ts.Item2, s],
|
||||
// false));
|
||||
|
||||
var solver = new CpSolver();
|
||||
|
||||
var cpSolverStatus = solver.Solve(model);
|
||||
Console.WriteLine($"Solver status: {cpSolverStatus}");
|
||||
|
||||
if (cpSolverStatus is CpSolverStatus.Optimal or CpSolverStatus.Feasible)
|
||||
{
|
||||
Console.WriteLine($"Total cost: {solver.ObjectiveValue}\n");
|
||||
|
||||
//foreach (var t in _teams)
|
||||
//foreach (var s in _timeSlots)
|
||||
//{
|
||||
// if (solver.Value(x[t, s]) > 0)
|
||||
// Console.WriteLine($"{_teamObjs[t].Name} : {s}");
|
||||
//}
|
||||
|
||||
//foreach (var i in _students)
|
||||
//foreach (var s in _timeSlots)
|
||||
//{
|
||||
// if (solver.Value(y[i, s]) > 0)
|
||||
// Console.WriteLine($"{_studentObjs[i].Name} : {s}");
|
||||
//}
|
||||
|
||||
var timeSlotTeams = new List<Team>[_timeSlots.Length];
|
||||
foreach (var s in _timeSlots)
|
||||
{
|
||||
var teams = new List<Team>();
|
||||
foreach (var t in _teams)
|
||||
{
|
||||
if (solver.Value(x[t, s]) > 0)
|
||||
{
|
||||
teams.Add(_teamObjs[t]);
|
||||
}
|
||||
}
|
||||
timeSlotTeams[s] = teams;
|
||||
}
|
||||
return timeSlotTeams;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Console.WriteLine("No solution found.");
|
||||
return Array.Empty<IList<Team>>();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using Core.Entities;
|
||||
|
||||
namespace Core.Calculation;
|
||||
public class TeamScheduler_DecisionTree
|
||||
{
|
||||
private readonly IList<Student> _students;
|
||||
private readonly IList<Team> _teams;
|
||||
|
||||
private readonly int _timeSlotCount;
|
||||
|
||||
public TeamScheduler_DecisionTree(IList<Team> teams, int timeSlotCount)
|
||||
{
|
||||
_timeSlotCount = timeSlotCount;
|
||||
_teams = teams;
|
||||
_students = teams.SelectMany(t => t.Students).Distinct().ToList();
|
||||
}
|
||||
|
||||
public IList<Team>[] Solve()
|
||||
{
|
||||
var timeSlots = new IList<Team>[_timeSlotCount];
|
||||
for (var i = 0; i < _timeSlotCount; i++)
|
||||
timeSlots[i] = new List<Team>();
|
||||
|
||||
foreach (var team in _teams.OrderByDescending(t => t.Students.Count))
|
||||
{
|
||||
// get overlapping students in each timeslot
|
||||
var overlaps
|
||||
= (from tsi in Enumerable.Range(0, timeSlots.Length)
|
||||
let ts = timeSlots[tsi]
|
||||
let tss = ts.SelectMany(t => t.Students).Distinct()
|
||||
select Tuple.Create(tsi, team.Students.Count(tss.Contains)))
|
||||
.OrderBy(t => t.Item2)
|
||||
.ThenBy(t => timeSlots[t.Item1].Count);
|
||||
|
||||
timeSlots[overlaps.First().Item1].Add(team);
|
||||
}
|
||||
return timeSlots;
|
||||
}
|
||||
|
||||
public IList<Team>[] SolveRecursive()
|
||||
{
|
||||
// initialize time slots
|
||||
var timeSlots = new IList<Team>[_timeSlotCount];
|
||||
for (var i = 0; i < _timeSlotCount; i++)
|
||||
timeSlots[i] = new List<Team>();
|
||||
|
||||
return
|
||||
(from i in Enumerable.Range(1, 5)
|
||||
let solution = Recursive(timeSlots, _teams, i)
|
||||
let overlapCount = Team.GetStudentTeamOverlapCount(solution)
|
||||
orderby overlapCount
|
||||
select solution).First();
|
||||
}
|
||||
|
||||
private static IList<Team>[] CopyTimeSlots(IList<Team>[] timeSlots)
|
||||
{
|
||||
return timeSlots.Select(ts => (IList<Team>)new List<Team>(ts)).ToArray();
|
||||
}
|
||||
|
||||
private static IList<Team>[] Recursive(IList<Team>[] timeSlots, IList<Team> teams, int overlapTriesStudents = 4)
|
||||
{
|
||||
if (!teams.Any())
|
||||
return timeSlots;
|
||||
|
||||
var overlapsTries =
|
||||
(from team in teams.OrderByDescending(t => t.Students.Count).Take(overlapTriesStudents)
|
||||
//from ts in timeSlots
|
||||
from tsi in Enumerable.Range(0, timeSlots.Length)
|
||||
let ts = timeSlots[tsi]
|
||||
let tss = ts.SelectMany(t => t.Students)
|
||||
select Tuple.Create(ts, tsi, team, team.Students.Count(tss.Contains)))
|
||||
.OrderBy(t => t.Item4)
|
||||
.ThenBy(t => t.Item1.Count);
|
||||
|
||||
var minOverlaps =
|
||||
(from o in overlapsTries
|
||||
group o by o.Item4
|
||||
into oo
|
||||
orderby oo.Key
|
||||
select oo).First();
|
||||
|
||||
var first = minOverlaps.First();
|
||||
|
||||
var results = new List<Tuple<IList<Team>[], int>>();
|
||||
|
||||
foreach (var minOverlap in minOverlaps)
|
||||
{
|
||||
var timeSlotsAfterAdd = CopyTimeSlots(timeSlots);
|
||||
timeSlotsAfterAdd[first.Item2].Add(first.Item3);
|
||||
var remainingTeams = teams.Where(t => t != first.Item3).ToList();
|
||||
|
||||
var result = Recursive(timeSlotsAfterAdd, remainingTeams);
|
||||
|
||||
results.Add(Tuple.Create(result, Team.GetStudentTeamOverlapCount(result)));
|
||||
if (minOverlap.Item4 == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
return results.OrderByDescending(r => r.Item2).First().Item1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using Core.Entities;
|
||||
using Core.Utility;
|
||||
using Google.OrTools.Sat;
|
||||
namespace Core.Calculation;
|
||||
public class TeamScheduler_Prototype
|
||||
{
|
||||
public void Solve()
|
||||
{
|
||||
// Model.
|
||||
var model = new CpModel();
|
||||
|
||||
// Data
|
||||
int[,] m =
|
||||
{
|
||||
{ 1, 0, 0, 0, 0, 1 }, // 1
|
||||
{ 0, 0, 1, 0, 0, 0 }, // 2
|
||||
{ 0, 0, 1, 0, 1, 0 }, // 3
|
||||
{ 0, 0, 0, 0, 1, 1 }, // 4
|
||||
{ 0, 0, 1, 0, 0, 0 }, // 5
|
||||
{ 1, 0, 1, 0, 0, 0 }, // 6
|
||||
{ 0, 0, 0, 0, 0, 1 }, // 7
|
||||
{ 0, 1, 0, 1, 0, 0 }, // 8
|
||||
{ 0, 0, 0, 0, 0, 0 }, // 9
|
||||
{ 1, 1, 0, 0, 1, 0 }, // 10
|
||||
{ 1, 1, 0, 0, 1, 1 }, // 11
|
||||
{ 0, 0, 0, 0, 1, 1 }, // 12
|
||||
{ 1, 0, 0, 0, 0, 1 }, // 13
|
||||
{ 0, 1, 1, 1, 0, 0 }, // 14
|
||||
{ 1, 1, 0, 0, 0, 0 }, // 15
|
||||
{ 0, 0, 0, 0, 0, 0 }, // 16
|
||||
{ 1, 0, 1, 0, 1, 1 }, // 17
|
||||
{ 0, 0, 0, 1, 0, 0 }, // 18
|
||||
{ 1, 0, 0, 1, 0, 0 }, // 19
|
||||
{ 0, 0, 1, 0, 1, 0 }, // 20
|
||||
};
|
||||
|
||||
// Variables.
|
||||
int[] individuals = Enumerable.Range(0, 20).ToArray();
|
||||
int[] teams = Enumerable.Range(0, 6).ToArray();
|
||||
int[] timeSlots = Enumerable.Range(0, 3).ToArray();
|
||||
|
||||
// x - 1 if meeting of team t takes place at time slot s, else 0
|
||||
var x = new IntVar[teams.Length, timeSlots.Length];
|
||||
foreach (var t in teams)
|
||||
foreach (var s in timeSlots)
|
||||
x[t, s] = model.NewIntVar(0, 1,$"team time slots[{t},{s}]");
|
||||
|
||||
// y - 1 if individual i has meetings at time slot s, 0 otherwise
|
||||
var y = new IntVar[individuals.Length, timeSlots.Length];
|
||||
foreach (var i in individuals)
|
||||
foreach (var s in timeSlots)
|
||||
y[i, s] = model.NewIntVar(0, 1, $"individual time slots[{i},{s}]");
|
||||
|
||||
// each team meets exactly one time
|
||||
foreach (var t in teams)
|
||||
model.AddLinearConstraint(LinearExpr.Sum(timeSlots.Select(s => x[t, s])), 1L, 1L);
|
||||
|
||||
//individual must have at least one team meeting at the given time slot to attend
|
||||
foreach (var i in individuals)
|
||||
foreach (var s in timeSlots)
|
||||
model.Add(
|
||||
new BoundedLinearExpression(
|
||||
y[i, s],
|
||||
LinearExpr.Sum(teams.Select(t => m[i, t] * x[t, s])),
|
||||
false));
|
||||
|
||||
// maximize number of times individuals meet
|
||||
var indTimeSlotVars = LinearExpr.NewBuilder();
|
||||
foreach (var i in individuals)
|
||||
foreach (var s in timeSlots)
|
||||
indTimeSlotVars.Add(y[i, s]);
|
||||
model.Minimize(indTimeSlotVars);
|
||||
|
||||
var solver = new CpSolver();
|
||||
var cpSolverStatus = solver.Solve(model);
|
||||
Console.WriteLine($"Solver status: {cpSolverStatus}");
|
||||
|
||||
if (cpSolverStatus is CpSolverStatus.Optimal or CpSolverStatus.Feasible)
|
||||
{
|
||||
Console.WriteLine($"Total cost: {solver.ObjectiveValue}\n");
|
||||
|
||||
Console.WriteLine("Team's Timeslots");
|
||||
TextUtil.ConsoleWriteTable((t, s) => solver.Value(x[t, s]) > 0, "team", teams, "timeslot", timeSlots);
|
||||
|
||||
Console.WriteLine("Individual's Timeslots");
|
||||
TextUtil.ConsoleWriteTable((i, s) => solver.Value(y[i, s]) > 0, "ind", individuals, "timeslot", timeSlots);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("No solution found.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
using Core.Entities;
|
||||
|
||||
namespace Core.Calculation;
|
||||
|
||||
public enum UnassignedScheduleStrategy
|
||||
{
|
||||
BiggestGroup,
|
||||
IndividualEvents,
|
||||
AnyNotMeetingAlready,
|
||||
LevelOfEffort,
|
||||
Any
|
||||
}
|
||||
|
||||
public class UnassignedStudentScheduler
|
||||
{
|
||||
private readonly IList<Student> _students;
|
||||
private readonly IList<Team> _teams;
|
||||
private readonly IList<Team>[] _timeslots;
|
||||
|
||||
public UnassignedStudentScheduler(IList<Team> teams, IList<Team>[] timeslots)
|
||||
{
|
||||
_teams = teams;
|
||||
_students = teams.SelectMany(t => t.Students).Distinct().ToList();
|
||||
_timeslots = timeslots.Select(ts => ts.Select(t => t.Clone()).ToList()).ToArray();
|
||||
}
|
||||
public static IEnumerable<Student> UnassignedStudents(IList<Student> students, IList<Team> timeSlot)
|
||||
=> students.Where(s => !timeSlot.SelectMany(t => t.Students).Contains(s));
|
||||
|
||||
|
||||
public static IEnumerable<Student>[] UnassignedStudents(IList<Student> students, IList<Team>[] schedule)
|
||||
=> schedule.Select(ts => UnassignedStudents(students, ts)).ToArray();
|
||||
|
||||
|
||||
public IList<Team>[] ScheduleStrategy(UnassignedScheduleStrategy scheduleStrategy)
|
||||
{
|
||||
switch (scheduleStrategy)
|
||||
{
|
||||
case UnassignedScheduleStrategy.BiggestGroup:
|
||||
return ScheduleStrategy(GetAvailableTeams_BiggestGroup);
|
||||
case UnassignedScheduleStrategy.IndividualEvents:
|
||||
return ScheduleStrategy(GetAvailableTeams_Individual);
|
||||
case UnassignedScheduleStrategy.AnyNotMeetingAlready:
|
||||
return ScheduleStrategy(GetAvailableTeams_AnyNotMeetingAlready);
|
||||
case UnassignedScheduleStrategy.Any:
|
||||
return ScheduleStrategy(GetAvailableTeams_Any);
|
||||
case UnassignedScheduleStrategy.LevelOfEffort:
|
||||
return ScheduleStrategy(GetAvailableTeams_LevelOfEffort);
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(scheduleStrategy), scheduleStrategy, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public IList<Team>[] ScheduleStrategy(Func<IEnumerable<Team>, IEnumerable<Student>, IEnumerable<Team>> availableTeamSelector)
|
||||
{
|
||||
// Find stuff for unassigned students in each timeslot
|
||||
var scheduledTeams = _timeslots.SelectMany(list => list).Distinct().ToList();
|
||||
foreach (var slot in _timeslots)
|
||||
{
|
||||
var unassigned = UnassignedStudents(_students, slot).ToList();
|
||||
while (unassigned.Count > 0)
|
||||
{
|
||||
var assignedStudents = slot.SelectMany(t => t.Students).Distinct();
|
||||
|
||||
var availableTeams = availableTeamSelector(scheduledTeams, assignedStudents);
|
||||
|
||||
var teamToAdd = availableTeams.FirstOrDefault();
|
||||
if (teamToAdd == null)
|
||||
break;
|
||||
|
||||
slot.Add(teamToAdd);
|
||||
scheduledTeams.Add(teamToAdd);
|
||||
|
||||
foreach (var student in teamToAdd.Students)
|
||||
unassigned.Remove(student);
|
||||
}
|
||||
}
|
||||
|
||||
return _timeslots;
|
||||
}
|
||||
|
||||
|
||||
// find teams where several unassigned students can work together
|
||||
private IEnumerable<Team> GetAvailableTeams_BiggestGroup(
|
||||
IEnumerable<Team> scheduledTeams, IEnumerable<Student> assignedStudents) =>
|
||||
_teams
|
||||
.Where(t => scheduledTeams.All(st => st.Name != t.Name))
|
||||
.Select(t => t.CloneWithOmittedStudents(assignedStudents))
|
||||
.Where(t => t.Students.Count > 1) //|| t.Event.Format is EventFormat.Individual
|
||||
//.OrderBy(t => scheduledTeams.Count(st => st.Name == t.Name))
|
||||
.OrderByDescending(t => t.Students.Count); // select descending greatest number of students assigned
|
||||
//.ThenBy(t => Random.Shared.Next()); // todo: sort by student historic record of event assignment
|
||||
|
||||
// find individual events unassigned students can work on
|
||||
private IEnumerable<Team> GetAvailableTeams_Individual(
|
||||
IEnumerable<Team> scheduledTeams, IEnumerable<Student> assignedStudents) =>
|
||||
_teams
|
||||
.Where(t => scheduledTeams.All(st => st.Name != t.Name))
|
||||
.Where(t => t.Event.Format == EventFormat.Individual || t.Students.Count == 1)
|
||||
.Select(t => t.CloneWithOmittedStudents(assignedStudents))
|
||||
.Where(t => t.Students.Count > 0);
|
||||
|
||||
// find any unassigned event students can work on
|
||||
private IEnumerable<Team> GetAvailableTeams_AnyNotMeetingAlready(
|
||||
IEnumerable<Team> scheduledTeams, IEnumerable<Student> assignedStudents) =>
|
||||
_teams
|
||||
.Where(t => scheduledTeams.All(st => st.Name != t.Name))
|
||||
.Select(t => t.CloneWithOmittedStudents(assignedStudents))
|
||||
.Where(t => t.Students.Count > 0);
|
||||
|
||||
private IEnumerable<Team> GetAvailableTeams_Any(
|
||||
IEnumerable<Team> scheduledTeams, IEnumerable<Student> assignedStudents) =>
|
||||
_teams
|
||||
.Select(t => t.CloneWithOmittedStudents(assignedStudents))
|
||||
.Where(t => t.Students.Count > 0);
|
||||
|
||||
|
||||
// find teams where several unassigned students can work together
|
||||
private IEnumerable<Team> GetAvailableTeams_LevelOfEffort(
|
||||
IEnumerable<Team> scheduledTeams, IEnumerable<Student> assignedStudents) =>
|
||||
_teams
|
||||
.Where(t => scheduledTeams.All(st => st.Name != t.Name))
|
||||
.Select(t => t.CloneWithOmittedStudents(assignedStudents))
|
||||
.Where(t => t.Students.Count > 1) //|| t.Event.Format is EventFormat.Individual
|
||||
//.OrderBy(t => scheduledTeams.Count(st => st.Name == t.Name))
|
||||
.OrderByDescending(t => t.Event.LevelOfEffort); // select descending greatest number of students assigned
|
||||
|
||||
//add team to another timeslot, if any available
|
||||
public IList<Team>[] AddAdditionalTimeSlot(Team team)
|
||||
{
|
||||
// sort by how many teammembers are already in that timeslot, descending
|
||||
foreach (var timeslot in _timeslots.OrderBy(ts => ts.SelectMany(t => t.Students).Count(team.Students.Contains)))
|
||||
{
|
||||
if (timeslot.Any(t => t.Name == team.Name))
|
||||
continue;
|
||||
timeslot.Add(team);
|
||||
break;
|
||||
}
|
||||
|
||||
return _timeslots;
|
||||
}
|
||||
|
||||
// Keep the current events rolling for the other time slots
|
||||
public IList<Team>[] ExtendEvents()
|
||||
{
|
||||
var scheduledTeams = _timeslots.SelectMany(list => list).Distinct().ToList();
|
||||
|
||||
// get all the students in each time slot
|
||||
var timeslotStudents = _timeslots.Select(ts => ts.SelectMany(t => t.Students).Distinct()).ToArray();
|
||||
|
||||
// clone teams from timeslot for each other timeslot, removing the students
|
||||
|
||||
//var copiedTeams = new Dictionary<int, IList<Team>>();
|
||||
|
||||
for (var i = 0; i < _timeslots.Length; i++)
|
||||
{
|
||||
var sourceTimeslot = _timeslots[i];
|
||||
|
||||
for (var j = 0; j < _timeslots.Length; j++)
|
||||
{
|
||||
if (i == j)
|
||||
continue;
|
||||
var targetTimeslot = _timeslots[j];
|
||||
var targetTimeslotStudents = targetTimeslot.SelectMany(t => t.Students).Distinct();
|
||||
var clonedTeams = sourceTimeslot.Select(t => t.CloneWithOmittedStudents(targetTimeslotStudents));
|
||||
foreach (var clonedTeam in clonedTeams.Where(t => t.Students.Any()))
|
||||
{
|
||||
targetTimeslot.Add(clonedTeam);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _timeslots;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CsvHelper" Version="30.0.1" />
|
||||
<PackageReference Include="FuzzySharp" Version="2.0.2" />
|
||||
<PackageReference Include="Google.OrTools" Version="9.7.2996" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Core.Entities;
|
||||
|
||||
public class AssignmentAssumption(CompetitiveEvent @event, Student student, Assumption assumption)
|
||||
{
|
||||
public CompetitiveEvent Event { get; } = @event;
|
||||
public Student Student { get; } = student;
|
||||
public Assumption Assumption { get; } = assumption;
|
||||
|
||||
public EventAssignment EventAssignment => new(@event, student);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace Core.Entities
|
||||
{
|
||||
public class AssignmentParameters(
|
||||
int effortLowerBound = 6,
|
||||
int effortUpperBound = 8,
|
||||
int assignmentLowerBound = 2,
|
||||
int assignmentUpperBound = 4,
|
||||
int teamSizeLimit = 4,
|
||||
bool limitTeamsToOne = true,
|
||||
bool requireRegional = true,
|
||||
bool requireOnSite = true)
|
||||
{
|
||||
public int EffortLowerBound { get; set; } = effortLowerBound;
|
||||
public int EffortUpperBound { get; set; } = effortUpperBound;
|
||||
public int AssignmentLowerBound { get; set; } = assignmentLowerBound;
|
||||
public int AssignmentUpperBound { get; set; } = assignmentUpperBound;
|
||||
public int TeamSizeLimit { get; set; } = teamSizeLimit;
|
||||
public bool LimitTeamsToOne { get; set; } = limitTeamsToOne;
|
||||
public bool RequireRegional { get; set; } = requireRegional;
|
||||
public bool RequireOnSite { get; set; } = requireOnSite;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Core.Entities;
|
||||
|
||||
public enum Assumption
|
||||
{
|
||||
Include,
|
||||
Exclude
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
namespace Core.Entities;
|
||||
|
||||
public class CompetitiveEvent
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string ShortName { get; set; }
|
||||
public EventFormat Format { get; set; }
|
||||
public int MinTeamSize { get; set; }
|
||||
public int MaxTeamSize { get; set; }
|
||||
|
||||
public string TeamSize =>
|
||||
MinTeamSize == MaxTeamSize
|
||||
? MinTeamSize.ToString()
|
||||
: $"{MinTeamSize.ToString()}-{MaxTeamSize.ToString()}";
|
||||
|
||||
public string SemifinalistActivity { get; set; }
|
||||
|
||||
public bool InterviewOrPresentation
|
||||
=> SemifinalistActivity.Contains("Interview") || SemifinalistActivity.Contains("Presentation");
|
||||
|
||||
public bool OnSiteActivity
|
||||
=> SemifinalistActivity.Contains("Challenge")
|
||||
|| SemifinalistActivity.Contains("Race")
|
||||
|| SemifinalistActivity.Contains("Speech")
|
||||
|| SemifinalistActivity.Contains("Test")
|
||||
|| SemifinalistActivity.Contains("Flight")
|
||||
|| SemifinalistActivity.Contains("Debate")
|
||||
|| SemifinalistActivity.Contains("Photography")
|
||||
|| SemifinalistActivity.Contains("Build")
|
||||
|| Name.Contains("Chapter")
|
||||
|| Name.Contains("Essay")
|
||||
|| SemifinalistActivity.Contains("Fly");
|
||||
|
||||
public string RegionalNotes { get; set; }
|
||||
|
||||
public int MaxTeamCountState { get; set; }
|
||||
public bool RegionalEvent { get; set; }
|
||||
|
||||
public bool RegionalPresubmit { get; set; }
|
||||
public bool StatePresubmission { get; set; }
|
||||
public bool StatePretesting { get; set; }
|
||||
public bool StatePreliminaryRound { get; set; }
|
||||
|
||||
public string Documentation { get; set; }
|
||||
|
||||
public string Eligibility { get; set; }
|
||||
public string Theme { get; set; }
|
||||
public string Description { get; set; }
|
||||
public int? LevelOfEffort { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
|
||||
public static readonly CompetitiveEvent GeneralSchedule = new(){Name = "General Schedule"};
|
||||
public static readonly CompetitiveEvent VotingDelegates = new(){Name = "Voting Delegates"};
|
||||
|
||||
|
||||
public string EventAttributes ()
|
||||
{
|
||||
var st = new List<string>();
|
||||
|
||||
if (Format is EventFormat.Individual)
|
||||
st.Add( "Ind.");
|
||||
if (RegionalEvent)
|
||||
st.Add( "Reg.");
|
||||
|
||||
return string.Join(", ", st);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Core.Entities;
|
||||
|
||||
public class EventAssignment
|
||||
{
|
||||
public CompetitiveEvent Event { get; }
|
||||
public Student Student { get; }
|
||||
|
||||
public EventAssignment(CompetitiveEvent @event, Student student)
|
||||
{
|
||||
Event = @event;
|
||||
Student = student;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Core.Entities;
|
||||
|
||||
public enum EventFormat
|
||||
{
|
||||
Team,
|
||||
Individual
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
namespace Core.Entities
|
||||
{
|
||||
public class EventOccurrence
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Time { get; set; }
|
||||
public string Date { get; set; }
|
||||
public DateTime StartTime { get; set; }
|
||||
public DateTime? EndTime { get; set; }
|
||||
public string Location { get; set; }
|
||||
|
||||
public bool SignupSubmitPickup =>
|
||||
Name.Contains("Sign-up") ||
|
||||
Name.Contains("Submit") ||
|
||||
Name.Contains("Submission") ||
|
||||
Name.Contains("Pick-up");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Core.Entities;
|
||||
|
||||
public class EventStudentPicks
|
||||
{
|
||||
public CompetitiveEvent Event { get; }
|
||||
public IList<Tuple<Student,int>> StudentPicks { get; }
|
||||
|
||||
public EventStudentPicks(CompetitiveEvent @event, IList<Tuple<Student, int>> studentPicks)
|
||||
{
|
||||
Event = @event;
|
||||
StudentPicks = studentPicks;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Core.Entities;
|
||||
|
||||
public class PartialTeam : Team
|
||||
{
|
||||
public IList<Student> OmittedStudents { get; }
|
||||
|
||||
public PartialTeam(string name, CompetitiveEvent @event, IList<Student> students, IList<Student> omittedStudents) : base(name, @event, students)
|
||||
{
|
||||
OmittedStudents = omittedStudents;
|
||||
}
|
||||
|
||||
public override Team CloneWithOmittedStudents(IEnumerable<Student> studentsToOmit)
|
||||
{
|
||||
var remainingStudents = Students.Where(s => !studentsToOmit.Contains(s)).ToList();
|
||||
var omittedStudents = OmittedStudents.Union(Students.Where(studentsToOmit.Contains)).Distinct().ToList();
|
||||
return new PartialTeam(Name, Event, remainingStudents, omittedStudents );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Core.Entities;
|
||||
|
||||
public class Student
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public string LastNameFirstName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Name.Contains(',')) return Name;
|
||||
var match = Regex.Match(Name, @"(.*)\s(.*)");
|
||||
if (match.Success)
|
||||
return $"{match.Groups[2].Value}, {match.Groups[2].Value} ";
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public string FirstNameLastName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!Name.Contains(',')) return Name;
|
||||
var match = Regex.Match(Name, @"(.*),\s*(.*)");
|
||||
if (match.Success)
|
||||
return $"{match.Groups[2].Value} {match.Groups[1].Value}";
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
|
||||
public string FirstName
|
||||
{
|
||||
get
|
||||
{
|
||||
var match = Regex.Match(LastNameFirstName, @"(.*),\s*(.*)");
|
||||
if (match.Success)
|
||||
return $"{match.Groups[2].Value}";
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
|
||||
public int Grade { get; }
|
||||
public string StateID { get; }
|
||||
public string RegionalID { get; }
|
||||
public string NationalID { get; }
|
||||
public int TsaYear { get; }
|
||||
public string Officer { get; }
|
||||
public IList<CompetitiveEvent> RankedEventPicks { get; }
|
||||
|
||||
public ICollection<Team> Teams { get; set; }
|
||||
|
||||
public Student(string name, int grade, int tsaYear, string officer, IList<CompetitiveEvent> rankedEventPicks,
|
||||
string stateID, string regionalID, string nationalId)
|
||||
{
|
||||
Name = name;
|
||||
Grade = grade;
|
||||
TsaYear = tsaYear;
|
||||
Officer = officer;
|
||||
RankedEventPicks = rankedEventPicks;
|
||||
StateID=stateID;
|
||||
RegionalID = regionalID;
|
||||
NationalID = nationalId;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return FirstName;
|
||||
}
|
||||
|
||||
public bool VotingDelegate => Officer.Contains("Pres");
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Core.Entities;
|
||||
|
||||
public class Team
|
||||
{
|
||||
public string Name { get; }
|
||||
public CompetitiveEvent Event { get; }
|
||||
public IList<Student> Students { get; }
|
||||
|
||||
public Student? Captain { get; set; }
|
||||
|
||||
public string TeamNumber { get; set; }
|
||||
|
||||
public string RegionalTimeSlot { get; set; }
|
||||
|
||||
public Tuple<DateTime,DateTime?>? RegionalTimeSlotObj
|
||||
{
|
||||
get
|
||||
{
|
||||
|
||||
if (string.IsNullOrEmpty(RegionalTimeSlot))
|
||||
return null;
|
||||
var times = Regex.Matches(RegionalTimeSlot, @"(.*)\s*-\s*(.*)");
|
||||
if (times.Count == 0)
|
||||
return Tuple.Create(P(RegionalTimeSlot), (DateTime?)null);
|
||||
var match = times[0];
|
||||
if (!match.Success)
|
||||
return Tuple.Create(P(RegionalTimeSlot), (DateTime?)null);
|
||||
|
||||
return Tuple.Create(P(match.Groups[1].Value), (DateTime?)P(match.Groups[2].Value));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private DateTime P(string s)
|
||||
{
|
||||
var dt = DateTime.Parse(s);
|
||||
if (dt.TimeOfDay < TimeSpan.FromHours(7))
|
||||
return dt + TimeSpan.FromHours(12);
|
||||
return dt;
|
||||
}
|
||||
|
||||
public Team(string name, CompetitiveEvent @event, IList<Student> students, Student? captain = null, string teamNumber = null, string regionalTimeSlot = null)
|
||||
{
|
||||
Name = name;
|
||||
Event = @event;
|
||||
Students = students;
|
||||
Captain = captain;
|
||||
TeamNumber = teamNumber;
|
||||
RegionalTimeSlot = regionalTimeSlot;
|
||||
}
|
||||
|
||||
public virtual Team CloneWithOmittedStudents(IEnumerable<Student> studentsToOmit)
|
||||
{
|
||||
var studentsToOmitList = studentsToOmit.ToList();
|
||||
var omittedStudents = Students.Where(studentsToOmitList.Contains).ToList();
|
||||
if (!omittedStudents.Any())
|
||||
return new Team(Name, Event, Students.ToList(), Captain);
|
||||
|
||||
var remainingStudents = Students.Where(s => !studentsToOmitList.Contains(s)).ToList();
|
||||
return new PartialTeam(Name, Event, remainingStudents, omittedStudents);
|
||||
}
|
||||
|
||||
public Team Clone() => CloneWithOmittedStudents(Array.Empty<Student>());
|
||||
|
||||
public static int GetStudentTeamOverlapCount(IList<Team>[] timeSlots)
|
||||
{
|
||||
return timeSlots.Sum(GetStudentTeamOverlapCount);
|
||||
}
|
||||
|
||||
private static int GetStudentTeamOverlapCount(IList<Team> timeSlot)
|
||||
{
|
||||
return GetStudentTeamOverlaps(timeSlot).Count();
|
||||
}
|
||||
|
||||
public static IEnumerable<Tuple<Student, IEnumerable<Team>>> GetStudentTeamOverlaps(IList<Team> timeSlot)
|
||||
{
|
||||
return
|
||||
from s in timeSlot.SelectMany(ts => ts.Students).Distinct()
|
||||
group s by timeSlot.Where(t => t.Students.Contains(s))
|
||||
into gs
|
||||
where gs.Key.Count() > 1
|
||||
select Tuple.Create(gs.First(), gs.Key);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
|
||||
public string ToStringWithIndividualAndRegional()
|
||||
{
|
||||
var ind = Event.Format is EventFormat.Individual ? " (Ind.)" : string.Empty;
|
||||
var regional= Event.RegionalEvent ? " (Reg.)" : string.Empty;
|
||||
//var regional= Event.RegionalEvent ? " (Reg.)" : string.Empty;
|
||||
|
||||
var eventAttributes = Event.EventAttributes();
|
||||
return string.IsNullOrEmpty(eventAttributes) ? Name : Name + " (" + eventAttributes + ")";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Core
|
||||
{
|
||||
public class MainClass
|
||||
{
|
||||
static void Main() { }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using Core.Entities;
|
||||
|
||||
namespace Core.Parsers;
|
||||
|
||||
public class AssignmentAssumptionParser : CsvParserBase
|
||||
{
|
||||
public AssignmentAssumptionParser(FileSystemInfo csvFile, bool ignoreBlankLines = true) : base(csvFile, ignoreBlankLines)
|
||||
{
|
||||
}
|
||||
|
||||
public AssignmentAssumption[] Parse(ICollection<CompetitiveEvent> events, ICollection<Student> students)
|
||||
{
|
||||
var assumptions = new List<AssignmentAssumption>();
|
||||
|
||||
CsvReader.Read();
|
||||
CsvReader.ReadHeader();
|
||||
var studentColumns =
|
||||
CsvReader.HeaderRecord.Select(h => h.Trim()).Where(h => !string.IsNullOrEmpty(h)).ToArray();
|
||||
|
||||
var studentArray = studentColumns.Select(c => students.First(s => s.FirstName == c)).ToArray();
|
||||
|
||||
while (CsvReader.Read())
|
||||
{
|
||||
var eventShortName= CsvReader.GetField(0);
|
||||
|
||||
var evt = events.FirstOrDefault(e => e.ShortName == eventShortName);
|
||||
if (evt == null)
|
||||
throw new Exception($"Could not find event named {eventShortName}");
|
||||
for (int i = 0; i <= studentArray.Length; i++)
|
||||
{
|
||||
var field = CsvReader.GetField(i + 1);
|
||||
switch (field)
|
||||
{
|
||||
case "x":
|
||||
case "X":
|
||||
assumptions.Add(new AssignmentAssumption(evt, studentArray[i], Assumption.Exclude));
|
||||
break;
|
||||
case "i":
|
||||
case "I":
|
||||
assumptions.Add(new AssignmentAssumption(evt, studentArray[i], Assumption.Include));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return assumptions.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Globalization;
|
||||
using CsvHelper;
|
||||
using CsvHelper.Configuration;
|
||||
|
||||
namespace Core.Parsers;
|
||||
|
||||
public class CsvParserBase : IDisposable
|
||||
{
|
||||
private readonly StreamReader _reader;
|
||||
//private readonly MemoryStream _memoryStream;
|
||||
protected readonly CsvReader CsvReader;
|
||||
|
||||
protected CsvParserBase(FileSystemInfo csvFile, bool ignoreBlankLines)
|
||||
{
|
||||
_reader = OpenCsv(csvFile);
|
||||
CsvReader = InitCsvReader(_reader, ignoreBlankLines);
|
||||
}
|
||||
|
||||
//protected CsvParserBase(byte[] fileContents, bool ignoreBlankLines)
|
||||
//{
|
||||
// _memoryStream = new MemoryStream(fileContents);
|
||||
// _reader = new StreamReader(_memoryStream);
|
||||
|
||||
// CsvReader = InitCsvReader(_reader, ignoreBlankLines);
|
||||
//}
|
||||
|
||||
private static CsvReader InitCsvReader(TextReader reader, bool ignoreBlankLines)
|
||||
{
|
||||
var csvConfiguration = new CsvConfiguration(CultureInfo.CurrentCulture)
|
||||
{
|
||||
HasHeaderRecord = true,
|
||||
IgnoreBlankLines = ignoreBlankLines,
|
||||
ReadingExceptionOccurred = exception => false,
|
||||
MissingFieldFound = null
|
||||
};
|
||||
|
||||
var csvReader = new CsvReader(reader, csvConfiguration);
|
||||
return csvReader;
|
||||
}
|
||||
|
||||
internal static StreamReader OpenCsv(FileSystemInfo csvFile)
|
||||
{
|
||||
if (!csvFile.Exists)
|
||||
throw new FileNotFoundException($"Cannot find file '{csvFile.Name}'");
|
||||
|
||||
return File.OpenText(csvFile.FullName);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_reader.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Core.Entities;
|
||||
|
||||
namespace Core.Parsers;
|
||||
|
||||
public class EventDefinitionParser : CsvParserBase
|
||||
{
|
||||
public EventDefinitionParser(FileSystemInfo csvFile, bool ignoreBlankLines = true) : base(csvFile, ignoreBlankLines)
|
||||
{
|
||||
}
|
||||
|
||||
public CompetitiveEvent[] Parse()
|
||||
{
|
||||
var events = new List<CompetitiveEvent>();
|
||||
|
||||
CsvReader.Read();
|
||||
CsvReader.ReadHeader();
|
||||
|
||||
while (CsvReader.Read())
|
||||
{
|
||||
var name = CsvReader.GetField("Event");
|
||||
if (string.IsNullOrEmpty(name))
|
||||
continue;
|
||||
var shortName = CsvReader.GetField("Short Name");
|
||||
|
||||
Enum.TryParse(CsvReader.GetField("Format"), out EventFormat format);
|
||||
|
||||
var teamSize = CsvReader.GetField("Team Size");
|
||||
if (string.IsNullOrEmpty(teamSize))
|
||||
throw new ArgumentException(@"Team Size is null for {name}");
|
||||
var match = Regex.Match(teamSize, @"(\d)(?:\s?to\s?)?(\d)?");
|
||||
var min = int.Parse(match.Groups[1].Captures[0].Value);
|
||||
var max = match.Groups[2].Success ? int.Parse(match.Groups[2].Captures[0].Value) : min;
|
||||
|
||||
var stateTeams = CsvReader.GetField<int>("State Count");
|
||||
var semifinalistActivity = CsvReader.GetField("Semifinalist Activity");
|
||||
var regionalCount = CsvReader.GetField("Regional Count");
|
||||
var regionalPresubmit = CsvReader.GetField("Regional Presubmission");
|
||||
var statePresubmission = CsvReader.GetField("State Presubmission");
|
||||
var statePretesting = CsvReader.GetField("State Pretesting");
|
||||
var statePreliminary = CsvReader.GetField("State Preliminary Round");
|
||||
var regionalNotes = CsvReader.GetField("Regional Notes");
|
||||
var documentation = CsvReader.GetField("Documentation");
|
||||
var eligibility = CsvReader.GetField("Eligibility");
|
||||
var theme = CsvReader.GetField("Theme");
|
||||
var description = CsvReader.GetField("Description");
|
||||
var levelOfEffort = CsvReader.GetField<int?>("Level of Effort");
|
||||
//var regionalTeams = CsvReader.GetField<int>("Regional Teams");
|
||||
|
||||
var competitiveEvent = new CompetitiveEvent
|
||||
{
|
||||
Name = name.Trim(),
|
||||
ShortName = shortName.Trim(),
|
||||
Format = format,
|
||||
MaxTeamCountState = stateTeams,
|
||||
MinTeamSize = min,
|
||||
MaxTeamSize = max,
|
||||
SemifinalistActivity = semifinalistActivity,
|
||||
RegionalEvent = !string.IsNullOrEmpty(regionalCount),
|
||||
RegionalPresubmit = regionalPresubmit.Trim() == "TRUE",
|
||||
RegionalNotes = regionalNotes,
|
||||
Documentation= documentation,
|
||||
StatePresubmission = statePresubmission.Trim() == "TRUE",
|
||||
StatePretesting = statePretesting.Trim() == "TRUE",
|
||||
StatePreliminaryRound = statePreliminary.Trim() == "TRUE",
|
||||
Eligibility = eligibility,
|
||||
Theme = theme,
|
||||
Description = description,
|
||||
LevelOfEffort = levelOfEffort
|
||||
};
|
||||
events.Add(competitiveEvent);
|
||||
}
|
||||
|
||||
return events.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Core.Entities;
|
||||
using FuzzySharp;
|
||||
|
||||
namespace Core.Parsers;
|
||||
|
||||
public class EventOccurrenceParser
|
||||
{
|
||||
private FileSystemInfo _txtFile;
|
||||
private ICollection<CompetitiveEvent> _events;
|
||||
|
||||
public EventOccurrenceParser(FileSystemInfo txtFile, ICollection<CompetitiveEvent> events)
|
||||
{
|
||||
_events = events;
|
||||
_txtFile = txtFile;
|
||||
}
|
||||
|
||||
private Regex _re =
|
||||
new (
|
||||
@"" + //
|
||||
@"(?<Name>^[^#].*)\s" +
|
||||
@"(?<Month>February|March|April|May|June|July)\s" +
|
||||
@"(?<DayOfMonth>\d{1,2});?\s" +
|
||||
@"(?<TimeAndLocation>.*)"
|
||||
);
|
||||
|
||||
private readonly Regex _timeRe = new(@"(?<Hour>\d{1,2}):?(?<Minute>\d{2})?\s?(?<APM>(?:a|p)\.?m\.?)");
|
||||
|
||||
private readonly Regex _timeLocationRegex = new(@"(?<Time>.*(?>[AaPp]\.?[Mm]\.?))(?<Location>[\s\t].*)?");
|
||||
|
||||
public IDictionary<CompetitiveEvent, List<EventOccurrence>> Parse()
|
||||
{
|
||||
var occurrences = new Dictionary<CompetitiveEvent, List<EventOccurrence>>();
|
||||
CompetitiveEvent currentEvent = null;
|
||||
|
||||
var lines = File.ReadLines(_txtFile.FullName);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var match = _re.Match(line);
|
||||
if (!match.Success)
|
||||
{
|
||||
if (line.Contains("MS"))
|
||||
{
|
||||
var evt =
|
||||
(from e in _events
|
||||
let rat = Fuzz.Ratio(e.Name, line.Trim())
|
||||
where rat > 50
|
||||
orderby rat descending
|
||||
select e).FirstOrDefault();
|
||||
if (evt == null)
|
||||
continue;
|
||||
currentEvent = evt;
|
||||
continue;
|
||||
}
|
||||
if (line == "General Schedule")
|
||||
{
|
||||
currentEvent = CompetitiveEvent.GeneralSchedule;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line == "Voting Delegates")
|
||||
{
|
||||
currentEvent = CompetitiveEvent.VotingDelegates;
|
||||
continue;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentEvent == null)
|
||||
continue;
|
||||
|
||||
var occurrenceName = match.Groups["Name"].Captures[0].Value;
|
||||
var month = match.Groups["Month"].Captures[0].Value;
|
||||
var dayOfMonth = match.Groups["DayOfMonth"].Captures[0].Value;
|
||||
var timeAndLocation = match.Groups["TimeAndLocation"].Captures[0].Value;
|
||||
|
||||
|
||||
occurrenceName = Regex.Replace(occurrenceName,
|
||||
@"(?<Weekday>Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday),\s?$", "").Trim();
|
||||
|
||||
|
||||
timeAndLocation = SanitizeInput(timeAndLocation);
|
||||
var timeAndLocationMatch = _timeLocationRegex.Match(timeAndLocation);
|
||||
|
||||
var time = timeAndLocation;
|
||||
var location = string.Empty;
|
||||
|
||||
if (timeAndLocationMatch.Success)
|
||||
{
|
||||
time= timeAndLocationMatch.Groups["Time"].Captures[0].Value;
|
||||
if (timeAndLocationMatch.Groups["Location"].Success)
|
||||
location = timeAndLocationMatch.Groups["Location"].Captures[0].Value;
|
||||
}
|
||||
|
||||
var startDate = ParseDate(month, dayOfMonth, DateTime.Now.Year);
|
||||
var startTime = ParseStartTime(time);
|
||||
var t = new DateTime(startDate, startTime);
|
||||
|
||||
var eventOccurrence = new EventOccurrence
|
||||
{
|
||||
Name = occurrenceName, StartTime = t, Time = $"{time}", Date = $"{month} {dayOfMonth}",
|
||||
Location = location
|
||||
};
|
||||
|
||||
if (!occurrences.ContainsKey(currentEvent))
|
||||
occurrences.Add(currentEvent, []);
|
||||
occurrences[currentEvent].Add(eventOccurrence);
|
||||
}
|
||||
|
||||
return occurrences;
|
||||
}
|
||||
|
||||
private string SanitizeInput(string input)
|
||||
{
|
||||
|
||||
input = input.Replace("–", "-");
|
||||
input = input.Replace("—", "-");
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
private DateOnly ParseDate(string month, string dayOfMonth, int year)
|
||||
{
|
||||
int monthNum = 1;
|
||||
switch (month)
|
||||
{
|
||||
case "February":
|
||||
monthNum = 2;
|
||||
break;
|
||||
case "March":
|
||||
monthNum = 3;
|
||||
break;
|
||||
case "April":
|
||||
monthNum = 4;
|
||||
break;
|
||||
case "May":
|
||||
monthNum = 5;
|
||||
break;
|
||||
case "June":
|
||||
monthNum = 6;
|
||||
break;
|
||||
case "July":
|
||||
monthNum = 7;
|
||||
break;
|
||||
}
|
||||
|
||||
var day = int.Parse(dayOfMonth);
|
||||
return new DateOnly(year, monthNum, day); ;
|
||||
}
|
||||
|
||||
private TimeOnly ParseStartTime(string time)
|
||||
{
|
||||
int hour = 0;
|
||||
int minute = 0;
|
||||
|
||||
// get the part of the time before a timespan
|
||||
if (time.Contains(" - "))
|
||||
{
|
||||
time = time[..time.IndexOf(" - ", StringComparison.Ordinal)];
|
||||
|
||||
}
|
||||
|
||||
if (time == "NOON")
|
||||
hour = 12;
|
||||
else
|
||||
{
|
||||
var timeMatch = _timeRe.Match(time.ToLower());
|
||||
if (timeMatch.Success)
|
||||
{
|
||||
hour = int.Parse(timeMatch.Groups["Hour"].Captures[0].Value);
|
||||
if (timeMatch.Groups["Minute"].Success)
|
||||
{
|
||||
minute = int.Parse(timeMatch.Groups["Minute"].Captures[0].Value);
|
||||
}
|
||||
|
||||
if (timeMatch.Groups["APM"].Captures[0].Value is "p.m." or "pm" && hour < 12)
|
||||
hour += 12;
|
||||
}
|
||||
}
|
||||
|
||||
return new TimeOnly(hour, minute, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using Core.Entities;
|
||||
using FuzzySharp;
|
||||
|
||||
namespace Core.Parsers;
|
||||
|
||||
public class StudentParser : CsvParserBase
|
||||
{
|
||||
public StudentParser(FileSystemInfo csvFile, bool ignoreBlankLines = true) : base(csvFile, ignoreBlankLines)
|
||||
{
|
||||
}
|
||||
|
||||
public Student[] Parse(ICollection<CompetitiveEvent> events)
|
||||
{
|
||||
var s = new List<Student>();
|
||||
|
||||
CsvReader.Read();
|
||||
CsvReader.ReadHeader();
|
||||
|
||||
while (CsvReader.Read())
|
||||
{
|
||||
var name = CsvReader.GetField("Student Name");
|
||||
if (string.IsNullOrEmpty(name))
|
||||
continue;
|
||||
|
||||
var stateID = CsvReader.GetField("State ID").Trim();
|
||||
var regionalID = CsvReader.GetField("Regional ID").Trim();
|
||||
var nationalID = CsvReader.GetField("National ID").Trim();
|
||||
var gr = CsvReader.GetField("Grade");
|
||||
var tsaYearsStr = CsvReader.GetField("TSA year");
|
||||
var tsaYear = int.Parse(tsaYearsStr?[..1] ?? "1");
|
||||
var officer = CsvReader.GetField("Officer");
|
||||
|
||||
var competitiveEvents = new List<CompetitiveEvent>(6);
|
||||
|
||||
for (var i = 1; i <= 6; i++)
|
||||
{
|
||||
var eventName = CsvReader.GetField(i.ToString());
|
||||
if (string.IsNullOrEmpty(eventName) || eventName == "") continue;
|
||||
|
||||
eventName = eventName.Trim();
|
||||
|
||||
if (eventName == "I&I")
|
||||
eventName = "Inventions & Innovations";
|
||||
if (eventName == "Med Tech")
|
||||
eventName = "Medical Technology";
|
||||
if (eventName.StartsWith("Challenging Tech"))
|
||||
eventName = "Challenging Technology Issues";
|
||||
|
||||
var matches =
|
||||
(from e in events
|
||||
let rat = Fuzz.Ratio(e.Name, eventName)
|
||||
where rat > 90
|
||||
orderby rat descending
|
||||
select e).ToList();
|
||||
|
||||
if (!matches.Any())
|
||||
{
|
||||
matches =
|
||||
(from e in events
|
||||
where e.Name.StartsWith(eventName)
|
||||
select e).ToList();
|
||||
}
|
||||
|
||||
var competitiveEvent = matches.FirstOrDefault();
|
||||
if (competitiveEvent == null)
|
||||
{
|
||||
|
||||
//todo: throw new ArgumentException($"Event named '{eventName}' not found");
|
||||
continue;
|
||||
}
|
||||
|
||||
competitiveEvents.Add(competitiveEvent);
|
||||
}
|
||||
|
||||
if (!competitiveEvents.Any())
|
||||
continue;
|
||||
|
||||
var student = new Student(
|
||||
name.Trim(),
|
||||
Convert.ToInt32(gr),
|
||||
tsaYear,
|
||||
officer?.Trim(),
|
||||
competitiveEvents, stateID, regionalID, nationalID);
|
||||
s.Add(student);
|
||||
}
|
||||
|
||||
return s.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
using System.Globalization;
|
||||
using Core.Entities;
|
||||
using CsvHelper;
|
||||
using CsvHelper.Configuration;
|
||||
using FuzzySharp;
|
||||
|
||||
namespace Core.Parsers
|
||||
{
|
||||
public class TeamWriter
|
||||
{
|
||||
private readonly ICollection<Team> _teams;
|
||||
|
||||
public string Filename { get; }
|
||||
|
||||
public TeamWriter(ICollection<Team> teams, string filename)
|
||||
{
|
||||
_teams = teams;
|
||||
Filename = filename;
|
||||
}
|
||||
|
||||
public void Write()
|
||||
{
|
||||
var csvConfiguration = new CsvConfiguration(CultureInfo.CurrentCulture)
|
||||
{
|
||||
HasHeaderRecord = true,
|
||||
};
|
||||
|
||||
using var writer = new StreamWriter(Filename);
|
||||
using var csv = new CsvWriter(writer, csvConfiguration);
|
||||
|
||||
// header
|
||||
csv.WriteField("Team Name");
|
||||
csv.WriteField("Event Name");
|
||||
var max = _teams.Max(t => t.Students.Count);
|
||||
for (var i = 1; i < max + 1; i++)
|
||||
{
|
||||
csv.WriteField($"Student {i}");
|
||||
}
|
||||
|
||||
foreach (var team in _teams)
|
||||
{
|
||||
csv.WriteField(team.Name);
|
||||
csv.WriteField(team.Event.Name);
|
||||
foreach (var teamStudent in team.Students)
|
||||
{
|
||||
csv.WriteField(teamStudent.Name);
|
||||
}
|
||||
csv.NextRecord();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class TeamParser : CsvParserBase
|
||||
{
|
||||
public TeamParser(FileSystemInfo csvFile, bool ignoreBlankLines = true) : base(csvFile, ignoreBlankLines)
|
||||
{
|
||||
}
|
||||
|
||||
public Team[] Parse(ICollection<CompetitiveEvent> events, ICollection<Student> students)
|
||||
{
|
||||
var teams = new List<Team>();
|
||||
|
||||
CsvReader.Read();
|
||||
CsvReader.ReadHeader();
|
||||
|
||||
while (CsvReader.Read())
|
||||
{
|
||||
|
||||
var eventName
|
||||
= CsvReader.GetField("Event Name");
|
||||
|
||||
if (string.IsNullOrEmpty(eventName))
|
||||
continue;
|
||||
|
||||
if (eventName.StartsWith("Team Size"))
|
||||
continue;
|
||||
|
||||
eventName = eventName.Replace("ᵃ", string.Empty);
|
||||
eventName = eventName.Replace("ⁱ", string.Empty);
|
||||
eventName = eventName.TrimEnd();
|
||||
|
||||
var teamName = CsvReader.GetField("Team Name");
|
||||
if (string.IsNullOrEmpty(teamName))
|
||||
teamName = eventName;
|
||||
|
||||
eventName = eventName.Trim();
|
||||
|
||||
var @event =
|
||||
(from e in events
|
||||
let rat = Fuzz.Ratio(e.Name, eventName)
|
||||
where rat > 50
|
||||
orderby rat descending
|
||||
select e).FirstOrDefault();
|
||||
|
||||
if (@event == null)
|
||||
continue;
|
||||
|
||||
var regionalTimeSlot = CsvReader.GetField("Regional Time Slot");
|
||||
if (!string.IsNullOrEmpty(regionalTimeSlot))
|
||||
regionalTimeSlot.Trim();
|
||||
|
||||
var teamStudents = new List<Student>();
|
||||
Student? captain = null;
|
||||
|
||||
for (var i = 1; i <= 9; i++)
|
||||
{
|
||||
var studentName = CsvReader.GetField($"Student {i}");
|
||||
if (string.IsNullOrEmpty(studentName)) continue;
|
||||
|
||||
studentName = studentName.Trim();
|
||||
|
||||
if (studentName == "?")
|
||||
continue;
|
||||
|
||||
var studentMatches =
|
||||
from s in students
|
||||
let rat = new[]
|
||||
{
|
||||
Fuzz.Ratio(s.Name, studentName),
|
||||
Fuzz.Ratio(s.FirstNameLastName, studentName),
|
||||
Fuzz.Ratio(s.FirstName, studentName)
|
||||
}.Max()
|
||||
where rat > 90
|
||||
orderby rat descending
|
||||
select s;
|
||||
|
||||
var student = studentMatches.FirstOrDefault();
|
||||
if (student == null)
|
||||
{
|
||||
//continue;
|
||||
throw new ArgumentException($"Student named '{studentName}' not found");
|
||||
}
|
||||
|
||||
teamStudents.Add(student);
|
||||
if (i == 1)
|
||||
captain = student;
|
||||
}
|
||||
|
||||
var teamNumber = string.Empty;
|
||||
if (teamName.EndsWith("Team 2"))
|
||||
teamNumber = "12227-2";
|
||||
else if (@event.Format == EventFormat.Team)
|
||||
teamNumber = "2227";
|
||||
|
||||
if (teamStudents.Count > 0)
|
||||
{
|
||||
if (@event.Format is EventFormat.Team)
|
||||
{
|
||||
teams.Add(new Team(teamName, @event, teamStudents, captain, teamNumber,
|
||||
regionalTimeSlot: regionalTimeSlot));
|
||||
}
|
||||
else if (@event.Format is EventFormat.Individual)
|
||||
{
|
||||
foreach (var student in teamStudents)
|
||||
{
|
||||
teams.Add(new Team($"{teamName} - {student.FirstName}", @event,
|
||||
new List<Student> { student }, student, teamNumber, regionalTimeSlot));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return teams.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace Core.Utility;
|
||||
|
||||
public static class FileUtility
|
||||
{
|
||||
public static FileInfo GetContentFile(string contentDirectory, string fileName, bool useAssemblyDirectory = false)
|
||||
{
|
||||
string basePath;
|
||||
//if (useAssemblyDirectory)
|
||||
// basePath = AssemblyDirectory;
|
||||
//else
|
||||
basePath = AppDomain.CurrentDomain.BaseDirectory;
|
||||
var path = Path.Combine(basePath, contentDirectory);
|
||||
return new FileInfo(path + fileName);
|
||||
}
|
||||
|
||||
//public static string AssemblyDirectory
|
||||
//{
|
||||
// get
|
||||
// {
|
||||
// var codeBase = Assembly.GetExecutingAssembly().Location;
|
||||
// var uri = new UriBuilder(codeBase);
|
||||
// var path = Uri.UnescapeDataString(uri.Path);
|
||||
// return Path.GetDirectoryName(path);
|
||||
// }
|
||||
//}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
namespace Core.Utility;
|
||||
|
||||
public static class TextUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the ordinal value of positive integers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only works for english-based cultures.
|
||||
/// Code from: http://stackoverflow.com/questions/20156/is-there-a-quick-way-to-create-ordinals-in-c/31066#31066
|
||||
/// With help: http://www.wisegeek.com/what-is-an-ordinal-number.htm
|
||||
/// </remarks>
|
||||
/// <param name="number">The number.</param>
|
||||
/// <returns>Ordinal value of positive integers, or <see cref="int.ToString"/> if less than 1.</returns>
|
||||
/// https://stackoverflow.com/a/620504/99492
|
||||
public static string Ordinal(this int number)
|
||||
{
|
||||
const string TH = "th";
|
||||
string s = number.ToString();
|
||||
|
||||
// Negative and zero have no ordinal representation
|
||||
if (number < 1)
|
||||
{
|
||||
return s;
|
||||
}
|
||||
|
||||
number %= 100;
|
||||
if ((number >= 11) && (number <= 13))
|
||||
{
|
||||
return s + TH;
|
||||
}
|
||||
|
||||
switch (number % 10)
|
||||
{
|
||||
case 1: return s + "st";
|
||||
case 2: return s + "nd";
|
||||
case 3: return s + "rd";
|
||||
default: return s + TH;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void ConsoleWriteTable(
|
||||
Func<int, int, bool> getVal, string rowHeader, int[] rowVars, string colHeader, int[] colVars)
|
||||
{
|
||||
var chl = $" {colHeader} 0".Length;
|
||||
var rhl = $"{rowHeader} 0".Length + 3;
|
||||
Console.Write(new string(' ', rhl));
|
||||
foreach (var c in colVars)
|
||||
{
|
||||
Console.Write($" {colHeader} {c + 1}");
|
||||
}
|
||||
Console.WriteLine();
|
||||
Console.WriteLine();
|
||||
foreach (var r in rowVars)
|
||||
{
|
||||
var rhead = $"{rowHeader} {r + 1}:";
|
||||
Console.Write(rhead.PadRight(rhl));
|
||||
foreach (var c in colVars)
|
||||
{
|
||||
var v = getVal(r, c) ? "1" : " ";
|
||||
Console.Write($"{v.PadLeft(chl)}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
}
|
||||
Console.WriteLine();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user