public IEnumerable <JGram.Entry> Lookup(string key)
        {
            int limit = 50;

            var(start, end) = index.EqualRange(key, kvp => kvp.Key);
            var outOfBoundLimit = (limit - (end - start)) / 2;

            start = Math.Max(0, start - outOfBoundLimit);
            end   = Math.Min(entries.Count, end + outOfBoundLimit);
            var mid = (start + end) / 2;

            var resultEntries = EnumerableExt.Range(start, end - start)
                                .OrderBy(i => Math.Abs(i - mid))
                                .Select(i => index[i])
                                .Select(indexEntry =>
            {
                var indexKey   = indexEntry.Key;
                var entryKey   = indexEntry.Value;
                var(entry, id) = entries.BinarySearch(entryKey, e => e.Id);
                return((indexKey, entry).SomeWhen(_ => id != -1));
            })
                                .Values()
                                .OrderByDescending(r => CommonPrefixLength(r.indexKey, key))
                                .Where(r => CommonPrefixLength(r.indexKey, key) != 0)
                                .Select(r => r.entry);

            return(EnumerableExt.DistinctBy(resultEntries, entry => entry.Id));
        }
        public static JMDictParser Create(Stream stream)
        {
            DateTime?versionDate = null;
            var      undoEntityExpansionDictionary = new DualDictionary <string, string>();

            var xmlReader = new XmlTextReader(stream);

            xmlReader.EntityHandling = EntityHandling.ExpandCharEntities;
            xmlReader.DtdProcessing  = DtdProcessing.Parse;
            xmlReader.XmlResolver    = null;
            while (xmlReader.Read())
            {
                if (xmlReader.NodeType == XmlNodeType.DocumentType)
                {
                    undoEntityExpansionDictionary = new DualDictionary <string, string>(
                        EnumerableExt.DistinctBy(
                            XmlEntities.ParseJMDictEntities(xmlReader.Value),
                            kvp => kvp.Key));
                }

                if (xmlReader.NodeType == XmlNodeType.Comment)
                {
                    var commentText = xmlReader.Value.Trim();
                    if (commentText.StartsWith("JMdict created:", StringComparison.Ordinal))
                    {
                        var generationDate = commentText.Split(':').ElementAtOrDefault(1)?.Trim();
                        if (DateTime.TryParseExact(generationDate, "yyyy-MM-dd", CultureInfo.InvariantCulture,
                                                   DateTimeStyles.AssumeUniversal, out var date))
                        {
                            versionDate = date;
                        }
                        else
                        {
                            versionDate = null;
                        }
                    }
                }

                if (xmlReader.NodeType == XmlNodeType.Element && xmlReader.Name == "JMdict")
                {
                    break;
                }
            }

            return(new JMDictParser(versionDate, undoEntityExpansionDictionary, xmlReader));
        }
        public Option <KanjiLookupResult, Error> SelectRadicals(
            string query,
            string sort,
            string select   = null,
            string deselect = null)
        {
            // validation and normalization
            query = query ?? "";
            var sortingCriteriaIndexOpt = sort == null
                ? Option.Some <int>(0)
                : radicalLookup.SortingCriteria
                                          .FindIndexOrNone(criterion => criterion.Description == sort);

            if (!sortingCriteriaIndexOpt.HasValue)
            {
                return(Option.None <KanjiLookupResult, Error>(
                           new Error(ErrorCodes.InvalidInput, "Invalid sorting criterion", new[] { nameof(sort), sort })));
            }

            var sortingCriteriaIndex = sortingCriteriaIndexOpt.ValueOrFailure();

            // get corresponding radicals
            var radicalSearchResults = radicalSearcher.Search(query);
            var usedRadicals         = EnumerableExt.DistinctBy(radicalSearchResults, r => r.Text)
                                       .Select(r => new KeyValuePair <string, string>(r.Text, r.Radical.ToString()));

            // select
            if (select != null)
            {
                query += " " + select;
            }

            // unselect
            if (deselect != null)
            {
                foreach (var kvp in usedRadicals.Where(x => x.Value == deselect))
                {
                    var name = kvp.Key;
                    query = query.Replace(name, "");
                }
            }

            query = query.Trim();

            // search again, with the new radicals
            radicalSearchResults = radicalSearcher.Search(query);
            var radicals = radicalSearchResults
                           .Select(result => result.Radical)
                           .ToList();

            if (radicals.Count == 0)
            {
                return(Option.Some <KanjiLookupResult, Error>(new KanjiLookupResult
                                                              (
                                                                  newQuery: query,
                                                                  kanji: Array.Empty <string>(),
                                                                  radicals: radicalLookup.AllRadicals
                                                                  .Select(r => new RadicalState(r.ToString(), isAvailable: true, isSelected: false))
                                                                  .ToList()
                                                              )));
            }

            var selectionResult = radicalLookup.SelectRadical(radicals, sortingCriteriaIndex);
            var usedRadicalsSet = new HashSet <CodePoint>(radicalSearchResults.Select(r => r.Radical));

            return(Option.Some <KanjiLookupResult, Error>(new KanjiLookupResult
                                                          (
                                                              newQuery: query,
                                                              kanji: selectionResult.Kanji
                                                              .Select(k => k.ToString())
                                                              .ToList(),
                                                              radicals: selectionResult.PossibleRadicals
                                                              .Select(k => new RadicalState(k.Key.ToString(), k.Value || usedRadicalsSet.Contains(k.Key), usedRadicalsSet.Contains(k.Key)))
                                                              .ToList()
                                                          )));
        }