diff --git a/src/SIL.Machine/Corpora/DblBundleTextCorpus.cs b/src/SIL.Machine/Corpora/DblBundleTextCorpus.cs index 6a6f2d730..923f16d25 100644 --- a/src/SIL.Machine/Corpora/DblBundleTextCorpus.cs +++ b/src/SIL.Machine/Corpora/DblBundleTextCorpus.cs @@ -38,8 +38,11 @@ public DblBundleTextCorpus(string fileName) doc.Root.Elements("identification").Elements("abbreviation").FirstOrDefault(); using (CorporaUtils.VersificationLock.Lock()) { - Versification = Scripture.Versification.Table.Implementation.Load(tempFile.Path, abbr); - Scripture.Versification.Table.Implementation.RemoveAllUnknownVersifications(); + Versification = SIL.Scripture.Versification.Table.Implementation.Load( + tempFile.Path, + abbr + ); + SIL.Scripture.Versification.Table.Implementation.RemoveAllUnknownVersifications(); } } } diff --git a/src/SIL.Machine/Corpora/UsxFileTextCorpus.cs b/src/SIL.Machine/Corpora/UsxFileTextCorpus.cs index 5682c8898..322c812f8 100644 --- a/src/SIL.Machine/Corpora/UsxFileTextCorpus.cs +++ b/src/SIL.Machine/Corpora/UsxFileTextCorpus.cs @@ -18,8 +18,8 @@ private static ScrVers GetVersification(string projectPath, ScrVers versificatio if (versification == null && File.Exists(versificationFileName)) { string vrsName = Path.GetFileName(projectPath); - versification = Scripture.Versification.Table.Implementation.Load(versificationFileName, vrsName); - Scripture.Versification.Table.Implementation.RemoveAllUnknownVersifications(); + versification = SIL.Scripture.Versification.Table.Implementation.Load(versificationFileName, vrsName); + SIL.Scripture.Versification.Table.Implementation.RemoveAllUnknownVersifications(); } return versification ?? ScrVers.English; } diff --git a/src/SIL.Machine/Scripture/ScriptureRangeParser.cs b/src/SIL.Machine/Scripture/ScriptureRangeParser.cs index ddd40ca50..52741d3cc 100644 --- a/src/SIL.Machine/Scripture/ScriptureRangeParser.cs +++ b/src/SIL.Machine/Scripture/ScriptureRangeParser.cs @@ -5,244 +5,280 @@ using SIL.Extensions; using SIL.Scripture; -public class ScriptureRangeParser +namespace SIL.Machine.Scripture { - private readonly Dictionary _bookLengths = new Dictionary(); - private static readonly Regex CommaSeparatedBooks = new Regex( - @"^([A-Z\d]{3}|OT|NT)(, ?([A-Z\d]{3}|OT|NT))*$", - RegexOptions.Compiled - ); - private static readonly Regex BookRange = new Regex(@"^-?[A-Z\d]{3}-[A-Z\d]{3}$", RegexOptions.Compiled); - private static readonly Regex ChapterSelection = new Regex( - @"^-?[A-Z\d]{3} ?(\d+|\d+-\d+)(, ?(\d+|\d+-\d+))*$", - RegexOptions.Compiled - ); - - public static Dictionary> GetChapters(string chapterSelections, ScrVers versification = null) + public class ScriptureRangeParser { - return new ScriptureRangeParser(versification).GetChapters(chapterSelections); - } + private readonly Dictionary _bookLengths = new Dictionary(); - public ScriptureRangeParser(ScrVers versification = null) - { - if (versification == null) - versification = ScrVers.Original; - foreach ((string bookId, int bookNum) in Canon.AllBookIds.Zip(Canon.AllBookNumbers)) - { - _bookLengths[bookId] = versification.GetLastChapter(bookNum); - } - } + private static readonly Regex CommaSeparatedBooks = new Regex( + @"^([A-Z\d]{3}|OT|NT)(, ?([A-Z\d]{3}|OT|NT))*$", + RegexOptions.Compiled + ); - private Dictionary> ParseSection(string section) - { - section = section.Trim(); - Dictionary> chaptersPerBook = new Dictionary>(); + private static readonly Regex BookRange = new Regex(@"^-?[A-Z\d]{3}-[A-Z\d]{3}$", RegexOptions.Compiled); + + private static readonly Regex ChapterSelection = new Regex( + @"^-?[A-Z\d]{3} ?(\d+|\d+-\d+)(, ?(\d+|\d+-\d+))*$", + RegexOptions.Compiled + ); + + public static Dictionary> GetChapters( + string chapterSelections, + ScrVers versification = null + ) => new ScriptureRangeParser(versification).GetChapters(chapterSelections); - //*Specific chapters from one book* - if (char.IsDigit(section.Last()) && section.Length > 3) + public static bool TryGetChapters( + string chapterSelections, + ScrVers versification, + out Dictionary> chapters + ) => new ScriptureRangeParser(versification).TryGetChapters(chapterSelections, out chapters); + + public ScriptureRangeParser(ScrVers versification = null) { - string bookName = section.Substring(0, 3); - if (!_bookLengths.ContainsKey(bookName)) + if (versification == null) + versification = ScrVers.Original; + foreach ((string bookId, int bookNum) in Canon.AllBookIds.Zip(Canon.AllBookNumbers)) { - throw new ArgumentException($"{bookName} is an invalid book ID."); + _bookLengths[bookId] = versification.GetLastChapter(bookNum); } + } - HashSet chapters = new HashSet(); + private Dictionary> ParseSection(string section) + { + section = section.Trim(); + Dictionary> chaptersPerBook = new Dictionary>(); - int lastChapter = _bookLengths[bookName]; - string[] chapterRangeStrings = section.Substring(3).Split(','); - foreach (string chapterRangeString in chapterRangeStrings.Select(s => s.Trim())) + //*Specific chapters from one book* + if (char.IsDigit(section.Last()) && section.Length > 3) { - if (chapterRangeString.Contains('-')) + string bookName = section.Substring(0, 3); + if (!_bookLengths.ContainsKey(bookName)) { - string[] startAndEnd = chapterRangeString.Split('-'); - int start, - end; - if (!(int.TryParse(startAndEnd[0], out start) && int.TryParse(startAndEnd[1], out end))) - { - throw new ArgumentException($"{chapterRangeString} is an invalid chapter range."); - } - if (start == 0 || end > lastChapter || end <= start) + throw new ArgumentException($"{bookName} is an invalid book ID."); + } + + HashSet chapters = new HashSet(); + + int lastChapter = _bookLengths[bookName]; + string[] chapterRangeStrings = section.Substring(3).Split(','); + foreach (string chapterRangeString in chapterRangeStrings.Select(s => s.Trim())) + { + if (chapterRangeString.Contains('-')) { - throw new ArgumentException($"{chapterRangeString} is an invalid chapter range."); + string[] startAndEnd = chapterRangeString.Split('-'); + int start, + end; + if (!(int.TryParse(startAndEnd[0], out start) && int.TryParse(startAndEnd[1], out end))) + { + throw new ArgumentException($"{chapterRangeString} is an invalid chapter range."); + } + + if (start == 0 || end > lastChapter || end <= start) + { + throw new ArgumentException($"{chapterRangeString} is an invalid chapter range."); + } + + for (int chapterNum = start; chapterNum <= end; chapterNum++) + { + chapters.Add(chapterNum); + } } - for (int chapterNum = start; chapterNum <= end; chapterNum++) + else { + int chapterNum; + if (!int.TryParse(chapterRangeString, out chapterNum)) + { + throw new ArgumentException($"{section} is an invalid chapter number."); + } + + if (chapterNum > lastChapter) + { + throw new ArgumentException($"{section} is an invalid chapter number."); + } + chapters.Add(chapterNum); } } + + if (chapters.Count() == lastChapter) + { + chaptersPerBook[bookName] = new List(); + } else { - int chapterNum; - if (!int.TryParse(chapterRangeString, out chapterNum)) - { - throw new ArgumentException($"{section} is an invalid chapter number."); - } - if (chapterNum > lastChapter) - { - throw new ArgumentException($"{section} is an invalid chapter number."); - } - chapters.Add(chapterNum); + chaptersPerBook[bookName] = chapters.ToList(); + chaptersPerBook[bookName].Sort(); } } - if (chapters.Count() == lastChapter) + //*Ranges of books to be added* + else if (section.Contains('-')) { - chaptersPerBook[bookName] = new List(); + string[] startAndEnd = section.Split('-'); + if ( + startAndEnd.Length != 2 + || !_bookLengths.ContainsKey(startAndEnd[0]) + || !_bookLengths.ContainsKey(startAndEnd[1]) + || Canon.BookIdToNumber(startAndEnd[1]) <= Canon.BookIdToNumber(startAndEnd[0]) + ) + { + throw new ArgumentException($"{section} is an invalid book range."); + } + + for ( + int bookNum = Canon.BookIdToNumber(startAndEnd[0]); + bookNum <= Canon.BookIdToNumber(startAndEnd[1]); + bookNum++ + ) + { + chaptersPerBook[Canon.BookNumberToId(bookNum)] = new List(); + } } - else + //*OT* + else if (section == "OT") { - chaptersPerBook[bookName] = chapters.ToList(); - chaptersPerBook[bookName].Sort(); + for (int bookNum = 1; bookNum <= 39; bookNum++) + { + chaptersPerBook[Canon.BookNumberToId(bookNum)] = new List(); + } } - } - //*Ranges of books to be added* - else if (section.Contains('-')) - { - string[] startAndEnd = section.Split('-'); - if ( - startAndEnd.Length != 2 - || !_bookLengths.ContainsKey(startAndEnd[0]) - || !_bookLengths.ContainsKey(startAndEnd[1]) - || Canon.BookIdToNumber(startAndEnd[1]) <= Canon.BookIdToNumber(startAndEnd[0]) - ) + //*NT* + else if (section == "NT") { - throw new ArgumentException($"{section} is an invalid book range."); + for (int bookNum = 40; bookNum <= 66; bookNum++) + { + chaptersPerBook[Canon.BookNumberToId(bookNum)] = new List(); + } } - for ( - int bookNum = Canon.BookIdToNumber(startAndEnd[0]); - bookNum <= Canon.BookIdToNumber(startAndEnd[1]); - bookNum++ - ) + //*Whole book* + else { - chaptersPerBook[Canon.BookNumberToId(bookNum)] = new List(); + if (!_bookLengths.ContainsKey(section)) + { + throw new ArgumentException($"{section} is an invalid book ID."); + } + + chaptersPerBook[section] = new List(); } + + return chaptersPerBook; } - //*OT* - else if (section == "OT") + + public Dictionary> GetChapters(string chapterSelections) { - for (int bookNum = 1; bookNum <= 39; bookNum++) + Dictionary> chaptersPerBook = new Dictionary>(); + chapterSelections = chapterSelections.Trim(); + + if (chapterSelections.Length == 0) { - chaptersPerBook[Canon.BookNumberToId(bookNum)] = new List(); + return chaptersPerBook; } - } - //*NT* - else if (section == "NT") - { - for (int bookNum = 40; bookNum <= 66; bookNum++) + + char delimiter = ';'; + if (chapterSelections.Contains(';')) { - chaptersPerBook[Canon.BookNumberToId(bookNum)] = new List(); + delimiter = ';'; } - } - //*Whole book* - else - { - if (!_bookLengths.ContainsKey(section)) + else if (CommaSeparatedBooks.IsMatch(chapterSelections)) { - throw new ArgumentException($"{section} is an invalid book ID."); + delimiter = ','; + } + else if (!BookRange.IsMatch(chapterSelections) && !ChapterSelection.IsMatch(chapterSelections)) + { + throw new ArgumentException( + "Invalid syntax. If you are providing multiple selections, e.g. a range of books followed by a selection of chapters from a book, separate each selection with a semicolon." + ); } - chaptersPerBook[section] = new List(); - } - - return chaptersPerBook; - } - - public Dictionary> GetChapters(string chapterSelections) - { - Dictionary> chaptersPerBook = new Dictionary>(); - chapterSelections = chapterSelections.Trim(); - - if (chapterSelections.Length == 0) - { - return chaptersPerBook; - } - char delimiter = ';'; - if (chapterSelections.Contains(';')) - { - delimiter = ';'; - } - else if (CommaSeparatedBooks.IsMatch(chapterSelections)) - { - delimiter = ','; - } - else if (!BookRange.IsMatch(chapterSelections) && !ChapterSelection.IsMatch(chapterSelections)) - { - throw new ArgumentException( - "Invalid syntax. If you are providing multiple selections, e.g. a range of books followed by a selection of chapters from a book, separate each selection with a semicolon." - ); - } - string[] selections = chapterSelections.Split(delimiter); - foreach (string section in selections.Select(s => s.Trim())) - { - //*Subtraction* - if (section.StartsWith("-")) + string[] selections = chapterSelections.Split(delimiter); + foreach (string section in selections.Select(s => s.Trim())) { - Dictionary> sectionChapters = ParseSection(section.Substring(1)); - foreach (string bookName in sectionChapters.Keys) + //*Subtraction* + if (section.StartsWith("-")) { - if (!chaptersPerBook.ContainsKey(bookName)) + Dictionary> sectionChapters = ParseSection(section.Substring(1)); + foreach (string bookName in sectionChapters.Keys) { - throw new ArgumentException( - $"{bookName} cannot be removed as it is not in the existing book selection." - ); - } + if (!chaptersPerBook.ContainsKey(bookName)) + { + throw new ArgumentException( + $"{bookName} cannot be removed as it is not in the existing book selection." + ); + } - if (sectionChapters[bookName].Count() == 0) - { - sectionChapters[bookName] = Enumerable.Range(1, _bookLengths[bookName]).ToList(); - } + if (sectionChapters[bookName].Count() == 0) + { + sectionChapters[bookName] = Enumerable.Range(1, _bookLengths[bookName]).ToList(); + } - if (chaptersPerBook[bookName].Count() == 0) - { - chaptersPerBook[bookName] = Enumerable.Range(1, _bookLengths[bookName]).ToList(); - } + if (chaptersPerBook[bookName].Count() == 0) + { + chaptersPerBook[bookName] = Enumerable.Range(1, _bookLengths[bookName]).ToList(); + } - foreach (int chapterNumber in sectionChapters[bookName]) - { - if (!chaptersPerBook[bookName].Remove(chapterNumber)) + foreach (int chapterNumber in sectionChapters[bookName]) { - throw new ArgumentException( - $"{chapterNumber} cannot be removed as it is not in the existing chapter selection." - ); + if (!chaptersPerBook[bookName].Remove(chapterNumber)) + { + throw new ArgumentException( + $"{chapterNumber} cannot be removed as it is not in the existing chapter selection." + ); + } } - } - if (chaptersPerBook[bookName].Count() == 0) - { - chaptersPerBook.Remove(bookName); + if (chaptersPerBook[bookName].Count() == 0) + { + chaptersPerBook.Remove(bookName); + } } } - } - //*Addition* - else - { - Dictionary> sectionChapters = ParseSection(section); - foreach (string bookName in sectionChapters.Keys) + //*Addition* + else { - if (chaptersPerBook.ContainsKey(bookName)) + Dictionary> sectionChapters = ParseSection(section); + foreach (string bookName in sectionChapters.Keys) { - if (chaptersPerBook[bookName].Count() == 0 || sectionChapters[bookName].Count() == 0) + if (chaptersPerBook.ContainsKey(bookName)) { - chaptersPerBook[bookName] = new List(); - continue; + if (chaptersPerBook[bookName].Count() == 0 || sectionChapters[bookName].Count() == 0) + { + chaptersPerBook[bookName] = new List(); + continue; + } + + chaptersPerBook[bookName] = chaptersPerBook[bookName] + .Concat(sectionChapters[bookName]) + .Distinct() + .ToList(); + chaptersPerBook[bookName].Sort(); + if (chaptersPerBook[bookName].Count() == _bookLengths[bookName]) + { + chaptersPerBook[bookName] = new List(); + } } - chaptersPerBook[bookName] = chaptersPerBook[bookName] - .Concat(sectionChapters[bookName]) - .Distinct() - .ToList(); - chaptersPerBook[bookName].Sort(); - if (chaptersPerBook[bookName].Count() == _bookLengths[bookName]) + else { - chaptersPerBook[bookName] = new List(); + chaptersPerBook[bookName] = sectionChapters[bookName]; } } - else - { - chaptersPerBook[bookName] = sectionChapters[bookName]; - } } } + + return chaptersPerBook; + } + + public bool TryGetChapters(string chapterSelections, out Dictionary> chapters) + { + try + { + chapters = GetChapters(chapterSelections); + return true; + } + catch (ArgumentException) + { + chapters = null; + return false; + } } - return chaptersPerBook; } } diff --git a/tests/SIL.Machine.Tests/PunctuationAnalysis/ParatextProjectQuoteConventionDetectorTests.cs b/tests/SIL.Machine.Tests/PunctuationAnalysis/ParatextProjectQuoteConventionDetectorTests.cs index 6b10ddbff..973ae3cc7 100644 --- a/tests/SIL.Machine.Tests/PunctuationAnalysis/ParatextProjectQuoteConventionDetectorTests.cs +++ b/tests/SIL.Machine.Tests/PunctuationAnalysis/ParatextProjectQuoteConventionDetectorTests.cs @@ -1,5 +1,6 @@ using NUnit.Framework; using SIL.Machine.Corpora; +using SIL.Machine.Scripture; using SIL.Scripture; namespace SIL.Machine.PunctuationAnalysis; diff --git a/tests/SIL.Machine.Tests/Scripture/ScriptureRangeParserTests.cs b/tests/SIL.Machine.Tests/Scripture/ScriptureRangeParserTests.cs index eaabf8c47..c3cf72de8 100644 --- a/tests/SIL.Machine.Tests/Scripture/ScriptureRangeParserTests.cs +++ b/tests/SIL.Machine.Tests/Scripture/ScriptureRangeParserTests.cs @@ -6,9 +6,8 @@ namespace SIL.Machine.Scripture; [TestFixture] public class ScriptureRangeParserTests { - [Test] [TestCaseSource(nameof(GetCases))] - public void TestParse(string rangeString, Dictionary> expectedOutput, bool throwsException) + public void GetChapters(string rangeString, Dictionary> expectedOutput, bool throwsException) { var parser = new ScriptureRangeParser(); if (!throwsException) @@ -24,18 +23,41 @@ public void TestParse(string rangeString, Dictionary> expected } } - public static IEnumerable GetCases() + [TestCaseSource(nameof(GetCases))] + public void TryGetChapters(string rangeString, Dictionary> expectedOutput, bool throwsException) { - yield return new TestCaseData("MAL", new Dictionary> { { "MAL", new List() } }, false); - yield return new TestCaseData("PS2", new Dictionary> { { "PS2", new List() } }, false); + var parser = new ScriptureRangeParser(); + bool actual = parser.TryGetChapters(rangeString, out Dictionary> chapters); + if (!throwsException) + { + using (Assert.EnterMultipleScope()) + { + Assert.That(chapters, Is.EquivalentTo(expectedOutput)); + Assert.That(actual, Is.True); + } + } + else + { + using (Assert.EnterMultipleScope()) + { + Assert.That(chapters, Is.Null); + Assert.That(actual, Is.False); + } + } + } + + private static IEnumerable GetCases() + { + yield return new TestCaseData("MAL", new Dictionary> { { "MAL", [] } }, false); + yield return new TestCaseData("PS2", new Dictionary> { { "PS2", [] } }, false); yield return new TestCaseData( "GEN,EXO", - new Dictionary> { { "GEN", new List() }, { "EXO", new List() } }, + new Dictionary> { { "GEN", [] }, { "EXO", [] } }, false ); yield return new TestCaseData( "1JN,2JN", - new Dictionary> { { "1JN", new List() }, { "2JN", new List() } }, + new Dictionary> { { "1JN", [] }, { "2JN", [] } }, false ); yield return new TestCaseData( @@ -55,43 +77,23 @@ public static IEnumerable GetCases() ); yield return new TestCaseData( "MAT;MRK", - new Dictionary> { { "MAT", new List() }, { "MRK", new List() } }, + new Dictionary> { { "MAT", [] }, { "MRK", [] } }, false ); yield return new TestCaseData( "MAT; MRK", - new Dictionary> { { "MAT", new List() }, { "MRK", new List() } }, - false - ); - yield return new TestCaseData( - "MAT1,2,3", - new Dictionary> - { - { - "MAT", - new List() { 1, 2, 3 } - }, - }, - false - ); - yield return new TestCaseData( - "MAT1, 2, 3", - new Dictionary> - { - { - "MAT", - new List() { 1, 2, 3 } - }, - }, + new Dictionary> { { "MAT", [] }, { "MRK", [] } }, false ); + yield return new TestCaseData("MAT1,2,3", new Dictionary> { { "MAT", [1, 2, 3] } }, false); + yield return new TestCaseData("MAT1, 2, 3", new Dictionary> { { "MAT", [1, 2, 3] } }, false); yield return new TestCaseData( "MAT-LUK", new Dictionary> { - { "MAT", new List() }, - { "MRK", new List() }, - { "LUK", new List() }, + { "MAT", [] }, + { "MRK", [] }, + { "LUK", [] }, }, false ); @@ -99,9 +101,9 @@ public static IEnumerable GetCases() "MAT1,2,3;MAT-LUK", new Dictionary> { - { "MAT", new List() }, - { "MRK", new List() }, - { "LUK", new List() }, + { "MAT", [] }, + { "MRK", [] }, + { "LUK", [] }, }, false ); @@ -109,38 +111,32 @@ public static IEnumerable GetCases() "2JN-3JN;EXO1,8,3-5;GEN", new Dictionary> { - { "GEN", new List() }, - { - "EXO", - new List() { 1, 3, 4, 5, 8 } - }, - { "2JN", new List() }, - { "3JN", new List() }, + { "GEN", [] }, + { "EXO", [1, 3, 4, 5, 8] }, + { "2JN", [] }, + { "3JN", [] }, }, false ); yield return new TestCaseData( "1JN 1;1JN 2;1JN 3-5", - new Dictionary> { { "1JN", new List() } }, + new Dictionary> { { "1JN", [] } }, false ); yield return new TestCaseData( "MAT-ROM;-ACT4-28", new Dictionary> { - { "MAT", new List() }, - { "MRK", new List() }, - { "LUK", new List() }, - { "JHN", new List() }, - { - "ACT", - new List() { 1, 2, 3 } - }, + { "MAT", [] }, + { "MRK", [] }, + { "LUK", [] }, + { "JHN", [] }, + { "ACT", [1, 2, 3] }, { "ROM", new List() }, }, false ); - yield return new TestCaseData("2JN;-2JN 1", new Dictionary> { }, false); + yield return new TestCaseData("2JN;-2JN 1", new Dictionary>(), false); yield return new TestCaseData( "NT;OT;-MRK;-EXO", Enumerable @@ -156,27 +152,20 @@ public static IEnumerable GetCases() .Range(40, 27) .Select(i => { - if (i == 40) + return i switch { - return ( + 40 => ( Canon.BookNumberToId(i), - Enumerable.Range(1, 28).Where(c => !(c == 3 || c == 4 || c == 5 || c == 17)).ToList() - ); - } - if (i == 66) - { - return (Canon.BookNumberToId(i), Enumerable.Range(1, 20).ToList()); - } - return (Canon.BookNumberToId(i), new List()); + Enumerable.Range(1, 28).Where(c => c is not (3 or 4 or 5 or 17)).ToList() + ), + 66 => (Canon.BookNumberToId(i), [.. Enumerable.Range(1, 20)]), + _ => (Canon.BookNumberToId(i), []), + }; }) .ToDictionary(), false ); - yield return new TestCaseData( - "MAT-JHN;-MAT-LUK", - new Dictionary> { { "JHN", new List() } }, - false - ); + yield return new TestCaseData("MAT-JHN;-MAT-LUK", new Dictionary> { { "JHN", [] } }, false); yield return new TestCaseData("", new Dictionary>(), false); //*Throw exceptions