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 as published by
5    * the Free Software Foundation. This program is distributed in the hope
6    * that it will be useful, but WITHOUT ANY WARRANTY; without even the
7    * 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   * Copyright: 2005
18   *     The copyright to this program is held by it's authors.
19   *
20   * ID: $Id: AccuracyType.java 2221 2012-01-25 21:32:57Z dmsmith $
21   */
22  package org.crosswire.jsword.passage;
23  
24  import org.crosswire.common.icu.NumberShaper;
25  import org.crosswire.jsword.JSMsg;
26  import org.crosswire.jsword.versification.BibleBook;
27  import org.crosswire.jsword.versification.Versification;
28  
29  /**
30   * Types of Accuracy for verse references. For example:
31   * <ul>
32   * <li>Gen == BOOK_ONLY;
33   * <li>Gen 1 == BOOK_CHAPTER;
34   * <li>Gen 1:1 == BOOK_VERSE;
35   * <li>Jude 1 == BOOK_VERSE;
36   * <li>Jude 1:1 == BOOK_VERSE;
37   * <li>1:1 == CHAPTER_VERSE;
38   * <li>10 == BOOK_ONLY, CHAPTER_ONLY, or VERSE_ONLY
39   * <ul>
40   * 
41   * With the last one, you will note that there is a choice. By itself there is
42   * not enough information to determine which one it is. There has to be a
43   * context in which it is used.
44   * <p>
45   * It may be found in a verse range like: Gen 1:2 - 10. In this case the context
46   * of 10 is Gen 1:2, which is BOOK_VERSE. So in this case, 10 is VERSE_ONLY.
47   * <p>
48   * If it is at the beginning of a range like 10 - 22:3, it has to have more
49   * context. If the context is a prior entry like Gen 2:5, 10 - 22:3, then its
50   * context is Gen 2:5, which is BOOK_VERSE and 10 is VERSE_ONLY.
51   * <p>
52   * However if it is Gen 2, 10 - 22:3 then the context is Gen 2, BOOK_CHAPTER so
53   * 10 is understood as BOOK_CHAPTER.
54   * <p>
55   * As a special case, if the preceding range is an entire chapter or book then
56   * 10 would understood as CHAPTER_ONLY or BOOK_ONLY (respectively)
57   * <p>
58   * If the number has no preceding context, then it is understood as being
59   * BOOK_ONLY.
60   * <p>
61   * In all of these examples, the start verse was being interpreted. In the case
62   * of a verse that is the end of a range, it is interpreted in the context of
63   * the range's start.
64   * 
65   * @see gnu.lgpl.License for license details.<br>
66   *      The copyright to this program is held by it's authors.
67   * @author Joe Walker [joe at eireneh dot com]
68   * @author DM Smith [dmsmith555 at yahoo dot com]
69   */
70  public enum AccuracyType {
71      /**
72       * The verse was specified as book, chapter and verse. For example, Gen 1:1,
73       * Jude 3 (which only has one chapter)
74       */
75      BOOK_VERSE {
76          @Override
77          public boolean isVerse() {
78              return true;
79          }
80  
81          @Override
82          public Verse createStartVerse(Versification refSystem, String original, VerseRange verseRangeBasis, String[] parts) throws NoSuchVerseException {
83              BibleBook book = BibleBook.getBook(parts[0]);
84              int chapter = 1;
85              int verse = 1;
86              if (parts.length == 3) {
87                  chapter = getChapter(refSystem, book, parts[1]);
88                  verse = getVerse(refSystem, book, chapter, parts[2]);
89              } else {
90                  // Some books only have 1 chapter
91                  verse = getVerse(refSystem, book, chapter, parts[1]);
92              }
93              return new Verse(original, book, chapter, verse);
94          }
95  
96          @Override
97          public Verse createEndVerse(Versification refSystem, String endVerseDesc, Verse verseBasis, String[] endParts) throws NoSuchVerseException {
98              // A fully specified verse is the same regardless of whether it is a
99              // start or an end to a range.
100             return createStartVerse(refSystem, endVerseDesc, null, endParts);
101         }
102     },
103 
104     /**
105      * The passage was specified to a book and chapter (no verse). For example,
106      * Gen 1
107      */
108     BOOK_CHAPTER {
109         @Override
110         public boolean isChapter() {
111             return true;
112         }
113 
114         @Override
115         public Verse createStartVerse(Versification refSystem, String original, VerseRange verseRangeBasis, String[] parts) throws NoSuchVerseException {
116             BibleBook book = BibleBook.getBook(parts[0]);
117             int chapter = getChapter(refSystem, book, parts[1]);
118             int verse = 0;
119             return new Verse(original, book, chapter, verse);
120         }
121 
122         @Override
123         public Verse createEndVerse(Versification refSystem, String endVerseDesc, Verse verseBasis, String[] endParts) throws NoSuchVerseException {
124             // Very similar to the start verse but we want the end of the chapter
125             BibleBook book = BibleBook.getBook(endParts[0]);
126             int chapter = getChapter(refSystem, book, endParts[1]);
127             int verse = refSystem.getLastVerse(book, chapter);
128             return new Verse(endVerseDesc, book, chapter, verse);
129         }
130     },
131 
132     /**
133      * The passage was specified to a book only (no chapter or verse). For
134      * example, Gen
135      */
136     BOOK_ONLY {
137         @Override
138         public boolean isBook() {
139             return true;
140         }
141 
142         @Override
143         public Verse createStartVerse(Versification refSystem, String original, VerseRange verseRangeBasis, String[] parts) throws NoSuchVerseException {
144             BibleBook book = BibleBook.getBook(parts[0]);
145             return new Verse(original, book, 0, 0);
146         }
147 
148         @Override
149         public Verse createEndVerse(Versification refSystem, String endVerseDesc, Verse verseBasis, String[] endParts) throws NoSuchVerseException {
150             BibleBook book = BibleBook.getBook(endParts[0]);
151             int chapter = refSystem.getLastChapter(book);
152             int verse = refSystem.getLastVerse(book, chapter);
153             return new Verse(endVerseDesc, book, chapter, verse);
154         }
155     },
156 
157     /**
158      * The passage was specified to a chapter and verse (no book). For example,
159      * 1:1
160      */
161     CHAPTER_VERSE {
162         @Override
163         public boolean isVerse() {
164             return true;
165         }
166 
167         @Override
168         public Verse createStartVerse(Versification refSystem, String original, VerseRange verseRangeBasis, String[] parts) throws NoSuchVerseException {
169             if (verseRangeBasis == null) {
170                 // TRANSLATOR: The user supplied a verse reference but did not give the book of the Bible.
171                 throw new NoSuchVerseException(JSMsg.gettext("Book is missing"));
172             }
173             BibleBook book = verseRangeBasis.getEnd().getBook();
174             int chapter = getChapter(refSystem, book, parts[0]);
175             int verse = getVerse(refSystem, book, chapter, parts[1]);
176 
177             return new Verse(original, book, chapter, verse);
178         }
179 
180         @Override
181         public Verse createEndVerse(Versification refSystem, String endVerseDesc, Verse verseBasis, String[] endParts) throws NoSuchVerseException {
182             // Very similar to the start verse but use the verse as a basis
183             BibleBook book = verseBasis.getBook();
184             int chapter = getChapter(refSystem, book, endParts[0]);
185             int verse = getVerse(refSystem, book, chapter, endParts[1]);
186             return new Verse(endVerseDesc, book, chapter, verse);
187         }
188     },
189 
190     /**
191      * There was only a chapter number
192      */
193     CHAPTER_ONLY {
194         @Override
195         public boolean isChapter() {
196             return true;
197         }
198 
199         @Override
200         public Verse createStartVerse(Versification refSystem, String original, VerseRange verseRangeBasis, String[] parts) throws NoSuchVerseException {
201             if (verseRangeBasis == null) {
202                 // TRANSLATOR: The user supplied a verse reference but did not give the book of the Bible.
203                 throw new NoSuchVerseException(JSMsg.gettext("Book is missing"));
204             }
205             BibleBook book = verseRangeBasis.getEnd().getBook();
206             int chapter = getChapter(refSystem, book, parts[0]);
207             return new Verse(original, book, chapter, 0);
208         }
209 
210         @Override
211         public Verse createEndVerse(Versification refSystem, String endVerseDesc, Verse verseBasis, String[] endParts) throws NoSuchVerseException {
212             // Very similar to the start verse but use the verse as a basis
213             // and it gets the end of the chapter
214             BibleBook book = verseBasis.getBook();
215             int chapter = getChapter(refSystem, book, endParts[0]);
216             return new Verse(endVerseDesc, book, chapter, refSystem.getLastVerse(book, chapter));
217         }
218     },
219 
220     /**
221      * There was only a verse number
222      */
223     VERSE_ONLY {
224         @Override
225         public boolean isVerse() {
226             return true;
227         }
228 
229         @Override
230         public Verse createStartVerse(Versification refSystem, String original, VerseRange verseRangeBasis, String[] parts) throws NoSuchVerseException {
231             if (verseRangeBasis == null) {
232                 // TRANSLATOR: The user supplied a verse reference but did not give the book or chapter of the Bible.
233                 throw new NoSuchVerseException(JSMsg.gettext("Book and chapter are missing"));
234             }
235             BibleBook book = verseRangeBasis.getEnd().getBook();
236             int chapter = verseRangeBasis.getEnd().getChapter();
237             int verse = getVerse(refSystem, book, chapter, parts[0]);
238             return new Verse(original, book, chapter, verse);
239         }
240 
241         @Override
242         public Verse createEndVerse(Versification refSystem, String endVerseDesc, Verse verseBasis, String[] endParts) throws NoSuchVerseException {
243             // Very similar to the start verse but use the verse as a basis
244             // and it gets the end of the chapter
245             BibleBook book = verseBasis.getBook();
246             int chapter = verseBasis.getChapter();
247             int verse = getVerse(refSystem, book, chapter, endParts[0]);
248             return new Verse(endVerseDesc, book, chapter, verse);
249         }
250     };
251 
252     /**
253      * @param original
254      *            the original verse reference as a string
255      * @param verseRangeBasis
256      *            the range that stood before the string reference
257      * @param parts
258      *            a tokenized version of the original
259      * @return a <code>Verse</code> for the original
260      * @throws NoSuchVerseException
261      */
262     public abstract Verse createStartVerse(Versification refSystem, String original, VerseRange verseRangeBasis, String[] parts) throws NoSuchVerseException;
263 
264     /**
265      * @param endVerseDesc
266      *            the original verse reference as a string
267      * @param verseBasis
268      *            the verse at the beginning of the range
269      * @param endParts
270      *            a tokenized version of the original
271      * @return a <code>Verse</code> for the original
272      * @throws NoSuchVerseException
273      */
274     public abstract Verse createEndVerse(Versification refSystem, String endVerseDesc, Verse verseBasis, String[] endParts) throws NoSuchVerseException;
275 
276     /**
277      * @return true if this AccuracyType specifies down to the book but not
278      *         chapter or verse.
279      */
280     public boolean isBook() {
281         return false;
282     }
283 
284     /**
285      * @return true if this AccuracyType specifies down to the chapter but not
286      *         the verse.
287      */
288     public boolean isChapter() {
289         return false;
290     }
291 
292     /**
293      * @return true if this AccuracyType specifies down to the verse.
294      */
295     public boolean isVerse() {
296         return false;
297     }
298 
299     /**
300      * Interprets the chapter value, which is either a number or "ff" or "$"
301      * (meaning "what follows")
302      * 
303      * @param lbook
304      *            the book
305      * @param chapter
306      *            a string representation of the chapter. May be "ff" or "$" for
307      *            "what follows".
308      * @return the number of the chapter
309      * @throws NoSuchVerseException
310      */
311     public static final int getChapter(Versification refSystem, BibleBook lbook, String chapter) throws NoSuchVerseException {
312         if (isEndMarker(chapter)) {
313             return refSystem.getLastChapter(lbook);
314         }
315         return parseInt(chapter);
316     }
317 
318     /**
319      * Interprets the verse value, which is either a number or "ff" or "$"
320      * (meaning "what follows")
321      * 
322      * @param lbook
323      *            the integer representation of the book
324      * @param lchapter
325      *            the integer representation of the chapter
326      * @param verse
327      *            the string representation of the verse
328      * @return the number of the verse
329      * @throws NoSuchVerseException
330      */
331     public static final int getVerse(Versification refSystem, BibleBook lbook, int lchapter, String verse) throws NoSuchVerseException {
332         if (isEndMarker(verse)) {
333             return refSystem.getLastVerse(lbook, lchapter);
334         }
335         return parseInt(verse);
336     }
337 
338     /**
339      * Get an integer representation for this RestrictionType
340      */
341     public int toInteger() {
342         return ordinal();
343     }
344 
345     /**
346      * Determine how closely the string defines a verse.
347      * 
348      * @param original
349      * @param parts
350      *            is a reference split into parts
351      * @return what is the kind of accuracy of the string reference.
352      * @throws NoSuchVerseException
353      */
354     public static AccuracyType fromText(Versification refSystem, String original, String[] parts) throws NoSuchVerseException {
355         return fromText(refSystem, original, parts, null, null);
356     }
357 
358     /**
359      * @param original
360      * @param parts
361      * @param verseAccuracy
362      * @return the accuracy of the parts
363      * @throws NoSuchVerseException
364      */
365     public static AccuracyType fromText(Versification refSystem, String original, String[] parts, AccuracyType verseAccuracy) throws NoSuchVerseException {
366         return fromText(refSystem, original, parts, verseAccuracy, null);
367     }
368 
369     /**
370      * @param original
371      * @param parts
372      * @param basis
373      * @return the accuracy of the parts
374      * @throws NoSuchVerseException
375      */
376     public static AccuracyType fromText(Versification refSystem, String original, String[] parts, VerseRange basis) throws NoSuchVerseException {
377         return fromText(refSystem, original, parts, null, basis);
378     }
379 
380     /**
381      * Does this string exactly define a Verse. For example:
382      * <ul>
383      * <li>fromText("Gen") == AccuracyType.BOOK_ONLY;
384      * <li>fromText("Gen 1:1") == AccuracyType.BOOK_VERSE;
385      * <li>fromText("Gen 1") == AccuracyType.BOOK_CHAPTER;
386      * <li>fromText("Jude 1") == AccuracyType.BOOK_VERSE;
387      * <li>fromText("Jude 1:1") == AccuracyType.BOOK_VERSE;
388      * <li>fromText("1:1") == AccuracyType.CHAPTER_VERSE;
389      * <li>fromText("1") == AccuracyType.VERSE_ONLY;
390      * <ul>
391      * 
392      * @param parts
393      * @param verseAccuracy
394      * @param basis
395      * @return the accuracy of the parts
396      * @throws NoSuchVerseException
397      */
398     public static AccuracyType fromText(Versification refSystem, String original, String[] parts, AccuracyType verseAccuracy, VerseRange basis) throws NoSuchVerseException {
399         switch (parts.length) {
400         case 1:
401             if (BibleBook.isBook(parts[0])) {
402                 return BOOK_ONLY;
403             }
404 
405             // At this point we should have a number.
406             // But double check it
407             checkValidChapterOrVerse(parts[0]);
408 
409             // What it is depends upon what stands before it.
410             if (verseAccuracy != null) {
411                 if (verseAccuracy.isVerse()) {
412                     return VERSE_ONLY;
413                 }
414 
415                 if (verseAccuracy.isChapter()) {
416                     return CHAPTER_ONLY;
417                 }
418             }
419 
420             if (basis != null) {
421                 if (basis.isWholeChapter()) {
422                     return CHAPTER_ONLY;
423                 }
424                 return VERSE_ONLY;
425             }
426 
427             throw buildVersePartsException(original, parts);
428 
429         case 2:
430             // Does it start with a book?
431             BibleBook pbook = BibleBook.getBook(parts[0]);
432             if (pbook == null) {
433                 checkValidChapterOrVerse(parts[0]);
434                 checkValidChapterOrVerse(parts[1]);
435                 return CHAPTER_VERSE;
436             }
437 
438             if (refSystem.getLastChapter(pbook) == 1) {
439                 return BOOK_VERSE;
440             }
441 
442             return BOOK_CHAPTER;
443 
444         case 3:
445             if (BibleBook.getBook(parts[0]) != null) {
446                 checkValidChapterOrVerse(parts[1]);
447                 checkValidChapterOrVerse(parts[2]);
448                 return BOOK_VERSE;
449             }
450 
451             throw buildVersePartsException(original, parts);
452 
453         default:
454             throw buildVersePartsException(original, parts);
455         }
456     }
457 
458     private static NoSuchVerseException buildVersePartsException(String original, String[] parts) {
459         StringBuilder buffer = new StringBuilder(original);
460         for (int i = 0; i < parts.length; i++) {
461             buffer.append(", ").append(parts[i]);
462         }
463         // TRANSLATOR: The user specified a verse with too many separators. {0} is a placeholder for the allowable separators.
464         return new NoSuchVerseException(JSMsg.gettext("Too many parts to the Verse. (Parts are separated by any of {0})", buffer.toString()));
465     }
466 
467     /**
468      * Is this text valid in a chapter/verse context
469      * 
470      * @param text
471      *            The string to test for validity
472      * @throws NoSuchVerseException
473      *             If the text is invalid
474      */
475     private static void checkValidChapterOrVerse(String text) throws NoSuchVerseException {
476         if (!isEndMarker(text)) {
477             parseInt(text);
478         }
479     }
480 
481     /**
482      * This is simply a convenience function to wrap Integer.parseInt() and give
483      * us a reasonable exception on failure. It is called by VerseRange hence
484      * protected, however I would prefer private
485      * 
486      * @param text
487      *            The string to be parsed
488      * @return The correctly parsed chapter or verse
489      * @exception NoSuchVerseException
490      *                If the reference is illegal
491      */
492     private static int parseInt(String text) throws NoSuchVerseException {
493         try {
494             return Integer.parseInt(new NumberShaper().unshape(text));
495         } catch (NumberFormatException ex) {
496             // TRANSLATOR: The chapter or verse number is actually not a number, but something else.
497             // {0} is a placeholder for what the user supplied.
498             throw new NoSuchVerseException(JSMsg.gettext("Cannot understand {0} as a chapter or verse.", text));
499         }
500     }
501 
502     /**
503      * Is this string a legal marker for 'to the end of the chapter'
504      * 
505      * @param text
506      *            The string to be checked
507      * @return true if this is a legal marker
508      */
509     private static boolean isEndMarker(String text) {
510         if (text.equals(AccuracyType.VERSE_END_MARK1)) {
511             return true;
512         }
513 
514         if (text.equals(AccuracyType.VERSE_END_MARK2)) {
515             return true;
516         }
517 
518         return false;
519     }
520 
521     /**
522      * Take a string representation of a verse and parse it into an Array of
523      * Strings where each part is likely to be a verse part. The goal is to
524      * allow the greatest possible variations in user input.
525      * <p>
526      * Parts can be separated by pretty much anything. No distinction is made
527      * between them. While chapter and verse need to be separated, a separator
528      * is assumed between digits and non-digits. Adjacent words, (i.e. sequences
529      * of non-digits) are understood to be a book reference. If a number runs up
530      * against a book name, it is considered to be either part of the book name
531      * (i.e. it is before it) or a chapter number (i.e. it stands after it.)
532      * </p>
533      * <p>
534      * Note: ff and $ are considered to be digits.
535      * </p>
536      * <p>
537      * Note: it is not necessary for this to be a BCV (book, chapter, verse), it
538      * may just be BC, B, C, V or CV. No distinction is needed here for a number
539      * that stands alone.
540      * </p>
541      * 
542      * @param input
543      *            The string to parse.
544      * @return The string array
545      * @throws NoSuchVerseException
546      */
547     public static String[] tokenize(String input) throws NoSuchVerseException {
548         // The results are expected to be no more than 3 parts
549         String[] args = {
550                 null, null, null, null, null, null, null, null
551         };
552 
553         // Normalize the input by replacing non-digits and non-letters with
554         // spaces.
555         int length = input.length();
556         // Create an output array big enough to add ' ' where needed
557         char[] normalized = new char[length * 2];
558         char lastChar = '0'; // start with a digit so normalized won't start
559                              // with a space
560         char curChar = ' '; // can be anything
561         int tokenCount = 0;
562         int normalizedLength = 0;
563         int startIndex = 0;
564         String token = null;
565         boolean foundBoundary = false;
566         for (int i = 0; i < length; i++) {
567             curChar = input.charAt(i);
568             boolean charIsDigit = curChar == '$' || Character.isDigit(curChar)
569                     || (curChar == 'f' && (i + 1 < length ? input.charAt(i + 1) : ' ') == 'f' && !Character.isLetter(lastChar));
570             if (charIsDigit || Character.isLetter(curChar)) {
571                 foundBoundary = true;
572                 boolean charWasDigit = lastChar == '$' || Character.isDigit(lastChar) || (lastChar == 'f' && (i > 2 ? input.charAt(i - 2) : '0') == 'f');
573                 if (charWasDigit || Character.isLetter(lastChar)) {
574                     foundBoundary = false;
575                     // Replace transitions between digits and letters with
576                     // spaces.
577                     if (normalizedLength > 0 && charWasDigit != charIsDigit) {
578                         foundBoundary = true;
579                     }
580                 }
581                 if (foundBoundary) {
582                     // On a boundary:
583                     // Digits always start a new token
584                     // Letters always continue a previous token
585                     if (charIsDigit) {
586                         if (tokenCount >= args.length) {
587                             // TRANSLATOR: The user specified a verse with too many separators. {0} is a placeholder for the allowable separators.
588                             throw new NoSuchVerseException(JSMsg.gettext("Too many parts to the Verse. (Parts are separated by any of {0})", input));
589                         }
590 
591                         token = new String(normalized, startIndex, normalizedLength - startIndex);
592                         args[tokenCount++] = token;
593                         normalizedLength = 0;
594                     } else {
595                         normalized[normalizedLength++] = ' ';
596                     }
597                 }
598                 normalized[normalizedLength++] = curChar;
599             }
600 
601             // Until the first character is copied, there is no last char
602             if (normalizedLength > 0) {
603                 lastChar = curChar;
604             }
605         }
606 
607         if (tokenCount >= args.length) {
608             // TRANSLATOR: The user specified a verse with too many separators. {0} is a placeholder for the allowable separators.
609             throw new NoSuchVerseException(JSMsg.gettext("Too many parts to the Verse. (Parts are separated by any of {0})", input));
610         }
611 
612         token = new String(normalized, startIndex, normalizedLength - startIndex);
613         args[tokenCount++] = token;
614 
615         String[] results = new String[tokenCount];
616         System.arraycopy(args, 0, results, 0, tokenCount);
617         return results;
618     }
619 
620     /**
621      * What characters can we use to separate parts to a verse
622      */
623     public static final String VERSE_ALLOWED_DELIMS = " :.";
624 
625     /**
626      * Characters that are used to indicate end of verse/chapter, part 1
627      */
628     public static final String VERSE_END_MARK1 = "$";
629 
630     /**
631      * Characters that are used to indicate end of verse/chapter, part 2
632      */
633     public static final String VERSE_END_MARK2 = "ff";
634 }
635