1   /**
2    * Distribution License:
3    * JSword is free software; you can redistribute it and/or modify it under
4    * the terms of the GNU Lesser General Public License, version 2.1 or later
5    * as published by the Free Software Foundation. This program is distributed
6    * in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
7    * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
8    * See the GNU Lesser General Public License for more details.
9    *
10   * The License is available on the internet at:
11   *      http://www.gnu.org/copyleft/lgpl.html
12   * or by writing to:
13   *      Free Software Foundation, Inc.
14   *      59 Temple Place - Suite 330
15   *      Boston, MA 02111-1307, USA
16   *
17   * © CrossWire Bible Society, 2012 - 2016
18   *
19   */
20  package org.crosswire.jsword.versification;
21  
22  import java.io.PrintStream;
23  import java.io.Serializable;
24  import java.util.Iterator;
25  
26  import org.crosswire.jsword.JSMsg;
27  import org.crosswire.jsword.JSOtherMsg;
28  import org.crosswire.jsword.book.ReferenceSystem;
29  import org.crosswire.jsword.passage.NoSuchVerseException;
30  import org.crosswire.jsword.passage.Verse;
31  import org.crosswire.jsword.passage.VerseRange;
32  
33  /**
34   * A named Versification defines
35   * the order of BibleBooks by Testament,
36   * the number of chapters in each BibleBook,
37   * the number of verses in each chapter.
38   *
39   * @see gnu.lgpl.License The GNU Lesser General Public License for details.
40   * @author DM Smith
41   */
42  public class Versification implements ReferenceSystem, Serializable {
43      public Versification() {
44      }
45  
46      /**
47       * Construct a Versification.
48       * 
49       * @param name
50       *            The name of this reference system
51       * @param booksOT
52       *            An ordered list of books in this reference system. The list
53       *            should not include INTRO_BIBLE, or INTRO_OT.
54       * @param booksNT
55       *            An ordered list of books in this reference system. The list
56       *            should not include INTRO_NT.
57       * @param lastVerseOT
58       *            For each book in booksOT, this has an array with one entry for
59       *            each chapter whose value is the highest numbered verse in that
60       *            chapter. Do not include chapter 0.
61       * @param lastVerseNT
62       *            For each book in booksNT, this has an array with one entry for
63       *            each chapter whose value is the highest numbered verse in that
64       *            chapter. Do not include chapter 0.
65       */
66      public Versification(String name, BibleBook[] booksOT, BibleBook[] booksNT, int[][] lastVerseOT, int[][] lastVerseNT) {
67          this.name = name;
68  
69          // Copy the books into an aggregated BibleBook array
70          // including INTRO_BIBLE and INTRO_OT/INTRO_NT for non-null book lists
71          int bookCount = 1; // Always include the INTRO_BIBLE
72          if (booksOT.length > 0) {
73              bookCount += booksOT.length + 1; // All of the OT books and INTRO_OT
74          }
75  
76          int ntStart = bookCount;
77          if (booksNT.length > 0) {
78              bookCount += booksNT.length + 1; // All of the NT books and INTRO_NT
79          }
80          BibleBook[] books = new BibleBook[bookCount];
81          books[0] = BibleBook.INTRO_BIBLE;
82          if (booksOT.length > 0) {
83              books[1] = BibleBook.INTRO_OT;
84              System.arraycopy(booksOT, 0, books, 2, booksOT.length);
85          }
86  
87          if (booksNT.length > 0) {
88              books[ntStart] = BibleBook.INTRO_NT;
89              System.arraycopy(booksNT, 0, books, ntStart + 1, booksNT.length);
90          }
91  
92          this.bookList = new BibleBookList(books);
93  
94          int ordinal = 0;
95  
96          // Create an independent copy of lastVerse.
97          this.lastVerse = new int[bookCount][];
98          int bookIndex = 0;
99  
100         // Add in the bible introduction
101         int[] chapters = new int[1];
102         chapters[0] = 0;
103         this.lastVerse[bookIndex++] = chapters;
104 
105         // Now append the OT info
106         if (lastVerseOT.length > 0) {
107             // Add in the testament intro
108             chapters = new int[1];
109             chapters[0] = 0;
110             this.lastVerse[bookIndex++] = chapters;
111             // then all the testament info
112             for (int i = 0; i < lastVerseOT.length; i++) {
113                 int[] src = lastVerseOT[i];
114                 // Add one as the location for chapter 0.
115                 int[] dest = new int[src.length + 1];
116                 this.lastVerse[bookIndex++] = dest;
117                 // The last verse of chapter 0 is 0
118                 dest[0] = 0;
119                 // copy the last verse array for the chapter
120                 System.arraycopy(src, 0, dest, 1, src.length);
121             }
122         }
123 
124         // Now append the NT info
125         if (lastVerseNT.length > 0) {
126             // Add in the testament intro
127             chapters = new int[1];
128             chapters[0] = 0;
129             this.lastVerse[bookIndex++] = chapters;
130             // then all the testament info
131             for (int i = 0; i < lastVerseNT.length; i++) {
132                 int[] src = lastVerseNT[i];
133                 // Add one as the location for chapter 0.
134                 int[] dest = new int[src.length + 1];
135                 this.lastVerse[bookIndex++] = dest;
136                 // The last verse of chapter 0 is 0
137                 dest[0] = 0;
138                 // copy the last verse array for the chapter
139                 System.arraycopy(src, 0, dest, 1, src.length);
140             }
141         }
142 
143         // Initialize chapterStarts to be a parallel array to lastVerse,
144         // but with chapter starts
145         this.chapterStarts = new int[bookCount][];
146         for (bookIndex = 0; bookIndex < bookCount; bookIndex++) {
147 
148             // Remember where the OT ends
149             if (bookList.getBook(bookIndex) == BibleBook.INTRO_NT) {
150                 // This is not reached for a v11n without a NT.
151                 this.otMaxOrdinal = ordinal - 1;
152             }
153 
154             // Save off the chapter starts
155             int[] src = this.lastVerse[bookIndex];
156             int numChapters = src.length;
157             int[] dest = new int[numChapters];
158             this.chapterStarts[bookIndex] = dest;
159             for (int chapterIndex = 0; chapterIndex < numChapters; chapterIndex++) {
160                 // Save off the chapter start
161                 dest[chapterIndex] = ordinal;
162 
163                 // Set ordinal to the start of the next chapter or book introduction.
164                 // The number of verses in each chapter, when including verse 0,
165                 // is one more that the largest numbered verse in the chapter.
166                 ordinal += src[chapterIndex] + 1;
167             }
168         }
169 
170         // Remember where the NT ends
171         this.ntMaxOrdinal = ordinal - 1;
172 
173         // The MT v11n has no NT, so at this point otMaxOrdinal == 0
174         if (booksNT.length == 0) {
175             this.otMaxOrdinal = this.ntMaxOrdinal;
176         }
177 //        Versification.dump(System.out, this.osisName, this.bookList, this.lastVerse);
178 //        Versification.dump(System.out, this.osisName, this.bookList, this.chapterStarts);
179     }
180 
181     /**
182      * Get the OSIS name for this Versification.
183      * @return the OSIS name of the Versification
184      */
185     public String getName() {
186         return name;
187     }
188 
189     /**
190      * Does this Versification contain the BibleBook.
191      *
192      * @param book
193      * @return true if it is present.
194      */
195     public boolean containsBook(BibleBook book) {
196         return bookList.contains(book);
197     }
198 
199     /**
200      * Get the BibleBook by its position in this Versification.
201      * If the position is negative, return the first book.
202      * If the position is greater than the last, return the last book.
203      *
204      * @param ordinal
205      * @return the indicated book
206      */
207     public BibleBook getBook(int ordinal) {
208         return bookList.getBook(ordinal);
209     }
210 
211     /**
212      * Get a book from its name.
213      *
214      * @param find
215      *            The string to identify
216      * @return The BibleBook, On error null
217      */
218     public BibleBook getBook(String find) {
219         BibleBook book = BibleNames.instance().getBook(find);
220         if (containsBook(book)) {
221             return book;
222         }
223         return null;
224     }
225 
226     /**
227      * Get the number of books in this Versification.
228      * @return the number of books
229      */
230     public int getBookCount() {
231         return bookList.getBookCount();
232     }
233 
234     /**
235      * The number of books between two verses includes
236      * the books of the two verses and everything in between.
237      * 
238      * @param start
239      *            The first Verse in the range
240      * @param end The last Verse in the range
241      * @return The number of books. Always &gt;= 1.
242      */
243     public int getBookCount(Verse start, Verse end) {
244         int startBook = bookList.getOrdinal(start.getBook());
245         int endBook = bookList.getOrdinal(end.getBook());
246 
247         return endBook - startBook + 1;
248     }
249 
250     /**
251      * Return the first book in the list.
252      *
253      * @return the first book in the list
254      */
255     public BibleBook getFirstBook() {
256         return bookList.getFirstBook();
257     }
258 
259     /**
260      * Return the first book in the list.
261      *
262      * @return the first book in the list
263      */
264     public BibleBook getLastBook() {
265         return bookList.getLastBook();
266     }
267 
268     /**
269      * Given a BibleBook, get the next BibleBook in this Versification. If it is the last book, return null.
270      * @param book A BibleBook in the Versification
271      * @return the previous BibleBook or null.
272      */
273     public BibleBook getNextBook(BibleBook book) {
274         return bookList.getNextBook(book);
275     }
276 
277     /**
278      * Given a BibleBook, get the previous BibleBook in this Versification. If it is the first book, return null.
279      * @param book A BibleBook in the Versification
280      * @return the previous BibleBook or null.
281      */
282     public BibleBook getPreviousBook(BibleBook book) {
283         return bookList.getPreviousBook(book);
284     }
285 
286     /**
287      * Get the BibleBooks in this Versification.
288      *
289      * @return an Iterator over the books
290      */
291     public Iterator<BibleBook> getBookIterator() {
292         return bookList.iterator();
293     }
294 
295     /**
296      * Get the BookName.
297      *
298      * @param book the desired book
299      * @return The requested BookName or null if not in this versification
300      */
301     public BookName getBookName(BibleBook book) {
302         if (containsBook(book)) {
303             return BibleNames.instance().getBookName(book);
304         }
305         return null;
306     }
307 
308     /**
309      * Get the preferred name of a book. Altered by the case setting (see
310      * setBookCase() and isFullBookName())
311      *
312      * @param book the desired book
313      * @return The full name of the book or null if not in this versification
314      */
315     public String getPreferredName(BibleBook book) {
316         if (containsBook(book)) {
317             return BibleNames.instance().getPreferredName(book);
318         }
319         return null;
320       }
321 
322     /**
323      * Get the full name of a book (e.g. "Genesis"). Altered by the case setting
324      * (see setBookCase())
325      *
326      * @param book the book of the Bible
327      * @return The full name of the book or null if not in this versification
328      */
329     public String getLongName(BibleBook book) {
330         if (containsBook(book)) {
331             return BibleNames.instance().getLongName(book);
332         }
333         return null;
334       }
335 
336     /**
337      * Get the short name of a book (e.g. "Gen"). Altered by the case setting
338      * (see setBookCase())
339      *
340      * @param book the book of the Bible
341      * @return The short name of the book or null if not in this versification
342      */
343     public String getShortName(BibleBook book) {
344         if (containsBook(book)) {
345             return BibleNames.instance().getShortName(book);
346         }
347         return null;
348       }
349 
350     /**
351      * Is the given string a valid book name. If this method returns true then
352      * getBook() will return a BibleBook and not null.
353      *
354      * @param find
355      *            The string to identify
356      * @return true when the book name is recognized
357      */
358     public boolean isBook(String find) {
359         return getBook(find) != null;
360     }
361 
362     /**
363      * Get the last valid chapter number for a book.
364      *
365      * @param book
366      *            The book part of the reference.
367      * @return The last valid chapter number for a book.
368      */
369     public int getLastChapter(BibleBook book) {
370         // This is faster than doing the check explicitly, unless
371         // The exception is actually thrown, then it is a lot slower
372         // I'd like to think that the norm is to get it right
373         try {
374             return lastVerse[bookList.getOrdinal(book)].length - 1;
375         } catch (NullPointerException ex) {
376             return 0;
377         } catch (ArrayIndexOutOfBoundsException ex) {
378             return 0;
379         }
380     }
381 
382     /**
383      * Get the last valid verse number for a chapter.
384      *
385      * @param book
386      *            The book part of the reference.
387      * @param chapter
388      *            The current chapter
389      * @return The last valid verse number for a chapter
390      */
391     public int getLastVerse(BibleBook book, int chapter) {
392         // This is faster than doing the check explicitly, unless
393         // The exception is actually thrown, then it is a lot slower
394         // I'd like to think that the norm is to get it right
395         try {
396             return lastVerse[bookList.getOrdinal(book)][chapter];
397         } catch (NullPointerException ex) {
398             return 0;
399         } catch (ArrayIndexOutOfBoundsException ex) {
400             return 0;
401         }
402     }
403 
404     /**
405      * Get a VerseRange encompassing this Versification.
406      * 
407      * @return a VerseRange for the whole versification
408      */
409     public VerseRange getAllVerses() {
410         Verse first = new Verse(this, bookList.getFirstBook(), 0, 0);
411         BibleBook book = bookList.getLastBook();
412         int chapter = getLastChapter(book);
413         Verse last = new Verse(this, book, chapter, getLastVerse(book, chapter));
414         return new VerseRange(this, first, last);
415     }
416 
417     /**
418      * An introduction is a Verse that has a verse number of 0.
419      *
420      * @param verse the verse to test
421      * @return true or false ...
422      */
423     public boolean isIntro(Verse verse) {
424         int v = verse.getVerse();
425         return v == 0;
426     }
427 
428     /**
429      * A book introduction is an introduction
430      * that has a chapter of 0.
431      *
432      * @param verse the verse to test
433      * @return true or false ...
434      */
435     public boolean isBookIntro(Verse verse) {
436         return 0 == verse.getChapter() && isIntro(verse);
437     }
438 
439     /**
440      * A chapter introduction is an introduction
441      * that has a chapter other than 0
442      *
443      * @param verse the verse to test
444      * @return true or false ...
445      */
446     public boolean isChapterIntro(Verse verse) {
447         return 0 != verse.getChapter() && isIntro(verse);
448     }
449 
450     /**
451      * The start of a chapter is indicated by
452      * a verse number of 0 or 1
453      *
454      * @param verse the verse to test
455      * @return true or false ...
456      */
457     public boolean isStartOfChapter(Verse verse) {
458         int v = verse.getVerse();
459         return v <= 1;
460     }
461 
462     /**
463      * The end of the chapter is indicated by
464      * the verse number matching the last in the chapter.
465      *
466      * @param verse the verse to test
467      * @return true or false ...
468      */
469     public boolean isEndOfChapter(Verse verse) {
470         BibleBook b = verse.getBook();
471         int v = verse.getVerse();
472         int c = verse.getChapter();
473         return v == getLastVerse(b, c);
474     }
475 
476     /**
477      * The start of a book is indicated by
478      * a chapter number of 0 or 1 and
479      * a verse number of 0 or 1.
480      *
481      * @param verse the verse to test
482      * @return true or false ...
483      */
484     public boolean isStartOfBook(Verse verse) {
485         int v = verse.getVerse();
486         int c = verse.getChapter();
487         return v <= 1 && c <= 1;
488     }
489 
490     /**
491      * The end of the book is indicated by
492      * the chapter number matching the last chapter
493      * in the book and the verse number matching
494      * the last verse in the chapter.
495      *
496      * @param verse the verse to test
497      * @return true or false ...
498      */
499     public boolean isEndOfBook(Verse verse) {
500         BibleBook b = verse.getBook();
501         int v = verse.getVerse();
502         int c = verse.getChapter();
503         return v == getLastVerse(b, c) && c == getLastChapter(b);
504     }
505 
506     /**
507      * Two verses are in the same chapter if both
508      * the book and chapter agree.
509      *
510      * @param first
511      *            The verse to compare to
512      * @param second
513      *            The verse to compare to
514      * @return true or false ...
515      */
516     public boolean isSameChapter(Verse first, Verse second) {
517         return first.getBook() == second.getBook() && first.getChapter() == second.getChapter();
518     }
519 
520     /**
521      * Two verse are adjacent if one immediately follows the other,
522      * even across book boundaries. Introductions are considered
523      * as having "zero width" in this determination. That is the
524      * last verse in a chapter or book is adjacent every verse that
525      * follows up to and including verse 1 of the next chapter in
526      * the versification.
527      * <br>
528      * For example:<br>
529      * The last verse in the Old Testament is adjacent to:
530      * <ul>
531      * <li>Intro.NT - the New Testament introduction</li>
532      * <li>Matt 0:0 - the book introduction</li>
533      * <li>Matt 1:0 - the chapter introduction</li>
534      * <li>Matt 1:1 - the first verse of Matt</li>
535      * </ul>
536      * Note: the verses can be in any order.
537      *
538      * @param first
539      *            The verse to compare to
540      * @param second
541      *            The verse to compare to
542      * @return true or false ...
543      */
544     public boolean isAdjacentChapter(Verse first, Verse second) {
545         Verse before = min(first, second);
546         Verse after = max(first, second);
547         if (isSameBook(first, second)) {
548             return after.getChapter() - before.getChapter() == 1;
549         }
550         // The earlier verse has to be the  last chapter
551         return isAdjacentBook(before, after)
552              && getLastChapter(before.getBook()) == before.getChapter()
553              && after.getChapter() <= 1;
554     }
555 
556     /**
557      * Two verses are in the same book
558      * when they have the same book.
559      *
560      * @param first
561      *            The verse to compare to
562      * @param second
563      *            The verse to compare to
564      * @return true or false ...
565      */
566     public boolean isSameBook(Verse first, Verse second) {
567         return first.getBook() == second.getBook();
568     }
569 
570     /**
571      * Two verses are in adjacent books if one book
572      * follows the other in this versification.
573      * Note: the verses can be in any order.
574      *
575      * @param first
576      *            The verse to compare to
577      * @param second
578      *            The verse to compare to
579      * @return true or false ...
580      */
581     public boolean isAdjacentBook(Verse first, Verse second) {
582         return Math.abs(bookList.getOrdinal(second.getBook()) - bookList.getOrdinal(first.getBook())) == 1;
583     }
584 
585     /**
586      * Is this verse adjacent to another verse
587      *
588      * @param first
589      *            The first verse in the comparison
590      * @param second
591      *            The second verse in the comparison
592      * @return true if the verses are adjacent.
593      */
594     public boolean isAdjacentVerse(Verse first, Verse second) {
595         Verse before = min(first, second);
596         Verse after = max(first, second);
597         if (isSameChapter(first, second)) {
598             return after.getVerse() - before.getVerse() == 1;
599         }
600         // The earlier verse has to be the last verse in the chapter
601         return isAdjacentChapter(before, after)
602              && getLastVerse(before.getBook(), before.getChapter()) == before.getVerse()
603              && after.getVerse() <= 1;
604     }
605 
606     /**
607      * How many verses are there in between the 2 Verses. The answer is -ve if
608      * start is bigger than end. The answer is inclusive of start and exclusive
609      * of end, so that <code>distance(gen11, gen12) == 1</code>
610      *
611      * @param start
612      *            The first Verse in the range
613      * @param end The last Verse in the range
614      * @return The count of verses between this and that.
615      */
616     public int distance(Verse start, Verse end) {
617         return end.getOrdinal() - start.getOrdinal();
618     }
619 
620     /**
621      * Determine the earlier of the two verses.
622      * If first == second then return first.
623      * 
624      * @param first the first verse to compare
625      * @param second the second verse to compare
626      * @return The earlier of the two verses
627      */
628     public Verse min(Verse first, Verse second) {
629         return first.getOrdinal() <= second.getOrdinal() ? first : second;
630     }
631 
632     /**
633      * Determine the later of the two verses.
634      * If first == second then return first.
635      * 
636      * @param first the first verse to compare
637      * @param second the second verse to compare
638      * @return The later of the two verses
639      */
640     public Verse max(Verse first, Verse second) {
641         return first.getOrdinal() > second.getOrdinal() ? first : second;
642     }
643 
644     /**
645      * Get the verse n down from here this Verse.
646      *
647      * @param verse
648      *            The verse to use as a start
649      * @param n
650      *            The number to count down by
651      * @return The new Verse
652      */
653     public Verse subtract(Verse verse, int n) {
654         int newVerse = verse.getVerse() - n;
655         // Try the simple case of the verse being in the same chapter
656         if (newVerse >= 0) {
657             return new Verse(verse.getVersification(), verse.getBook(), verse.getChapter(), newVerse);
658         }
659         return decodeOrdinal(verse.getOrdinal() - n);
660     }
661 
662     /**
663      * Get the verse that is a verses on from the one we've got.
664      *
665      * @param verse
666      *            The verse to use as a start
667      * @return The new verse or null if there is no next verse
668      */
669     public Verse next(Verse verse) {
670         // Cannot increment past the end.
671         if (verse.getOrdinal() == ntMaxOrdinal) {
672             return null;
673         }
674 
675         BibleBook nextBook = verse.getBook();
676         int nextChapter = verse.getChapter();
677         int nextVerse = verse.getVerse() + 1;
678         if (nextVerse > getLastVerse(nextBook, nextChapter)) {
679             // Go to an introduction.
680             nextVerse = 0;
681             // of the next chapter
682             nextChapter += 1;
683             // check to see that the chapter is valid for the book
684             if (nextChapter > getLastChapter(nextBook)) {
685                 // To to an introduction
686                 nextChapter = 0;
687                 // of the next book
688                 nextBook = bookList.getNextBook(verse.getBook());
689             }
690         }
691 
692         // nextBook is null when we try to increment past the last verse
693         // The test at the beginning is designed to prevent that
694         if (nextBook == null) {
695             assert false;
696             return null;
697         }
698 
699         return new Verse(this, nextBook, nextChapter, nextVerse);
700     }
701 
702     /**
703      * Get the verse that is a few verses on from the one we've got.
704      *
705      * @param verse
706      *            The verse to use as a start
707      * @param n
708      *            the number of verses later than the one we're one
709      * @return The new verse
710      */
711     public Verse add(Verse verse, int n) {
712         int newVerse = verse.getVerse() + n;
713         // Try the simple case of the verse being in the same chapter
714         if (newVerse <= getLastVerse(verse.getBook(), verse.getChapter())) {
715             return new Verse(verse.getVersification(), verse.getBook(), verse.getChapter(), newVerse);
716         }
717         return decodeOrdinal(verse.getOrdinal() + n);
718     }
719 
720     /**
721      * The number of chapters between two verses includes
722      * the chapters of the two verses and everything in between.
723      * 
724      * @param start
725      *            The first Verse in the range
726      * @param end The last Verse in the range
727      * @return The number of chapters. Always &gt;= 1.
728      */
729     public int getChapterCount(Verse start, Verse end) {
730         BibleBook startBook = start.getBook();
731         int startChap = start.getChapter();
732         BibleBook endBook = end.getBook();
733         int endChap = end.getChapter();
734 
735         if (startBook == endBook) {
736             return endChap - startChap + 1;
737         }
738 
739         // So we are going to have to count up chapters from start to end
740         int total = getLastChapter(startBook) - startChap;
741         startBook = bookList.getNextBook(startBook);
742         endBook = bookList.getPreviousBook(endBook);
743         for (BibleBook b =  startBook; b != endBook; b = bookList.getNextBook(b)) {
744             total += getLastChapter(b);
745         }
746         total += endChap;
747 
748         return total;
749     }
750 
751     /**
752      * The maximum number of verses in the Bible, including module, testament, book and chapter introductions.
753      *
754      * @return the number of addressable verses in this versification.
755      */
756     public int maximumOrdinal() {
757         // This is the same as the last ordinal in the Reference System.
758         return ntMaxOrdinal;
759     }
760 
761     /**
762      * Where does this verse come in the Bible. The value that this returns should be treated as opaque, useful for a bit set.
763      * The introductions to the Book, OT/NT Testaments, Bible books and chapters are included here.
764      * <ul>
765      * <li>0 - INTRO_BIBLE 0:0 - The Book introduction</li>
766      * <li>1 - INTRO_OT 0:0 - The OT Testament introduction</li>
767      * <li>2 - Gen 0:0 - The introduction to the book of Genesis</li>
768      * <li>3 - Gen 1:0 - The introduction to Genesis chapter 1</li>
769      * <li>4 - Gen 1:1</li>
770      * <li>...</li>
771      * <li>35 - Gen 1:31</li>
772      * <li>36 - Gen 2:0 - The introduction to Genesis chapter 2</li>
773      * <li>37 - Gen 2:1</li>
774      * <li>...</li>
775      * <li>n - last verse in the OT</li>
776      * <li>n + 1 - INTRO_NT, 0, 0 - The New Testament introduction</li>
777      * <li>n + 2 - Matt 0:0 - The introduction to Matt</li>
778      * <li>n + 3 - Matt 1:0 - The introduction to Matt 1</li>
779      * <li>n + 4 - Matt 1:1</li>
780      * <li>...</li>
781      * </ul>
782      * 
783      * If the verse is not in this versification, return 0.
784      *
785      * @param verse
786      *            The verse to convert
787      * @return The ordinal number of verses
788     */
789     public int getOrdinal(Verse verse) {
790         try {
791             return chapterStarts[bookList.getOrdinal(verse.getBook())][verse.getChapter()] + verse.getVerse();
792         } catch (ArrayIndexOutOfBoundsException e) {
793             return 0;
794         }
795     }
796 
797     /**
798      * Determine the ordinal value for this versification given the
799      * ordinal value in a testament. If the ordinal is out of bounds it
800      * is constrained to be within the boundaries of the testament.
801      * This unwinds getTestamentOrdinal.
802      * 
803      * @param testament the testament in which the ordinal value pertains
804      * @param testamentOrdinal the ordinal value within the testament
805      * @return the ordinal value for the versification as a whole
806      */
807     public int getOrdinal(Testament testament, int testamentOrdinal) {
808         int ordinal = testamentOrdinal >= 0 ? testamentOrdinal : 0;
809         if (Testament.NEW == testament) {
810             ordinal = otMaxOrdinal + testamentOrdinal;
811             return ordinal <= ntMaxOrdinal ? ordinal : ntMaxOrdinal;
812         }
813         return ordinal <= otMaxOrdinal ? ordinal : otMaxOrdinal;
814     }
815 
816     /**
817      * Where does this verse come in the Bible. The value that this returns should be treated as opaque, useful for a bit set.
818      * The introductions to the Book, OT/NT Testaments, Bible books and chapters are included here.
819      * <ul>
820      * <li>0 - INTRO_BIBLE 0:0 - The Book introduction</li>
821      * <li>1 - INTRO_OT 0:0 - The OT Testament introduction</li>
822      * <li>2 - Gen 0:0 - The introduction to the book of Genesis</li>
823      * <li>3 - Gen 1:0 - The introduction to Genesis chapter 1</li>
824      * <li>4 - Gen 1:1</li>
825      * <li>...</li>
826      * <li>35 - Gen 1:31</li>
827      * <li>36 - Gen 2:0 - The introduction to Genesis chapter 2</li>
828      * <li>37 - Genesis 2:1</li>
829      * <li>...</li>
830      * <li>n - last verse in the OT</li>
831      * <li>0 - INTRO_NT, 0, 0 - The New Testament introduction</li>
832      * <li>1 - Matt 0:0 - The introduction to Matt</li>
833      * <li>2 - Matt 1:0 - The introduction to Matt 1</li>
834      * <li>3 - Matt 1:1</li>
835      * <li>...</li>
836      * </ul>
837      *
838      * @param ordinal
839      *            The ordinal number of the verse to convert
840      * @return The ordinal number of the Verse within its Testament
841      */
842     public int getTestamentOrdinal(int ordinal) {
843         int ntOrdinal = otMaxOrdinal + 1;
844         if (ordinal >= ntOrdinal) {
845             return ordinal - ntOrdinal + 1;
846         }
847         return ordinal;
848     }
849 
850     /**
851      * Get the testament of a given verse
852      * @param ordinal the ordinal position of the verse in the whole Bible
853      * @return the testament in which that verse is found
854      */
855     public Testament getTestament(int ordinal) {
856         if (ordinal > otMaxOrdinal) {
857             // This is an NT verse
858             return Testament.NEW;
859         }
860         // This is an OT verse
861         return Testament.OLD;
862     }
863 
864     /**
865      * Give the count of verses in the testament or the whole Bible.
866      *
867      * @param testament The testament to count. If null, then all testaments.
868      * @return the number of verses in the testament
869      */
870     public int getCount(Testament testament) {
871         int total = ntMaxOrdinal + 1;
872         if (testament == null) {
873             return total;
874         }
875 
876         int otCount = otMaxOrdinal + 1;
877         if (testament == Testament.OLD) {
878             return otCount;
879         }
880 
881         return total - otCount;
882     }
883 
884     /**
885      * Where does this verse come in the Bible. This will unwind the value returned by getOrdinal(Verse).
886      * If the ordinal value is less than 0 or greater than the last verse in this Versification,
887      * then constrain it to the first or last verse in this Versification.
888      *
889      * @param ordinal
890      *            The ordinal number of the verse
891      * @return A Verse
892      */
893     public Verse decodeOrdinal(int ordinal) {
894         int ord = ordinal;
895 
896         if (ord < 0) {
897             ord = 0;
898         } else if (ord > ntMaxOrdinal) {
899             ord = ntMaxOrdinal;
900         }
901 
902         // Handle three special cases
903         // Book/Module introduction
904         if (ord == 0) {
905             return new Verse(this, BibleBook.INTRO_BIBLE, 0, 0);
906         }
907 
908         // OT introduction
909         if (ord == 1) {
910             return new Verse(this, BibleBook.INTRO_OT, 0, 0);
911         }
912 
913         // NT introduction
914         if (ord == otMaxOrdinal + 1) {
915             return new Verse(this, BibleBook.INTRO_NT, 0, 0);
916         }
917 
918         // To find the book, do a binary search in chapterStarts against chapter 0
919         int low = 0;
920         int high = chapterStarts.length;
921         int match = -1;
922 
923         while (high - low > 1) {
924             // use >>> to keep mid always in range
925             int mid = (low + high) >>> 1;
926 
927             // Compare the for the item at "mid"
928             int cmp = chapterStarts[mid][0] - ord;
929             if (cmp < 0) {
930                 low = mid;
931             } else if (cmp > 0) {
932                 high = mid;
933             } else {
934                 match = mid;
935                 break;
936             }
937         }
938 
939         // If we didn't have an exact match then use the low value
940         int bookIndex = match >= 0 ? match : low;
941         BibleBook book = bookList.getBook(bookIndex);
942 
943         // To find the chapter, do a binary search in chapterStarts against bookIndex
944         low = 0;
945         high = chapterStarts[bookIndex].length;
946         match = -1;
947 
948         while (high - low > 1) {
949             // use >>> to keep mid always in range
950             int mid = (low + high) >>> 1;
951 
952             // Compare the for the item at "mid"
953             int cmp = chapterStarts[bookIndex][mid] - ord;
954             if (cmp < 0) {
955                 low = mid;
956             } else if (cmp > 0) {
957                 high = mid;
958             } else {
959                 match = mid;
960                 break;
961             }
962         }
963 
964         // If we didn't have an exact match then use the low value
965         int chapterIndex = match >= 0 ? match : low;
966         int verse = chapterIndex == 0 ? 0 : ord - chapterStarts[bookIndex][chapterIndex];
967         return new Verse(this, book, chapterIndex, verse);
968     }
969 
970     /**
971      * Does the following represent a real verse?. It is code like this that
972      * makes me wonder if I18 is done well/worth doing. All this code does is
973      * check if the numbers are valid, but the exception handling code is huge
974      * :(
975      *
976      * @param book
977      *            The book part of the reference.
978      * @param chapter
979      *            The chapter part of the reference.
980      * @param verse
981      *            The verse part of the reference.
982      * @exception NoSuchVerseException
983      *                If the reference is illegal
984      */
985     public void validate(BibleBook book, int chapter, int verse) throws NoSuchVerseException {
986         validate(book, chapter, verse, false);
987     }
988 
989     /**
990      * Does the following represent a real verse?. It is code like this that
991      * makes me wonder if I18 is done well/worth doing. All this code does is
992      * check if the numbers are valid, but the exception handling code is huge
993      * :(
994      *
995      * @param book
996      *            The book part of the reference.
997      * @param chapter
998      *            The chapter part of the reference.
999      * @param verse
1000     *            The verse part of the reference.
1001     * @param silent
1002     *            true to indicate we do not want to throw an exception
1003     * @return true if validation was succesful
1004     * @exception NoSuchVerseException
1005     *                If the reference is illegal and silent was false
1006     */
1007    public boolean validate(BibleBook book, int chapter, int verse, boolean silent) throws NoSuchVerseException {
1008        // Check the book
1009        if (book == null) {
1010            if (silent) {
1011                return false;
1012            }
1013            // TRANSLATOR: The user did not supply a book for a verse reference.
1014            throw new NoSuchVerseException(JSOtherMsg.lookupText("Book must not be null"));
1015        }
1016
1017        // Check the chapter
1018        int maxChapter = getLastChapter(book);
1019        if (chapter < 0 || chapter > maxChapter) {
1020            if (silent) {
1021                return false;
1022            }
1023            // TRANSLATOR: The user supplied a chapter that was out of bounds. This tells them what is allowed.
1024            // {0} is the lowest value that is allowed. This is always 0.
1025            // {1,number,integer} is the place holder for the highest chapter number in the book. The format is special in that it will present it in the user's preferred format.
1026            // {2} is a placeholder for the Bible book name.
1027            // {3,number,integer} is a placeholder for the chapter number that the user gave.
1028            throw new NoSuchVerseException(JSMsg.gettext("Chapter should be between {0} and {1,number,integer} for {2} (given {3,number,integer}).",
1029                    Integer.valueOf(0), Integer.valueOf(maxChapter), getPreferredName(book), Integer.valueOf(chapter)
1030            ));
1031        }
1032
1033        // Check the verse
1034        int maxVerse = getLastVerse(book, chapter);
1035        if (verse < 0 || verse > maxVerse) {
1036            if (silent) {
1037                return false;
1038            }
1039            // TRANSLATOR: The user supplied a verse number that was out of bounds. This tells them what is allowed.
1040            // {0} is the lowest value that is allowed. This is always 0.
1041            // {1,number,integer} is the place holder for the highest verse number in the chapter. The format is special in that it will present it in the user's preferred format.
1042            // {2} is a placeholder for the Bible book name.
1043            // {3,number,integer} is a placeholder for the chapter number that the user gave.
1044            // {4,number,integer} is a placeholder for the verse number that the user gave.
1045            throw new NoSuchVerseException(JSMsg.gettext("Verse should be between {0} and {1,number,integer} for {2} {3,number,integer} (given {4,number,integer}).",
1046                    Integer.valueOf(0), Integer.valueOf(maxVerse), getPreferredName(book), Integer.valueOf(chapter), Integer.valueOf(verse)
1047                    ));
1048        }
1049        return true;
1050    }
1051
1052    /**
1053     * Fix up these verses so that they are as valid a possible. This is
1054     * currently done so that we can say "Gen 1:1" + 31 = "Gen 1:32" and
1055     * "Gen 1:32".patch() is "Gen 2:1".
1056     * <p>
1057     * There is another patch system that allows us to use large numbers to mean
1058     * "the end of" so "Gen 1:32".otherPatch() gives "Gen 1:31". This could be
1059     * useful to allow the user to enter things like "Gen 1:99" meaning the end
1060     * of the chapter. Or "Isa 99:1" to mean the last chapter in Isaiah verse 1
1061     * or even "Rev 99:99" to mean the last verse in the Bible.
1062     * <p>
1063     * However I have not implemented this because I've used a different
1064     * convention: "Gen 1:$" (OLB compatible) or "Gen 1:ff" (common commentary
1065     * usage) to mean the end of the chapter - So the functionality is there
1066     * anyway.
1067     * <p>
1068     * I think that getting into the habit of typing "Gen 1:99" is bad. It could
1069     * be the source of surprises "Psa 119:99" is not what you'd might expect,
1070     * and neither is "Psa 99:1" is you wanted the last chapter in Psalms -
1071     * expecting us to type "Psa 999:1" seems like we're getting silly.
1072     * <p>
1073     * However despite this maybe we should provide the functionality anyway.
1074     *
1075     * @param book the book to obtain
1076     * @param chapter the supposed chapter
1077     * @param verse the supposed verse
1078     * @return The resultant verse.
1079     */
1080    public Verse patch(BibleBook book, int chapter, int verse) {
1081        BibleBook patchedBook = book;
1082        int patchedChapter = chapter;
1083        int patchedVerse = verse;
1084
1085        // If the book is null,
1086        // then patch to the first book in the reference system
1087        if (patchedBook == null) {
1088            patchedBook = bookList.getFirstBook();
1089        }
1090        // If they are too small
1091        if (patchedChapter < 0) {
1092            patchedChapter = 0;
1093        }
1094        if (patchedVerse < 0) {
1095            patchedVerse = 0;
1096        }
1097
1098        // Goal is to start in the current book and go forward that number of chapters
1099        // which might cause one to land in a later book.
1100        // For each book, the chapters number from 0 to n, where n is the last chapter number.
1101        // So if we want Genesis 53, then that would be 3 chapters into Exodus,
1102        // which would be chapter 2.
1103        while (patchedBook != null && patchedChapter > getLastChapter(patchedBook)) {
1104            patchedChapter -= getLastChapter(patchedBook) + 1;
1105            patchedBook = bookList.getNextBook(patchedBook);
1106        }
1107
1108        // At this point we have a valid chapter.
1109        // Now we do the same for the verses.
1110        // For each book, the chapters number from 0 to n, where n is the last chapter number.
1111        // So if we want Genesis 49:36, then that would be 3 verses into Genesis 50,
1112        // which would be verse 50:2.     
1113        while (patchedBook != null && patchedVerse > getLastVerse(patchedBook, patchedChapter)) {
1114            patchedVerse -= getLastVerse(patchedBook, patchedChapter) + 1;
1115            patchedChapter += 1;
1116
1117            if (patchedChapter > getLastChapter(patchedBook)) {
1118                patchedChapter -= getLastChapter(patchedBook) + 1;
1119                patchedBook = bookList.getNextBook(patchedBook);
1120            }
1121        }
1122
1123        // If we have gone beyond the last book
1124        // then return the last chapter and verse in the last book
1125        if (patchedBook == null) {
1126            patchedBook = bookList.getLastBook();
1127            patchedChapter = getLastChapter(patchedBook);
1128            patchedVerse = getLastVerse(patchedBook, patchedChapter);
1129        }
1130
1131        return new Verse(this, patchedBook, patchedChapter, patchedVerse);
1132    }
1133
1134    public static void dump(PrintStream out, String name, BibleBookList bookList, int[][] array) {
1135        String vstr1 = "";
1136        String vstr2 = "";
1137        int count = 0;
1138        out.println("    private final int[][] " + name + " =");
1139        out.println("    {");
1140        // Output an array just like lastVerse, indexed by book and chapter,
1141        // that accumulates verse counts for offsets,
1142        // having a sentinel at the end.
1143        int bookCount = array.length;
1144        for (int bookIndex = 0; bookIndex < bookCount; bookIndex++) {
1145            count = 0;
1146            out.print("        // ");
1147            if (bookIndex < bookList.getBookCount()) {
1148                BibleBook book = bookList.getBook(bookIndex);
1149                out.println(book.getOSIS());
1150            } else {
1151                out.println("Sentinel");
1152            }
1153            out.print("        { ");
1154
1155            int numChapters = array[bookIndex].length;
1156            for (int chapterIndex = 0; chapterIndex < numChapters; chapterIndex++) {
1157
1158                // Pretty print with 10 items per line
1159                if (count++ % 10 == 0) {
1160                    out.println();
1161                    out.print("            ");
1162                }
1163
1164                // Output the offset for the chapter introduction
1165                // This is referenced with a verse number of 0
1166                vstr1 = "     " + array[bookIndex][chapterIndex];
1167                vstr2 = vstr1.substring(vstr1.length() - 5);
1168                out.print(vstr2 + ", ");
1169            }
1170            out.println();
1171            out.println("        },");
1172        }
1173        out.println("    };");
1174    }
1175
1176    public static void optimize(PrintStream out, BibleBookList bookList, int[][] lastVerse) {
1177        String vstr1 = "";
1178        String vstr2 = "";
1179        int count = 0;
1180        int ordinal = 0;
1181        out.println("    private final int[][] chapterStarts =");
1182        out.println("    {");
1183        // Output an array just like lastVerse, indexed by book and chapter,
1184        // that accumulates verse counts for offsets,
1185        // having a sentinel at the end.
1186        int bookIndex = 0;
1187        int ntStartOrdinal = 0;
1188        for (BibleBook book = bookList.getBook(0); book != null; book = bookList.getNextBook(book)) {
1189            count = 0;
1190            out.print("        // ");
1191            out.println(book.getOSIS());
1192            out.print("        { ");
1193
1194            // Remember where the NT Starts
1195            if (book == BibleBook.INTRO_NT) {
1196                ntStartOrdinal = ordinal;
1197            }
1198
1199            int numChapters = lastVerse[bookIndex].length;
1200            for (int chapterIndex = 0; chapterIndex < numChapters; chapterIndex++) {
1201
1202                // Pretty print with 10 items per line
1203                if (count++ % 10 == 0) {
1204                    out.println();
1205                    out.print("            ");
1206                }
1207
1208                // Output the offset for the chapter introduction
1209                // This is referenced with a verse number of 0
1210                vstr1 = "     " + ordinal;
1211                vstr2 = vstr1.substring(vstr1.length() - 5);
1212                out.print(vstr2 + ", ");
1213                // Set ordinal to the start of the next chapter or book introduction
1214                int versesInChapter = lastVerse[bookIndex][chapterIndex] + 1;
1215                ordinal += versesInChapter;
1216            }
1217            out.println();
1218            out.println("        },");
1219            bookIndex++;
1220        }
1221
1222        // Output a sentinel value:
1223        // It is a book of one chapter starting with what would be the ordinal of the next chapter's introduction.
1224        vstr1 = "     " + ordinal;
1225        vstr2 = vstr1.substring(vstr1.length() - 5);
1226        out.println("        // Sentinel");
1227        out.println("        { ");
1228        out.println("            " + vstr2 + ", ");
1229        out.println("        },");
1230        out.println("    };");
1231        out.println();
1232        out.println("    /** The last ordinal number of the Old Testament */");
1233        out.println("    private int otMaxOrdinal = " + (ntStartOrdinal - 1) + ";");
1234        out.println("    /** The last ordinal number of the New Testament and the maximum ordinal number of this Reference System */");
1235        out.println("    private int ntMaxOrdinal = " + (ordinal - 1) + ";");
1236    }
1237
1238
1239    /** The OSIS name of the reference system. */
1240    private String name;
1241
1242    /** The ordered list of books in this versification. */
1243    private BibleBookList bookList;
1244
1245    /** The last ordinal number of the Old Testament */
1246    private int otMaxOrdinal;
1247
1248    /** The last ordinal number of the New Testament and the maximum ordinal number of this Reference System */
1249    private int ntMaxOrdinal;
1250
1251    /** Constant for the max verse number in each chapter */
1252    private int[][] lastVerse;
1253
1254    /**
1255     * Constant for the ordinal number of the first verse in each chapter.
1256     */
1257    private int[][] chapterStarts;
1258
1259    /**
1260     * Serialization ID
1261     */
1262    private static final long serialVersionUID = -6226916242596368765L;
1263}
1264