using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text.RegularExpressions; using System.Web.Routing; using DR.FV2016.Service.Interfaces; using DR.FV2016.Web.Models; using DRCMSRedesign.Contracts; namespace DR.FV2016.Web.PoliticianLink { public class PoliticianLinkProcessor { private readonly SearchIndexCache _searchIndexCache; public PoliticianLinkProcessor(ICompositeCandidateService compositeCandidateService) { if (compositeCandidateService == null) throw new ArgumentNullException("compositeCandidateService", "compositeCandidateService cannot be null"); _searchIndexCache = new SearchIndexCache(compositeCandidateService); } public void PostProcessArticleWrapper(RequestContext requestContext, ArticleWrapper articleWrapper) { #region Documentation of this method /* Documentation of this method: (TODO: rewrite it to fit searchitem) * Index is a SortedDictionary. The key is the first word of the candidates name. * * The use case performance wise here is that the index is much larger than the number of words in a paragraphs. * Because a SortedDictionarys lookup-speed on keys is probably the best we can get in the .NET Framework, lookups will be fast. * * If a word in a paragraph is present in the index, it means that the paragraph might contain a name starting with the index key. * The candidates that has this first name is then the key to all candidates with this, severely limiting the searches required for replacement. * * The _candidateCache is a simply in-memory cache that stores this index in a static variable, and asyncronously updates it every 30 minutes. * This is because it's a relatively expensive operation to build the index (several seconds). * * The taggedNames collection is used to ensure that only the first occurance of a politicians name is linked - not all of them. * This is a business requirement - it has nothing to do with the algorithm it self as such. * * The old method used took ~600ms on a large article. This one takes ~3ms. Plus it's less code. Gonna love code some times. * * So to recap: * 1) Break the paragraph into words. * 2) For each word -> if word is a key in the index, go to 3 - else move to the next word * 3) Get the list of candidates from the index using the word as key * 4) Search for the returned candidates name in the paragraph and replace. */ #endregion var index = _searchIndexCache.GetSearchIndex(); var taggedNames = new List(); //TODO: Make case insensitive foreach (var paragraphElement in articleWrapper.Body.OfType()) { //Fetch already tagged candidates var pretaggedRegex = new Regex(@"(\[(?.*?)\] | \[(?.*?)\]\((?.*?)\:(?[0-9]*?)\))"); //var pretaggedRegex = new Regex(@"\[(?.*?)\]\(.*?\)"); var matches = pretaggedRegex.Matches(paragraphElement.Text); foreach (Match match in matches) { var name = match.Groups["name"].Value; if (!string.IsNullOrWhiteSpace(match.Groups["id"].Value)) { taggedNames.Add(name); } } foreach (var word in paragraphElement.Text.Split(" ".ToCharArray())) { if (index.ContainsKey(word)) { var indexedCandidates = index[word]; foreach (var searchItem in indexedCandidates) { var searchRegex = new Regex("[^\\[]" + searchItem.Word + "[s]{0,1}[^\\]]", RegexOptions.IgnoreCase); var match = searchRegex.Match(" " + paragraphElement.Text); if (!match.Success || taggedNames.Contains(searchItem.Word)) continue; var actualWord = match.Value.Remove(0, 1).Remove(match.Value.Length - 2); Debug.WriteLine("Tagging: " + searchItem.Word); var tag = FormatSearchItemToTag(actualWord, searchItem); paragraphElement.Text = paragraphElement.Text.Replace(actualWord, tag); taggedNames.Add(searchItem.Word); } } } paragraphElement.Text = CleanParagraphFromIgnoreTags(paragraphElement.Text); } } private static string CleanParagraphFromIgnoreTags(string text) { var charDir = new Dictionary(); var arr = text.ToCharArray(); for (int i = 0; i < arr.Length; i++) { if (arr[i] == '[' || arr[i] == ']') { charDir.Add(i, arr[i]); } } var removeNextStart = false; foreach (var ch in charDir.Reverse()) { if (ch.Value == ']' && arr[ch.Key + 1] != '(') { removeNextStart = true; text = text.Remove(ch.Key, 1); } if (ch.Value != '[' || !removeNextStart) continue; removeNextStart = false; text = text.Remove(ch.Key, 1); } return text; } private static string FormatSearchItemToTag(string name, SearchItem searchItem) { string tag; switch (searchItem.Type) { case SearchType.Fv15Candidate: tag = string.Format("[{0}](fv15candidate:{1})", name, searchItem.Id); break; //Todo:This should not be possible, but do something smart here as this will stop the tagging process default: throw new Exception("Unidentified searchitem"); } return tag; } } }