Страницы

Поиск по вопросам

пятница, 2 ноября 2018 г.

Distinct на основе входимости

Есть признаки документа и введенные вручную примечания к документу. Признак документа содержит текстовое описание, которое может пересекаться с примечанием. Хочется (и требуется по ТЗ) печатать что-то одно. В идеале - то, что длиннее, но для начала - хотя бы чтобы остался только один.
Пример: в комментарии введено "поставить печать", в признаке - "Поставить печать!!!"
Описан следующий компарер:
internal class StringIncludeComparer:IEqualityComparer { public bool Equals(string x, string y) { if (x.ToUpper().Contains(y.ToUpper())) {return true;} if (y.ToUpper().Contains(x.ToUpper())) {return true;} return false; }
public int GetHashCode(string obj) { return obj.GetHashCode(); } }
Собрав в итоге комментарии из документов и комментарии из признаков документов в один List<{ID, Comment}> делаю следующее:
from trp in tmpPostReport group trp.Comment by trp.Id into CommPers select new { Id= CommPers.Key, Comment = string.Join("; ", CommPers.Distinct(new StringIncludeComparer()).ToArray()) };
Но в итоге получаю строку "поставить печать; Поставить печать!!!"
Вопрос:
Что не так с компарером? Как сделать так, чтобы при сравнении строк "поставить печать" и "Поставить печать!!!" выбиралась всегда большая по длине, т.е. "Поставить печать!!!"


Ответ

Ваша проблема в том, что GetHashCode не согласован с Equals. так делать нельзя. Должно выполняться условие: если Equals возвращает true, то и хэшкоды должны быть равны.
Например, вы можете вернуть 0 в GetHashCode

Для того, чтобы выбрать самую короткую строку, можно попробовать использовать groupby:
CommPers.GroupBy(s => s, new StringIncludeComparer()) .Select(g => g.OrderBy(s => s.Length).First());
Но учтите, что для EqualityComparer'а ваше отношение равенства должно быть транзитивно, как это отмечено в другом ответе. Поэтому код имеет право и не сработать.

Для того, чтобы ваше сравнение гарантировано работало несмотря на то, что оно нетранзитивно, применим тяжёлую артиллерию. Откажемся от IEqualityComparer'а (так как мы всё равно не можем удовлетворить его инвариант), и сделаем группировку, которая вычисляет транзитивное замыкание вашего равенства: если Equals(a, b) и Equals(b, c) оба равны true, то мы считаем элементы a и c равными вне зависимости от Equals(a, c) (и так далее). Заметьте, что нам теперь придётся сравнивать (почти) каждый элемент с каждым, так что производительность пострадает.
static class EnumerableExtensions { class TransitiveGrouping : List, IGrouping { public TransitiveGrouping(K key) { Key = key; } public K Key { get; private set; } internal List EqualKeys = new List(); }
public static IEnumerable> TransitiveGroupBy( this IEnumerable sequence, Func keySelector, Func keyComparer) { var result = new List>(); foreach (T curr in sequence) { K currKey = keySelector(curr); var containingGroups = result .Where(tg => tg.EqualKeys.Any(kk => keyComparer(kk, currKey))) .ToList(); if (containingGroups.Count == 0) { // add a new group var newGroup = new TransitiveGrouping(currKey); newGroup.Add(curr); newGroup.EqualKeys.Add(currKey); result.Add(newGroup); } else { var targetGroup = containingGroups.First(); targetGroup.Add(curr); // merge the groups (transitive closure) foreach (var group in containingGroups.Skip(1)) { targetGroup.AddRange(group); targetGroup.EqualKeys.AddRange(group.EqualKeys); result.Remove(group); } } } return result; } }
Теперь ваш запрос должен работать так:
CommPers.TransitiveGroupBy(s => s, new StringIncludeComparer().Equals) .Select(g => g.OrderByDescending(s => s.Length).First());
Вот рабочий пример: http://ideone.com/OT2GqM
Я сильно не отлаживал, так что возможны баги. Если что, сообщайте, пофиксим.

Комментариев нет:

Отправить комментарий