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