Source code: com/eireneh/bible/passage/VerseRange.java
1
2 package com.eireneh.bible.passage;
3
4 import java.io.*;
5 import java.util.*;
6 import com.sun.java.util.collections.*;
7
8 import com.eireneh.util.*;
9
10 /**
11 * A VerseRange is one step between a Verse and a Passage - it is a
12 * Verse plus a verse_count. Every VerseRange has a start, a verse_count
13 * and an end. A VerseRange is designed to be immutable. This is a
14 * necessary from a collections point of view. A VerseRange should always
15 * be valid, although some versions may not return any text for verses
16 * that they consider to be mis-translated in some way.
17 *
18 * <table border='1' cellPadding='3' cellSpacing='0' width="100%">
19 * <tr><td bgColor='white'class='TableRowColor'><font size='-7'>
20 * Distribution Licence:<br />
21 * Project B is free software; you can redistribute it
22 * and/or modify it under the terms of the GNU General Public License,
23 * version 2 as published by the Free Software Foundation.<br />
24 * This program is distributed in the hope that it will be useful,
25 * but WITHOUT ANY WARRANTY; without even the implied warranty of
26 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
27 * General Public License for more details.<br />
28 * The License is available on the internet
29 * <a href='http://www.gnu.org/copyleft/gpl.html'>here</a>, by writing to
30 * <i>Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
31 * MA 02111-1307, USA</i>, Or locally at the Licence link below.<br />
32 * The copyright to this program is held by it's authors.
33 * </font></td></tr></table>
34 * @see <a href='http://www.eireneh.com/servlets/Web'>Project B Home</a>
35 * @see docs.Licence
36 * @author Joe Walker
37 * @version D9.I9.T7
38 * @stereotype thing
39 */
40 public class VerseRange implements VerseBase
41 {
42 /**
43 * The default VerseRange is a single verse - Genesis 1:1. I didn't
44 * want to provide this constructor however, you are supposed to
45 * provide a default ctor for all beans. For this reason I suggest you
46 * don't use it.
47 */
48 public VerseRange()
49 {
50 this.original_name = null;
51 this.start = Verse.DEFAULT;
52 this.end = Verse.DEFAULT;
53 this.verse_count = 1;
54
55 verifyData();
56 }
57
58 /**
59 * Construct a VerseRange from a human readable string. For example
60 * "Gen 1:1-3" in case the user does not want to have their typing
61 * 'fixed' by a meddling patronizing computer.
62 * @param desc The textual representation
63 * @exception NoSuchVerseException If the text can not be understood
64 */
65 public VerseRange(String desc) throws NoSuchVerseException
66 {
67 this(desc, Verse.DEFAULT);
68 }
69
70 /**
71 * Construct a VerseRange from a String and a Verse. For example given
72 * "2:2" and a basis of Gen 1:1 the result would be range of 1 verse
73 * starting at Gen 2:2. Also given "2:2-5" and a basis of Gen 1:1 the
74 * result would be a range of 5 verses starting at Gen 1:1.
75 * @param desc The string describing the verse e.g "2:2"
76 * @param basis The basis by which to understand the desc.
77 * @exception NoSuchVerseException If the reference is illegal
78 */
79 public VerseRange(String desc, Verse basis) throws NoSuchVerseException
80 {
81 original_name = desc;
82
83 // Do we need this?
84 String[] parts = PassageUtil.tokenize(desc, RANGE_ALLOWED_DELIMS);
85
86 switch (parts.length)
87 {
88 case 0:
89 start = basis;
90 verse_count = 1;
91 end = calcEnd(start, verse_count);
92 break;
93
94 case 1:
95 switch (Verse.getAccuracy(parts[0]))
96 {
97 case ACCURACY_BOOK_VERSE:
98 case ACCURACY_CHAPTER_VERSE:
99 case ACCURACY_VERSE_ONLY:
100 case ACCURACY_NONE:
101 start = new Verse(parts[0], basis);
102 end = start;
103 verse_count = 1;
104 break;
105
106 case ACCURACY_BOOK_CHAPTER:
107 start = new Verse(parts[0], basis);
108 verse_count = Books.versesInChapter(start.getBook(), start.getChapter());
109 end = calcEnd(start, verse_count);
110 break;
111
112 case ACCURACY_BOOK_ONLY:
113 start = new Verse(parts[0], basis);
114 verse_count = Books.versesInBook(start.getBook());
115 end = calcEnd(start, verse_count);
116 break;
117
118 default:
119 throw new LogicError();
120 }
121 break;
122
123 case 2:
124 start = new Verse(parts[0], basis);
125 switch (Verse.getAccuracy(parts[1]))
126 {
127 case ACCURACY_BOOK_VERSE:
128 case ACCURACY_CHAPTER_VERSE:
129 case ACCURACY_VERSE_ONLY:
130 case ACCURACY_NONE:
131 end = new Verse(parts[1], start);
132 break;
133
134 case ACCURACY_BOOK_CHAPTER:
135 case ACCURACY_BOOK_ONLY:
136 end = new VerseRange(parts[1], start).getEnd();
137 break;
138
139 default:
140 throw new LogicError();
141 }
142 verse_count = calcVerseCount(start, end);
143 break;
144
145 default:
146 throw new NoSuchVerseException("passg_range_parts",
147 new Object[] { RANGE_ALLOWED_DELIMS, desc });
148 }
149
150 verifyData();
151 }
152
153 /**
154 * Construct a VerseRange from a String and a VerseRange. For example given "2:2"
155 * and a basis of Gen 1:1-2 the result would be range of 1 verse starting at
156 * Gen 2:2. Also given "2:2-5" and a basis of Gen 1:1-2 the result would be a
157 * range of 5 verses starting at Gen 1:1.
158 * <p>This constructor is different from the (String, Verse) constructor in that
159 * if the basis is a range that exactly covers a chapter and the string is a
160 * single number, then we assume that the number referrs to a chapter and not to
161 * a verse. This allows us to have a Passage like "Gen 1,2" and have the 2
162 * understood as chapter 2 and not verse 2 of Gen 1, which would have occured
163 * otherwise.
164 * @param desc The string describing the verse e.g "2:2"
165 * @param basis The verse that forms the basis by which to understand the desc.
166 * @exception com.eireneh.bible.NoSuchVerseException If the reference is illegal
167 */
168 public VerseRange(String desc, VerseRange basis) throws NoSuchVerseException
169 {
170 original_name = desc;
171
172 String[] parts = PassageUtil.tokenize(desc, RANGE_ALLOWED_DELIMS);
173
174 switch (parts.length)
175 {
176 case 0:
177 // This happens when someone types "Gen 1:1-" not sure what
178 // someone would expect to happen as a result, maybe 1 verse,
179 // maybe the whole chapter, maybe the whole book. I think that
180 // we'd be silly to try to be too clever. So I'm assuming the
181 // former. If you want either latter option use "Gen 1:1-$:$"
182 start = basis.getStart();
183 verse_count = 1;
184 end = calcEnd(start, verse_count);
185 break;
186
187 case 1:
188 // We need to cope with 2 special cases here:
189 // o The first is of "Gen 1, 2": If there is 1 part to a verse,
190 // and the basis is a whole chapter then we are ACCURACY_CHAPTER
191 // o The second is of "Mat, Mar": If there is 1 part to the verse
192 // and the basis is a whole book, then we are ACCURACY_BOOK
193 // However the second case is dealt with automatically because
194 // "Mar" can only be a book and never a chapter or verse
195 switch (Verse.getAccuracy(parts[0]))
196 {
197 case ACCURACY_BOOK_VERSE:
198 start = new Verse(parts[0], basis.getStart());
199 end = start;
200 verse_count = 1;
201 break;
202
203 case ACCURACY_VERSE_ONLY:
204 if (basis.isChapter())
205 {
206 // This should be ACCURACY_CHAPTER_ONLY if it existed
207 int book = basis.getStart().getBook();
208 int chapter = 0;
209 if (Verse.isEndMarker(parts[0])) chapter = Books.chaptersInBook(book);
210 else chapter = Verse.parseInt(parts[0]);
211
212 start = new Verse(book, chapter, 1);
213 end = new Verse(book, chapter, Books.versesInChapter(book, chapter));
214 verse_count = calcVerseCount(start, end);
215 }
216 else
217 {
218 start = new Verse(parts[0], basis.getStart());
219 end = start;
220 verse_count = 1;
221 }
222 break;
223
224 case ACCURACY_CHAPTER_VERSE:
225 case ACCURACY_NONE:
226 start = new Verse(parts[0], basis.getStart());
227 end = start;
228 verse_count = 1;
229 break;
230
231 case ACCURACY_BOOK_CHAPTER:
232 start = new Verse(parts[0], basis.getStart());
233 verse_count = Books.versesInChapter(start.getBook(), start.getChapter());
234 end = calcEnd(start, verse_count);
235 break;
236
237 case ACCURACY_BOOK_ONLY:
238 start = new Verse(parts[0], basis.getStart());
239 verse_count = Books.versesInBook(start.getBook());
240 end = calcEnd(start, verse_count);
241 break;
242
243 default:
244 throw new LogicError();
245 }
246 break;
247
248 case 2:
249 start = new Verse(parts[0], basis.getStart());
250 end = new Verse(parts[1], start);
251 verse_count = calcVerseCount(start, end);
252 break;
253
254 default:
255 throw new NoSuchVerseException("passg_range_parts",
256 new Object[] { RANGE_ALLOWED_DELIMS, desc });
257 }
258
259 verifyData();
260 }
261
262 /**
263 * Construct a VerseRange from a Verse. The resultant VerseRange will be
264 * 1 verse in verse_count.
265 * @param start The verse to start from
266 */
267 public VerseRange(Verse start)
268 {
269 if (start == null)
270 throw new NullPointerException(PassageUtil.getResource("range_error_null"));
271
272 this.original_name = null;
273 this.start = start;
274 this.end = start;
275 this.verse_count = 1;
276
277 verifyData();
278 }
279
280 /**
281 * Construct a VerseRange from a Verse and a range.
282 * @param start The verse to start from
283 * @param verse_count The number of verses
284 * @exception com.eireneh.bible.passage.NoSuchVerseException If there arn't that many verses
285 */
286 public VerseRange(Verse start, int verse_count) throws NoSuchVerseException
287 {
288 if (verse_count < 1)
289 throw new NoSuchVerseException(PassageUtil.getResource("range_error_count"));
290
291 if (start.getOrdinal()+verse_count-1 > Books.versesInBible())
292 {
293 Object[] params =
294 {
295 start.getName(),
296 new Integer(Books.versesInBible()-start.getOrdinal()),
297 new Integer(verse_count)
298 };
299 throw new NoSuchVerseException(PassageUtil.getResource("range_error_size", params));
300 }
301
302 this.original_name = null;
303 this.start = start;
304 this.verse_count = verse_count;
305 this.end = calcEnd(start, verse_count);
306
307 verifyData();
308 }
309
310 /**
311 * Construct a VerseRange from a Verse and a range.
312 * Now the actual value of the boolean is ignored. However for future proofing
313 * you should only use 'true'. Do not use patch_up=false, use Verse(int, int, int)
314 * This so that we can declare this constructor to not throw an exception.
315 * Is there a better way of doing this?
316 * @param start The verse to start from
317 * @param verse_count The number of verse to count
318 * @param patch_up True to trigger reference fixing
319 */
320 public VerseRange(Verse start, int verse_count, boolean patch_up)
321 {
322 if (!patch_up)
323 throw new IllegalArgumentException(PassageUtil.getResource("range_error_patch"));
324
325 // Not sure that any of the code below (except verifyData() which may not stay there)
326 // Checks for null so we do it explictly here.
327 if (start == null)
328 throw new NullPointerException(PassageUtil.getResource("range_error_null"));
329
330 this.original_name = null;
331 this.start = start;
332 this.end = start.add(Math.max(verse_count, 1) - 1);
333 this.verse_count = calcVerseCount(start, end);
334
335 verifyData();
336 }
337
338 /**
339 * Construct a VerseRange from 2 Verses
340 * If start is later than end then swap the two around.
341 * @param start The verse to start from
342 * @param start The verse to end with
343 */
344 public VerseRange(Verse start, Verse end)
345 {
346 if (start == null || end == null)
347 throw new NullPointerException(PassageUtil.getResource("range_error_null"));
348
349 this.original_name = null;
350
351 switch (start.compareTo(end))
352 {
353 case -1:
354 this.start = start;
355 this.end = end;
356 this.verse_count = calcVerseCount(start, end);
357 break;
358
359 case 0:
360 this.start = start;
361 this.end = start;
362 this.verse_count = 1;
363 break;
364
365 case 1:
366 this.start = end;
367 this.end = start;
368 this.verse_count = calcVerseCount(this.start, this.end);
369 break;
370
371 default:
372 throw new LogicError();
373 }
374
375 verifyData();
376 }
377
378 /**
379 * Widen the range of the verses in this list. This is primarily for
380 * "find x within n verses of y" type applications.
381 * @param base_start The verse to start from
382 * @param blur_down The number of verses to extend down by
383 * @param blur_up The number of verses to extend up by
384 * @param restrict How should we restrict the blurring?
385 * @exception java.lang.IllegalArgumentException If a blurring is negative or the restrict mode is illegal
386 * @see com.eireneh.bible.passage.Passage
387 */
388 public VerseRange(Verse base_start, int blur_down, int blur_up, int restrict)
389 {
390 if (base_start == null)
391 throw new NullPointerException(PassageUtil.getResource("range_error_null"));
392
393 if (blur_down < 0 || blur_up < 0)
394 throw new IllegalArgumentException(PassageUtil.getResource("range_error_blur_negative"));
395
396 this.original_name = null;
397
398 switch (restrict)
399 {
400 case RESTRICT_CHAPTER:
401 try
402 {
403 int start_book = base_start.getBook();
404 int start_chapter = base_start.getChapter();
405 int start_verse = base_start.getVerse() - blur_down;
406 int end_verse = base_start.getVerse() + blur_up;
407
408 start_verse = Math.max(start_verse, 1);
409 end_verse = Math.min(end_verse, Books.versesInChapter(start_book, start_chapter));
410
411 start = new Verse(start_book, start_chapter, start_verse);
412 verse_count = end_verse - start_verse + 1;
413 end = calcEnd(start, verse_count);
414 }
415 catch (NoSuchVerseException ex)
416 {
417 throw new LogicError(ex);
418 }
419 break;
420
421 case RESTRICT_NONE:
422 start = base_start.subtract(blur_down);
423 end = base_start.add(blur_up);
424 verse_count = calcVerseCount(start, end);
425 break;
426
427 case RESTRICT_BOOK:
428 throw new IllegalArgumentException(PassageUtil.getResource("range_error_blur_book"));
429
430 default:
431 throw new IllegalArgumentException(PassageUtil.getResource("range_error_blur_mode"));
432 }
433
434 verifyData();
435 }
436
437 /**
438 * Widen the range of the verses in this list. This is primarily for
439 * "find x within n verses of y" type applications.
440 * @param base_start The verse range to start from
441 * @param blur_down The number of verses to extend down by
442 * @param blur_up The number of verses to extend up by
443 * @param restrict How should we restrict the blurring?
444 * @exception java.lang.IllegalArgumentException If a blurring is negative or the restrict mode is illegal
445 * @see com.eireneh.bible.passage.Passage
446 */
447 public VerseRange(VerseRange base_start, int blur_down, int blur_up, int restrict)
448 {
449 if (base_start == null)
450 throw new NullPointerException(PassageUtil.getResource("range_error_null"));
451
452 if (blur_down < 0 || blur_up < 0)
453 throw new IllegalArgumentException(PassageUtil.getResource("range_error_blur_negative"));
454
455 this.original_name = null;
456
457 switch (restrict)
458 {
459 case RESTRICT_CHAPTER:
460 try
461 {
462
463 int start_book = base_start.getStart().getBook();
464 int start_chapter = base_start.getStart().getChapter();
465 int start_verse = base_start.getStart().getVerse() - blur_down;
466
467 int end_book = base_start.getEnd().getBook();
468 int end_chapter = base_start.getEnd().getChapter();
469 int end_verse = base_start.getEnd().getVerse() + blur_up;
470
471 start_verse = Math.max(start_verse, 1);
472 end_verse = Math.min(end_verse, Books.versesInChapter(end_book, end_chapter));
473
474 start = new Verse(start_book, start_chapter, start_verse);
475 end = new Verse(end_book, end_chapter, end_verse);
476 verse_count = calcVerseCount(start, end);
477 }
478 catch (NoSuchVerseException ex)
479 {
480 throw new LogicError(ex);
481 }
482 break;
483
484 case RESTRICT_NONE:
485 start = base_start.getStart().subtract(blur_down);
486 end = base_start.getEnd().add(blur_up);
487 verse_count = calcVerseCount(start, end);
488 break;
489
490 case RESTRICT_BOOK:
491 throw new IllegalArgumentException(PassageUtil.getResource("range_error_blur_book"));
492
493 default:
494 throw new IllegalArgumentException(PassageUtil.getResource("range_error_blur_mode"));
495 }
496
497 verifyData();
498 }
499
500 /**
501 * Merge 2 VerseRanges together. The resulting range will encompass
502 * Everying in-between the extremities of the 2 ranges.
503 * @param a The first verse range to be merged
504 * @param a The second verse range to be merged
505 */
506 public VerseRange(VerseRange a, VerseRange b)
507 {
508 original_name = null;
509 start = Verse.min(a.getStart(), b.getStart());
510 end = Verse.max(a.getEnd(), b.getEnd());
511 verse_count = calcVerseCount(start, end);
512 }
513
514 /**
515 * Fetch a more sensible shortened version of the name
516 * @return A string like 'Gen 1:1-2'
517 */
518 public String getName()
519 {
520 return getName(null);
521 }
522
523 /**
524 * Fetch a more sensible shortened version of the name
525 * @param base A reference to allow things like Gen 1:1,3,5 as an output
526 * @return A string like 'Gen 1:1-2'
527 */
528 public String getName(Verse base)
529 {
530 if (PassageUtil.isPersistentNaming() && original_name != null)
531 {
532 return original_name;
533 }
534
535 // Cache these we're going to be using them a lot.
536 int start_book = start.getBook();
537 int start_chapter = start.getChapter();
538 int start_verse = start.getVerse();
539 int end_book = end.getBook();
540 int end_chapter = end.getChapter();
541 int end_verse = end.getVerse();
542
543 try
544 {
545 // If this is in 2 separate books
546 if (start_book != end_book)
547 {
548 // This range is exactly a whole book
549 if (isBooks())
550 {
551 // Just report the name of the book, we don't need to worry about the
552 // base since we start at the start of a book, and should have been
553 // recently normalized()
554 return Books.getShortBookName(start_book)
555 + RANGE_PREF_DELIM
556 + Books.getShortBookName(end_book);
557 }
558
559 // If this range is exactly a whole chapter
560 if (isChapters())
561 {
562 // Just report book and chapter names
563 return Books.getShortBookName(start_book)
564 + VERSE_PREF_DELIM1 + start_chapter
565 + RANGE_PREF_DELIM + Books.getShortBookName(end_book)
566 + VERSE_PREF_DELIM1 + end_chapter;
567 }
568
569 return start.getName(base) + RANGE_PREF_DELIM + end.getName(base);
570 }
571
572 // This range is exactly a whole book
573 if (isBook())
574 {
575 // Just report the name of the book, we don't need to worry about the
576 // base since we start at the start of a book, and should have been
577 // recently normalized()
578 return Books.getShortBookName(start_book);
579 }
580
581 // If this is 2 separate chapters
582 if (start_chapter != end_chapter)
583 {
584 // If this range is a whole number of chapters
585 if (isChapters())
586 {
587 // Just report the name of the book and the chapters
588 return Books.getShortBookName(start_book)
589 + VERSE_PREF_DELIM1 + start_chapter
590 + RANGE_PREF_DELIM + end_chapter;
591 }
592
593 return start.getName(base)
594 + RANGE_PREF_DELIM + end_chapter
595 + VERSE_PREF_DELIM2 + end_verse;
596 }
597
598 // If this range is exactly a whole chapter
599 if (isChapter())
600 {
601 // Just report the name of the book and the chapter
602 return Books.getShortBookName(start_book)
603 + VERSE_PREF_DELIM1 + start_chapter;
604 }
605
606 // If this is 2 separate verses
607 if (start_verse != end_verse)
608 {
609 return start.getName(base)
610 + RANGE_PREF_DELIM + end_verse;
611 }
612
613 // The range is a single verse
614 return start.getName(base);
615 }
616 catch (NoSuchVerseException ex)
617 {
618 throw new LogicError(ex);
619 }
620 }
621
622 /**
623 * This just clones getName which seems the most sensible
624 * type of string to return.
625 * @return A string like 'Gen 1:1-2'
626 */
627 public String toString()
628 {
629 return getName();
630 }
631
632 /**
633 * Fetch the first verse in this range.
634 * @return The first verse in the range
635 */
636 public Verse getStart()
637 {
638 return start;
639 }
640
641 /**
642 * Fetch the last verse in this range.
643 * @return The last verse in the range
644 */
645 public Verse getEnd()
646 {
647 return end;
648 }
649
650 /**
651 * How many verses in this range
652 * @return The number of verses. Always >= 1.
653 */
654 public int getVerseCount()
655 {
656 return verse_count;
657 }
658
659 /**
660 * Get a copy of ourselves. Points to note:
661 * Call clone() not new() on member Objects, and on us.
662 * Do not use Copy Constructors! - they do not inherit well.
663 * Think about this needing to be synchronized
664 * If this is not cloneable then writing cloneable children is harder
665 * @return A complete copy of ourselves
666 * @exception java.lang.CloneNotSupportedException We don't do this but our kids might
667 */
668 public Object clone() throws CloneNotSupportedException
669 {
670 // This gets us a shallow copy
671 VerseRange copy = (VerseRange) super.clone();
672
673 copy.start = (Verse) start.clone();
674 copy.end = (Verse) end.clone();
675 copy.verse_count = verse_count;
676 copy.original_name = original_name;
677
678 return copy;
679 }
680
681 /**
682 * Is this Object equal to us. Points to note:
683 * If you override equals(), you must override hashCode() too.
684 * If you are doing this it is a good idea to be immutable.
685 * @param obj The thing to test against
686 * @return True/False is we are or are not equal to obj
687 */
688 public boolean equals(Object obj)
689 {
690 // Since this can not be null
691 if (obj == null) return false;
692
693 // Check that that is the same as this
694 // Don't use instanceOf since that breaks inheritance
695 if (!obj.getClass().equals(this.getClass())) return false;
696
697 VerseRange vr = (VerseRange) obj;
698
699 // The real tests
700 if (!vr.getStart().equals(getStart())) return false;
701 if (vr.getVerseCount() != getVerseCount()) return false;
702
703 // We don't really need to check this one too.
704 //if (!vr.getEnd().equals(getEnd())) return false;
705
706 return true;
707 }
708
709 /**
710 * The hashing number is currently calculated using the start ordinal
711 * in the upper 16 bits, and the verse_count in the lower.
712 * <p><b>Note that this may change and should not be relied upon</b>
713 * Use getStart().getOrdinal() and so on to get that kind of info.
714 * <p>The news from this however is that sorting by hashCode() is
715 * currently the same as sorting using compareTo().
716 * @return The hashing number
717 */
718 public int hashCode()
719 {
720 return (start.getOrdinal() << 16) + verse_count;
721 }
722
723 /**
724 * Compare initially using the first element in a VerseRange. If the
725 * starting verses are the same then sort according to length, shortest
726 * first so:
727 * <tt>Gen 1:1 < Gen 1:1-2 < Gen 1:1-26 < Gen 1:2</tt>
728 * <p>Note that this compares Verse("Gen 1:1") = VerseRange("Gen 1:1")
729 * I'm not sure if this is 100% pucka, but it doesn't seem to cause any
730 * problems.
731 * @param obj The thing to compare against
732 * @return 1 means he is earlier than me, -1 means he is later ...
733 */
734 public int compareTo(Object obj)
735 {
736 // This ensures a ClassCastException without further test
737 Verse that = null;
738 if (obj instanceof Verse) that = (Verse) obj;
739 else that = ((VerseRange) obj).getStart();
740
741 int start_compare = getStart().compareTo(that);
742 if (start_compare != 0) return start_compare;
743
744 // So the start verses are the same, but the Verse(Range)s may not
745 // be equal() since they have lengths
746 int that_length = 1;
747 if (obj instanceof VerseRange) that_length = ((VerseRange) obj).getVerseCount();
748
749 if (that_length == getVerseCount()) return 0;
750 if (that_length < getVerseCount()) return 1;
751 return -1;
752 }
753
754 /**
755 * Are the 2 VerseRanges in question contigious. ie - could they be
756 * represented by a single VerseRange. Note that one range could be
757 * entirely contained within the other and they would be considered
758 * adjacentTo()
759 * For example Gen 1:1-2 is adjacent to Gen 1:1-5 and Gen 1:3-4 but
760 * not to Gen 1:4-10. Also Gen 1:29-30 is adjacent to Gen 2:1-10
761 * @param that The VerseRange to compare to
762 * @return true if the ranges are adjacent
763 */
764 public boolean adjacentTo(VerseRange that)
765 {
766 int that_start = that.getStart().getOrdinal();
767 int that_end = that.getEnd().getOrdinal();
768 int this_start = getStart().getOrdinal();
769 int this_end = getEnd().getOrdinal();
770
771 // if that starts inside or is next to this we are adjacent.
772 if (that_start >= this_start-1 && that_start <= this_end+1) return true;
773
774 // if this starts inside or is next to that we are adjacent.
775 if (this_start >= that_start-1 && this_start <= that_end+1) return true;
776
777 // otherwise we're not adjacent
778 return false;
779 }
780
781 /**
782 * Do the 2 VerseRanges in question actually overlap. This is slightly
783 * more restrictive than the adjacentTo() test which could be satisfied by
784 * ranges like Gen 1:1-2 and Gen 1:3-4. overlaps() however would return
785 * false given these ranges.
786 * For example Gen 1:1-2 is adjacent to Gen 1:1-5 but not to Gen 1:3-4
787 * not to Gen 1:4-10. Also Gen 1:29-30 does not overlap Gen 2:1-10
788 * @param that The VerseRange to compare to
789 * @return true if the ranges are adjacent
790 */
791 public boolean overlaps(VerseRange that)
792 {
793 int that_start = that.getStart().getOrdinal();
794 int that_end = that.getEnd().getOrdinal();
795 int this_start = getStart().getOrdinal();
796 int this_end = getEnd().getOrdinal();
797
798 // if that starts inside this we are adjacent.
799 if (that_start >= this_start && that_start <= this_end) return true;
800
801 // if this starts inside that we are adjacent.
802 if (this_start >= that_start && this_start <= that_end) return true;
803
804 // otherwise we're not adjacent
805 return false;
806 }
807
808 /**
809 * Is the given verse entirely within our range.
810 * For example if this = "Gen 1:1-31" then:
811 * <tt>contains(Verse("Gen 1:3")) == true</tt>
812 * <tt>contains(Verse("Gen 2:1")) == false</tt>
813 * @param that The Verse to compare to
814 * @return true if we contain it.
815 */
816 public boolean contains(Verse that)
817 {
818 if (start.compareTo(that) == 1) return false;
819 if (end.compareTo(that) == -1) return false;
820
821 return true;
822 }
823
824 /**
825 * Is the given range within our range.
826 * For example if this = "Gen 1:1-31" then:
827 * <tt>this.contains(Verse("Gen 1:3-10")) == true</tt>
828 * <tt>this.contains(Verse("Gen 2:1-1")) == false</tt>
829 * @param that The Verse to compare to
830 * @return true if we contain it.
831 */
832 public boolean contains(VerseRange that)
833 {
834 if (start.compareTo(that.getStart()) == 1) return false;
835 if (end.compareTo(that.getEnd()) == -1) return false;
836
837 return true;
838 }
839
840 /**
841 * Does this range represent exactly one chapter, no more or less.
842 * @return true if we are exactly one chapter.
843 */
844 public boolean isChapter()
845 {
846 if (!start.isStartOfChapter()) return false;
847 if (!end.isEndOfChapter()) return false;
848 if (!start.isSameChapter(end)) return false;
849
850 return true;
851 }
852
853 /**
854 * Does this range represent a number of whole chapters
855 * @return true if we are a whole number of chapters.
856 */
857 public boolean isChapters()
858 {
859 if (!start.isStartOfChapter()) return false;
860 if (!end.isEndOfChapter()) return false;
861
862 return true;
863 }
864
865 /**
866 * Does this range represent exactly one book, no more or less.
867 * @return true if we are exactly one book.
868 */
869 public boolean isBook()
870 {
871 if (!start.isStartOfBook()) return false;
872 if (!end.isEndOfBook()) return false;
873 if (!start.isSameBook(end)) return false;
874
875 return true;
876 }
877
878 /**
879 * Does this range represent a whole number of books.
880 * @return true if we are a whole number of books.
881 */
882 public boolean isBooks()
883 {
884 if (!start.isStartOfBook()) return false;
885 if (!end.isEndOfBook()) return false;
886
887 return true;
888 }
889
890 /**
891 * Create an array of Verses
892 * @return The array of verses that this makes up
893 */
894 public Verse[] toVerseArray()
895 {
896 try
897 {
898 Verse[] retcode = new Verse[verse_count];
899
900 for (int i=0; i<verse_count; i++)
901 {
902 retcode[i] = new Verse(start.getOrdinal()+i);
903 }
904
905 return retcode;
906 }
907 catch (NoSuchVerseException ex)
908 {
909 throw new LogicError(ex);
910 }
911 }
912
913 /**
914 * Enumerate over the verse in this range
915 * @return A verse iterator
916 */
917 public Enumeration verseElements()
918 {
919 return new Enumeration()
920 {
921 private int next_ordinal = start.getOrdinal();
922
923 public boolean hasMoreElements()
924 {
925 return next_ordinal <= end.getOrdinal();
926 }
927
928 public Object nextElement()
929 {
930 try
931 {
932 return new Verse(next_ordinal++);
933 }
934 catch (NoSuchVerseException ex)
935 {
936 throw new LogicError(ex);
937 }
938 }
939 };
940 }
941
942 /**
943 * Create a DistinctPassage that is the stuff left of VerseRange a
944 * when you remove the stuff in VerseRange b.
945 * @param a The verses that you might want
946 * @param a The verses that you definately don't
947 * @return A list of the Verses outstanding
948 */
949 public static VerseRange[] remainder(VerseRange a, VerseRange b)
950 {
951 VerseRange start = null;
952 VerseRange end = null;
953
954 // If a starts before b get the Range of the prequel
955 if (a.getStart().compareTo(b.getStart()) == -1)
956 {
957 start = new VerseRange(a.getStart(), b.getEnd().subtract(1));
958 }
959
960 // If a ends after b get the Range of the sequel
961 if (a.getEnd().compareTo(b.getEnd()) == 1)
962 {
963 end = new VerseRange(b.getEnd().add(1), a.getEnd());
964 }
965
966 if (start == null)
967 {
968 if (end == null) return new VerseRange[] { };
969 else return new VerseRange[] { end };
970 }
971 else
972 {
973 if (end == null) return new VerseRange[] { start };
974 else return new VerseRange[] { start, end };
975 }
976 }
977
978 /**
979 * Create a DistinctPassage that is the stuff in VerseRange a
980 * that is also in VerseRange b.
981 * @param a The verses that you might want
982 * @param a The verses that you definately don't
983 * @return A list of the Verses outstanding
984 */
985 public static VerseRange intersection(VerseRange a, VerseRange b)
986 {
987 Verse new_start = Verse.max(a.getStart(), b.getStart());
988 Verse new_end = Verse.min(a.getEnd(), b.getEnd());
989
990 if (new_start.compareTo(new_end) < 1)
991 {
992 return new VerseRange(new_start, new_end);
993 }
994
995 return null;
996 }
997
998 /**
999 * Is the string likely to be a VerseRange and not a Verse?
1000 * @param desc The string to be tested for Rangeness
1001 * @return true/false if this is likely to be a range
1002 */
1003 public static boolean isVerseRange(String desc)
1004 {
1005 for (int i=0; i<RANGE_ALLOWED_DELIMS.length(); i++)
1006 {
1007 if (desc.indexOf(RANGE_ALLOWED_DELIMS.charAt(i)) != -1)
1008 return true;
1009 }
1010
1011 return false;
1012 }
1013
1014 /**
1015 * Returns a VerseRange that wraps the whole Bible
1016 * @return The whole bible VerseRange
1017 */
1018 public static VerseRange getWholeBibleVerseRange()
1019 {
1020 try
1021 {
1022 if (whole == null)
1023 whole = new VerseRange(new Verse(1, 1, 1), new Verse(66, 22, 21));
1024 }
1025 catch (NoSuchVerseException ex)
1026 {
1027 throw new LogicError(ex);
1028 }
1029
1030 return whole;
1031 }
1032
1033 /**
1034 * Calculate the last verse in this range.
1035 * @param start The first verse in the range
1036 * @param verse_count The number of verses
1037 * @return The last verse in the range
1038 */
1039 private static final Verse calcEnd(Verse start, int verse_count)
1040 {
1041 return start.add(verse_count - 1);
1042 }
1043
1044 /**
1045 * Calcualte how many verses in this range
1046 * @param start The first verse in the range
1047 * @param end The last verse in the range
1048 * @return The number of verses. Always >= 1.
1049 */
1050 private static final int calcVerseCount(Verse start, Verse end)
1051 {
1052 return end.subtract(start) + 1;
1053 }
1054
1055 /**
1056 * Check to see that everything is ok with the Data
1057 */
1058 private void verifyData()
1059 {
1060 if (verse_count != end.subtract(start) + 1)
1061 {
1062 log.warning("start="+start);
1063 log.warning("end="+end);
1064 log.warning("verse_count="+verse_count);
1065
1066 throw new LogicError();
1067 }
1068 }
1069
1070 /**
1071 * Write out the object to the given ObjectOutputStream
1072 * @param out The stream to write our state to
1073 * @serialData Write the ordinal number of this verse
1074 */
1075 private void writeObject(ObjectOutputStream out) throws IOException
1076 {
1077 // Call even if there is no default serializable fields.
1078 out.defaultWriteObject();
1079
1080 out.writeInt(start.getOrdinal());
1081 out.writeInt(verse_count);
1082
1083 // Ignore the original name. Is this wise?
1084 // I am expecting that people are not that fussed about it and
1085 // it could make everything far more verbose
1086 }
1087
1088 /**
1089 * Write out the object to the given ObjectOutputStream
1090 * @param in The stream to read our state from
1091 * @serialData Write the ordinal number of this verse
1092 */
1093 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
1094 {
1095 // Call even if there is no default serializable fields.
1096 in.defaultReadObject();
1097
1098 try
1099 {
1100 start = new Verse(in.readInt());
1101 verse_count = in.readInt();
1102 end = calcEnd(start, verse_count);
1103
1104 verifyData();
1105 }
1106 catch (NoSuchVerseException ex)
1107 {
1108 throw new IOException(ex.getMessage());
1109 }
1110
1111 // We are ignoring the original_name. It was set to null in the
1112 // default ctor so I will ignore it here.
1113 }
1114
1115 /** To make serialization work across new versions */
1116 static final long serialVersionUID = 8307795549869653580L;
1117
1118 /** The real data - how many verses long are we? All ctors init this so leave default */
1119 private transient int verse_count;
1120
1121 /** The real data - where do we start? All ctors init this so leave default */
1122 private transient Verse start;
1123
1124 /** The real data - where do we end? All ctors init this so leave default */
1125 private transient Verse end;
1126
1127 /** The original string for picky users */
1128 private transient String original_name;
1129
1130 /** The whole Bible VerseRange */
1131 private transient static VerseRange whole;
1132
1133 /** The log stream */
1134 protected transient static Logger log = Logger.getLogger("bible.passage");
1135}