Add TeamClipboardMatcher utility and corresponding tests for fuzzy team name matching
This commit introduces the TeamClipboardMatcher class, which provides functionality to match team names from clipboard text using fuzzy matching techniques. The class includes methods for extracting team names and finding the best match based on a specified threshold. Additionally, comprehensive unit tests are added in TeamClipboardMatcher_Tests to validate various matching scenarios, including exact matches, fuzzy matches, and handling of different clipboard formats. This enhancement improves the application's ability to efficiently match teams from user input.
This commit is contained in:
@@ -47,46 +47,75 @@
|
||||
<MudText Typo="Typo.h4">Time Slots</MudText>
|
||||
<MudPaper Class="pa-2 ma-2" Elevation="3">
|
||||
<MudGrid>
|
||||
<MudItem xs="6" sm="3" lg="2">
|
||||
<MudNumericField Value="_parameters.TimeSlots"
|
||||
ValueChanged="async (int val) => await OnTimeSlotCountChanged(val)"
|
||||
Label="Time Slots" Min="1" Max="4">
|
||||
</MudNumericField>
|
||||
</MudItem>
|
||||
<MudFlexBreak/>
|
||||
<MudItem xs="12" sm="6" lg="4">
|
||||
<MudTooltip Text="Schedule teams with Level of Effort >= 3" Inline="false">
|
||||
<MudButton Variant="Variant.Outlined" OnClick="AddHighLevelOfEffort" FullWidth="true">Add High Effort</MudButton>
|
||||
</MudTooltip>
|
||||
<MudPaper Elevation="0" Class="pa-1" Style="border: 1px solid var(--mud-palette-lines-default); border-radius: var(--mud-default-borderradius);">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudText Typo="Typo.body2">Time Slots</MudText>
|
||||
<MudNumericField Value="_parameters.TimeSlots"
|
||||
ValueChanged="async (int val) => await OnTimeSlotCountChanged(val)"
|
||||
Label=""
|
||||
Min="1"
|
||||
Max="4"
|
||||
Variant="Variant.Outlined"
|
||||
Style="width: 80px;">
|
||||
</MudNumericField>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" lg="4">
|
||||
<MudButton Variant="Variant.Outlined" OnClick="AddRegionals" FullWidth="true">Add Regionals</MudButton>
|
||||
<AddRemoveFilter Label="High Effort"
|
||||
OnAdd="AddHighLevelOfEffort"
|
||||
OnRemove="RemoveHighLevelOfEffort"
|
||||
AddTooltip="Schedule teams with Level of Effort >= 3"
|
||||
RemoveTooltip="Remove teams with Level of Effort >= 3" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" lg="4">
|
||||
<MudButton Variant="Variant.Outlined" OnClick="RemoveIndividual" FullWidth="true">Remove Individual</MudButton>
|
||||
<AddRemoveFilter Label="Regionals"
|
||||
OnAdd="AddRegionals"
|
||||
OnRemove="RemoveRegionals"
|
||||
AddTooltip="Add regional event teams"
|
||||
RemoveTooltip="Remove regional event teams" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" lg="4">
|
||||
<MudButton Variant="Variant.Outlined" OnClick="RemoveLowLevelOfEffort" FullWidth="true">Remove Low Effort</MudButton>
|
||||
<AddRemoveFilter Label="Individual"
|
||||
OnAdd="AddIndividual"
|
||||
OnRemove="RemoveIndividual"
|
||||
AddTooltip="Add individual event teams"
|
||||
RemoveTooltip="Remove individual event teams" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" lg="4">
|
||||
<AddRemoveFilter Label="Low Effort"
|
||||
OnAdd="AddLowLevelOfEffort"
|
||||
OnRemove="RemoveLowLevelOfEffort"
|
||||
AddTooltip="Add teams with Level of Effort <= 1"
|
||||
RemoveTooltip="Remove teams with Level of Effort <= 1" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" lg="4">
|
||||
<MudButton Variant="Variant.Outlined" OnClick="Invert" FullWidth="true">Invert</MudButton>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6" lg="4">
|
||||
<MudTooltip Text="Load teams from clipboard text by matching team names">
|
||||
<MudButton Variant="Variant.Outlined" OnClick="LoadTeamsFromClipboard" FullWidth="true" Disabled="@_isLoadingClipboard">Load from Clipboard</MudButton>
|
||||
</MudTooltip>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" lg="4">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Warning" OnClick="Reset" FullWidth="true">Reset</MudButton>
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudButton Variant="@(IsDirty() ? Variant.Outlined : Variant.Filled)"
|
||||
Class="ma-3"
|
||||
OnClick="Solve"
|
||||
Color="Color.Primary"
|
||||
Disabled="@_isSolving">
|
||||
Solve
|
||||
</MudButton>
|
||||
<MudTooltip Text="Copy to Clipboard">
|
||||
<MudIconButton OnClick="CopyToClipboard" Icon="@Icons.Material.Filled.ContentCopy"></MudIconButton>
|
||||
</MudTooltip>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudSpacer />
|
||||
<MudTooltip Text="Copy to Clipboard">
|
||||
<MudIconButton OnClick="CopyToClipboard" Icon="@Icons.Material.Filled.ContentCopy"></MudIconButton>
|
||||
</MudTooltip>
|
||||
<MudButton Variant="@(IsDirty() ? Variant.Outlined : Variant.Filled)"
|
||||
OnClick="Solve"
|
||||
Color="Color.Primary"
|
||||
Disabled="@_isSolving">
|
||||
Solve
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
||||
@@ -149,6 +178,7 @@
|
||||
private TeamSchedulerSolution _solution = null!;
|
||||
private TeamSchedulerOptions _parameters = null!;
|
||||
bool _isSolving;
|
||||
private bool _isLoadingClipboard = false;
|
||||
private IEnumerable<Team> _scheduledTeams = [];
|
||||
private IEnumerable<Student> _absentStudents = [];
|
||||
private IEnumerable<Team> _possibleAdditions = [];
|
||||
@@ -195,12 +225,40 @@
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void RemoveHighLevelOfEffort()
|
||||
{
|
||||
var highEffortTeamIds = _teams.Where(t => t.Event.LevelOfEffort >= 3).Select(t => t.Id).ToHashSet();
|
||||
_scheduledTeams = _scheduledTeams.Where(t => !highEffortTeamIds.Contains(t.Id));
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void RemoveRegionals()
|
||||
{
|
||||
var regionalTeamIds = _teams.Where(t => t.Event.RegionalEvent).Select(t => t.Id).ToHashSet();
|
||||
_scheduledTeams = _scheduledTeams.Where(t => !regionalTeamIds.Contains(t.Id));
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void AddIndividual()
|
||||
{
|
||||
var individualTeams = _teams.Where(t => t.Event.EventFormat == EventFormat.Individual);
|
||||
_scheduledTeams = _scheduledTeams.Concat(individualTeams).Distinct();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void RemoveIndividual()
|
||||
{
|
||||
_scheduledTeams = _scheduledTeams.RemoveIndividual();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void AddLowLevelOfEffort()
|
||||
{
|
||||
var lowEffortTeams = _teams.Where(t => t.Event.LevelOfEffort <= 1);
|
||||
_scheduledTeams = _scheduledTeams.Concat(lowEffortTeams).Distinct();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void RemoveLowLevelOfEffort()
|
||||
{
|
||||
_scheduledTeams = _scheduledTeams.RemoveLowLevelOfEffort();
|
||||
@@ -512,6 +570,63 @@
|
||||
// Note: Success message is already shown in the dialog, no need to show another here
|
||||
}
|
||||
|
||||
private async Task LoadTeamsFromClipboard()
|
||||
{
|
||||
if (_isLoadingClipboard) return;
|
||||
|
||||
try
|
||||
{
|
||||
_isLoadingClipboard = true;
|
||||
StateHasChanged();
|
||||
|
||||
var clipboardText = await ClipboardService.ReadTextAsync();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(clipboardText))
|
||||
{
|
||||
Snackbar.Add("Clipboard is empty", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var matchedTeams = TeamClipboardMatcher.MatchTeamsFromClipboard(clipboardText, _teams);
|
||||
|
||||
if (!matchedTeams.Any())
|
||||
{
|
||||
Snackbar.Add("No matching teams found in clipboard text", Severity.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
// Combine with existing scheduled teams, avoiding duplicates
|
||||
var existingTeamIds = _scheduledTeams.Select(t => t.Id).ToHashSet();
|
||||
var newTeams = matchedTeams.Where(t => !existingTeamIds.Contains(t.Id));
|
||||
var updatedTeams = _scheduledTeams.Concat(newTeams).ToList();
|
||||
|
||||
OnScheduledTeamsChanged(updatedTeams);
|
||||
|
||||
var newCount = newTeams.Count();
|
||||
var totalCount = matchedTeams.Count();
|
||||
if (newCount == totalCount)
|
||||
{
|
||||
Snackbar.Add($"Selected {totalCount} team(s) from clipboard", Severity.Success);
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add($"Selected {newCount} new team(s) from clipboard ({totalCount - newCount} already selected)", Severity.Success);
|
||||
}
|
||||
}
|
||||
catch (JSException ex)
|
||||
{
|
||||
Snackbar.Add("Unable to access clipboard. Please ensure clipboard permissions are granted.", Severity.Error);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Error loading teams from clipboard: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoadingClipboard = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
@namespace WebApp.Components.Shared.Components
|
||||
|
||||
<MudPaper Elevation="0" Class="pa-1" Style="border: 1px solid var(--mud-palette-lines-default); border-radius: var(--mud-default-borderradius);">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudTooltip Text="@AddTooltip">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Add"
|
||||
Variant="Variant.Outlined"
|
||||
Color="Color.Primary"
|
||||
OnClick="HandleAdd"
|
||||
Size="Size.Small" />
|
||||
</MudTooltip>
|
||||
<MudText Typo="Typo.body2" Style="flex: 1; text-align: center;">@Label</MudText>
|
||||
<MudTooltip Text="@RemoveTooltip">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Remove"
|
||||
Variant="Variant.Outlined"
|
||||
Color="Color.Error"
|
||||
OnClick="HandleRemove"
|
||||
Size="Size.Small" />
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public required string Label { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback OnAdd { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback OnRemove { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string? AddTooltip { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string? RemoveTooltip { get; set; }
|
||||
|
||||
private async Task HandleAdd()
|
||||
{
|
||||
if (OnAdd.HasDelegate)
|
||||
{
|
||||
await OnAdd.InvokeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleRemove()
|
||||
{
|
||||
if (OnRemove.HasDelegate)
|
||||
{
|
||||
await OnRemove.InvokeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user