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