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: 2007
18   *     The copyright to this program is held by it's authors.
19   *
20   * ID: $Id: StrongsNumber.java 2099 2011-03-07 17:13:00Z dmsmith $
21   */
22  package org.crosswire.jsword.book.study;
23  
24  import java.text.DecimalFormat;
25  import java.util.regex.Matcher;
26  import java.util.regex.Pattern;
27  
28  import org.crosswire.jsword.JSMsg;
29  import org.crosswire.jsword.book.BookException;
30  
31  /**
32   * A Strong's Number is either Greek or Hebrew, where the actual numbers for
33   * each start at 1. This class can parse Strong's Numbers that begin with G, g,
34   * H or h and are immediately followed by a number. That number can have leading
35   * 0's. It can be followed by an OSISref extension of !a, !b, which is ignored.
36   * 
37   * <p>
38   * The canonical representation of the number is a G or H followed by 4 digits,
39   * with leading 0's as needed.
40   * </p>
41   * 
42   * <p>
43   * Numbers that exist:
44   * <ul>
45   * <li>Hebrew: 1-8674
46   * <li>Greek: 1-5624 (but not 1418, 2717, 3203-3302, 4452)
47   * </ul>
48   * </p>
49   * 
50   * @see gnu.lgpl.License for license details.<br>
51   *      The copyright to this program is held by it's authors.
52   * @author DM Smith [dmsmith555 at yahoo dot com]
53   */
54  public class StrongsNumber {
55      /**
56       * Build an immutable Strong's Number. Anything that does not match causes a
57       * BookException.
58       * 
59       * @param input
60       *            a string that needs to be parsed.
61       * @throws BookException
62       */
63      public StrongsNumber(String input) throws BookException {
64          parse(input);
65          validate();
66      }
67  
68      /**
69       * Build an immutable Strong's Number. If the language is not 'G' or 'H' or
70       * the number is invalid, throw a BookException.
71       * 
72       * @param language
73       * @param strongsNumber
74       * @throws BookException
75       */
76      public StrongsNumber(char language, short strongsNumber) throws BookException {
77          this(language, strongsNumber, null);
78      }
79  
80      /**
81       * Build an immutable Strong's Number. If the language is not 'G' or 'H' or
82       * the number is invalid, throw a BookException.
83       * 
84       * @param language
85       * @param strongsNumber
86       * @throws BookException
87       */
88      public StrongsNumber(char language, short strongsNumber, String part) throws BookException {
89          this.language = language;
90          this.strongsNumber = strongsNumber;
91          this.part = part;
92          validate();
93      }
94  
95      /**
96       * Return the canonical form of a Strong's Number, without the part.
97       * 
98       * @return the strongsNumber
99       */
100     public String getStrongsNumber() {
101         StringBuilder buf = new StringBuilder(5);
102         buf.append(language);
103         buf.append(ZERO_PAD.format(strongsNumber));
104         return buf.toString();
105     }
106 
107     /**
108      * Return the canonical form of a Strong's Number, with the part, if any
109      * 
110      * @return the strongsNumber
111      */
112     public String getFullStrongsNumber() {
113         StringBuilder buf = new StringBuilder(5);
114         buf.append(language);
115         buf.append(ZERO_PAD.format(strongsNumber));
116         if (part != null) {
117             buf.append(part);
118         }
119         return buf.toString();
120     }
121 
122     /**
123      * @return true if the Strong's number is for Greek
124      */
125     public boolean isGreek() {
126         return language == 'G';
127     }
128 
129     /**
130      * @return true if the Strong's number is for Hebrew
131      */
132     public boolean isHebrew() {
133         return language == 'H';
134     }
135 
136     /**
137      * @return true if this Strong's number is identified by a sub part
138      */
139     public boolean isPart() {
140         return part != null;
141     }
142 
143     /**
144      * Validates the number portion of this StrongsNumber.
145      * <ul>
146      * <li>Hebrew Strong's numbers are in the range of: 1-8674.</li>
147      * <li>Greek Strong's numbers in the range of: 1-5624
148      * (but not 1418, 2717, 3203-3302, 4452).</li>
149      * </ul>
150      * 
151      * @return true if the Strong's number is in range.
152      */
153     public boolean isValid() {
154         if (language == 'H'
155                 && strongsNumber >= 1
156                 && strongsNumber <= 8674)
157         {
158             return true;
159         }
160 
161         if (language == 'G'
162                 && ((strongsNumber >= 1 && strongsNumber < 1418)
163                         || (strongsNumber > 1418 && strongsNumber < 2717)
164                         || (strongsNumber > 2717 && strongsNumber < 3203)
165                         || (strongsNumber > 3302 && strongsNumber <= 5624)))
166         {
167             return true;
168         }
169 
170         return false;
171     }
172 
173     /*
174      * (non-Javadoc)
175      * 
176      * @see java.lang.Object#hashCode()
177      */
178     @Override
179     public int hashCode() {
180         int result = 31 + language;
181         return 31 * result + strongsNumber;
182     }
183 
184     /*
185      * (non-Javadoc)
186      * 
187      * @see java.lang.Object#equals(java.lang.Object)
188      */
189     @Override
190     public boolean equals(Object obj) {
191         if (this == obj) {
192             return true;
193         }
194 
195         if (obj == null || getClass() != obj.getClass()) {
196             return false;
197         }
198 
199         final StrongsNumber other = (StrongsNumber) obj;
200 
201         return language == other.language && strongsNumber == other.strongsNumber;
202     }
203 
204     /*
205      * (non-Javadoc)
206      * 
207      * @see java.lang.Object#toString()
208      */
209     @Override
210     public String toString() {
211         return getStrongsNumber();
212     }
213 
214     /**
215      * Do the actual parsing. Anything that does not match causes a
216      * BookException.
217      * 
218      * @param input
219      * @throws BookException
220      */
221     private void parse(String input) throws BookException {
222         String text = input;
223 
224         // Does it match
225         Matcher m = STRONGS_PATTERN.matcher(text);
226         if (!m.lookingAt()) {
227             // TRANSLATOR: User error condition: Indicates that what was given is not a Strong's Number. {0} is a placeholder for the bad Strong's Number.
228             throw new BookException(JSMsg.gettext("Not a valid Strong's Number \"{0}\"", input));
229         }
230 
231         String lang = m.group(1);
232         language = lang.charAt(0);
233         switch (language) {
234         case 'g':
235             language = 'G';
236             break;
237         case 'h':
238             language = 'H';
239             break;
240         default:
241             // pass through
242         }
243 
244         // Get the number after the G or H
245         strongsNumber = Short.parseShort(m.group(2));
246 
247         // FYI: OSIS refers to what follows a ! as a grain
248         part = m.group(3);
249     }
250 
251     private void validate() throws BookException {
252         if (language != 'G' && language != 'H') {
253             // TRANSLATOR: User error condition: Indicates that what was given is not a Strong's Number. {0} is a placeholder for the bad Strong's Number.
254             throw new BookException(JSMsg.gettext("Not a valid Strong's Number \"{0}\"", toString()));
255         }
256     }
257 
258     /**
259      * Whether it is Greek (G) or Hebrew (H).
260      */
261     private char language;
262 
263     /**
264      * The Strong's Number.
265      */
266     private short strongsNumber;
267 
268     /**
269      * The part if any.
270      */
271     private String part;
272 
273     /**
274      * The pattern of an acceptable Strong's number.
275      */
276     private static final Pattern STRONGS_PATTERN = Pattern.compile("([GgHh])0*([1-9][0-9]*)!?([A-Za-z]+)?");
277     private static final DecimalFormat ZERO_PAD = new DecimalFormat("0000");
278 }
279