using System; using System.Collections.Generic; using System.Xml.Serialization; using System.Linq; using System.Diagnostics; using System.Text; using JetBrains.Annotations; using System.Globalization; namespace SIL.Scripture { /// /// Stores a reference to a specific verse in Scripture. /// public struct VerseRef : IComparable, IComparable, IScrVerseRef { #region Constants [PublicAPI] #if DEBUG public static readonly ScrVers defaultVersification = null; #else public static readonly ScrVers defaultVersification = ScrVers.English; #endif [PublicAPI] public const char verseRangeSeparator = '-'; [PublicAPI] public const char verseSequenceIndicator = ','; [PublicAPI] public static readonly string[] verseRangeSeparators = new[] { verseRangeSeparator.ToString() }; [PublicAPI] public static readonly string[] verseSequenceIndicators = new[] { verseSequenceIndicator.ToString() }; private const int chapterDigitShifter = 1000; private const int bookDigitShifter = chapterDigitShifter * chapterDigitShifter; private const int bcvMaxValue = chapterDigitShifter - 1; private const string rtlMark = "\u200f"; #endregion #region Enumerated types /// /// The valid status of the VerseRef /// public enum ValidStatusType { Valid, UnknownVersification, OutOfRange, VerseOutOfOrder, VerseRepeated } #endregion #region Member variables private short bookNum; private short chapterNum; private short verseNum; private string verse; private ScrVers versification; #endregion #region Constructors /// /// Creates an empty reference with the specified versification /// public VerseRef(ScrVers versification) { bookNum = 0; chapterNum = verseNum = -1; verse = null; this.versification = versification; } public VerseRef(string book, string chapter, string verse, ScrVers versification) : this(versification) { UpdateInternal(book, chapter, verse); } /// /// Creates a new reference /// WARNING: This constructor creates a VerseRef that has no versification. Use with caution! /// public VerseRef(int bbbcccvvv) : this(bbbcccvvv / 1000000, bbbcccvvv % 1000000 / 1000, bbbcccvvv % 1000) { } /// /// Creates a new reference /// public VerseRef(int bbbcccvvv, ScrVers versification) : this(bbbcccvvv) { this.versification = versification; } /// /// Creates a new reference /// WARNING: This constructor creates a VerseRef that has no versification. Use with caution! /// public VerseRef(int bookNum, int chapterNum, int verseNum) : this(defaultVersification) { BookNum = bookNum; ChapterNum = chapterNum; VerseNum = verseNum; } public VerseRef(int bookNum, int chapterNum, int verseNum, ScrVers versification) : this(bookNum, chapterNum, verseNum) { this.versification = versification; } public VerseRef(VerseRef vref) { bookNum = vref.bookNum; chapterNum = vref.chapterNum; verseNum = vref.verseNum; verse = vref.verse; versification = vref.versification; } /// /// Creates a reference by parsing the specified string /// WARNING: This constructor creates a VerseRef that has no versification. Use with caution! /// /// verse string to parse (e.g. "MAT 3:11") /// public VerseRef(string verseStr) : this(defaultVersification) { Parse(verseStr); } /// /// Creates a reference by parsing the specified string /// /// verse string to parse (e.g. "MAT 3:11") /// /// public VerseRef(string verseStr, ScrVers versification) : this(verseStr) { this.versification = versification; } void UpdateInternal(string bookStr, string chapterStr, string verseStr) { BookNum = Canon.BookIdToNumber(bookStr); Chapter = chapterStr; Verse = verseStr; } #endregion #region Attribute Properties (access information about the reference) /// /// Checks to see if a VerseRef hasn't been set - all values are the default. /// public bool IsDefault => bookNum == 0 && chapterNum == 0 && verseNum == 0 && versification == null; /// /// Number of first chapter. /// TODO bro Do we need to make this 0 for intro material? /// [XmlIgnore] [PublicAPI] public int FirstChapter { get { return 1; } } [XmlIgnore] public int LastChapter { get { return versification.GetLastChapter(BookNum); } } [XmlIgnore] public int LastVerse { get { return versification.GetLastVerse(BookNum, ChapterNum); } } /// /// Gets whether the verse is defined as an excluded verse in the versification. /// /// Does not handle verse ranges [XmlIgnore] [PublicAPI] public bool IsExcluded { get { return versification.IsExcluded(BBBCCCVVV); } } /// /// Gets whether the verse has explicit segments defined in the versification. /// /// Does not handle verse ranges [XmlIgnore] [PublicAPI] public bool HasSegmentsDefined { get { return versification != null && versification.VerseSegments(BBBCCCVVV) != null; } } /// /// Gets whether the verse contains multiple verses. /// [XmlIgnore] public bool HasMultiple { get { return verse != null && (verse.IndexOf(verseRangeSeparator) != -1 || verse.IndexOf(verseSequenceIndicator) != -1); } } /// /// Gets or sets the book of the reference. Book is the /// three letter abbreviation in capital letters. e.g. "MAT" /// [XmlIgnore] public string Book { get { return Canon.BookNumberToId(BookNum, string.Empty); } set { BookNum = Canon.BookIdToNumber(value); } } /// /// Gets or sets the chapter of the reference. e.g. "3" /// [XmlIgnore] public string Chapter { get { return IsDefault || chapterNum < 0 ? string.Empty : chapterNum.ToString(); } set { short chapter; chapterNum = short.TryParse(value, out chapter) ? chapter : (short)-1; if (chapterNum < 0) Trace.TraceWarning("Just failed to parse a chapter number: " + value); } } /// /// Gets or sets the verse of the reference e.g. "11" /// [XmlIgnore] public string Verse { get => verse ?? (IsDefault || verseNum < 0 ? string.Empty : verseNum.ToString()); set => TrySetVerse(value, true); // The USX standard only expects support for Latin numerals {0-9}* in verse numbers. } /// /// Value as "BBB C:V". /// This is used for XML serialization. /// [XmlText] public string Text { get { return ToString(); } set { bookNum = 0; chapterNum = -1; verseNum = -1; verse = null; if (versification == null) versification = defaultVersification; try { Parse(value); } catch (VerseRefException e) { // Allow parse to fail during deserialization. VerseRef will just be invalid. Console.WriteLine("Invalid deserialized reference: " + e.InvalidVerseRef); } } } /// /// Tries to set verse and verseNum by parsing the `value` string. /// This is used by Verse.set and TrySetVerseUnicode /// /// true if the verse was set successfully bool TrySetVerse(string value, bool romanOnly) { verse = !TryGetVerseNum(value, romanOnly, out verseNum) ? value.Replace(rtlMark, "") : null; if (verseNum >= 0) return true; Trace.TraceWarning("Just failed to parse a verse number: " + value); TryGetVerseNum(verse, romanOnly, out verseNum); return false; } /// /// Parses a verse string and gets the leading numeric portion as a number. /// /// true if the entire string could be parsed as a single, /// simple verse number (1-999); false if the verse string represented /// a verse bridge, contained segment letters, or was invalid public static bool TryGetVerseNum(string verseStr, bool romanOnly , out short vNum) { if (string.IsNullOrEmpty(verseStr)) { vNum = -1; return true; } vNum = 0; for (int i = 0; i < verseStr.Length; i++) { char ch = verseStr[i]; if (!char.IsSurrogate(ch)) { switch (char.GetUnicodeCategory(ch)) { case UnicodeCategory.DecimalDigitNumber: if (romanOnly ? (ch < '0' || ch > '9') : !char.IsDigit(ch)) { if (i == 0) vNum = -1; return false; } vNum = (short)(vNum * 10 + (romanOnly ? ch - '0' : char.GetNumericValue(ch))); if (vNum > bcvMaxValue) { // whoops, we got too big! vNum = -1; return false; } break; case UnicodeCategory.Surrogate: vNum = (short)(vNum * 10 + (char.GetNumericValue(verseStr, i))); if (vNum > bcvMaxValue) { // whoops, we got too big! vNum = -1; return false; } break; case UnicodeCategory.LetterNumber: case UnicodeCategory.OtherNumber: vNum += (short)char.GetNumericValue(verseStr, i); break; default: if (i == 0) vNum = -1; return false; } } else if (i + 1 < verseStr.Length && char.IsSurrogatePair(verseStr[i], verseStr[i + 1])) { vNum = (short)(vNum * 10 + (char.GetNumericValue(verseStr, i))); if (vNum > bcvMaxValue) { // whoops, we got too big! vNum = -1; return false; } } } return true; } /// /// Gets the reference as a comparable integer where the book, /// chapter, and verse each occupy three digits and the verse is 0. /// public int BBBCCC { get { return GetBBBCCCVVV(bookNum, chapterNum, 0); } } /// /// Gets the reference as a comparable integer where the book, /// chapter, and verse each occupy three digits. If verse is not null /// (i.e., this reference represents a complex reference with verse /// segments or bridge) this cannot be used for an exact comparison. /// public int BBBCCCVVV { get { return GetBBBCCCVVV(bookNum, chapterNum, verseNum); } } /// /// Returns comparable string in the format BBBCCCVVV with the segment /// letter, if any, tacked on the end. /// public string BBBCCCVVVS { get { return BBBCCCVVV.ToString().PadLeft(9, '0') + Segment(); } } /// /// Returns a long hash code for the verse reference which is guaranteed to be unique /// provided that there are no more than 999 books, chapters and/or verses. /// public long LongHashCode { get { return ((long)BBBCCCVVV << 32) + (string.IsNullOrEmpty(verse) ? 0L : verse.GetHashCode()); } } /// /// Get segments associated with this verse, if any. Otherwise, get default segments. /// /// verse segments defined for the current language /// [PublicAPI] public string[] GetSegments(string[] defaultSegments) { if (versification == null) return defaultSegments; string[] segsForThisVerse = versification.VerseSegments(BBBCCCVVV); return segsForThisVerse ?? defaultSegments; } public void AdvanceToLastSegment() { string[] segments = GetSegments(null); if (segments?.Length > 0) Verse += segments[segments.Length - 1]; } /// /// Get the segment from the verse. /// /// valid segments defined for the language or null if not defined or available /// validated segment (according to default segments or versification, if available for verse); /// empty string if no segment or if segment did not validate [PublicAPI] public string Segment(string[] validSegments) { string seg = Segment(); if (seg.Length == 0) return ""; validSegments = GetSegments(validSegments); if (validSegments != null && validSegments.Length > 0) return validSegments.Contains(seg) ? seg : ""; return seg; } /// /// Get the segment number for the verse. /// /// valid segments defined for the language or null if not defined or available /// returns the index of the segment (according to default segments or versification, if available for verse) or /// -1 if no segment or an unknown segment public int SegmentNumber(string[] validSegments) { string seg = Segment(); if (seg.Length == 0) return -1; validSegments = GetSegments(validSegments); if (validSegments != null) return Array.IndexOf(validSegments, seg); return -1; } /// /// Get segment from verse string. /// /// non-validated segment, or empty string if no segment found [PublicAPI] public string Segment() { if (string.IsNullOrEmpty(verse) || !char.IsDigit(verse[0])) return ""; bool foundSegStart = false; StringBuilder strBldr = new StringBuilder(); for (int i = 0; i < verse.Length; i++) { char c = verse[i]; if (c == verseRangeSeparator || c == verseSequenceIndicator) break; if (!char.IsDigit(c)) { foundSegStart = true; strBldr.Append(c); } else if (foundSegStart) break; } return strBldr.ToString(); } /// /// Simplifies this verse ref so that it has no bridging of verses or /// verse segments like "1a". /// public void Simplify() { verse = null; } /// /// Returns verse ref with no bridging, but maintaining segments like "1a". /// /// [PublicAPI] public VerseRef UnBridge() { return AllVerses().FirstOrDefault(); } /// /// String representing the versification (should ONLY be used for serialization/deserialization) /// /// This is for backwards compatibility when ScrVers was an enumeration. [XmlAttribute("Versification")] public string VersificationStr { get { return versification?.Name; } set { versification = value != null ? new ScrVers(value) : null; } } /// /// Gets or sets the versification of the reference. /// Setting this value does not attempt to convert between /// versifications. To do so, use one of the ChangeVersification methods /// [XmlIgnore] public ScrVers Versification { get { return versification; } set { versification = value; } } /// /// Determines if the reference is valid /// public bool Valid { get { return ValidStatus == ValidStatusType.Valid; } } /// /// Get the valid status for this reference. /// [PublicAPI] public ValidStatusType ValidStatus { get { return ValidateVerse(verseRangeSeparators, verseSequenceIndicators); } } /// /// Gets whether a single verse reference is valid. /// ValidStatusType InternalValid { get { // Unknown versification is always invalid if (versification == null) return ValidStatusType.UnknownVersification; // If invalid book, reference is invalid if (bookNum <= 0 || bookNum > Canon.LastBook) return ValidStatusType.OutOfRange; // If non-biblical book, any chapter/verse is valid if (!Canon.IsCanonical(bookNum)) return ValidStatusType.Valid; if (bookNum > versification.GetLastBook() || chapterNum <= 0 || chapterNum > versification.GetLastChapter(bookNum) || VerseNum < 0 || VerseNum > versification.GetLastVerse(bookNum, chapterNum)) { return ValidStatusType.OutOfRange; } return versification.IsExcluded(BBBCCCVVV) ? ValidStatusType.OutOfRange : ValidStatusType.Valid; } } /// /// Attempts to change the versification to the specified versification /// /// new versification to use [PublicAPI] public void ChangeVersification(ScrVers newVersification) { if (!HasMultiple) newVersification.ChangeVersification(ref this); else { VerseRef result; newVersification.ChangeVersificationWithRanges(this, out result); CopyFrom(result); } } /// /// Change the versification of an entry with Verse like 1-3 or 1,3a. /// Can't really work in the most general case because the verse parts could become separate chapters. /// [PublicAPI] public bool ChangeVersificationWithRanges(ScrVers newVersification) { VerseRef temp; bool result = newVersification.ChangeVersificationWithRanges(this, out temp); CopyFrom(temp); return result; } #endregion #region Convert, Copy /// /// Makes a clone of the reference /// /// public VerseRef Clone() { // Leaving this for now to reduce code changes. Isn't really nessary when VerseRef is a struct since a = b is a copy. return new VerseRef(this); } /// /// Copy contents from vref /// /// VerseRef to be copied from public void CopyFrom(VerseRef vref) { bookNum = vref.bookNum; chapterNum = vref.chapterNum; verseNum = vref.verseNum; verse = vref.verse; versification = vref.versification; } /// /// Copies the verse information to this object from vref. /// /// VerseRef to be copied from public void CopyVerseFrom(VerseRef vref) { verseNum = vref.verseNum; verse = vref.verse; } /// /// Parses the reference in the specified string. /// Optionally versification can follow reference as in GEN 3:11/4 /// Throw an exception if /// - invalid book name /// - chapter number is missing or not a number /// - verse number is missing or does not start with a number /// - versifcation is invalid /// /// string to parse e.g. "MAT 3:11" /// [PublicAPI] public void Parse(string verseStr) { verseStr = verseStr.Replace(rtlMark, ""); if (verseStr.IndexOf('/') >= 0) { string[] parts = verseStr.Split('/'); verseStr = parts[0]; if (parts.Length > 1) { try { int scrVerseCode = int.Parse(parts[1].Trim()); versification = new ScrVers((ScrVersType)scrVerseCode); } catch (Exception) { throw new VerseRefException("Invalid reference : " + verseStr) { InvalidVerseRef = verseStr }; } } } string[] b_cv = verseStr.Trim().Split(' '); if (b_cv.Length != 2) throw new VerseRefException("Invalid reference : " + verseStr) { InvalidVerseRef = verseStr }; string[] c_v = b_cv[1].Split(':'); int cnum; if (c_v.Length != 2 || Canon.BookIdToNumber(b_cv[0]) == 0 || !int.TryParse(c_v[0], out cnum) || cnum < 0 || !IsVerseParseable(c_v[1])) { throw new VerseRefException("Invalid reference : " + verseStr) { InvalidVerseRef = verseStr }; } UpdateInternal(b_cv[0], c_v[0], c_v[1]); } public override string ToString() { StringBuilder toStringBuilder = new StringBuilder(20); // length of 20 should get 99.99% of references string book = Book; if (book.Length == 0) return ""; // Handle empty book by just returning empty string - works around a bug in Mono 3. toStringBuilder.Append(book).Append(' ').Append(Chapter).Append(':').Append(Verse); return toStringBuilder.ToString(); } /// /// .e.g GEN 3:11/4. Parse understands this format. /// public string ToStringWithVersification() { return ToString() + "/" + (int)Versification.Type; } public override int GetHashCode() { // ENHANCE: Currently the hashcode can change when the values of the VerseRef change. // This will create problems if/when VerseRef is ever used for a key in a hashtable. // The best thing we could do would be to make VerseRef immutable, but that seems unlikely // to go well. return verse != null ? BBBCCCVVV ^ verse.GetHashCode() : BBBCCCVVV; } #endregion #region Navigation: Book ( e.g. Next/Previous) /// /// Get or set Book based on book number. We still do quite a bit with book numbers /// so lets leave this public. /// /// If BookNum is set to an invalid value [XmlIgnore] public int BookNum { get { return bookNum; } set { if (value <= 0 || value > Canon.LastBook) throw new VerseRefException("BookNum must be greater than zero and less than or equal to last book"); bookNum = (short)value; } } /// /// Gets chapter number. -1 if not valid /// /// If ChapterNum is negative [XmlIgnore] public int ChapterNum { get { return chapterNum; } set { if (value < 0) throw new VerseRefException("ChapterNum can not be negative"); chapterNum = (short)value; } } /// /// Gets verse start number. -1 if not valid /// /// If VerseNum is negative [XmlIgnore] public int VerseNum { get { return verseNum; } set { if (value < 0) throw new VerseRefException("VerseNum can not be negative"); verseNum = (short)value; verse = null; } } // ---------- BOOK NEXT AND PREVIOUS ---------- // NOTES ABOUT ALL NAVIGATION FUNCTIONS: //All the navigiation funcitons have two forms. One takes a BookSet //and constrains Next/Previous within that set. The other takes no //no arguments and constrains Next/Previous only to the entire canon //as Canon presents it to BookSet. /// /// Tries to move to the next book among a set of books present. /// /// Set of books present or selected. /// true if successful public bool NextBook(BookSet present) { int curBook = bookNum; int newBook = present.NextSelected(curBook); if (newBook == curBook) return false; bookNum = (short)newBook; chapterNum = 1; VerseNum = 0; // Use property to reset verse string return true; } /// /// Tries to move to the next book in the entire canon superset. /// /// True if successful. public bool NextBook() { return NextBook(BookSet.AllBooks); } public bool PreviousBook(BookSet present) { int curBook = bookNum; int newBook = present.PreviousSelected(curBook); if (newBook == curBook) return false; //no previous selected book bookNum = (short)newBook; chapterNum = 1; VerseNum = 1; // Use property to reset verse string return true; } public bool PreviousBook() { return PreviousBook(BookSet.AllBooks); } // ---------- CHAPTER NEXT AND PREVIOUS ---------- [PublicAPI] public bool NextChapter(BookSet present, bool skipExcluded) { // If current book doesn't exist, try jump to next. if (!present.IsSelected(bookNum)) return NextBook(present); int newPosition = chapterNum + 1; if (newPosition > LastChapter) return NextBook(present); if (skipExcluded) { var nextRef = versification.FirstIncludedVerse(bookNum, newPosition); if (nextRef != null) CopyFrom(nextRef.Value); else return NextBook(present); } else { chapterNum = (short)newPosition; VerseNum = 1; } return true; } public bool NextChapter(BookSet present) { return NextChapter(present, false); } public bool NextChapter() { return NextChapter(BookSet.AllBooks); } bool PreviousBookLastChapter(BookSet present) { bool result = PreviousBook(present); if (result) chapterNum = (short)(LastChapter != Scripture.Versification.NonCanonicalLastChapterOrVerse ? LastChapter : 1); return result; } public bool PreviousChapter(BookSet present) { // current ref doesn't exist? try find an existing one prior if (!present.IsSelected(bookNum)) return PreviousBookLastChapter(present); int newPosition = chapterNum - 1; if (newPosition < FirstChapter) return PreviousBookLastChapter(present); VerseNum = 1; // Use property to reset verse string chapterNum = (short)newPosition; return true; } public bool PreviousChapter() { return PreviousChapter(BookSet.AllBooks); } // ---------- VERSE NEXT AND PREVIOUS ---------- /// /// Moves to the next verse (or verse segment, if available in the current versification). /// /// true if successful, false if at end of scripture public bool NextVerse(BookSet present) { return NextVerse(present, false); } /// /// Moves to the next verse (or verse segment, if available in the current versification). /// /// true if successful, false if at end of scripture [PublicAPI] public bool NextVerse(BookSet present, bool skipExcluded) { // avoid incrementing through a blank book if (!present.IsSelected(bookNum)) return NextBook(present); string[] verseSegments = Versification.VerseSegments(BBBCCCVVV); if (verseSegments != null) { int segIndex = FindSegment(verseSegments); Debug.Assert(segIndex != -1, "No valid segment found."); if (segIndex != -1) { if (segIndex < verseSegments.Length - 1 && !verseSegments[segIndex].Equals(verseSegments[segIndex + 1])) { // There is another segment and it isn't identical. // (Navigation for an identical segment gets stalled so we just want to go to the next verse.) Verse = verseNum + verseSegments[segIndex + 1]; return true; } } } do { if (verseNum >= versification.GetLastVerse(bookNum, chapterNum)) { if (NextChapter(present, skipExcluded)) { SetVerseWithSegmentInfo(true); return true; } return false; } VerseNum++; // Use property to reset verse string SetVerseWithSegmentInfo(true); } while (skipExcluded && versification.IsExcluded(BBBCCCVVV)); // search for next included verse if needed return true; } public bool NextVerse() { return NextVerse(BookSet.AllBooks); } bool PreviousChapterLastVerse(BookSet present) { bool result; // This current logic prevents simple nav to chapter 0: // Book doesn't exist or we just asked for the chapter before #1 if (!present.IsSelected(bookNum) || chapterNum <= 1) result = PreviousBookLastChapter(present); else { result = true; chapterNum--; } if (result) VerseNum = LastVerse; // Use property to reset verse string return result; } /// /// Moves to the previous verse (or verse segment, if available in the current versification). /// /// true if successful, false if at beginning of scripture public bool PreviousVerse(BookSet present) { // avoid moving through nonexistent books. if (!present.IsSelected(bookNum)) { if (PreviousChapterLastVerse(present)) { SetVerseWithSegmentInfo(false); return true; } return false; } string[] verseSegments = Versification.VerseSegments(GetBBBCCCVVV(bookNum, chapterNum, verseNum)); if (verseSegments != null) { int segIndex = FindSegment(verseSegments); Debug.Assert(segIndex != -1, "No valid segment found"); if (segIndex != -1) { if (segIndex > 0) { Verse = verseNum + verseSegments[segIndex - 1]; return true; } } } // No segment information available for current verse if (verseNum == 1 && chapterNum == 1) VerseNum = 0; // Use property to reset verse string else if (verseNum <= 1) { if (PreviousChapterLastVerse(present)) { SetVerseWithSegmentInfo(false); return true; } return false; } else { VerseNum--; // Use property to reset verse string SetVerseWithSegmentInfo(false); } return true; } public bool PreviousVerse() { return PreviousVerse(BookSet.AllBooks); } /// /// Set the contents of the verse with any applicable segment information after changing the reference /// given whether the verse reference is moving forward or backward. /// void SetVerseWithSegmentInfo(bool movingForward) { string[] verseSegments = Versification.VerseSegments(GetBBBCCCVVV(bookNum, chapterNum, verseNum)); if (verseSegments != null) { string segment = (movingForward) ? verseSegments[0] : verseSegments[verseSegments.Length - 1]; Verse = verseNum + segment; } else verse = null; } /// /// Gets the index of the current segment for this verse given a list of segments for the verse. /// int FindSegment(string[] segments) { for (int iSeg = 0; iSeg < segments.Length; iSeg++) { if (segments[iSeg].Equals(Segment())) return iSeg; } return -1; } #endregion #region Compare: Equals and IComparable Members public override bool Equals(object obj) { if (obj is VerseRef) { VerseRef v = (VerseRef)obj; return (v.bookNum == bookNum) && (v.chapterNum == chapterNum) && (v.verseNum == verseNum) && (v.verse == verse) && (v.versification == versification); } return false; } public int CompareTo(object obj) { if (obj == null) return 1; if (!(obj is VerseRef)) throw new ArgumentException("Object must be of type VerseRef."); return CompareTo((VerseRef) obj); } /// /// Compares two verse refs using the default canon. /// public int CompareTo(VerseRef other) { return CompareTo(other, false); // by default, compare only initial verses in a verse bridge } /// /// Compares two verse refs using the default canon. /// /// the other VerseRef to compare to this one /// if true, compare both the starting and, if it exists, /// the ending verse in a verse bridge; if false compare only the first verse number in a bridge [PublicAPI] public int CompareTo(VerseRef other, bool compareAllVerses) { return CompareTo(other, null, compareAllVerses, true); } /// /// Compares two verse refs using the default canon. /// /// the other VerseRef to compare to this one /// ordered array of verse segments or null to use default segment /// comparison (Unicode value) /// if true, compare both the starting and, if it exists, /// the ending verse in a verse bridge; if false compare only the first verse number in a bridge /// if set to true compare segments; false to ignore /// segment differences. /// [PublicAPI] public int CompareTo(VerseRef other, string[] segmentOrder, bool compareAllVerses, bool compareSegments) { if (other.Versification != Versification) { if (!string.IsNullOrEmpty(other.verse) && (other.verse.IndexOf(verseRangeSeparator) != -1 || other.verse.IndexOf(verseSequenceIndicator) != -1)) { other.ChangeVersificationWithRanges(Versification); } else other.ChangeVersification(Versification); } if (bookNum != other.bookNum) return bookNum - other.bookNum; if (chapterNum != other.chapterNum) return chapterNum - other.chapterNum; if (compareAllVerses) { // compare all available verses (whether a single verse or a verse bridge) return CompareVerses(other); } // REVIEW: Using this method of comparison, two verse references where // one is a verse bridge and the other is the first part of that bridge // (e.g. MAT 1:2-3 and MAT 1:2) will be considered equal. However, the // Equals method will not consider those two references equal. Is this // the behavior we want?) // compare only the first verse bridge if (verseNum != other.verseNum) return verseNum - other.verseNum; if (!compareSegments) return 0; // Comparing same verse so get segment order for this verse string thisSegment = Segment(segmentOrder); string otherSegment = other.Segment(segmentOrder); if (string.IsNullOrEmpty(thisSegment) && string.IsNullOrEmpty(otherSegment)) return 0; if (string.IsNullOrEmpty(thisSegment) && !string.IsNullOrEmpty(otherSegment)) return -1; if (!string.IsNullOrEmpty(thisSegment) && string.IsNullOrEmpty(otherSegment)) return 1; if (segmentOrder != null) { // Both verses have segments. Compare according to custom order. string[] verseSegOrder = GetSegments(segmentOrder); int thisVerseSegIndex = Array.IndexOf(verseSegOrder, thisSegment); int otherVerseSegIndex = Array.IndexOf(verseSegOrder, otherSegment); return thisVerseSegIndex - otherVerseSegIndex; } // REVIEW: is this comparison adequate for a customized verse segment definition? return string.Compare(thisSegment, otherSegment, StringComparison.Ordinal); } /// /// Compare the verses with verses in otherVerse (verses can be a single verse or a verse bridge). /// int CompareVerses(VerseRef otherVerse) { List verseList = GetVerses(); List otherVerseList = otherVerse.GetVerses(); for (int i = 0; i < verseList.Count && i < otherVerseList.Count; i++) { if (verseList[i] != otherVerseList[i]) return verseList[i] - otherVerseList[i]; } return verseList.Count - otherVerseList.Count; } /// /// GetVerses gets a list of verses from the verses specified in this VerseRef. /// private List GetVerses() { // Get verses from the verse strings List verseList = new List(); if (string.IsNullOrEmpty(verse)) { verseList.Add(verseNum); // no bridge or segment info included in verse return verseList; } StringBuilder verseStr = new StringBuilder(); for (int ich = 0; ich < verse.Length; ich++) { if (char.IsDigit(verse[ich])) verseStr.Append(verse[ich]); else if (verseStr.Length > 0) { verseList.Add(int.Parse(verseStr.ToString())); verseStr.Remove(0, verseStr.Length); // clear verse string for next use } } if (verseStr.Length > 0) verseList.Add(int.Parse(verseStr.ToString())); // add any accumulated digits return verseList; } public static bool operator <(VerseRef a, VerseRef b) { return a.CompareTo(b) < 0; } public static bool operator >(VerseRef a, VerseRef b) { return a.CompareTo(b) > 0; } public static bool operator >=(VerseRef a, VerseRef b) { return a.CompareTo(b) >= 0; } public static bool operator <=(VerseRef a, VerseRef b) { return a.CompareTo(b) <= 0; } #endregion #region Methods for parsing and comparing verse numbers /// /// True if there is any overlap between these two verse numbers. /// Verse numbers may be ranges with the form \d+[^-\s]*(-\d+\S*)? /// This is used when we have a list of verses and we wish to extract /// the ones that match a given reference. /// Examples: /// 1, 1: true /// 1b, 1-2c: true /// 1-3, 2-4: true /// 1a, 1b: false /// /// first verse, e.g. 1, 1a, 1-2c /// second verse /// public static bool AreOverlappingVersesRanges(string verse1, string verse2) { string[] verse1Parts = verse1.Split(verseSequenceIndicator); string[] verse2Parts = verse2.Split(verseSequenceIndicator); foreach (string verse1Part in verse1Parts) { foreach (string verse2Part in verse2Parts) { int verse1Num, verse1EndNum, verse2Num, verse2EndNum; string verse1Seg, verse1EndSeg, verse2Seg, verse2EndSeg; ParseVerseNumberRange(verse1Part, out verse1Num, out verse1Seg, out verse1EndNum, out verse1EndSeg); ParseVerseNumberRange(verse2Part, out verse2Num, out verse2Seg, out verse2EndNum, out verse2EndSeg); if (verse1Num == verse1EndNum && verse2Num == verse2EndNum && verse1Seg == verse1EndSeg && verse2Seg == verse2EndSeg) { // no ranges, this is easy if (verse1Num == verse2Num && (verse1Seg == "" || verse2Seg == "" || verse1Seg == verse2Seg)) return true; } else { if (InVerseRange(verse1Num, verse1Seg, verse2Num, verse2Seg, verse2EndNum, verse2EndSeg)) return true; if (InVerseRange(verse1EndNum, verse1EndSeg, verse2Num, verse2Seg, verse2EndNum, verse2EndSeg)) return true; if (InVerseRange(verse2Num, verse2Seg, verse1Num, verse1Seg, verse1EndNum, verse1EndSeg)) return true; if (InVerseRange(verse2EndNum, verse2EndSeg, verse1Num, verse1Seg, verse1EndNum, verse1EndSeg)) return true; } } } return false; } /// /// True if there is any overlap between these two verse references /// public static bool AreOverlappingVersesRanges(VerseRef verseRef1, VerseRef verseRef2) { if (verseRef1.IsDefault || verseRef1.IsDefault) return false; Debug.Assert(verseRef1.Versification == verseRef2.Versification, "Versification of verse references does not match"); // Check simple cases first if (verseRef1.BookNum != verseRef2.BookNum || verseRef1.ChapterNum != verseRef2.ChapterNum) return false; // If both verse references are not complex, then we just need to check for verse number equality if (string.IsNullOrEmpty(verseRef1.verse) && string.IsNullOrEmpty(verseRef2.verse)) return verseRef1.VerseNum == verseRef2.VerseNum; return AreOverlappingVersesRanges(verseRef1.Verse, verseRef2.Verse); } // true if the verse1 (a number and a (possibly empty) verse seg) // lies between verse2 and verse2 end inclusive. // Examples: // 1a, 1b, 1c: false // 1, 1, 1: true // 1b, 1a, 2: true static bool InVerseRange(int verse1, string verse1Seg, int verse2, string verse2Seg, int verse2End, string verse2EndSeg) { if (verse1 < verse2) return false; if (verse1 == verse2 && verse1Seg != "" && verse2Seg != "") { if (string.CompareOrdinal(verse1Seg, verse2Seg) < 0) return false; } if (verse1 > verse2End) return false; if (verse1 == verse2End && verse1Seg != "" && verse2EndSeg != "") { if (string.CompareOrdinal(verse1Seg, verse2EndSeg) > 0) return false; } return true; } // break verse number with an optional range into two parts // Examples: // 1 -> 1, "", 1, "" // 1a-2 -> 1, "a", 2, "" public static void ParseVerseNumberRange(string vNum, out int number1, out string segment1, out int number2, out string segment2) { string[] parts = vNum.Split(verseRangeSeparator, '\u2013', '\u2014'); if (parts.Length == 1) { ParseVerseNumber(parts[0], out number1, out segment1); number2 = number1; segment2 = segment1; return; } ParseVerseNumber(parts[0], out number1, out segment1); ParseVerseNumber(parts[1], out number2, out segment2); } // Break a verse number into a number and an optional segment // Examples: // 1 -> 1, "" // 1a -> 1, "a" public static void ParseVerseNumber(string vNum, out int number, out string segment) { int j; for (j = 0; j < vNum.Length && char.IsDigit(vNum[j]); ++j) { } number = 0; if (j > 0) { string num = vNum.Substring(0, j); int.TryParse(num, out number); // Can't fail, we have already validated digits } segment = vNum.Substring(j); } /// /// Parses a verse string and gets the leading numeric portion as a number. /// Functionally identical to Verse.Set for Roman numbers, made distinct to preserve USX standard. /// /// true if the entire string could be parsed as a single, /// simple verse number in any supported script; false if the verse string represented /// a verse bridge, contained segment letters, or was invalid public bool TrySetVerseUnicode(string value) { return TrySetVerse(value, false); } /// /// Returns whether any of the specified references overlap with this one /// /// public bool OverlapsAny(params VerseRef[] compareTo) { VerseRef temp = this; return compareTo.Any(vref => AreOverlappingVersesRanges(temp, vref)); } /// /// Determines if this reference falls in the specified range of verses. /// /// The start of the range (inclusive) /// The end of the range (inclusive) /// True to require an exact match of the range start or /// range end if it does not fall strictly between the range start and end; /// False to accept partial overlaps at the start or end of the range public bool InRange(VerseRef rangeStart, VerseRef rangeEnd, bool exact) { if (rangeStart < this && this < rangeEnd) return true; if (exact) return rangeStart.Equals(this) || rangeEnd.Equals(this); return BBBCCCVVV == rangeStart.BBBCCCVVV || BBBCCCVVV == rangeEnd.BBBCCCVVV || OverlapsAny(rangeStart, rangeEnd); } #endregion #region Other public methods /// /// Gets the reference as a comparable integer where the book, /// chapter, and verse each occupy three digits. /// public static int GetBBBCCCVVV(int bookNum, int chapterNum, int verseNum) { return (bookNum % bcvMaxValue) * bookDigitShifter + (chapterNum >= 0 ? (chapterNum % bcvMaxValue) * chapterDigitShifter : 0) + (verseNum >= 0 ? (verseNum % bcvMaxValue) : 0); } /// /// Enumerate all individual verses contained in a VerseRef. /// Verse ranges are indicated by "-" and consecutive verses by ","s. /// Examples: /// GEN 1:2 returns GEN 1:2 /// GEN 1:1a-3b,5 returns GEN 1:1a, GEN 1:2, GEN 1:3b, GEN 1:5 /// GEN 1:2a-2c returns //! ?????? /// /// if set to true return only verses that are explicitly specified only, /// not verses within a range. /// public IEnumerable AllVerses(bool specifiedVersesOnly = false) { return AllVerses(specifiedVersesOnly, verseRangeSeparators, verseSequenceIndicators); } public IEnumerable AllVerses(bool specifiedVersesOnly, string[] verseRangeSeparators, string[] verseSequenceSeparators) { if (verse == null || ChapterNum <= 0) yield return Clone(); else { VerseRef vref; int book = BookNum; int chapter = ChapterNum; string[] parts = verse.Split(verseSequenceSeparators, StringSplitOptions.None); foreach ( string[] pieces in parts.Select(part => part.Split(verseRangeSeparators, StringSplitOptions.None))) { vref = Clone(); vref.Verse = pieces[0]; int startVerse = vref.VerseNum; yield return vref; if (pieces.Length > 1) { VerseRef vlast = Clone(); vlast.Verse = pieces[1]; if (!specifiedVersesOnly) { // get all verses within a range for (int verseNum = startVerse + 1; verseNum < vlast.VerseNum; verseNum++) { VerseRef verseInRange = new VerseRef(book, chapter, verseNum, versification); if (!verseInRange.IsExcluded) yield return new VerseRef(book, chapter, verseNum, versification); } } yield return vlast; } } } } /// /// Gets the single verses or verse ranges that are represented in this verse. /// public IEnumerable GetRanges() { if (verse == null || ChapterNum <= 0) yield return Clone(); else { string[] ranges = verse.Split(','); foreach (string range in ranges) { VerseRef vRef = Clone(); vRef.Verse = range; yield return vRef; } } } /// /// Tests if the string can be parsed into a verse reference /// /// /// true if parsable public static bool IsParseable(string str) { if (string.IsNullOrEmpty(str)) return false; VerseRef dummy; return TryParse(str, out dummy); } /// /// Determines if the verse string is in a valid format (does not consider versification). /// [PublicAPI] public static bool IsVerseParseable(string verse) { return verse.Length != 0 && char.IsDigit(verse[0]) && verse[verse.Length - 1] != verseRangeSeparator && verse[verse.Length - 1] != verseSequenceIndicator; } /// /// Tries to parse the specified string into a verse reference /// /// The string to attempt to parse /// The result of the parse if successful, or default if it failed /// True if the specified string was successfully parsed, false otherwise [PublicAPI] public static bool TryParse(string str, out VerseRef vref) { try { vref = new VerseRef(str); return true; } catch (VerseRefException) { vref = default; return false; } } /// /// Validates a verse number using the supplied separators rather than the defaults. /// [PublicAPI] public ValidStatusType ValidateVerse(string[] verseRangeSeparators, string[] verseSequenceSeparators) { if (string.IsNullOrEmpty(verse)) return InternalValid; int prevVerse = 0; foreach (VerseRef vRef in AllVerses(true, verseRangeSeparators, verseSequenceSeparators)) { ValidStatusType validStatus = vRef.InternalValid; if (validStatus != ValidStatusType.Valid) return validStatus; int bbbcccvvv = vRef.BBBCCCVVV; if (prevVerse > bbbcccvvv) return ValidStatusType.VerseOutOfOrder; if (prevVerse == bbbcccvvv) return ValidStatusType.VerseRepeated; prevVerse = bbbcccvvv; } return ValidStatusType.Valid; // TODO: make Valid tests Valid Status tests } #endregion #region IScrVerseRef-specific implementation public IScrVerseRef Create(string book, string chapter, string verse) { return new VerseRef(book, chapter, verse, Versification); } IScrVerseRef IScrVerseRef.Clone() => Clone(); IScrVerseRef IScrVerseRef.UnBridge() => UnBridge(); public bool VersificationHasVerseSegments => Versification.HasVerseSegments; #endregion } #region VerseRefException class /// /// Indicates a problem in the verse reference. /// [Serializable] public class VerseRefException : ApplicationException { public VerseRefException(string message) : base(message) { } public string InvalidVerseRef; } #endregion }