/* * Copyright (c) 2014-2017, Eren Okka * Copyright (c) 2016-2017, Paul Miller * Copyright (c) 2017-2018, Tyler Bratton * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; namespace AnitomySharp { /// /// A utility class to assist in number parsing. /// public class ParserNumber { /// /// 动画最小计算年份 /// public const int AnimeYearMin = 1900; /// /// 动画最大计算年份 /// public const int AnimeYearMax = 2100; /// /// /// private const int EpisodeNumberMax = 9999; /// /// 最大卷数 /// private const int VolumeNumberMax = 99; /// /// 正则开头 /// public const string RegexMatchOnlyStart = @"\A(?:"; /// /// 正则结尾 /// public const string RegexMatchOnlyEnd = @")\z"; /// /// /// private readonly Parser _parser; /// /// /// /// public ParserNumber(Parser parser) { _parser = parser; } /// /// Returns whether or not the number is a volume number /// /// 返验证卷数字符串是否有效 /// private static bool IsValidVolumeNumber(string number) { return StringHelper.StringToInt(number) <= VolumeNumberMax; } /// /// Returns whether or not the number is a valid episode number. /// /// 验证集数字符串是否有效 /// private static bool IsValidEpisodeNumber(string number) { // Eliminate non numeric portion of number, then parse as double. var temp = ""; for (var i = 0; i < number.Length && char.IsDigit(number[i]); i++) { temp += number[i]; } return !string.IsNullOrEmpty(temp) && double.Parse(temp) <= EpisodeNumberMax; } /// /// Sets the alternative episode number. /// private bool SetAlternativeEpisodeNumber(string number, Token token) { _parser.Elements.Add(new Element(Element.ElementCategory.ElementEpisodeNumberAlt, number)); token.Category = Token.TokenCategory.Identifier; return true; } /// /// Sets the volume number. /// /// 添加卷数元素 /// /// the number /// the token which contains the volume number /// true if we should check if it's a valid number, false to disable verification /// true if the volume number was set public bool SetVolumeNumber(string number, Token token, bool validate) { if (validate && !IsValidVolumeNumber(number)) return false; _parser.Elements.Add(new Element(Element.ElementCategory.ElementVolumeNumber, number)); token.Category = Token.TokenCategory.Identifier; return true; } /// /// Sets the anime episode number. /// /// 添加集数元素 /// /// the episode number /// the token which contains the volume number /// true if we should check if it's a valid episode number; false to disable validation /// true if the episode number was set public bool SetEpisodeNumber(string number, Token token, bool validate) { if (validate && !IsValidEpisodeNumber(number)) return false; token.Category = Token.TokenCategory.Identifier; var category = Element.ElementCategory.ElementEpisodeNumber; /** Handle equivalent numbers */ if (_parser.IsEpisodeKeywordsFound) { foreach (var element in _parser.Elements) { if (element.Category != Element.ElementCategory.ElementEpisodeNumber) continue; /** The larger number gets to be the alternative one */ var comparison = StringHelper.StringToInt(number) - StringHelper.StringToInt(element.Value); if (comparison > 0) { category = Element.ElementCategory.ElementEpisodeNumberAlt; } else if (comparison < 0) { element.Category = Element.ElementCategory.ElementEpisodeNumberAlt; } else { return false; /** No need to add the same number twice */ } break; } } _parser.Elements.Add(new Element(category, number)); return true; } /// /// Checks if a number follows the specified token /// /// 确认此标记中是否包含给定元素类型的关键词,如果包含且其能满足匹配模式,则添加此元素 /// /// the category to set if a number follows the token /// the token /// true if a number follows the token; false otherwise private bool NumberComesAfterPrefix(Element.ElementCategory category, Token token) { var numberBegin = ParserHelper.IndexOfFirstDigit(token.Content); var prefix = StringHelper.SubstringWithCheck(token.Content, 0, numberBegin).ToUpperInvariant(); if (!KeywordManager.Contains(category, prefix)) return false; var number = StringHelper.SubstringWithCheck(token.Content, numberBegin, token.Content.Length - numberBegin); switch (category) { case Element.ElementCategory.ElementEpisodePrefix: if (!MatchEpisodePatterns(number, token)) { SetEpisodeNumber(number, token, false); } return true; case Element.ElementCategory.ElementVolumePrefix: if (!MatchVolumePatterns(number, token)) { SetVolumeNumber(number, token, false); } return true; default: return false; } } /// /// Checks whether the number precedes the word "of" /// /// the token /// the index of the token /// true if the token precedes the word "of" private bool NumberComesBeforeAnotherNumber(Token token, int currentTokenIdx) { var separatorToken = Token.FindNextToken(_parser.Tokens, currentTokenIdx, Token.TokenFlag.FlagNotDelimiter); if (!Token.InListRange(separatorToken, _parser.Tokens)) return false; var separators = new List> { Tuple.Create("&", true), Tuple.Create("of", false) }; foreach (var separator in separators) { if (_parser.Tokens[separatorToken].Content != separator.Item1) continue; var otherToken = Token.FindNextToken(_parser.Tokens, separatorToken, Token.TokenFlag.FlagNotDelimiter); if (!Token.InListRange(otherToken, _parser.Tokens) || !StringHelper.IsNumericString(_parser.Tokens[otherToken].Content)) continue; SetEpisodeNumber(token.Content, token, false); if (separator.Item2) { SetEpisodeNumber(_parser.Tokens[otherToken].Content, _parser.Tokens[otherToken], false); } _parser.Tokens[separatorToken].Category = Token.TokenCategory.Identifier; _parser.Tokens[otherToken].Category = Token.TokenCategory.Identifier; return true; } return false; } // EPISODE MATCHERS /// /// Attempts to find an episode/season inside a word /// /// 在传入的字符串中共尝试匹配季/集 /// /// the word /// the token /// true if the word was matched to an episode/season number public bool MatchEpisodePatterns(string word, Token token) { if (StringHelper.IsNumericString(word)) return false; word = word.Trim(" -".ToCharArray()); // 根据前后是否为数字进行分流处理 var numericFront = char.IsDigit(word[0]); var numericBack = char.IsDigit(word[word.Length - 1]); if (numericFront && numericBack) { // e.g. "01v2" if (MatchSingleEpisodePattern(word, token)) { return true; } // e.g. "01-02", "03-05v2" if (MatchMultiEpisodePattern(word, token)) { return true; } // e.g. "07.5" if (MatchFractionalEpisodePattern(word, token)) { return true; } } if (numericBack) { // e.g. "2x01", "S01E03", "S01-02xE001-150" if (MatchSeasonAndEpisodePattern(word, token)) { return true; } // e.g. "#01", "#02-03v2" if (MatchNumberSignPattern(word, token)) { return true; } } // e.g. "ED1", "OP4a", "OVA2" if (!numericFront && MatchTypeAndEpisodePattern(word, token)) { return true; } // e.g. "4a", "111C" if (numericFront && !numericBack && MatchPartialEpisodePattern(word, token)) { return true; } // e.g. "01-24Fin" if (word.IndexOf("fin", StringComparison.OrdinalIgnoreCase) >= 0) { if (MatchMultiEpisodePattern(word, token)) { return true; } } // U+8A71 is used as counter for stories, episodes of TV series, etc. return MatchJapaneseCounterPattern(word, token); } /// /// Match a single episode pattern. e.g. "01v2". /// /// the word /// the token /// true if the token matched private bool MatchSingleEpisodePattern(string word, Token token) { const string regexPattern = RegexMatchOnlyStart + @"(\d{1,4})[vV](\d)" + RegexMatchOnlyEnd; var match = Regex.Match(word, regexPattern); if (!match.Success) return false; SetEpisodeNumber(match.Groups[1].Value, token, false); _parser.Elements.Add(new Element(Element.ElementCategory.ElementReleaseVersion, match.Groups[2].Value)); return true; } /// /// Match a multi episode pattern. e.g. "01-02", "03-05v2", "01-24Fin". /// /// the word /// the token /// true if the token matched private bool MatchMultiEpisodePattern(string word, Token token) { const string regexPattern = RegexMatchOnlyStart + @"(\d{1,4})(?:[vV](\d))?[-~&+](\d{1,4})(?:[vV](\d))?(FIN|Fin|fin)?" + RegexMatchOnlyEnd; var match = Regex.Match(word, regexPattern); if (!match.Success) return false; var lowerBound = match.Groups[1].Value; var upperBound = match.Groups[3].Value; /** Avoid matching expressions such as "009-1" or "5-2" */ if (StringHelper.StringToInt(lowerBound) >= StringHelper.StringToInt(upperBound)) return false; if (!SetEpisodeNumber(lowerBound, token, true)) return false; SetEpisodeNumber(upperBound, token, true); if (!string.IsNullOrEmpty(match.Groups[2].Value)) { _parser.Elements.Add(new Element(Element.ElementCategory.ElementReleaseVersion, match.Groups[2].Value)); } if (!string.IsNullOrEmpty(match.Groups[4].Value)) { _parser.Elements.Add(new Element(Element.ElementCategory.ElementReleaseVersion, match.Groups[4].Value)); } if (!string.IsNullOrEmpty(match.Groups[5].Value)) { _parser.Elements.Add(new Element(Element.ElementCategory.ElementReleaseInformation, match.Groups[5].Value)); } return true; } /// /// Match season and episode patterns. e.g. "2x01", "S01E03", "S01-02xE001-150", "S01E06v2". /// /// the word /// the token /// true if the token matched private bool MatchSeasonAndEpisodePattern(string word, Token token) { const string regexPattern = RegexMatchOnlyStart + @"S?(\d{1,2})(?:-S?(\d{1,2}))?(?:x|[ ._-x]?EP?)(\d{1,4})(?:-E?P?(\d{1,4}))?(?:[vV](\d{1,2}))?" + RegexMatchOnlyEnd; var match = Regex.Match(word, regexPattern); if (!match.Success) return false; _parser.Elements.Add(new Element(Element.ElementCategory.ElementAnimeSeason, match.Groups[1].Value)); if (!string.IsNullOrEmpty(match.Groups[2].Value)) { _parser.Elements.Add(new Element(Element.ElementCategory.ElementAnimeSeason, match.Groups[2].Value)); } SetEpisodeNumber(match.Groups[3].Value, token, false); if (!string.IsNullOrEmpty(match.Groups[4].Value)) { SetEpisodeNumber(match.Groups[4].Value, token, false); } return true; } /// /// Match type and episode. e.g. "ED1", "OP4a", "OVA2". /// /// the word /// the token /// true if the token matched private bool MatchTypeAndEpisodePattern(string word, Token token) { var numberBegin = ParserHelper.IndexOfFirstDigit(word); var prefix = StringHelper.SubstringWithCheck(word, 0, numberBegin); var category = Element.ElementCategory.ElementAnimeType; var options = new KeywordOptions(); if (!KeywordManager.FindAndSet(KeywordManager.Normalize(prefix), ref category, ref options)) return false; _parser.Elements.Add(new Element(Element.ElementCategory.ElementAnimeType, prefix)); var number = word.Substring(numberBegin); if (!MatchEpisodePatterns(number, token) && !SetEpisodeNumber(number, token, true)) return false; var foundIdx = _parser.Tokens.IndexOf(token); if (foundIdx == -1) return true; token.Content = number; _parser.Tokens.Insert(foundIdx, new Token(options.Identifiable ? Token.TokenCategory.Identifier : Token.TokenCategory.Unknown, token.Enclosed, prefix)); return true; } /// /// Match fractional episodes. e.g. "07.5" /// /// the word /// the token /// true if the token matched private bool MatchFractionalEpisodePattern(string word, Token token) { if (string.IsNullOrEmpty(word)) { word = ""; } const string regexPattern = RegexMatchOnlyStart + @"\d+\.5" + RegexMatchOnlyEnd; var match = Regex.Match(word, regexPattern); return match.Success && SetEpisodeNumber(word, token, true); } /// /// Match partial episodes. e.g. "4a", "111C". /// /// the word /// the token /// true if the token matched private bool MatchPartialEpisodePattern(string word, Token token) { if (string.IsNullOrEmpty(word)) return false; var foundIdx = Enumerable.Range(0, word.Length) .DefaultIfEmpty(word.Length) .FirstOrDefault(value => !char.IsDigit(word[value])); var suffixLength = word.Length - foundIdx; bool IsValidSuffix(int c) => c >= 'A' && c <= 'C' || c >= 'a' && c <= 'c'; return suffixLength == 1 && IsValidSuffix(word[foundIdx]) && SetEpisodeNumber(word, token, true); } /// /// Match episodes with number signs. e.g. "#01", "#02-03v2" /// /// the word /// the token /// true if the token matched private bool MatchNumberSignPattern(string word, Token token) { if (string.IsNullOrEmpty(word) || word[0] != '#') word = ""; const string regexPattern = RegexMatchOnlyStart + @"#(\d{1,4})(?:[-~&+](\d{1,4}))?(?:[vV](\d))?" + RegexMatchOnlyEnd; var match = Regex.Match(word, regexPattern); if (!match.Success) return false; if (!SetEpisodeNumber(match.Groups[1].Value, token, true)) return false; if (!string.IsNullOrEmpty(match.Groups[2].Value)) { SetEpisodeNumber(match.Groups[2].Value, token, false); } if (!string.IsNullOrEmpty(match.Groups[3].Value)) { _parser.Elements.Add(new Element(Element.ElementCategory.ElementReleaseVersion, match.Groups[3].Value)); } return true; } /// /// Match Japanese patterns. e.g. U+8A71 is used as counter for stories, episodes of TV series, etc. /// /// 匹配日文中常见顺序词 /// /// 符合这种匹配模式的,一般在集数后都紧跟本集标题 #TODO /// /// the word /// the token /// true if the token matched public bool MatchJapaneseCounterPattern(string word, Token token) { if (string.IsNullOrEmpty(word)) return false; // 1st|2nd|3rd| #TODO string regexPattern = @"(Ⅰ|Ⅱ|Ⅲ)"; var match = Regex.Match(word, RegexMatchOnlyStart + regexPattern + RegexMatchOnlyEnd, RegexOptions.IgnoreCase); if (match.Success) { var episodeNumber = ParserHelper.GetNumberFromOrdinal(match.Groups[1].Value); SetEpisodeNumber(episodeNumber, token, false); return true; } regexPattern = @"([上中下前後])([巻卷編编])"; match = Regex.Match(word, RegexMatchOnlyStart + regexPattern + RegexMatchOnlyEnd, RegexOptions.IgnoreCase); if (match.Success) { var episodeNumber = ParserHelper.GetNumberFromOrdinal(match.Groups[1].Value); SetEpisodeNumber(episodeNumber, token, false); return true; } regexPattern = @"([第全]?)([0-9一二三四五六七八九十壱弐参]+)([期章話话巻卷幕夜期発縛])"; match = Regex.Match(word, RegexMatchOnlyStart + regexPattern + RegexMatchOnlyEnd, RegexOptions.IgnoreCase); if (match.Success) { var episodeNumber = match.Groups[2].Value; if (!StringHelper.IsNumericString(episodeNumber)) { episodeNumber = ParserHelper.GetNumberFromOrdinal(episodeNumber); } SetEpisodeNumber(episodeNumber, token, false); return true; } regexPattern = @"(vol|EPISODE|ACT|scene|ep|volume|screen|voice|case|menu|rail|round|game|page|collection|cage|office|doll|Princess)([ \.\-_])([0-9]+)"; match = Regex.Match(word, RegexMatchOnlyStart + regexPattern + RegexMatchOnlyEnd, RegexOptions.IgnoreCase); if (match.Success) { var episodeNumber = match.Groups[3].Value; SetEpisodeNumber(episodeNumber, token, false); return true; } else { return false; } } // VOLUME MATCHES /// /// Attempts to find an episode/season inside a word /// /// 在传入的字符串中共尝试匹配季/集 /// /// the word /// the token /// true if the word was matched to an episode/season number public bool MatchVolumePatterns(string word, Token token) { // All patterns contain at least one non-numeric character if (StringHelper.IsNumericString(word)) return false; word = word.Trim(" -".ToCharArray()); var numericFront = char.IsDigit(word[0]); var numericBack = char.IsDigit(word[word.Length - 1]); if (numericFront && numericBack) { // e.g. "01v2" e.g. "01-02", "03-05v2" return MatchSingleVolumePattern(word, token) || MatchMultiVolumePattern(word, token); } return false; } /// /// Match single volume. e.g. "01v2" /// /// the word /// the token /// true if the token matched private bool MatchSingleVolumePattern(string word, Token token) { if (string.IsNullOrEmpty(word)) word = ""; const string regexPattern = RegexMatchOnlyStart + @"(\d{1,2})[vV](\d)" + RegexMatchOnlyEnd; var match = Regex.Match(word, regexPattern); if (!match.Success) return false; SetVolumeNumber(match.Groups[1].Value, token, false); _parser.Elements.Add(new Element(Element.ElementCategory.ElementReleaseVersion, match.Groups[2].Value)); return true; } /// /// Match multi-volume. e.g. "01-02", "03-05v2". /// /// the word /// the token /// true if the token matched private bool MatchMultiVolumePattern(string word, Token token) { if (string.IsNullOrEmpty(word)) word = ""; const string regexPattern = RegexMatchOnlyStart + @"(\d{1,2})[-~&+](\d{1,2})(?:[vV](\d))?" + RegexMatchOnlyEnd; var match = Regex.Match(word, regexPattern); if (!match.Success) return false; var lowerBound = match.Groups[1].Value; var upperBound = match.Groups[2].Value; if (StringHelper.StringToInt(lowerBound) >= StringHelper.StringToInt(upperBound)) return false; if (!SetVolumeNumber(lowerBound, token, true)) return false; SetVolumeNumber(upperBound, token, false); if (string.IsNullOrEmpty(match.Groups[3].Value)) { _parser.Elements.Add(new Element(Element.ElementCategory.ElementReleaseVersion, match.Groups[3].Value)); } return true; } // SEARCH /// /// Searches for isolated numbers in a list of tokens. /// /// 搜索孤立数字 /// /// the list of tokens /// true if an isolated number was found public bool SearchForIsolatedNumbers(IEnumerable tokens) { return tokens .Where(it => _parser.Tokens[it].Enclosed && _parser.ParseHelper.IsTokenIsolated(it)) .Any(it => SetEpisodeNumber(_parser.Tokens[it].Content, _parser.Tokens[it], true)); } /// /// Searches for separated numbers in a list of tokens. /// /// 搜索带分隔符的数字 /// /// the list of tokens /// true fi a separated number was found public bool SearchForSeparatedNumbers(List tokens) { foreach (var it in tokens) { var previousToken = Token.FindPrevToken(_parser.Tokens, it, Token.TokenFlag.FlagNotDelimiter); // See if the number has a preceding "-" separator if (!_parser.ParseHelper.IsTokenCategory(previousToken, Token.TokenCategory.Unknown) || !ParserHelper.IsDashCharacter(_parser.Tokens[previousToken].Content[0])) continue; if (!SetEpisodeNumber(_parser.Tokens[it].Content, _parser.Tokens[it], true)) continue; _parser.Tokens[previousToken].Category = Token.TokenCategory.Identifier; return true; } return false; } /// /// Searches for episode patterns in a list of tokens. /// /// 在标记列表中匹配集数模式 /// /// the list of tokens /// true if an episode number was found public bool SearchForEpisodePatterns(List tokens) { foreach (var it in tokens) { var numericFront = _parser.Tokens[it].Content.Length > 0 && char.IsDigit(_parser.Tokens[it].Content[0]); if (!numericFront) { // e.g. "EP.1", "Vol.1" if (NumberComesAfterPrefix(Element.ElementCategory.ElementEpisodePrefix, _parser.Tokens[it])) { return true; } if (NumberComesAfterPrefix(Element.ElementCategory.ElementVolumePrefix, _parser.Tokens[it])) { continue; } } else { // e.g. "8 & 10", "01 of 24" if (NumberComesBeforeAnotherNumber(_parser.Tokens[it], it)) { return true; } } // Look for other patterns if (MatchEpisodePatterns(_parser.Tokens[it].Content, _parser.Tokens[it])) { return true; } } return false; } /// /// 搜索同动画类型同时出现的集数 /// /// /// public bool SearchForSymbolWithEpisode(List tokens) { // Match from back to front for (int i = tokens.Count - 1; i >= 0; i--) { var it = tokens[i]; // e.g. OVA 3, [Web Preview 06]: Web Preview in PeekEntries if ((_parser.ParseHelper.IsPrevTokenContainAnimeType(it) || _parser.ParseHelper.IsPrevTokenContainAnimeTypeInPeekEntries(it)) && !_parser.ParseHelper.IsTokenIsolated(it)) { SetEpisodeNumber(_parser.Tokens[it].Content, _parser.Tokens[it], false); return true; } // e.g. OtherToken[Hint05] // it>1: makesure this token is not first one if (it > 1 && _parser.Tokens[it].Enclosed && _parser.ParseHelper.IsTokenIsolated(it)) { var tokenContent = _parser.Tokens[it].Content; var numberBegin = ParserHelper.IndexOfFirstDigit(tokenContent); var prefix = StringHelper.SubstringWithCheck(tokenContent, 0, numberBegin); var number = StringHelper.SubstringWithCheck(tokenContent, numberBegin, tokenContent.Length - numberBegin); // token should be: alphaNumeric if (prefix != "" && StringHelper.IsAlphaString(prefix) && StringHelper.IsNumericString(number)) { SetEpisodeNumber(number, _parser.Tokens[it], true); return true; } } // e.g. OtherToken[Disc 01] if (it > 1 && _parser.Tokens[it].Enclosed && _parser.ParseHelper.IsTokenIsolatedWithDelimiterAndBracket(it) && StringHelper.IsNumericString(_parser.Tokens[it].Content)) { SetEpisodeNumber(_parser.Tokens[it].Content, _parser.Tokens[it], true); return true; } } return false; } /// /// Searches for equivalent number in a list of tokens. e.g. 08(114) /// /// 匹配自带等效集数的数字,常见于分割放送 /// /// the list of tokens /// true if an equivalent number was found public bool SearchForEquivalentNumbers(List tokens) { foreach (var it in tokens) { // Find number must be isolated. if (_parser.ParseHelper.IsTokenIsolated(it) || !IsValidEpisodeNumber(_parser.Tokens[it].Content)) { continue; } // Find the first enclosed, non-delimiter token var nextToken = Token.FindNextToken(_parser.Tokens, it, Token.TokenFlag.FlagNotDelimiter); if (!_parser.ParseHelper.IsTokenCategory(nextToken, Token.TokenCategory.Bracket)) continue; nextToken = Token.FindNextToken(_parser.Tokens, nextToken, Token.TokenFlag.FlagEnclosed, Token.TokenFlag.FlagNotDelimiter); if (!_parser.ParseHelper.IsTokenCategory(nextToken, Token.TokenCategory.Unknown)) continue; // Check if it's an isolated number if (!_parser.ParseHelper.IsTokenIsolated(nextToken) || !StringHelper.IsNumericString(_parser.Tokens[nextToken].Content) || !IsValidEpisodeNumber(_parser.Tokens[nextToken].Content)) { continue; } var list = new List { _parser.Tokens[it], _parser.Tokens[nextToken] }; list.Sort((o1, o2) => StringHelper.StringToInt(o1.Content) - StringHelper.StringToInt(o2.Content)); SetEpisodeNumber(list[0].Content, list[0], false); SetAlternativeEpisodeNumber(list[1].Content, list[1]); return true; } return false; } /// /// Searches for equivalent number in a list of tokens. e.g. 08(114) /// /// 匹配自带等效集数的数字,常见于分割放送,匹配括号包裹的数字 /// /// the list of tokens /// true if an equivalent number was found public bool SearchForEquivalentNumbersWithBracket(List tokens) { foreach (var it in tokens) { // Find the first enclosed, non-delimiter token var nextToken = Token.FindNextToken(_parser.Tokens, it, Token.TokenFlag.FlagNotDelimiter); if (!Token.InListRange(nextToken, _parser.Tokens) || !(_parser.Tokens[it].Content.Contains("(") || _parser.Tokens[nextToken].Content.Contains(")"))) { continue; } // e.g. [13(341)] if (it > 1 && _parser.Tokens[it].Enclosed && _parser.ParseHelper.IsTokenIsolated(it)) { string[] episodes = _parser.Tokens[it].Content.Split(new string[] { "(", ")" }, StringSplitOptions.RemoveEmptyEntries); if (StringHelper.IsNumericString(episodes[0]) && StringHelper.IsNumericString(episodes[1])) { SetEpisodeNumber(episodes[0], _parser.Tokens[it], false); SetAlternativeEpisodeNumber(episodes[1], _parser.Tokens[it]); return true; } } // e.g. [13 (341)] if (it > 1 && _parser.Tokens[nextToken].Enclosed && _parser.ParseHelper.IsTokenIsolatedWithDelimiterAndBracket(nextToken)) { string episode = _parser.Tokens[nextToken].Content.Replace("(", "").Replace(")", ""); if (StringHelper.IsNumericString(_parser.Tokens[it].Content) && StringHelper.IsNumericString(episode)) { SetEpisodeNumber(_parser.Tokens[it].Content, _parser.Tokens[it], true); SetAlternativeEpisodeNumber(episode, _parser.Tokens[nextToken]); return true; } } } return false; } /// /// Searches for the last number token in a list of tokens /// /// 搜索最后一个数字 /// /// the list of tokens /// true if the last number token was found public bool SearchForLastNumber(List tokens) { for (var i = tokens.Count - 1; i >= 0; i--) { var it = tokens[i]; // Assuming that episode number always comes after the title, // the first token cannot be what we're looking for if (it == 0) continue; if (_parser.Tokens[it].Enclosed) continue; // Ignore if it's the first non-enclosed, non-delimiter token if (_parser.Tokens.GetRange(0, it) .All(r => r.Enclosed || r.Category == Token.TokenCategory.Delimiter)) { continue; } var previousToken = Token.FindPrevToken(_parser.Tokens, it, Token.TokenFlag.FlagNotDelimiter); if (_parser.ParseHelper.IsTokenCategory(previousToken, Token.TokenCategory.Unknown)) { if (_parser.Tokens[previousToken].Content.Equals("Movie", StringComparison.InvariantCultureIgnoreCase) || _parser.Tokens[previousToken].Content.Equals("Part", StringComparison.InvariantCultureIgnoreCase)) { continue; } } // We'll use this number after all if (SetEpisodeNumber(_parser.Tokens[it].Content, _parser.Tokens[it], true)) { return true; } } return false; } } }