| AccuracyType.java |
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