Preserve all matching Excel rows when merging

This commit is contained in:
dedicraf committed 2026-10-17 21:01:14 +03:00
1 parent 8fa4a3d7b3
commit 5c9572ce9d
3 files changed
+173 -196

No files matched your search

+28 -54
View File
@@ -137,8 +137,8 @@ public sealed class MainWindow : Window
private readonly RadioButton _rbBoth = new() { Content = "Совпадения + чего нет в A" };
private readonly RadioButton _rbExact = new() { Content = "Точное совпадение", IsChecked = true };
private readonly RadioButton _rbPartial = new() { Content = "Частичное (по фрагменту)" };
private readonly CheckBox _chkDedupe = new() { Content = "Удалять дубликаты по ключевым колонкам" };
private readonly CheckBox _chkCleanCols = new() { Content = "Убирать одинаковые колонки после слияния", IsChecked = true };
private readonly CheckBox _chkDedupe = new() { Content = "Удалять полностью одинаковые строки" };
private readonly CheckBox _chkCleanCols = new() { Content = "Объединять одноимённые колонки", IsChecked = true };
private readonly RadioButton _rbFuzzy = new() { Content = "Нечёткое (по сходству %)" };
private readonly Slider _slFuzzy = new()
@@ -561,7 +561,7 @@ private Control BuildFilePanel(TextBox tb, Button browse, ComboBox cb, StackPane
var colLabel = new TextBlock
{
Text = "Ключевые колонки для сопоставления",
Text = "Ключи (выберите соответствующие поля в одинаковом порядке)",
Foreground = Ui.Muted,
FontWeight = FontWeight.SemiBold,
FontSize = 12
@@ -900,6 +900,11 @@ private Control BuildFilePanel(TextBox tb, Button browse, ComboBox cb, StackPane
ShowError("Отметьте хотя бы одну ключевую колонку в каждой таблице (по умолчанию отмечена первая).");
return;
}
if (mainUsed.Count(x => x) != secUsed.Count(x => x))
{
ShowError("Выберите одинаковое число ключевых колонок в обеих таблицах.");
return;
}
var cts = new CancellationTokenSource();
_cts = cts;
@@ -978,8 +983,9 @@ private Control BuildFilePanel(TextBox tb, Button browse, ComboBox cb, StackPane
sb.Append($"Совпадений: {r.Matches.Count} · Только в A: {r.OnlyMain.Count} · Только в B: {r.OnlySecondary.Count}.");
if (r.Pairs.Count > 0)
{
double avg = Math.Round(r.Pairs.Average(p => p.BCount), 2);
int max = r.Pairs.Max(p => p.BCount);
var countsPerMainRow = r.Pairs.GroupBy(p => p.RowA).Select(group => group.First().BCount).ToList();
double avg = Math.Round(countsPerMainRow.Average(), 2);
int max = countsPerMainRow.Max();
sb.Append($" Пары A↔B: {r.Pairs.Count}, ср. N→M: {avg.ToString(CultureInfo.InvariantCulture)}, макс: {max}.");
}
sb.Append($" Ключи заполнены: A={covA.ToString(CultureInfo.InvariantCulture)}%, B={covB.ToString(CultureInfo.InvariantCulture)}%.");
@@ -1081,59 +1087,27 @@ private Control BuildFilePanel(TextBox tb, Button browse, ComboBox cb, StackPane
break;
default:
// Build a union of both schemas. Columns with the same header are
// aligned; B-only columns get their own position in the result.
headers = new List<string>(main.Columns);
var mainColumnMap = Enumerable.Range(0, main.Columns.Count).ToArray();
var secondaryColumnMap = new int[sec.Columns.Count];
var matchedSecondaryColumns = new HashSet<int>();
for (int b = 0; b < sec.Columns.Count; b++)
{
int matchingMain = -1;
for (int a = 0; a < main.Columns.Count; a++)
{
if (!matchedSecondaryColumns.Contains(a) &&
string.Equals(main.Columns[a].Trim(), sec.Columns[b].Trim(), StringComparison.OrdinalIgnoreCase))
{
matchingMain = a;
break;
}
}
if (matchingMain >= 0)
{
secondaryColumnMap[b] = matchingMain;
matchedSecondaryColumns.Add(matchingMain);
}
else
{
var header = sec.Columns[b];
if (headers.Contains(header, StringComparer.OrdinalIgnoreCase))
header = $"B: {header}";
secondaryColumnMap[b] = headers.Count;
headers.Add(header);
}
}
string[] CombineRows(string[] rowA, string[] rowB)
{
var combined = new string[headers.Count];
Array.Fill(combined, "");
for (int a = 0; a < mainColumnMap.Length && a < rowA.Length; a++)
combined[mainColumnMap[a]] = rowA[a];
for (int b = 0; b < secondaryColumnMap.Length && b < rowB.Length; b++)
combined[secondaryColumnMap[b]] = rowB[b];
return combined;
}
foreach (var pair in result.Pairs)
data.Add(CombineRows(pair.RowA, pair.RowB));
headers = _merged?.Columns ?? new List<string>(main.Columns);
data = _merged is not null ? new List<string[]>(_merged.Rows) : new List<string[]>();
sources = new List<string>();
foreach (var _ in result.Pairs) sources.Add("A+B");
foreach (var r in result.OnlySecondary)
{
data.Add(CombineRows(Array.Empty<string>(), r));
var row = new string[headers.Count];
Array.Fill(row, "");
if (_merged is not null)
{
for (int b = 0; b < _merged.SecondaryColumnMap.Length && b < r.Length; b++)
{
int destination = _merged.SecondaryColumnMap[b];
if (destination >= 0 && destination < row.Length) row[destination] = r[b];
}
}
else
{
for (int b = 0; b < r.Length && b < row.Length; b++) row[b] = r[b];
}
data.Add(row);
sources.Add("B");
}
break;