Optimize price-list name search for large catalogs.

Replace O(n^2) per-keystroke rebuild with indexed unique names, a single-pass filter, and debounce so typing stays responsive with ~15k items.
This commit is contained in:
Magomed 2026-07-30 18:35:57 +03:00
parent 9dfdb913cb
commit 09535e9b02

View File

@ -49,6 +49,11 @@ namespace Электронная_Фармация.UserControls
ModernSplitContainer splitNameMgBrowse; ModernSplitContainer splitNameMgBrowse;
ModernDataGridView dgvBrowseNames; ModernDataGridView dgvBrowseNames;
ModernDataGridView dgvBrowseMg; ModernDataGridView dgvBrowseMg;
List<string> _uniqueBaseNames = new List<string>();
Dictionary<string, List<string>> _mgByBaseName =
new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
Timer _searchDebounceTimer;
const int SearchDebounceMs = 180;
static readonly Regex DosageRegex = new Regex( static readonly Regex DosageRegex = new Regex(
@"(\d+(?:[.,]\d+)?\s*(?:мг|мкг|г|мл|МЕ|ме)\b)", @"(\d+(?:[.,]\d+)?\s*(?:мг|мкг|г|мл|МЕ|ме)\b)",
RegexOptions.IgnoreCase | RegexOptions.Compiled); RegexOptions.IgnoreCase | RegexOptions.Compiled);
@ -407,11 +412,77 @@ namespace Электронная_Фармация.UserControls
EnsureDerivedBrowseColumns(tablePrice); EnsureDerivedBrowseColumns(tablePrice);
_fullPriceTable = tablePrice; _fullPriceTable = tablePrice;
RebuildBrowseIndex(tablePrice);
_selectedBaseName = string.Empty; _selectedBaseName = string.Empty;
_selectedMg = string.Empty; _selectedMg = string.Empty;
ShowBrowseView(); ShowBrowseView();
} }
private void RebuildBrowseIndex(DataTable tablePrice)
{
var names = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
var mgMap = new Dictionary<string, SortedSet<string>>(StringComparer.OrdinalIgnoreCase);
foreach (DataRow row in tablePrice.Rows)
{
var baseName = row["Базовое наименование"]?.ToString() ?? string.Empty;
if (string.IsNullOrWhiteSpace(baseName))
{
continue;
}
names.Add(baseName);
if (!mgMap.TryGetValue(baseName, out var dosages))
{
dosages = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
mgMap[baseName] = dosages;
}
dosages.Add(row["МГ"]?.ToString() ?? string.Empty);
}
_uniqueBaseNames = names.ToList();
_mgByBaseName = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
foreach (var pair in mgMap)
{
var list = new List<string>(pair.Value.Count);
foreach (var dosage in pair.Value)
{
list.Add(string.IsNullOrEmpty(dosage) ? EmptyMgDisplay : dosage);
}
_mgByBaseName[pair.Key] = list;
}
}
private void EnsureSearchDebounceTimer()
{
if (_searchDebounceTimer != null)
{
return;
}
_searchDebounceTimer = new Timer { Interval = SearchDebounceMs };
_searchDebounceTimer.Tick += (_, __) =>
{
_searchDebounceTimer.Stop();
ResetBrowseToNames();
};
}
private void ScheduleBrowseSearchRefresh(bool immediate = false)
{
EnsureSearchDebounceTimer();
_searchDebounceTimer.Stop();
if (immediate)
{
ResetBrowseToNames();
return;
}
_searchDebounceTimer.Start();
}
private void HideOptionalColumn(string columnName) private void HideOptionalColumn(string columnName)
{ {
if (dgvPriceList.Columns.Contains(columnName)) if (dgvPriceList.Columns.Contains(columnName))
@ -506,55 +577,51 @@ namespace Электронная_Фармация.UserControls
} }
var search = (GoodsFilter ?? string.Empty).Trim(); var search = (GoodsFilter ?? string.Empty).Trim();
var namesTable = new DataTable(); IList<string> namesToShow;
namesTable.Columns.Add("Наименование", typeof(string)); if (string.IsNullOrWhiteSpace(search))
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (DataRow row in _fullPriceTable.Rows)
{ {
var baseName = row["Базовое наименование"]?.ToString() ?? string.Empty; namesToShow = _uniqueBaseNames;
if (string.IsNullOrWhiteSpace(baseName) || !seen.Add(baseName)) }
else
{
var matching = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (DataRow row in _fullPriceTable.Rows)
{ {
continue; var baseName = row["Базовое наименование"]?.ToString() ?? string.Empty;
} if (string.IsNullOrWhiteSpace(baseName) || matching.Contains(baseName))
if (!string.IsNullOrWhiteSpace(search))
{
var keep = false;
foreach (DataRow candidate in _fullPriceTable.Rows)
{
if (!string.Equals(candidate["Базовое наименование"]?.ToString(), baseName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
var drugName = candidate["Наименование"]?.ToString() ?? string.Empty;
var mgText = candidate["МГ"]?.ToString() ?? string.Empty;
if (baseName.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0
|| drugName.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0
|| mgText.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0)
{
keep = true;
break;
}
}
if (!keep)
{ {
continue; continue;
} }
var drugName = row["Наименование"]?.ToString() ?? string.Empty;
var mgText = row["МГ"]?.ToString() ?? string.Empty;
if (baseName.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0
|| drugName.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0
|| mgText.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0)
{
matching.Add(baseName);
}
} }
namesTable.Rows.Add(baseName); namesToShow = matching
.OrderBy(n => n, StringComparer.OrdinalIgnoreCase)
.ToList();
} }
namesTable.DefaultView.Sort = "Наименование ASC"; var namesTable = new DataTable();
var sortedNames = namesTable.DefaultView.ToTable(); namesTable.Columns.Add("Наименование", typeof(string));
namesTable.BeginLoadData();
foreach (var baseName in namesToShow)
{
namesTable.Rows.Add(baseName);
}
namesTable.EndLoadData();
try try
{ {
_suppressBrowseEvents = true; _suppressBrowseEvents = true;
dgvBrowseNames.DataSource = sortedNames; dgvBrowseNames.SuspendLayout();
dgvBrowseNames.DataSource = namesTable;
ConfigureSingleColumnGrid(dgvBrowseNames, "Наименование"); ConfigureSingleColumnGrid(dgvBrowseNames, "Наименование");
ClearMgPanel(); ClearMgPanel();
@ -569,6 +636,7 @@ namespace Электронная_Фармация.UserControls
} }
finally finally
{ {
dgvBrowseNames.ResumeLayout();
_suppressBrowseEvents = false; _suppressBrowseEvents = false;
} }
@ -595,28 +663,18 @@ namespace Электронная_Фармация.UserControls
var mgTable = new DataTable(); var mgTable = new DataTable();
mgTable.Columns.Add("МГ", typeof(string)); mgTable.Columns.Add("МГ", typeof(string));
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase); if (_mgByBaseName.TryGetValue(baseName, out var dosages))
foreach (DataRow row in _fullPriceTable.Rows)
{ {
if (!string.Equals(row["Базовое наименование"]?.ToString(), baseName, StringComparison.OrdinalIgnoreCase)) foreach (var dosage in dosages)
{ {
continue; mgTable.Rows.Add(dosage);
} }
var dosage = row["МГ"]?.ToString() ?? string.Empty;
if (!seen.Add(dosage))
{
continue;
}
mgTable.Rows.Add(string.IsNullOrEmpty(dosage) ? EmptyMgDisplay : dosage);
} }
mgTable.DefaultView.Sort = "МГ ASC";
try try
{ {
_suppressBrowseEvents = true; _suppressBrowseEvents = true;
dgvBrowseMg.DataSource = mgTable.DefaultView.ToTable(); dgvBrowseMg.DataSource = mgTable;
ConfigureSingleColumnGrid(dgvBrowseMg, "МГ"); ConfigureSingleColumnGrid(dgvBrowseMg, "МГ");
dgvBrowseMg.ClearSelection(); dgvBrowseMg.ClearSelection();
} }
@ -990,7 +1048,7 @@ namespace Электронная_Фармация.UserControls
GoodsFilter = GoodsFilter.Substring(0, GoodsFilter.Length - 1); GoodsFilter = GoodsFilter.Substring(0, GoodsFilter.Length - 1);
txtSearchGoodNameInPriceList.Text = GoodsFilter; txtSearchGoodNameInPriceList.Text = GoodsFilter;
ResetBrowseToNames(); ScheduleBrowseSearchRefresh(immediate: GoodsFilter.Length == 0);
if (GoodsFilter.Length == 0) if (GoodsFilter.Length == 0)
{ {
@ -1501,7 +1559,7 @@ select strftime('%Y-%m-%d','{OrderDate}'),
{ {
txtSearchGoodNameInPriceList.Text = string.Empty; txtSearchGoodNameInPriceList.Text = string.Empty;
GoodsFilter = string.Empty; GoodsFilter = string.Empty;
ResetBrowseToNames(); ScheduleBrowseSearchRefresh(immediate: true);
//loadPriceList(string.Empty); //loadPriceList(string.Empty);
} }
@ -1560,6 +1618,27 @@ and tempOrder.GoodName = PriceList.GoodName
string GoodsFilter = string.Empty; string GoodsFilter = string.Empty;
string stringIWantBuy = string.Empty; string stringIWantBuy = string.Empty;
private static bool IsGoodsSearchKey(char keyChar, bool allowDigitsAndSeparators = true)
{
if ((keyChar >= 'A' && keyChar <= 'Z')
|| (keyChar >= 'a' && keyChar <= 'z')
|| (keyChar >= 'А' && keyChar <= 'Я')
|| (keyChar >= 'а' && keyChar <= 'я')
|| keyChar == (char)Keys.Subtract)
{
return true;
}
if (!allowDigitsAndSeparators)
{
return false;
}
return (keyChar >= '0' && keyChar <= '9')
|| keyChar == ' '
|| keyChar == '.'
|| keyChar == ',';
}
private void dgvPriceList_KeyPress(object sender, KeyPressEventArgs e) private void dgvPriceList_KeyPress(object sender, KeyPressEventArgs e)
{ {
@ -1573,7 +1652,7 @@ and tempOrder.GoodName = PriceList.GoodName
{ {
txtSearchGoodNameInPriceList.Text = string.Empty; txtSearchGoodNameInPriceList.Text = string.Empty;
GoodsFilter = string.Empty; GoodsFilter = string.Empty;
ResetBrowseToNames(); ScheduleBrowseSearchRefresh(immediate: true);
} }
return; return;
@ -1613,67 +1692,27 @@ and tempOrder.GoodName = PriceList.GoodName
if (_browseMode != PriceBrowseMode.Offers) if (_browseMode != PriceBrowseMode.Offers)
{ {
if ((e.KeyChar >= 'A' && e.KeyChar <= 'Z') if (IsGoodsSearchKey(e.KeyChar))
|| (e.KeyChar >= 'a' && e.KeyChar <= 'z')
|| (e.KeyChar >= 'А' && e.KeyChar <= 'Я')
|| (e.KeyChar >= 'а' && e.KeyChar <= 'я')
|| (e.KeyChar == (Char)Keys.Subtract))
{ {
dgvPriceList.ClearSelection(); dgvPriceList.ClearSelection();
GoodsFilter += e.KeyChar.ToString(); GoodsFilter += e.KeyChar.ToString();
txtSearchGoodNameInPriceList.Text = GoodsFilter; txtSearchGoodNameInPriceList.Text = GoodsFilter;
ResetBrowseToNames(); ScheduleBrowseSearchRefresh();
if (dgvBrowseNames != null && dgvBrowseNames.Rows.Count == 0) timerForResetSearch.Enabled = true;
{ timerForResetSearch.Start();
UiDialogs.ShowInfo("Подходящий товар не найден.", "Поиск", FindForm());
GoodsFilter = string.Empty;
txtSearchGoodNameInPriceList.Text = string.Empty;
ResetBrowseToNames();
}
} }
return; return;
} }
if ((e.KeyChar >= 'A' && e.KeyChar <= 'Z') || (e.KeyChar >= 'a' && e.KeyChar <= 'z') || (e.KeyChar >= 'А' && e.KeyChar <= 'Я') || (e.KeyChar >= 'а' && e.KeyChar <= 'я') || (e.KeyChar == (Char)Keys.Subtract)) if (IsGoodsSearchKey(e.KeyChar, allowDigitsAndSeparators: false))
{ {
if (e.KeyChar == (Char)Keys.Enter) dgvPriceList.ClearSelection();
{ GoodsFilter += e.KeyChar.ToString();
txtSearchGoodNameInPriceList.Text = GoodsFilter;
} ScheduleBrowseSearchRefresh();
else timerForResetSearch.Enabled = true;
{ timerForResetSearch.Start();
dgvPriceList.ClearSelection();
GoodsFilter += e.KeyChar.ToString();
txtSearchGoodNameInPriceList.Text = GoodsFilter;
#region поиск без фильтрации (НЕ РАБОТАЕТ)
//foreach(DataGridViewRow row in dgvPriceList.Rows)
//{
// //string cellValue = row.Cells["Наименование"].Value.ToString().ToLower();
// if (row.Cells[2].Value.ToString().Equals(GoodsFilter))
// {
// int indexOfSearchValue = row.Index;
// dgvPriceList.Rows[indexOfSearchValue].Selected = true;
// }
//}
#endregion
#region старый способ поиска с фильтрацией данных
if (!string.IsNullOrEmpty(GoodsFilter))
{
ResetBrowseToNames();
if (dgvPriceList.Rows.Count == 0)
{
UiDialogs.ShowInfo("Подходящий товар не найден.", "Поиск", FindForm());
GoodsFilter = string.Empty;
txtSearchGoodNameInPriceList.Text = string.Empty;
ResetBrowseToNames();
}
}
#endregion
}
return; return;
} }
else if ((e.KeyChar >= (Char)Keys.D1 && e.KeyChar <= (Char)Keys.D9) || else if ((e.KeyChar >= (Char)Keys.D1 && e.KeyChar <= (Char)Keys.D9) ||
@ -2511,7 +2550,7 @@ and SumOrderedItems = '{SumOrder}'";
{ {
txtSearchGoodNameInPriceList.Text = string.Empty; txtSearchGoodNameInPriceList.Text = string.Empty;
GoodsFilter = string.Empty; GoodsFilter = string.Empty;
ResetBrowseToNames(); ScheduleBrowseSearchRefresh(immediate: true);
timerForResetSearch.Enabled = false; timerForResetSearch.Enabled = false;
timerForResetSearch.Stop(); timerForResetSearch.Stop();
} }