public static SearchResult ParseRouteFromString(string inputString) { Stopwatch stopwatch = Stopwatch.StartNew(); SearchResult finalResult = new SearchResult(); //Todo: in the future support returning multiple routes (but only if there are multiple grades in the title? Maybe only if all of the routes' full names are in the title?) List <PossibleRouteResult> possibleResults = new List <PossibleRouteResult>(); List <Grade> postGrades = Grade.ParseString(inputString); WriteToConsole($"\tRecognized grade(s): {string.Join(" | ", postGrades)}"); List <string> possibleRouteNames = GetPossibleRouteNames(inputString); WriteToConsole($"\tRecognized name(s): {string.Join(" | ", possibleRouteNames)}"); foreach (string possibleRouteName in possibleRouteNames) { string inputWithoutName = inputString.Replace(possibleRouteName, ""); SearchResult searchResult = Search(possibleRouteName, new SearchParameters() { OnlyRoutes = true }); if (!searchResult.IsEmpty() && searchResult.AllResults.Count < 75) //If the number of matching results is greater than 75, it was probably a very generic word for a search (eg "There") { List <PossibleRouteResult> allSearchResults = searchResult.AllResults.Select(r => new PossibleRouteResult { Route = r as Route, Area = searchResult.RelatedLocation, RemainingInputString = inputWithoutName, }).ToList(); PossibleRouteResult filteredSearchResult = new PossibleRouteResult { Route = searchResult.FilteredResult as Route, Area = searchResult.RelatedLocation, RemainingInputString = inputWithoutName, }; if (allSearchResults.Count == 1 && ParentsInString(allSearchResults.First(), false, true).Any()) { possibleResults.Add(allSearchResults.First()); } else if (allSearchResults.Count(r => ParentsInString(r, false, true).Any() && Utilities.StringContainsWithFilters(inputString, r.Route.Name, true)) == 1) { PossibleRouteResult possibleResult = allSearchResults.First(r => ParentsInString(r, false, true).Any() && Utilities.StringContainsWithFilters(inputString, r.Route.Name, true)); possibleResults.Add(possibleResult); } else if (postGrades.Any()) { if (allSearchResults.Any(r => ParentsInString(r).Any())) //If some routes have a location in the inputString, work with those { foreach (PossibleRouteResult possibleResult in allSearchResults.Where(r => ParentsInString(r).Any())) { if (possibleResult.Route.Grades.Any(g => postGrades.Any(p => g.Equals(p, true, true)))) { possibleResults.Add(possibleResult); } } } else { foreach (PossibleRouteResult possibleResult in allSearchResults) { if (possibleResult.Route.Grades.Any(g => postGrades.Any(p => g.Equals(p, true, true)))) { possibleResults.Add(possibleResult); } } } } else if (allSearchResults.Any(r => ParentsInString(r, false, true).Any() && Utilities.StringContainsWithFilters(inputString, r.Route.Name, true))) { possibleResults = allSearchResults.Where(r => ParentsInString(r, false, true).Any() && Utilities.StringContainsWithFilters(inputString, r.Route.Name, true)).ToList(); } } } possibleResults = possibleResults.GroupBy(x => x.Route).Select(g => g.First()).ToList(); //Distinct routes if (possibleResults.Any()) { //Todo: prioritize routes where the grade matches exactly (eg 5.11a matches 5.11a rather than matching 5.11a-b). Also prioritize 5.11a towards 5.11a-b (or vice-versa) rather than 5.11c //Todo: for matching parents in string, maybe give higher priority to results that match MORE parents. EG "Once Upon a Time - Black Mountain, California" gives // 2 routes named "Once upon a time" in California. But only one is also at "Black Mountain" //Todo: rather than trying to match the whole route name in filteredResults, we should just prioritize matching the largest string of words in a row (in the case of "Birthing Experience // V1 Bishop, CA" we should prioritize "The Womb (Birthing Experience)" over routes just named "Bishop") //Prioritize routes where the full name is in the input string //(Additionally, we could also prioritize how close - within the input string - the name is to the grade) List <PossibleRouteResult> filteredResults = possibleResults.Where(p => Utilities.StringContainsWithFilters(inputString, p.Route.Name, true)).ToList(); if (filteredResults.Count > 1) { //Try to filter down to "most parents matched" //Todo: possibly a more efficient way to do this int maxParentsMatched = filteredResults.Max(r => r.FoundParents.Select(p => p.Value.Count).Max()); filteredResults = filteredResults.Where(r => r.FoundParents.Select(p => p.Value.Count).Max() == maxParentsMatched).ToList(); } int highConfidence = 1; int medConfidence = 2; int lowConfidence = 3; int confidence = lowConfidence; string unconfidentReason = null; if (filteredResults.Count == 1) { if (ParentsInString(filteredResults.First(), true).Any() || Grade.ParseString(inputString, false).Any(g => filteredResults.First().Route.Grades.Any(p => g.Equals(p)))) { confidence = highConfidence; //Highest confidence when we also match a location in the string or if we match a full grade } else { confidence = medConfidence; //Medium confidence when we have only found one match with that exact name but can't match a location in the string unconfidentReason = "Single result found, but no parents or grades matched"; } } else if (filteredResults.Count > 1) { //Prioritize routes where one of the parents (locations) is also in the input string List <PossibleRouteResult> routesWithMatchingLocations = filteredResults.Where(r => ParentsInString(r).Any()).ToList(); if (routesWithMatchingLocations.Any()) { filteredResults = routesWithMatchingLocations; if (postGrades.Any()) { confidence = highConfidence; //Highest confidence when we have found the location in the string } else { confidence = medConfidence; unconfidentReason = $"{filteredResults.Count} EXACTLY matching routes (name & location w/o abbrev). No grades matched"; } } else { routesWithMatchingLocations = filteredResults.Where(r => ParentsInString(r, true).Any()).ToList(); if (routesWithMatchingLocations.Any()) { filteredResults = routesWithMatchingLocations; if (postGrades.Any()) { confidence = highConfidence; //Highest confidence when we have found the location in the string } else { confidence = medConfidence; unconfidentReason = $"{filteredResults.Count} EXACTLY matching routes (name & location w/ abbrev). No grades matched"; } } else { routesWithMatchingLocations = filteredResults.Where(r => ParentsInString(r, true, true).Any()).ToList(); if (routesWithMatchingLocations.Any()) { filteredResults = routesWithMatchingLocations; confidence = medConfidence; //Medium confidence when we have matched only part of a parent's name unconfidentReason = $"{filteredResults.Count} EXACTLY matching routes (name & PARTIAL location w/ abbrev)"; } } } } else { //Prioritize routes where one of the parents (locations) is also in the input string List <PossibleRouteResult> routesWithMatchingLocations = possibleResults.Where(r => ParentsInString(r).Any()).ToList(); if (routesWithMatchingLocations.Any()) { filteredResults = routesWithMatchingLocations; confidence = medConfidence; //Medium confidence when we didn't match a full route name, but have found a parent location in the string unconfidentReason = $"{filteredResults.Count} PARTIALLY matching routes (name & location w/o abbrev)"; } else { routesWithMatchingLocations = possibleResults.Where(r => ParentsInString(r, true).Any()).ToList(); if (routesWithMatchingLocations.Any()) { filteredResults = routesWithMatchingLocations; confidence = medConfidence; //Medium confidence when we didn't match a full route name, but have found a parent location in the string (including the possibility of the state abbrv) unconfidentReason = $"{filteredResults.Count} PARTIALLY matching routes (name & location w/ abbrev)"; } else { routesWithMatchingLocations = possibleResults.Where(r => ParentsInString(r, true, true).Any()).ToList(); if (routesWithMatchingLocations.Any()) { filteredResults = routesWithMatchingLocations; confidence = medConfidence; //Medium confidence when we didn't match a full route name and have matched only part of a parent's name in the string unconfidentReason = $"{filteredResults.Count} PARTIALLY matching routes (name & PARTIAL location w/ abbrev)"; } } } } //Todo: temporary fix for posts about "covid-19" where the route is called "COVID-19" AND the parent is "Covid 19 Boulder" if (filteredResults.Count == 1 && filteredResults[0].Route.ID == "119484798" && filteredResults[0].FoundParents.All(kvp => kvp.Value.Count == 1)) { return(new SearchResult() { TimeTakenMS = stopwatch.ElapsedMilliseconds }); } PossibleRouteResult chosenRoute; Area location; List <MPObject> allResults = new List <MPObject>(); if (filteredResults.Count == 1) { chosenRoute = filteredResults.First(); allResults.Add(chosenRoute.Route); } else if (filteredResults.Count > 1) { chosenRoute = filteredResults.OrderByDescending(p => p.Route.Popularity).First(); allResults.AddRange(filteredResults.Select(p => p.Route)); confidence = medConfidence; //Medium confidence when we have matched the string exactly, but there are multiple results unconfidentReason ??= $"Too many filtered results ({filteredResults.Count})"; } else { chosenRoute = possibleResults.OrderByDescending(p => p.Route.Popularity).First(); allResults.AddRange(possibleResults.Select(p => p.Route)); confidence = lowConfidence; //Low confidence when we can't match the string exactly, haven't matched any locations, and there are multiple results unconfidentReason ??= "No filtered results. Chose most popular partial match instead"; } location = chosenRoute.Area; if (location == null) { location = ParentsInString(chosenRoute, allowPartialParents: true).FirstOrDefault(p => p.ID != GetOuterParent(chosenRoute.Route).ID) as Area; } finalResult = new SearchResult(chosenRoute.Route, location) { AllResults = allResults, Confidence = confidence, UnconfidentReason = unconfidentReason }; } finalResult.TimeTakenMS = stopwatch.ElapsedMilliseconds; return(finalResult); }